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..5a9e8bc58511 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-canvas +/src/plugins/input_control_vis/ @elastic/kibana-canvas +/src/plugins/vis_type_markdown/ @elastic/kibana-canvas /x-pack/plugins/canvas/ @elastic/kibana-canvas +/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-canvas /x-pack/test/functional/apps/canvas/ @elastic/kibana-canvas +#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-canvas +#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-canvas #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-canvas +#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-canvas # 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..bd7d57c72ea5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report for Security Solution +about: Help us identify bugs in Elastic Security, SIEM, and Endpoint so we can fix them! +title: '[Security Solution]' +labels: Team: SecuritySolution +--- + +**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/api/saved-objects.asciidoc b/docs/api/saved-objects.asciidoc index a4e9fa32f8a5..0d8ceefb47e9 100644 --- a/docs/api/saved-objects.asciidoc +++ b/docs/api/saved-objects.asciidoc @@ -28,6 +28,8 @@ The following saved objects APIs are available: * <> to resolve errors from the import API +* <> to rotate the encryption key for encrypted saved objects + include::saved-objects/get.asciidoc[] include::saved-objects/bulk_get.asciidoc[] include::saved-objects/find.asciidoc[] @@ -38,3 +40,4 @@ include::saved-objects/delete.asciidoc[] include::saved-objects/export.asciidoc[] include::saved-objects/import.asciidoc[] include::saved-objects/resolve_import_errors.asciidoc[] +include::saved-objects/rotate_encryption_key.asciidoc[] diff --git a/docs/api/saved-objects/rotate_encryption_key.asciidoc b/docs/api/saved-objects/rotate_encryption_key.asciidoc new file mode 100644 index 000000000000..0a66ed2b4b36 --- /dev/null +++ b/docs/api/saved-objects/rotate_encryption_key.asciidoc @@ -0,0 +1,110 @@ +[role="xpack"] +[[saved-objects-api-rotate-encryption-key]] +=== Rotate encryption key API +++++ +Rotate encryption key +++++ + +experimental[] Rotate the encryption key for encrypted saved objects. + +If a saved object cannot be decrypted using the primary encryption key, then {kib} will attempt to decrypt it using the specified <>. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key. + +[IMPORTANT] +============================================================================ +Bulk key rotation can consume a considerable amount of resources and hence only user with a `superuser` role can trigger it. +============================================================================ + +[[saved-objects-api-rotate-encryption-key-request]] +==== Request + +`POST :/api/encrypted_saved_objects/_rotate_key` + +[[saved-objects-api-rotate-encryption-key-request-query-params]] +==== Query parameters + +`type`:: +(Optional, string) Limits encryption key rotation only to the saved objects with the specified type. By default, {kib} tries to rotate the encryption key for all saved object types that may contain encrypted attributes. + +`batchSize`:: +(Optional, number) Specifies a maximum number of saved objects that {kib} can process in a single batch. Bulk key rotation is an iterative process since {kib} may not be able to fetch and process all required saved objects in one go and splits processing into consequent batches. By default, the batch size is 10000, which is also a maximum allowed value. + +[[saved-objects-api-rotate-encryption-key-response-body]] +==== Response body + +`total`:: +(number) Indicates the total number of _all_ encrypted saved objects (optionally filtered by the requested `type`), regardless of the key {kib} used for encryption. + +`successful`:: +(number) Indicates the total number of _all_ encrypted saved objects (optionally filtered by the requested `type`), regardless of the key {kib} used for encryption. ++ +NOTE: In most cases, `total` will be greater than `successful` even if `failed` is zero. The reason is that {kib} may not need or may not be able to rotate encryption keys for all encrypted saved objects. + +`failed`:: +(number) Indicates the number of the saved objects that were still encrypted with one of the old encryption keys that {kib} failed to re-encrypt with the primary key. + +[[saved-objects-api-rotate-encryption-key-response-codes]] +==== Response code + +`200`:: +Indicates a successful call. + +`400`:: +Indicates that either query parameters are wrong or <> aren't configured. + +`429`:: +Indicates that key rotation is already in progress. + +[[saved-objects-api-rotate-encryption-key-example]] +==== Examples + +[[saved-objects-api-rotate-encryption-key-example-1]] +===== Encryption key rotation with default parameters + +[source,sh] +-------------------------------------------------- +$ curl -X POST /api/encrypted_saved_objects/_rotate_key +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "total": 1000, + "successful": 300, + "failed": 0 +} +-------------------------------------------------- + +The result indicates that the encryption key was successfully rotated for 300 out of 1000 saved objects with encrypted attributes, and 700 of the saved objects either didn't require key rotation, or were encrypted with an unknown encryption key. + +[[saved-objects-api-rotate-encryption-key-example-2]] +===== Encryption key rotation for the specific type with reduce batch size + +[IMPORTANT] +============================================================================ +Default parameters are optimized for speed. Change the parameters only when necessary. However, if you're experiencing any issues with this API, you may want to decrease a batch size or rotate the encryption keys for the specific types only. In this case, you may need to run key rotation multiple times in a row. +============================================================================ + +In this example, key rotation is performed for all saved objects with the `alert` type in batches of 5000. + +[source,sh] +-------------------------------------------------- +$ curl -X POST /api/encrypted_saved_objects/_rotate_key?type=alert&batchSize=5000 +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "total": 100, + "successful": 100, + "failed": 0 +} +-------------------------------------------------- + +The result indicates that the encryption key was successfully rotated for all 100 saved objects with the `alert` type. + diff --git a/docs/developer/best-practices/typescript.asciidoc b/docs/developer/best-practices/typescript.asciidoc index 3321aae3c099..583a98f296de 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. +Run `node scripts/find_plugins_without_ts_refs.js --id your_plugin_id` to get a list of plugins that should be switched to TS project refs to unblock your plugin migration. [discrete] ==== Implementation diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 21c51f8cabd3..8e08c3806446 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -286,10 +286,6 @@ which will load the visualization's editor. |To access an elasticsearch instance that has live data you have two options: -|{kib-repo}blob/{branch}/x-pack/plugins/audit_trail[auditTrail] -|WARNING: Missing README. - - |{kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement] |Notes: Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place @@ -435,8 +431,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] @@ -468,7 +465,8 @@ using the CURL scripts in the scripts folder. |{kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security] -|See Configuring security in Kibana. +|See Configuring security in +Kibana. |{kib-repo}blob/{branch}/x-pack/plugins/security_solution/README.md[securitySolution] @@ -498,8 +496,8 @@ routes, etc. |Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. -|{kib-repo}blob/{branch}/x-pack/plugins/transform[transform] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/transform/readme.md[transform] +|This plugin provides access to the transforms features provided by Elastic. |{kib-repo}blob/{branch}/x-pack/plugins/translations[translations] diff --git a/docs/development/core/server/kibana-plugin-core-server.auditableevent.md b/docs/development/core/server/kibana-plugin-core-server.auditableevent.md deleted file mode 100644 index aa109c506488..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditableevent.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) - -## AuditableEvent interface - -Event to audit. - -Signature: - -```typescript -export interface AuditableEvent -``` - -## Remarks - -Not a complete interface. - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [message](./kibana-plugin-core-server.auditableevent.message.md) | string | | -| [type](./kibana-plugin-core-server.auditableevent.type.md) | string | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.auditableevent.message.md b/docs/development/core/server/kibana-plugin-core-server.auditableevent.message.md deleted file mode 100644 index 3ac4167c6998..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditableevent.message.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) > [message](./kibana-plugin-core-server.auditableevent.message.md) - -## AuditableEvent.message property - -Signature: - -```typescript -message: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.auditableevent.type.md b/docs/development/core/server/kibana-plugin-core-server.auditableevent.type.md deleted file mode 100644 index 374874836668..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditableevent.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) > [type](./kibana-plugin-core-server.auditableevent.type.md) - -## AuditableEvent.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.auditor.add.md b/docs/development/core/server/kibana-plugin-core-server.auditor.add.md deleted file mode 100644 index 40245a93753f..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditor.add.md +++ /dev/null @@ -1,36 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Auditor](./kibana-plugin-core-server.auditor.md) > [add](./kibana-plugin-core-server.auditor.add.md) - -## Auditor.add() method - -Add a record to audit log. Service attaches to a log record: - metadata about an end-user initiating an operation - scope name, if presents - -Signature: - -```typescript -add(event: AuditableEvent): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| event | AuditableEvent | | - -Returns: - -`void` - -## Example - -How to add a record in audit log: - -```typescript -router.get({ path: '/my_endpoint', validate: false }, async (context, request, response) => { - context.core.auditor.withAuditScope('my_plugin_operation'); - const value = await context.core.elasticsearch.legacy.client.callAsCurrentUser('...'); - context.core.add({ type: 'operation.type', message: 'perform an operation in ... endpoint' }); - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.auditor.md b/docs/development/core/server/kibana-plugin-core-server.auditor.md deleted file mode 100644 index 191a34df647a..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditor.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Auditor](./kibana-plugin-core-server.auditor.md) - -## Auditor interface - -Provides methods to log user actions and access events. - -Signature: - -```typescript -export interface Auditor -``` - -## Methods - -| Method | Description | -| --- | --- | -| [add(event)](./kibana-plugin-core-server.auditor.add.md) | Add a record to audit log. Service attaches to a log record: - metadata about an end-user initiating an operation - scope name, if presents | -| [withAuditScope(name)](./kibana-plugin-core-server.auditor.withauditscope.md) | Add a high-level scope name for logged events. It helps to identify the root cause of low-level events. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.auditor.withauditscope.md b/docs/development/core/server/kibana-plugin-core-server.auditor.withauditscope.md deleted file mode 100644 index 0ae0c48ab92f..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditor.withauditscope.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Auditor](./kibana-plugin-core-server.auditor.md) > [withAuditScope](./kibana-plugin-core-server.auditor.withauditscope.md) - -## Auditor.withAuditScope() method - -Add a high-level scope name for logged events. It helps to identify the root cause of low-level events. - -Signature: - -```typescript -withAuditScope(name: string): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| name | string | | - -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-core-server.auditorfactory.asscoped.md b/docs/development/core/server/kibana-plugin-core-server.auditorfactory.asscoped.md deleted file mode 100644 index 4a60931e6094..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditorfactory.asscoped.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) > [asScoped](./kibana-plugin-core-server.auditorfactory.asscoped.md) - -## AuditorFactory.asScoped() method - -Signature: - -```typescript -asScoped(request: KibanaRequest): Auditor; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| request | KibanaRequest | | - -Returns: - -`Auditor` - diff --git a/docs/development/core/server/kibana-plugin-core-server.auditorfactory.md b/docs/development/core/server/kibana-plugin-core-server.auditorfactory.md deleted file mode 100644 index fd4760caa355..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditorfactory.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) - -## AuditorFactory interface - -Creates [Auditor](./kibana-plugin-core-server.auditor.md) instance bound to the current user credentials. - -Signature: - -```typescript -export interface AuditorFactory -``` - -## Methods - -| Method | Description | -| --- | --- | -| [asScoped(request)](./kibana-plugin-core-server.auditorfactory.asscoped.md) | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.md b/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.md deleted file mode 100644 index 50885232a088..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) - -## AuditTrailSetup interface - -Signature: - -```typescript -export interface AuditTrailSetup -``` - -## Methods - -| Method | Description | -| --- | --- | -| [register(auditor)](./kibana-plugin-core-server.audittrailsetup.register.md) | Register a custom [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) implementation. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.register.md b/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.register.md deleted file mode 100644 index 36695844ced7..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.register.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) > [register](./kibana-plugin-core-server.audittrailsetup.register.md) - -## AuditTrailSetup.register() method - -Register a custom [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) implementation. - -Signature: - -```typescript -register(auditor: AuditorFactory): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| auditor | AuditorFactory | | - -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-core-server.audittrailstart.md b/docs/development/core/server/kibana-plugin-core-server.audittrailstart.md deleted file mode 100644 index 4fb9f5cb9354..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.audittrailstart.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditTrailStart](./kibana-plugin-core-server.audittrailstart.md) - -## AuditTrailStart type - -Signature: - -```typescript -export declare type AuditTrailStart = AuditorFactory; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.audittrail.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.audittrail.md deleted file mode 100644 index 1aa7a75b7a08..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.audittrail.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [auditTrail](./kibana-plugin-core-server.coresetup.audittrail.md) - -## CoreSetup.auditTrail property - -[AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) - -Signature: - -```typescript -auditTrail: AuditTrailSetup; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index 75da8df2ae15..7a733cc34dac 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -16,7 +16,6 @@ export interface CoreSetupAuditTrailSetup | [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) | | [capabilities](./kibana-plugin-core-server.coresetup.capabilities.md) | CapabilitiesSetup | [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md) | | [context](./kibana-plugin-core-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) | | [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.audittrail.md b/docs/development/core/server/kibana-plugin-core-server.corestart.audittrail.md deleted file mode 100644 index 879e0df83619..000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.audittrail.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStart](./kibana-plugin-core-server.corestart.md) > [auditTrail](./kibana-plugin-core-server.corestart.audittrail.md) - -## CoreStart.auditTrail property - -[AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) - -Signature: - -```typescript -auditTrail: AuditTrailStart; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md index 0d5474fae5e1..f98088648689 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md @@ -16,7 +16,6 @@ export interface CoreStart | Property | Type | Description | | --- | --- | --- | -| [auditTrail](./kibana-plugin-core-server.corestart.audittrail.md) | AuditTrailStart | [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) | | [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | | [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md index 6a56d31bbd55..823f34bd7dd2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `LegacyClusterClient` class Signature: ```typescript -constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuditorFactory: () => AuditorFactory, getAuthHeaders?: GetAuthHeaders); +constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders); ``` ## Parameters @@ -18,6 +18,5 @@ constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuditorFact | --- | --- | --- | | config | LegacyElasticsearchClientConfig | | | log | Logger | | -| getAuditorFactory | () => AuditorFactory | | | getAuthHeaders | GetAuthHeaders | | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md index 668d0b2866a2..d24aeb44ca86 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md @@ -21,7 +21,7 @@ export declare class LegacyClusterClient implements ILegacyClusterClient | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(config, log, getAuditorFactory, getAuthHeaders)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) | | Constructs a new instance of the LegacyClusterClient class | +| [(constructor)(config, log, getAuthHeaders)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) | | Constructs a new instance of the LegacyClusterClient class | ## Properties diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md index ffadab765660..bd1cd1e9f3d9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `LegacyScopedClusterClient` class Signature: ```typescript -constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined, auditor?: Auditor | undefined); +constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined); ``` ## Parameters @@ -19,5 +19,4 @@ constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller | internalAPICaller | LegacyAPICaller | | | scopedAPICaller | LegacyAPICaller | | | headers | Headers | undefined | | -| auditor | Auditor | undefined | | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md index 7f752d70921b..6b6649e833a9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md @@ -21,7 +21,7 @@ export declare class LegacyScopedClusterClient implements ILegacyScopedClusterCl | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(internalAPICaller, scopedAPICaller, headers, auditor)](./kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md) | | Constructs a new instance of the LegacyScopedClusterClient class | +| [(constructor)(internalAPICaller, scopedAPICaller, headers)](./kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md) | | Constructs a new instance of the LegacyScopedClusterClient class | ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index a484c856ec01..29f522079491 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -53,10 +53,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppCategory](./kibana-plugin-core-server.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav | | [AssistanceAPIResponse](./kibana-plugin-core-server.assistanceapiresponse.md) | | | [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) | | -| [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) | Event to audit. | -| [Auditor](./kibana-plugin-core-server.auditor.md) | Provides methods to log user actions and access events. | -| [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) | Creates [Auditor](./kibana-plugin-core-server.auditor.md) instance bound to the current user credentials. | -| [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) | | | [Authenticated](./kibana-plugin-core-server.authenticated.md) | | | [AuthNotHandled](./kibana-plugin-core-server.authnothandled.md) | | | [AuthRedirected](./kibana-plugin-core-server.authredirected.md) | | @@ -132,7 +128,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | -| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request | +| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route | @@ -223,7 +219,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | Type Alias | Description | | --- | --- | | [AppenderConfigType](./kibana-plugin-core-server.appenderconfigtype.md) | | -| [AuditTrailStart](./kibana-plugin-core-server.audittrailstart.md) | | | [AuthenticationHandler](./kibana-plugin-core-server.authenticationhandler.md) | See [AuthToolkit](./kibana-plugin-core-server.authtoolkit.md). | | [AuthHeaders](./kibana-plugin-core-server.authheaders.md) | Auth Headers map | | [AuthResult](./kibana-plugin-core-server.authresult.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index 5b8492ec5ece..b195e9798916 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -21,6 +21,5 @@ core: { uiSettings: { client: IUiSettingsClient; }; - auditor: Auditor; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 4e530973f9d5..1de7313f2c40 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request +Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request Signature: @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
auditor: Auditor;
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
} | | 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/public/kibana-plugin-plugins-data-public.isearchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md index c9018b0048aa..76d091417344 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md @@ -15,5 +15,6 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [sessionId](./kibana-plugin-plugins-data-public.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-public.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.sessionid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.sessionid.md new file mode 100644 index 000000000000..b1d569e58bf1 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.sessionid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [sessionId](./kibana-plugin-plugins-data-public.isearchoptions.sessionid.md) + +## ISearchOptions.sessionId property + +A session ID, grouping multiple search requests into a single session. + +Signature: + +```typescript +sessionId?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md index b68c4d61e4e0..bbf856480aed 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md @@ -17,5 +17,6 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup | | +| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | session management | | [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md new file mode 100644 index 000000000000..7f39d9714a3a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) > [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) + +## ISearchSetup.session property + +session management + +Signature: + +```typescript +session: ISessionService; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index 5defe4a64761..4a69e94dd6f5 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -19,5 +19,6 @@ export interface ISearchStart | [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | +| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | session management | | [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md new file mode 100644 index 000000000000..de25cccd6d27 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) + +## ISearchStart.session property + +session management + +Signature: + +```typescript +session: ISessionService; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md index 02db74b1a9e9..1c8b6eb41a72 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md @@ -7,7 +7,7 @@ Signature: ```typescript -protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error; +protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; ``` ## Parameters @@ -17,7 +17,7 @@ protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal | e | any | | | request | IKibanaSearchRequest | | | timeoutSignal | AbortSignal | | -| appAbortSignal | AbortSignal | | +| options | ISearchOptions | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index a02a6116d7ae..40c7055e4c05 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -27,7 +27,7 @@ export declare class SearchInterceptor | Method | Modifiers | Description | | --- | --- | --- | | [getTimeoutMode()](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) | | | -| [handleSearchError(e, request, timeoutSignal, appAbortSignal)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | | +| [handleSearchError(e, request, timeoutSignal, options)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | | | [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates pendingCount$ when the request is started/finalized. | | [showError(e)](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md index 63eb67ce4824..3653394d28b9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md @@ -15,6 +15,7 @@ export interface SearchInterceptorDeps | Property | Type | Description | | --- | --- | --- | | [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | CoreSetup['http'] | | +| [session](./kibana-plugin-plugins-data-public.searchinterceptordeps.session.md) | ISessionService | | | [startServices](./kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md) | Promise<[CoreStart, any, unknown]> | | | [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | ToastsSetup | | | [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | CoreSetup['uiSettings'] | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.session.md new file mode 100644 index 000000000000..40d00483317b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.session.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) > [session](./kibana-plugin-plugins-data-public.searchinterceptordeps.session.md) + +## SearchInterceptorDeps.session property + +Signature: + +```typescript +session: ISessionService; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md index 21ddaef3a0b9..af96e1413ba0 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md @@ -15,5 +15,6 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [sessionId](./kibana-plugin-plugins-data-server.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-server.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.sessionid.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.sessionid.md new file mode 100644 index 000000000000..03043de5193d --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.sessionid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [sessionId](./kibana-plugin-plugins-data-server.isearchoptions.sessionid.md) + +## ISearchOptions.sessionId property + +A session ID, grouping multiple search requests into a single session. + +Signature: + +```typescript +sessionId?: string; +``` diff --git a/docs/development/plugins/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/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md index 6d2774d86f10..f11003887a6d 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md @@ -8,7 +8,7 @@ ```typescript data: { - table: KibanaDatatable; + table: Datatable; column: number; range: number[]; timeFieldName?: string; diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.md index 0f92ed86301d..f23cb44a7f01 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.md @@ -14,6 +14,6 @@ export interface RangeSelectContext | Property | Type | Description | | --- | --- | --- | -| [data](./kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md) | {
table: KibanaDatatable;
column: number;
range: number[];
timeFieldName?: string;
} | | +| [data](./kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md) | {
table: Datatable;
column: number;
range: number[];
timeFieldName?: string;
} | | | [embeddable](./kibana-plugin-plugins-embeddable-public.rangeselectcontext.embeddable.md) | T | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md index 92c33affc47a..e7c1be172cd7 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md @@ -9,7 +9,7 @@ ```typescript data: { data: Array<{ - table: Pick; + table: Pick; column: number; row: number; value: any; diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.md index 13133095956c..875c8d276160 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.md @@ -14,6 +14,6 @@ export interface ValueClickContext | Property | Type | Description | | --- | --- | --- | -| [data](./kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md) | {
data: Array<{
table: Pick<KibanaDatatable, 'rows' | 'columns'>;
column: number;
row: number;
value: any;
}>;
timeFieldName?: string;
negate?: boolean;
} | | +| [data](./kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md) | {
data: Array<{
table: Pick<Datatable, 'rows' | 'columns'>;
column: number;
row: number;
value: any;
}>;
timeFieldName?: string;
negate?: boolean;
} | | | [embeddable](./kibana-plugin-plugins-embeddable-public.valueclickcontext.embeddable.md) | T | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.datatablecolumntype.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.datatablecolumntype.md index a06ab351e62c..8f134bd3bfe9 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.datatablecolumntype.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.datatablecolumntype.md @@ -4,10 +4,10 @@ ## DatatableColumnType type -This type represents the `type` of any `DatatableColumn` in a `Datatable`. +This type represents the `type` of any `DatatableColumn` in a `Datatable`. its duplicated from KBN\_FIELD\_TYPES Signature: ```typescript -export declare type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; +export declare type DatatableColumnType = '_source' | 'attachment' | 'boolean' | 'date' | 'geo_point' | 'geo_shape' | 'ip' | 'murmur3' | 'number' | 'string' | 'unknown' | 'conflict' | 'object' | 'nested' | 'histogram' | 'null'; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md index 2f96ad6e040b..013624f30b45 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md @@ -41,6 +41,6 @@ export declare class Executor = RecordSignature: ```typescript -run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; +run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; ``` ## Parameters @@ -19,6 +19,7 @@ run = Recordstring | ExpressionAstExpression | | | input | Input | | | context | ExtraContext | | +| options | ExpressionExecOptions | | Returns: 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.expressionsservicestart.execute.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md index b8211a6bff27..18b856b946da 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md @@ -9,5 +9,5 @@ Starts expression execution and immediately returns `ExecutionContract` instance Signature: ```typescript -execute: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => ExecutionContract; +execute: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => ExecutionContract; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md index 34bf16c12132..def572abead2 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md @@ -16,12 +16,12 @@ export interface ExpressionsServiceStart | Property | Type | Description | | --- | --- | --- | -| [execute](./kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md) | <Input = unknown, Output = unknown, ExtraContext extends Record<string, unknown> = Record<string, unknown>>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => ExecutionContract<ExtraContext, Input, Output> | Starts expression execution and immediately returns ExecutionContract instance that tracks the progress of the execution and can be used to interact with the execution. | +| [execute](./kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md) | <Input = unknown, Output = unknown, ExtraContext extends Record<string, unknown> = Record<string, unknown>>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => ExecutionContract<ExtraContext, Input, Output> | Starts expression execution and immediately returns ExecutionContract instance that tracks the progress of the execution and can be used to interact with the execution. | | [fork](./kibana-plugin-plugins-expressions-public.expressionsservicestart.fork.md) | () => ExpressionsService | Create a new instance of ExpressionsService. The new instance inherits all state of the original ExpressionsService, including all expression types, expression functions and context. Also, all new types and functions registered in the original services AFTER the forking event will be available in the forked instance. However, all new types and functions registered in the forked instances will NOT be available to the original service. | | [getFunction](./kibana-plugin-plugins-expressions-public.expressionsservicestart.getfunction.md) | (name: string) => ReturnType<Executor['getFunction']> | Get a registered ExpressionFunction by its name, which was registered using the registerFunction method. The returned ExpressionFunction instance is an internal representation of the function in Expressions service - do not mutate that object. | | [getRenderer](./kibana-plugin-plugins-expressions-public.expressionsservicestart.getrenderer.md) | (name: string) => ReturnType<ExpressionRendererRegistry['get']> | Get a registered ExpressionRenderer by its name, which was registered using the registerRenderer method. The returned ExpressionRenderer instance is an internal representation of the renderer in Expressions service - do not mutate that object. | | [getType](./kibana-plugin-plugins-expressions-public.expressionsservicestart.gettype.md) | (name: string) => ReturnType<Executor['getType']> | Get a registered ExpressionType by its name, which was registered using the registerType method. The returned ExpressionType instance is an internal representation of the type in Expressions service - do not mutate that object. | -| [run](./kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md) | <Input, Output, ExtraContext extends Record<string, unknown> = Record<string, unknown>>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => Promise<Output> | Executes expression string or a parsed expression AST and immediately returns the result.Below example will execute sleep 100 | clog expression with 123 initial input to the first function. +| [run](./kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md) | <Input, Output, ExtraContext extends Record<string, unknown> = Record<string, unknown>>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => Promise<Output> | Executes expression string or a parsed expression AST and immediately returns the result.Below example will execute sleep 100 | clog expression with 123 initial input to the first function. ```ts expressions.run('sleep 100 | clog', 123); diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md index 578c583624ad..d717af51a00f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md @@ -24,5 +24,5 @@ expressions.run('...', null, { elasticsearchClient }); Signature: ```typescript -run: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => Promise; +run: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => Promise; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md new file mode 100644 index 000000000000..b27246449cc7 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) > [debug](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md) + +## IExpressionLoaderParams.debug property + +Signature: + +```typescript +debug?: boolean; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index b8a174f93fb9..d6e02350bae3 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -17,6 +17,7 @@ export interface IExpressionLoaderParams | [context](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.context.md) | ExpressionValue | | | [customFunctions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.customfunctions.md) | [] | | | [customRenderers](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.customrenderers.md) | [] | | +| [debug](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md) | boolean | | | [disableCaching](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.disablecaching.md) | boolean | | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | Adapters | | | [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.columns.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.columns.md deleted file mode 100644 index c8aa768a883d..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.columns.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-public.kibanadatatable.md) > [columns](./kibana-plugin-plugins-expressions-public.kibanadatatable.columns.md) - -## KibanaDatatable.columns property - -Signature: - -```typescript -columns: KibanaDatatableColumn[]; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.md deleted file mode 100644 index 4ea1d6f42b66..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-public.kibanadatatable.md) - -## KibanaDatatable interface - -Signature: - -```typescript -export interface KibanaDatatable -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [columns](./kibana-plugin-plugins-expressions-public.kibanadatatable.columns.md) | KibanaDatatableColumn[] | | -| [rows](./kibana-plugin-plugins-expressions-public.kibanadatatable.rows.md) | KibanaDatatableRow[] | | -| [type](./kibana-plugin-plugins-expressions-public.kibanadatatable.type.md) | typeof name | | - diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.rows.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.rows.md deleted file mode 100644 index 43f3243dc4fa..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.rows.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-public.kibanadatatable.md) > [rows](./kibana-plugin-plugins-expressions-public.kibanadatatable.rows.md) - -## KibanaDatatable.rows property - -Signature: - -```typescript -rows: KibanaDatatableRow[]; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.type.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.type.md deleted file mode 100644 index 996f59cbb77a..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-public.kibanadatatable.md) > [type](./kibana-plugin-plugins-expressions-public.kibanadatatable.type.md) - -## KibanaDatatable.type property - -Signature: - -```typescript -type: typeof name; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.formathint.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.formathint.md deleted file mode 100644 index b517c1610261..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.formathint.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) > [formatHint](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.formathint.md) - -## KibanaDatatableColumn.formatHint property - -Signature: - -```typescript -formatHint?: SerializedFieldFormat; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.id.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.id.md deleted file mode 100644 index e7d43190589a..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) > [id](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.id.md) - -## KibanaDatatableColumn.id property - -Signature: - -```typescript -id: string; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md deleted file mode 100644 index 138c19f0ec7b..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) - -## KibanaDatatableColumn interface - -Signature: - -```typescript -export interface KibanaDatatableColumn -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [formatHint](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.formathint.md) | SerializedFieldFormat | | -| [id](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.id.md) | string | | -| [meta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.meta.md) | KibanaDatatableColumnMeta | | -| [name](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.name.md) | string | | - diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.meta.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.meta.md deleted file mode 100644 index df2d09bf3cc5..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.meta.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) > [meta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.meta.md) - -## KibanaDatatableColumn.meta property - -Signature: - -```typescript -meta?: KibanaDatatableColumnMeta; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.name.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.name.md deleted file mode 100644 index 841ad67f3f52..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.name.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) > [name](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.name.md) - -## KibanaDatatableColumn.name property - -Signature: - -```typescript -name: string; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.aggconfigparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.aggconfigparams.md deleted file mode 100644 index 2ec6edda4cbc..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.aggconfigparams.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md) > [aggConfigParams](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.aggconfigparams.md) - -## KibanaDatatableColumnMeta.aggConfigParams property - -Signature: - -```typescript -aggConfigParams?: Record; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.indexpatternid.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.indexpatternid.md deleted file mode 100644 index 2287c28398f7..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.indexpatternid.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md) > [indexPatternId](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.indexpatternid.md) - -## KibanaDatatableColumnMeta.indexPatternId property - -Signature: - -```typescript -indexPatternId?: string; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md deleted file mode 100644 index b2f8c9d06a72..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md) - -## KibanaDatatableColumnMeta interface - -Signature: - -```typescript -export interface KibanaDatatableColumnMeta -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [aggConfigParams](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.aggconfigparams.md) | Record<string, any> | | -| [indexPatternId](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.indexpatternid.md) | string | | -| [type](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.type.md) | string | | - diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.type.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.type.md deleted file mode 100644 index 98d4a0c2d43c..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md) > [type](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.type.md) - -## KibanaDatatableColumnMeta.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablerow.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablerow.md deleted file mode 100644 index cb5f1ad70f62..000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablerow.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableRow](./kibana-plugin-plugins-expressions-public.kibanadatatablerow.md) - -## KibanaDatatableRow interface - -Signature: - -```typescript -export interface KibanaDatatableRow -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md index b0c732188a46..db09f966e2fa 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md @@ -72,10 +72,6 @@ | [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) | | | [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md) | | | [IRegistry](./kibana-plugin-plugins-expressions-public.iregistry.md) | | -| [KibanaDatatable](./kibana-plugin-plugins-expressions-public.kibanadatatable.md) | | -| [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) | | -| [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md) | | -| [KibanaDatatableRow](./kibana-plugin-plugins-expressions-public.kibanadatatablerow.md) | | | [PointSeriesColumn](./kibana-plugin-plugins-expressions-public.pointseriescolumn.md) | Column in a PointSeries | | [Range](./kibana-plugin-plugins-expressions-public.range.md) | | | [ReactExpressionRendererProps](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md) | | @@ -95,7 +91,7 @@ | [AnyExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-public.anyexpressionfunctiondefinition.md) | Type to capture every possible expression function definition. | | [AnyExpressionTypeDefinition](./kibana-plugin-plugins-expressions-public.anyexpressiontypedefinition.md) | | | [ArgumentType](./kibana-plugin-plugins-expressions-public.argumenttype.md) | This type represents all of the possible combinations of properties of an Argument in an Expression Function. The presence or absence of certain fields influence the shape and presence of others within each arg in the specification. | -| [DatatableColumnType](./kibana-plugin-plugins-expressions-public.datatablecolumntype.md) | This type represents the type of any DatatableColumn in a Datatable. | +| [DatatableColumnType](./kibana-plugin-plugins-expressions-public.datatablecolumntype.md) | This type represents the type of any DatatableColumn in a Datatable. its duplicated from KBN\_FIELD\_TYPES | | [DatatableRow](./kibana-plugin-plugins-expressions-public.datatablerow.md) | This type represents a row in a Datatable. | | [ExecutionContainer](./kibana-plugin-plugins-expressions-public.executioncontainer.md) | | | [ExecutorContainer](./kibana-plugin-plugins-expressions-public.executorcontainer.md) | | 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.datatablecolumntype.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.datatablecolumntype.md index 4afce913526d..dc98acffa123 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.datatablecolumntype.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.datatablecolumntype.md @@ -4,10 +4,10 @@ ## DatatableColumnType type -This type represents the `type` of any `DatatableColumn` in a `Datatable`. +This type represents the `type` of any `DatatableColumn` in a `Datatable`. its duplicated from KBN\_FIELD\_TYPES Signature: ```typescript -export declare type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; +export declare type DatatableColumnType = '_source' | 'attachment' | 'boolean' | 'date' | 'geo_point' | 'geo_shape' | 'ip' | 'murmur3' | 'number' | 'string' | 'unknown' | 'conflict' | 'object' | 'nested' | 'histogram' | 'null'; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md index ec4e0bdcc456..46ad60ae0712 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md @@ -41,6 +41,6 @@ export declare class Executor = RecordSignature: ```typescript -run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; +run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; ``` ## Parameters @@ -19,6 +19,7 @@ run = Recordstring | ExpressionAstExpression | | | input | Input | | | context | ExtraContext | | +| options | ExpressionExecOptions | | Returns: diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.columns.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.columns.md deleted file mode 100644 index 423e543e4307..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.columns.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-server.kibanadatatable.md) > [columns](./kibana-plugin-plugins-expressions-server.kibanadatatable.columns.md) - -## KibanaDatatable.columns property - -Signature: - -```typescript -columns: KibanaDatatableColumn[]; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.md deleted file mode 100644 index 30ee3ac2fcd1..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-server.kibanadatatable.md) - -## KibanaDatatable interface - -Signature: - -```typescript -export interface KibanaDatatable -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [columns](./kibana-plugin-plugins-expressions-server.kibanadatatable.columns.md) | KibanaDatatableColumn[] | | -| [rows](./kibana-plugin-plugins-expressions-server.kibanadatatable.rows.md) | KibanaDatatableRow[] | | -| [type](./kibana-plugin-plugins-expressions-server.kibanadatatable.type.md) | typeof name | | - diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.rows.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.rows.md deleted file mode 100644 index 42170a83fc3c..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.rows.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-server.kibanadatatable.md) > [rows](./kibana-plugin-plugins-expressions-server.kibanadatatable.rows.md) - -## KibanaDatatable.rows property - -Signature: - -```typescript -rows: KibanaDatatableRow[]; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.type.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.type.md deleted file mode 100644 index c36674540a1b..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-server.kibanadatatable.md) > [type](./kibana-plugin-plugins-expressions-server.kibanadatatable.type.md) - -## KibanaDatatable.type property - -Signature: - -```typescript -type: typeof name; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.formathint.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.formathint.md deleted file mode 100644 index a1e6949019dc..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.formathint.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) > [formatHint](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.formathint.md) - -## KibanaDatatableColumn.formatHint property - -Signature: - -```typescript -formatHint?: SerializedFieldFormat; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.id.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.id.md deleted file mode 100644 index 6f90da1ac9c9..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) > [id](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.id.md) - -## KibanaDatatableColumn.id property - -Signature: - -```typescript -id: string; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md deleted file mode 100644 index 171477911502..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) - -## KibanaDatatableColumn interface - -Signature: - -```typescript -export interface KibanaDatatableColumn -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [formatHint](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.formathint.md) | SerializedFieldFormat | | -| [id](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.id.md) | string | | -| [meta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.meta.md) | KibanaDatatableColumnMeta | | -| [name](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.name.md) | string | | - diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.meta.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.meta.md deleted file mode 100644 index 40b20d51e6ec..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.meta.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) > [meta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.meta.md) - -## KibanaDatatableColumn.meta property - -Signature: - -```typescript -meta?: KibanaDatatableColumnMeta; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.name.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.name.md deleted file mode 100644 index 3a85e2325483..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.name.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) > [name](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.name.md) - -## KibanaDatatableColumn.name property - -Signature: - -```typescript -name: string; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.aggconfigparams.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.aggconfigparams.md deleted file mode 100644 index 539b24174f72..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.aggconfigparams.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md) > [aggConfigParams](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.aggconfigparams.md) - -## KibanaDatatableColumnMeta.aggConfigParams property - -Signature: - -```typescript -aggConfigParams?: Record; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.indexpatternid.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.indexpatternid.md deleted file mode 100644 index 2704915a1507..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.indexpatternid.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md) > [indexPatternId](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.indexpatternid.md) - -## KibanaDatatableColumnMeta.indexPatternId property - -Signature: - -```typescript -indexPatternId?: string; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md deleted file mode 100644 index d9a96e665f01..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md) - -## KibanaDatatableColumnMeta interface - -Signature: - -```typescript -export interface KibanaDatatableColumnMeta -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [aggConfigParams](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.aggconfigparams.md) | Record<string, any> | | -| [indexPatternId](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.indexpatternid.md) | string | | -| [type](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.type.md) | string | | - diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.type.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.type.md deleted file mode 100644 index 56e3757ef621..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md) > [type](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.type.md) - -## KibanaDatatableColumnMeta.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablerow.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablerow.md deleted file mode 100644 index dd0f3f4cb2f6..000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablerow.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableRow](./kibana-plugin-plugins-expressions-server.kibanadatatablerow.md) - -## KibanaDatatableRow interface - -Signature: - -```typescript -export interface KibanaDatatableRow -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md index dd7c7af466bd..9e2189dad273 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md @@ -63,10 +63,6 @@ | [Font](./kibana-plugin-plugins-expressions-server.font.md) | An interface representing a font in Canvas, with a textual label and the CSS font-value. | | [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md) | | | [IRegistry](./kibana-plugin-plugins-expressions-server.iregistry.md) | | -| [KibanaDatatable](./kibana-plugin-plugins-expressions-server.kibanadatatable.md) | | -| [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) | | -| [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md) | | -| [KibanaDatatableRow](./kibana-plugin-plugins-expressions-server.kibanadatatablerow.md) | | | [PointSeriesColumn](./kibana-plugin-plugins-expressions-server.pointseriescolumn.md) | Column in a PointSeries | | [Range](./kibana-plugin-plugins-expressions-server.range.md) | | | [SerializedDatatable](./kibana-plugin-plugins-expressions-server.serializeddatatable.md) | | @@ -79,7 +75,7 @@ | [AnyExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-server.anyexpressionfunctiondefinition.md) | Type to capture every possible expression function definition. | | [AnyExpressionTypeDefinition](./kibana-plugin-plugins-expressions-server.anyexpressiontypedefinition.md) | | | [ArgumentType](./kibana-plugin-plugins-expressions-server.argumenttype.md) | This type represents all of the possible combinations of properties of an Argument in an Expression Function. The presence or absence of certain fields influence the shape and presence of others within each arg in the specification. | -| [DatatableColumnType](./kibana-plugin-plugins-expressions-server.datatablecolumntype.md) | This type represents the type of any DatatableColumn in a Datatable. | +| [DatatableColumnType](./kibana-plugin-plugins-expressions-server.datatablecolumntype.md) | This type represents the type of any DatatableColumn in a Datatable. its duplicated from KBN\_FIELD\_TYPES | | [DatatableRow](./kibana-plugin-plugins-expressions-server.datatablerow.md) | This type represents a row in a Datatable. | | [ExecutionContainer](./kibana-plugin-plugins-expressions-server.executioncontainer.md) | | | [ExecutorContainer](./kibana-plugin-plugins-expressions-server.executorcontainer.md) | | 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/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc index 8fc2b7381de8..7b24de42d8e1 100644 --- a/docs/plugins/known-plugins.asciidoc +++ b/docs/plugins/known-plugins.asciidoc @@ -14,7 +14,6 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea * https://github.com/sivasamyk/logtrail[LogTrail] - View, analyze, search and tail log events in realtime with a developer/sysadmin friendly interface * https://github.com/wtakase/kibana-own-home[Own Home] (wtakase) - enables multi-tenancy * https://github.com/asileon/kibana_shard_allocation[Shard Allocation] (asileon) - visualize elasticsearch shard allocation -* https://github.com/samtecspg/conveyor[Conveyor] - Simple (GUI) interface for importing data into Elasticsearch. * https://github.com/wazuh/wazuh-kibana-app[Wazuh] - Wazuh provides host-based security visibility using lightweight multi-platform agents. * https://github.com/TrumanDu/indices_view[Indices View] - View indices related information. * https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index adfc3964d420..d44c42db92f4 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -10,6 +10,7 @@ You can configure `xpack.reporting` settings in your `kibana.yml` to: * <> * <> * <> +* <> [float] [[general-reporting-settings]] @@ -65,7 +66,7 @@ proxy host requires that the {kib} server has network access to the proxy. [NOTE] ============ -Reporting authenticates requests on the Kibana page only when the hostname matches the +Reporting authenticates requests on the {kib} page only when the hostname matches the <> setting. Therefore Reporting would fail if the set value redirects to another server. For that reason, `"0"` is an invalid setting because, in the Reporting browser, it becomes an automatic redirect to `"0.0.0.0"`. @@ -214,6 +215,23 @@ a| `xpack.reporting.capture.browser` | The maximum {ref}/common-options.html#byte-units[byte size] of a CSV file before being truncated. This setting exists to prevent large exports from causing performance and storage issues. Can be specified as number of bytes. Defaults to `10mb`. +|=== + +[NOTE] +============ +Setting `xpack.reporting.csv.maxSizeBytes` much larger than the default 10 MB limit has the potential to negatively affect the +performance of {kib} and your {es} cluster. There is no enforced maximum for this setting, but a reasonable maximum value depends +on multiple factors: + +* The `http.max_content_length` setting in {es}. +* Network proxies, which are often configured by default to block large requests with a 413 error. +* The amount of memory available to the {kib} server, which limits the size of CSV data that must be held temporarily. + +For information about {kib} memory limits, see <>. +============ + +[cols="2*<"] +|=== | `xpack.reporting.csv.scroll.size` | Number of documents retrieved from {es} for each scroll iteration during a CSV @@ -248,6 +266,11 @@ a| `xpack.reporting.capture.browser` exist. Configure this to a unique value, beginning with `.reporting-`, for every {kib} instance that has a unique <> setting. Defaults to `.reporting`. +| `xpack.reporting.capture.networkPolicy` + | Capturing a screenshot from a {kib} page involves sending out requests for all the linked web assets. For example, a Markdown + visualization can show an image from a remote server. You can configure what type of requests to allow or filter by setting a + <> for Reporting. + | `xpack.reporting.roles.allow` | Specifies the roles in addition to superusers that can use reporting. Defaults to `[ "reporting_user" ]`. + diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 00e5f973f7d8..6b01094f7248 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -155,7 +155,7 @@ There is a very limited set of cases when you'd want to change these settings. F | `xpack.security.authc.http.autoSchemesEnabled` | Determines if HTTP authentication schemes used by the enabled authentication providers should be automatically supported during HTTP authentication. By default, this setting is set to `true`. -| `xpack.security.authc.http.schemes` +| `xpack.security.authc.http.schemes[]` | List of HTTP authentication schemes that {kib} HTTP authentication should support. By default, this setting is set to `['apikey']` to support HTTP authentication with <> scheme. |=== @@ -239,3 +239,133 @@ The format is a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', ============ |=== + +[[security-encrypted-saved-objects-settings]] +==== Encrypted saved objects settings + +These settings control the encryption of saved objects with sensitive data. For more details, refer to <>. + +[IMPORTANT] +============ +In high-availability deployments, make sure you use the same encryption and decryption keys for all instances of {kib}. Although the keys can be specified in clear text in `kibana.yml`, it's recommended to store them securely in the <>. +============ + +[cols="2*<"] +|=== +| [[xpack-encryptedSavedObjects-encryptionKey]] `xpack.encryptedSavedObjects.` +`encryptionKey` +| An arbitrary string of at least 32 characters that is used to encrypt sensitive properties of saved objects before they're stored in {es}. If not set, {kib} will generate a random key on startup, but certain features won't be available until you set the encryption key explicitly. + +| [[xpack-encryptedSavedObjects-keyRotation-decryptionOnlyKeys]] `xpack.encryptedSavedObjects.` +`keyRotation.decryptionOnlyKeys` +| An optional list of previously used encryption keys. Like <>, these must be at least 32 characters in length. {kib} doesn't use these keys for encryption, but may still require them to decrypt some existing saved objects. Use this setting if you wish to change your encryption key, but don't want to lose access to saved objects that were previously encrypted with a different key. + +|=== + +[float] +[[audit-logging-settings]] +===== Audit logging settings + +You can enable audit logging to support compliance, accountability, and security. When enabled, {kib} will capture: + +- Who performed an action +- What action was performed +- When the action occurred + +For more details and a reference of audit events, refer to <>. + +[cols="2*<"] +|=== +| `xpack.security.audit.enabled` +| Set to `true` to enable audit logging for security events. *Default:* `false` +|=== + +[float] +[[ecs-audit-logging-settings]] +===== ECS audit logging settings + +To enable the <>, specify where you want to write the audit events using `xpack.security.audit.appender`. + +[cols="2*<"] +|=== +| `xpack.security.audit.appender` +| Optional. Specifies where audit logs should be written to and how they should be formatted. + +2+a| For example: + +[source,yaml] +---------------------------------------- +xpack.security.audit.appender: + kind: file + path: /path/to/audit.log + layout: + kind: json +---------------------------------------- + +| `xpack.security.audit.appender.kind` +| Required. Specifies where audit logs should be written to. Allowed values are `console` or `file`. +|=== + +[float] +[[audit-logging-file-appender]] +===== File appender + +The file appender can be configured using the following settings: + +[cols="2*<"] +|=== +| `xpack.security.audit.appender.path` +| Required. Full file path the log file should be written to. + +| `xpack.security.audit.appender.layout.kind` +| Required. Specifies how audit logs should be formatted. Allowed values are `json` or `pattern`. +|=== + +[float] +[[audit-logging-pattern-layout]] +===== Pattern layout + +The pattern layout can be configured using the following settings: + +[cols="2*<"] +|=== +| `xpack.security.audit.appender.layout.highlight` +| Optional. Set to `true` to enable highlighting log messages with colors. + +| `xpack.security.audit.appender.layout.pattern` +| Optional. Specifies how the log line should be formatted. *Default:* `[%date][%level][%logger]%meta %message` +|=== + +[float] +[[audit-logging-ignore-filters]] +===== Ignore filters + +[cols="2*<"] +|=== +| `xpack.security.audit.ignore_filters[]` +| List of filters that determine which events should be excluded from the audit log. An event will get filtered out if at least one of the provided filters matches. + +2+a| For example: + +[source,yaml] +---------------------------------------- +xpack.security.audit.ignore_filters: +- actions: [http_request] <1> +- categories: [database] + types: [creation, change, deletion] <2> +---------------------------------------- +<1> Filters out HTTP request events +<2> Filters out any data write events + +| `xpack.security.audit.ignore_filters[].actions[]` +| List of values matched against the `event.action` field of an audit event. Refer to <> for a list of available events. + +| `xpack.security.audit.ignore_filters[].categories[]` +| List of values matched against the `event.category` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-category.html[ECS categorization field] for allowed values. + +| `xpack.security.audit.ignore_filters[].types[]` +| List of values matched against the `event.type` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-type.html[ECS type field] for allowed values. + +| `xpack.security.audit.ignore_filters[].outcomes[]` +| List of values matched against the `event.outcome` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-outcome.html[ECS outcome field] for allowed values. +|=== 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/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index b71dfb016c76..cdb17e9daa5e 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -135,6 +135,92 @@ Example: `{{ date event.from “YYYY MM DD”}}` + `{{date “now-15”}}` + +|formatNumber +a|Format numbers. Numbers can be formatted to look like currency, percentages, times or numbers with decimal places, thousands, and abbreviations. +Refer to the http://numeraljs.com/#format[numeral.js] for different formatting options. + +Example: + +`{{formatNumber event.value "0.0"}}` + +|lowercase +a|Converts a string to lower case. + +Example: + +`{{lowercase event.value}}` + +|uppercase +a|Converts a string to upper case. + +Example: + +`{{uppercase event.value}}` + +|trim +a|Removes leading and trailing spaces from a string. + +Example: + +`{{trim event.value}}` + +|trimLeft +a|Removes leading spaces from a string. + +Example: + +`{{trimLeft event.value}}` + +|trimRight +a|Removes trailing spaces from a string. + +Example: + +`{{trimRight event.value}}` + +|mid +a|Extracts a substring from a string by start position and number of characters to extract. + +Example: + +`{{mid event.value 3 5}}` - extracts five characters starting from a third character. + +|left +a|Extracts a number of characters from a string (starting from left). + +Example: + +`{{left event.value 3}}` + +|right +a|Extracts a number of characters from a string (starting from right). + +Example: + +`{{right event.value 3}}` + +|concat +a|Concatenates two or more strings. + +Example: + +`{{concat event.value "," event.key}}` + +|replace +a|Replaces all substrings within a string. + +Example: + +`{{replace event.value "stringToReplace" "stringToReplaceWith"}}` + +|split +a|Splits a string using a provided splitter. + +Example: + +`{{split event.value ","}}` + |=== diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 378f7a53a665..d6593143e4f6 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -296,7 +296,8 @@ a configuration option for changing the tooltip position and padding: kibana: { tooltips: { position: 'top', - padding: 15 + padding: 15, + textTruncate: true, } } } 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/docs/user/reporting/configuring-reporting.asciidoc b/docs/user/reporting/configuring-reporting.asciidoc index 6a0c44cf4c2a..a8b76f36b9a8 100644 --- a/docs/user/reporting/configuring-reporting.asciidoc +++ b/docs/user/reporting/configuring-reporting.asciidoc @@ -75,3 +75,4 @@ to point to a proxy host requires that the Kibana server has network access to the proxy. include::{kib-repo-dir}/user/security/reporting.asciidoc[] +include::network-policy.asciidoc[] diff --git a/docs/user/reporting/network-policy.asciidoc b/docs/user/reporting/network-policy.asciidoc new file mode 100644 index 000000000000..782473a3b0f1 --- /dev/null +++ b/docs/user/reporting/network-policy.asciidoc @@ -0,0 +1,71 @@ +[role="xpack"] +[[reporting-network-policy]] +=== Restrict requests with a Reporting network policy + +When Reporting generates PDF reports, it uses the Chromium browser to fully load the {kib} page on the server. This +potentially involves sending requests to external hosts. For example, a request might go to an external image server to show a +field formatted as an image, or to show an image in a Markdown visualization. + +If the Chromium browser is asked to send a request that violates the network policy, Reporting stops processing the page +before the request goes out, and the report is marked as a failure. Additional information about the event is in +the Kibana server logs. + +[NOTE] +============ +{kib} installations are not designed to be publicly accessible over the Internet. The Reporting network policy and other capabilities +of the Elastic Stack security features do not change this condition. +============ + +==== Configure a Reporting network policy + +You configure the network policy by specifying the `xpack.reporting.capture.networkPolicy.rules` setting in `kibana.yml`. A policy is specified as +an array of objects that describe what to allow or deny based on a host or protocol. If a host or protocol +is not specified, the rule matches any host or protocol. + +The rule objects are evaluated sequentially from the beginning to the end of the array, and continue until there is a matching rule. +If no rules allow a request, the request is denied. + +[source,yaml] +------------------------------------------------------- +# Only allow requests to placeholder.com +xpack.reporting.capture.networkPolicy: + rules: [ { allow: true, host: "placeholder.com" } ] +------------------------------------------------------- + +[source,yaml] +------------------------------------------------------- +# Only allow requests to https://placeholder.com +xpack.reporting.capture.networkPolicy: + rules: [ { allow: true, host: "placeholder.com", protocol: "https:" } ] +------------------------------------------------------- + +A final `allow` rule with no host or protocol will allow all requests that are not explicitly denied. + +[source,yaml] +------------------------------------------------------- +# Denies requests from http://placeholder.com, but anything else is allowed. +xpack.reporting.capture.networkPolicy: + rules: [{ allow: false, host: "placeholder.com", protocol: "http:" }, { allow: true }]; +------------------------------------------------------- + +A network policy can be composed of multiple rules. + +[source,yaml] +------------------------------------------------------- +# Allow any request to http://placeholder.com but for any other host, https is required +xpack.reporting.capture.networkPolicy + rules: [ + { allow: true, host: "placeholder.com", protocol: "http:" }, + { allow: true, protocol: "https:" }, + ] +------------------------------------------------------- + +[NOTE] +============ +The `file:` protocol is always denied, even if no network policy is configured. +============ + +==== Disable a Reporting network policy + +You can use the `xpack.reporting.capture.networkPolicy.enabled: false` setting to disable the network policy feature. The default for +this configuration property is `true`, so it is not necessary to explicitly enable it. diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index a7359af38c1c..d4370c4d840c 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -3,30 +3,30 @@ === Audit logs You can enable auditing to keep track of security-related events such as -authorization success and failures. Logging these events enables you -to monitor {kib} for suspicious activity and provides evidence in the -event of an attack. +authorization success and failures. Logging these events enables you to monitor +{kib} for suspicious activity and provides evidence in the event of an attack. -Use the {kib} audit logs in conjunction with {es}'s -audit logging to get a holistic view of all security related events. -{kib} defers to {es}'s security model for authentication, data -index authorization, and features that are driven by cluster-wide privileges. -For more information on enabling audit logging in {es}, see -{ref}/auditing.html[Auditing security events]. +Use the {kib} audit logs in conjunction with {ref}/enable-audit-logging.html[{es} audit logging] to get a +holistic view of all security related events. {kib} defers to the {es} security +model for authentication, data index authorization, and features that are driven +by cluster-wide privileges. For more information on enabling audit logging in +{es}, refer to {ref}/auditing.html[Auditing security events]. [IMPORTANT] ============================================================================ -Audit logs are **disabled** by default. To enable this functionality, you -must set `xpack.security.audit.enabled` to `true` in `kibana.yml`. +Audit logs are **disabled** by default. To enable this functionality, you must +set `xpack.security.audit.enabled` to `true` in `kibana.yml`. ============================================================================ -Audit logging uses the standard {kib} logging output, which can be configured -in the `kibana.yml` and is discussed in <>. +The current version of the audit logger uses the standard {kib} logging output, +which can be configured in `kibana.yml`. For more information, refer to <>. +The audit logger uses a separate logger and can be configured using +the options in <>. ==== Audit event types -When you are auditing security events, each request can generate -multiple audit events. The following is a list of the events that can be generated: +When you are auditing security events, each request can generate multiple audit +events. The following is a list of the events that can be generated: |====== | `saved_objects_authorization_success` | Logged when a user is authorized to access a saved @@ -34,3 +34,110 @@ multiple audit events. The following is a list of the events that can be generat | `saved_objects_authorization_failure` | Logged when a user isn't authorized to access a saved objects when using a role with <> |====== + +[[xpack-security-ecs-audit-logging]] +==== ECS audit events + +[IMPORTANT] +============================================================================ +The following events are only logged if the ECS audit logger is enabled. +For information on how to configure `xpack.security.audit.appender`, refer to +<>. +============================================================================ + +Refer to the table of events that can be logged for auditing purposes. + +Each event is broken down into `category`, `type`, `action` and `outcome` fields +to make it easy to filter, query and aggregate the resulting logs. + +[NOTE] +============================================================================ +To ensure that a record of every operation is persisted even in case of an +unexpected error, asynchronous write operations are logged immediately after all +authorization checks have passed, but before the response from {es} is received. +Refer to the corresponding {es} logs for potential write errors. +============================================================================ + + +[cols="3*<"] +|====== +3+a| +===== Category: authentication + +| *Action* +| *Outcome* +| *Description* + +.2+| `user_login` +| `success` | User has logged in successfully. +| `failure` | Failed login attempt (e.g. due to invalid credentials). + +3+a| +===== Category: database +====== Type: creation + +| *Action* +| *Outcome* +| *Description* + +.2+| `saved_object_create` +| `unknown` | User is creating a saved object. +| `failure` | User is not authorized to create a saved object. + + +3+a| +====== Type: change + +| *Action* +| *Outcome* +| *Description* + +.2+| `saved_object_update` +| `unknown` | User is updating a saved object. +| `failure` | User is not authorized to update a saved object. + +.2+| `saved_object_add_to_spaces` +| `unknown` | User is adding a saved object to other spaces. +| `failure` | User is not authorized to add a saved object to other spaces. + +.2+| `saved_object_delete_from_spaces` +| `unknown` | User is removing a saved object from other spaces. +| `failure` | User is not authorized to remove a saved object from other spaces. + +3+a| +====== Type: deletion + +| *Action* +| *Outcome* +| *Description* + +.2+| `saved_object_delete` +| `unknown` | User is deleting a saved object. +| `failure` | User is not authorized to delete a saved object. + +3+a| +====== Type: access + +| *Action* +| *Outcome* +| *Description* + +.2+| `saved_object_get` +| `success` | User has accessed a saved object. +| `failure` | User is not authorized to access a saved object. + +.2+| `saved_object_find` +| `success` | User has accessed a saved object as part of a search operation. +| `failure` | User is not authorized to search for saved objects. + + +3+a| +===== Category: web + +| *Action* +| *Outcome* +| *Description* + +| `http_request` +| `unknown` | User is making an HTTP request. +|====== diff --git a/docs/user/security/secure-saved-objects.asciidoc b/docs/user/security/secure-saved-objects.asciidoc new file mode 100644 index 000000000000..3b15a576500f --- /dev/null +++ b/docs/user/security/secure-saved-objects.asciidoc @@ -0,0 +1,47 @@ +[role="xpack"] +[[xpack-security-secure-saved-objects]] +=== Secure saved objects + +{kib} stores entities such as dashboards, visualizations, alerts, actions, and advanced settings as saved objects, which are kept in a dedicated, internal {es} index. If such an object includes sensitive information, for example a PagerDuty integration key or email server credentials used by the alert action, {kib} encrypts it and makes sure it cannot be accidentally leaked or tampered with. + +Encrypting sensitive information means that a malicious party with access to the {kib} internal indices won't be able to extract that information without also knowing the encryption key. + +Example `kibana.yml`: + +[source,yaml] +-------------------------------------------------------------------------------- +xpack.encryptedSavedObjects: + encryptionKey: "min-32-byte-long-strong-encryption-key" +-------------------------------------------------------------------------------- + +[IMPORTANT] +============================================================================ +If you don't specify an encryption key, {kib} automatically generates a random key at startup. Every time you restart {kib}, it uses a new ephemeral encryption key and is unable to decrypt saved objects encrypted with another key. To prevent data loss, {kib} might disable features that rely on this encryption until you explicitly set an encryption key. +============================================================================ + +[[encryption-key-rotation]] +==== Encryption key rotation + +Many policies and best practices stipulate that encryption keys should be periodically rotated to decrease the amount of content encrypted with one key and therefore limit the potential damage if the key is compromised. {kib} allows you to rotate encryption keys whenever there is a need. + +When you change an encryption key, be sure to keep the old one for some time. Although {kib} only uses a new encryption key to encrypt all new and updated data, it still may need the old one to decrypt data that was encrypted using the old key. It's possible to have multiple old keys used only for decryption. {kib} doesn't automatically re-encrypt existing saved objects with the new encryption key. Re-encryption only happens when you update existing object or use the <>. + +Here is how your `kibana.yml` might look if you use key rotation functionality: + +[source,yaml] +-------------------------------------------------------------------------------- +xpack.encryptedSavedObjects: + encryptionKey: "min-32-byte-long-NEW-encryption-key" <1> + keyRotation: + decryptionOnlyKeys: ["min-32-byte-long-OLD#1-encryption-key", "min-32-byte-long-OLD#2-encryption-key"] <2> +-------------------------------------------------------------------------------- + +<1> The encryption key {kib} will use to encrypt all new or updated saved objects. This is known as the primary encryption key. +<2> A list of encryption keys {kib} will try to use to decrypt existing saved objects if decryption with the primary encryption key isn't possible. These keys are known as the decryption-only or secondary encryption keys. + +[NOTE] +============================================================================ +You might also leverage this functionality if multiple {kib} instances connected to the same {es} cluster use different encryption keys. In this case, you might have a mix of saved objects encrypted with different keys, and every {kib} instance can only deal with a specific subset of objects. To fix this, you must choose a single primary encryption key for `xpack.encryptedSavedObjects.encryptionKey`, move all other encryption keys to `xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys`, and sync this configuration across all {kib} instances. +============================================================================ + +At some point, you might want to dispose of old encryption keys completely. Make sure there are no saved objects that {kib} encrypted with these encryption keys. You can use the <> to determine which existing saved objects require decryption-only keys and re-encrypt them with the primary key. diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 0f02279eaf1f..e7bd297a3ebb 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -129,3 +129,4 @@ include::securing-communications/elasticsearch-mutual-tls.asciidoc[] include::audit-logging.asciidoc[] include::access-agreement.asciidoc[] include::session-management.asciidoc[] +include::secure-saved-objects.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/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx b/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx index e9543814ff01..5ef2cb73b593 100644 --- a/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx +++ b/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx @@ -39,7 +39,7 @@ export const PanelEditWithDrilldownsAndContextActions: React.FC = () => { const customActionGrouping: Action['grouping'] = [ { id: 'actions', - getDisplayName: () => 'Custom actions', + getDisplayName: () => 'API actions', getIconType: () => 'cloudStormy', order: 20, }, diff --git a/package.json b/package.json index 1e334f75d41b..8a87598aec56 100644 --- a/package.json +++ b/package.json @@ -131,10 +131,7 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/logging": "1.0.0", - "@kbn/pm": "1.0.0", "@kbn/std": "1.0.0", - "@kbn/telemetry-tools": "1.0.0", - "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ace": "1.0.0", "@kbn/monaco": "1.0.0", @@ -247,12 +244,22 @@ "@kbn/expect": "1.0.0", "@kbn/optimizer": "1.0.0", "@kbn/plugin-generator": "1.0.0", + "@kbn/pm": "1.0.0", "@kbn/release-notes": "1.0.0", + "@kbn/telemetry-tools": "1.0.0", "@kbn/test": "1.0.0", + "@kbn/test-subj-selector": "0.2.1", "@kbn/utility-types": "1.0.0", "@microsoft/api-documenter": "7.7.2", "@microsoft/api-extractor": "7.7.0", "@percy/agent": "^0.26.0", + "@storybook/addon-a11y": "^6.0.26", + "@storybook/addon-actions": "^6.0.26", + "@storybook/addon-essentials": "^6.0.26", + "@storybook/addon-knobs": "^6.0.26", + "@storybook/addon-storyshots": "^6.0.26", + "@storybook/react": "^6.0.26", + "@storybook/theming": "^6.0.26", "@testing-library/dom": "^7.24.2", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", @@ -301,6 +308,7 @@ "@types/json5": "^0.0.30", "@types/license-checker": "15.0.0", "@types/listr": "^0.14.0", + "@types/loader-utils": "^1.1.3", "@types/lodash": "^4.14.159", "@types/lru-cache": "^5.1.0", "@types/markdown-it": "^0.0.7", @@ -346,6 +354,7 @@ "@types/vinyl-fs": "^2.4.11", "@types/webpack": "^4.41.3", "@types/webpack-env": "^1.15.2", + "@types/webpack-merge": "^4.1.5", "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^3.10.0", "@typescript-eslint/parser": "^3.10.0", @@ -480,7 +489,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/elastic-eslint-config-kibana/package.json b/packages/elastic-eslint-config-kibana/package.json index 3f2c6e9edb26..9d0d57908654 100644 --- a/packages/elastic-eslint-config-kibana/package.json +++ b/packages/elastic-eslint-config-kibana/package.json @@ -7,6 +7,9 @@ "type": "git", "url": "git+https://github.com/elastic/kibana.git" }, + "kibana": { + "devOnly": true + }, "keywords": [], "author": "Spencer Alger ", "license": "Apache-2.0", diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json index 79d2fd8687da..2fab970c5c71 100644 --- a/packages/kbn-babel-preset/package.json +++ b/packages/kbn-babel-preset/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "private": true, "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "dependencies": { "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-export-namespace-from": "^7.10.4", diff --git a/packages/kbn-config/package.json b/packages/kbn-config/package.json index 6d2d56b929ea..f994836af884 100644 --- a/packages/kbn-config/package.json +++ b/packages/kbn-config/package.json @@ -12,10 +12,8 @@ "dependencies": { "@elastic/safer-lodash-set": "0.0.0", "@kbn/config-schema": "1.0.0", - "@kbn/dev-utils": "1.0.0", "@kbn/logging": "1.0.0", "@kbn/std": "1.0.0", - "@kbn/utility-types": "1.0.0", "js-yaml": "^3.14.0", "load-json-file": "^6.2.0", "lodash": "^4.17.20", @@ -24,6 +22,8 @@ "type-detect": "^4.0.8" }, "devDependencies": { + "@kbn/dev-utils": "1.0.0", + "@kbn/utility-types": "1.0.0", "typescript": "4.0.2", "tsd": "^0.13.1" } diff --git a/packages/kbn-config/src/env.ts b/packages/kbn-config/src/env.ts index e4585056696f..e7b465826223 100644 --- a/packages/kbn-config/src/env.ts +++ b/packages/kbn-config/src/env.ts @@ -19,6 +19,7 @@ import { resolve, join } from 'path'; import loadJsonFile from 'load-json-file'; +import { getPluginSearchPaths } from './plugins'; import { PackageInfo, EnvironmentMode } from './types'; /** @internal */ @@ -114,21 +115,11 @@ export class Env { this.binDir = resolve(this.homeDir, 'bin'); this.logDir = resolve(this.homeDir, 'log'); - /** - * BEWARE: this needs to stay roughly synchronized with the @kbn/optimizer - * `packages/kbn-optimizer/src/optimizer_config.ts` determines the paths - * that should be searched for plugins to build - */ - this.pluginSearchPaths = [ - resolve(this.homeDir, 'src', 'plugins'), - ...(options.cliArgs.oss ? [] : [resolve(this.homeDir, 'x-pack', 'plugins')]), - resolve(this.homeDir, 'plugins'), - ...(options.cliArgs.runExamples ? [resolve(this.homeDir, 'examples')] : []), - ...(options.cliArgs.runExamples && !options.cliArgs.oss - ? [resolve(this.homeDir, 'x-pack', 'examples')] - : []), - resolve(this.homeDir, '..', 'kibana-extra'), - ]; + this.pluginSearchPaths = getPluginSearchPaths({ + rootDir: this.homeDir, + oss: options.cliArgs.oss, + examples: options.cliArgs.runExamples, + }); this.cliArgs = Object.freeze(options.cliArgs); this.configs = Object.freeze(options.configs); diff --git a/packages/kbn-config/src/index.ts b/packages/kbn-config/src/index.ts index f02514a92e60..68609c6d5c7c 100644 --- a/packages/kbn-config/src/index.ts +++ b/packages/kbn-config/src/index.ts @@ -35,3 +35,4 @@ export { ObjectToConfigAdapter } from './object_to_config_adapter'; export { CliArgs, Env, RawPackageInfo } from './env'; export { EnvironmentMode, PackageInfo } from './types'; export { LegacyObjectToConfigAdapter, LegacyLoggingConfig } from './legacy'; +export { getPluginSearchPaths } from './plugins'; diff --git a/packages/kbn-config/src/plugins/index.ts b/packages/kbn-config/src/plugins/index.ts new file mode 100644 index 000000000000..7d02f9fb984c --- /dev/null +++ b/packages/kbn-config/src/plugins/index.ts @@ -0,0 +1,19 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { getPluginSearchPaths } from './plugin_search_paths'; diff --git a/packages/kbn-config/src/plugins/plugin_search_paths.ts b/packages/kbn-config/src/plugins/plugin_search_paths.ts new file mode 100644 index 000000000000..a7d151c3275c --- /dev/null +++ b/packages/kbn-config/src/plugins/plugin_search_paths.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { resolve } from 'path'; + +interface SearchOptions { + rootDir: string; + oss: boolean; + examples: boolean; +} + +export function getPluginSearchPaths({ rootDir, oss, examples }: SearchOptions) { + return [ + resolve(rootDir, 'src', 'plugins'), + ...(oss ? [] : [resolve(rootDir, 'x-pack', 'plugins')]), + resolve(rootDir, 'plugins'), + ...(examples ? [resolve(rootDir, 'examples')] : []), + ...(examples && !oss ? [resolve(rootDir, 'x-pack', 'examples')] : []), + resolve(rootDir, '..', 'kibana-extra'), + ]; +} diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index a51734168cf7..7fd9a9e7d67e 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -9,6 +9,9 @@ "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, + "kibana": { + "devOnly": true + }, "dependencies": { "@babel/core": "^7.11.6", "@kbn/utils": "1.0.0", diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 6a845825f0fd..98385b49dafa 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -40,7 +40,6 @@ export * from './axios'; export * from './stdio'; export * from './ci_stats_reporter'; export * from './plugin_list'; -export * from './simple_kibana_platform_plugin_discovery'; +export * from './plugins'; export * from './streams'; export * from './babel'; -export * from './parse_kibana_platform_plugin'; diff --git a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts index e8f6735205b1..9782067e6134 100644 --- a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts +++ b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts @@ -24,7 +24,7 @@ import MarkdownIt from 'markdown-it'; import cheerio from 'cheerio'; import { REPO_ROOT } from '@kbn/utils'; -import { simpleKibanaPlatformPluginDiscovery } from '../simple_kibana_platform_plugin_discovery'; +import { simpleKibanaPlatformPluginDiscovery } from '../plugins'; import { extractAsciidocInfo } from './extract_asciidoc_info'; export interface Plugin { diff --git a/src/core/server/audit_trail/index.ts b/packages/kbn-dev-utils/src/plugins/index.ts similarity index 83% rename from src/core/server/audit_trail/index.ts rename to packages/kbn-dev-utils/src/plugins/index.ts index 3f01e6fa3582..8705682f355c 100644 --- a/src/core/server/audit_trail/index.ts +++ b/packages/kbn-dev-utils/src/plugins/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { AuditTrailService } from './audit_trail_service'; -export { AuditableEvent, Auditor, AuditorFactory, AuditTrailSetup, AuditTrailStart } from './types'; +export * from './parse_kibana_platform_plugin'; +export * from './simple_kibana_platform_plugin_discovery'; diff --git a/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts b/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts similarity index 56% rename from packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts rename to packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts index 83d8c2684d7c..16aaecb3e478 100644 --- a/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts +++ b/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts @@ -23,12 +23,27 @@ import loadJsonFile from 'load-json-file'; export interface KibanaPlatformPlugin { readonly directory: string; readonly manifestPath: string; - readonly manifest: { - id: string; - ui: boolean; - server: boolean; - [key: string]: unknown; - }; + readonly manifest: Manifest; +} + +function isValidDepsDeclaration(input: unknown, type: string): string[] { + if (typeof input === 'undefined') return []; + if (Array.isArray(input) && input.every((i) => typeof i === 'string')) { + return input; + } + throw new TypeError(`The "${type}" in plugin manifest should be an array of strings.`); +} + +interface Manifest { + id: string; + ui: boolean; + server: boolean; + kibanaVersion: string; + version: string; + requiredPlugins: readonly string[]; + optionalPlugins: readonly string[]; + requiredBundles: readonly string[]; + extraPublicDirs: readonly string[]; } export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { @@ -36,7 +51,7 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP throw new TypeError('expected new platform manifest path to be absolute'); } - const manifest = loadJsonFile.sync(manifestPath); + const manifest: Partial = loadJsonFile.sync(manifestPath); if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); } @@ -45,6 +60,10 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP throw new TypeError('expected new platform plugin manifest to have a string id'); } + if (typeof manifest.version !== 'string') { + throw new TypeError('expected new platform plugin manifest to have a string version'); + } + return { directory: Path.dirname(manifestPath), manifestPath, @@ -54,6 +73,12 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP ui: !!manifest.ui, server: !!manifest.server, id: manifest.id, + version: manifest.version, + kibanaVersion: manifest.kibanaVersion || manifest.version, + requiredPlugins: isValidDepsDeclaration(manifest.requiredPlugins, 'requiredPlugins'), + optionalPlugins: isValidDepsDeclaration(manifest.optionalPlugins, 'optionalPlugins'), + requiredBundles: isValidDepsDeclaration(manifest.requiredBundles, 'requiredBundles'), + extraPublicDirs: isValidDepsDeclaration(manifest.extraPublicDirs, 'extraPublicDirs'), }, }; } diff --git a/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts similarity index 100% rename from packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts rename to packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts diff --git a/packages/kbn-es-archiver/package.json b/packages/kbn-es-archiver/package.json index 81c1747bb272..645abd619590 100644 --- a/packages/kbn-es-archiver/package.json +++ b/packages/kbn-es-archiver/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "license": "Apache-2.0", "main": "target/index.js", + "kibana": { + "devOnly": true + }, "scripts": { "kbn:bootstrap": "rm -rf target && tsc", "kbn:watch": "rm -rf target && tsc --watch" diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index c3733094350b..6ed3ae2eb2fb 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -4,6 +4,9 @@ "version": "1.0.0", "license": "Apache-2.0", "private": true, + "kibana": { + "devOnly": true + }, "scripts": { "kbn:bootstrap": "node scripts/build", "kbn:watch": "node scripts/build --watch" diff --git a/packages/kbn-eslint-import-resolver-kibana/package.json b/packages/kbn-eslint-import-resolver-kibana/package.json index 223c73e97908..ffbd94810a40 100755 --- a/packages/kbn-eslint-import-resolver-kibana/package.json +++ b/packages/kbn-eslint-import-resolver-kibana/package.json @@ -5,6 +5,9 @@ "version": "2.0.0", "main": "import_resolver_kibana.js", "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "repository": { "type": "git", "url": "https://github.com/elastic/kibana/tree/master/packages/kbn-eslint-import-resolver-kibana" diff --git a/packages/kbn-eslint-plugin-eslint/package.json b/packages/kbn-eslint-plugin-eslint/package.json index 026938213ac8..72b8577cb094 100644 --- a/packages/kbn-eslint-plugin-eslint/package.json +++ b/packages/kbn-eslint-plugin-eslint/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "private": true, "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "peerDependencies": { "eslint": "6.8.0", "babel-eslint": "^10.0.3" diff --git a/packages/kbn-expect/package.json b/packages/kbn-expect/package.json index 0975f5762fa1..8ca37c7c8867 100644 --- a/packages/kbn-expect/package.json +++ b/packages/kbn-expect/package.json @@ -3,5 +3,8 @@ "main": "./expect.js", "version": "1.0.0", "license": "MIT", - "private": true + "private": true, + "kibana": { + "devOnly": true + } } diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index c9e414dbc517..63146fc7a183 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -14,6 +14,7 @@ "@babel/core": "^7.11.6", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", + "@kbn/config": "1.0.0", "@kbn/std": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", "autoprefixer": "^9.7.4", diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json index 33f53e336598..a5e9f34a22aa 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json @@ -1,5 +1,6 @@ { "id": "bar", "ui": true, - "requiredBundles": ["foo"] + "requiredBundles": ["foo"], + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json index 256856181ccd..27730df19988 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json @@ -1,4 +1,5 @@ { "id": "foo", - "ui": true + "ui": true, + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json index 6e4e9c70a115..a8f991ee1146 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json @@ -1,3 +1,4 @@ { - "id": "baz" + "id": "baz", + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json index b9e044523a6a..d8a8b2e548e4 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json @@ -1,3 +1,4 @@ { - "id": "test_baz" + "id": "test_baz", + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json index 10602d2e7981..64ec7ff5ccf3 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json @@ -1,4 +1,5 @@ { "id": "baz", - "ui": true + "ui": true, + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index 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/get_output_stats.ts b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts index cc4cd05f42c3..82d1a276ccda 100644 --- a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts +++ b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts @@ -109,6 +109,11 @@ export function getMetrics(log: ToolingLog, config: OptimizerConfig) { id: bundle.id, value: sumSize(asyncChunks), }, + { + group: `async chunk count`, + id: bundle.id, + value: asyncChunks.length, + }, { group: `miscellaneous assets size`, id: bundle.id, diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index 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..1443fccda04d 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -19,6 +19,7 @@ import Path from 'path'; import Os from 'os'; +import { getPluginSearchPaths } from '@kbn/config'; import { Bundle, @@ -32,6 +33,7 @@ import { import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; import { getPluginBundles } from './get_plugin_bundles'; import { filterById } from './filter_by_id'; +import { focusBundles } from './focus_bundles'; import { readLimits } from '../limits'; export interface Limits { @@ -104,6 +106,11 @@ interface Options { * --filter f*r # [foobar], excludes [foo, bar] */ filter?: string[]; + /** + * behaves just like filter, but includes required bundles and plugins of the + * listed bundle ids. Filters only apply to bundles selected by focus + */ + focus?: string[]; /** flag that causes the core bundle to be built along with plugins */ includeCoreBundle?: boolean; @@ -132,6 +139,7 @@ export interface ParsedOptions { pluginPaths: string[]; pluginScanDirs: string[]; filters: string[]; + focus: string[]; inspectWorkers: boolean; includeCoreBundle: boolean; themeTags: ThemeTags; @@ -148,6 +156,7 @@ export class OptimizerConfig { const cache = options.cache !== false && !process.env.KBN_OPTIMIZER_NO_CACHE; const includeCoreBundle = !!options.includeCoreBundle; const filters = options.filter || []; + const focus = options.focus || []; const repoRoot = options.repoRoot; if (!Path.isAbsolute(repoRoot)) { @@ -159,19 +168,14 @@ export class OptimizerConfig { throw new TypeError('outputRoot must be an absolute path'); } - /** - * BEWARE: this needs to stay roughly synchronized with - * `src/core/server/config/env.ts` which determines which paths - * should be searched for plugins to load - */ - const pluginScanDirs = options.pluginScanDirs || [ - Path.resolve(repoRoot, 'src/plugins'), - ...(oss ? [] : [Path.resolve(repoRoot, 'x-pack/plugins')]), - Path.resolve(repoRoot, 'plugins'), - ...(examples ? [Path.resolve('examples')] : []), - ...(examples && !oss ? [Path.resolve('x-pack/examples')] : []), - Path.resolve(repoRoot, '../kibana-extra'), - ]; + const pluginScanDirs = + options.pluginScanDirs || + getPluginSearchPaths({ + rootDir: repoRoot, + oss, + examples, + }); + if (!pluginScanDirs.every((p) => Path.isAbsolute(p))) { throw new TypeError('pluginScanDirs must all be absolute paths'); } @@ -210,6 +214,7 @@ export class OptimizerConfig { pluginScanDirs, pluginPaths, filters, + focus, inspectWorkers, includeCoreBundle, themeTags, @@ -236,7 +241,7 @@ export class OptimizerConfig { ]; return new OptimizerConfig( - filterById(options.filters, bundles), + filterById(options.filters, focusBundles(options.focus, bundles)), options.cache, options.watch, options.inspectWorkers, diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 9678dd5de868..7987dd71f765 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -23,7 +23,6 @@ import { stringifyRequest } from 'loader-utils'; import webpack from 'webpack'; // @ts-expect-error import TerserPlugin from 'terser-webpack-plugin'; -// @ts-expect-error import webpackMerge from 'webpack-merge'; import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import CompressionPlugin from 'compression-webpack-plugin'; @@ -64,6 +63,14 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: optimization: { noEmitOnErrors: true, + splitChunks: { + maxAsyncRequests: 10, + cacheGroups: { + default: { + reuseExistingChunk: false, + }, + }, + }, }, externals: [UiSharedDeps.externals], diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 2e50f4214beb..c053445285c0 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -150,7 +150,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(128); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(500); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(501); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(144); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -8897,9 +8897,9 @@ exports.ToolingLogCollectingWriter = ToolingLogCollectingWriter; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(129); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(280); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(399); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(400); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(281); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(400); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(401); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -8943,7 +8943,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(273); /* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(278); /* harmony import */ var _utils_yarn_lock__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(275); -/* harmony import */ var _utils_validate_yarn_lock__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(279); +/* harmony import */ var _utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(279); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -9002,7 +9002,7 @@ const BootstrapCommand = { const yarnLock = await Object(_utils_yarn_lock__WEBPACK_IMPORTED_MODULE_6__["readYarnLock"])(kbn); if (options.validate) { - await Object(_utils_validate_yarn_lock__WEBPACK_IMPORTED_MODULE_7__["validateYarnLock"])(kbn, yarnLock); + await Object(_utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_7__["validateDependencies"])(kbn, yarnLock); } await Object(_utils_link_project_executables__WEBPACK_IMPORTED_MODULE_0__["linkProjectExecutables"])(projects, projectGraph); @@ -14713,6 +14713,10 @@ class Project { return this.json.kibana && this.json.kibana.clean || {}; } + isFlaggedAsDevOnly() { + return !!(this.json.kibana && this.json.kibana.devOnly); + } + hasScript(name) { return name in this.scripts; } @@ -37954,13 +37958,16 @@ class BootstrapCacheFile { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "validateYarnLock", function() { return validateYarnLock; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "validateDependencies", function() { return validateDependencies; }); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(276); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(dedent__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(131); -/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(144); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(113); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(131); +/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(144); +/* harmony import */ var _projects_tree__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(280); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -37984,7 +37991,9 @@ __webpack_require__.r(__webpack_exports__); -async function validateYarnLock(kbn, yarnLock) { + + +async function validateDependencies(kbn, yarnLock) { // look through all of the packages in the yarn.lock file to see if // we have accidentally installed multiple lodash v4 versions const lodash4Versions = new Set(); @@ -38005,8 +38014,8 @@ async function validateYarnLock(kbn, yarnLock) { delete yarnLock[req]; } - await Object(_fs__WEBPACK_IMPORTED_MODULE_2__["writeFile"])(kbn.getAbsolute('yarn.lock'), Object(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__["stringify"])(yarnLock), 'utf8'); - _log__WEBPACK_IMPORTED_MODULE_3__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + await Object(_fs__WEBPACK_IMPORTED_MODULE_3__["writeFile"])(kbn.getAbsolute('yarn.lock'), Object(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__["stringify"])(yarnLock), 'utf8'); + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` Multiple version of lodash v4 were detected, so they have been removed from the yarn.lock file. Please rerun yarn kbn bootstrap to coalese the @@ -38025,7 +38034,7 @@ async function validateYarnLock(kbn, yarnLock) { // of lodash v3 in the distributable - const prodDependencies = kbn.resolveAllProductionDependencies(yarnLock, _log__WEBPACK_IMPORTED_MODULE_3__["log"]); + const prodDependencies = kbn.resolveAllProductionDependencies(yarnLock, _log__WEBPACK_IMPORTED_MODULE_4__["log"]); const lodash3Versions = new Set(); for (const dep of prodDependencies.values()) { @@ -38036,7 +38045,7 @@ async function validateYarnLock(kbn, yarnLock) { if (lodash3Versions.size) { - _log__WEBPACK_IMPORTED_MODULE_3__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` Due to changes in the yarn.lock file and/or package.json files a version of lodash 3 is now included in the production dependencies. To reduce the size of @@ -38088,7 +38097,7 @@ async function validateYarnLock(kbn, yarnLock) { }) => ` ${range} => ${projects.map(p => p.name).join(', ')}`)], []).join('\n '); if (duplicateRanges) { - _log__WEBPACK_IMPORTED_MODULE_3__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` [single_version_dependencies] Multiple version ranges for the same dependency were found declared across different package.json files. Please consolidate @@ -38102,21 +38111,207 @@ async function validateYarnLock(kbn, yarnLock) { ${duplicateRanges} `); process.exit(1); + } // look for packages that have the the `kibana.devOnly` flag in their package.json + // and make sure they aren't included in the production dependencies of Kibana + + + const devOnlyProjectsInProduction = getDevOnlyProductionDepsTree(kbn, 'kibana'); + + if (devOnlyProjectsInProduction) { + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + Some of the packages in the production dependency chain for Kibana and X-Pack are + flagged with "kibana.devOnly" in their package.json. Please check changes made to + packages and their dependencies to ensure they don't end up in production. + + The devOnly dependencies that are being dependend on in production are: + + ${Object(_projects_tree__WEBPACK_IMPORTED_MODULE_5__["treeToString"])(devOnlyProjectsInProduction).split('\n').join('\n ')} + `); + process.exit(1); } - _log__WEBPACK_IMPORTED_MODULE_3__["log"].success('yarn.lock analysis completed without any issues'); + _log__WEBPACK_IMPORTED_MODULE_4__["log"].success('yarn.lock analysis completed without any issues'); +} + +function getDevOnlyProductionDepsTree(kbn, projectName) { + const project = kbn.getProject(projectName); + const childProjectNames = [...Object.keys(project.productionDependencies).filter(name => kbn.hasProject(name)), ...(projectName === 'kibana' ? ['x-pack'] : [])]; + const children = childProjectNames.map(n => getDevOnlyProductionDepsTree(kbn, n)).filter(t => !!t); + + if (!children.length && !project.isFlaggedAsDevOnly()) { + return; + } + + const tree = { + name: project.isFlaggedAsDevOnly() ? chalk__WEBPACK_IMPORTED_MODULE_2___default.a.red.bold(projectName) : projectName, + children + }; + return tree; } /***/ }), /* 280 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "renderProjectsTree", function() { return renderProjectsTree; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "treeToString", function() { return treeToString; }); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(113); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +const projectKey = Symbol('__project'); +function renderProjectsTree(rootPath, projects) { + const projectsTree = buildProjectsTree(rootPath, projects); + return treeToString(createTreeStructure(projectsTree)); +} +function treeToString(tree) { + return [tree.name].concat(childrenToStrings(tree.children, '')).join('\n'); +} + +function childrenToStrings(tree, treePrefix) { + if (tree === undefined) { + return []; + } + + let strings = []; + tree.forEach((node, index) => { + const isLastNode = tree.length - 1 === index; + const nodePrefix = isLastNode ? '└── ' : '├── '; + const childPrefix = isLastNode ? ' ' : '│ '; + const childrenPrefix = treePrefix + childPrefix; + strings.push(`${treePrefix}${nodePrefix}${node.name}`); + strings = strings.concat(childrenToStrings(node.children, childrenPrefix)); + }); + return strings; +} + +function createTreeStructure(tree) { + let name; + const children = []; + + for (const [dir, project] of tree.entries()) { + // This is a leaf node (aka a project) + if (typeof project === 'string') { + name = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(project); + continue; + } // If there's only one project and the key indicates it's a leaf node, we + // know that we're at a package folder that contains a package.json, so we + // "inline it" so we don't get unnecessary levels, i.e. we'll just see + // `foo` instead of `foo -> foo`. + + + if (project.size === 1 && project.has(projectKey)) { + const projectName = project.get(projectKey); + children.push({ + children: [], + name: dirOrProjectName(dir, projectName) + }); + continue; + } + + const subtree = createTreeStructure(project); // If the name is specified, we know there's a package at the "root" of the + // subtree itself. + + if (subtree.name !== undefined) { + const projectName = subtree.name; + children.push({ + children: subtree.children, + name: dirOrProjectName(dir, projectName) + }); + continue; + } // Special-case whenever we have one child, so we don't get unnecessary + // folders in the output. E.g. instead of `foo -> bar -> baz` we get + // `foo/bar/baz` instead. + + + if (subtree.children && subtree.children.length === 1) { + const child = subtree.children[0]; + const newName = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(path__WEBPACK_IMPORTED_MODULE_1___default.a.join(dir.toString(), child.name)); + children.push({ + children: child.children, + name: newName + }); + continue; + } + + children.push({ + children: subtree.children, + name: chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(dir.toString()) + }); + } + + return { + name, + children + }; +} + +function dirOrProjectName(dir, projectName) { + return dir === projectName ? chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(dir) : chalk__WEBPACK_IMPORTED_MODULE_0___default.a`{dim ${dir.toString()} ({reset.green ${projectName}})}`; +} + +function buildProjectsTree(rootPath, projects) { + const tree = new Map(); + + for (const project of projects.values()) { + if (rootPath === project.path) { + tree.set(projectKey, project.name); + } else { + const relativeProjectPath = path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(rootPath, project.path); + addProjectToTree(tree, relativeProjectPath.split(path__WEBPACK_IMPORTED_MODULE_1___default.a.sep), project); + } + } + + return tree; +} + +function addProjectToTree(tree, pathParts, project) { + if (pathParts.length === 0) { + tree.set(projectKey, project.name); + } else { + const [currentDir, ...rest] = pathParts; + + if (!tree.has(currentDir)) { + tree.set(currentDir, new Map()); + } + + const subtree = tree.get(currentDir); + addProjectToTree(subtree, rest, project); + } +} + +/***/ }), +/* 281 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(281); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(282); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(367); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(368); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); @@ -38216,21 +38411,21 @@ const CleanCommand = { }; /***/ }), -/* 281 */ +/* 282 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const path = __webpack_require__(4); -const globby = __webpack_require__(282); -const isGlob = __webpack_require__(294); -const slash = __webpack_require__(358); +const globby = __webpack_require__(283); +const isGlob = __webpack_require__(295); +const slash = __webpack_require__(359); const gracefulFs = __webpack_require__(133); -const isPathCwd = __webpack_require__(360); -const isPathInside = __webpack_require__(361); -const rimraf = __webpack_require__(362); -const pMap = __webpack_require__(363); +const isPathCwd = __webpack_require__(361); +const isPathInside = __webpack_require__(362); +const rimraf = __webpack_require__(363); +const pMap = __webpack_require__(364); const rimrafP = promisify(rimraf); @@ -38344,19 +38539,19 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options /***/ }), -/* 282 */ +/* 283 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const arrayUnion = __webpack_require__(283); -const merge2 = __webpack_require__(284); +const arrayUnion = __webpack_require__(284); +const merge2 = __webpack_require__(285); const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(285); -const dirGlob = __webpack_require__(354); -const gitignore = __webpack_require__(356); -const {FilterStream, UniqueStream} = __webpack_require__(359); +const fastGlob = __webpack_require__(286); +const dirGlob = __webpack_require__(355); +const gitignore = __webpack_require__(357); +const {FilterStream, UniqueStream} = __webpack_require__(360); const DEFAULT_FILTER = () => false; @@ -38529,7 +38724,7 @@ module.exports.gitignore = gitignore; /***/ }), -/* 283 */ +/* 284 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38541,7 +38736,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 284 */ +/* 285 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38692,17 +38887,17 @@ function pauseStreams (streams, options) { /***/ }), -/* 285 */ +/* 286 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(286); -const async_1 = __webpack_require__(315); -const stream_1 = __webpack_require__(350); -const sync_1 = __webpack_require__(351); -const settings_1 = __webpack_require__(353); -const utils = __webpack_require__(287); +const taskManager = __webpack_require__(287); +const async_1 = __webpack_require__(316); +const stream_1 = __webpack_require__(351); +const sync_1 = __webpack_require__(352); +const settings_1 = __webpack_require__(354); +const utils = __webpack_require__(288); async function FastGlob(source, options) { assertPatternsInput(source); const works = getWorks(source, async_1.default, options); @@ -38766,13 +38961,13 @@ module.exports = FastGlob; /***/ }), -/* 286 */ +/* 287 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -38837,30 +39032,30 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 287 */ +/* 288 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const array = __webpack_require__(288); +const array = __webpack_require__(289); exports.array = array; -const errno = __webpack_require__(289); +const errno = __webpack_require__(290); exports.errno = errno; -const fs = __webpack_require__(290); +const fs = __webpack_require__(291); exports.fs = fs; -const path = __webpack_require__(291); +const path = __webpack_require__(292); exports.path = path; -const pattern = __webpack_require__(292); +const pattern = __webpack_require__(293); exports.pattern = pattern; -const stream = __webpack_require__(313); +const stream = __webpack_require__(314); exports.stream = stream; -const string = __webpack_require__(314); +const string = __webpack_require__(315); exports.string = string; /***/ }), -/* 288 */ +/* 289 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38888,7 +39083,7 @@ exports.splitWhen = splitWhen; /***/ }), -/* 289 */ +/* 290 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38901,7 +39096,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 290 */ +/* 291 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38926,7 +39121,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 291 */ +/* 292 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38965,16 +39160,16 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; /***/ }), -/* 292 */ +/* 293 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const globParent = __webpack_require__(293); -const micromatch = __webpack_require__(296); -const picomatch = __webpack_require__(307); +const globParent = __webpack_require__(294); +const micromatch = __webpack_require__(297); +const picomatch = __webpack_require__(308); const GLOBSTAR = '**'; const ESCAPE_SYMBOL = '\\'; const COMMON_GLOB_SYMBOLS_RE = /[*?]|^!/; @@ -39084,13 +39279,13 @@ exports.matchAny = matchAny; /***/ }), -/* 293 */ +/* 294 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isGlob = __webpack_require__(294); +var isGlob = __webpack_require__(295); var pathPosixDirname = __webpack_require__(4).posix.dirname; var isWin32 = __webpack_require__(121).platform() === 'win32'; @@ -39132,7 +39327,7 @@ module.exports = function globParent(str, opts) { /***/ }), -/* 294 */ +/* 295 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -39142,7 +39337,7 @@ module.exports = function globParent(str, opts) { * Released under the MIT License. */ -var isExtglob = __webpack_require__(295); +var isExtglob = __webpack_require__(296); var chars = { '{': '}', '(': ')', '[': ']'}; var strictRegex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; var relaxedRegex = /\\(.)|(^!|[*?{}()[\]]|\(\?)/; @@ -39186,7 +39381,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 295 */ +/* 296 */ /***/ (function(module, exports) { /*! @@ -39212,16 +39407,16 @@ module.exports = function isExtglob(str) { /***/ }), -/* 296 */ +/* 297 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const util = __webpack_require__(112); -const braces = __webpack_require__(297); -const picomatch = __webpack_require__(307); -const utils = __webpack_require__(310); +const braces = __webpack_require__(298); +const picomatch = __webpack_require__(308); +const utils = __webpack_require__(311); const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); /** @@ -39686,16 +39881,16 @@ module.exports = micromatch; /***/ }), -/* 297 */ +/* 298 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(298); -const compile = __webpack_require__(300); -const expand = __webpack_require__(304); -const parse = __webpack_require__(305); +const stringify = __webpack_require__(299); +const compile = __webpack_require__(301); +const expand = __webpack_require__(305); +const parse = __webpack_require__(306); /** * Expand the given pattern or create a regex-compatible string. @@ -39863,13 +40058,13 @@ module.exports = braces; /***/ }), -/* 298 */ +/* 299 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(299); +const utils = __webpack_require__(300); module.exports = (ast, options = {}) => { let stringify = (node, parent = {}) => { @@ -39902,7 +40097,7 @@ module.exports = (ast, options = {}) => { /***/ }), -/* 299 */ +/* 300 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40021,14 +40216,14 @@ exports.flatten = (...args) => { /***/ }), -/* 300 */ +/* 301 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(301); -const utils = __webpack_require__(299); +const fill = __webpack_require__(302); +const utils = __webpack_require__(300); const compile = (ast, options = {}) => { let walk = (node, parent = {}) => { @@ -40085,7 +40280,7 @@ module.exports = compile; /***/ }), -/* 301 */ +/* 302 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40099,7 +40294,7 @@ module.exports = compile; const util = __webpack_require__(112); -const toRegexRange = __webpack_require__(302); +const toRegexRange = __webpack_require__(303); const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); @@ -40341,7 +40536,7 @@ module.exports = fill; /***/ }), -/* 302 */ +/* 303 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40354,7 +40549,7 @@ module.exports = fill; -const isNumber = __webpack_require__(303); +const isNumber = __webpack_require__(304); const toRegexRange = (min, max, options) => { if (isNumber(min) === false) { @@ -40636,7 +40831,7 @@ module.exports = toRegexRange; /***/ }), -/* 303 */ +/* 304 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40661,15 +40856,15 @@ module.exports = function(num) { /***/ }), -/* 304 */ +/* 305 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(301); -const stringify = __webpack_require__(298); -const utils = __webpack_require__(299); +const fill = __webpack_require__(302); +const stringify = __webpack_require__(299); +const utils = __webpack_require__(300); const append = (queue = '', stash = '', enclose = false) => { let result = []; @@ -40781,13 +40976,13 @@ module.exports = expand; /***/ }), -/* 305 */ +/* 306 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(298); +const stringify = __webpack_require__(299); /** * Constants @@ -40809,7 +41004,7 @@ const { CHAR_SINGLE_QUOTE, /* ' */ CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE -} = __webpack_require__(306); +} = __webpack_require__(307); /** * parse @@ -41121,7 +41316,7 @@ module.exports = parse; /***/ }), -/* 306 */ +/* 307 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41185,27 +41380,27 @@ module.exports = { /***/ }), -/* 307 */ +/* 308 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(308); +module.exports = __webpack_require__(309); /***/ }), -/* 308 */ +/* 309 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const scan = __webpack_require__(309); -const parse = __webpack_require__(312); -const utils = __webpack_require__(310); -const constants = __webpack_require__(311); +const scan = __webpack_require__(310); +const parse = __webpack_require__(313); +const utils = __webpack_require__(311); +const constants = __webpack_require__(312); const isObject = val => val && typeof val === 'object' && !Array.isArray(val); /** @@ -41541,13 +41736,13 @@ module.exports = picomatch; /***/ }), -/* 309 */ +/* 310 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(310); +const utils = __webpack_require__(311); const { CHAR_ASTERISK, /* * */ CHAR_AT, /* @ */ @@ -41564,7 +41759,7 @@ const { CHAR_RIGHT_CURLY_BRACE, /* } */ CHAR_RIGHT_PARENTHESES, /* ) */ CHAR_RIGHT_SQUARE_BRACKET /* ] */ -} = __webpack_require__(311); +} = __webpack_require__(312); const isPathSeparator = code => { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -41931,7 +42126,7 @@ module.exports = scan; /***/ }), -/* 310 */ +/* 311 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41944,7 +42139,7 @@ const { REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL -} = __webpack_require__(311); +} = __webpack_require__(312); exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); @@ -42002,7 +42197,7 @@ exports.wrapOutput = (input, state = {}, options = {}) => { /***/ }), -/* 311 */ +/* 312 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -42188,14 +42383,14 @@ module.exports = { /***/ }), -/* 312 */ +/* 313 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const constants = __webpack_require__(311); -const utils = __webpack_require__(310); +const constants = __webpack_require__(312); +const utils = __webpack_require__(311); /** * Constants @@ -43273,13 +43468,13 @@ module.exports = parse; /***/ }), -/* 313 */ +/* 314 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const merge2 = __webpack_require__(284); +const merge2 = __webpack_require__(285); function merge(streams) { const mergedStream = merge2(streams); streams.forEach((stream) => { @@ -43296,7 +43491,7 @@ function propagateCloseEventToSources(streams) { /***/ }), -/* 314 */ +/* 315 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43313,14 +43508,14 @@ exports.isEmpty = isEmpty; /***/ }), -/* 315 */ +/* 316 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(316); -const provider_1 = __webpack_require__(343); +const stream_1 = __webpack_require__(317); +const provider_1 = __webpack_require__(344); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -43348,16 +43543,16 @@ exports.default = ProviderAsync; /***/ }), -/* 316 */ +/* 317 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(138); -const fsStat = __webpack_require__(317); -const fsWalk = __webpack_require__(322); -const reader_1 = __webpack_require__(342); +const fsStat = __webpack_require__(318); +const fsWalk = __webpack_require__(323); +const reader_1 = __webpack_require__(343); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -43410,15 +43605,15 @@ exports.default = ReaderStream; /***/ }), -/* 317 */ +/* 318 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(318); -const sync = __webpack_require__(319); -const settings_1 = __webpack_require__(320); +const async = __webpack_require__(319); +const sync = __webpack_require__(320); +const settings_1 = __webpack_require__(321); exports.Settings = settings_1.default; function stat(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -43441,7 +43636,7 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 318 */ +/* 319 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43479,7 +43674,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 319 */ +/* 320 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43508,13 +43703,13 @@ exports.read = read; /***/ }), -/* 320 */ +/* 321 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(321); +const fs = __webpack_require__(322); class Settings { constructor(_options = {}) { this._options = _options; @@ -43531,7 +43726,7 @@ exports.default = Settings; /***/ }), -/* 321 */ +/* 322 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43554,16 +43749,16 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 322 */ +/* 323 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(323); -const stream_1 = __webpack_require__(338); -const sync_1 = __webpack_require__(339); -const settings_1 = __webpack_require__(341); +const async_1 = __webpack_require__(324); +const stream_1 = __webpack_require__(339); +const sync_1 = __webpack_require__(340); +const settings_1 = __webpack_require__(342); exports.Settings = settings_1.default; function walk(directory, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -43593,13 +43788,13 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 323 */ +/* 324 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(324); +const async_1 = __webpack_require__(325); class AsyncProvider { constructor(_root, _settings) { this._root = _root; @@ -43630,17 +43825,17 @@ function callSuccessCallback(callback, entries) { /***/ }), -/* 324 */ +/* 325 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = __webpack_require__(156); -const fsScandir = __webpack_require__(325); -const fastq = __webpack_require__(334); -const common = __webpack_require__(336); -const reader_1 = __webpack_require__(337); +const fsScandir = __webpack_require__(326); +const fastq = __webpack_require__(335); +const common = __webpack_require__(337); +const reader_1 = __webpack_require__(338); class AsyncReader extends reader_1.default { constructor(_root, _settings) { super(_root, _settings); @@ -43730,15 +43925,15 @@ exports.default = AsyncReader; /***/ }), -/* 325 */ +/* 326 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(326); -const sync = __webpack_require__(331); -const settings_1 = __webpack_require__(332); +const async = __webpack_require__(327); +const sync = __webpack_require__(332); +const settings_1 = __webpack_require__(333); exports.Settings = settings_1.default; function scandir(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -43761,16 +43956,16 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 326 */ +/* 327 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(317); -const rpl = __webpack_require__(327); -const constants_1 = __webpack_require__(328); -const utils = __webpack_require__(329); +const fsStat = __webpack_require__(318); +const rpl = __webpack_require__(328); +const constants_1 = __webpack_require__(329); +const utils = __webpack_require__(330); function read(directory, settings, callback) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(directory, settings, callback); @@ -43858,7 +44053,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 327 */ +/* 328 */ /***/ (function(module, exports) { module.exports = runParallel @@ -43912,7 +44107,7 @@ function runParallel (tasks, cb) { /***/ }), -/* 328 */ +/* 329 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43932,18 +44127,18 @@ exports.IS_SUPPORT_READDIR_WITH_FILE_TYPES = IS_MATCHED_BY_MAJOR || IS_MATCHED_B /***/ }), -/* 329 */ +/* 330 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(330); +const fs = __webpack_require__(331); exports.fs = fs; /***/ }), -/* 330 */ +/* 331 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43968,15 +44163,15 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 331 */ +/* 332 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(317); -const constants_1 = __webpack_require__(328); -const utils = __webpack_require__(329); +const fsStat = __webpack_require__(318); +const constants_1 = __webpack_require__(329); +const utils = __webpack_require__(330); function read(directory, settings) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(directory, settings); @@ -44027,15 +44222,15 @@ exports.readdir = readdir; /***/ }), -/* 332 */ +/* 333 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsStat = __webpack_require__(317); -const fs = __webpack_require__(333); +const fsStat = __webpack_require__(318); +const fs = __webpack_require__(334); class Settings { constructor(_options = {}) { this._options = _options; @@ -44058,7 +44253,7 @@ exports.default = Settings; /***/ }), -/* 333 */ +/* 334 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44083,13 +44278,13 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 334 */ +/* 335 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var reusify = __webpack_require__(335) +var reusify = __webpack_require__(336) function fastqueue (context, worker, concurrency) { if (typeof context === 'function') { @@ -44263,7 +44458,7 @@ module.exports = fastqueue /***/ }), -/* 335 */ +/* 336 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44303,7 +44498,7 @@ module.exports = reusify /***/ }), -/* 336 */ +/* 337 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44334,13 +44529,13 @@ exports.joinPathSegments = joinPathSegments; /***/ }), -/* 337 */ +/* 338 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const common = __webpack_require__(336); +const common = __webpack_require__(337); class Reader { constructor(_root, _settings) { this._root = _root; @@ -44352,14 +44547,14 @@ exports.default = Reader; /***/ }), -/* 338 */ +/* 339 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(138); -const async_1 = __webpack_require__(324); +const async_1 = __webpack_require__(325); class StreamProvider { constructor(_root, _settings) { this._root = _root; @@ -44389,13 +44584,13 @@ exports.default = StreamProvider; /***/ }), -/* 339 */ +/* 340 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(340); +const sync_1 = __webpack_require__(341); class SyncProvider { constructor(_root, _settings) { this._root = _root; @@ -44410,15 +44605,15 @@ exports.default = SyncProvider; /***/ }), -/* 340 */ +/* 341 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsScandir = __webpack_require__(325); -const common = __webpack_require__(336); -const reader_1 = __webpack_require__(337); +const fsScandir = __webpack_require__(326); +const common = __webpack_require__(337); +const reader_1 = __webpack_require__(338); class SyncReader extends reader_1.default { constructor() { super(...arguments); @@ -44476,14 +44671,14 @@ exports.default = SyncReader; /***/ }), -/* 341 */ +/* 342 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsScandir = __webpack_require__(325); +const fsScandir = __webpack_require__(326); class Settings { constructor(_options = {}) { this._options = _options; @@ -44509,15 +44704,15 @@ exports.default = Settings; /***/ }), -/* 342 */ +/* 343 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsStat = __webpack_require__(317); -const utils = __webpack_require__(287); +const fsStat = __webpack_require__(318); +const utils = __webpack_require__(288); class Reader { constructor(_settings) { this._settings = _settings; @@ -44549,17 +44744,17 @@ exports.default = Reader; /***/ }), -/* 343 */ +/* 344 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const deep_1 = __webpack_require__(344); -const entry_1 = __webpack_require__(347); -const error_1 = __webpack_require__(348); -const entry_2 = __webpack_require__(349); +const deep_1 = __webpack_require__(345); +const entry_1 = __webpack_require__(348); +const error_1 = __webpack_require__(349); +const entry_2 = __webpack_require__(350); class Provider { constructor(_settings) { this._settings = _settings; @@ -44604,14 +44799,14 @@ exports.default = Provider; /***/ }), -/* 344 */ +/* 345 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); -const partial_1 = __webpack_require__(345); +const utils = __webpack_require__(288); +const partial_1 = __webpack_require__(346); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -44665,13 +44860,13 @@ exports.default = DeepFilter; /***/ }), -/* 345 */ +/* 346 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const matcher_1 = __webpack_require__(346); +const matcher_1 = __webpack_require__(347); class PartialMatcher extends matcher_1.default { match(filepath) { const parts = filepath.split('/'); @@ -44710,13 +44905,13 @@ exports.default = PartialMatcher; /***/ }), -/* 346 */ +/* 347 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class Matcher { constructor(_patterns, _settings, _micromatchOptions) { this._patterns = _patterns; @@ -44767,13 +44962,13 @@ exports.default = Matcher; /***/ }), -/* 347 */ +/* 348 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -44829,13 +45024,13 @@ exports.default = EntryFilter; /***/ }), -/* 348 */ +/* 349 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -44851,13 +45046,13 @@ exports.default = ErrorFilter; /***/ }), -/* 349 */ +/* 350 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -44884,15 +45079,15 @@ exports.default = EntryTransformer; /***/ }), -/* 350 */ +/* 351 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(138); -const stream_2 = __webpack_require__(316); -const provider_1 = __webpack_require__(343); +const stream_2 = __webpack_require__(317); +const provider_1 = __webpack_require__(344); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -44922,14 +45117,14 @@ exports.default = ProviderStream; /***/ }), -/* 351 */ +/* 352 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(352); -const provider_1 = __webpack_require__(343); +const sync_1 = __webpack_require__(353); +const provider_1 = __webpack_require__(344); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -44952,15 +45147,15 @@ exports.default = ProviderSync; /***/ }), -/* 352 */ +/* 353 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(317); -const fsWalk = __webpack_require__(322); -const reader_1 = __webpack_require__(342); +const fsStat = __webpack_require__(318); +const fsWalk = __webpack_require__(323); +const reader_1 = __webpack_require__(343); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -45002,7 +45197,7 @@ exports.default = ReaderSync; /***/ }), -/* 353 */ +/* 354 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45061,13 +45256,13 @@ exports.default = Settings; /***/ }), -/* 354 */ +/* 355 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(355); +const pathType = __webpack_require__(356); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -45143,7 +45338,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 355 */ +/* 356 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45193,7 +45388,7 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 356 */ +/* 357 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45201,9 +45396,9 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); const {promisify} = __webpack_require__(112); const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(285); -const gitIgnore = __webpack_require__(357); -const slash = __webpack_require__(358); +const fastGlob = __webpack_require__(286); +const gitIgnore = __webpack_require__(358); +const slash = __webpack_require__(359); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -45317,7 +45512,7 @@ module.exports.sync = options => { /***/ }), -/* 357 */ +/* 358 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -45920,7 +46115,7 @@ if ( /***/ }), -/* 358 */ +/* 359 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45938,7 +46133,7 @@ module.exports = path => { /***/ }), -/* 359 */ +/* 360 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45991,7 +46186,7 @@ module.exports = { /***/ }), -/* 360 */ +/* 361 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46013,7 +46208,7 @@ module.exports = path_ => { /***/ }), -/* 361 */ +/* 362 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46041,7 +46236,7 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 362 */ +/* 363 */ /***/ (function(module, exports, __webpack_require__) { const assert = __webpack_require__(140) @@ -46407,12 +46602,12 @@ rimraf.sync = rimrafSync /***/ }), -/* 363 */ +/* 364 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(364); +const AggregateError = __webpack_require__(365); module.exports = async ( iterable, @@ -46495,13 +46690,13 @@ module.exports = async ( /***/ }), -/* 364 */ +/* 365 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(365); -const cleanStack = __webpack_require__(366); +const indentString = __webpack_require__(366); +const cleanStack = __webpack_require__(367); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -46549,7 +46744,7 @@ module.exports = AggregateError; /***/ }), -/* 365 */ +/* 366 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46591,7 +46786,7 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 366 */ +/* 367 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46638,20 +46833,20 @@ module.exports = (stack, options) => { /***/ }), -/* 367 */ +/* 368 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readline = __webpack_require__(368); -const chalk = __webpack_require__(369); -const cliCursor = __webpack_require__(376); -const cliSpinners = __webpack_require__(380); -const logSymbols = __webpack_require__(382); -const stripAnsi = __webpack_require__(391); -const wcwidth = __webpack_require__(393); -const isInteractive = __webpack_require__(397); -const MuteStream = __webpack_require__(398); +const readline = __webpack_require__(369); +const chalk = __webpack_require__(370); +const cliCursor = __webpack_require__(377); +const cliSpinners = __webpack_require__(381); +const logSymbols = __webpack_require__(383); +const stripAnsi = __webpack_require__(392); +const wcwidth = __webpack_require__(394); +const isInteractive = __webpack_require__(398); +const MuteStream = __webpack_require__(399); const TEXT = Symbol('text'); const PREFIX_TEXT = Symbol('prefixText'); @@ -47004,23 +47199,23 @@ module.exports.promise = (action, options) => { /***/ }), -/* 368 */ +/* 369 */ /***/ (function(module, exports) { module.exports = require("readline"); /***/ }), -/* 369 */ +/* 370 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiStyles = __webpack_require__(370); +const ansiStyles = __webpack_require__(371); const {stdout: stdoutColor, stderr: stderrColor} = __webpack_require__(120); const { stringReplaceAll, stringEncaseCRLFWithFirstIndex -} = __webpack_require__(374); +} = __webpack_require__(375); // `supportsColor.level` → `ansiStyles.color[name]` mapping const levelMapping = [ @@ -47221,7 +47416,7 @@ const chalkTag = (chalk, ...strings) => { } if (template === undefined) { - template = __webpack_require__(375); + template = __webpack_require__(376); } return template(chalk, parts.join('')); @@ -47250,7 +47445,7 @@ module.exports = chalk; /***/ }), -/* 370 */ +/* 371 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47296,7 +47491,7 @@ const setLazyProperty = (object, property, get) => { let colorConvert; const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => { if (colorConvert === undefined) { - colorConvert = __webpack_require__(371); + colorConvert = __webpack_require__(372); } const offset = isBackground ? 10 : 0; @@ -47421,11 +47616,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 371 */ +/* 372 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(372); -const route = __webpack_require__(373); +const conversions = __webpack_require__(373); +const route = __webpack_require__(374); const convert = {}; @@ -47508,7 +47703,7 @@ module.exports = convert; /***/ }), -/* 372 */ +/* 373 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ @@ -48353,10 +48548,10 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 373 */ +/* 374 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(372); +const conversions = __webpack_require__(373); /* This function routes a model to all other models. @@ -48456,7 +48651,7 @@ module.exports = function (fromModel) { /***/ }), -/* 374 */ +/* 375 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48502,7 +48697,7 @@ module.exports = { /***/ }), -/* 375 */ +/* 376 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48643,12 +48838,12 @@ module.exports = (chalk, temporary) => { /***/ }), -/* 376 */ +/* 377 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(377); +const restoreCursor = __webpack_require__(378); let isHidden = false; @@ -48685,12 +48880,12 @@ exports.toggle = (force, writableStream) => { /***/ }), -/* 377 */ +/* 378 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(378); +const onetime = __webpack_require__(379); const signalExit = __webpack_require__(218); module.exports = onetime(() => { @@ -48701,12 +48896,12 @@ module.exports = onetime(() => { /***/ }), -/* 378 */ +/* 379 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(379); +const mimicFn = __webpack_require__(380); const calledFunctions = new WeakMap(); @@ -48758,7 +48953,7 @@ module.exports.callCount = fn => { /***/ }), -/* 379 */ +/* 380 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48778,13 +48973,13 @@ module.exports.default = mimicFn; /***/ }), -/* 380 */ +/* 381 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const spinners = Object.assign({}, __webpack_require__(381)); +const spinners = Object.assign({}, __webpack_require__(382)); const spinnersList = Object.keys(spinners); @@ -48802,18 +48997,18 @@ module.exports.default = spinners; /***/ }), -/* 381 */ +/* 382 */ /***/ (function(module) { module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"dots8Bit\":{\"interval\":80,\"frames\":[\"⠀\",\"⠁\",\"⠂\",\"⠃\",\"⠄\",\"⠅\",\"⠆\",\"⠇\",\"⡀\",\"⡁\",\"⡂\",\"⡃\",\"⡄\",\"⡅\",\"⡆\",\"⡇\",\"⠈\",\"⠉\",\"⠊\",\"⠋\",\"⠌\",\"⠍\",\"⠎\",\"⠏\",\"⡈\",\"⡉\",\"⡊\",\"⡋\",\"⡌\",\"⡍\",\"⡎\",\"⡏\",\"⠐\",\"⠑\",\"⠒\",\"⠓\",\"⠔\",\"⠕\",\"⠖\",\"⠗\",\"⡐\",\"⡑\",\"⡒\",\"⡓\",\"⡔\",\"⡕\",\"⡖\",\"⡗\",\"⠘\",\"⠙\",\"⠚\",\"⠛\",\"⠜\",\"⠝\",\"⠞\",\"⠟\",\"⡘\",\"⡙\",\"⡚\",\"⡛\",\"⡜\",\"⡝\",\"⡞\",\"⡟\",\"⠠\",\"⠡\",\"⠢\",\"⠣\",\"⠤\",\"⠥\",\"⠦\",\"⠧\",\"⡠\",\"⡡\",\"⡢\",\"⡣\",\"⡤\",\"⡥\",\"⡦\",\"⡧\",\"⠨\",\"⠩\",\"⠪\",\"⠫\",\"⠬\",\"⠭\",\"⠮\",\"⠯\",\"⡨\",\"⡩\",\"⡪\",\"⡫\",\"⡬\",\"⡭\",\"⡮\",\"⡯\",\"⠰\",\"⠱\",\"⠲\",\"⠳\",\"⠴\",\"⠵\",\"⠶\",\"⠷\",\"⡰\",\"⡱\",\"⡲\",\"⡳\",\"⡴\",\"⡵\",\"⡶\",\"⡷\",\"⠸\",\"⠹\",\"⠺\",\"⠻\",\"⠼\",\"⠽\",\"⠾\",\"⠿\",\"⡸\",\"⡹\",\"⡺\",\"⡻\",\"⡼\",\"⡽\",\"⡾\",\"⡿\",\"⢀\",\"⢁\",\"⢂\",\"⢃\",\"⢄\",\"⢅\",\"⢆\",\"⢇\",\"⣀\",\"⣁\",\"⣂\",\"⣃\",\"⣄\",\"⣅\",\"⣆\",\"⣇\",\"⢈\",\"⢉\",\"⢊\",\"⢋\",\"⢌\",\"⢍\",\"⢎\",\"⢏\",\"⣈\",\"⣉\",\"⣊\",\"⣋\",\"⣌\",\"⣍\",\"⣎\",\"⣏\",\"⢐\",\"⢑\",\"⢒\",\"⢓\",\"⢔\",\"⢕\",\"⢖\",\"⢗\",\"⣐\",\"⣑\",\"⣒\",\"⣓\",\"⣔\",\"⣕\",\"⣖\",\"⣗\",\"⢘\",\"⢙\",\"⢚\",\"⢛\",\"⢜\",\"⢝\",\"⢞\",\"⢟\",\"⣘\",\"⣙\",\"⣚\",\"⣛\",\"⣜\",\"⣝\",\"⣞\",\"⣟\",\"⢠\",\"⢡\",\"⢢\",\"⢣\",\"⢤\",\"⢥\",\"⢦\",\"⢧\",\"⣠\",\"⣡\",\"⣢\",\"⣣\",\"⣤\",\"⣥\",\"⣦\",\"⣧\",\"⢨\",\"⢩\",\"⢪\",\"⢫\",\"⢬\",\"⢭\",\"⢮\",\"⢯\",\"⣨\",\"⣩\",\"⣪\",\"⣫\",\"⣬\",\"⣭\",\"⣮\",\"⣯\",\"⢰\",\"⢱\",\"⢲\",\"⢳\",\"⢴\",\"⢵\",\"⢶\",\"⢷\",\"⣰\",\"⣱\",\"⣲\",\"⣳\",\"⣴\",\"⣵\",\"⣶\",\"⣷\",\"⢸\",\"⢹\",\"⢺\",\"⢻\",\"⢼\",\"⢽\",\"⢾\",\"⢿\",\"⣸\",\"⣹\",\"⣺\",\"⣻\",\"⣼\",\"⣽\",\"⣾\",\"⣿\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕛 \",\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"material\":{\"interval\":17,\"frames\":[\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███████▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"██████████▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"█████████████▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁██████████████▁▁▁▁\",\"▁▁▁██████████████▁▁▁\",\"▁▁▁▁█████████████▁▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁▁█████████████▁▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁▁███████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁▁█████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]},\"grenade\":{\"interval\":80,\"frames\":[\"، \",\"′ \",\" ´ \",\" ‾ \",\" ⸌\",\" ⸊\",\" |\",\" ⁎\",\" ⁕\",\" ෴ \",\" ⁓\",\" \",\" \",\" \"]},\"point\":{\"interval\":125,\"frames\":[\"∙∙∙\",\"●∙∙\",\"∙●∙\",\"∙∙●\",\"∙∙∙\"]},\"layer\":{\"interval\":150,\"frames\":[\"-\",\"=\",\"≡\"]},\"betaWave\":{\"interval\":80,\"frames\":[\"ρββββββ\",\"βρβββββ\",\"ββρββββ\",\"βββρβββ\",\"ββββρββ\",\"βββββρβ\",\"ββββββρ\"]}}"); /***/ }), -/* 382 */ +/* 383 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(383); +const chalk = __webpack_require__(384); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -48835,16 +49030,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 383 */ +/* 384 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(179); -const ansiStyles = __webpack_require__(384); -const stdoutColor = __webpack_require__(389).stdout; +const ansiStyles = __webpack_require__(385); +const stdoutColor = __webpack_require__(390).stdout; -const template = __webpack_require__(390); +const template = __webpack_require__(391); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -49070,12 +49265,12 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 384 */ +/* 385 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(module) { -const colorConvert = __webpack_require__(385); +const colorConvert = __webpack_require__(386); const wrapAnsi16 = (fn, offset) => function () { const code = fn.apply(colorConvert, arguments); @@ -49243,11 +49438,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 385 */ +/* 386 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(386); -var route = __webpack_require__(388); +var conversions = __webpack_require__(387); +var route = __webpack_require__(389); var convert = {}; @@ -49327,11 +49522,11 @@ module.exports = convert; /***/ }), -/* 386 */ +/* 387 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ -var cssKeywords = __webpack_require__(387); +var cssKeywords = __webpack_require__(388); // NOTE: conversions should only return primitive values (i.e. arrays, or // values that give correct `typeof` results). @@ -50201,7 +50396,7 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 387 */ +/* 388 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50360,10 +50555,10 @@ module.exports = { /***/ }), -/* 388 */ +/* 389 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(386); +var conversions = __webpack_require__(387); /* this function routes a model to all other models. @@ -50463,7 +50658,7 @@ module.exports = function (fromModel) { /***/ }), -/* 389 */ +/* 390 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50601,7 +50796,7 @@ module.exports = { /***/ }), -/* 390 */ +/* 391 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50736,18 +50931,18 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 391 */ +/* 392 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(392); +const ansiRegex = __webpack_require__(393); module.exports = string => typeof string === 'string' ? string.replace(ansiRegex(), '') : string; /***/ }), -/* 392 */ +/* 393 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50764,14 +50959,14 @@ module.exports = ({onlyFirst = false} = {}) => { /***/ }), -/* 393 */ +/* 394 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var defaults = __webpack_require__(394) -var combining = __webpack_require__(396) +var defaults = __webpack_require__(395) +var combining = __webpack_require__(397) var DEFAULTS = { nul: 0, @@ -50870,10 +51065,10 @@ function bisearch(ucs) { /***/ }), -/* 394 */ +/* 395 */ /***/ (function(module, exports, __webpack_require__) { -var clone = __webpack_require__(395); +var clone = __webpack_require__(396); module.exports = function(options, defaults) { options = options || {}; @@ -50888,7 +51083,7 @@ module.exports = function(options, defaults) { }; /***/ }), -/* 395 */ +/* 396 */ /***/ (function(module, exports, __webpack_require__) { var clone = (function() { @@ -51060,7 +51255,7 @@ if ( true && module.exports) { /***/ }), -/* 396 */ +/* 397 */ /***/ (function(module, exports) { module.exports = [ @@ -51116,7 +51311,7 @@ module.exports = [ /***/ }), -/* 397 */ +/* 398 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51132,7 +51327,7 @@ module.exports = ({stream = process.stdout} = {}) => { /***/ }), -/* 398 */ +/* 399 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -51283,7 +51478,7 @@ MuteStream.prototype.close = proxy('close') /***/ }), -/* 399 */ +/* 400 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51344,7 +51539,7 @@ const RunCommand = { }; /***/ }), -/* 400 */ +/* 401 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51354,7 +51549,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(144); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(145); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(146); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(401); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(402); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -51439,14 +51634,14 @@ const WatchCommand = { }; /***/ }), -/* 401 */ +/* 402 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "waitUntilWatchIsReady", function() { return waitUntilWatchIsReady; }); /* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8); -/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(402); +/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(403); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -51513,141 +51708,141 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 402 */ +/* 403 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(403); +/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(404); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__["audit"]; }); -/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(404); +/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(405); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__["auditTime"]; }); -/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(405); +/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(406); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__["buffer"]; }); -/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(406); +/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(407); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__["bufferCount"]; }); -/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(407); +/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(408); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__["bufferTime"]; }); -/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(408); +/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(409); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__["bufferToggle"]; }); -/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(409); +/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(410); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__["bufferWhen"]; }); -/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(410); +/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(411); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__["catchError"]; }); -/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(411); +/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(412); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__["combineAll"]; }); -/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(412); +/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(413); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__["combineLatest"]; }); -/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(413); +/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(414); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__["concat"]; }); /* harmony import */ var _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(80); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__["concatAll"]; }); -/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(414); +/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(415); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__["concatMap"]; }); -/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(415); +/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(416); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__["concatMapTo"]; }); -/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(416); +/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(417); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "count", function() { return _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__["count"]; }); -/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(417); +/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(418); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__["debounce"]; }); -/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(418); +/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(419); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__["debounceTime"]; }); -/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(419); +/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(420); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__["defaultIfEmpty"]; }); -/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(420); +/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(421); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__["delay"]; }); -/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(422); +/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(423); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__["delayWhen"]; }); -/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(423); +/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(424); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__["dematerialize"]; }); -/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(424); +/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(425); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__["distinct"]; }); -/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(425); +/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(426); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__["distinctUntilChanged"]; }); -/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(426); +/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(427); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__["distinctUntilKeyChanged"]; }); -/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(427); +/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(428); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__["elementAt"]; }); -/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(430); +/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(431); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__["endWith"]; }); -/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(431); +/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(432); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "every", function() { return _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__["every"]; }); -/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(432); +/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(433); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__["exhaust"]; }); -/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(433); +/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(434); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__["exhaustMap"]; }); -/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(434); +/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(435); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__["expand"]; }); /* harmony import */ var _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(105); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__["filter"]; }); -/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(435); +/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(436); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__["finalize"]; }); -/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(436); +/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(437); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "find", function() { return _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__["find"]; }); -/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(437); +/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(438); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__["findIndex"]; }); -/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(438); +/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(439); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "first", function() { return _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__["first"]; }); /* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(31); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__["groupBy"]; }); -/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(439); +/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(440); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__["ignoreElements"]; }); -/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(440); +/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(441); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__["isEmpty"]; }); -/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(441); +/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(442); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "last", function() { return _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__["last"]; }); /* harmony import */ var _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(66); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "map", function() { return _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__["map"]; }); -/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(443); +/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(444); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__["mapTo"]; }); -/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(444); +/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(445); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__["materialize"]; }); -/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(445); +/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(446); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "max", function() { return _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__["max"]; }); -/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(448); +/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(449); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__["merge"]; }); /* harmony import */ var _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(81); @@ -51658,175 +51853,175 @@ __webpack_require__.r(__webpack_exports__); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "flatMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["flatMap"]; }); -/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(449); +/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(450); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__["mergeMapTo"]; }); -/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(450); +/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(451); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__["mergeScan"]; }); -/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(451); +/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(452); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "min", function() { return _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__["min"]; }); -/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(452); +/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(453); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__["multicast"]; }); /* harmony import */ var _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(41); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__["observeOn"]; }); -/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(453); +/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(454); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__["onErrorResumeNext"]; }); -/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(454); +/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(455); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__["pairwise"]; }); -/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(455); +/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(456); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__["partition"]; }); -/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(456); +/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(457); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__["pluck"]; }); -/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(457); +/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(458); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__["publish"]; }); -/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(458); +/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(459); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__["publishBehavior"]; }); -/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(459); +/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(460); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__["publishLast"]; }); -/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(460); +/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(461); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__["publishReplay"]; }); -/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(461); +/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(462); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__["race"]; }); -/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(446); +/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(447); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__["reduce"]; }); -/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(462); +/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(463); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__["repeat"]; }); -/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(463); +/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(464); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__["repeatWhen"]; }); -/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(464); +/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(465); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__["retry"]; }); -/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(465); +/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(466); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__["retryWhen"]; }); /* harmony import */ var _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__ = __webpack_require__(30); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__["refCount"]; }); -/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(466); +/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(467); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__["sample"]; }); -/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(467); +/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(468); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__["sampleTime"]; }); -/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(447); +/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(448); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__["scan"]; }); -/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(468); +/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(469); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__["sequenceEqual"]; }); -/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(469); +/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(470); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "share", function() { return _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__["share"]; }); -/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(470); +/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(471); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__["shareReplay"]; }); -/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(471); +/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(472); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "single", function() { return _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__["single"]; }); -/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(472); +/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(473); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__["skip"]; }); -/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(473); +/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(474); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__["skipLast"]; }); -/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(474); +/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(475); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__["skipUntil"]; }); -/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(475); +/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(476); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__["skipWhile"]; }); -/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(476); +/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(477); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__["startWith"]; }); -/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(477); +/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(478); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__["subscribeOn"]; }); -/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(479); +/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(480); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__["switchAll"]; }); -/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(480); +/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(481); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__["switchMap"]; }); -/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(481); +/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(482); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__["switchMapTo"]; }); -/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(429); +/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(430); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "take", function() { return _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__["take"]; }); -/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(442); +/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(443); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__["takeLast"]; }); -/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(482); +/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(483); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__["takeUntil"]; }); -/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(483); +/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(484); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__["takeWhile"]; }); -/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(484); +/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(485); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__["tap"]; }); -/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(485); +/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(486); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__["throttle"]; }); -/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(486); +/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(487); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__["throttleTime"]; }); -/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(428); +/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(429); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__["throwIfEmpty"]; }); -/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(487); +/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(488); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__["timeInterval"]; }); -/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(488); +/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(489); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__["timeout"]; }); -/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(489); +/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(490); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__["timeoutWith"]; }); -/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(490); +/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(491); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__["timestamp"]; }); -/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(491); +/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(492); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__["toArray"]; }); -/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(492); +/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(493); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "window", function() { return _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__["window"]; }); -/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(493); +/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(494); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__["windowCount"]; }); -/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(494); +/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(495); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__["windowTime"]; }); -/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(495); +/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(496); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__["windowToggle"]; }); -/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(496); +/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(497); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__["windowWhen"]; }); -/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(497); +/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(498); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__["withLatestFrom"]; }); -/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(498); +/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(499); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__["zip"]; }); -/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(499); +/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(500); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__["zipAll"]; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ @@ -51937,7 +52132,7 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 403 */ +/* 404 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52016,14 +52211,14 @@ var AuditSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 404 */ +/* 405 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return auditTime; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(403); +/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(404); /* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(108); /** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */ @@ -52039,7 +52234,7 @@ function auditTime(duration, scheduler) { /***/ }), -/* 405 */ +/* 406 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52086,7 +52281,7 @@ var BufferSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 406 */ +/* 407 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52187,7 +52382,7 @@ var BufferSkipCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 407 */ +/* 408 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52348,7 +52543,7 @@ function dispatchBufferClose(arg) { /***/ }), -/* 408 */ +/* 409 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52467,7 +52662,7 @@ var BufferToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 409 */ +/* 410 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52560,7 +52755,7 @@ var BufferWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 410 */ +/* 411 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52620,7 +52815,7 @@ var CatchSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 411 */ +/* 412 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52636,7 +52831,7 @@ function combineAll(project) { /***/ }), -/* 412 */ +/* 413 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52668,7 +52863,7 @@ function combineLatest() { /***/ }), -/* 413 */ +/* 414 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52688,7 +52883,7 @@ function concat() { /***/ }), -/* 414 */ +/* 415 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52704,13 +52899,13 @@ function concatMap(project, resultSelector) { /***/ }), -/* 415 */ +/* 416 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return concatMapTo; }); -/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(414); +/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(415); /** PURE_IMPORTS_START _concatMap PURE_IMPORTS_END */ function concatMapTo(innerObservable, resultSelector) { @@ -52720,7 +52915,7 @@ function concatMapTo(innerObservable, resultSelector) { /***/ }), -/* 416 */ +/* 417 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52785,7 +52980,7 @@ var CountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 417 */ +/* 418 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52870,7 +53065,7 @@ var DebounceSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 418 */ +/* 419 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52946,7 +53141,7 @@ function dispatchNext(subscriber) { /***/ }), -/* 419 */ +/* 420 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52996,7 +53191,7 @@ var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 420 */ +/* 421 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53004,7 +53199,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return delay; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(421); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(422); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(11); /* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(42); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_Subscriber,_Notification PURE_IMPORTS_END */ @@ -53103,7 +53298,7 @@ var DelayMessage = /*@__PURE__*/ (function () { /***/ }), -/* 421 */ +/* 422 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53117,7 +53312,7 @@ function isDate(value) { /***/ }), -/* 422 */ +/* 423 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53263,7 +53458,7 @@ var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 423 */ +/* 424 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53301,7 +53496,7 @@ var DeMaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 424 */ +/* 425 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53377,7 +53572,7 @@ var DistinctSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 425 */ +/* 426 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53448,13 +53643,13 @@ var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 426 */ +/* 427 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return distinctUntilKeyChanged; }); -/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(425); +/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(426); /** PURE_IMPORTS_START _distinctUntilChanged PURE_IMPORTS_END */ function distinctUntilKeyChanged(key, compare) { @@ -53464,7 +53659,7 @@ function distinctUntilKeyChanged(key, compare) { /***/ }), -/* 427 */ +/* 428 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53472,9 +53667,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return elementAt; }); /* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(62); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(428); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(419); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(429); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(429); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(420); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(430); /** PURE_IMPORTS_START _util_ArgumentOutOfRangeError,_filter,_throwIfEmpty,_defaultIfEmpty,_take PURE_IMPORTS_END */ @@ -53496,7 +53691,7 @@ function elementAt(index, defaultValue) { /***/ }), -/* 428 */ +/* 429 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53562,7 +53757,7 @@ function defaultErrorFactory() { /***/ }), -/* 429 */ +/* 430 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53624,7 +53819,7 @@ var TakeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 430 */ +/* 431 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53646,7 +53841,7 @@ function endWith() { /***/ }), -/* 431 */ +/* 432 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53708,7 +53903,7 @@ var EverySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 432 */ +/* 433 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53762,7 +53957,7 @@ var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 433 */ +/* 434 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53856,7 +54051,7 @@ var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 434 */ +/* 435 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53968,7 +54163,7 @@ var ExpandSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 435 */ +/* 436 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54006,7 +54201,7 @@ var FinallySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 436 */ +/* 437 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54078,13 +54273,13 @@ var FindValueSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 437 */ +/* 438 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return findIndex; }); -/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(436); +/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(437); /** PURE_IMPORTS_START _operators_find PURE_IMPORTS_END */ function findIndex(predicate, thisArg) { @@ -54094,7 +54289,7 @@ function findIndex(predicate, thisArg) { /***/ }), -/* 438 */ +/* 439 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54102,9 +54297,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "first", function() { return first; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(429); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(419); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(428); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(430); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(420); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(429); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -54121,7 +54316,7 @@ function first(predicate, defaultValue) { /***/ }), -/* 439 */ +/* 440 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54158,7 +54353,7 @@ var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 440 */ +/* 441 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54202,7 +54397,7 @@ var IsEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 441 */ +/* 442 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54210,9 +54405,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "last", function() { return last; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(442); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(428); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(419); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(443); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(429); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(420); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -54229,7 +54424,7 @@ function last(predicate, defaultValue) { /***/ }), -/* 442 */ +/* 443 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54306,7 +54501,7 @@ var TakeLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 443 */ +/* 444 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54345,7 +54540,7 @@ var MapToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 444 */ +/* 445 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54395,13 +54590,13 @@ var MaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 445 */ +/* 446 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "max", function() { return max; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(446); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function max(comparer) { @@ -54414,15 +54609,15 @@ function max(comparer) { /***/ }), -/* 446 */ +/* 447 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return reduce; }); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(442); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(419); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(448); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(443); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(420); /* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(24); /** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */ @@ -54443,7 +54638,7 @@ function reduce(accumulator, seed) { /***/ }), -/* 447 */ +/* 448 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54525,7 +54720,7 @@ var ScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 448 */ +/* 449 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54545,7 +54740,7 @@ function merge() { /***/ }), -/* 449 */ +/* 450 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54570,7 +54765,7 @@ function mergeMapTo(innerObservable, resultSelector, concurrent) { /***/ }), -/* 450 */ +/* 451 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54679,13 +54874,13 @@ var MergeScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 451 */ +/* 452 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "min", function() { return min; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(446); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function min(comparer) { @@ -54698,7 +54893,7 @@ function min(comparer) { /***/ }), -/* 452 */ +/* 453 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54747,7 +54942,7 @@ var MulticastOperator = /*@__PURE__*/ (function () { /***/ }), -/* 453 */ +/* 454 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54837,7 +55032,7 @@ var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 454 */ +/* 455 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54885,7 +55080,7 @@ var PairwiseSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 455 */ +/* 456 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54908,7 +55103,7 @@ function partition(predicate, thisArg) { /***/ }), -/* 456 */ +/* 457 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54948,14 +55143,14 @@ function plucker(props, length) { /***/ }), -/* 457 */ +/* 458 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return publish; }); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(27); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _Subject,_multicast PURE_IMPORTS_END */ @@ -54968,14 +55163,14 @@ function publish(selector) { /***/ }), -/* 458 */ +/* 459 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return publishBehavior; }); /* harmony import */ var _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(32); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _BehaviorSubject,_multicast PURE_IMPORTS_END */ @@ -54986,14 +55181,14 @@ function publishBehavior(value) { /***/ }), -/* 459 */ +/* 460 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return publishLast; }); /* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(50); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _AsyncSubject,_multicast PURE_IMPORTS_END */ @@ -55004,14 +55199,14 @@ function publishLast() { /***/ }), -/* 460 */ +/* 461 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return publishReplay; }); /* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(33); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _ReplaySubject,_multicast PURE_IMPORTS_END */ @@ -55027,7 +55222,7 @@ function publishReplay(bufferSize, windowTime, selectorOrScheduler, scheduler) { /***/ }), -/* 461 */ +/* 462 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55054,7 +55249,7 @@ function race() { /***/ }), -/* 462 */ +/* 463 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55119,7 +55314,7 @@ var RepeatSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 463 */ +/* 464 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55213,7 +55408,7 @@ var RepeatWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 464 */ +/* 465 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55266,7 +55461,7 @@ var RetrySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 465 */ +/* 466 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55352,7 +55547,7 @@ var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 466 */ +/* 467 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55407,7 +55602,7 @@ var SampleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 467 */ +/* 468 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55467,7 +55662,7 @@ function dispatchNotification(state) { /***/ }), -/* 468 */ +/* 469 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55590,13 +55785,13 @@ var SequenceEqualCompareToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 469 */ +/* 470 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "share", function() { return share; }); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(453); /* harmony import */ var _refCount__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(27); /** PURE_IMPORTS_START _multicast,_refCount,_Subject PURE_IMPORTS_END */ @@ -55613,7 +55808,7 @@ function share() { /***/ }), -/* 470 */ +/* 471 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55682,7 +55877,7 @@ function shareReplayOperator(_a) { /***/ }), -/* 471 */ +/* 472 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55762,7 +55957,7 @@ var SingleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 472 */ +/* 473 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55804,7 +55999,7 @@ var SkipSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 473 */ +/* 474 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55866,7 +56061,7 @@ var SkipLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 474 */ +/* 475 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55923,7 +56118,7 @@ var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 475 */ +/* 476 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55979,7 +56174,7 @@ var SkipWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 476 */ +/* 477 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56008,13 +56203,13 @@ function startWith() { /***/ }), -/* 477 */ +/* 478 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return subscribeOn; }); -/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(478); +/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(479); /** PURE_IMPORTS_START _observable_SubscribeOnObservable PURE_IMPORTS_END */ function subscribeOn(scheduler, delay) { @@ -56039,7 +56234,7 @@ var SubscribeOnOperator = /*@__PURE__*/ (function () { /***/ }), -/* 478 */ +/* 479 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56103,13 +56298,13 @@ var SubscribeOnObservable = /*@__PURE__*/ (function (_super) { /***/ }), -/* 479 */ +/* 480 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return switchAll; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(480); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(481); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25); /** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */ @@ -56121,7 +56316,7 @@ function switchAll() { /***/ }), -/* 480 */ +/* 481 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56209,13 +56404,13 @@ var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 481 */ +/* 482 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return switchMapTo; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(480); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(481); /** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */ function switchMapTo(innerObservable, resultSelector) { @@ -56225,7 +56420,7 @@ function switchMapTo(innerObservable, resultSelector) { /***/ }), -/* 482 */ +/* 483 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56273,7 +56468,7 @@ var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 483 */ +/* 484 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56341,7 +56536,7 @@ var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 484 */ +/* 485 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56429,7 +56624,7 @@ var TapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 485 */ +/* 486 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56531,7 +56726,7 @@ var ThrottleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 486 */ +/* 487 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56540,7 +56735,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(55); -/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(485); +/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(486); /** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */ @@ -56629,7 +56824,7 @@ function dispatchNext(arg) { /***/ }), -/* 487 */ +/* 488 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56637,7 +56832,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return timeInterval; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeInterval", function() { return TimeInterval; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(447); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(448); /* harmony import */ var _observable_defer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(91); /* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(66); /** PURE_IMPORTS_START _scheduler_async,_scan,_observable_defer,_map PURE_IMPORTS_END */ @@ -56673,7 +56868,7 @@ var TimeInterval = /*@__PURE__*/ (function () { /***/ }), -/* 488 */ +/* 489 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56681,7 +56876,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return timeout; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); /* harmony import */ var _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(64); -/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(489); +/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(490); /* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(49); /** PURE_IMPORTS_START _scheduler_async,_util_TimeoutError,_timeoutWith,_observable_throwError PURE_IMPORTS_END */ @@ -56698,7 +56893,7 @@ function timeout(due, scheduler) { /***/ }), -/* 489 */ +/* 490 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56706,7 +56901,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return timeoutWith; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(421); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(422); /* harmony import */ var _innerSubscribe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(90); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_innerSubscribe PURE_IMPORTS_END */ @@ -56777,7 +56972,7 @@ var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 490 */ +/* 491 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56807,13 +57002,13 @@ var Timestamp = /*@__PURE__*/ (function () { /***/ }), -/* 491 */ +/* 492 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return toArray; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(446); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function toArrayReducer(arr, item, index) { @@ -56830,7 +57025,7 @@ function toArray() { /***/ }), -/* 492 */ +/* 493 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56908,7 +57103,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 493 */ +/* 494 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56998,7 +57193,7 @@ var WindowCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 494 */ +/* 495 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57168,7 +57363,7 @@ function dispatchWindowClose(state) { /***/ }), -/* 495 */ +/* 496 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57311,7 +57506,7 @@ var WindowToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 496 */ +/* 497 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57408,7 +57603,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 497 */ +/* 498 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57503,7 +57698,7 @@ var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 498 */ +/* 499 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57525,7 +57720,7 @@ function zip() { /***/ }), -/* 499 */ +/* 500 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57541,7 +57736,7 @@ function zipAll(project) { /***/ }), -/* 500 */ +/* 501 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57550,7 +57745,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(144); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(146); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(501); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(280); /* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(502); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } @@ -57632,159 +57827,6 @@ function toArray(value) { return Array.isArray(value) ? value : [value]; } -/***/ }), -/* 501 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "renderProjectsTree", function() { return renderProjectsTree; }); -/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(113); -/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - - -const projectKey = Symbol('__project'); -function renderProjectsTree(rootPath, projects) { - const projectsTree = buildProjectsTree(rootPath, projects); - return treeToString(createTreeStructure(projectsTree)); -} - -function treeToString(tree) { - return [tree.name].concat(childrenToStrings(tree.children, '')).join('\n'); -} - -function childrenToStrings(tree, treePrefix) { - if (tree === undefined) { - return []; - } - - let strings = []; - tree.forEach((node, index) => { - const isLastNode = tree.length - 1 === index; - const nodePrefix = isLastNode ? '└── ' : '├── '; - const childPrefix = isLastNode ? ' ' : '│ '; - const childrenPrefix = treePrefix + childPrefix; - strings.push(`${treePrefix}${nodePrefix}${node.name}`); - strings = strings.concat(childrenToStrings(node.children, childrenPrefix)); - }); - return strings; -} - -function createTreeStructure(tree) { - let name; - const children = []; - - for (const [dir, project] of tree.entries()) { - // This is a leaf node (aka a project) - if (typeof project === 'string') { - name = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(project); - continue; - } // If there's only one project and the key indicates it's a leaf node, we - // know that we're at a package folder that contains a package.json, so we - // "inline it" so we don't get unnecessary levels, i.e. we'll just see - // `foo` instead of `foo -> foo`. - - - if (project.size === 1 && project.has(projectKey)) { - const projectName = project.get(projectKey); - children.push({ - children: [], - name: dirOrProjectName(dir, projectName) - }); - continue; - } - - const subtree = createTreeStructure(project); // If the name is specified, we know there's a package at the "root" of the - // subtree itself. - - if (subtree.name !== undefined) { - const projectName = subtree.name; - children.push({ - children: subtree.children, - name: dirOrProjectName(dir, projectName) - }); - continue; - } // Special-case whenever we have one child, so we don't get unnecessary - // folders in the output. E.g. instead of `foo -> bar -> baz` we get - // `foo/bar/baz` instead. - - - if (subtree.children && subtree.children.length === 1) { - const child = subtree.children[0]; - const newName = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(path__WEBPACK_IMPORTED_MODULE_1___default.a.join(dir.toString(), child.name)); - children.push({ - children: child.children, - name: newName - }); - continue; - } - - children.push({ - children: subtree.children, - name: chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(dir.toString()) - }); - } - - return { - name, - children - }; -} - -function dirOrProjectName(dir, projectName) { - return dir === projectName ? chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(dir) : chalk__WEBPACK_IMPORTED_MODULE_0___default.a`{dim ${dir.toString()} ({reset.green ${projectName}})}`; -} - -function buildProjectsTree(rootPath, projects) { - const tree = new Map(); - - for (const project of projects.values()) { - if (rootPath === project.path) { - tree.set(projectKey, project.name); - } else { - const relativeProjectPath = path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(rootPath, project.path); - addProjectToTree(tree, relativeProjectPath.split(path__WEBPACK_IMPORTED_MODULE_1___default.a.sep), project); - } - } - - return tree; -} - -function addProjectToTree(tree, pathParts, project) { - if (pathParts.length === 0) { - tree.set(projectKey, project.name); - } else { - const [currentDir, ...rest] = pathParts; - - if (!tree.has(currentDir)) { - tree.set(currentDir, new Map()); - } - - const subtree = tree.get(currentDir); - addProjectToTree(subtree, rest, project); - } -} - /***/ }), /* 502 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { @@ -57796,7 +57838,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(503); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(361); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(362); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(275); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(146); @@ -58088,7 +58130,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(509); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(281); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(282); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); @@ -58239,7 +58281,7 @@ const os = __webpack_require__(121); const pAll = __webpack_require__(510); const arrify = __webpack_require__(512); const globby = __webpack_require__(513); -const isGlob = __webpack_require__(294); +const isGlob = __webpack_require__(295); const cpFile = __webpack_require__(713); const junk = __webpack_require__(723); const CpyError = __webpack_require__(724); @@ -58957,7 +58999,7 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); var globParent = __webpack_require__(521); -var isGlob = __webpack_require__(294); +var isGlob = __webpack_require__(295); var micromatch = __webpack_require__(524); var GLOBSTAR = '**'; /** @@ -59145,7 +59187,7 @@ module.exports = function globParent(str) { * Licensed under the MIT License. */ -var isExtglob = __webpack_require__(295); +var isExtglob = __webpack_require__(296); module.exports = function isGlob(str) { if (typeof str !== 'string' || str === '') { @@ -82800,7 +82842,7 @@ exports.flatten = flatten; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var merge2 = __webpack_require__(284); +var merge2 = __webpack_require__(285); /** * Merge multiple streams and propagate their errors into one stream in parallel. */ diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 8ffd86b84bf7..944fcf599863 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -4,6 +4,9 @@ "version": "1.0.0", "license": "Apache-2.0", "private": true, + "kibana": { + "devOnly": true + }, "scripts": { "build": "webpack", "kbn:watch": "webpack --watch --progress", diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 0fa3f355ae9d..9a8048d6fd10 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -26,7 +26,7 @@ import { ICommand } from './'; import { getAllChecksums } from '../utils/project_checksums'; import { BootstrapCacheFile } from '../utils/bootstrap_cache_file'; import { readYarnLock } from '../utils/yarn_lock'; -import { validateYarnLock } from '../utils/validate_yarn_lock'; +import { validateDependencies } from '../utils/validate_dependencies'; export const BootstrapCommand: ICommand = { description: 'Install dependencies and crosslink projects', @@ -59,7 +59,7 @@ export const BootstrapCommand: ICommand = { const yarnLock = await readYarnLock(kbn); if (options.validate) { - await validateYarnLock(kbn, yarnLock); + await validateDependencies(kbn, yarnLock); } await linkProjectExecutables(projects, projectGraph); diff --git a/packages/kbn-pm/src/utils/project.ts b/packages/kbn-pm/src/utils/project.ts index 8f45df52c7a2..4e4d76544aa3 100644 --- a/packages/kbn-pm/src/utils/project.ts +++ b/packages/kbn-pm/src/utils/project.ts @@ -153,6 +153,10 @@ export class Project { return (this.json.kibana && this.json.kibana.clean) || {}; } + public isFlaggedAsDevOnly() { + return !!(this.json.kibana && this.json.kibana.devOnly); + } + public hasScript(name: string) { return name in this.scripts; } diff --git a/packages/kbn-pm/src/utils/projects_tree.ts b/packages/kbn-pm/src/utils/projects_tree.ts index c7a13ce2de34..4ba000bc1b15 100644 --- a/packages/kbn-pm/src/utils/projects_tree.ts +++ b/packages/kbn-pm/src/utils/projects_tree.ts @@ -29,7 +29,7 @@ export function renderProjectsTree(rootPath: string, projects: Map {} -function treeToString(tree: ITree) { +export function treeToString(tree: ITree) { return [tree.name].concat(childrenToStrings(tree.children, '')).join('\n'); } diff --git a/packages/kbn-pm/src/utils/validate_yarn_lock.ts b/packages/kbn-pm/src/utils/validate_dependencies.ts similarity index 77% rename from packages/kbn-pm/src/utils/validate_yarn_lock.ts rename to packages/kbn-pm/src/utils/validate_dependencies.ts index ec853a3a958f..045d6332dcc2 100644 --- a/packages/kbn-pm/src/utils/validate_yarn_lock.ts +++ b/packages/kbn-pm/src/utils/validate_dependencies.ts @@ -20,14 +20,16 @@ // @ts-expect-error published types are useless import { stringify as stringifyLockfile } from '@yarnpkg/lockfile'; import dedent from 'dedent'; +import chalk from 'chalk'; import { writeFile } from './fs'; import { Kibana } from './kibana'; import { YarnLock } from './yarn_lock'; import { log } from './log'; import { Project } from './project'; +import { ITree, treeToString } from './projects_tree'; -export async function validateYarnLock(kbn: Kibana, yarnLock: YarnLock) { +export async function validateDependencies(kbn: Kibana, yarnLock: YarnLock) { // look through all of the packages in the yarn.lock file to see if // we have accidentally installed multiple lodash v4 versions const lodash4Versions = new Set(); @@ -157,5 +159,45 @@ export async function validateYarnLock(kbn: Kibana, yarnLock: YarnLock) { process.exit(1); } + // look for packages that have the the `kibana.devOnly` flag in their package.json + // and make sure they aren't included in the production dependencies of Kibana + const devOnlyProjectsInProduction = getDevOnlyProductionDepsTree(kbn, 'kibana'); + if (devOnlyProjectsInProduction) { + log.error(dedent` + Some of the packages in the production dependency chain for Kibana and X-Pack are + flagged with "kibana.devOnly" in their package.json. Please check changes made to + packages and their dependencies to ensure they don't end up in production. + + The devOnly dependencies that are being dependend on in production are: + + ${treeToString(devOnlyProjectsInProduction).split('\n').join('\n ')} + `); + + process.exit(1); + } + log.success('yarn.lock analysis completed without any issues'); } + +function getDevOnlyProductionDepsTree(kbn: Kibana, projectName: string) { + const project = kbn.getProject(projectName); + const childProjectNames = [ + ...Object.keys(project.productionDependencies).filter((name) => kbn.hasProject(name)), + ...(projectName === 'kibana' ? ['x-pack'] : []), + ]; + + const children = childProjectNames + .map((n) => getDevOnlyProductionDepsTree(kbn, n)) + .filter((t): t is ITree => !!t); + + if (!children.length && !project.isFlaggedAsDevOnly()) { + return; + } + + const tree: ITree = { + name: project.isFlaggedAsDevOnly() ? chalk.red.bold(projectName) : projectName, + children, + }; + + return tree; +} diff --git a/packages/kbn-release-notes/package.json b/packages/kbn-release-notes/package.json index 268530c22399..e3306b7a5491 100644 --- a/packages/kbn-release-notes/package.json +++ b/packages/kbn-release-notes/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "license": "Apache-2.0", "main": "target/index.js", + "kibana": { + "devOnly": true + }, "scripts": { "kbn:bootstrap": "tsc", "kbn:watch": "tsc --watch" diff --git a/packages/kbn-std/package.json b/packages/kbn-std/package.json index a931dd3f3154..8a5e885c456c 100644 --- a/packages/kbn-std/package.json +++ b/packages/kbn-std/package.json @@ -9,12 +9,12 @@ "build": "tsc", "kbn:bootstrap": "yarn build" }, + "dependencies": { + "lodash": "^4.17.20" + }, "devDependencies": { + "@kbn/utility-types": "1.0.0", "typescript": "4.0.2", "tsd": "^0.13.1" - }, - "dependencies": { - "@kbn/utility-types": "1.0.0", - "lodash": "^4.17.20" } } diff --git a/packages/kbn-std/src/rxjs_7.test.ts b/packages/kbn-std/src/rxjs_7.test.ts index 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-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index 1fad9e2a3e08..dc2647b7b575 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -20,7 +20,12 @@ import { StorybookConfig } from '@storybook/core/types'; export const defaultConfig: StorybookConfig = { - addons: ['@kbn/storybook/preset', '@storybook/addon-knobs', '@storybook/addon-essentials'], + addons: [ + '@kbn/storybook/preset', + '@storybook/addon-a11y', + '@storybook/addon-knobs', + '@storybook/addon-essentials', + ], stories: ['../**/*.stories.tsx'], typescript: { reactDocgen: false, diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index 58359159e950..cf0bb1262ea7 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -4,34 +4,13 @@ "private": true, "license": "Apache-2.0", "main": "./target/index.js", - "dependencies": { + "devDependencies": { "@kbn/dev-utils": "1.0.0", - "@storybook/addon-actions": "^6.0.16", - "@storybook/addon-essentials": "^6.0.16", - "@storybook/addon-knobs": "^6.0.16", - "@storybook/addon-storyshots": "^6.0.16", - "@storybook/core": "^6.0.16", - "@storybook/react": "^6.0.16", - "@storybook/theming": "^6.0.16", "@types/loader-utils": "^1.1.3", - "@types/webpack": "^4.41.3", - "@types/webpack-env": "^1.15.2", - "@types/webpack-merge": "^4.1.5", - "@kbn/utils": "1.0.0", - "babel-loader": "^8.0.6", - "copy-webpack-plugin": "^6.0.2", - "fast-glob": "2.2.7", - "glob-watcher": "5.0.3", - "jest-specific-snapshot": "2.0.0", - "jest-styled-components": "^7.0.2", - "mkdirp": "0.5.1", - "mini-css-extract-plugin": "0.8.0", - "normalize-path": "^3.0.0", - "react-docgen-typescript-loader": "^3.1.1", - "rxjs": "^6.5.5", - "serve-static": "1.14.1", - "styled-components": "^5.1.0", - "webpack": "^4.41.5" + "@types/webpack-merge": "^4.1.5" + }, + "kibana": { + "devOnly": true }, "scripts": { "build": "tsc", diff --git a/packages/kbn-storybook/yarn.lock b/packages/kbn-storybook/yarn.lock deleted file mode 120000 index 3f82ebc9cdba..000000000000 --- a/packages/kbn-storybook/yarn.lock +++ /dev/null @@ -1 +0,0 @@ -../../yarn.lock \ No newline at end of file diff --git a/packages/kbn-telemetry-tools/package.json b/packages/kbn-telemetry-tools/package.json index 4318cbcf2ec4..cda2998901d5 100644 --- a/packages/kbn-telemetry-tools/package.json +++ b/packages/kbn-telemetry-tools/package.json @@ -4,6 +4,9 @@ "license": "Apache-2.0", "main": "./target/index.js", "private": true, + "kibana": { + "devOnly": true + }, "scripts": { "build": "babel src --out-dir target --delete-dir-on-start --extensions .ts --source-maps=inline", "kbn:bootstrap": "yarn build", diff --git a/packages/kbn-test-subj-selector/package.json b/packages/kbn-test-subj-selector/package.json index 82a26dc4807b..b823c68f9560 100755 --- a/packages/kbn-test-subj-selector/package.json +++ b/packages/kbn-test-subj-selector/package.json @@ -5,5 +5,8 @@ "main": "index.js", "keywords": [], "author": "Spencer Alger ", - "license": "Apache-2.0" + "license": "Apache-2.0", + "kibana": { + "devOnly": true + } } diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 8422c34c9ed0..24096a41a5fd 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -9,6 +9,9 @@ "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, + "kibana": { + "devOnly": true + }, "devDependencies": { "@babel/cli": "^7.10.5", "@jest/types": "^26.5.2", diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap b/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap index 809b635369a3..cd3174d13c3e 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap @@ -26,6 +26,7 @@ Object { "debug": true, "esFrom": "snapshot", "extraKbnOpts": undefined, + "useDefaultConfig": true, } `; @@ -35,6 +36,7 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "useDefaultConfig": true, } `; @@ -49,6 +51,7 @@ Object { "extraKbnOpts": Object { "server.foo": "bar", }, + "useDefaultConfig": true, } `; @@ -59,6 +62,7 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "quiet": true, + "useDefaultConfig": true, } `; @@ -69,6 +73,7 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "silent": true, + "useDefaultConfig": true, } `; @@ -78,6 +83,7 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "useDefaultConfig": true, } `; @@ -87,6 +93,7 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "useDefaultConfig": true, } `; @@ -97,6 +104,7 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "installDir": "foo", + "useDefaultConfig": true, } `; @@ -106,6 +114,7 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "useDefaultConfig": true, "verbose": true, } `; @@ -116,5 +125,6 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "useDefaultConfig": true, } `; diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/args.js b/packages/kbn-test/src/functional_tests/cli/start_servers/args.js index e604e86de8b3..2b32726557ba 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/args.js +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/args.js @@ -75,7 +75,8 @@ export function displayHelp() { export function processOptions(userOptions, defaultConfigPath) { validateOptions(userOptions); - const config = userOptions.config || defaultConfigPath; + const useDefaultConfig = !userOptions.config; + const config = useDefaultConfig ? defaultConfigPath : userOptions.config; if (!config) { throw new Error(`functional_tests_server: config is required`); @@ -100,6 +101,7 @@ export function processOptions(userOptions, defaultConfigPath) { return { ...userOptions, config: resolve(config), + useDefaultConfig, createLogger, extraKbnOpts: userOptions._, }; diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 7d4fc84d47bd..c2833cbbda33 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -36,6 +36,13 @@ import { readConfigFile } from '../functional_test_runner/lib'; const makeSuccessMessage = (options) => { const installDirFlag = options.installDir ? ` --kibana-install-dir=${options.installDir}` : ''; + const configPaths = Array.isArray(options.config) ? options.config : [options.config]; + const pathsMessage = options.useDefaultConfig + ? '' + : configPaths + .map((path) => relative(process.cwd(), path)) + .map((path) => ` --config ${path}`) + .join(''); return ( '\n\n' + @@ -43,7 +50,7 @@ const makeSuccessMessage = (options) => { Elasticsearch and Kibana are ready for functional testing. Start the functional tests in another terminal session by running this command from this directory: - node ${relative(process.cwd(), KIBANA_FTR_SCRIPT)}${installDirFlag} + node ${relative(process.cwd(), KIBANA_FTR_SCRIPT)}${installDirFlag}${pathsMessage} ` + '\n\n' ); 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/packages/kbn-utility-types/package.json b/packages/kbn-utility-types/package.json index d1d7a1c0397c..6b531efcebac 100644 --- a/packages/kbn-utility-types/package.json +++ b/packages/kbn-utility-types/package.json @@ -5,6 +5,9 @@ "license": "Apache-2.0", "main": "target", "types": "target/index.d.ts", + "kibana": { + "devOnly": true + }, "scripts": { "build": "tsc", "kbn:bootstrap": "tsc", diff --git a/renovate.json5 b/renovate.json5 index 17391c2f8382..0d4627e35950 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -2,9 +2,9 @@ extends: [ 'config:base', ], - includePaths: [ - 'package.json', - 'x-pack/package.json', + ignorePaths: [ + '**/__fixtures__/**', + '**/fixtures/**' ], baseBranches: [ 'master', diff --git a/src/plugins/timelion/public/flot/index.js b/scripts/find_plugin_circular_deps.js similarity index 78% rename from src/plugins/timelion/public/flot/index.js rename to scripts/find_plugin_circular_deps.js index a066fd3ab860..6b0661cb841b 100644 --- a/src/plugins/timelion/public/flot/index.js +++ b/scripts/find_plugin_circular_deps.js @@ -17,10 +17,5 @@ * under the License. */ -import './jquery.flot'; -import './jquery.flot.time'; -import './jquery.flot.symbol'; -import './jquery.flot.crosshair'; -import './jquery.flot.selection'; -import './jquery.flot.stack'; -import './jquery.flot.axislabels'; +require('../src/setup_node_env'); +require('../src/dev/run_find_plugin_circular_deps'); diff --git a/src/plugins/vis_type_timelion/public/flot/index.js b/scripts/find_plugins_without_ts_refs.js similarity index 78% rename from src/plugins/vis_type_timelion/public/flot/index.js rename to scripts/find_plugins_without_ts_refs.js index a066fd3ab860..5f543a045f73 100644 --- a/src/plugins/vis_type_timelion/public/flot/index.js +++ b/scripts/find_plugins_without_ts_refs.js @@ -17,10 +17,5 @@ * under the License. */ -import './jquery.flot'; -import './jquery.flot.time'; -import './jquery.flot.symbol'; -import './jquery.flot.crosshair'; -import './jquery.flot.selection'; -import './jquery.flot.stack'; -import './jquery.flot.axislabels'; +require('../src/setup_node_env'); +require('../src/dev/run_find_plugins_without_ts_refs'); diff --git a/src/cli_keystore/add.js b/src/cli_keystore/add.js index 232392f34c63..d88256da1aa5 100644 --- a/src/cli_keystore/add.js +++ b/src/cli_keystore/add.js @@ -59,7 +59,15 @@ export async function add(keystore, key, options = {}) { value = await question(`Enter value for ${key}`, { mask: '*' }); } - keystore.add(key, value.trim()); + const parsedValue = value.trim(); + let parsedJsonValue; + try { + parsedJsonValue = JSON.parse(parsedValue); + } catch { + // noop, only treat value as json if it parses as JSON + } + + keystore.add(key, parsedJsonValue ?? parsedValue); keystore.save(); } diff --git a/src/cli_keystore/add.test.js b/src/cli_keystore/add.test.js index f1adee8879bc..ba381ca2f3e1 100644 --- a/src/cli_keystore/add.test.js +++ b/src/cli_keystore/add.test.js @@ -129,6 +129,17 @@ describe('Kibana keystore', () => { expect(keystore.data.foo).toEqual('bar'); }); + it('parses JSON values', async () => { + prompt.question.returns(Promise.resolve('["bar"]\n')); + + const keystore = new Keystore('/data/test.keystore'); + sandbox.stub(keystore, 'save'); + + await add(keystore, 'foo'); + + expect(keystore.data.foo).toEqual(['bar']); + }); + it('persists updated keystore', async () => { prompt.question.returns(Promise.resolve('bar\n')); 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/application/application_service.tsx b/src/core/public/application/application_service.tsx index 0d08f6f3007b..4d54d4831698 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -242,11 +242,17 @@ export class ApplicationService { appId, { path, state, replace = false }: NavigateToAppOptions = {} ) => { - if (await this.shouldNavigate(overlays)) { + const currentAppId = this.currentAppId$.value; + const navigatingToSameApp = currentAppId === appId; + const shouldNavigate = navigatingToSameApp ? true : await this.shouldNavigate(overlays); + + if (shouldNavigate) { if (path === undefined) { path = applications$.value.get(appId)?.defaultPath; } - this.appInternalStates.delete(this.currentAppId$.value!); + if (!navigatingToSameApp) { + this.appInternalStates.delete(this.currentAppId$.value!); + } this.navigate!(getAppUrl(availableMounters, appId, path), state, replace); this.currentAppId$.next(appId); } diff --git a/src/core/public/application/integration_tests/application_service.test.tsx b/src/core/public/application/integration_tests/application_service.test.tsx index d28486928b7e..82933576bc49 100644 --- a/src/core/public/application/integration_tests/application_service.test.tsx +++ b/src/core/public/application/integration_tests/application_service.test.tsx @@ -258,6 +258,34 @@ describe('ApplicationService', () => { expect(history.entries.length).toEqual(2); expect(history.entries[1].pathname).toEqual('/app/app1'); }); + + it('does not trigger navigation check if navigating to the current app', async () => { + startDeps.overlays.openConfirm.mockResolvedValue(false); + + const { register } = service.setup(setupDeps); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: ({ onAppLeave }: AppMountParameters) => { + onAppLeave((actions) => actions.confirm('confirmation-message', 'confirmation-title')); + return () => undefined; + }, + }); + + const { navigateToApp, getComponent } = await service.start(startDeps); + + update = createRenderer(getComponent()); + + await act(async () => { + await navigate('/app/app1'); + await navigateToApp('app1', { path: '/internal-path' }); + }); + + expect(startDeps.overlays.openConfirm).not.toHaveBeenCalled(); + expect(history.entries.length).toEqual(3); + expect(history.entries[2].pathname).toEqual('/app/app1/internal-path'); + }); }); describe('registering action menus', () => { @@ -331,6 +359,48 @@ describe('ApplicationService', () => { expect(await getValue(currentActionMenu$)).toBe(mounter2); }); + it('does not update the observable value when navigating to the current app', async () => { + const { register } = service.setup(setupDeps); + + let initialMount = true; + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + if (initialMount) { + setHeaderActionMenu(mounter1); + initialMount = false; + } + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + let mountedMenuCount = 0; + currentActionMenu$.subscribe(() => { + mountedMenuCount++; + }); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + // there is an initial 'undefined' emission + expect(mountedMenuCount).toBe(2); + }); + it('updates the observable value to undefined when switching to an application without action menu', async () => { const { register } = service.setup(setupDeps); diff --git a/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap b/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap index e6bf7e898d8c..10e6e9befe4f 100644 --- a/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap +++ b/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap @@ -1,19 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`kbnLoadingIndicator is hidden by default 1`] = ` - `; exports[`kbnLoadingIndicator is visible when loadingCount is > 0 1`] = ` - `; diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index e733c7fda5d5..5e563c406109 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -1715,17 +1715,10 @@ exports[`Header renders 1`] = ` } } href="/" - navLinks$={ + loadingCount$={ BehaviorSubject { "_isScalar": false, - "_value": Array [ - Object { - "baseUrl": "", - "href": "", - "id": "kibana", - "title": "kibana", - }, - ], + "_value": 0, "closed": false, "hasError": false, "isStopped": false, @@ -1767,6 +1760,25 @@ exports[`Header renders 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, + ], + "thrownError": null, + } + } + navLinks$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "baseUrl": "", + "href": "", + "id": "kibana", + "title": "kibana", + }, + ], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -1804,21 +1816,6 @@ exports[`Header renders 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - navigateToApp={[MockFunction]} - />, - , ], }, @@ -2821,17 +2818,10 @@ exports[`Header renders 1`] = ` } } href="/" - navLinks$={ + loadingCount$={ BehaviorSubject { "_isScalar": false, - "_value": Array [ - Object { - "baseUrl": "", - "href": "", - "id": "kibana", - "title": "kibana", - }, - ], + "_value": 0, "closed": false, "hasError": false, "isStopped": false, @@ -2873,6 +2863,25 @@ exports[`Header renders 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, + ], + "thrownError": null, + } + } + navLinks$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "baseUrl": "", + "href": "", + "id": "kibana", + "title": "kibana", + }, + ], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -2910,66 +2919,6 @@ exports[`Header renders 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - navigateToApp={[MockFunction]} - > - - - - - - -
- - - + +
+ + + - - + className="chrHeaderLogo__mark" + > + + + Elastic + + + + + +
diff --git a/src/core/public/chrome/ui/header/elastic_mark.tsx b/src/core/public/chrome/ui/header/elastic_mark.tsx new file mode 100644 index 000000000000..e4456e9b751f --- /dev/null +++ b/src/core/public/chrome/ui/header/elastic_mark.tsx @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { HTMLAttributes } from 'react'; + +export const ElasticMark = ({ ...props }: HTMLAttributes) => ( + + Elastic + + +); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index d0b39e362ecb..7089ec108727 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -112,8 +112,8 @@ export function Header({ forceNavigation$={observables.forceAppSwitcherNavigation$} navLinks$={observables.navLinks$} navigateToApp={application.navigateToApp} + loadingCount$={observables.loadingCount$} />, - , ], borders: 'none', }, diff --git a/src/core/public/chrome/ui/header/header_logo.scss b/src/core/public/chrome/ui/header/header_logo.scss new file mode 100644 index 000000000000..f75fd9cfa246 --- /dev/null +++ b/src/core/public/chrome/ui/header/header_logo.scss @@ -0,0 +1,4 @@ +.chrHeaderLogo__mark { + margin-left: $euiSizeS; + fill: $euiColorGhost; +} diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index 83e0c52ab3f3..df961ebb0983 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -17,13 +17,16 @@ * under the License. */ -import { EuiHeaderLogo } from '@elastic/eui'; +import './header_logo.scss'; import { i18n } from '@kbn/i18n'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import Url from 'url'; import { ChromeNavLink } from '../..'; +import { ElasticMark } from './elastic_mark'; +import { HttpStart } from '../../../http'; +import { LoadingIndicator } from '../loading_indicator'; function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { let current = element; @@ -90,23 +93,25 @@ interface Props { navLinks$: Observable; forceNavigation$: Observable; navigateToApp: (appId: string) => void; + loadingCount$?: ReturnType; } -export function HeaderLogo({ href, navigateToApp, ...observables }: Props) { +export function HeaderLogo({ href, navigateToApp, loadingCount$, ...observables }: Props) { const forceNavigation = useObservable(observables.forceNavigation$, false); const navLinks = useObservable(observables.navLinks$, []); return ( - onClick(e, forceNavigation, navLinks, navigateToApp)} + className="euiHeaderLogo" href={href} + data-test-subj="logo" aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', { - defaultMessage: 'Go to home page', + defaultMessage: 'Elastic home', })} > - Elastic - + + + ); } diff --git a/src/core/public/chrome/ui/loading_indicator.test.tsx b/src/core/public/chrome/ui/loading_indicator.test.tsx index ff56ca668ae0..2d45a3d07961 100644 --- a/src/core/public/chrome/ui/loading_indicator.test.tsx +++ b/src/core/public/chrome/ui/loading_indicator.test.tsx @@ -32,7 +32,10 @@ describe('kbnLoadingIndicator', () => { it('is visible when loadingCount is > 0', () => { const wrapper = shallow(); - expect(wrapper.prop('data-test-subj')).toBe('globalLoadingIndicator'); + // Pause the check beyond the 250ms delay that it has + setTimeout(() => { + expect(wrapper.prop('data-test-subj')).toBe('globalLoadingIndicator'); + }, 300); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/src/core/public/chrome/ui/loading_indicator.tsx b/src/core/public/chrome/ui/loading_indicator.tsx index ca3e95f722ec..25ec52e8dbb5 100644 --- a/src/core/public/chrome/ui/loading_indicator.tsx +++ b/src/core/public/chrome/ui/loading_indicator.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { EuiLoadingSpinner, EuiProgress } from '@elastic/eui'; +import { EuiLoadingSpinner, EuiProgress, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import classNames from 'classnames'; @@ -39,16 +39,26 @@ export class LoadingIndicator extends React.Component { - this.setState({ - visible: count > 0, - }); + if (this.increment > 1) { + clearTimeout(this.timer); + } + this.increment += this.increment; + this.timer = setTimeout(() => { + this.setState({ + visible: count > 0, + }); + }, 250); }); } componentWillUnmount() { if (this.loadingCountSubscription) { + clearTimeout(this.timer); this.loadingCountSubscription.unsubscribe(); this.loadingCountSubscription = undefined; } @@ -67,13 +77,27 @@ export class LoadingIndicator extends React.Component + ) : ( + + ); + + return !this.props.showAsBar ? ( + logo ) : ( { - const mocked: jest.Mocked = { - register: jest.fn(), - }; - return mocked; -}; - -const createAuditorMock = () => { - const mocked: jest.Mocked = { - add: jest.fn(), - withAuditScope: jest.fn(), - }; - return mocked; -}; - -const createStartContractMock = () => { - const mocked: jest.Mocked = { - asScoped: jest.fn(), - }; - mocked.asScoped.mockReturnValue(createAuditorMock()); - return mocked; -}; - -const createServiceMock = (): jest.Mocked> => ({ - setup: jest.fn().mockResolvedValue(createSetupContractMock()), - start: jest.fn().mockResolvedValue(createStartContractMock()), - stop: jest.fn(), -}); - -export const auditTrailServiceMock = { - create: createServiceMock, - createSetupContract: createSetupContractMock, - createStartContract: createStartContractMock, - createAuditorFactory: createStartContractMock, - createAuditor: createAuditorMock, -}; diff --git a/src/core/server/audit_trail/audit_trail_service.test.ts b/src/core/server/audit_trail/audit_trail_service.test.ts deleted file mode 100644 index 63b45b62275b..000000000000 --- a/src/core/server/audit_trail/audit_trail_service.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { AuditTrailService } from './audit_trail_service'; -import { AuditorFactory } from './types'; -import { mockCoreContext } from '../core_context.mock'; -import { httpServerMock } from '../http/http_server.mocks'; - -describe('AuditTrailService', () => { - const coreContext = mockCoreContext.create(); - - describe('#setup', () => { - describe('register', () => { - it('throws if registered the same auditor factory twice', () => { - const auditTrail = new AuditTrailService(coreContext); - const { register } = auditTrail.setup(); - const auditorFactory: AuditorFactory = { - asScoped() { - return { add: () => undefined, withAuditScope: (() => {}) as any }; - }, - }; - register(auditorFactory); - expect(() => register(auditorFactory)).toThrowErrorMatchingInlineSnapshot( - `"An auditor factory has been already registered"` - ); - }); - }); - }); - - describe('#start', () => { - describe('asScoped', () => { - it('initialize every auditor with a request', () => { - const scopedMock = jest.fn(() => ({ add: jest.fn(), withAuditScope: jest.fn() })); - const auditorFactory = { asScoped: scopedMock }; - - const auditTrail = new AuditTrailService(coreContext); - const { register } = auditTrail.setup(); - register(auditorFactory); - - const { asScoped } = auditTrail.start(); - const kibanaRequest = httpServerMock.createKibanaRequest(); - asScoped(kibanaRequest); - - expect(scopedMock).toHaveBeenCalledWith(kibanaRequest); - }); - - it('passes auditable event to an auditor', () => { - const addEventMock = jest.fn(); - const auditorFactory = { - asScoped() { - return { add: addEventMock, withAuditScope: jest.fn() }; - }, - }; - - const auditTrail = new AuditTrailService(coreContext); - const { register } = auditTrail.setup(); - register(auditorFactory); - - const { asScoped } = auditTrail.start(); - const kibanaRequest = httpServerMock.createKibanaRequest(); - const auditor = asScoped(kibanaRequest); - const message = { - type: 'foo', - message: 'bar', - }; - auditor.add(message); - - expect(addEventMock).toHaveBeenLastCalledWith(message); - }); - - describe('return the same auditor instance for the same KibanaRequest', () => { - const auditTrail = new AuditTrailService(coreContext); - auditTrail.setup(); - const { asScoped } = auditTrail.start(); - - const rawRequest1 = httpServerMock.createKibanaRequest(); - const rawRequest2 = httpServerMock.createKibanaRequest(); - expect(asScoped(rawRequest1)).toBe(asScoped(rawRequest1)); - expect(asScoped(rawRequest1)).not.toBe(asScoped(rawRequest2)); - }); - }); - }); -}); diff --git a/src/core/server/audit_trail/audit_trail_service.ts b/src/core/server/audit_trail/audit_trail_service.ts deleted file mode 100644 index f1841858dbc9..000000000000 --- a/src/core/server/audit_trail/audit_trail_service.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { CoreService } from '../../types'; -import { CoreContext } from '../core_context'; -import { Logger } from '../logging'; -import { KibanaRequest, LegacyRequest } from '../http'; -import { ensureRawRequest } from '../http/router'; -import { Auditor, AuditorFactory, AuditTrailSetup, AuditTrailStart } from './types'; - -const defaultAuditorFactory: AuditorFactory = { - asScoped() { - return { - add() {}, - withAuditScope() {}, - }; - }, -}; - -export class AuditTrailService implements CoreService { - private readonly log: Logger; - private auditor: AuditorFactory = defaultAuditorFactory; - private readonly auditors = new WeakMap(); - - constructor(core: CoreContext) { - this.log = core.logger.get('audit_trail'); - } - - setup() { - return { - register: (auditor: AuditorFactory) => { - if (this.auditor !== defaultAuditorFactory) { - throw new Error('An auditor factory has been already registered'); - } - this.auditor = auditor; - this.log.debug('An auditor factory has been registered'); - }, - }; - } - - start() { - return { - asScoped: (request: KibanaRequest) => { - const key = ensureRawRequest(request); - if (!this.auditors.has(key)) { - this.auditors.set(key, this.auditor!.asScoped(request)); - } - return this.auditors.get(key)!; - }, - }; - } - - stop() {} -} diff --git a/src/core/server/audit_trail/types.ts b/src/core/server/audit_trail/types.ts deleted file mode 100644 index b3c1fc3c222f..000000000000 --- a/src/core/server/audit_trail/types.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { KibanaRequest } from '../http'; - -/** - * Event to audit. - * @public - * - * @remarks - * Not a complete interface. - */ -export interface AuditableEvent { - message: string; - type: string; -} - -/** - * Provides methods to log user actions and access events. - * @public - */ -export interface Auditor { - /** - * Add a record to audit log. - * Service attaches to a log record: - * - metadata about an end-user initiating an operation - * - scope name, if presents - * - * @example - * How to add a record in audit log: - * ```typescript - * router.get({ path: '/my_endpoint', validate: false }, async (context, request, response) => { - * context.core.auditor.withAuditScope('my_plugin_operation'); - * const value = await context.core.elasticsearch.legacy.client.callAsCurrentUser('...'); - * context.core.add({ type: 'operation.type', message: 'perform an operation in ... endpoint' }); - * ``` - */ - add(event: AuditableEvent): void; - /** - * Add a high-level scope name for logged events. - * It helps to identify the root cause of low-level events. - */ - withAuditScope(name: string): void; -} - -/** - * Creates {@link Auditor} instance bound to the current user credentials. - * @public - */ -export interface AuditorFactory { - asScoped(request: KibanaRequest): Auditor; -} - -export interface AuditTrailSetup { - /** - * Register a custom {@link AuditorFactory} implementation. - */ - register(auditor: AuditorFactory): void; -} - -export type AuditTrailStart = AuditorFactory; diff --git a/src/core/server/core_route_handler_context.test.ts b/src/core/server/core_route_handler_context.test.ts index 563e337e6c7e..d4599d91c1b9 100644 --- a/src/core/server/core_route_handler_context.test.ts +++ b/src/core/server/core_route_handler_context.test.ts @@ -19,41 +19,6 @@ import { CoreRouteHandlerContext } from './core_route_handler_context'; import { coreMock, httpServerMock } from './mocks'; -describe('#auditor', () => { - test('returns the results of coreStart.audiTrail.asScoped', () => { - const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); - const context = new CoreRouteHandlerContext(coreStart, request); - - const auditor = context.auditor; - expect(auditor).toBe(coreStart.auditTrail.asScoped.mock.results[0].value); - }); - - test('lazily created', () => { - const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); - const context = new CoreRouteHandlerContext(coreStart, request); - - expect(coreStart.auditTrail.asScoped).not.toHaveBeenCalled(); - const auditor = context.auditor; - expect(coreStart.auditTrail.asScoped).toHaveBeenCalled(); - expect(auditor).toBeDefined(); - }); - - test('only creates one instance', () => { - const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); - const context = new CoreRouteHandlerContext(coreStart, request); - - const auditor1 = context.auditor; - const auditor2 = context.auditor; - expect(coreStart.auditTrail.asScoped.mock.calls.length).toBe(1); - const mockResult = coreStart.auditTrail.asScoped.mock.results[0].value; - expect(auditor1).toBe(mockResult); - expect(auditor2).toBe(mockResult); - }); -}); - describe('#elasticsearch', () => { describe('#client', () => { test('returns the results of coreStart.elasticsearch.client.asScoped', () => { diff --git a/src/core/server/core_route_handler_context.ts b/src/core/server/core_route_handler_context.ts index 8a182a523f52..520c5bd3f685 100644 --- a/src/core/server/core_route_handler_context.ts +++ b/src/core/server/core_route_handler_context.ts @@ -27,7 +27,6 @@ import { IScopedClusterClient, LegacyScopedClusterClient, } from './elasticsearch'; -import { Auditor } from './audit_trail'; import { InternalUiSettingsServiceStart, IUiSettingsClient } from './ui_settings'; class CoreElasticsearchRouteHandlerContext { @@ -99,8 +98,6 @@ class CoreUiSettingsRouteHandlerContext { } export class CoreRouteHandlerContext { - #auditor?: Auditor; - readonly elasticsearch: CoreElasticsearchRouteHandlerContext; readonly savedObjects: CoreSavedObjectsRouteHandlerContext; readonly uiSettings: CoreUiSettingsRouteHandlerContext; @@ -122,11 +119,4 @@ export class CoreRouteHandlerContext { this.savedObjects ); } - - public get auditor() { - if (this.#auditor == null) { - this.#auditor = this.coreStart.auditTrail.asScoped(this.request); - } - return this.#auditor; - } } diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index ce82410f6061..e527fdb91597 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -26,7 +26,6 @@ import { configServiceMock, getEnvOptions } from '../config/mocks'; import { CoreContext } from '../core_context'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { httpServiceMock } from '../http/http_service.mock'; -import { auditTrailServiceMock } from '../audit_trail/audit_trail_service.mock'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { elasticsearchServiceMock } from './elasticsearch_service.mock'; @@ -41,9 +40,6 @@ const configService = configServiceMock.create(); const setupDeps = { http: httpServiceMock.createInternalSetupContract(), }; -const startDeps = { - auditTrail: auditTrailServiceMock.createStartContract(), -}; configService.atPath.mockReturnValue( new BehaviorSubject({ hosts: ['http://1.2.3.4'], @@ -113,7 +109,6 @@ describe('#setup', () => { expect(MockLegacyClusterClient).toHaveBeenCalledWith( expect.objectContaining(customConfig), expect.objectContaining({ context: ['elasticsearch', 'some-custom-type'] }), - expect.any(Function), expect.any(Function) ); }); @@ -260,14 +255,14 @@ describe('#setup', () => { describe('#start', () => { it('throws if called before `setup`', async () => { - expect(() => elasticsearchService.start(startDeps)).rejects.toMatchInlineSnapshot( + expect(() => elasticsearchService.start()).rejects.toMatchInlineSnapshot( `[Error: ElasticsearchService needs to be setup before calling start]` ); }); it('returns elasticsearch client as a part of the contract', async () => { await elasticsearchService.setup(setupDeps); - const startContract = await elasticsearchService.start(startDeps); + const startContract = await elasticsearchService.start(); const client = startContract.client; expect(client.asInternalUser).toBe(mockClusterClientInstance.asInternalUser); @@ -276,7 +271,7 @@ describe('#start', () => { describe('#createClient', () => { it('allows to specify config properties', async () => { await elasticsearchService.setup(setupDeps); - const startContract = await elasticsearchService.start(startDeps); + const startContract = await elasticsearchService.start(); // reset all mocks called during setup phase MockClusterClient.mockClear(); @@ -295,7 +290,7 @@ describe('#start', () => { }); it('creates a new client on each call', async () => { await elasticsearchService.setup(setupDeps); - const startContract = await elasticsearchService.start(startDeps); + const startContract = await elasticsearchService.start(); // reset all mocks called during setup phase MockClusterClient.mockClear(); @@ -310,7 +305,7 @@ describe('#start', () => { it('falls back to elasticsearch default config values if property not specified', async () => { await elasticsearchService.setup(setupDeps); - const startContract = await elasticsearchService.start(startDeps); + const startContract = await elasticsearchService.start(); // reset all mocks called during setup phase MockClusterClient.mockClear(); @@ -347,7 +342,7 @@ describe('#start', () => { describe('#stop', () => { it('stops both legacy and new clients', async () => { await elasticsearchService.setup(setupDeps); - await elasticsearchService.start(startDeps); + await elasticsearchService.start(); await elasticsearchService.stop(); expect(mockLegacyClusterClientInstance.close).toHaveBeenCalledTimes(1); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index 5d07840e8bda..a0b9e8c6f2bf 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -32,7 +32,6 @@ import { import { ClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from './client'; import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config'; import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; -import { AuditTrailStart, AuditorFactory } from '../audit_trail'; import { InternalElasticsearchServiceSetup, InternalElasticsearchServiceStart } from './types'; import { pollEsNodesVersion } from './version_check/ensure_es_version'; import { calculateStatus$ } from './status'; @@ -41,16 +40,11 @@ interface SetupDeps { http: InternalHttpServiceSetup; } -interface StartDeps { - auditTrail: AuditTrailStart; -} - /** @internal */ export class ElasticsearchService implements CoreService { private readonly log: Logger; private readonly config$: Observable; - private auditorFactory?: AuditorFactory; private stop$ = new Subject(); private kibanaVersion: string; private getAuthHeaders?: GetAuthHeaders; @@ -103,8 +97,7 @@ export class ElasticsearchService status$: calculateStatus$(esNodesCompatibility$), }; } - public async start({ auditTrail }: StartDeps): Promise { - this.auditorFactory = auditTrail; + public async start(): Promise { if (!this.legacyClient || !this.createLegacyCustomClient) { throw new Error('ElasticsearchService needs to be setup before calling start'); } @@ -153,15 +146,7 @@ export class ElasticsearchService return new LegacyClusterClient( config, this.coreContext.logger.get('elasticsearch', type), - this.getAuditorFactory, this.getAuthHeaders ); } - - private getAuditorFactory = () => { - if (!this.auditorFactory) { - throw new Error('auditTrail has not been initialized'); - } - return this.auditorFactory; - }; } diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.ts index 745ef4304d0b..812f81a1affd 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.test.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.test.ts @@ -27,7 +27,6 @@ import { import { errors } from 'elasticsearch'; import { get } from 'lodash'; -import { auditTrailServiceMock } from '../../audit_trail/audit_trail_service.mock'; import { Logger } from '../../logging'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { httpServerMock } from '../../http/http_server.mocks'; @@ -43,11 +42,7 @@ test('#constructor creates client with parsed config', () => { const mockEsConfig = { apiVersion: 'es-version' } as any; const mockLogger = logger.get(); - const clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + const clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); expect(clusterClient).toBeDefined(); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); @@ -73,11 +68,7 @@ describe('#callAsInternalUser', () => { }; MockClient.mockImplementation(() => mockEsClientInstance); - clusterClient = new LegacyClusterClient( - { apiVersion: 'es-version' } as any, - logger.get(), - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient({ apiVersion: 'es-version' } as any, logger.get()); }); test('fails if cluster client is closed', async () => { @@ -246,11 +237,7 @@ describe('#asScoped', () => { requestHeadersWhitelist: ['one', 'two'], } as any; - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); jest.clearAllMocks(); }); @@ -285,11 +272,7 @@ describe('#asScoped', () => { test('properly configures `ignoreCertAndKey` for various configurations', () => { // Config without SSL. - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); @@ -302,11 +285,7 @@ describe('#asScoped', () => { // Config ssl.alwaysPresentCertificate === false mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: false } } as any; - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); @@ -319,11 +298,7 @@ describe('#asScoped', () => { // Config ssl.alwaysPresentCertificate === true mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: true } } as any; - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); @@ -344,8 +319,7 @@ describe('#asScoped', () => { expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: '1', two: '2' }, - expect.any(Object) + { one: '1', two: '2' } ); }); @@ -360,8 +334,7 @@ describe('#asScoped', () => { expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { 'x-opaque-id': 'alpha' }, - expect.any(Object) + { 'x-opaque-id': 'alpha' } ); }); @@ -383,142 +356,75 @@ describe('#asScoped', () => { }); test('does not fail when scope to not defined request', async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); clusterClient.asScoped(); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - {}, - undefined + {} ); }); test('does not fail when scope to a request without headers', async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); clusterClient.asScoped({} as any); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - {}, - undefined + {} ); }); test('calls getAuthHeaders and filters results for a real request', async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory, - () => ({ - one: '1', - three: '3', - }) - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({ + one: '1', + three: '3', + })); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { two: '2' } })); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: '1', two: '2' }, - expect.any(Object) + { one: '1', two: '2' } ); }); test('getAuthHeaders results rewrite extends a request headers', async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory, - () => ({ one: 'foo' }) - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({ one: 'foo' })); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1', two: '2' } })); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: 'foo', two: '2' }, - expect.any(Object) + { one: 'foo', two: '2' } ); }); test("doesn't call getAuthHeaders for a fake request", async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory, - () => ({}) - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({})); clusterClient.asScoped({ headers: { one: 'foo' } }); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: 'foo' }, - undefined + { one: 'foo' } ); }); test('filters a fake request headers', async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } }); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: '1', two: '2' }, - undefined + { one: '1', two: '2' } ); }); - - describe('Auditor', () => { - it('creates Auditor for KibanaRequest', async () => { - const auditor = auditTrailServiceMock.createAuditor(); - const auditorFactory = auditTrailServiceMock.createAuditorFactory(); - auditorFactory.asScoped.mockReturnValue(auditor); - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => auditorFactory); - clusterClient.asScoped(httpServerMock.createKibanaRequest()); - - expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); - expect(MockScopedClusterClient).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function), - expect.objectContaining({ 'x-opaque-id': expect.any(String) }), - auditor - ); - }); - - it("doesn't create Auditor for a fake request", async () => { - const getAuthHeaders = jest.fn(); - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, getAuthHeaders); - clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } }); - - expect(getAuthHeaders).not.toHaveBeenCalled(); - }); - - it("doesn't create Auditor when no request passed", async () => { - const getAuthHeaders = jest.fn(); - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, getAuthHeaders); - clusterClient.asScoped(); - - expect(getAuthHeaders).not.toHaveBeenCalled(); - }); - }); }); describe('#close', () => { @@ -536,8 +442,7 @@ describe('#close', () => { clusterClient = new LegacyClusterClient( { apiVersion: 'es-version', requestHeadersWhitelist: [] } as any, - logger.get(), - auditTrailServiceMock.createAuditorFactory + logger.get() ); }); diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index 81cbb5a10d7c..00417e3bef4f 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -20,8 +20,7 @@ import { Client } from 'elasticsearch'; import { get } from 'lodash'; import { LegacyElasticsearchErrorHelpers } from './errors'; -import { GetAuthHeaders, KibanaRequest, isKibanaRequest, isRealRequest } from '../../http'; -import { AuditorFactory } from '../../audit_trail'; +import { GetAuthHeaders, isKibanaRequest, isRealRequest } from '../../http'; import { filterHeaders, ensureRawRequest } from '../../http/router'; import { Logger } from '../../logging'; import { ScopeableRequest } from '../types'; @@ -132,7 +131,6 @@ export class LegacyClusterClient implements ILegacyClusterClient { constructor( private readonly config: LegacyElasticsearchClientConfig, private readonly log: Logger, - private readonly getAuditorFactory: () => AuditorFactory, private readonly getAuthHeaders: GetAuthHeaders = noop ) { this.client = new Client(parseElasticsearchClientConfig(config, log)); @@ -210,20 +208,10 @@ export class LegacyClusterClient implements ILegacyClusterClient { filterHeaders(this.getHeaders(request), [ 'x-opaque-id', ...this.config.requestHeadersWhitelist, - ]), - this.getScopedAuditor(request) + ]) ); } - private getScopedAuditor(request?: ScopeableRequest) { - // TODO: support alternative credential owners from outside of Request context in #39430 - if (request && isRealRequest(request)) { - const kibanaRequest = isKibanaRequest(request) ? request : KibanaRequest.from(request); - const auditorFactory = this.getAuditorFactory(); - return auditorFactory.asScoped(kibanaRequest); - } - } - /** * Calls specified endpoint with provided clientParams on behalf of the * user initiated request to the Kibana server (via HTTP request headers). diff --git a/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts b/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts index f1096d5d602f..2eb8cefb564a 100644 --- a/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts +++ b/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts @@ -18,7 +18,6 @@ */ import { LegacyScopedClusterClient } from './scoped_cluster_client'; -import { auditTrailServiceMock } from '../../audit_trail/audit_trail_service.mock'; let internalAPICaller: jest.Mock; let scopedAPICaller: jest.Mock; @@ -84,28 +83,6 @@ describe('#callAsInternalUser', () => { expect(scopedAPICaller).not.toHaveBeenCalled(); }); - - describe('Auditor', () => { - it('does not fail when no auditor provided', () => { - const clusterClientWithoutAuditor = new LegacyScopedClusterClient(jest.fn(), jest.fn()); - expect(() => clusterClientWithoutAuditor.callAsInternalUser('endpoint')).not.toThrow(); - }); - it('creates an audit record if auditor provided', () => { - const auditor = auditTrailServiceMock.createAuditor(); - const clusterClientWithoutAuditor = new LegacyScopedClusterClient( - jest.fn(), - jest.fn(), - {}, - auditor - ); - clusterClientWithoutAuditor.callAsInternalUser('endpoint'); - expect(auditor.add).toHaveBeenCalledTimes(1); - expect(auditor.add).toHaveBeenLastCalledWith({ - message: 'endpoint', - type: 'elasticsearch.call.internalUser', - }); - }); - }); }); describe('#callAsCurrentUser', () => { @@ -229,26 +206,4 @@ describe('#callAsCurrentUser', () => { expect(internalAPICaller).not.toHaveBeenCalled(); }); - - describe('Auditor', () => { - it('does not fail when no auditor provided', () => { - const clusterClientWithoutAuditor = new LegacyScopedClusterClient(jest.fn(), jest.fn()); - expect(() => clusterClientWithoutAuditor.callAsCurrentUser('endpoint')).not.toThrow(); - }); - it('creates an audit record if auditor provided', () => { - const auditor = auditTrailServiceMock.createAuditor(); - const clusterClientWithoutAuditor = new LegacyScopedClusterClient( - jest.fn(), - jest.fn(), - {}, - auditor - ); - clusterClientWithoutAuditor.callAsCurrentUser('endpoint'); - expect(auditor.add).toHaveBeenCalledTimes(1); - expect(auditor.add).toHaveBeenLastCalledWith({ - message: 'endpoint', - type: 'elasticsearch.call.currentUser', - }); - }); - }); }); diff --git a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts index aee7a1daa816..65484f0927c9 100644 --- a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts @@ -18,7 +18,6 @@ */ import { intersection, isObject } from 'lodash'; -import { Auditor } from '../../audit_trail'; import { Headers } from '../../http/router'; import { LegacyAPICaller, LegacyCallAPIOptions } from './api_types'; @@ -47,8 +46,7 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { constructor( private readonly internalAPICaller: LegacyAPICaller, private readonly scopedAPICaller: LegacyAPICaller, - private readonly headers?: Headers, - private readonly auditor?: Auditor + private readonly headers?: Headers ) { this.callAsCurrentUser = this.callAsCurrentUser.bind(this); this.callAsInternalUser = this.callAsInternalUser.bind(this); @@ -68,13 +66,6 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { clientParams: Record = {}, options?: LegacyCallAPIOptions ) { - if (this.auditor) { - this.auditor.add({ - message: endpoint, - type: 'elasticsearch.call.internalUser', - }); - } - return this.internalAPICaller(endpoint, clientParams, options); } @@ -107,13 +98,6 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { clientParams.headers = Object.assign({}, clientParams.headers, this.headers); } - if (this.auditor) { - this.auditor.add({ - message: endpoint, - type: 'elasticsearch.call.currentUser', - }); - } - return this.scopedAPICaller(endpoint, clientParams, options); } } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index fc091bd17bdf..efb196590ea9 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -62,7 +62,6 @@ import { import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; -import { Auditor, AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; @@ -77,7 +76,6 @@ import { export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData }; -export { AuditableEvent, Auditor, AuditorFactory, AuditTrailSetup } from './audit_trail'; export { bootstrap } from './bootstrap'; export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; export { @@ -378,7 +376,6 @@ export { CoreUsageDataStart } from './core_usage_data'; * data client which uses the credentials of the incoming request * - {@link IUiSettingsClient | uiSettings.client} - uiSettings client * which uses the credentials of the incoming request - * - {@link Auditor | uiSettings.auditor} - AuditTrail client scoped to the incoming request * * @public */ @@ -397,7 +394,6 @@ export interface RequestHandlerContext { uiSettings: { client: IUiSettingsClient; }; - auditor: Auditor; }; } @@ -434,8 +430,6 @@ export interface CoreSetup; - /** {@link AuditTrailSetup} */ - auditTrail: AuditTrailSetup; } /** @@ -469,8 +463,6 @@ export interface CoreStart { savedObjects: SavedObjectsServiceStart; /** {@link UiSettingsServiceStart} */ uiSettings: UiSettingsServiceStart; - /** {@link AuditTrailSetup} */ - auditTrail: AuditTrailStart; /** @internal {@link CoreUsageDataStart} */ coreUsageData: CoreUsageDataStart; } @@ -483,7 +475,6 @@ export { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId, - AuditTrailStart, }; /** diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index ce58348a1415..294af5ec34c3 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -37,7 +37,6 @@ import { InternalMetricsServiceSetup, InternalMetricsServiceStart } from './metr import { InternalRenderingServiceSetup } from './rendering'; import { InternalHttpResourcesSetup } from './http_resources'; import { InternalStatusServiceSetup } from './status'; -import { AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { InternalLoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; @@ -53,7 +52,6 @@ export interface InternalCoreSetup { environment: InternalEnvironmentServiceSetup; rendering: InternalRenderingServiceSetup; httpResources: InternalHttpResourcesSetup; - auditTrail: AuditTrailSetup; logging: InternalLoggingServiceSetup; metrics: InternalMetricsServiceSetup; } @@ -68,7 +66,6 @@ export interface InternalCoreStart { metrics: InternalMetricsServiceStart; savedObjects: InternalSavedObjectsServiceStart; uiSettings: InternalUiSettingsServiceStart; - auditTrail: AuditTrailStart; coreUsageData: CoreUsageDataStart; } diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 57009f0d35c1..b8f5757f0b67 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -44,7 +44,6 @@ import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; import { LegacyService } from './legacy_service'; import { coreMock } from '../mocks'; import { statusServiceMock } from '../status/status_service.mock'; -import { auditTrailServiceMock } from '../audit_trail/audit_trail_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { metricsServiceMock } from '../metrics/metrics_service.mock'; @@ -92,7 +91,6 @@ beforeEach(() => { rendering: renderingServiceMock, environment: environmentSetup, status: statusServiceMock.createInternalSetupContract(), - auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), metrics: metricsServiceMock.createInternalSetupContract(), }, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 75e8ae652492..c42771179aba 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -216,7 +216,6 @@ export class LegacyService implements CoreService { getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$, }, uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, - auditTrail: startDeps.core.auditTrail, coreUsageData: { getCoreUsageData: () => { throw new Error('core.start.coreUsageData.getCoreUsageData is unsupported in legacy'); @@ -284,7 +283,6 @@ export class LegacyService implements CoreService { uiSettings: { register: setupDeps.core.uiSettings.register, }, - auditTrail: setupDeps.core.auditTrail, getStartServices: () => Promise.resolve([coreStart, startDeps.plugins, {}]), }; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 34e85920efb2..e47d06409894 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -36,7 +36,6 @@ import { capabilitiesServiceMock } from './capabilities/capabilities_service.moc import { metricsServiceMock } from './metrics/metrics_service.mock'; import { environmentServiceMock } from './environment/environment_service.mock'; import { statusServiceMock } from './status/status_service.mock'; -import { auditTrailServiceMock } from './audit_trail/audit_trail_service.mock'; import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; export { configServiceMock } from './config/mocks'; @@ -139,7 +138,6 @@ function createCoreSetupMock({ savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createSetupContract(), uiSettings: uiSettingsMock, - auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createSetupContract(), metrics: metricsServiceMock.createSetupContract(), getStartServices: jest @@ -152,7 +150,6 @@ function createCoreSetupMock({ function createCoreStartMock() { const mock: MockedKeys = { - auditTrail: auditTrailServiceMock.createStartContract(), capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createStart(), http: httpServiceMock.createStartContract(), @@ -177,7 +174,6 @@ function createInternalCoreSetupMock() { httpResources: httpResourcesMock.createSetupContract(), rendering: renderingMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), - auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), metrics: metricsServiceMock.createInternalSetupContract(), }; @@ -192,7 +188,6 @@ function createInternalCoreStartMock() { metrics: metricsServiceMock.createInternalStartContract(), savedObjects: savedObjectsServiceMock.createInternalStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), - auditTrail: auditTrailServiceMock.createStartContract(), coreUsageData: coreUsageDataServiceMock.createStartContract(), }; return startDeps; @@ -213,7 +208,6 @@ function createCoreRequestHandlerContextMock() { uiSettings: { client: uiSettingsServiceMock.createClient(), }, - auditor: auditTrailServiceMock.createAuditor(), }; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index a8249ed7e321..22e79741e854 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -201,7 +201,6 @@ export function createPluginSetupContext( register: deps.uiSettings.register, }, getStartServices: () => plugin.startDependencies, - auditTrail: deps.auditTrail, }; } @@ -250,7 +249,6 @@ export function createPluginStartContext( uiSettings: { asScopedToClient: deps.uiSettings.asScopedToClient, }, - auditTrail: deps.auditTrail, coreUsageData: deps.coreUsageData, }; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 20bd102e6f50..7cd8682050e6 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -198,38 +198,6 @@ export interface AssistantAPIClientParams extends GenericParams { path: '/_migration/assistance'; } -// @public -export interface AuditableEvent { - // (undocumented) - message: string; - // (undocumented) - type: string; -} - -// @public -export interface Auditor { - add(event: AuditableEvent): void; - withAuditScope(name: string): void; -} - -// @public -export interface AuditorFactory { - // (undocumented) - asScoped(request: KibanaRequest): Auditor; -} - -// Warning: (ae-missing-release-tag) "AuditTrailSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface AuditTrailSetup { - register(auditor: AuditorFactory): void; -} - -// Warning: (ae-missing-release-tag) "AuditTrailStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type AuditTrailStart = AuditorFactory; - // @public (undocumented) export interface Authenticated extends AuthResultParams { // (undocumented) @@ -499,8 +467,6 @@ export interface CoreServicesUsageData { // @public export interface CoreSetup { - // (undocumented) - auditTrail: AuditTrailSetup; // (undocumented) capabilities: CapabilitiesSetup; // (undocumented) @@ -527,8 +493,6 @@ export interface CoreSetup AuditorFactory, getAuthHeaders?: GetAuthHeaders); + constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders); asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient; callAsInternalUser: LegacyAPICaller; close(): void; @@ -1396,7 +1360,7 @@ export interface LegacyRequest extends Request { // @public @deprecated export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { - constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined, auditor?: Auditor | undefined); + constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined); callAsCurrentUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; callAsInternalUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; } @@ -1738,7 +1702,6 @@ export interface RequestHandlerContext { uiSettings: { client: IUiSettingsClient; }; - auditor: Auditor; }; } diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 77f2787b7541..fe299c6d1167 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -100,9 +100,3 @@ export const mockLoggingService = loggingServiceMock.create(); jest.doMock('./logging/logging_service', () => ({ LoggingService: jest.fn(() => mockLoggingService), })); - -import { auditTrailServiceMock } from './audit_trail/audit_trail_service.mock'; -export const mockAuditTrailService = auditTrailServiceMock.create(); -jest.doMock('./audit_trail/audit_trail_service', () => ({ - AuditTrailService: jest.fn(() => mockAuditTrailService), -})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 51defb7d0392..78703ceeec7a 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -31,7 +31,6 @@ import { mockMetricsService, mockStatusService, mockLoggingService, - mockAuditTrailService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -71,7 +70,6 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); - expect(mockAuditTrailService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -85,7 +83,6 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).toHaveBeenCalledTimes(1); expect(mockStatusService.setup).toHaveBeenCalledTimes(1); expect(mockLoggingService.setup).toHaveBeenCalledTimes(1); - expect(mockAuditTrailService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -126,7 +123,6 @@ test('runs services on "start"', async () => { expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); expect(mockUiSettingsService.start).not.toHaveBeenCalled(); expect(mockMetricsService.start).not.toHaveBeenCalled(); - expect(mockAuditTrailService.start).not.toHaveBeenCalled(); await server.start(); @@ -135,7 +131,6 @@ test('runs services on "start"', async () => { expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1); expect(mockMetricsService.start).toHaveBeenCalledTimes(1); - expect(mockAuditTrailService.start).toHaveBeenCalledTimes(1); }); test('does not fail on "setup" if there are unused paths detected', async () => { @@ -160,7 +155,6 @@ test('stops services on "stop"', async () => { expect(mockMetricsService.stop).not.toHaveBeenCalled(); expect(mockStatusService.stop).not.toHaveBeenCalled(); expect(mockLoggingService.stop).not.toHaveBeenCalled(); - expect(mockAuditTrailService.stop).not.toHaveBeenCalled(); await server.stop(); @@ -173,7 +167,6 @@ test('stops services on "stop"', async () => { expect(mockMetricsService.stop).toHaveBeenCalledTimes(1); expect(mockStatusService.stop).toHaveBeenCalledTimes(1); expect(mockLoggingService.stop).toHaveBeenCalledTimes(1); - expect(mockAuditTrailService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { @@ -227,7 +220,6 @@ test(`doesn't validate config if env.isDevClusterMaster is true`, async () => { expect(mockEnsureValidConfiguration).not.toHaveBeenCalled(); expect(mockContextService.setup).toHaveBeenCalled(); - expect(mockAuditTrailService.setup).toHaveBeenCalled(); expect(mockHttpService.setup).toHaveBeenCalled(); expect(mockElasticsearchService.setup).toHaveBeenCalled(); expect(mockSavedObjectsService.setup).toHaveBeenCalled(); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index f38cac4f4376..eaa03d11cab9 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -21,7 +21,6 @@ import { config as pathConfig } from '@kbn/utils'; import { mapToObject } from '@kbn/std'; import { ConfigService, Env, RawConfigurationProvider, coreDeprecationProvider } from './config'; import { CoreApp } from './core_app'; -import { AuditTrailService } from './audit_trail'; import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; import { HttpResourcesService } from './http_resources'; @@ -72,7 +71,6 @@ export class Server { private readonly status: StatusService; private readonly logging: LoggingService; private readonly coreApp: CoreApp; - private readonly auditTrail: AuditTrailService; private readonly coreUsageData: CoreUsageDataService; #pluginsInitialized?: boolean; @@ -103,7 +101,6 @@ export class Server { this.status = new StatusService(core); this.coreApp = new CoreApp(core); this.httpResources = new HttpResourcesService(core); - this.auditTrail = new AuditTrailService(core); this.logging = new LoggingService(core); this.coreUsageData = new CoreUsageDataService(core); } @@ -139,8 +136,6 @@ export class Server { ]), }); - const auditTrailSetup = this.auditTrail.setup(); - const httpSetup = await this.http.setup({ context: contextServiceSetup, }); @@ -200,7 +195,6 @@ export class Server { uiSettings: uiSettingsSetup, rendering: renderingSetup, httpResources: httpResourcesSetup, - auditTrail: auditTrailSetup, logging: loggingSetup, metrics: metricsSetup, }; @@ -225,11 +219,7 @@ export class Server { this.log.debug('starting server'); const startTransaction = apm.startTransaction('server_start', 'kibana_platform'); - const auditTrailStart = this.auditTrail.start(); - - const elasticsearchStart = await this.elasticsearch.start({ - auditTrail: auditTrailStart, - }); + const elasticsearchStart = await this.elasticsearch.start(); const soStartSpan = startTransaction?.startSpan('saved_objects.migration', 'migration'); const savedObjectsStart = await this.savedObjects.start({ elasticsearch: elasticsearchStart, @@ -252,7 +242,6 @@ export class Server { metrics: metricsStart, savedObjects: savedObjectsStart, uiSettings: uiSettingsStart, - auditTrail: auditTrailStart, coreUsageData: coreUsageDataStart, }; @@ -285,7 +274,6 @@ export class Server { await this.metrics.stop(); await this.status.stop(); await this.logging.stop(); - await this.auditTrail.stop(); } private registerCoreContext(coreSetup: InternalCoreSetup) { diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 0039debe383b..f5cf6c85fcbe 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -47,6 +47,10 @@ kibana_vars=( elasticsearch.ssl.truststore.password elasticsearch.ssl.verificationMode elasticsearch.username + enterpriseSearch.accessCheckTimeout + enterpriseSearch.accessCheckTimeoutWarning + enterpriseSearch.enabled + enterpriseSearch.host i18n.locale interpreter.enableInVisualize kibana.autocompleteTerminateAfter 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/dev/plugin_discovery/find_plugins.ts b/src/dev/plugin_discovery/find_plugins.ts new file mode 100644 index 000000000000..4e7c34698c96 --- /dev/null +++ b/src/dev/plugin_discovery/find_plugins.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import Path from 'path'; +import { REPO_ROOT } from '@kbn/dev-utils'; +import { getPluginSearchPaths } from '@kbn/config'; +import { KibanaPlatformPlugin, simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; + +export interface SearchOptions { + oss: boolean; + examples: boolean; + extraPluginScanDirs: string[]; +} + +export function findPlugins({ + oss, + examples, + extraPluginScanDirs, +}: SearchOptions): Map { + const pluginSearchPaths = getPluginSearchPaths({ + rootDir: REPO_ROOT, + oss, + examples, + }); + + for (const extraScanDir of extraPluginScanDirs) { + if (!Path.isAbsolute(extraScanDir)) { + throw new TypeError('extraPluginScanDirs must all be absolute paths'); + } + pluginSearchPaths.push(extraScanDir); + } + + const plugins = simpleKibanaPlatformPluginDiscovery(pluginSearchPaths, []); + return new Map(plugins.map((p) => [p.manifest.id, p])); +} diff --git a/src/dev/plugin_discovery/get_plugin_deps.ts b/src/dev/plugin_discovery/get_plugin_deps.ts new file mode 100644 index 000000000000..498feefd9709 --- /dev/null +++ b/src/dev/plugin_discovery/get_plugin_deps.ts @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { KibanaPlatformPlugin } from '@kbn/dev-utils'; + +interface AllOptions { + id: string; + pluginMap: Map; +} + +interface CircularRefsError { + from: string; + to: string; + stack: string[]; +} + +export type SearchErrors = CircularRefsError; + +interface State { + deps: Set; + stack: string[]; + errors: Map; +} + +function traverse(pluginMap: Map, state: State, id: string) { + const plugin = pluginMap.get(id); + if (plugin === undefined) { + throw new Error(`Unknown plugin id: ${id}`); + } + + const prevIndex = state.stack.indexOf(id); + const isVisited = prevIndex > -1; + if (isVisited) { + const from = state.stack[state.stack.length - 1]; + const to = id; + const key = `circular-${[from, to].sort().join('-')}`; + + if (!state.errors.has(key)) { + const error: CircularRefsError = { + from, + to, + // provide sub-stack with circular refs only + stack: state.stack.slice(prevIndex), + }; + state.errors.set(key, error); + } + + return; + } + + state.stack.push(id); + new Set([ + ...plugin.manifest.requiredPlugins, + ...plugin.manifest.optionalPlugins, + ...plugin.manifest.requiredBundles, + ]).forEach((depId) => { + state.deps.add(pluginMap.get(depId)!); + traverse(pluginMap, state, depId); + }); + + state.stack.pop(); +} + +export function getPluginDeps({ pluginMap, id }: AllOptions): State { + const state: State = { + deps: new Set(), + errors: new Map(), + stack: [], + }; + + traverse(pluginMap, state, id); + + return state; +} diff --git a/src/dev/plugin_discovery/index.ts b/src/dev/plugin_discovery/index.ts new file mode 100644 index 000000000000..4a4be65dfaef --- /dev/null +++ b/src/dev/plugin_discovery/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './find_plugins'; +export * from './get_plugin_deps'; diff --git a/src/dev/run_find_plugin_circular_deps.ts b/src/dev/run_find_plugin_circular_deps.ts new file mode 100644 index 000000000000..501e2c4fed04 --- /dev/null +++ b/src/dev/run_find_plugin_circular_deps.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { run } from '@kbn/dev-utils'; +import { findPlugins, getPluginDeps, SearchErrors } from './plugin_discovery'; + +interface AllOptions { + examples?: boolean; + extraPluginScanDirs?: string[]; +} + +run( + async ({ flags, log }) => { + const { examples = false, extraPluginScanDirs = [] } = flags as AllOptions; + + const pluginMap = findPlugins({ + oss: false, + examples, + extraPluginScanDirs, + }); + + const allErrors = new Map(); + for (const pluginId of pluginMap.keys()) { + const { errors } = getPluginDeps({ + pluginMap, + id: pluginId, + }); + + for (const [errorId, error] of errors) { + if (!allErrors.has(errorId)) { + allErrors.set(errorId, error); + } + } + } + + if (allErrors.size > 0) { + allErrors.forEach((error) => { + log.warning( + `Circular refs detected: ${[...error.stack, error.to].map((p) => `[${p}]`).join(' --> ')}` + ); + }); + } + }, + { + flags: { + boolean: ['examples'], + default: { + examples: false, + }, + allowUnexpected: false, + help: ` + --examples Include examples folder + --extraPluginScanDirs Include extra scan folder + `, + }, + } +); diff --git a/src/dev/run_find_plugins_without_ts_refs.ts b/src/dev/run_find_plugins_without_ts_refs.ts new file mode 100644 index 000000000000..ad63884671e2 --- /dev/null +++ b/src/dev/run_find_plugins_without_ts_refs.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Path from 'path'; +import Fs from 'fs'; +import { get } from 'lodash'; +import { run } from '@kbn/dev-utils'; +import { getPluginDeps, findPlugins } from './plugin_discovery'; + +interface AllOptions { + id?: string; + examples?: boolean; + extraPluginScanDirs?: string[]; +} + +run( + async ({ flags, log }) => { + const { examples = false, extraPluginScanDirs = [], id } = flags as AllOptions; + + if (!id) { + throw new Error('Plugin id required'); + } + + const pluginMap = findPlugins({ + oss: false, + examples, + extraPluginScanDirs, + }); + + const result = getPluginDeps({ + pluginMap, + id, + }); + + if (result.errors.size > 0) { + result.errors.forEach((error) => { + log.warning( + `Circular refs detected: ${[...error.stack, error.to].map((p) => `[${p}]`).join(' --> ')}` + ); + }); + } + + const notMigratedPlugins = [...result.deps].filter( + (plugin) => !isMigratedToTsProjectRefs(plugin.directory) + ); + if (notMigratedPlugins.length > 0) { + log.info( + `Dependencies haven't been migrated to TS project refs yet:\n${notMigratedPlugins + .map((p) => p.manifest.id) + .join('\n')}` + ); + } + }, + { + flags: { + boolean: ['examples'], + string: ['id'], + default: { + examples: false, + }, + allowUnexpected: false, + help: ` + --id Plugin id to perform deps search for + --examples Include examples folder + --extraPluginScanDirs Include extra scan folder + `, + }, + } +); + +function isMigratedToTsProjectRefs(dir: string): boolean { + try { + const path = Path.join(dir, 'tsconfig.json'); + const content = Fs.readFileSync(path, { encoding: 'utf8' }); + return get(JSON.parse(content), 'compilerOptions.composite', false); + } catch (e) { + return false; + } +} diff --git a/src/legacy/server/i18n/index.ts b/src/legacy/server/i18n/index.ts index cb86c3220bec..61caefb2fb59 100644 --- a/src/legacy/server/i18n/index.ts +++ b/src/legacy/server/i18n/index.ts @@ -33,7 +33,7 @@ export async function i18nMixin(kbnServer: KbnServer, server: Server, config: Ki const translationPaths = await Promise.all([ getTranslationPaths({ cwd: fromRoot('.'), - glob: I18N_RC, + glob: `*/${I18N_RC}`, }), ...(config.get('plugins.paths') as string[]).map((cwd) => getTranslationPaths({ cwd, glob: I18N_RC }) diff --git a/src/legacy/server/keystore/keystore.test.js b/src/legacy/server/keystore/keystore.test.js index 0897ce55d086..e35edd185948 100644 --- a/src/legacy/server/keystore/keystore.test.js +++ b/src/legacy/server/keystore/keystore.test.js @@ -157,11 +157,13 @@ describe('Keystore', () => { it('adds a key/value pair', () => { const keystore = new Keystore('/data/unprotected.keystore'); keystore.add('a3', 'baz'); + keystore.add('a4', [1, 'a', 2, 'b']); expect(keystore.data).toEqual({ 'a1.b2.c3': 'foo', a2: 'bar', a3: 'baz', + a4: [1, 'a', 2, 'b'], }); }); }); diff --git a/src/plugins/charts/public/services/colors/color_palette.ts b/src/plugins/charts/public/services/colors/color_palette.ts index e1c32fe68da1..df76edb1e30e 100644 --- a/src/plugins/charts/public/services/colors/color_palette.ts +++ b/src/plugins/charts/public/services/colors/color_palette.ts @@ -58,7 +58,7 @@ export function createColorPalette(num: number): string[] { const seedLength = seedColors.length; _.times(num - seedLength, function (i) { - colors.push(hsl((fraction(i + seedLength + 1) * 360 + offset) % 360, 0.5, 0.5).hex()); + colors.push(hsl((fraction(i + seedLength + 1) * 360 + offset) % 360, 50, 50).hex()); }); return colors; diff --git a/src/plugins/charts/public/services/colors/colors_palette.test.ts b/src/plugins/charts/public/services/colors/colors_palette.test.ts index 02ff5a6056d5..273a36f6a43a 100644 --- a/src/plugins/charts/public/services/colors/colors_palette.test.ts +++ b/src/plugins/charts/public/services/colors/colors_palette.test.ts @@ -90,4 +90,8 @@ describe('Color Palette', () => { it('should create new darker colors when input is greater than 72', () => { expect(createColorPalette(num3)[72]).not.toEqual(seedColors[0]); }); + + it('should create new colors and convert them correctly', () => { + expect(createColorPalette(num3)[72]).toEqual('#404ABF'); + }); }); 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 `#${DashboardConstants.LANDING_PAGE_PATH}`; const getDashTitle = () => diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 2fda8008788e..574d456c10a8 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -412,10 +412,7 @@ export class DashboardPlugin public start(core: CoreStart, plugins: StartDependencies): DashboardStart { const { notifications } = core; - const { - uiActions, - data: { indexPatterns, search }, - } = plugins; + const { uiActions } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -433,24 +430,22 @@ export class DashboardPlugin uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id); if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) { - const addToLibraryAction = new AddToLibraryAction(); + const addToLibraryAction = new AddToLibraryAction({ toasts: notifications.toasts }); uiActions.registerAction(addToLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id); - const unlinkFromLibraryAction = new UnlinkFromLibraryAction(); + + const unlinkFromLibraryAction = new UnlinkFromLibraryAction({ toasts: notifications.toasts }); uiActions.registerAction(unlinkFromLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id); - const libraryNotificationAction = new LibraryNotificationAction(); + const libraryNotificationAction = new LibraryNotificationAction(unlinkFromLibraryAction); uiActions.registerAction(libraryNotificationAction); uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); } const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, - indexPatterns, - search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects: plugins.savedObjects, }); const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts index f3bdfd8e17f0..bfc52ec33c35 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts @@ -16,11 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { - createSavedObjectClass, - SavedObject, - SavedObjectKibanaServices, -} from '../../../../plugins/saved_objects/public'; +import { SavedObject, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { extractReferences, injectReferences } from './saved_dashboard_references'; import { Filter, ISearchSource, Query, RefreshInterval } from '../../../../plugins/data/public'; @@ -45,10 +41,9 @@ export interface SavedObjectDashboard extends SavedObject { // Used only by the savedDashboards service, usually no reason to change this export function createSavedDashboardClass( - services: SavedObjectKibanaServices + savedObjectStart: SavedObjectsStart ): new (id: string) => 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/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 9e4308d6fd55..15ecf6e4fc3e 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -185,7 +185,8 @@ export abstract class FieldFormat { const params = transform( this._params, - (uniqParams: any, val, param) => { + (uniqParams: any, val, param: string) => { + if (param === 'parsedUrl') return; if (param && val !== get(defaultsParams, param)) { uniqParams[param] = val; } @@ -195,7 +196,7 @@ export abstract class FieldFormat { return { id, - params: size(params) ? params : undefined, + params: size(params) ? (params as any) : undefined, }; } diff --git a/src/plugins/data/common/mocks.ts b/src/plugins/data/common/mocks.ts new file mode 100644 index 000000000000..dde70b1d0744 --- /dev/null +++ b/src/plugins/data/common/mocks.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getSessionServiceMock } from './search/session/mocks'; diff --git a/src/plugins/data/common/search/aggs/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/lib/time_buckets/time_buckets.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts index 04e64233ce19..8128f1a18a66 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts @@ -69,6 +69,7 @@ describe('TimeBuckets', () => { test('setInterval/getInterval - intreval is a string', () => { const timeBuckets = new TimeBuckets(timeBucketConfig); timeBuckets.setInterval('20m'); + const interval = timeBuckets.getInterval(); expect(interval.description).toEqual('20 minutes'); @@ -77,6 +78,23 @@ describe('TimeBuckets', () => { expect(interval.expression).toEqual('20m'); }); + test('getInterval - should scale interval', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + const bounds = { + min: moment('2020-03-25'), + max: moment('2020-03-31'), + }; + timeBuckets.setBounds(bounds); + timeBuckets.setInterval('1m'); + + const interval = timeBuckets.getInterval(); + + expect(interval.description).toEqual('day'); + expect(interval.esValue).toEqual(1); + expect(interval.esUnit).toEqual('d'); + expect(interval.expression).toEqual('1d'); + }); + test('setInterval/getInterval - intreval is a string and bounds is defined', () => { const timeBuckets = new TimeBuckets(timeBucketConfig); const bounds = { diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts index d054df0c9274..f11f89317aea 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -263,18 +263,16 @@ export class TimeBuckets { } const maxLength: number = this._timeBucketConfig['histogram:maxBars']; - const approxLen = Number(duration) / Number(interval); + const minInterval = calcAutoIntervalLessThan(maxLength, Number(duration)); let scaled; - if (approxLen > maxLength) { - scaled = calcAutoIntervalLessThan(maxLength, Number(duration)); + if (interval < minInterval) { + scaled = minInterval; } else { return interval; } - if (+scaled === +interval) return interval; - interval = decorateInterval(interval); return Object.assign(scaled, { preScaled: interval, 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/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts index b1c3e5cdd396..4d3bc088749a 100644 --- a/src/plugins/data/common/search/es_search/types.ts +++ b/src/plugins/data/common/search/es_search/types.ts @@ -31,6 +31,11 @@ export interface ISearchOptions { * Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. */ strategy?: string; + + /** + * A session ID, grouping multiple search requests into a single session. + */ + sessionId?: string; } export type ISearchRequestParams> = { diff --git a/src/plugins/data/common/search/expressions/esaggs.ts b/src/plugins/data/common/search/expressions/esaggs.ts index 2957512886b4..4f65babdcd36 100644 --- a/src/plugins/data/common/search/expressions/esaggs.ts +++ b/src/plugins/data/common/search/expressions/esaggs.ts @@ -19,12 +19,12 @@ import { KibanaContext, - KibanaDatatable, + Datatable, ExpressionFunctionDefinition, } from '../../../../../plugins/expressions/common'; type Input = KibanaContext | null; -type Output = Promise; +type Output = Promise; interface Arguments { index: string; diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 2ee0db384cf0..e650cf10db87 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -23,3 +23,4 @@ export * from './expressions'; export * from './search_source'; export * from './tabify'; export * from './types'; +export * from './session'; diff --git a/src/plugins/data/public/search/expressions/utils/index.ts b/src/plugins/data/common/search/session/index.ts similarity index 95% rename from src/plugins/data/public/search/expressions/utils/index.ts rename to src/plugins/data/common/search/session/index.ts index 094536fc1843..d8f7b5091eb8 100644 --- a/src/plugins/data/public/search/expressions/utils/index.ts +++ b/src/plugins/data/common/search/session/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './serialize_agg_config'; +export * from './types'; diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/common/search/session/mocks.ts new file mode 100644 index 000000000000..7d5cd75b5753 --- /dev/null +++ b/src/plugins/data/common/search/session/mocks.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ISessionService } from './types'; + +export function getSessionServiceMock(): jest.Mocked { + return { + clear: jest.fn(), + start: jest.fn(), + getSessionId: jest.fn(), + getSession$: jest.fn(), + }; +} diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts new file mode 100644 index 000000000000..80ab74f1aa14 --- /dev/null +++ b/src/plugins/data/common/search/session/types.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; + +export interface ISessionService { + /** + * Returns the active session ID + * @returns The active session ID + */ + getSessionId: () => string | undefined; + /** + * Returns the observable that emits an update every time the session ID changes + * @returns `Observable` + */ + getSession$: () => Observable; + /** + * Starts a new session + */ + start: () => string; + /** + * Clears the active session. + */ + clear: () => void; +} diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts index aa96d77d873d..6dcfa4d02bcb 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts @@ -21,7 +21,12 @@ import moment from 'moment'; import { createFiltersFromRangeSelectAction } from './create_filters_from_range_select'; -import { IndexPatternsContract, RangeFilter } from '../../../public'; +import { + fieldFormats, + FieldFormatsGetConfigFn, + IndexPatternsContract, + RangeFilter, +} from '../../../public'; import { dataPluginMock } from '../../../public/mocks'; import { setIndexPatterns, setSearchService } from '../../../public/services'; import { TriggerContextMapping } from '../../../../ui_actions/public'; @@ -31,23 +36,30 @@ describe('brushEvent', () => { const JAN_01_2014 = 1388559600000; let baseEvent: TriggerContextMapping['SELECT_RANGE_TRIGGER']['data']; + const mockField = { + name: 'time', + indexPattern: { + id: 'logstash-*', + }, + filterable: true, + format: new fieldFormats.DateFormat({}, (() => {}) as FieldFormatsGetConfigFn), + }; + const indexPattern = { id: 'indexPatternId', timeFieldName: 'time', fields: { - getByName: () => undefined, - filter: () => [], + getByName: () => mockField, + filter: () => [mockField], }, }; - const aggConfigs = [ - { - params: { - field: {}, - }, - getIndexPattern: () => indexPattern, + const serializedAggConfig = { + type: 'date_histogram', + params: { + field: {}, }, - ]; + }; beforeEach(() => { const dataStart = dataPluginMock.createStartContract(); @@ -60,15 +72,18 @@ describe('brushEvent', () => { baseEvent = { column: 0, table: { - type: 'kibana_datatable', + type: 'datatable', columns: [ { id: '1', name: '1', meta: { - type: 'histogram', - indexPatternId: 'indexPatternId', - aggConfigParams: aggConfigs[0].params, + type: 'date', + sourceParams: { + indexPatternId: 'indexPatternId', + ...serializedAggConfig, + }, + source: 'esaggs', }, }, ], @@ -90,7 +105,7 @@ describe('brushEvent', () => { describe('handles an event when the x-axis field is a date field', () => { describe('date field is index pattern timefield', () => { beforeEach(() => { - aggConfigs[0].params.field = { + serializedAggConfig.params.field = { name: 'time', type: 'date', }; @@ -98,7 +113,7 @@ describe('brushEvent', () => { afterAll(() => { baseEvent.range = []; - aggConfigs[0].params.field = {}; + serializedAggConfig.params.field = {}; }); test('by ignoring the event when range spans zero time', async () => { @@ -123,7 +138,7 @@ describe('brushEvent', () => { describe('date field is not index pattern timefield', () => { beforeEach(() => { - aggConfigs[0].params.field = { + serializedAggConfig.params.field = { name: 'anotherTimeField', type: 'date', }; @@ -131,7 +146,7 @@ describe('brushEvent', () => { afterAll(() => { baseEvent.range = []; - aggConfigs[0].params.field = {}; + serializedAggConfig.params.field = {}; }); test('creates a new range filter', async () => { @@ -157,7 +172,7 @@ describe('brushEvent', () => { describe('handles an event when the x-axis field is a number', () => { beforeAll(() => { - aggConfigs[0].params.field = { + serializedAggConfig.params.field = { name: 'numberField', type: 'number', }; diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts index d9aa1b8ec804..2d7aeff79a68 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts @@ -20,9 +20,9 @@ import { last } from 'lodash'; import moment from 'moment'; import { esFilters, IFieldType, RangeFilterParams } from '../../../public'; -import { getIndexPatterns } from '../../../public/services'; -import { deserializeAggConfig } from '../../search/expressions/utils'; -import type { RangeSelectContext } from '../../../../embeddable/public'; +import { getIndexPatterns, getSearchService } from '../../../public/services'; +import { RangeSelectContext } from '../../../../embeddable/public'; +import { AggConfigSerialized } from '../../../common/search/aggs'; export async function createFiltersFromRangeSelectAction(event: RangeSelectContext['data']) { const column: Record = event.table.columns[event.column]; @@ -31,11 +31,12 @@ export async function createFiltersFromRangeSelectAction(event: RangeSelectConte return []; } - const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); - const aggConfig = deserializeAggConfig({ - ...column.meta, - indexPattern, - }); + const { indexPatternId, ...aggConfigs } = column.meta.sourceParams; + const indexPattern = await getIndexPatterns().get(indexPatternId); + const aggConfigsInstance = getSearchService().aggs.createAggConfigs(indexPattern, [ + aggConfigs as AggConfigSerialized, + ]); + const aggConfig = aggConfigsInstance.aggs[0]; const field: IFieldType = aggConfig.params.field; if (!field || event.range.length <= 1) { diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index 2ad20c380781..23d2ab080d75 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -45,12 +45,16 @@ describe('createFiltersFromValueClick', () => { name: 'test', id: '1-1', meta: { - type: 'histogram', - indexPatternId: 'logstash-*', - aggConfigParams: { - field: 'bytes', - interval: 30, - otherBucket: true, + type: 'date', + source: 'esaggs', + sourceParams: { + indexPatternId: 'logstash-*', + type: 'histogram', + params: { + field: 'bytes', + interval: 30, + otherBucket: true, + }, }, }, }, @@ -91,9 +95,7 @@ describe('createFiltersFromValueClick', () => { }); test('handles an event when aggregations type is a terms', async () => { - if (dataPoints[0].table.columns[0].meta) { - dataPoints[0].table.columns[0].meta.type = 'terms'; - } + (dataPoints[0].table.columns[0].meta.sourceParams as any).type = 'terms'; const filters = await createFiltersFromValueClickAction({ data: dataPoints }); expect(filters.length).toEqual(1); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts index 9429df91f693..ce7ecf434056 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts @@ -17,11 +17,11 @@ * under the License. */ -import { KibanaDatatable } from '../../../../../plugins/expressions/public'; -import { deserializeAggConfig } from '../../search/expressions'; +import { Datatable } from '../../../../../plugins/expressions/public'; import { esFilters, Filter } from '../../../public'; -import { getIndexPatterns } from '../../../public/services'; -import type { ValueClickContext } from '../../../../embeddable/public'; +import { getIndexPatterns, getSearchService } from '../../../public/services'; +import { ValueClickContext } from '../../../../embeddable/public'; +import { AggConfigSerialized } from '../../../common/search/aggs'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -33,7 +33,7 @@ import type { ValueClickContext } from '../../../../embeddable/public'; * @return {array} - array of terms to filter against */ const getOtherBucketFilterTerms = ( - table: Pick, + table: Pick, columnIndex: number, rowIndex: number ) => { @@ -71,22 +71,28 @@ const getOtherBucketFilterTerms = ( * @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters() */ const createFilter = async ( - table: Pick, + table: Pick, columnIndex: number, rowIndex: number ) => { - if (!table || !table.columns || !table.columns[columnIndex]) { + if ( + !table || + !table.columns || + !table.columns[columnIndex] || + !table.columns[columnIndex].meta || + table.columns[columnIndex].meta.source !== 'esaggs' || + !table.columns[columnIndex].meta.sourceParams?.indexPatternId + ) { return; } const column = table.columns[columnIndex]; - if (!column.meta || !column.meta.indexPatternId) { - return; - } - const aggConfig = deserializeAggConfig({ - type: column.meta.type, - aggConfigParams: column.meta.aggConfigParams ? column.meta.aggConfigParams : {}, - indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), - }); + const { indexPatternId, ...aggConfigParams } = table.columns[columnIndex].meta + .sourceParams as any; + const aggConfigsInstance = getSearchService().aggs.createAggConfigs( + await getIndexPatterns().get(indexPatternId), + [aggConfigParams as AggConfigSerialized] + ); + const aggConfig = aggConfigsInstance.aggs[0]; let filter: Filter[] = []; const value: any = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; if (value === null || value === undefined || !aggConfig.isFilterable()) { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 208bea8d43bf..688509a0758c 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) @@ -1395,6 +1396,7 @@ export type ISearchGeneric = void; } @@ -1993,7 +2000,7 @@ export class SearchInterceptor { // (undocumented) protected getTimeoutMode(): TimeoutErrorMode; // (undocumented) - protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error; + protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; // @internal protected pendingCount$: BehaviorSubject; // @internal (undocumented) @@ -2004,8 +2011,8 @@ export class SearchInterceptor { abortSignal?: AbortSignal; timeout?: number; }): { - combinedSignal: AbortSignal; timeoutSignal: AbortSignal; + combinedSignal: AbortSignal; cleanup: () => void; }; // (undocumented) @@ -2019,6 +2026,8 @@ export interface SearchInterceptorDeps { // (undocumented) http: CoreSetup_2['http']; // (undocumented) + session: ISessionService; + // (undocumented) startServices: Promise<[CoreStart, any, unknown]>; // (undocumented) toasts: ToastsSetup; diff --git a/src/plugins/data/public/query/query_string/query_string_manager.test.ts b/src/plugins/data/public/query/query_string/query_string_manager.test.ts new file mode 100644 index 000000000000..aa1556480452 --- /dev/null +++ b/src/plugins/data/public/query/query_string/query_string_manager.test.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { QueryStringManager } from './query_string_manager'; +import { Storage } from '../../../../kibana_utils/public/storage'; +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; +import { coreMock } from '../../../../../core/public/mocks'; +import { Query } from '../../../common/query'; + +describe('QueryStringManager', () => { + let service: QueryStringManager; + + beforeEach(() => { + service = new QueryStringManager( + new Storage(new StubBrowserStorage()), + coreMock.createSetup().uiSettings + ); + }); + + test('getUpdates$ is a cold emits only after query changes', () => { + const obs$ = service.getUpdates$(); + const emittedValues: Query[] = []; + obs$.subscribe((v) => { + emittedValues.push(v); + }); + expect(emittedValues).toHaveLength(0); + + const newQuery = { query: 'new query', language: 'kquery' }; + service.setQuery(newQuery); + expect(emittedValues).toHaveLength(1); + expect(emittedValues[0]).toEqual(newQuery); + + service.setQuery({ ...newQuery }); + expect(emittedValues).toHaveLength(1); + }); +}); diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts index bd02830f4aed..50732c99a62d 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.ts @@ -17,8 +17,8 @@ * under the License. */ -import _ from 'lodash'; import { BehaviorSubject } from 'rxjs'; +import { skip } from 'rxjs/operators'; import { CoreStart } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { Query, UI_SETTINGS } from '../../../common'; @@ -61,7 +61,7 @@ export class QueryStringManager { } public getUpdates$ = () => { - return this.query$.asObservable(); + return this.query$.asObservable().pipe(skip(1)); }; public getQuery = (): Query => { diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 1021ef0f91d5..de7a4ffce8bd 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -19,7 +19,7 @@ import { get, hasIn } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { KibanaDatatable, KibanaDatatableColumn } from 'src/plugins/expressions/public'; +import { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; import { PersistedState } from '../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../plugins/inspector/public'; @@ -49,7 +49,6 @@ import { getSearchService, } from '../../services'; import { buildTabularInspectorData } from './build_tabular_inspector_data'; -import { serializeAggConfig } from './utils'; export interface RequestHandlerParams { searchSource: ISearchSource; @@ -149,7 +148,9 @@ const handleCourierRequest = async ({ request.stats(getRequestInspectorStats(requestSearchSource)); try { - const response = await requestSearchSource.fetch({ abortSignal }); + const response = await requestSearchSource.fetch({ + abortSignal, + }); request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); @@ -193,11 +194,9 @@ const handleCourierRequest = async ({ : undefined, }; - (searchSource as any).tabifiedResponse = tabifyAggResponse( - aggs, - (searchSource as any).finalResponse, - tabifyParams - ); + const response = tabifyAggResponse(aggs, (searchSource as any).finalResponse, tabifyParams); + + (searchSource as any).tabifiedResponse = response; inspectorAdapters.data.setTabularLoader( () => @@ -208,12 +207,12 @@ const handleCourierRequest = async ({ { returnsFormattedValues: true } ); - return (searchSource as any).tabifiedResponse; + return response; }; export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ name, - type: 'kibana_datatable', + type: 'datatable', inputTypes: ['kibana_context', 'null'], help: i18n.translate('data.functions.esaggs.help', { defaultMessage: 'Run AggConfig aggregation', @@ -279,18 +278,25 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ abortSignal: (abortSignal as unknown) as AbortSignal, }); - const table: KibanaDatatable = { - type: 'kibana_datatable', + const table: Datatable = { + type: 'datatable', rows: response.rows, - columns: response.columns.map((column: any) => { - const cleanedColumn: KibanaDatatableColumn = { + columns: response.columns.map((column) => { + const cleanedColumn: DatatableColumn = { id: column.id, name: column.name, - meta: serializeAggConfig(column.aggConfig), + meta: { + type: column.aggConfig.params.field?.type || 'number', + field: column.aggConfig.params.field?.name, + index: indexPattern.title, + params: column.aggConfig.toSerializedFieldFormat(), + source: 'esaggs', + sourceParams: { + indexPatternId: indexPattern.id, + ...column.aggConfig.serialize(), + }, + }, }; - if (args.includeFormatHints) { - cleanedColumn.formatHint = column.aggConfig.toSerializedFieldFormat(); - } return cleanedColumn; }), }; diff --git a/src/plugins/data/public/search/expressions/index.ts b/src/plugins/data/public/search/expressions/index.ts index 02df7986479a..98ed1d08af8a 100644 --- a/src/plugins/data/public/search/expressions/index.ts +++ b/src/plugins/data/public/search/expressions/index.ts @@ -20,4 +20,3 @@ export * from './esaggs'; export * from './es_raw_response'; export * from './esdsl'; -export * from './utils'; diff --git a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts b/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts deleted file mode 100644 index 6ba323b65783..000000000000 --- a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { KibanaDatatableColumnMeta } from '../../../../../../plugins/expressions/public'; -import { IAggConfig } from '../../../../common'; -import { IndexPattern } from '../../../index_patterns'; -import { getSearchService } from '../../../../public/services'; - -/** @internal */ -export const serializeAggConfig = (aggConfig: IAggConfig): KibanaDatatableColumnMeta => { - return { - type: aggConfig.type.name, - indexPatternId: aggConfig.getIndexPattern().id, - aggConfigParams: aggConfig.serialize().params, - }; -}; - -interface DeserializeAggConfigParams { - type: string; - aggConfigParams: Record; - indexPattern: IndexPattern; -} - -/** @internal */ -export const deserializeAggConfig = ({ - type, - aggConfigParams, - indexPattern, -}: DeserializeAggConfigParams) => { - const { aggs } = getSearchService(); - const aggConfigs = aggs.createAggConfigs(indexPattern); - const aggConfig = aggConfigs.createAggConfig({ - enabled: true, - type, - params: aggConfigParams, - }); - return aggConfig; -}; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index 8bad4cd269b3..836ddb618e74 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -20,11 +20,13 @@ import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { searchSourceMock } from './search_source/mocks'; import { ISearchSetup, ISearchStart } from './types'; +import { getSessionServiceMock } from '../../common/mocks'; function createSetupContract(): jest.Mocked { return { aggs: searchAggsSetupMock(), __enhance: jest.fn(), + session: getSessionServiceMock(), }; } @@ -33,6 +35,7 @@ function createStartContract(): jest.Mocked { aggs: searchAggsStartMock(), search: jest.fn(), showError: jest.fn(), + session: getSessionServiceMock(), searchSource: searchSourceMock.createStartContract(), }; } diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index ade15adc1c3a..e8a728bb9cec 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -17,12 +17,14 @@ * under the License. */ -import { CoreSetup } from '../../../../core/public'; +import { CoreSetup, CoreStart } from '../../../../core/public'; import { coreMock } from '../../../../core/public/mocks'; import { IEsSearchRequest } from '../../common/search'; import { SearchInterceptor } from './search_interceptor'; import { AbortError } from '../../common'; -import { SearchTimeoutError, PainlessError } from './errors'; +import { SearchTimeoutError, PainlessError, TimeoutErrorMode } from './errors'; +import { searchServiceMock } from './mocks'; +import { ISearchStart } from '.'; let searchInterceptor: SearchInterceptor; let mockCoreSetup: MockedKeys; @@ -31,13 +33,61 @@ const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); jest.useFakeTimers(); describe('SearchInterceptor', () => { + let searchMock: jest.Mocked; + let mockCoreStart: MockedKeys; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); + searchMock = searchServiceMock.createStartContract(); searchInterceptor = new SearchInterceptor({ toasts: mockCoreSetup.notifications.toasts, - startServices: mockCoreSetup.getStartServices(), + startServices: new Promise((resolve) => { + resolve([mockCoreStart, {}, {}]); + }), uiSettings: mockCoreSetup.uiSettings, http: mockCoreSetup.http, + session: searchMock.session, + }); + }); + + describe('showError', () => { + test('Ignores an AbortError', async () => { + searchInterceptor.showError(new AbortError()); + expect(mockCoreSetup.notifications.toasts.addDanger).not.toBeCalled(); + expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled(); + }); + + test('Ignores a SearchTimeoutError', async () => { + searchInterceptor.showError(new SearchTimeoutError(new Error(), TimeoutErrorMode.UPGRADE)); + expect(mockCoreSetup.notifications.toasts.addDanger).not.toBeCalled(); + expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled(); + }); + + test('Renders a PainlessError', async () => { + searchInterceptor.showError( + new PainlessError( + { + body: { + attributes: { + error: { + failed_shards: { + reason: 'bananas', + }, + }, + }, + } as any, + }, + {} as any + ) + ); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); + expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled(); + }); + + test('Renders a general error', async () => { + searchInterceptor.showError(new Error('Oopsy')); + expect(mockCoreSetup.notifications.toasts.addDanger).not.toBeCalled(); + expect(mockCoreSetup.notifications.toasts.addError).toBeCalledTimes(1); }); }); @@ -49,149 +99,172 @@ describe('SearchInterceptor', () => { params: {}, }; const response = searchInterceptor.search(mockRequest); - - const result = await response.toPromise(); - expect(result).toBe(mockResponse); + expect(response.toPromise()).resolves.toBe(mockResponse); }); - test('Observable should fail if fetch has an internal error', async () => { - const mockResponse: any = { result: 500, message: 'Internal Error' }; - mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest); + describe('Should throw typed errors', () => { + test('Observable should fail if fetch has an internal error', async () => { + const mockResponse: any = new Error('Internal Error'); + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); + await expect(response.toPromise()).rejects.toThrow('Internal Error'); + }); - try { - await response.toPromise(); - } catch (e) { - expect(e).toBe(mockResponse); - } - }); + describe('Should handle Timeout errors', () => { + test('Should throw SearchTimeoutError on server timeout AND show toast', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); + await expect(response.toPromise()).rejects.toThrow(SearchTimeoutError); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); + }); - test('Should throw SearchTimeoutError on server timeout AND show toast', async (done) => { - const mockResponse: any = { - result: 500, - body: { - message: 'Request timed out', - }, - }; - mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest); + test('Timeout error should show multiple times if not in a session', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; - try { - await response.toPromise(); - } catch (e) { - expect(e).toBeInstanceOf(SearchTimeoutError); - expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); - done(); - } - }); + await expect(searchInterceptor.search(mockRequest).toPromise()).rejects.toThrow( + SearchTimeoutError + ); + await expect(searchInterceptor.search(mockRequest).toPromise()).rejects.toThrow( + SearchTimeoutError + ); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(2); + }); - test('Search error should be debounced', async (done) => { - const mockResponse: any = { - result: 500, - body: { - message: 'Request timed out', - }, - }; - mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - try { - await searchInterceptor.search(mockRequest).toPromise(); - } catch (e) { - expect(e).toBeInstanceOf(SearchTimeoutError); - try { - await searchInterceptor.search(mockRequest).toPromise(); - } catch (e2) { + test('Timeout error should show once per each session', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'abc' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'def' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(2); + }); + + test('Timeout error should show once in a single session', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'abc' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'abc' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); - done(); - } - } - }); + }); + }); - test('Should throw Painless error on server error with OSS format', async (done) => { - const mockResponse: any = { - result: 500, - body: { - attributes: { - error: { - failed_shards: [ - { - reason: { - lang: 'painless', - script_stack: ['a', 'b'], - reason: 'banana', + test('Should throw Painless error on server error with OSS format', async () => { + const mockResponse: any = { + result: 500, + body: { + attributes: { + error: { + failed_shards: [ + { + reason: { + lang: 'painless', + script_stack: ['a', 'b'], + reason: 'banana', + }, }, - }, - ], + ], + }, }, }, - }, - }; - mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest); + }; + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); + await expect(response.toPromise()).rejects.toThrow(PainlessError); + }); - try { - await response.toPromise(); - } catch (e) { - expect(e).toBeInstanceOf(PainlessError); - done(); - } - }); + test('Observable should fail if user aborts (test merged signal)', async () => { + const abortController = new AbortController(); + mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { + return new Promise((resolve, reject) => { + options.signal.addEventListener('abort', () => { + reject(new AbortError()); + }); - test('Observable should fail if user aborts (test merged signal)', async () => { - const abortController = new AbortController(); - mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { - return new Promise((resolve, reject) => { - options.signal.addEventListener('abort', () => { - reject(new AbortError()); + setTimeout(resolve, 500); }); - - setTimeout(resolve, 500); }); - }); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest, { - abortSignal: abortController.signal, - }); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest, { + abortSignal: abortController.signal, + }); - const next = jest.fn(); - const error = (e: any) => { - expect(next).not.toBeCalled(); - expect(e).toBeInstanceOf(AbortError); - }; - response.subscribe({ next, error }); - setTimeout(() => abortController.abort(), 200); - jest.advanceTimersByTime(5000); + const next = jest.fn(); + const error = (e: any) => { + expect(next).not.toBeCalled(); + expect(e).toBeInstanceOf(AbortError); + }; + response.subscribe({ next, error }); + setTimeout(() => abortController.abort(), 200); + jest.advanceTimersByTime(5000); - await flushPromises(); - }); + await flushPromises(); + }); - test('Immediately aborts if passed an aborted abort signal', async (done) => { - const abort = new AbortController(); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest, { abortSignal: abort.signal }); - abort.abort(); + test('Immediately aborts if passed an aborted abort signal', async (done) => { + const abort = new AbortController(); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest, { abortSignal: abort.signal }); + abort.abort(); - const error = (e: any) => { - expect(e).toBeInstanceOf(AbortError); - expect(mockCoreSetup.http.fetch).not.toBeCalled(); - done(); - }; - response.subscribe({ error }); + const error = (e: any) => { + expect(e).toBeInstanceOf(AbortError); + expect(mockCoreSetup.http.fetch).not.toBeCalled(); + done(); + }; + response.subscribe({ error }); + }); }); }); }); diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 2e42635a7f81..e3c6dd3e287d 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -17,7 +17,7 @@ * under the License. */ -import { get, trimEnd, debounce } from 'lodash'; +import { get, memoize, trimEnd } from 'lodash'; import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; @@ -28,6 +28,7 @@ import { IKibanaSearchResponse, ISearchOptions, ES_SEARCH_STRATEGY, + ISessionService, } from '../../common'; import { SearchUsageCollector } from './collectors'; import { SearchTimeoutError, PainlessError, isPainlessError, TimeoutErrorMode } from './errors'; @@ -39,6 +40,7 @@ export interface SearchInterceptorDeps { startServices: Promise<[CoreStart, any, unknown]>; toasts: ToastsSetup; usageCollector?: SearchUsageCollector; + session: ISessionService; } export class SearchInterceptor { @@ -86,16 +88,17 @@ export class SearchInterceptor { e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, - appAbortSignal?: AbortSignal + options?: ISearchOptions ): Error { if (timeoutSignal.aborted || get(e, 'body.message') === 'Request timed out') { // Handle a client or a server side timeout const err = new SearchTimeoutError(e, this.getTimeoutMode()); // Show the timeout error here, so that it's shown regardless of how an application chooses to handle errors. - this.showTimeoutError(err); + // The timeout error is shown any time a request times out, or once per session, if the request is part of a session. + this.showTimeoutError(err, options?.sessionId); return err; - } else if (appAbortSignal?.aborted) { + } else if (options?.abortSignal?.aborted) { // In the case an application initiated abort, throw the existing AbortError. return e; } else if (isPainlessError(e)) { @@ -162,27 +165,37 @@ export class SearchInterceptor { combinedSignal.addEventListener('abort', cleanup); return { - combinedSignal, timeoutSignal, + combinedSignal, cleanup, }; } + private showTimeoutErrorToast = (e: SearchTimeoutError, sessionId?: string) => { + this.deps.toasts.addDanger({ + title: 'Timed out', + text: toMountPoint(e.getErrorMessage(this.application)), + }); + }; + + private showTimeoutErrorMemoized = memoize( + this.showTimeoutErrorToast, + (_: SearchTimeoutError, sessionId: string) => { + return sessionId; + } + ); + /** - * Right now we are throttling but we will hook this up with background sessions to show only one - * error notification per session. + * Show one error notification per session. * @internal */ - private showTimeoutError = debounce( - (e: SearchTimeoutError) => { - this.deps.toasts.addDanger({ - title: 'Timed out', - text: toMountPoint(e.getErrorMessage(this.application)), - }); - }, - 30000, - { leading: true, trailing: false } - ); + private showTimeoutError = (e: SearchTimeoutError, sessionId?: string) => { + if (sessionId) { + this.showTimeoutErrorMemoized(e, sessionId); + } else { + this.showTimeoutErrorToast(e, sessionId); + } + }; /** * Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort @@ -207,12 +220,9 @@ export class SearchInterceptor { abortSignal: options?.abortSignal, }); this.pendingCount$.next(this.pendingCount$.getValue() + 1); - return this.runSearch(request, combinedSignal, options?.strategy).pipe( - catchError((e: any) => { - return throwError( - this.handleSearchError(e, request, timeoutSignal, options?.abortSignal) - ); + catchError((e: Error) => { + return throwError(this.handleSearchError(e, request, timeoutSignal, options)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 2d582b30bcd1..f955dc5b6ebd 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -31,6 +31,7 @@ import { ISearchOptions, SearchSourceService, SearchSourceDependencies, + ISessionService, } from '../../common/search'; import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; @@ -40,6 +41,7 @@ import { SearchUsageCollector, createUsageCollector } from './collectors'; import { UsageCollectionSetup } from '../../../usage_collection/public'; import { esdsl, esRawResponse } from './expressions'; import { ExpressionsSetup } from '../../../expressions/public'; +import { SessionService } from './session_service'; import { ConfigSchema } from '../../config'; import { SHARD_DELAY_AGG_NAME, @@ -64,6 +66,7 @@ export class SearchService implements Plugin { private readonly searchSourceService = new SearchSourceService(); private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; + private sessionService!: ISessionService; constructor(private initializerContext: PluginInitializerContext) {} @@ -73,6 +76,7 @@ export class SearchService implements Plugin { ): ISearchSetup { this.usageCollector = createUsageCollector(getStartServices, usageCollection); + this.sessionService = new SessionService(this.initializerContext, getStartServices); /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -83,6 +87,7 @@ export class SearchService implements Plugin { uiSettings, startServices: getStartServices(), usageCollector: this.usageCollector!, + session: this.sessionService, }); expressions.registerFunction(esdsl); @@ -104,6 +109,7 @@ export class SearchService implements Plugin { __enhance: (enhancements: SearchEnhancements) => { this.searchInterceptor = enhancements.searchInterceptor; }, + session: this.sessionService, }; } @@ -127,7 +133,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: { @@ -142,6 +148,7 @@ export class SearchService implements Plugin { showError: (e: Error) => { this.searchInterceptor.showError(e); }, + session: this.sessionService, searchSource: this.searchSourceService.start(indexPatterns, searchSourceDependencies), }; } diff --git a/src/plugins/data/public/search/session_service.test.ts b/src/plugins/data/public/search/session_service.test.ts new file mode 100644 index 000000000000..dd64d187f47d --- /dev/null +++ b/src/plugins/data/public/search/session_service.test.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SessionService } from './session_service'; +import { ISessionService } from '../../common'; +import { coreMock } from '../../../../core/public/mocks'; + +describe('Session service', () => { + let sessionService: ISessionService; + + beforeEach(() => { + const initializerContext = coreMock.createPluginInitializerContext(); + sessionService = new SessionService( + initializerContext, + coreMock.createSetup().getStartServices + ); + }); + + describe('Session management', () => { + it('Creates and clears a session', async () => { + sessionService.start(); + expect(sessionService.getSessionId()).not.toBeUndefined(); + sessionService.clear(); + expect(sessionService.getSessionId()).toBeUndefined(); + }); + }); +}); diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts new file mode 100644 index 000000000000..31524434af30 --- /dev/null +++ b/src/plugins/data/public/search/session_service.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import uuid from 'uuid'; +import { Subject, Subscription } from 'rxjs'; +import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; +import { ISessionService } from '../../common/search'; +import { ConfigSchema } from '../../config'; + +export class SessionService implements ISessionService { + private sessionId?: string; + private session$: Subject = new Subject(); + private appChangeSubscription$?: Subscription; + private curApp?: string; + + constructor( + initializerContext: PluginInitializerContext, + getStartServices: StartServicesAccessor + ) { + /* + Make sure that apps don't leave sessions open. + */ + getStartServices().then(([coreStart]) => { + this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => { + if (this.sessionId) { + const message = `Application '${this.curApp}' had an open session while navigating`; + if (initializerContext.env.mode.dev) { + // TODO: This setTimeout is necessary due to a race condition while navigating. + setTimeout(() => { + coreStart.fatalErrors.add(message); + }, 100); + } else { + // eslint-disable-next-line no-console + console.warn(message); + } + } + this.curApp = appName; + }); + }); + } + + public destroy() { + this.appChangeSubscription$?.unsubscribe(); + } + + public getSessionId() { + return this.sessionId; + } + + public getSession$() { + return this.session$.asObservable(); + } + + public start() { + this.sessionId = uuid.v4(); + this.session$.next(this.sessionId); + return this.sessionId; + } + + public clear() { + this.sessionId = undefined; + this.session$.next(this.sessionId); + } +} diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 85ef7aa4d97c..c08d9f4c7be3 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -21,7 +21,7 @@ import { PackageInfo } from 'kibana/server'; import { ISearchInterceptor } from './search_interceptor'; import { SearchUsageCollector } from './collectors'; import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } from './aggs'; -import { ISearchGeneric, ISearchStartSearchSource } from '../../common/search'; +import { ISearchGeneric, ISessionService, ISearchStartSearchSource } from '../../common/search'; import { IndexPatternsContract } from '../../common/index_patterns/index_patterns'; import { UsageCollectionSetup } from '../../../usage_collection/public'; @@ -38,6 +38,11 @@ export interface SearchEnhancements { export interface ISearchSetup { aggs: AggsSetup; usageCollector?: SearchUsageCollector; + /** + * session management + * {@link ISessionService} + */ + session: ISessionService; /** * @internal */ @@ -67,6 +72,11 @@ export interface ISearchStart { * {@link ISearchStartSearchSource} */ searchSource: ISearchStartSearchSource; + /** + * session management + * {@link ISessionService} + */ + session: ISessionService; } export { SEARCH_EVENT_TYPE } from './collectors'; diff --git a/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx index 80e1a26163b7..19606cafc5c8 100644 --- a/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx @@ -18,17 +18,12 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import { IIndexPattern, Filter } from '../..'; type CancelFnType = () => void; type SubmitFnType = (filters: Filter[]) => void; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyApplyFiltersPopoverContent = React.lazy(() => import('./apply_filter_popover_content')); diff --git a/src/plugins/data/public/ui/filter_bar/_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: (
( - - - -); +const Fallback = () =>
; const LazyFilterLabel = React.lazy(() => import('./filter_editor/lib/filter_label')); export const FilterLabel = (props: FilterLabelProps) => ( diff --git a/src/plugins/data/public/ui/index_pattern_select/index.tsx b/src/plugins/data/public/ui/index_pattern_select/index.tsx index f0db37eb963f..c909b202a409 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index.tsx @@ -18,14 +18,9 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import type { IndexPatternSelectInternalProps } from './index_pattern_select'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyIndexPatternSelect = React.lazy(() => import('./index_pattern_select')); export const IndexPatternSelect = (props: IndexPatternSelectInternalProps) => ( diff --git a/src/plugins/data/public/ui/query_string_input/index.tsx b/src/plugins/data/public/ui/query_string_input/index.tsx index 5bc5bd509796..eb6641bf3661 100644 --- a/src/plugins/data/public/ui/query_string_input/index.tsx +++ b/src/plugins/data/public/ui/query_string_input/index.tsx @@ -18,16 +18,11 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import { withKibana } from '../../../../kibana_react/public'; import type { QueryBarTopRowProps } from './query_bar_top_row'; import type { QueryStringInputProps } from './query_string_input'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyQueryBarTopRow = React.lazy(() => import('./query_bar_top_row')); export const QueryBarTopRow = (props: QueryBarTopRowProps) => ( diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index e01fbedbe38d..7a44b924870f 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -36,12 +36,14 @@ import { EuiSuperUpdateButton, OnRefreshProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Toast } from 'src/core/public'; import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..'; -import { useKibana, toMountPoint } from '../../../../kibana_react/public'; -import { QueryStringInput } from './'; +import { useKibana, toMountPoint, withKibana } from '../../../../kibana_react/public'; +import QueryStringInputUI from './query_string_input'; import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; import { PersistedLog, getQueryLog } from '../../query'; import { NoDataPopover } from './no_data_popover'; +const QueryStringInput = withKibana(QueryStringInputUI); + // @internal export interface QueryBarTopRowProps { query?: Query; diff --git a/src/plugins/data/public/ui/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx index d81ed7333655..310542f4b12b 100644 --- a/src/plugins/data/public/ui/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -19,15 +19,10 @@ import React from 'react'; import { injectI18n } from '@kbn/i18n/react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import { withKibana } from '../../../../kibana_react/public'; import type { SearchBarProps } from './search_bar'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazySearchBar = React.lazy(() => import('./search_bar')); const WrappedSearchBar = (props: SearchBarProps) => ( diff --git a/src/plugins/data/public/ui/search_bar/search_bar.test.tsx b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx index a89b9bb7f91e..74992f35fffc 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.test.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx @@ -18,10 +18,7 @@ */ import React from 'react'; -import { waitFor } from '@testing-library/dom'; -import { render } from '@testing-library/react'; - -import { SearchBar } from './'; +import SearchBar from './search_bar'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { I18nProvider } from '@kbn/i18n/react'; @@ -29,6 +26,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { coreMock } from '../../../../../core/public/mocks'; const startMock = coreMock.createStart(); +import { mount } from 'enzyme'; import { IIndexPattern } from '../..'; const mockTimeHistory = { @@ -37,16 +35,14 @@ const mockTimeHistory = { }, }; -jest.mock('..', () => { +jest.mock('../filter_bar/filter_bar', () => { return { FilterBar: () =>
, }; }); -jest.mock('../query_string_input', () => { - return { - QueryBarTopRow: () =>
, - }; +jest.mock('../query_string_input/query_bar_top_row', () => { + return () =>
; }); const noop = jest.fn(); @@ -117,48 +113,42 @@ function wrapSearchBarInContext(testProps: any) { ); } -// FLAKY: https://github.com/elastic/kibana/issues/79910 -describe.skip('SearchBar', () => { - const SEARCH_BAR_TEST_ID = 'globalQueryBar'; +describe('SearchBar', () => { const SEARCH_BAR_ROOT = '.globalQueryBar'; - const FILTER_BAR = '.globalFilterBar'; + const FILTER_BAR = '.filterBar'; const QUERY_BAR = '.queryBar'; beforeEach(() => { jest.clearAllMocks(); }); - it('Should render query bar when no options provided (in reality - timepicker)', async () => { - const { container, getByTestId } = render( + it('Should render query bar when no options provided (in reality - timepicker)', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(1); }); - it('Should render empty when timepicker is off and no options provided', async () => { - const { container, getByTestId } = render( + it('Should render empty when timepicker is off and no options provided', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showDatePicker: false, }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should render filter bar, when required fields are provided', async () => { - const { container, getByTestId } = render( + it('Should render filter bar, when required fields are provided', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showDatePicker: false, @@ -167,15 +157,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(1); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(1); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should NOT render filter bar, if disabled', async () => { - const { container, getByTestId } = render( + it('Should NOT render filter bar, if disabled', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showFilterBar: false, @@ -185,15 +173,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should render query bar, when required fields are provided', async () => { - const { container, getByTestId } = render( + it('Should render query bar, when required fields are provided', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -202,15 +188,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(1); }); - it('Should NOT render query bar, if disabled', async () => { - const { container, getByTestId } = render( + it('Should NOT render query bar, if disabled', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -220,15 +204,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should render query bar and filter bar', async () => { - const { container, getByTestId } = render( + it('Should render query bar and filter bar', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -239,10 +221,8 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(1); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(1); + expect(component.find(QUERY_BAR).length).toBe(1); }); }); diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 95651ac9ed8b..daa6fa0dd80a 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -26,7 +26,7 @@ import { get, isEqual } from 'lodash'; import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public'; -import { QueryBarTopRow } from '../query_string_input'; +import QueryBarTopRow from '../query_string_input/query_bar_top_row'; import { SavedQueryAttributes, TimeHistoryContract, SavedQuery } from '../../query'; import { IDataPluginServices } from '../../types'; import { TimeRange, Query, Filter, IIndexPattern } from '../../../common'; diff --git a/src/plugins/data/public/ui/shard_failure_modal/index.tsx b/src/plugins/data/public/ui/shard_failure_modal/index.tsx index cea882deff36..2ac470573c42 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/index.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/index.tsx @@ -18,14 +18,9 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import type { ShardFailureOpenModalButtonProps } from './shard_failure_open_modal_button'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyShardFailureOpenModalButton = React.lazy( () => import('./shard_failure_open_modal_button') diff --git a/src/plugins/data/public/ui/typeahead/index.tsx b/src/plugins/data/public/ui/typeahead/index.tsx index aa3c2d71300d..58547cd2ccbe 100644 --- a/src/plugins/data/public/ui/typeahead/index.tsx +++ b/src/plugins/data/public/ui/typeahead/index.tsx @@ -18,14 +18,9 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import type { SuggestionsComponentProps } from './suggestions_component'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazySuggestionsComponent = React.lazy(() => import('./suggestions_component')); export const SuggestionsComponent = (props: SuggestionsComponentProps) => ( diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts index 35cee799ddb6..1794df7391cb 100644 --- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts +++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts @@ -19,6 +19,8 @@ import { fetchProvider } from './fetch'; import { LegacyAPICaller } from 'kibana/server'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; jest.mock('../../../common', () => ({ 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 e9336302ed82..add923ad2da4 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'; @@ -691,6 +692,7 @@ export class IndexPatternsService implements Plugin_3 ISearchStrategy; // (undocumented) - search: (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise; + search: ISearchStrategy['search']; // (undocumented) searchSource: { asScoped: (request: KibanaRequest) => Promise; @@ -734,7 +736,7 @@ export interface ISearchStrategy Promise; // (undocumented) - search: (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise; + search: (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable; } // @public (undocumented) @@ -1147,7 +1149,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/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 078a04732411..612cedb7780b 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -193,7 +193,7 @@ app.directive('discoverApp', function () { function discoverController($element, $route, $scope, $timeout, $window, Promise, uiCapabilities) { const { isDefault: isDefaultType } = indexPatternsUtils; const subscriptions = new Subscription(); - const $fetchObservable = new Subject(); + const refetch$ = new Subject(); let inspectorRequest; const savedSearch = $route.current.locals.savedObjects.savedSearch; $scope.searchSource = savedSearch.searchSource; @@ -267,7 +267,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise ); if (changes.length) { - $fetchObservable.next(); + refetch$.next(); } }); } @@ -332,6 +332,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise if (abortController) abortController.abort(); savedSearch.destroy(); subscriptions.unsubscribe(); + data.search.session.clear(); appStateUnsubscribe(); stopStateSync(); stopSyncingGlobalStateWithUrl(); @@ -633,12 +634,18 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise const init = _.once(() => { $scope.updateDataSource().then(async () => { - const searchBarChanges = merge(data.query.state$, $fetchObservable).pipe(debounceTime(100)); + const fetch$ = merge( + refetch$, + filterManager.getFetches$(), + timefilter.getFetch$(), + timefilter.getAutoRefreshFetch$(), + data.query.queryString.getUpdates$() + ).pipe(debounceTime(100)); subscriptions.add( subscribeWithScope( $scope, - searchBarChanges, + fetch$, { next: $scope.fetch, }, @@ -717,7 +724,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise init.complete = true; if (shouldSearchOnPageLoad()) { - $fetchObservable.next(); + refetch$.next(); } }); }); @@ -788,6 +795,8 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise if (abortController) abortController.abort(); abortController = new AbortController(); + const sessionId = data.search.session.start(); + $scope .updateDataSource() .then(setupVisualization) @@ -796,6 +805,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise logInspectorRequest(); return $scope.searchSource.fetch({ abortSignal: abortController.signal, + sessionId, }); }) .then(onResults) @@ -812,7 +822,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise $scope.handleRefresh = function (_payload, isUpdate) { if (isUpdate === false) { - $fetchObservable.next(); + refetch$.next(); } }; 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, + }, + }) ) : ( { embeddable?: T; data: { data: Array<{ - table: Pick; + table: Pick; column: number; row: number; value: any; @@ -42,7 +42,7 @@ export interface ValueClickContext { export interface RangeSelectContext { embeddable?: T; data: { - table: KibanaDatatable; + table: Datatable; column: number; range: number[]; timeFieldName?: string; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 6280d3a2e4a5..a6d90f2766c1 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -326,7 +326,7 @@ export abstract class Embeddable { +export class EmbeddableChildPanel extends React.Component { constructor(props: EmbeddableChildPanelProps); // (undocumented) [panel: string]: any; @@ -477,7 +477,7 @@ export interface EmbeddablePackageState { // Warning: (ae-missing-release-tag) "EmbeddablePanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class EmbeddablePanel extends React.Component { +export class EmbeddablePanel extends React.Component { constructor(props: Props); // (undocumented) closeMyContextMenuPanel: () => void; @@ -810,7 +810,7 @@ export interface PropertySpec { export interface RangeSelectContext { // (undocumented) data: { - table: KibanaDatatable; + table: Datatable; column: number; range: number[]; timeFieldName?: string; @@ -841,7 +841,7 @@ export interface ValueClickContext { // (undocumented) data: { data: Array<{ - table: Pick; + table: Pick; column: number; row: number; value: any; @@ -881,7 +881,7 @@ export const withEmbeddableSubscription: { + // error.name is slightly better in terms of performance, since all errors now have name property + if (error.name === 'ResponseError') { + const { statusCode, body } = error as ResponseError; + return response.customError({ + statusCode, + body: { message: body.error?.reason }, + }); + } + // Case: default + return response.internalError({ body: error }); +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts index 0d025442f4a9..484dc17868ab 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts @@ -18,3 +18,4 @@ */ export { isEsError } from './is_es_error'; +export { handleEsError } from './handle_es_error'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/is_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/is_es_error.ts index 1e212307ca1c..80a53aac328a 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/is_es_error.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/is_es_error.ts @@ -25,6 +25,10 @@ interface RequestError extends Error { statusCode?: number; } +/* + * @deprecated + * Only works with legacy elasticsearch js client errors and will be removed after 7.x last + */ export function isEsError(err: RequestError) { const isInstanceOfEsError = err instanceof esErrorsParent; const hasStatusCode = Boolean(err.statusCode); diff --git a/src/plugins/es_ui_shared/server/errors/index.ts b/src/plugins/es_ui_shared/server/errors/index.ts index c18374cd9ec3..532e02774ff5 100644 --- a/src/plugins/es_ui_shared/server/errors/index.ts +++ b/src/plugins/es_ui_shared/server/errors/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { isEsError } from '../../__packages_do_not_import__/errors'; +export { isEsError, handleEsError } from '../../__packages_do_not_import__/errors'; diff --git a/src/plugins/es_ui_shared/server/index.ts b/src/plugins/es_ui_shared/server/index.ts index 0118bbda5326..b2c9c85d956b 100644 --- a/src/plugins/es_ui_shared/server/index.ts +++ b/src/plugins/es_ui_shared/server/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { isEsError } from './errors'; +export { isEsError, handleEsError } from './errors'; /** dummy plugin*/ export function plugin() { diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx index c8ba9f5ac410..b80d6caf54f4 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx @@ -39,8 +39,8 @@ export const CheckBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > = Record = Record - >(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) { - const execution = this.createExecution(ast, context); + >( + ast: string | ExpressionAstExpression, + input: Input, + context?: ExtraContext, + options?: ExpressionExecOptions + ) { + const execution = this.createExecution(ast, context, options); execution.start(input); return (await execution.result) as Output; } diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index c201e99faeb0..abb8fbd4348c 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -22,6 +22,7 @@ import { map, pick, zipObject } from 'lodash'; import { ExpressionTypeDefinition } from '../types'; import { PointSeries, PointSeriesColumn } from './pointseries'; import { ExpressionValueRender } from './render'; +import { SerializedFieldFormat } from '../../types'; type State = string | number | boolean | null | undefined | SerializableState; @@ -41,22 +42,58 @@ export const isDatatable = (datatable: unknown): datatable is Datatable => /** * This type represents the `type` of any `DatatableColumn` in a `Datatable`. + * its duplicated from KBN_FIELD_TYPES */ -export type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; +export type DatatableColumnType = + | '_source' + | 'attachment' + | 'boolean' + | 'date' + | 'geo_point' + | 'geo_shape' + | 'ip' + | 'murmur3' + | 'number' + | 'string' + | 'unknown' + | 'conflict' + | 'object' + | 'nested' + | 'histogram' + | 'null'; /** * This type represents a row in a `Datatable`. */ export type DatatableRow = Record; +/** + * Datatable column meta information + */ export interface DatatableColumnMeta { type: DatatableColumnType; + /** + * field this column is based on + */ field?: string; + /** + * index/table this column is based on + */ index?: string; - params?: SerializableState; + /** + * serialized field format + */ + params?: SerializedFieldFormat; + /** + * source function that produced this column + */ source?: string; + /** + * any extra parameters for the source that produced this column + */ sourceParams?: SerializableState; } + /** * This type represents the shape of a column in a `Datatable`. */ diff --git a/src/plugins/expressions/common/expression_types/specs/index.ts b/src/plugins/expressions/common/expression_types/specs/index.ts index 31210b11f6b7..00c52a2545cd 100644 --- a/src/plugins/expressions/common/expression_types/specs/index.ts +++ b/src/plugins/expressions/common/expression_types/specs/index.ts @@ -23,7 +23,6 @@ import { error } from './error'; import { filter } from './filter'; import { image } from './image'; import { kibanaContext } from './kibana_context'; -import { kibanaDatatable } from './kibana_datatable'; import { nullType } from './null'; import { num } from './num'; import { number } from './number'; @@ -42,7 +41,6 @@ export const typeSpecs: AnyExpressionTypeDefinition[] = [ filter, image, kibanaContext, - kibanaDatatable, nullType, num, number, @@ -60,7 +58,6 @@ export * from './error'; export * from './filter'; export * from './image'; export * from './kibana_context'; -export * from './kibana_datatable'; export * from './null'; export * from './num'; export * from './number'; diff --git a/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts b/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts deleted file mode 100644 index e226f3b124ee..000000000000 --- a/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { map } from 'lodash'; -import { SerializedFieldFormat } from '../../types/common'; -import { Datatable, PointSeries, PointSeriesColumn } from '.'; - -const name = 'kibana_datatable'; - -export interface KibanaDatatableColumnMeta { - type: string; - indexPatternId?: string; - aggConfigParams?: Record; -} - -export interface KibanaDatatableColumn { - id: string; - name: string; - meta?: KibanaDatatableColumnMeta; - formatHint?: SerializedFieldFormat; -} - -export interface KibanaDatatableRow { - [key: string]: unknown; -} - -export interface KibanaDatatable { - type: typeof name; - columns: KibanaDatatableColumn[]; - rows: KibanaDatatableRow[]; -} - -export const kibanaDatatable = { - name, - from: { - datatable: (context: Datatable) => { - return { - type: name, - rows: context.rows, - columns: context.columns.map((column) => { - return { - id: column.name, - name: column.name, - }; - }), - }; - }, - pointseries: (context: PointSeries) => { - const columns = map(context.columns, (column: PointSeriesColumn, n) => { - return { id: n, name: n, ...column }; - }); - return { - type: name, - rows: context.rows, - columns, - }; - }, - }, -}; diff --git a/src/plugins/expressions/common/expression_types/specs/range.ts b/src/plugins/expressions/common/expression_types/specs/range.ts index 3d7170cf715d..53fd4894fd2b 100644 --- a/src/plugins/expressions/common/expression_types/specs/range.ts +++ b/src/plugins/expressions/common/expression_types/specs/range.ts @@ -26,6 +26,7 @@ export interface Range { type: typeof name; from: number; to: number; + label?: string; } export const range: ExpressionTypeDefinition = { @@ -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/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index 4a87fd9e7f33..3d0fb968e8a3 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Executor } from '../executor'; +import { Executor, ExpressionExecOptions } from '../executor'; import { AnyExpressionRenderDefinition, ExpressionRendererRegistry } from '../expression_renderers'; import { ExpressionAstExpression } from '../ast'; import { ExecutionContract } from '../execution/execution_contract'; @@ -101,7 +101,8 @@ export interface ExpressionsServiceStart { run: = Record>( ast: string | ExpressionAstExpression, input: Input, - context?: ExtraContext + context?: ExtraContext, + options?: ExpressionExecOptions ) => Promise; /** @@ -117,7 +118,8 @@ export interface ExpressionsServiceStart { ast: string | ExpressionAstExpression, // This any is for legacy reasons. input: Input, - context?: ExtraContext + context?: ExtraContext, + options?: ExpressionExecOptions ) => ExecutionContract; /** @@ -212,8 +214,8 @@ export class ExpressionsService implements PersistableState AnyExpressionRenderDefinition) ): void => this.renderers.register(definition); - public readonly run: ExpressionsServiceStart['run'] = (ast, input, context) => - this.executor.run(ast, input, context); + public readonly run: ExpressionsServiceStart['run'] = (ast, input, context, options) => + this.executor.run(ast, input, context, options); public readonly getFunction: ExpressionsServiceStart['getFunction'] = (name) => this.executor.getFunction(name); @@ -244,8 +246,8 @@ export class ExpressionsService implements PersistableState => this.executor.getTypes(); - public readonly execute: ExpressionsServiceStart['execute'] = ((ast, input, context) => { - const execution = this.executor.createExecution(ast, context); + public readonly execute: ExpressionsServiceStart['execute'] = ((ast, input, context, options) => { + const execution = this.executor.createExecution(ast, context, options); execution.start(input); return execution.contract; }) as ExpressionsServiceStart['execute']; 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/index.ts b/src/plugins/expressions/public/index.ts index 039890c9233c..893d68238747 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -98,10 +98,6 @@ export { isExpressionAstBuilder, KIBANA_CONTEXT_NAME, KibanaContext, - KibanaDatatable, - KibanaDatatableColumn, - KibanaDatatableColumnMeta, - KibanaDatatableRow, KnownTypeToString, Overflow, parse, diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index c4c40e0812e4..aef4b73f86e3 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -145,11 +145,18 @@ export class ExpressionLoader { this.execution.cancel(); } this.setParams(params); - this.execution = getExpressionsService().execute(expression, params.context, { - search: params.searchContext, - variables: params.variables || {}, - inspectorAdapters: params.inspectorAdapters, - }); + this.execution = getExpressionsService().execute( + expression, + params.context, + { + search: params.searchContext, + variables: params.variables || {}, + inspectorAdapters: params.inspectorAdapters, + }, + { + debug: params.debug, + } + ); const prevDataHandler = this.execution; const data = await prevDataHandler.getData(); @@ -181,6 +188,7 @@ export class ExpressionLoader { if (params.variables && this.params) { this.params.variables = params.variables; } + this.params.debug = Boolean(params.debug); this.params.inspectorAdapters = (params.inspectorAdapters || this.execution?.inspect()) as Adapters; diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 355bd502df3b..95ee651d433a 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -82,7 +82,7 @@ export interface DatatableColumn { // Warning: (ae-missing-release-tag) "DatatableColumnType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; +export type DatatableColumnType = '_source' | 'attachment' | 'boolean' | 'date' | 'geo_point' | 'geo_shape' | 'ip' | 'murmur3' | 'number' | 'string' | 'unknown' | 'conflict' | 'object' | 'nested' | 'histogram' | 'null'; // Warning: (ae-missing-release-tag) "DatatableRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -228,7 +228,7 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; - run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; + run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; // (undocumented) readonly state: ExecutorContainer; // (undocumented) @@ -511,6 +511,8 @@ export class ExpressionRendererRegistry implements IRegistry // // @public (undocumented) export interface ExpressionRenderError extends Error { + // (undocumented) + original?: Error; // (undocumented) type?: string; } @@ -607,12 +609,12 @@ export type ExpressionsServiceSetup = Pick = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => ExecutionContract; + execute: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => ExecutionContract; fork: () => ExpressionsService; getFunction: (name: string) => ReturnType; getRenderer: (name: string) => ReturnType; getType: (name: string) => ReturnType; - run: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => Promise; + run: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => Promise; } // Warning: (ae-missing-release-tag) "ExpressionsSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -876,6 +878,8 @@ export interface IExpressionLoaderParams { // (undocumented) customRenderers?: []; // (undocumented) + debug?: boolean; + // (undocumented) disableCaching?: boolean; // (undocumented) inspectorAdapters?: Adapters; @@ -940,54 +944,6 @@ export type KIBANA_CONTEXT_NAME = 'kibana_context'; // @public (undocumented) export type KibanaContext = ExpressionValueSearchContext; -// Warning: (ae-missing-release-tag) "KibanaDatatable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatable { - // (undocumented) - columns: KibanaDatatableColumn[]; - // (undocumented) - rows: KibanaDatatableRow[]; - // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts - // - // (undocumented) - type: typeof name_3; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableColumn { - // (undocumented) - formatHint?: SerializedFieldFormat; - // (undocumented) - id: string; - // (undocumented) - meta?: KibanaDatatableColumnMeta; - // (undocumented) - name: string; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableColumnMeta" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableColumnMeta { - // (undocumented) - aggConfigParams?: Record; - // (undocumented) - indexPatternId?: string; - // (undocumented) - type: string; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableRow { - // (undocumented) - [key: string]: unknown; -} - // Warning: (ae-missing-release-tag) "KnownTypeToString" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -1067,11 +1023,13 @@ 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 // // (undocumented) - type: typeof name_4; + type: typeof name_3; } // Warning: (ae-missing-release-tag) "ReactExpressionRenderer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1095,7 +1053,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)}
; + // Enables debug tracking on each expression in the AST + debug?: boolean; disableCaching?: boolean; customFunctions?: []; customRenderers?: []; @@ -55,6 +57,7 @@ export interface IExpressionLoaderParams { export interface ExpressionRenderError extends Error { type?: string; + original?: Error; } export type RenderErrorHandlerFnType = ( diff --git a/src/plugins/expressions/server/index.ts b/src/plugins/expressions/server/index.ts index 678545732159..cc22d4b500d9 100644 --- a/src/plugins/expressions/server/index.ts +++ b/src/plugins/expressions/server/index.ts @@ -89,10 +89,6 @@ export { isExpressionAstBuilder, KIBANA_CONTEXT_NAME, KibanaContext, - KibanaDatatable, - KibanaDatatableColumn, - KibanaDatatableColumnMeta, - KibanaDatatableRow, KnownTypeToString, Overflow, parse, diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 1a905de4a3bd..d5da60af8f8e 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -79,7 +79,7 @@ export interface DatatableColumn { // Warning: (ae-missing-release-tag) "DatatableColumnType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; +export type DatatableColumnType = '_source' | 'attachment' | 'boolean' | 'date' | 'geo_point' | 'geo_shape' | 'ip' | 'murmur3' | 'number' | 'string' | 'unknown' | 'conflict' | 'object' | 'nested' | 'histogram' | 'null'; // Warning: (ae-missing-release-tag) "DatatableRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -210,7 +210,7 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; - run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; + run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; // (undocumented) readonly state: ExecutorContainer; // (undocumented) @@ -768,54 +768,6 @@ export type KIBANA_CONTEXT_NAME = 'kibana_context'; // @public (undocumented) export type KibanaContext = ExpressionValueSearchContext; -// Warning: (ae-missing-release-tag) "KibanaDatatable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatable { - // (undocumented) - columns: KibanaDatatableColumn[]; - // (undocumented) - rows: KibanaDatatableRow[]; - // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts - // - // (undocumented) - type: typeof name_3; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableColumn { - // (undocumented) - formatHint?: SerializedFieldFormat; - // (undocumented) - id: string; - // (undocumented) - meta?: KibanaDatatableColumnMeta; - // (undocumented) - name: string; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableColumnMeta" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableColumnMeta { - // (undocumented) - aggConfigParams?: Record; - // (undocumented) - indexPatternId?: string; - // (undocumented) - type: string; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableRow { - // (undocumented) - [key: string]: unknown; -} - // Warning: (ae-missing-release-tag) "KnownTypeToString" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -894,11 +846,13 @@ 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 // // (undocumented) - type: typeof name_4; + type: typeof name_3; } // Warning: (ae-missing-release-tag) "SerializedDatatable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts b/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts index 736e79015af9..54fed3db1de4 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts @@ -17,39 +17,39 @@ * under the License. */ -import sinon from 'sinon'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { fetchProvider } from './collector_fetch'; -describe('Sample Data Fetch', () => { - 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/input_control_vis/public/input_control_fn.ts b/src/plugins/input_control_vis/public/input_control_fn.ts index 59c0e03505bb..1664555b916b 100644 --- a/src/plugins/input_control_vis/public/input_control_fn.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; interface Arguments { visConfig: string; @@ -34,7 +34,7 @@ interface RenderValue { export const createInputControlVisFn = (): ExpressionFunctionDefinition< 'input_control_vis', - KibanaDatatable, + Datatable, Arguments, Render > => ({ 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/region_map/public/__snapshots__/region_map_fn.test.js.snap b/src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap index 2d615a105906..cb12712ae824 100644 --- a/src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap +++ b/src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap @@ -50,7 +50,7 @@ Object { "col-0-1": 0, }, ], - "type": "kibana_datatable", + "type": "datatable", }, "visType": "region_map", }, diff --git a/src/plugins/region_map/public/region_map_fn.js b/src/plugins/region_map/public/region_map_fn.js index 314def1fbfdc..fdb7c273720f 100644 --- a/src/plugins/region_map/public/region_map_fn.js +++ b/src/plugins/region_map/public/region_map_fn.js @@ -23,7 +23,7 @@ export const createRegionMapFn = () => ({ name: 'regionmap', type: 'render', context: { - types: ['kibana_datatable'], + types: ['datatable'], }, help: i18n.translate('regionMap.function.help', { defaultMessage: 'Regionmap visualization', diff --git a/src/plugins/region_map/public/region_map_fn.test.js b/src/plugins/region_map/public/region_map_fn.test.js index 684cc5e897df..32467541dee0 100644 --- a/src/plugins/region_map/public/region_map_fn.test.js +++ b/src/plugins/region_map/public/region_map_fn.test.js @@ -24,7 +24,7 @@ import { createRegionMapFn } from './region_map_fn'; describe('interpreter/functions#regionmap', () => { const fn = functionWrapper(createRegionMapFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; 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/saved_objects/public/mocks.ts b/src/plugins/saved_objects/public/mocks.ts new file mode 100644 index 000000000000..d34a6ded7c8d --- /dev/null +++ b/src/plugins/saved_objects/public/mocks.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { 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/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx index d788a25eb7d6..fc4d111d2a04 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -77,7 +77,9 @@ const SavedObjectsTablePage = ({ goInspectObject={(savedObject) => { const { editUrl } = savedObject.meta; if (editUrl) { - return coreStart.application.navigateToUrl('/app' + editUrl); + return coreStart.application.navigateToUrl( + coreStart.http.basePath.prepend(`/app${editUrl}`) + ); } }} canGoInApp={(savedObject) => { 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/tile_map/public/tile_map_fn.js b/src/plugins/tile_map/public/tile_map_fn.js index 5f43077bcb24..3253598d98d9 100644 --- a/src/plugins/tile_map/public/tile_map_fn.js +++ b/src/plugins/tile_map/public/tile_map_fn.js @@ -23,7 +23,7 @@ export const createTileMapFn = () => ({ name: 'tilemap', type: 'render', context: { - types: ['kibana_datatable'], + types: ['datatable'], }, help: i18n.translate('tileMap.function.help', { defaultMessage: 'Tilemap visualization', diff --git a/src/plugins/tile_map/public/tile_map_visualization.js b/src/plugins/tile_map/public/tile_map_visualization.js index b09a2f3bac48..80084be28365 100644 --- a/src/plugins/tile_map/public/tile_map_visualization.js +++ b/src/plugins/tile_map/public/tile_map_visualization.js @@ -73,19 +73,19 @@ export const createTileMapVisualization = (dependencies) => { }; const bounds = this._kibanaMap.getBounds(); const mapCollar = scaleBounds(bounds); - if (!geoContains(geohashAgg.aggConfigParams.boundingBox, mapCollar)) { + if (!geoContains(geohashAgg.sourceParams.params.boundingBox, mapCollar)) { updateVarsObject.data.boundingBox = { top_left: mapCollar.top_left, bottom_right: mapCollar.bottom_right, }; } else { - updateVarsObject.data.boundingBox = geohashAgg.aggConfigParams.boundingBox; + updateVarsObject.data.boundingBox = geohashAgg.sourceParams.params.boundingBox; } // todo: autoPrecision should be vis parameter, not aggConfig one const zoomPrecision = getZoomPrecision(); - updateVarsObject.data.precision = geohashAgg.aggConfigParams.autoPrecision + updateVarsObject.data.precision = geohashAgg.sourceParams.params.autoPrecision ? zoomPrecision[this.vis.getUiState().get('mapZoom')] - : getPrecision(geohashAgg.aggConfigParams.precision); + : getPrecision(geohashAgg.sourceParams.params.precision); this.vis.eventsSubject.next(updateVarsObject); }; @@ -118,8 +118,8 @@ export const createTileMapVisualization = (dependencies) => { return; } const isAutoPrecision = - typeof geohashAgg.aggConfigParams.autoPrecision === 'boolean' - ? geohashAgg.aggConfigParams.autoPrecision + typeof geohashAgg.sourceParams.params.autoPrecision === 'boolean' + ? geohashAgg.sourceParams.params.autoPrecision : true; if (!isAutoPrecision) { return; @@ -243,7 +243,7 @@ export const createTileMapVisualization = (dependencies) => { } const indexPatternName = agg.indexPatternId; - const field = agg.aggConfigParams.field; + const field = agg.field; const filter = { meta: { negate: false, index: indexPatternName } }; filter[filterName] = { ignore_unmapped: true }; filter[filterName][field] = filterData; @@ -264,7 +264,7 @@ export const createTileMapVisualization = (dependencies) => { const DEFAULT = false; const agg = this._getGeoHashAgg(); if (agg) { - return get(agg, 'aggConfigParams.isFilteredByCollar', DEFAULT); + return get(agg, 'sourceParams.params.isFilteredByCollar', DEFAULT); } else { return DEFAULT; } diff --git a/src/plugins/tile_map/public/tilemap_fn.test.js b/src/plugins/tile_map/public/tilemap_fn.test.js index 8fa12c9f9dbb..df9fc10a7303 100644 --- a/src/plugins/tile_map/public/tilemap_fn.test.js +++ b/src/plugins/tile_map/public/tilemap_fn.test.js @@ -41,7 +41,7 @@ import { convertToGeoJson } from '../../maps_legacy/public'; describe('interpreter/functions#tilemap', () => { const fn = functionWrapper(createTileMapFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; 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/_app.scss b/src/plugins/timelion/public/_app.scss index 8b9078caba5a..d8a6eb423a67 100644 --- a/src/plugins/timelion/public/_app.scss +++ b/src/plugins/timelion/public/_app.scss @@ -15,7 +15,69 @@ margin: $euiSizeM; } +.timApp__title { + display: flex; + align-items: center; + padding: $euiSizeM $euiSizeS; + font-size: $euiSize; + font-weight: $euiFontWeightBold; + border-bottom: 1px solid $euiColorLightShade; + flex-grow: 1; + background-color: $euiColorEmptyShade; +} + .timApp__stats { font-weight: $euiFontWeightRegular; color: $euiColorMediumShade; } + +.timApp__form { + display: flex; + align-items: flex-start; + margin-top: $euiSize; + margin-bottom: $euiSize; +} + +.timApp__expression { + display: flex; + flex: 1; + margin-right: $euiSizeS; +} + +.timApp__button { + margin-top: $euiSizeS; + padding: $euiSizeXS $euiSizeM; + font-size: $euiSize; + border: none; + border-radius: $euiSizeXS; + color: $euiColorEmptyShade; + background-color: $euiColorPrimary; +} + +.timApp__button--secondary { + margin-top: $euiSizeS; + padding: $euiSizeXS $euiSizeM; + font-size: $euiSize; + border: 1px solid $euiColorPrimary; + border-radius: $euiSizeXS; + color: $euiColorPrimary; + width: 100%; +} + +.timApp__sectionTitle { + margin-bottom: $euiSizeM; + font-size: 18px; + color: $euiColorDarkestShade; +} + +.timApp__helpText { + margin-bottom: $euiSize; + font-size: 14px; + color: $euiColorDarkShade; +} + +.timApp__label { + font-size: $euiSize; + line-height: 1.5; + font-weight: $euiFontWeightBold; +} diff --git a/src/plugins/timelion/public/app.js b/src/plugins/timelion/public/app.js index 40fffe7a5a06..3838b319cda6 100644 --- a/src/plugins/timelion/public/app.js +++ b/src/plugins/timelion/public/app.js @@ -44,6 +44,7 @@ import { initSavedObjectSaveAsCheckBoxDirective } from './directives/saved_objec import { initSavedObjectFinderDirective } from './directives/saved_object_finder'; import { initTimelionTabsDirective } from './components/timelionhelp_tabs_directive'; import { initTimelionTDeprecationDirective } from './components/timelion_deprecation_directive'; +import { initTimelionTopNavDirective } from './components/timelion_top_nav_directive'; import { initInputFocusDirective } from './directives/input_focus'; import { Chart } from './directives/chart/chart'; import { TimelionInterval } from './directives/timelion_interval/timelion_interval'; @@ -86,6 +87,7 @@ export function initTimelionApp(app, deps) { initInputFocusDirective(app); initTimelionTabsDirective(app, deps); initTimelionTDeprecationDirective(app, deps); + initTimelionTopNavDirective(app, deps); initSavedObjectFinderDirective(app, savedSheetLoader, deps.core.uiSettings); initSavedObjectSaveAsCheckBoxDirective(app); initCellsDirective(app); diff --git a/src/plugins/timelion/public/application.ts b/src/plugins/timelion/public/application.ts index a4963ee6b1b0..4f58b83a4910 100644 --- a/src/plugins/timelion/public/application.ts +++ b/src/plugins/timelion/public/application.ts @@ -36,12 +36,8 @@ import { import { getTimeChart } from './panels/timechart/timechart'; import { Panel } from './panels/panel'; -import { - configureAppAngularModule, - createTopNavDirective, - createTopNavHelper, -} from '../../kibana_legacy/public'; -import { TimelionPluginDependencies } from './plugin'; +import { configureAppAngularModule } from '../../kibana_legacy/public'; +import { TimelionPluginStartDependencies } from './plugin'; import { DataPublicPluginStart } from '../../data/public'; // @ts-ignore import { initTimelionApp } from './app'; @@ -50,7 +46,7 @@ export interface RenderDeps { pluginInitializerContext: PluginInitializerContext; mountParams: AppMountParameters; core: CoreStart; - plugins: TimelionPluginDependencies; + plugins: TimelionPluginStartDependencies; timelionPanels: Map; } @@ -120,11 +116,9 @@ function mountTimelionApp(appBasePath: string, element: HTMLElement, deps: Rende function createLocalAngularModule(deps: RenderDeps) { createLocalI18nModule(); createLocalIconModule(); - createLocalTopNavModule(deps.plugins.navigation); const dashboardAngularModule = angular.module(moduleName, [ ...thirdPartyAngularDependencies, - 'app/timelion/TopNav', 'app/timelion/I18n', 'app/timelion/icon', ]); @@ -137,13 +131,6 @@ function createLocalIconModule() { .directive('icon', (reactDirective) => reactDirective(EuiIcon)); } -function createLocalTopNavModule(navigation: TimelionPluginDependencies['navigation']) { - angular - .module('app/timelion/TopNav', ['react']) - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(navigation.ui)); -} - function createLocalI18nModule() { angular .module('app/timelion/I18n', []) diff --git a/src/plugins/timelion/public/components/timelion_top_nav_directive.js b/src/plugins/timelion/public/components/timelion_top_nav_directive.js new file mode 100644 index 000000000000..322bf6e8fe71 --- /dev/null +++ b/src/plugins/timelion/public/components/timelion_top_nav_directive.js @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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'; + +export function initTimelionTopNavDirective(app, deps) { + app.directive('timelionTopNav', function (reactDirective) { + return reactDirective( + (props) => { + const { TopNavMenu } = deps.plugins.navigation.ui; + return ( + + + + ); + }, + [ + ['topNavMenu', { watchDepth: 'reference' }], + ['onTimeUpdate', { watchDepth: 'reference' }], + ], + { + restrict: 'E', + scope: { + topNavMenu: '=', + onTimeUpdate: '=', + }, + } + ); + }); +} diff --git a/src/plugins/timelion/public/directives/_form.scss b/src/plugins/timelion/public/directives/_form.scss index 3fcf70700a86..370dd25f8263 100644 --- a/src/plugins/timelion/public/directives/_form.scss +++ b/src/plugins/timelion/public/directives/_form.scss @@ -34,3 +34,51 @@ select.form-control { .fullWidth { width: 100%; } + +.timDropdownWarning { + margin-bottom: $euiSize; + padding: $euiSizeXS $euiSizeS; + color: $euiColorDarkestShade; + border-left: solid 2px $euiColorDanger; + font-size: $euiSizeM; +} + +.timFormCheckbox { + display: flex; + align-items: center; + line-height: 1.5; + position: relative; +} + +.timFormCheckbox__input { + appearance: none; + background-color: $euiColorLightestShade; + border: 1px solid $euiColorLightShade; + border-radius: $euiSizeXS; + width: $euiSize; + height: $euiSize; + font-size: $euiSizeM; + transition: background-color .1s linear; +} + +.timFormCheckbox__input:checked { + border-color: $euiColorPrimary; + background-color: $euiColorPrimary; +} + +.timFormCheckbox__icon { + position: absolute; + top: 0; + left: 2px; +} + +.timFormTextarea { + padding: $euiSizeXS $euiSizeM; + font-size: $euiSize; + line-height: 1.5; + color: $euiColorDarkestShade; + background-color: $euiFormBackgroundColor; + border: 1px solid $euiColorLightShade; + border-radius: $euiSizeXS; + transition: border-color .1s linear; +} diff --git a/src/plugins/timelion/public/directives/_saved_object_finder.scss b/src/plugins/timelion/public/directives/_saved_object_finder.scss index e1a055a5f49e..3a2489afb572 100644 --- a/src/plugins/timelion/public/directives/_saved_object_finder.scss +++ b/src/plugins/timelion/public/directives/_saved_object_finder.scss @@ -27,6 +27,42 @@ saved-object-finder { + .timSearchBar { + display: flex; + align-items: center; + } + + .timSearchBar__section { + position: relative; + margin-right: $euiSize; + flex: 1; + } + + .timSearchBar__icon { + position: absolute; + top: $euiSizeS; + left: $euiSizeS; + font-size: $euiSize; + color: $euiColorDarkShade; + } + + .timSearchBar__input { + padding: $euiSizeS $euiSizeM; + color: $euiColorDarkestShade; + background-color: $euiColorEmptyShade; + border: 1px solid $euiColorLightShade; + border-radius: $euiSizeXS; + transition: border-color .1s linear; + padding-left: $euiSizeXL; + width: 100%; + font-size: $euiSize; + } + + .timSearchBar__pagecount { + font-size: $euiSize; + color: $euiColorDarkShade; + } + .list-sort-button { border-top-left-radius: 0; border-top-right-radius: 0; @@ -34,6 +70,7 @@ saved-object-finder { padding: $euiSizeS $euiSize; font-weight: $euiFontWeightRegular; background-color: $euiColorLightestShade; + margin-top: $euiSize; } .li-striped { diff --git a/src/plugins/timelion/public/directives/cells/_cells.scss b/src/plugins/timelion/public/directives/cells/_cells.scss index 899bf984e72c..6cd71378a81d 100644 --- a/src/plugins/timelion/public/directives/cells/_cells.scss +++ b/src/plugins/timelion/public/directives/cells/_cells.scss @@ -33,7 +33,6 @@ text-align: center; width: $euiSizeL; height: $euiSizeL; - line-height: $euiSizeL; border-radius: $euiSizeL / 2; border: $euiBorderThin; background-color: $euiColorLightestShade; diff --git a/src/plugins/timelion/public/directives/cells/cells.html b/src/plugins/timelion/public/directives/cells/cells.html index 6be1b089d2de..f90b85abaf92 100644 --- a/src/plugins/timelion/public/directives/cells/cells.html +++ b/src/plugins/timelion/public/directives/cells/cells.html @@ -25,7 +25,7 @@ tooltip-append-to-body="1" aria-label="{{ ::'timelion.cells.actions.removeAriaLabel' | i18n: { defaultMessage: 'Remove chart' } }}" > - +
diff --git a/src/plugins/timelion/public/directives/fullscreen/fullscreen.html b/src/plugins/timelion/public/directives/fullscreen/fullscreen.html index 194596ba79d0..1ed6aa82ea3b 100644 --- a/src/plugins/timelion/public/directives/fullscreen/fullscreen.html +++ b/src/plugins/timelion/public/directives/fullscreen/fullscreen.html @@ -8,7 +8,7 @@ tooltip-append-to-body="1" aria-label="{{ ::'timelion.fullscreen.exitAriaLabel' | i18n: { defaultMessage: 'Exit full screen' } }}" > - +
diff --git a/src/plugins/timelion/public/directives/saved_object_finder.html b/src/plugins/timelion/public/directives/saved_object_finder.html index ad148801c03a..1ce10efe4e0a 100644 --- a/src/plugins/timelion/public/directives/saved_object_finder.html +++ b/src/plugins/timelion/public/directives/saved_object_finder.html @@ -1,13 +1,11 @@
-
-
-
- +
+
+ -
-
-

+

-
+
@@ -45,7 +45,7 @@ @@ -82,7 +82,7 @@ @@ -100,7 +100,7 @@ @@ -222,7 +222,7 @@ @@ -230,7 +230,7 @@ @@ -371,7 +371,7 @@ @@ -379,7 +379,7 @@ @@ -484,7 +484,7 @@ @@ -492,7 +492,7 @@ @@ -587,7 +587,7 @@ @@ -596,7 +596,7 @@ @@ -606,7 +606,7 @@

@@ -618,7 +618,7 @@
-
+
. diff --git a/src/plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss b/src/plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss index b371c4400a30..7ce09155cafd 100644 --- a/src/plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss +++ b/src/plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss @@ -4,6 +4,12 @@ timelion-interval { .timInterval__input { width: $euiSizeXL * 2; + padding: $euiSizeXS $euiSizeM; + color: $euiColorDarkestShade; + border: 1px solid $euiColorLightShade; + border-radius: $euiSizeXS; + transition: border-color .1s linear; + font-size: 14px; } .timInterval__input--compact { diff --git a/src/plugins/timelion/public/directives/timelion_interval/timelion_interval.html b/src/plugins/timelion/public/directives/timelion_interval/timelion_interval.html index 11c79e6a1682..49009355e49f 100644 --- a/src/plugins/timelion/public/directives/timelion_interval/timelion_interval.html +++ b/src/plugins/timelion/public/directives/timelion_interval/timelion_interval.html @@ -1,7 +1,7 @@ =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/index.html b/src/plugins/timelion/public/index.html index 0cf64287a3bd..3fb518e81e88 100644 --- a/src/plugins/timelion/public/index.html +++ b/src/plugins/timelion/public/index.html @@ -1,5 +1,5 @@ - + - - - +
@@ -41,9 +27,9 @@ -
+
-
+
@@ -62,14 +47,14 @@
-
+

diff --git a/src/plugins/timelion/public/partials/save_sheet.html b/src/plugins/timelion/public/partials/save_sheet.html index a0e0727f3ec8..7773a9d25df7 100644 --- a/src/plugins/timelion/public/partials/save_sheet.html +++ b/src/plugins/timelion/public/partials/save_sheet.html @@ -19,7 +19,7 @@
@@ -28,20 +28,21 @@ id="savedSheet" ng-model="opts.savedSheet.title" input-focus="select" - class="form-control kuiVerticalRhythmSmall" + class="form-control" + style="margin-bottom: 4px;" placeholder="{{ ::'timelion.topNavMenu.save.saveEntireSheet.inputPlaceholder' | i18n: { defaultMessage: 'Name this sheet...' } }}" aria-label="{{ ::'timelion.topNavMenu.save.saveEntireSheet.inputAriaLabel' | i18n: { defaultMessage: 'Name' } }}" > diff --git a/src/plugins/timelion/public/partials/sheet_options.html b/src/plugins/timelion/public/partials/sheet_options.html index e882cfe52958..eae570933165 100644 --- a/src/plugins/timelion/public/partials/sheet_options.html +++ b/src/plugins/timelion/public/partials/sheet_options.html @@ -1,6 +1,6 @@

diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index 7656a808dfb0..e5bfe7a27ad1 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -21,7 +21,6 @@ import { BehaviorSubject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { CoreSetup, - CoreStart, Plugin, PluginInitializerContext, DEFAULT_APP_CATEGORIES, @@ -31,25 +30,33 @@ import { AppNavLinkStatus, } from '../../../core/public'; import { Panel } from './panels/panel'; -import { initAngularBootstrap, KibanaLegacyStart } from '../../kibana_legacy/public'; +import { initAngularBootstrap } from '../../kibana_legacy/public'; 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; } /** @internal */ -export class TimelionPlugin implements Plugin { +export class TimelionPlugin + implements Plugin { initializerContext: PluginInitializerContext; private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking: (() => void) | undefined = undefined; @@ -60,7 +67,7 @@ export class TimelionPlugin implements Plugin { } public setup( - core: CoreSetup, + core: CoreSetup, { data, visTypeTimelion, @@ -122,7 +129,7 @@ export class TimelionPlugin implements Plugin { pluginInitializerContext: this.initializerContext, timelionPanels, core: coreStart, - plugins: pluginsStart as TimelionPluginDependencies, + plugins: pluginsStart, }); return () => { unlistenParentHistory(); @@ -133,9 +140,7 @@ export class TimelionPlugin implements Plugin { }); } - public start(core: CoreStart, { kibanaLegacy }: { kibanaLegacy: KibanaLegacyStart }) { - kibanaLegacy.loadFontAwesome(); - } + public start() {} public stop(): void { if (this.stopUrlTracking) { 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/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 1fdddfc272e9..c7efb6dad326 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -201,8 +201,10 @@ export async function buildContextMenuForActions({ for (const panel of Object.values(panels)) { if (panel._level === 0) { - // TODO: Add separator line here once it is available in EUI. - // See https://github.com/elastic/eui/pull/4018 + panels.mainMenu.items.push({ + isSeparator: true, + key: panel.id + '__separator', + }); if (panel.items.length > 3) { panels.mainMenu.items.push({ name: panel.title || panel.id, diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 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_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx index ed94e52ee239..a7251acfdf75 100644 --- a/src/plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -18,6 +18,7 @@ */ import './index.scss'; +import 'brace/mode/json'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EventEmitter } from 'events'; diff --git a/src/plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx index 0efd6e7746fd..707b14c23ea7 100644 --- a/src/plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -22,12 +22,12 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { EventEmitter } from 'events'; import { EuiErrorBoundary, EuiLoadingChart } from '@elastic/eui'; -import { EditorRenderProps } from 'src/plugins/visualize/public'; +import { EditorRenderProps, IEditorController } from 'src/plugins/visualize/public'; import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; const DefaultEditor = lazy(() => import('./default_editor')); -class DefaultEditorController { +class DefaultEditorController implements IEditorController { constructor( private el: HTMLElement, private vis: Vis, diff --git a/src/plugins/vis_type_markdown/public/markdown_renderer.tsx b/src/plugins/vis_type_markdown/public/markdown_renderer.tsx index 8071196c6a21..f36ffadff7c5 100644 --- a/src/plugins/vis_type_markdown/public/markdown_renderer.tsx +++ b/src/plugins/vis_type_markdown/public/markdown_renderer.tsx @@ -36,7 +36,7 @@ export const markdownVisRenderer: ExpressionRenderDefinition + , domNode diff --git a/src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap b/src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap index 706d2a902aa9..fd8f3a712d8a 100644 --- a/src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap +++ b/src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap @@ -43,7 +43,7 @@ Object { "col-0-1": 0, }, ], - "type": "kibana_datatable", + "type": "datatable", }, "visType": "metric", }, diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx index e5c7db65c09a..5ab3ee6eed8e 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx @@ -23,7 +23,7 @@ import { isColorDark } from '@elastic/eui'; import { MetricVisValue } from './metric_vis_value'; import { Input } from '../metric_vis_fn'; import { FieldFormatsContentType, IFieldFormat } from '../../../data/public'; -import { KibanaDatatable } from '../../../expressions/public'; +import { Datatable } from '../../../expressions/public'; import { getHeatmapColors } from '../../../charts/public'; import { VisParams, MetricVisMetric } from '../types'; import { getFormatService } from '../services'; @@ -109,7 +109,7 @@ class MetricVisComponent extends Component { return fieldFormatter.convert(value, format); }; - private processTableGroups(table: KibanaDatatable) { + private processTableGroups(table: Datatable) { const config = this.props.visParams.metric; const dimensions = this.props.visParams.dimensions; const isPercentageMode = config.percentageMode; diff --git a/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts b/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts index 3ed8f8f79a83..8faa3d2aab26 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts @@ -23,7 +23,7 @@ import { functionWrapper } from '../../expressions/common/expression_functions/s describe('interpreter/functions#metric', () => { const fn = functionWrapper(createMetricVisFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; diff --git a/src/plugins/vis_type_metric/public/metric_vis_fn.ts b/src/plugins/vis_type_metric/public/metric_vis_fn.ts index 97b1e6822333..20de22f50e63 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_fn.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_fn.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, - KibanaDatatable, + Datatable, Range, Render, Style, @@ -29,7 +29,7 @@ import { import { visType, DimensionsVisParam, VisParams } from './types'; import { ColorSchemas, vislibColorMaps, ColorModes } from '../../charts/public'; -export type Input = KibanaDatatable; +export type Input = Datatable; interface Arguments { percentageMode: boolean; @@ -63,7 +63,7 @@ export type MetricVisExpressionFunctionDefinition = ExpressionFunctionDefinition export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({ name: 'metricVis', type: 'render', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], help: i18n.translate('visTypeMetric.function.help', { defaultMessage: 'Metric visualization', }), diff --git a/src/plugins/vis_type_metric/public/metric_vis_renderer.tsx b/src/plugins/vis_type_metric/public/metric_vis_renderer.tsx index bf0d6da9fba0..8e0cb35ca52a 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_renderer.tsx +++ b/src/plugins/vis_type_metric/public/metric_vis_renderer.tsx @@ -36,7 +36,11 @@ export const metricVisRenderer: () => ExpressionRenderDefinition + ({ describe('interpreter/functions#table', () => { const fn = functionWrapper(createTableVisFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; diff --git a/src/plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts index 2e446ba4e4fc..28990f28caf3 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -19,10 +19,10 @@ import { i18n } from '@kbn/i18n'; import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; -import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; import { TableVisConfig } from './types'; -export type Input = KibanaDatatable; +export type Input = Datatable; interface Arguments { visConfig: string | null; @@ -44,7 +44,7 @@ export type TableExpressionFunctionDefinition = ExpressionFunctionDefinition< export const createTableVisFn = (): TableExpressionFunctionDefinition => ({ name: 'kibana_table', type: 'render', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], help: i18n.translate('visTypeTable.function.help', { defaultMessage: 'Table visualization', }), diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap index debc7ab27c63..17a91a4d43cc 100644 --- a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap +++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap @@ -17,7 +17,7 @@ Object { "col-0-1": 0, }, ], - "type": "kibana_datatable", + "type": "datatable", }, "visParams": Object { "maxFontSize": 72, diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts index eb16b0855a13..e481c311d545 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts @@ -24,7 +24,7 @@ import { functionWrapper } from '../../expressions/common/expression_functions/s describe('interpreter/functions#tagcloud', () => { const fn = functionWrapper(createTagCloudFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts index 42e126908c00..ff59572e0817 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; import { TagCloudVisParams } from './types'; const name = 'tagcloud'; @@ -31,13 +31,13 @@ interface Arguments extends TagCloudVisParams { export interface TagCloudVisRenderValue { visType: typeof name; - visData: KibanaDatatable; + visData: Datatable; visParams: Arguments; } export type TagcloudExpressionFunctionDefinition = ExpressionFunctionDefinition< typeof name, - KibanaDatatable, + Datatable, Arguments, Render >; @@ -45,7 +45,7 @@ export type TagcloudExpressionFunctionDefinition = ExpressionFunctionDefinition< export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({ name, type: 'render', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], help: i18n.translate('visTypeTagCloud.function.help', { defaultMessage: 'Tagcloud visualization', }), diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx b/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx index b433ed9cbed2..21194189745a 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx @@ -39,7 +39,7 @@ export const getTagCloudVisRenderer: ( }); render( - + ' + - 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/public/timelion_vis_renderer.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx index 13a279138a8e..04579407105e 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx @@ -49,7 +49,7 @@ export const getTimelionVisRenderer: ( } render( - + 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_timeseries/public/application/components/lib/label_date_formatter.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts new file mode 100644 index 000000000000..f4de19b084c7 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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'; + +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/public/_vega_vis.scss b/src/plugins/vis_type_vega/public/_vega_vis.scss index 12108c7ba3de..6a0d20246089 100644 --- a/src/plugins/vis_type_vega/public/_vega_vis.scss +++ b/src/plugins/vis_type_vega/public/_vega_vis.scss @@ -113,14 +113,19 @@ margin-bottom: $euiSizeS; } + &--textTruncate { + td { + @include euiTextTruncate; + } + } + td { - @include euiTextTruncate; padding-top: $euiSizeXS; padding-bottom: $euiSizeXS; &.key { - color: $euiColorMediumShade; max-width: $euiSize * 10; + color: $euiColorMediumShade; text-align: right; padding-right: $euiSizeXS; } @@ -131,7 +136,6 @@ } } - @media only screen and (max-width: map-get($euiBreakpoints, 'm')) { td { &.key { diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index 11bdf4f06402..acd35e174762 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -207,6 +207,7 @@ export interface TooltipConfig { position?: ToolTipPositions; padding?: number | Padding; centerOnMark?: boolean | number; + textTruncate?: boolean; } export interface DstObj { diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index c9f8e0a4394e..9fb80c6a1b19 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -243,7 +243,7 @@ describe('VegaParser.parseSchema', () => { }); describe('VegaParser._parseTooltips', () => { - function check(tooltips, position, padding, centerOnMark) { + function check(tooltips, position, padding, centerOnMark, textTruncate = false) { return () => { const vp = new VegaParser(tooltips !== undefined ? { config: { kibana: { tooltips } } } : {}); vp._config = vp._parseConfig(); @@ -253,7 +253,7 @@ describe('VegaParser._parseTooltips', () => { } else if (position === false) { expect(vp._parseTooltips()).toEqual(false); } else { - expect(vp._parseTooltips()).toEqual({ position, padding, centerOnMark }); + expect(vp._parseTooltips()).toEqual({ position, padding, centerOnMark, textTruncate }); } }; } @@ -267,6 +267,7 @@ describe('VegaParser._parseTooltips', () => { test('centerOnMark=10', check({ centerOnMark: 10 }, 'top', 16, 10)); test('centerOnMark=true', check({ centerOnMark: true }, 'top', 16, Number.MAX_VALUE)); test('centerOnMark=false', check({ centerOnMark: false }, 'top', 16, -1)); + test('textTruncate=false', check({ textTruncate: true }, 'top', 16, 50, true)); test('false', check(false, false)); diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 894c34c494c1..9cbb38fa87a7 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -409,6 +409,17 @@ The URL is an identifier only. Kibana and your browser will never access this UR ); } + if (result.textTruncate === undefined) { + result.textTruncate = false; + } else if (typeof result.textTruncate !== 'boolean') { + throw new Error( + i18n.translate('visTypeVega.vegaParser.textTruncateConfigValueTypeErrorMessage', { + defaultMessage: '{configName} is expected to be a boolean', + values: { configName: 'textTruncate' }, + }) + ); + } + if (result.centerOnMark === undefined) { // if mark's width & height is less than this value, center on it result.centerOnMark = 50; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js index 7b0274478cea..60f2f6e5a13d 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js @@ -50,6 +50,7 @@ export class TooltipHandler { this.position = opts.position; this.padding = opts.padding; this.centerOnMark = opts.centerOnMark; + this.textTruncate = opts.textTruncate; view.tooltip(this.handler.bind(this)); } @@ -73,6 +74,10 @@ export class TooltipHandler { } ); + if (this.textTruncate) { + el.classList.add('vgaVis__tooltip--textTruncate'); + } + // Sanitized HTML is created by the tooltip library, // with a large number of tests, hence suppressing eslint here. // eslint-disable-next-line no-unsanitized/property 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/vis_type_vislib/public/pie_fn.test.ts b/src/plugins/vis_type_vislib/public/pie_fn.test.ts index eb68353b7c0e..58901a5dad72 100644 --- a/src/plugins/vis_type_vislib/public/pie_fn.test.ts +++ b/src/plugins/vis_type_vislib/public/pie_fn.test.ts @@ -42,7 +42,7 @@ jest.mock('./vislib/response_handler', () => ({ describe('interpreter/functions#pie', () => { const fn = functionWrapper(createPieVisFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; diff --git a/src/plugins/vis_type_vislib/public/pie_fn.ts b/src/plugins/vis_type_vislib/public/pie_fn.ts index 52da0f7ac14e..bee200cbe30e 100644 --- a/src/plugins/vis_type_vislib/public/pie_fn.ts +++ b/src/plugins/vis_type_vislib/public/pie_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; // @ts-ignore import { vislibSlicesResponseHandler } from './vislib/response_handler'; @@ -34,13 +34,13 @@ interface RenderValue { export const createPieVisFn = (): ExpressionFunctionDefinition< 'kibana_pie', - KibanaDatatable, + Datatable, Arguments, Render > => ({ name: 'kibana_pie', type: 'render', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], help: i18n.translate('visTypeVislib.functions.pie.help', { defaultMessage: 'Pie visualization', }), diff --git a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts index a4243c6d25c4..557f9930f55b 100644 --- a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts +++ b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; // @ts-ignore import { vislibSeriesResponseHandler } from './vislib/response_handler'; @@ -36,13 +36,13 @@ interface RenderValue { export const createVisTypeVislibVisFn = (): ExpressionFunctionDefinition< 'vislib', - KibanaDatatable, + Datatable, Arguments, Render > => ({ name: 'vislib', type: 'render', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], help: i18n.translate('visTypeVislib.functions.vislib.help', { defaultMessage: 'Vislib visualization', }), 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/components/visualization_container.tsx b/src/plugins/visualizations/public/components/visualization_container.tsx index 007a9e6e9dde..5695a84269bd 100644 --- a/src/plugins/visualizations/public/components/visualization_container.tsx +++ b/src/plugins/visualizations/public/components/visualization_container.tsx @@ -21,16 +21,19 @@ import React, { ReactNode, Suspense } from 'react'; import { EuiLoadingChart } from '@elastic/eui'; import classNames from 'classnames'; import { VisualizationNoResults } from './visualization_noresults'; +import { IInterpreterRenderHandlers } from '../../../expressions/common'; interface VisualizationContainerProps { className?: string; children: ReactNode; + handlers: IInterpreterRenderHandlers; showNoResult?: boolean; } export const VisualizationContainer = ({ className, children, + handlers, showNoResult = false, }: VisualizationContainerProps) => { const classes = classNames('visualization', className); @@ -44,7 +47,7 @@ export const VisualizationContainer = ({ return (
- {showNoResult ? : children} + {showNoResult ? handlers.done()} /> : children}
); diff --git a/src/plugins/visualizations/public/expression_functions/range.ts b/src/plugins/visualizations/public/expression_functions/range.ts index 42eb6aa78197..409199deb818 100644 --- a/src/plugins/visualizations/public/expression_functions/range.ts +++ b/src/plugins/visualizations/public/expression_functions/range.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaDatatable, Range } from '../../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Range } from '../../../expressions/public'; interface Arguments { from: number; @@ -27,7 +27,7 @@ interface Arguments { export const range = (): ExpressionFunctionDefinition< 'range', - KibanaDatatable | null, + Datatable | null, Arguments, Range > => ({ diff --git a/src/plugins/visualizations/public/expression_functions/vis_dimension.ts b/src/plugins/visualizations/public/expression_functions/vis_dimension.ts index 286804d2fa76..a78634b7eef1 100644 --- a/src/plugins/visualizations/public/expression_functions/vis_dimension.ts +++ b/src/plugins/visualizations/public/expression_functions/vis_dimension.ts @@ -21,8 +21,8 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, ExpressionValueBoxed, - KibanaDatatable, - KibanaDatatableColumn, + Datatable, + DatatableColumn, } from '../../../expressions/public'; interface Arguments { @@ -34,7 +34,7 @@ interface Arguments { type ExpressionValueVisDimension = ExpressionValueBoxed< 'vis_dimension', { - accessor: number | KibanaDatatableColumn; + accessor: number | DatatableColumn; format: { id?: string; params: unknown; @@ -44,7 +44,7 @@ type ExpressionValueVisDimension = ExpressionValueBoxed< export const visDimension = (): ExpressionFunctionDefinition< 'visdimension', - KibanaDatatable, + Datatable, Arguments, ExpressionValueVisDimension > => ({ @@ -53,7 +53,7 @@ export const visDimension = (): ExpressionFunctionDefinition< defaultMessage: 'Generates visConfig dimension object', }), type: 'vis_dimension', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], args: { accessor: { types: ['string', 'number'], diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 99c13b42b8b2..081399fd1fbe 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -36,7 +36,7 @@ export { getSchemas as getVisSchemas } from './legacy/build_pipeline'; /** @public types */ export { VisualizationsSetup, VisualizationsStart }; -export { VisTypeAlias, VisType, BaseVisTypeOptions, ReactVisTypeOptions } from './vis_types'; +export type { VisTypeAlias, VisType, BaseVisTypeOptions, ReactVisTypeOptions } from './vis_types'; export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; 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/public/vis_types/index.ts b/src/plugins/visualizations/public/vis_types/index.ts index 22561decabea..a46b257c9905 100644 --- a/src/plugins/visualizations/public/vis_types/index.ts +++ b/src/plugins/visualizations/public/vis_types/index.ts @@ -18,6 +18,6 @@ */ export * from './types_service'; -export { VisType } from './types'; +export type { VisType } from './types'; export type { BaseVisTypeOptions } from './base_vis_type'; export type { ReactVisTypeOptions } from './react_vis_type'; diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 0cf345bf07be..7206e9612f10 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -20,6 +20,7 @@ import { IconType } from '@elastic/eui'; import React from 'react'; import { Adapters } from 'src/plugins/inspector'; +import { VisEditorConstructor } from 'src/plugins/visualize/public'; import { ISchemas } from 'src/plugins/vis_default_editor/public'; import { TriggerContextMapping } from '../../../ui_actions/public'; import { Vis, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; @@ -69,12 +70,14 @@ export interface VisType { readonly options: VisTypeOptions; - // TODO: The following types still need to be refined properly. - /** * The editor that should be used to edit visualizations of this type. + * If this is not specified the default visualize editor will be used (and should be configured via schemas) + * and editorConfig. */ - readonly editor?: any; + readonly editor?: VisEditorConstructor; + + // TODO: The following types still need to be refined properly. readonly editorConfig: Record; readonly visConfig: Record; } 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/types.ts b/src/plugins/visualize/public/application/types.ts index 00fa6e74f952..6cc8f5c26584 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -45,6 +45,7 @@ import { SharePluginStart } from 'src/plugins/share/public'; import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { UrlForwardingStart } from 'src/plugins/url_forwarding/public'; +import { EventEmitter } from 'events'; import { DashboardStart } from '../../../dashboard/public'; export type PureVisState = SavedVisState; @@ -131,7 +132,14 @@ export interface ByValueVisInstance { export type VisualizeEditorVisInstance = SavedVisInstance | ByValueVisInstance; +export type VisEditorConstructor = new ( + element: HTMLElement, + vis: Vis, + eventEmitter: EventEmitter, + embeddableHandler: VisualizeEmbeddableContract +) => IEditorController; + export interface IEditorController { - render(props: EditorRenderProps): void; + render(props: EditorRenderProps): Promise | void; destroy(): void; } 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/application/utils/use/use_saved_vis_instance.test.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts index ce0f5fe965d7..3f9676a9c938 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts @@ -116,6 +116,7 @@ describe('useSavedVisInstance', () => { useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) ); + result.current.visEditorRef.current = document.createElement('div'); expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId); expect(mockGetVisualizationInstance.mock.calls.length).toBe(1); @@ -129,10 +130,12 @@ describe('useSavedVisInstance', () => { }); test('should destroy the editor and the savedVis on unmount if chrome exists', async () => { - const { unmount, waitForNextUpdate } = renderHook(() => + const { result, unmount, waitForNextUpdate } = renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) ); + result.current.visEditorRef.current = document.createElement('div'); + await waitForNextUpdate(); unmount(); @@ -158,6 +161,8 @@ describe('useSavedVisInstance', () => { useSavedVisInstance(mockServices, eventEmitter, true, undefined) ); + result.current.visEditorRef.current = document.createElement('div'); + expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, { indexPattern: '1a2b3c4d', type: 'area', diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index ec815b8cfcbe..44fbcce82f45 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -44,7 +44,7 @@ export const useSavedVisInstance = ( savedVisInstance?: SavedVisInstance; visEditorController?: IEditorController; }>({}); - const visEditorRef = useRef(null); + const visEditorRef = useRef(null); const visId = useRef(''); useEffect(() => { @@ -102,16 +102,18 @@ export const useSavedVisInstance = ( let visEditorController; // do not create editor in embeded mode - if (isChromeVisible) { - const Editor = vis.type.editor || DefaultEditorController; - visEditorController = new Editor( - visEditorRef.current, - vis, - eventEmitter, - embeddableHandler - ); - } else if (visEditorRef.current) { - embeddableHandler.render(visEditorRef.current); + if (visEditorRef.current) { + if (isChromeVisible) { + const Editor = vis.type.editor || DefaultEditorController; + visEditorController = new Editor( + visEditorRef.current, + vis, + eventEmitter, + embeddableHandler + ); + } else { + embeddableHandler.render(visEditorRef.current); + } } setState({ diff --git a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts index f2758d0cc01a..e0286a63b9fe 100644 --- a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts +++ b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts @@ -41,7 +41,7 @@ export const useVisByValue = ( useEffect(() => { const { chrome } = services; const getVisInstance = async () => { - if (!valueInput || loaded.current) { + if (!valueInput || loaded.current || !visEditorRef.current) { return; } const byValueVisInstance = await getVisualizationInstanceFromInput(services, valueInput); diff --git a/src/plugins/visualize/public/index.ts b/src/plugins/visualize/public/index.ts index d437cadad9fa..246806f30080 100644 --- a/src/plugins/visualize/public/index.ts +++ b/src/plugins/visualize/public/index.ts @@ -20,7 +20,11 @@ import { PluginInitializerContext } from 'kibana/public'; import { VisualizePlugin } from './plugin'; -export { EditorRenderProps } from './application/types'; +export type { + EditorRenderProps, + IEditorController, + VisEditorConstructor, +} from './application/types'; export { VisualizeConstants } from './application/visualize_constants'; export const plugin = (context: PluginInitializerContext) => { 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/common/services/security/role_mappings.ts b/test/common/services/security/role_mappings.ts index 7951d4b5b47b..267294991f30 100644 --- a/test/common/services/security/role_mappings.ts +++ b/test/common/services/security/role_mappings.ts @@ -23,10 +23,24 @@ import { KbnClient, ToolingLog } from '@kbn/dev-utils'; export class RoleMappings { constructor(private log: ToolingLog, private kbnClient: KbnClient) {} + public async getAll() { + this.log.debug(`Getting role mappings`); + const { data, status, statusText } = await this.kbnClient.request>({ + path: `/internal/security/role_mapping`, + method: 'GET', + }); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + return data; + } + public async create(name: string, roleMapping: Record) { this.log.debug(`creating role mapping ${name}`); const { data, status, statusText } = await this.kbnClient.request({ - path: `/internal/security/role_mapping/${name}`, + path: `/internal/security/role_mapping/${encodeURIComponent(name)}`, method: 'POST', body: roleMapping, }); @@ -41,7 +55,7 @@ export class RoleMappings { public async delete(name: string) { this.log.debug(`deleting role mapping ${name}`); const { data, status, statusText } = await this.kbnClient.request({ - path: `/internal/security/role_mapping/${name}`, + path: `/internal/security/role_mapping/${encodeURIComponent(name)}`, method: 'DELETE', }); if (status !== 200 && status !== 404) { diff --git a/test/functional/apps/dashboard/url_field_formatter.ts b/test/functional/apps/dashboard/url_field_formatter.ts index 9b05b9b777b9..a18ad740681b 100644 --- a/test/functional/apps/dashboard/url_field_formatter.ts +++ b/test/functional/apps/dashboard/url_field_formatter.ts @@ -46,7 +46,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(currentUrl).to.equal(fieldUrl); }; - describe('Changing field formatter to Url', () => { + // FLAKY: https://github.com/elastic/kibana/issues/79463 + describe.skip('Changing field formatter to Url', () => { before(async function () { await esArchiver.load('dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index faf272daba09..e597cc14654b 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const queryBar = getService('queryBar'); + const inspector = getService('inspector'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); const defaultSettings = { defaultIndex: 'logstash-*', @@ -292,5 +293,37 @@ export default function ({ getService, getPageObjects }) { expect(currentUrlWithoutScore).not.to.contain('_score'); }); }); + + describe('refresh interval', function () { + it('should refetch when autofresh is enabled', async () => { + const intervalS = 5; + await PageObjects.timePicker.startAutoRefresh(intervalS); + + // check inspector panel request stats for timestamp + await inspector.open(); + + const getRequestTimestamp = async () => { + const requestStats = await inspector.getTableData(); + const requestTimestamp = requestStats.filter((r) => + r[0].includes('Request timestamp') + )[0][1]; + return requestTimestamp; + }; + + const requestTimestampBefore = await getRequestTimestamp(); + await retry.waitFor('refetch because of refresh interval', async () => { + const requestTimestampAfter = await getRequestTimestamp(); + log.debug( + `Timestamp before: ${requestTimestampBefore}, Timestamp after: ${requestTimestampAfter}` + ); + return requestTimestampBefore !== requestTimestampAfter; + }); + }); + + after(async () => { + await inspector.close(); + await PageObjects.timePicker.pauseAutoRefresh(); + }); + }); }); } diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index d9cb09432b26..d45b8f4841cb 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -27,21 +27,17 @@ export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); - // FLAKY: https://github.com/elastic/kibana/issues/78689 - describe.skip('discover tab', function describeIndexTests() { + describe('discover tab', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('discover'); - // delete .kibana index and update configDoc await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', }); - + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setDefaultAbsoluteRange(); }); - describe('field data', function () { it('search php should show the correct hit count', async function () { const expectedHitCount = '445'; 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/apps/discover/_sidebar.js b/test/functional/apps/discover/_sidebar.js index ce7ebff9cce7..f7784b739336 100644 --- a/test/functional/apps/discover/_sidebar.js +++ b/test/functional/apps/discover/_sidebar.js @@ -25,7 +25,8 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); - describe('discover sidebar', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/80914 + describe.skip('discover sidebar', function describeIndexTests() { before(async function () { // delete .kibana index and update configDoc await kibanaServer.uiSettings.replace({ diff --git a/test/functional/apps/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js index 24e4ef4a7fe2..8dfc4d352b13 100644 --- a/test/functional/apps/visualize/_line_chart.js +++ b/test/functional/apps/visualize/_line_chart.js @@ -136,15 +136,8 @@ export default function ({ getService, getPageObjects }) { }); it('should request new data when autofresh is enabled', async () => { - // enable autorefresh - const interval = 3; - await PageObjects.timePicker.openQuickSelectTimeMenu(); - await PageObjects.timePicker.inputValue( - 'superDatePickerRefreshIntervalInput', - interval.toString() - ); - await testSubjects.click('superDatePickerToggleRefreshButton'); - await PageObjects.timePicker.closeQuickSelectTimeMenu(); + const intervalS = 3; + await PageObjects.timePicker.startAutoRefresh(intervalS); // check inspector panel request stats for timestamp await inspector.open(); @@ -155,7 +148,7 @@ export default function ({ getService, getPageObjects }) { )[0][1]; // pause to allow time for autorefresh to fire another request - await PageObjects.common.sleep(interval * 1000 * 1.5); + await PageObjects.common.sleep(intervalS * 1000 * 1.5); // get the latest timestamp from request stats const requestStatsAfter = await inspector.getTableData(); diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index e165341dbd63..8e65b6488836 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -146,6 +146,20 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv } } + async clickInspectByTitle(title: string) { + const table = keyBy(await this.getElementsInTable(), 'title'); + if (table[title].menuElement) { + await table[title].menuElement?.click(); + // Wait for context menu to render + const menuPanel = await find.byCssSelector('.euiContextMenuPanel'); + const panelButton = await menuPanel.findByTestSubject('savedObjectsTableAction-inspect'); + await panelButton.click(); + } else { + // or the action elements are on the row without the menu + await table[title].copySaveObjectsElement?.click(); + } + } + async clickCheckboxByTitle(title: string) { const table = keyBy(await this.getElementsInTable(), 'title'); // should we check if table size > 0 and log error if not? diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index cf75d5ad7c10..e29f75b80657 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -282,6 +282,10 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await PageObjects.header.waitUntilLoadingHasFinished(); } + async hasIndexPattern(name: string) { + return await find.existsByLinkText(name); + } + async clickIndexPatternByName(name: string) { const indexLink = await find.byXPath(`//a[descendant::*[text()='${name}']]`); await indexLink.click(); @@ -324,6 +328,13 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await retry.try(async () => { await PageObjects.header.waitUntilLoadingHasFinished(); await this.clickKibanaIndexPatterns(); + const exists = await this.hasIndexPattern(indexPatternName); + + if (exists) { + await this.clickIndexPatternByName(indexPatternName); + return; + } + await PageObjects.header.waitUntilLoadingHasFinished(); await this.clickAddNewIndexPatternButton(); if (!isStandardIndexPattern) { diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 237dc8946ae0..3ac6c83e61f1 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -269,6 +269,17 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo return moment.duration(endMoment.diff(startMoment)).asHours(); } + public async startAutoRefresh(intervalS = 3) { + await this.openQuickSelectTimeMenu(); + await this.inputValue('superDatePickerRefreshIntervalInput', intervalS.toString()); + const refreshConfig = await this.getRefreshConfig(true); + if (refreshConfig.isPaused) { + log.debug('start auto refresh'); + await testSubjects.click('superDatePickerToggleRefreshButton'); + } + await this.closeQuickSelectTimeMenu(); + } + public async pauseAutoRefresh() { log.debug('pauseAutoRefresh'); const refreshConfig = await this.getRefreshConfig(true); 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/test/functional/services/toasts.ts b/test/functional/services/toasts.ts index f5416a44e3b5..1148da14556e 100644 --- a/test/functional/services/toasts.ts +++ b/test/functional/services/toasts.ts @@ -71,6 +71,12 @@ export function ToastsProvider({ getService }: FtrProviderContext) { private async getGlobalToastList() { return await testSubjects.find('globalToastList'); } + + public async getToastCount() { + const list = await this.getGlobalToastList(); + const toasts = await list.findAllByCssSelector(`.euiToast`); + return toasts.length; + } } return new Toasts(); diff --git a/test/interpreter_functional/screenshots/baseline/combined_test.png b/test/interpreter_functional/screenshots/baseline/combined_test.png index 56c055b8b1cf..b828012f3930 100644 Binary files a/test/interpreter_functional/screenshots/baseline/combined_test.png and b/test/interpreter_functional/screenshots/baseline/combined_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png index 753ab2c2c6e9..4f728f511174 100644 Binary files a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png and b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_all_data.png b/test/interpreter_functional/screenshots/baseline/metric_all_data.png index 44226877bdc5..0a9475fc710d 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_all_data.png and b/test/interpreter_functional/screenshots/baseline/metric_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png index e0cffd065fc4..aab2905cee19 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png and b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png index 6578f8e30415..2ae380df282b 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png and b/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png index 14457f0a4d0a..03cc2e4d77d3 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png and b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png index c4fc4d397915..2cf25aff54a7 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png and b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_1.png b/test/interpreter_functional/screenshots/baseline/partial_test_1.png index 51998d019c66..9d52fb30b7d6 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_1.png and b/test/interpreter_functional/screenshots/baseline/partial_test_1.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_2.png b/test/interpreter_functional/screenshots/baseline/partial_test_2.png index 56c055b8b1cf..b828012f3930 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_2.png and b/test/interpreter_functional/screenshots/baseline/partial_test_2.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_3.png b/test/interpreter_functional/screenshots/baseline/partial_test_3.png index 7b96f3ec43c7..c43169bfb710 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_3.png and b/test/interpreter_functional/screenshots/baseline/partial_test_3.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png index a7088de3849a..4938d13fcb41 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png index 8f93ba81ad2a..b3703ecc7a33 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png index e0cffd065fc4..c43169bfb710 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png index 98890b9687ac..f8de00f81926 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png index 479280f598ae..f862a9cd46c6 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png differ diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index 84203617ff85..550b3b5df12b 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 276087511919..59de1f285799 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 276087511919..59de1f285799 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index ae72bcfa6d5e..cf488ac7f3ff 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json b/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json index fa5892190e5b..0a47cdb8ff74 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json @@ -1 +1 @@ -"[metricVis] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 8568215fd9e1..8c272901c4e8 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index d11e1dfb925f..abc0d3a44698 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index b160e05935f1..1809df5e709f 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index 9c642e5e266d..ec32b07ed9f2 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index 276087511919..59de1f285799 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json index 4241d6f208bf..09602eca4abf 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_3.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index 84203617ff85..550b3b5df12b 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 276087511919..59de1f285799 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 153eea71dd8d..071172c698ad 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index d5f01afa468a..ad38bb28b332 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json index 46b52a7b3eaa..0c50947beca9 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json @@ -1 +1 @@ -"[tagcloud] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index 72b5e957c19a..997285adfe5f 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index 7cbe7cc79882..10e23d860637 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json index 84203617ff85..550b3b5df12b 100644 --- a/test/interpreter_functional/snapshots/session/combined_test2.json +++ b/test/interpreter_functional/snapshots/session/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json index 276087511919..59de1f285799 100644 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json index 276087511919..59de1f285799 100644 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json index ae72bcfa6d5e..cf488ac7f3ff 100644 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_invalid_data.json b/test/interpreter_functional/snapshots/session/metric_invalid_data.json index fa5892190e5b..0a47cdb8ff74 100644 --- a/test/interpreter_functional/snapshots/session/metric_invalid_data.json +++ b/test/interpreter_functional/snapshots/session/metric_invalid_data.json @@ -1 +1 @@ -"[metricVis] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json index 8568215fd9e1..8c272901c4e8 100644 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json index d11e1dfb925f..abc0d3a44698 100644 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json index b160e05935f1..1809df5e709f 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index 9c642e5e266d..ec32b07ed9f2 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json index 276087511919..59de1f285799 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_3.json b/test/interpreter_functional/snapshots/session/partial_test_3.json index 4241d6f208bf..09602eca4abf 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_3.json +++ b/test/interpreter_functional/snapshots/session/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json index 84203617ff85..550b3b5df12b 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test2.json +++ b/test/interpreter_functional/snapshots/session/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json index 276087511919..59de1f285799 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index 153eea71dd8d..071172c698ad 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index d5f01afa468a..ad38bb28b332 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json index 46b52a7b3eaa..0c50947beca9 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json @@ -1 +1 @@ -"[tagcloud] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index 72b5e957c19a..997285adfe5f 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index 7cbe7cc79882..10e23d860637 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/test_suites/run_pipeline/helpers.ts b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts index bbf45b003c33..e5130ac95b7f 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/helpers.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts @@ -176,10 +176,16 @@ export function expectExpressionProvider({ log.debug('starting to render'); const result = await browser.executeAsync( (_context: ExpressionResult, done: (renderResult: any) => void) => - window.renderPipelineResponse(_context).then((renderResult: any) => { - done(renderResult); - return renderResult; - }), + window + .renderPipelineResponse(_context) + .then((renderResult: any) => { + done(renderResult); + return renderResult; + }) + .catch((e) => { + done(e); + return e; + }), pipelineResponse ); log.debug('response of rendering: ', result); diff --git a/test/plugin_functional/plugins/session_notifications/kibana.json b/test/plugin_functional/plugins/session_notifications/kibana.json new file mode 100644 index 000000000000..0b80b531d2f8 --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "session_notifications", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["session_notifications"], + "server": false, + "ui": true, + "requiredPlugins": ["data", "navigation"] +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/session_notifications/package.json b/test/plugin_functional/plugins/session_notifications/package.json new file mode 100644 index 000000000000..7a61867db2b5 --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/package.json @@ -0,0 +1,18 @@ +{ + "name": "session_notifications", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/session_notifications", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "4.0.2" + } +} + diff --git a/test/plugin_functional/plugins/session_notifications/public/index.ts b/test/plugin_functional/plugins/session_notifications/public/index.ts new file mode 100644 index 000000000000..fbc573e8dc6f --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/public/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/public'; +import { SessionNotificationsPlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new SessionNotificationsPlugin(); diff --git a/test/plugin_functional/plugins/session_notifications/public/plugin.tsx b/test/plugin_functional/plugins/session_notifications/public/plugin.tsx new file mode 100644 index 000000000000..a41ecb3edebd --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/public/plugin.tsx @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { AppPluginDependenciesStart, AppPluginDependenciesSetup } from './types'; + +export class SessionNotificationsPlugin implements Plugin { + private sessionIds: Array = []; + public setup(core: CoreSetup, { navigation }: AppPluginDependenciesSetup) { + const showSessions = { + id: 'showSessionsButton', + label: 'Show Sessions', + description: 'Sessions', + run: () => { + core.notifications.toasts.addInfo(this.sessionIds.join(','), { + toastLifeTimeMs: 50000, + }); + }, + tooltip: () => { + return this.sessionIds.join(','); + }, + testId: 'showSessionsButton', + }; + + navigation.registerMenuItem(showSessions); + + const clearSessions = { + id: 'clearSessionsButton', + label: 'Clear Sessions', + description: 'Sessions', + run: () => { + this.sessionIds.length = 0; + }, + testId: 'clearSessionsButton', + }; + + navigation.registerMenuItem(clearSessions); + } + + public start(core: CoreStart, { data }: AppPluginDependenciesStart) { + core.application.currentAppId$.subscribe(() => { + this.sessionIds.length = 0; + }); + + data.search.session.getSession$().subscribe((sessionId?: string) => { + this.sessionIds.push(sessionId); + }); + } + public stop() {} +} diff --git a/test/plugin_functional/plugins/session_notifications/public/types.ts b/test/plugin_functional/plugins/session_notifications/public/types.ts new file mode 100644 index 000000000000..de9055d03d21 --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/public/types.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { NavigationPublicPluginSetup } from '../../../../../src/plugins/navigation/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; + +export interface AppPluginDependenciesSetup { + navigation: NavigationPublicPluginSetup; +} +export interface AppPluginDependenciesStart { + data: DataPublicPluginStart; +} diff --git a/test/plugin_functional/plugins/session_notifications/tsconfig.json b/test/plugin_functional/plugins/session_notifications/tsconfig.json new file mode 100644 index 000000000000..3d9d8ca9451d --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/data_plugin/index.ts b/test/plugin_functional/test_suites/data_plugin/index.ts index bbf9d823e357..212a75b9cf44 100644 --- a/test/plugin_functional/test_suites/data_plugin/index.ts +++ b/test/plugin_functional/test_suites/data_plugin/index.ts @@ -35,7 +35,9 @@ export default function ({ await PageObjects.common.navigateToApp('settings'); await PageObjects.settings.createIndexPattern('shakespeare', ''); }); - loadTestFile(require.resolve('./index_patterns')); + loadTestFile(require.resolve('./search')); + loadTestFile(require.resolve('./session')); + loadTestFile(require.resolve('./index_patterns')); }); } diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 4359816efb95..2c846dc78031 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -23,7 +23,9 @@ import '../../plugins/core_provider_plugin/types'; export default function ({ getService }: PluginFunctionalProviderContext) { const supertest = getService('supertest'); - describe('index patterns', function () { + // skipping the tests as it deletes index patterns created by other test causing unexpected failures + // https://github.com/elastic/kibana/issues/79886 + describe.skip('index patterns', function () { let indexPatternId = ''; it('can get all ids', async () => { diff --git a/test/plugin_functional/test_suites/data_plugin/session.ts b/test/plugin_functional/test_suites/data_plugin/session.ts new file mode 100644 index 000000000000..88241fffae90 --- /dev/null +++ b/test/plugin_functional/test_suites/data_plugin/session.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'discover', 'timePicker']); + const filterBar = getService('filterBar'); + const testSubjects = getService('testSubjects'); + const toasts = getService('toasts'); + + const getSessionIds = async () => { + const sessionsBtn = await testSubjects.find('showSessionsButton'); + await sessionsBtn.click(); + const toast = await toasts.getToastElement(1); + const sessionIds = await toast.getVisibleText(); + return sessionIds.split(','); + }; + + describe('Session management', function describeIndexTests() { + describe('Discover', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover'); + await testSubjects.click('clearSessionsButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + afterEach(async () => { + await testSubjects.click('clearSessionsButton'); + await toasts.dismissAllToasts(); + }); + + it('Starts on index pattern select', async () => { + await PageObjects.discover.selectIndexPattern('shakespeare'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + + // Discover calls destroy on index pattern change, which explicitly closes a session + expect(sessionIds.length).to.be(2); + expect(sessionIds[0].length).to.be(0); + expect(sessionIds[1].length).not.to.be(0); + }); + + it('Starts on a refresh', async () => { + await testSubjects.click('querySubmitButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + + it('Starts a new session on sort', async () => { + await PageObjects.discover.clickFieldListItemAdd('speaker'); + await PageObjects.discover.clickFieldSort('speaker'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + + it('Starts a new session on filter change', async () => { + await filterBar.addFilter('line_number', 'is', '4.3.108'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + }); + }); +} diff --git a/x-pack/package.json b/x-pack/package.json index 67efa9f474c0..91fecff09411 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -44,12 +44,6 @@ "@mapbox/mapbox-gl-draw": "^1.2.0", "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@scant/router": "^0.1.0", - "@storybook/addon-actions": "^6.0.16", - "@storybook/addon-essentials": "^6.0.16", - "@storybook/addon-knobs": "^6.0.16", - "@storybook/addon-storyshots": "^6.0.16", - "@storybook/react": "^6.0.16", - "@storybook/theming": "^6.0.16", "@testing-library/dom": "^7.24.2", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", @@ -198,7 +192,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/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index b25e33400df5..e641b81189b9 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -11,6 +11,7 @@ import { ActionExecutor, ExecutorError, ILicenseState, TaskRunnerFactory } from import { actionsConfigMock } from './actions_config.mock'; import { licenseStateMock } from './lib/license_state.mock'; import { ActionsConfigurationUtilities } from './actions_config'; +import { licensingMock } from '../../licensing/server/mocks'; const mockTaskManager = taskManagerMock.setup(); let mockedLicenseState: jest.Mocked; @@ -22,6 +23,7 @@ beforeEach(() => { mockedLicenseState = licenseStateMock.create(); mockedActionsConfig = actionsConfigMock.create(); actionTypeRegistryParams = { + licensing: licensingMock.createSetup(), taskManager: mockTaskManager, taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) @@ -51,7 +53,7 @@ describe('register()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - minimumLicenseRequired: 'basic', + minimumLicenseRequired: 'gold', executor, }); expect(actionTypeRegistry.has('my-action-type')).toEqual(true); @@ -69,6 +71,10 @@ describe('register()', () => { }, ] `); + expect(actionTypeRegistryParams.licensing.featureUsage.register).toHaveBeenCalledWith( + 'Connector: My action type', + 'gold' + ); }); test('shallow clones the given action type', () => { @@ -123,6 +129,31 @@ describe('register()', () => { expect(getRetry(0, new ExecutorError('my message', {}, undefined))).toEqual(false); expect(getRetry(0, new ExecutorError('my message', {}, retryTime))).toEqual(retryTime); }); + + test('registers gold+ action types to the licensing feature usage API', () => { + const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'gold', + executor, + }); + expect(actionTypeRegistryParams.licensing.featureUsage.register).toHaveBeenCalledWith( + 'Connector: My action type', + 'gold' + ); + }); + + test(`doesn't register basic action types to the licensing feature usage API`, () => { + const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + executor, + }); + expect(actionTypeRegistryParams.licensing.featureUsage.register).not.toHaveBeenCalled(); + }); }); describe('get()', () => { @@ -232,10 +263,20 @@ describe('isActionTypeEnabled', () => { expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true); }); - test('should call isLicenseValidForActionType of the license state', async () => { + test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => { mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); actionTypeRegistry.isActionTypeEnabled('foo'); - expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: false, + }); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionTypeEnabled('foo', { notifyUsage: true }); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: true, + }); }); test('should return false when isActionTypeEnabled is false and isLicenseValidForActionType is true', async () => { @@ -298,3 +339,36 @@ describe('ensureActionTypeEnabled', () => { ).toThrowErrorMatchingInlineSnapshot(`"Fail"`); }); }); + +describe('isActionExecutable()', () => { + let actionTypeRegistry: ActionTypeRegistry; + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'basic', + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, + }; + + beforeEach(() => { + actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register(fooActionType); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionExecutable('123', 'foo'); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: false, + }); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionExecutable('123', 'foo', { notifyUsage: true }); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: true, + }); + }); +}); diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 4015381ff950..b93d4a6e78ac 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -7,9 +7,15 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; -import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib'; import { ActionType as CommonActionType } from '../common'; import { ActionsConfigurationUtilities } from './actions_config'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { + ExecutorError, + getActionTypeFeatureUsageName, + TaskRunnerFactory, + ILicenseState, +} from './lib'; import { ActionType, PreConfiguredAction, @@ -19,6 +25,7 @@ import { } from './types'; export interface ActionTypeRegistryOpts { + licensing: LicensingPluginSetup; taskManager: TaskManagerSetupContract; taskRunnerFactory: TaskRunnerFactory; actionsConfigUtils: ActionsConfigurationUtilities; @@ -33,6 +40,7 @@ export class ActionTypeRegistry { private readonly actionsConfigUtils: ActionsConfigurationUtilities; private readonly licenseState: ILicenseState; private readonly preconfiguredActions: PreConfiguredAction[]; + private readonly licensing: LicensingPluginSetup; constructor(constructorParams: ActionTypeRegistryOpts) { this.taskManager = constructorParams.taskManager; @@ -40,6 +48,7 @@ export class ActionTypeRegistry { this.actionsConfigUtils = constructorParams.actionsConfigUtils; this.licenseState = constructorParams.licenseState; this.preconfiguredActions = constructorParams.preconfiguredActions; + this.licensing = constructorParams.licensing; } /** @@ -54,26 +63,36 @@ export class ActionTypeRegistry { */ public ensureActionTypeEnabled(id: string) { this.actionsConfigUtils.ensureActionTypeEnabled(id); + // Important to happen last because the function will notify of feature usage at the + // same time and it shouldn't notify when the action type isn't enabled this.licenseState.ensureLicenseForActionType(this.get(id)); } /** * Returns true if action type is enabled in the config and a valid license is used. */ - public isActionTypeEnabled(id: string) { + public isActionTypeEnabled( + id: string, + options: { notifyUsage: boolean } = { notifyUsage: false } + ) { return ( this.actionsConfigUtils.isActionTypeEnabled(id) && - this.licenseState.isLicenseValidForActionType(this.get(id)).isValid === true + this.licenseState.isLicenseValidForActionType(this.get(id), options).isValid === true ); } /** * Returns true if action type is enabled or it is a preconfigured action type. */ - public isActionExecutable(actionId: string, actionTypeId: string) { + public isActionExecutable( + actionId: string, + actionTypeId: string, + options: { notifyUsage: boolean } = { notifyUsage: false } + ) { + const actionTypeEnabled = this.isActionTypeEnabled(actionTypeId, options); return ( - this.isActionTypeEnabled(actionTypeId) || - (!this.isActionTypeEnabled(actionTypeId) && + actionTypeEnabled || + (!actionTypeEnabled && this.preconfiguredActions.find( (preconfiguredAction) => preconfiguredAction.id === actionId ) !== undefined) @@ -118,6 +137,13 @@ export class ActionTypeRegistry { createTaskRunner: (context: RunContext) => this.taskRunnerFactory.create(context), }, }); + // No need to notify usage on basic action types + if (actionType.minimumLicenseRequired !== 'basic') { + this.licensing.featureUsage.register( + getActionTypeFeatureUsageName(actionType as ActionType), + actionType.minimumLicenseRequired + ); + } } /** diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 48122a5ce4e0..0c16c88ad7a8 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -20,6 +20,7 @@ const createActionsClientMock = () => { execute: jest.fn(), enqueueExecution: jest.fn(), listTypes: jest.fn(), + isActionTypeEnabled: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index adef12454f2d..2b6aec42e0d2 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -8,12 +8,13 @@ import { schema } from '@kbn/config-schema'; import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_registry'; import { ActionsClient } from './actions_client'; -import { ExecutorType } from './types'; +import { ExecutorType, ActionType } from './types'; import { ActionExecutor, TaskRunnerFactory, ILicenseState } from './lib'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { actionsConfigMock } from './actions_config.mock'; import { getActionsConfigurationUtilities } from './actions_config'; import { licenseStateMock } from './lib/license_state.mock'; +import { licensingMock } from '../../licensing/server/mocks'; import { elasticsearchServiceMock, @@ -47,6 +48,7 @@ beforeEach(() => { jest.resetAllMocks(); mockedLicenseState = licenseStateMock.create(); actionTypeRegistryParams = { + licensing: licensingMock.createSetup(), taskManager: mockTaskManager, taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) @@ -299,6 +301,7 @@ describe('create()', () => { }); const localActionTypeRegistryParams = { + licensing: licensingMock.createSetup(), taskManager: mockTaskManager, taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) @@ -1244,3 +1247,31 @@ describe('enqueueExecution()', () => { expect(executionEnqueuer).toHaveBeenCalledWith(unsecuredSavedObjectsClient, opts); }); }); + +describe('isActionTypeEnabled()', () => { + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'gold', + executor: jest.fn(), + }; + beforeEach(() => { + actionTypeRegistry.register(fooActionType); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionsClient.isActionTypeEnabled('foo'); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: false, + }); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionsClient.isActionTypeEnabled('foo', { notifyUsage: true }); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: true, + }); + }); +}); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 4079a6ddeeb8..e565d420d772 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -343,6 +343,13 @@ export class ActionsClient { public async listTypes(): Promise { return this.actionTypeRegistry.list(); } + + public isActionTypeEnabled( + actionTypeId: string, + options: { notifyUsage: boolean } = { notifyUsage: false } + ) { + return this.actionTypeRegistry.isActionTypeEnabled(actionTypeId, options); + } } function actionFromSavedObject(savedObject: SavedObject): ActionResult { diff --git a/x-pack/plugins/actions/server/authorization/audit_logger.ts b/x-pack/plugins/actions/server/authorization/audit_logger.ts index 7e0adc920665..3bbf60b0b3ed 100644 --- a/x-pack/plugins/actions/server/authorization/audit_logger.ts +++ b/x-pack/plugins/actions/server/authorization/audit_logger.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditLogger } from '../../../security/server'; +import { LegacyAuditLogger } from '../../../security/server'; export enum AuthorizationResult { Unauthorized = 'Unauthorized', @@ -12,9 +12,9 @@ export enum AuthorizationResult { } export class ActionsAuthorizationAuditLogger { - private readonly auditLogger: AuditLogger; + private readonly auditLogger: LegacyAuditLogger; - constructor(auditLogger: AuditLogger = { log() {} }) { + constructor(auditLogger: LegacyAuditLogger = { log() {} }) { this.auditLogger = auditLogger; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index acab6dd41b4b..f7882849708e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -12,6 +12,7 @@ import { Logger } from '../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../actions_config.mock'; import { licenseStateMock } from '../lib/license_state.mock'; +import { licensingMock } from '../../../licensing/server/mocks'; const ACTION_TYPE_IDS = ['.index', '.email', '.pagerduty', '.server-log', '.slack', '.webhook']; @@ -21,6 +22,7 @@ export function createActionTypeRegistry(): { } { const logger = loggingSystemMock.create().get() as jest.Mocked; const actionTypeRegistry = new ActionTypeRegistry({ + licensing: licensingMock.createSetup(), taskManager: taskManagerMock.setup(), taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 7682f01ed769..33e78ee444cd 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -23,9 +23,10 @@ beforeEach(() => jest.resetAllMocks()); describe('execute()', () => { test('schedules the action with all given parameters', async () => { + const actionTypeRegistry = actionTypeRegistryMock.create(); const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, - actionTypeRegistry: actionTypeRegistryMock.create(), + actionTypeRegistry, isESOUsingEphemeralEncryptionKey: false, preconfiguredActions: [], }); @@ -76,6 +77,9 @@ describe('execute()', () => { }, {} ); + expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('123', 'mock-action', { + notifyUsage: true, + }); }); test('schedules the action with all given parameters with a preconfigured action', async () => { diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index b226583fade5..f0a22c642cf6 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -51,7 +51,7 @@ export function createExecutionEnqueuerFunction({ id ); - if (!actionTypeRegistry.isActionExecutable(id, actionTypeId)) { + if (!actionTypeRegistry.isActionExecutable(id, actionTypeId, { notifyUsage: true })) { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); } diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 692d14e859b3..4ff56536e386 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -90,6 +90,9 @@ test('successfully executes', async () => { ); expect(actionTypeRegistry.get).toHaveBeenCalledWith('test'); + expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('1', 'test', { + notifyUsage: true, + }); expect(actionType.executor).toHaveBeenCalledWith({ actionId: '1', diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 0d4d6de3be1f..0015b417d72c 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -102,7 +102,7 @@ export class ActionExecutor { namespace.namespace ); - if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId)) { + if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId, { notifyUsage: true })) { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); } const actionType = actionTypeRegistry.get(actionTypeId); diff --git a/x-pack/plugins/audit_trail/server/types.ts b/x-pack/plugins/actions/server/lib/get_action_type_feature_usage_name.ts similarity index 50% rename from x-pack/plugins/audit_trail/server/types.ts rename to x-pack/plugins/actions/server/lib/get_action_type_feature_usage_name.ts index 1b7afb09f062..75919442b2ce 100644 --- a/x-pack/plugins/audit_trail/server/types.ts +++ b/x-pack/plugins/actions/server/lib/get_action_type_feature_usage_name.ts @@ -3,15 +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. */ -/** - * Event enhanced with request context data. Provided to an external consumer. - * @public - */ -export interface AuditEvent { - message: string; - type: string; - scope?: string; - user?: string; - space?: string; - requestId?: string; + +import { ActionType } from '../types'; + +export function getActionTypeFeatureUsageName(actionType: ActionType) { + return `Connector: ${actionType.name}`; } diff --git a/x-pack/plugins/actions/server/lib/index.ts b/x-pack/plugins/actions/server/lib/index.ts index e97875b91cf3..4c8e7ab17db6 100644 --- a/x-pack/plugins/actions/server/lib/index.ts +++ b/x-pack/plugins/actions/server/lib/index.ts @@ -10,6 +10,7 @@ export { TaskRunnerFactory } from './task_runner_factory'; export { ActionExecutor, ActionExecutorContract } from './action_executor'; export { ILicenseState, LicenseState } from './license_state'; export { verifyApiAccess } from './verify_api_access'; +export { getActionTypeFeatureUsageName } from './get_action_type_feature_usage_name'; export { ActionTypeDisabledError, ActionTypeDisabledReason, diff --git a/x-pack/plugins/actions/server/lib/license_state.mock.ts b/x-pack/plugins/actions/server/lib/license_state.mock.ts index d59e9dbdc540..e5bd9fc9d16c 100644 --- a/x-pack/plugins/actions/server/lib/license_state.mock.ts +++ b/x-pack/plugins/actions/server/lib/license_state.mock.ts @@ -12,6 +12,7 @@ export const createLicenseStateMock = () => { getLicenseInformation: jest.fn(), ensureLicenseForActionType: jest.fn(), isLicenseValidForActionType: jest.fn(), + setNotifyUsage: jest.fn(), checkLicense: jest.fn().mockResolvedValue({ state: 'valid', }), diff --git a/x-pack/plugins/actions/server/lib/license_state.test.ts b/x-pack/plugins/actions/server/lib/license_state.test.ts index 32c3c54faf00..06148b1825e7 100644 --- a/x-pack/plugins/actions/server/lib/license_state.test.ts +++ b/x-pack/plugins/actions/server/lib/license_state.test.ts @@ -55,6 +55,7 @@ describe('checkLicense()', () => { describe('isLicenseValidForActionType', () => { let license: Subject; let licenseState: ILicenseState; + const mockNotifyUsage = jest.fn(); const fooActionType: ActionType = { id: 'foo', name: 'Foo', @@ -67,6 +68,7 @@ describe('isLicenseValidForActionType', () => { beforeEach(() => { license = new Subject(); licenseState = new LicenseState(license); + licenseState.setNotifyUsage(mockNotifyUsage); }); test('should return false when license not defined', () => { @@ -113,11 +115,42 @@ describe('isLicenseValidForActionType', () => { isValid: true, }); }); + + test('should not call notifyUsage by default', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.isLicenseValidForActionType(fooActionType); + expect(mockNotifyUsage).not.toHaveBeenCalled(); + }); + + test('should not call notifyUsage on basic action types', () => { + const basicLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'basic' }, + }); + license.next(basicLicense); + licenseState.isLicenseValidForActionType({ + ...fooActionType, + minimumLicenseRequired: 'basic', + }); + expect(mockNotifyUsage).not.toHaveBeenCalled(); + }); + + test('should call notifyUsage when specified', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.isLicenseValidForActionType(fooActionType, { notifyUsage: true }); + expect(mockNotifyUsage).toHaveBeenCalledWith('Connector: Foo'); + }); }); describe('ensureLicenseForActionType()', () => { let license: Subject; let licenseState: ILicenseState; + const mockNotifyUsage = jest.fn(); const fooActionType: ActionType = { id: 'foo', name: 'Foo', @@ -130,6 +163,7 @@ describe('ensureLicenseForActionType()', () => { beforeEach(() => { license = new Subject(); licenseState = new LicenseState(license); + licenseState.setNotifyUsage(mockNotifyUsage); }); test('should throw when license not defined', () => { @@ -178,6 +212,15 @@ describe('ensureLicenseForActionType()', () => { license.next(goldLicense); licenseState.ensureLicenseForActionType(fooActionType); }); + + test('should call notifyUsage', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.ensureLicenseForActionType(fooActionType); + expect(mockNotifyUsage).toHaveBeenCalledWith('Connector: Foo'); + }); }); function createUnavailableLicense() { diff --git a/x-pack/plugins/actions/server/lib/license_state.ts b/x-pack/plugins/actions/server/lib/license_state.ts index 1686d0201e96..902fadb3da17 100644 --- a/x-pack/plugins/actions/server/lib/license_state.ts +++ b/x-pack/plugins/actions/server/lib/license_state.ts @@ -11,6 +11,8 @@ import { ILicense } from '../../../licensing/common/types'; import { PLUGIN } from '../constants/plugin'; import { ActionType } from '../types'; import { ActionTypeDisabledError } from './errors'; +import { LicensingPluginStart } from '../../../licensing/server'; +import { getActionTypeFeatureUsageName } from './get_action_type_feature_usage_name'; export type ILicenseState = PublicMethodsOf; @@ -24,6 +26,7 @@ export class LicenseState { private licenseInformation: ActionsLicenseInformation = this.checkLicense(undefined); private subscription: Subscription; private license?: ILicense; + private _notifyUsage: LicensingPluginStart['featureUsage']['notifyUsage'] | null = null; constructor(license$: Observable) { this.subscription = license$.subscribe(this.updateInformation.bind(this)); @@ -34,6 +37,10 @@ export class LicenseState { this.licenseInformation = this.checkLicense(license); } + public setNotifyUsage(notifyUsage: LicensingPluginStart['featureUsage']['notifyUsage']) { + this._notifyUsage = notifyUsage; + } + public clean() { this.subscription.unsubscribe(); } @@ -43,8 +50,13 @@ export class LicenseState { } public isLicenseValidForActionType( - actionType: ActionType + actionType: ActionType, + { notifyUsage }: { notifyUsage: boolean } = { notifyUsage: false } ): { isValid: true } | { isValid: false; reason: 'unavailable' | 'expired' | 'invalid' } { + if (notifyUsage) { + this.notifyUsage(actionType); + } + if (!this.license?.isAvailable) { return { isValid: false, reason: 'unavailable' }; } @@ -65,7 +77,16 @@ export class LicenseState { } } + private notifyUsage(actionType: ActionType) { + // No need to notify usage on basic action types + if (this._notifyUsage && actionType.minimumLicenseRequired !== 'basic') { + this._notifyUsage(getActionTypeFeatureUsageName(actionType)); + } + } + public ensureLicenseForActionType(actionType: ActionType) { + this.notifyUsage(actionType); + const check = this.isLicenseValidForActionType(actionType); if (check.isValid) { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 9d545600e61e..7f7f9e196da0 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -211,6 +211,7 @@ describe('Actions Plugin', () => { features: featuresPluginMock.createSetup(), }; pluginsStart = { + licensing: licensingMock.createStart(), taskManager: taskManagerMock.createStart(), encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), }; @@ -255,5 +256,49 @@ describe('Actions Plugin', () => { ); }); }); + + describe('isActionTypeEnabled()', () => { + const actionType: ActionType = { + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'gold', + executor: jest.fn(), + }; + + it('passes through the notifyUsage option when set to true', async () => { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + pluginSetup.registerType(actionType); + const pluginStart = plugin.start(coreStart, pluginsStart); + + pluginStart.isActionTypeEnabled('my-action-type', { notifyUsage: true }); + expect(pluginsStart.licensing.featureUsage.notifyUsage).toHaveBeenCalledWith( + 'Connector: My action type' + ); + }); + }); + + describe('isActionExecutable()', () => { + const actionType: ActionType = { + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'gold', + executor: jest.fn(), + }; + + it('passes through the notifyUsage option when set to true', async () => { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + pluginSetup.registerType(actionType); + const pluginStart = plugin.start(coreStart, pluginsStart); + + pluginStart.isActionExecutable('123', 'my-action-type', { notifyUsage: true }); + expect(pluginsStart.licensing.featureUsage.notifyUsage).toHaveBeenCalledWith( + 'Connector: My action type' + ); + }); + }); }); }); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 1a15a5a81519..ef20ffbb9ee6 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -26,7 +26,7 @@ import { EncryptedSavedObjectsPluginStart, } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import { LicensingPluginSetup } from '../../licensing/server'; +import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { LICENSE_TYPE } from '../../licensing/common/types'; import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; @@ -93,8 +93,12 @@ export interface PluginSetupContract { } export interface PluginStartContract { - isActionTypeEnabled(id: string): boolean; - isActionExecutable(actionId: string, actionTypeId: string): boolean; + isActionTypeEnabled(id: string, options?: { notifyUsage: boolean }): boolean; + isActionExecutable( + actionId: string, + actionTypeId: string, + options?: { notifyUsage: boolean } + ): boolean; getActionsClientWithRequest(request: KibanaRequest): Promise>; getActionsAuthorizationWithRequest(request: KibanaRequest): PublicMethodsOf; preconfiguredActions: PreConfiguredAction[]; @@ -113,6 +117,7 @@ export interface ActionsPluginsSetup { export interface ActionsPluginsStart { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; + licensing: LicensingPluginStart; } const includedHiddenTypes = [ @@ -196,6 +201,7 @@ export class ActionsPlugin implements Plugin, Plugi } const actionTypeRegistry = new ActionTypeRegistry({ + licensing: plugins.licensing, taskRunnerFactory, taskManager: plugins.taskManager, actionsConfigUtils, @@ -268,6 +274,7 @@ export class ActionsPlugin implements Plugin, Plugi public start(core: CoreStart, plugins: ActionsPluginsStart): PluginStartContract { const { logger, + licenseState, actionExecutor, actionTypeRegistry, taskRunnerFactory, @@ -278,6 +285,8 @@ export class ActionsPlugin implements Plugin, Plugi getUnsecuredSavedObjectsClient, } = this; + licenseState?.setNotifyUsage(plugins.licensing.featureUsage.notifyUsage); + const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({ includedHiddenTypes, }); @@ -368,11 +377,15 @@ export class ActionsPlugin implements Plugin, Plugi scheduleActionsTelemetry(this.telemetryLogger, plugins.taskManager); return { - isActionTypeEnabled: (id) => { - return this.actionTypeRegistry!.isActionTypeEnabled(id); + isActionTypeEnabled: (id, options = { notifyUsage: false }) => { + return this.actionTypeRegistry!.isActionTypeEnabled(id, options); }, - isActionExecutable: (actionId: string, actionTypeId: string) => { - return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId); + isActionExecutable: ( + actionId: string, + actionTypeId: string, + options = { notifyUsage: false } + ) => { + return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId, options); }, getActionsAuthorizationWithRequest(request: KibanaRequest) { return instantiateAuthorization(request); diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts deleted file mode 100644 index b20018fcc26f..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/task_manager.mock'; -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.start(); -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..88abce729862 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[]; @@ -1038,6 +1038,11 @@ export class AlertsClient { const actionsClient = await this.getActionsClient(); const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; const actionResults = await actionsClient.getBulk(actionIds); + const actionTypeIds = [...new Set(actionResults.map((action) => action.actionTypeId))]; + actionTypeIds.forEach((id) => { + // Notify action type usage via "isActionTypeEnabled" function + actionsClient.isActionTypeEnabled(id, { notifyUsage: true }); + }); alertActions.forEach(({ id, ...alertAction }, i) => { const actionResultValue = actionResults.find((action) => action.id === id); if (actionResultValue) { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts b/x-pack/plugins/alerts/server/alerts_client/index.ts similarity index 76% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts rename to x-pack/plugins/alerts/server/alerts_client/index.ts index f9e939058adb..e40076a29fff 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts +++ 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 { DataTierAllocationField } from './data_tier_allocation_field'; +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..56e868732e3f --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -0,0 +1,1105 @@ +/* + * 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/task_manager.mock'; +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, ActionsClient } from '../../../../actions/server'; +import { TaskStatus } from '../../../../task_manager/server'; +import { getBeforeSetup, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.start(); +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", + } + `); + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< + ActionsClient + >; + expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); + }); + + 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(); + // Reset from default behaviour + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< + ActionsClient + >; + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"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..1ebd9fc296b1 --- /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/task_manager.mock'; +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.start(); +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..2dd3da07234c --- /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/task_manager.mock'; +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.start(); +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..b214d8ba697b --- /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/task_manager.mock'; +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.start(); +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..bf55a2070d8f --- /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/task_manager.mock'; +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.start(); +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..327a1fa23ef0 --- /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/task_manager.mock'; +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.start(); +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..09212732b76e --- /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/task_manager.mock'; +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.start(); +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..42e573aea347 --- /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/task_manager.mock'; +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.start(); +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..4337ed6c491d --- /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/task_manager.mock'; +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.start(); +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..44ee6713f256 --- /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/task_manager.mock'; +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.start(); +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..dc9a1600a577 --- /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/task_manager.mock'; +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.start(); +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..45920db105c2 --- /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/task_manager.mock'; +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.start(); +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..560401150113 --- /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/task_manager.mock'; +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.start(); +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..60b5b62954f0 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -0,0 +1,1262 @@ +/* + * 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/task_manager.mock'; +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, ActionsClient } from '../../../../actions/server'; +import { TaskStatus } from '../../../../task_manager/server'; +import { getBeforeSetup, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.start(); +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", + } + `); + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< + ActionsClient + >; + expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); + expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test2', { notifyUsage: true }); + }); + + it('calls the createApiKey function', async () => { + 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..97ddfa5e4adb --- /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/task_manager.mock'; +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.start(); +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/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index ac91d689798c..d747efbb959d 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -20,7 +20,7 @@ import { securityMock } from '../../security/server/mocks'; import { PluginStartContract as ActionsStartContract } from '../../actions/server'; import { actionsMock, actionsAuthorizationMock } from '../../actions/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; -import { AuditLogger } from '../../security/server'; +import { LegacyAuditLogger } from '../../security/server'; import { ALERTS_FEATURE_ID } from '../common'; import { eventLogMock } from '../../event_log/server/mocks'; @@ -85,7 +85,7 @@ test('creates an alerts client with proper constructor arguments when security i const logger = { log: jest.fn(), - } as jest.Mocked; + } as jest.Mocked; securityPluginSetup.audit.getLogger.mockReturnValue(logger); factory.create(request, savedObjectsService); diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.ts index f930da2ce428..7f259df71746 100644 --- a/x-pack/plugins/alerts/server/authorization/audit_logger.ts +++ b/x-pack/plugins/alerts/server/authorization/audit_logger.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditLogger } from '../../../security/server'; +import { LegacyAuditLogger } from '../../../security/server'; export enum ScopeType { Consumer, @@ -17,9 +17,9 @@ export enum AuthorizationResult { } export class AlertsAuthorizationAuditLogger { - private readonly auditLogger: AuditLogger; + private readonly auditLogger: LegacyAuditLogger; - constructor(auditLogger: AuditLogger = { log() {} }) { + constructor(auditLogger: LegacyAuditLogger = { log() {} }) { this.auditLogger = auditLogger; } diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index aca447b6aded..21e642d228b4 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -86,7 +86,9 @@ export function createExecutionHandler({ const alertLabel = `${alertType.id}:${alertId}: '${alertName}'`; for (const action of actions) { - if (!actionsPlugin.isActionExecutable(action.id, action.actionTypeId)) { + if ( + !actionsPlugin.isActionExecutable(action.id, action.actionTypeId, { notifyUsage: true }) + ) { logger.warn( `Alert "${alertId}" skipped scheduling action "${action.id}" because it is disabled` ); diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index f910f34d258f..4f87e1310437 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -17,7 +17,7 @@ import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; import { AnomalySeverity, SelectAnomalySeverity, -} from './SelectAnomalySeverity'; +} from './select_anomaly_severity'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { EnvironmentField, diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx new file mode 100644 index 000000000000..0db8fa6c9d0d --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { IntlProvider } from 'react-intl'; +import { ANOMALY_SEVERITY } from '../../../../../ml/common'; +import { SelectAnomalySeverity } from './select_anomaly_severity'; + +function Wrapper({ children }: { children?: ReactNode }) { + return {children}; +} + +describe('SelectAnomalySeverity', () => { + it('shows the correct text for each item', async () => { + const result = render( + {}} + value={ANOMALY_SEVERITY.CRITICAL} + />, + { wrapper: Wrapper } + ); + const button = (await result.findAllByText('critical'))[1]; + + button.click(); + + const options = await result.findAllByTestId( + 'SelectAnomalySeverity option text' + ); + + expect( + options.map((option) => (option.firstChild as HTMLElement)?.innerHTML) + ).toEqual([ + 'score critical ', // Trailing space is intentional here, to keep the i18n simple + 'score major and above', + 'score minor and above', + 'score warning and above', + ]); + }); +}); diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx index 468d08339431..b0513c3b5957 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx @@ -45,11 +45,14 @@ export function SelectAnomalySeverity({ onChange, value }: Props) { -

+

diff --git a/x-pack/plugins/apm/public/components/app/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/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx index baba592e5886..b4408e20c04d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -14,7 +14,7 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { APMQueryParams } from '../../shared/Links/url_helpers'; import { CytoscapeContext } from './Cytoscape'; -import { getAnimationOptions, getNodeHeight } from './cytoscapeOptions'; +import { getAnimationOptions, getNodeHeight } from './cytoscape_options'; const ControlsContainer = styled('div')` left: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; 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..8a76c5f7bd8f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -17,7 +17,7 @@ import React, { useState, } from 'react'; import { useTheme } from '../../../hooks/useTheme'; -import { getCytoscapeOptions } from './cytoscapeOptions'; +import { getCytoscapeOptions } from './cytoscape_options'; import { useCytoscapeEventHandlers } from './use_cytoscape_event_handlers'; cytoscape.use(dagre); @@ -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/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index 3938349050e5..788e5f25b631 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -22,7 +22,7 @@ import { useTheme } from '../../../../hooks/useTheme'; import { fontSize, px } from '../../../../style/variables'; import { asInteger, asDuration } from '../../../../../common/utils/formatters'; import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { popoverWidth } from '../cytoscapeOptions'; +import { popoverWidth } from '../cytoscape_options'; import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; import { getSeverity, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 197bc94c6260..6dd0c6816573 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -15,7 +15,7 @@ import React, { MouseEvent } from 'react'; import { Buttons } from './Buttons'; import { Info } from './Info'; import { ServiceStatsFetcher } from './ServiceStatsFetcher'; -import { popoverWidth } from '../cytoscapeOptions'; +import { popoverWidth } from '../cytoscape_options'; interface ContentsProps { isService: boolean; 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/ServiceMap/Popover/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx index c4272d286901..7b7e3b46bb31 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx @@ -18,7 +18,7 @@ import cytoscape from 'cytoscape'; import { useTheme } from '../../../../hooks/useTheme'; import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; import { CytoscapeContext } from '../Cytoscape'; -import { getAnimationOptions } from '../cytoscapeOptions'; +import { getAnimationOptions } from '../cytoscape_options'; import { Contents } from './Contents'; interface PopoverProps { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts similarity index 95% rename from x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts rename to x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts index 136be1c7d947..e51f53567b5f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts @@ -5,17 +5,18 @@ */ import cytoscape from 'cytoscape'; import { CSSProperties } from 'react'; -import { - getServiceHealthStatusColor, - ServiceHealthStatus, -} from '../../../../common/service_health_status'; +import { EuiTheme } from '../../../../../observability/public'; +import { ServiceAnomalyStats } from '../../../../common/anomaly_detection'; import { SERVICE_NAME, SPAN_DESTINATION_SERVICE_RESOURCE, } from '../../../../common/elasticsearch_fieldnames'; -import { EuiTheme } from '../../../../../observability/public'; +import { + getServiceHealthStatusColor, + ServiceHealthStatus, +} from '../../../../common/service_health_status'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { defaultIcon, iconForNode } from './icons'; -import { ServiceAnomalyStats } from '../../../../common/anomaly_detection'; export const popoverWidth = 280; @@ -104,6 +105,11 @@ function isService(el: cytoscape.NodeSingular) { const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => { const lineColor = theme.eui.euiColorMediumShade; return [ + { + selector: 'core', + // @ts-expect-error DefinitelyTyped does not recognize 'active-bg-opacity' + style: { 'active-bg-opacity': 0 }, + }, { selector: 'node', style: { @@ -226,7 +232,10 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => { // The CSS styles for the div containing the cytoscape element. Makes a // background grid of dots. -export const getCytoscapeDivStyle = (theme: EuiTheme): CSSProperties => ({ +export const getCytoscapeDivStyle = ( + theme: EuiTheme, + status: FETCH_STATUS +): CSSProperties => ({ background: `linear-gradient( 90deg, ${theme.eui.euiPageBackgroundColor} @@ -242,6 +251,7 @@ linear-gradient( center, ${theme.eui.euiColorLightShade}`, backgroundSize: `${theme.eui.euiSizeL} ${theme.eui.euiSizeL}`, + cursor: `${status === FETCH_STATUS.LOADING ? 'wait' : 'grab'}`, margin: `-${theme.eui.gutterTypes.gutterLarge}`, marginTop: 0, }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 1d2e4ada43ad..d167b6a9a056 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -19,7 +19,7 @@ import { callApmApi } from '../../../services/rest/createCallApmApi'; import { LicensePrompt } from '../../shared/LicensePrompt'; import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; -import { getCytoscapeDivStyle } from './cytoscapeOptions'; +import { getCytoscapeDivStyle } from './cytoscape_options'; import { EmptyBanner } from './EmptyBanner'; import { EmptyPrompt } from './empty_prompt'; import { Popover } from './Popover'; @@ -121,7 +121,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { elements={data.elements} height={height} serviceName={serviceName} - style={getCytoscapeDivStyle(theme)} + style={getCytoscapeDivStyle(theme, status)} > {serviceName && } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.test.tsx index 4212d866c085..ab16da141066 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.test.tsx @@ -6,9 +6,12 @@ import { renderHook } from '@testing-library/react-hooks'; import cytoscape from 'cytoscape'; -import { EuiTheme } from '../../../../../observability/public'; -import { useCytoscapeEventHandlers } from './use_cytoscape_event_handlers'; import dagre from 'cytoscape-dagre'; +import { EuiTheme, useUiTracker } from '../../../../../observability/public'; +import { useCytoscapeEventHandlers } from './use_cytoscape_event_handlers'; +import lodash from 'lodash'; + +jest.mock('../../../../../observability/public'); cytoscape.use(dagre); @@ -25,14 +28,109 @@ describe('useCytoscapeEventHandlers', () => { }); }); + describe('when data is received', () => { + describe('with a service name', () => { + it('sets the primary class', () => { + const cy = cytoscape({ + elements: [{ data: { id: 'test' } }], + }); + + // Mock the chain that leads to layout run + jest.spyOn(cy, 'elements').mockReturnValueOnce(({ + difference: () => + (({ + layout: () => + (({ run: () => {} } as unknown) as cytoscape.Layouts), + } as unknown) as cytoscape.CollectionReturnValue), + } as unknown) as cytoscape.CollectionReturnValue); + + renderHook(() => + useCytoscapeEventHandlers({ serviceName: 'test', cy, theme }) + ); + cy.trigger('custom:data'); + + expect(cy.getElementById('test').hasClass('primary')).toEqual(true); + }); + }); + + it('runs the layout', () => { + const cy = cytoscape({ + elements: [{ data: { id: 'test' } }], + }); + const run = jest.fn(); + + // Mock the chain that leads to layout run + jest.spyOn(cy, 'elements').mockReturnValueOnce(({ + difference: () => + (({ + layout: () => (({ run } as unknown) as cytoscape.Layouts), + } as unknown) as cytoscape.CollectionReturnValue), + } as unknown) as cytoscape.CollectionReturnValue); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.trigger('custom:data'); + + expect(run).toHaveBeenCalled(); + }); + }); + + describe('when layoutstop is triggered', () => { + it('applies cubic bézier styles', () => { + const cy = cytoscape({ + elements: [ + { data: { id: 'test', source: 'a', target: 'b' } }, + { data: { id: 'a' } }, + { data: { id: 'b' } }, + ], + }); + const edge = cy.getElementById('test'); + const style = jest.spyOn(edge, 'style'); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.trigger('layoutstop'); + + expect(style).toHaveBeenCalledWith('control-point-distances', [-0, 0]); + }); + }); + describe('when an element is dragged', () => { it('sets the hasBeenDragged data', () => { const cy = cytoscape({ elements: [{ data: { id: 'test' } }] }); + const node = cy.getElementById('test'); renderHook(() => useCytoscapeEventHandlers({ cy, theme })); - cy.getElementById('test').trigger('drag'); + node.trigger('drag'); - expect(cy.getElementById('test').data('hasBeenDragged')).toEqual(true); + expect(node.data('hasBeenDragged')).toEqual(true); + }); + + describe('when it has already been dragged', () => { + it('keeps hasBeenDragged as true', () => { + const cy = cytoscape({ + elements: [{ data: { hasBeenDragged: true, id: 'test' } }], + }); + const node = cy.getElementById('test'); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + node.trigger('drag'); + + expect(node.data('hasBeenDragged')).toEqual(true); + }); + }); + }); + + describe('when a drag ends', () => { + it('changes the cursor to pointer', () => { + const cy = cytoscape({ elements: [{ data: { id: 'test' } }] }); + const container = ({ + style: { cursor: 'grabbing' }, + } as unknown) as HTMLElement; + jest.spyOn(cy, 'container').mockReturnValueOnce(container); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.getElementById('test').trigger('dragfree'); + + expect(container.style.cursor).toEqual('pointer'); }); }); @@ -48,6 +146,36 @@ describe('useCytoscapeEventHandlers', () => { expect(node.hasClass('hover')).toEqual(true); }); + + it('sets the cursor to pointer', () => { + const cy = cytoscape({ elements: [{ data: { id: 'test' } }] }); + const container = ({ + style: { cursor: 'default' }, + } as unknown) as HTMLElement; + jest.spyOn(cy, 'container').mockReturnValueOnce(container); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.getElementById('test').trigger('mouseover'); + + expect(container.style.cursor).toEqual('pointer'); + }); + + it('tracks an event', () => { + const cy = cytoscape({ elements: [{ data: { id: 'test' } }] }); + const trackApmEvent = jest.fn(); + (useUiTracker as jest.Mock).mockReturnValueOnce(trackApmEvent); + jest.spyOn(lodash, 'debounce').mockImplementationOnce((fn: any) => { + fn(); + return fn; + }); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.getElementById('test').trigger('mouseover'); + + expect(trackApmEvent).toHaveBeenCalledWith({ + metric: 'service_map_node_or_edge_hover', + }); + }); }); describe('when a node is un-hovered', () => { @@ -62,5 +190,157 @@ describe('useCytoscapeEventHandlers', () => { expect(node.hasClass('hover')).toEqual(false); }); + + it('sets the cursor to the default', () => { + const cy = cytoscape({ elements: [{ data: { id: 'test' } }] }); + const container = ({ + style: { cursor: 'pointer' }, + } as unknown) as HTMLElement; + jest.spyOn(cy, 'container').mockReturnValueOnce(container); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.getElementById('test').trigger('mouseout'); + + expect(container.style.cursor).toEqual('grab'); + }); + }); + + describe('when an edge is hovered', () => { + it('does not set the cursor to pointer', () => { + const cy = cytoscape({ + elements: [ + { data: { id: 'test', source: 'a', target: 'b' } }, + { data: { id: 'a' } }, + { data: { id: 'b' } }, + ], + }); + const container = ({ + style: { cursor: 'default' }, + } as unknown) as HTMLElement; + jest.spyOn(cy, 'container').mockReturnValueOnce(container); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.getElementById('test').trigger('mouseover'); + + expect(container.style.cursor).toEqual('default'); + }); + }); + + describe('when a node is selected', () => { + it('tracks an event', () => { + const cy = cytoscape({ elements: [{ data: { id: 'test' } }] }); + const trackApmEvent = jest.fn(); + (useUiTracker as jest.Mock).mockReturnValueOnce(trackApmEvent); + jest.spyOn(lodash, 'debounce').mockImplementationOnce((fn: any) => { + fn(); + return fn; + }); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.getElementById('test').trigger('select'); + + expect(trackApmEvent).toHaveBeenCalledWith({ + metric: 'service_map_node_select', + }); + }); + }); + + describe('when a node is unselected', () => { + it('resets connected edge styles', () => { + const cy = cytoscape({ + elements: [ + { data: { id: 'test' } }, + { data: { id: 'edge', source: 'test', target: 'test2' } }, + { data: { id: 'test2' } }, + ], + }); + + renderHook(() => + useCytoscapeEventHandlers({ + serviceName: 'test', + cy, + theme, + }) + ); + cy.getElementById('test').trigger('unselect'); + + expect(cy.getElementById('edge').hasClass('highlight')).toEqual(true); + }); + }); + + describe('when a tap starts', () => { + it('sets the cursor to grabbing', () => { + const cy = cytoscape({}); + const container = ({ + style: { cursor: 'grab' }, + } as unknown) as HTMLElement; + jest.spyOn(cy, 'container').mockReturnValueOnce(container); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.trigger('tapstart'); + + expect(container.style.cursor).toEqual('grabbing'); + }); + + describe('when the target is a node', () => { + it('does not change the cursor', () => { + const cy = cytoscape({ elements: [{ data: { id: 'test' } }] }); + const container = ({ + style: { cursor: 'grab' }, + } as unknown) as HTMLElement; + jest.spyOn(cy, 'container').mockReturnValueOnce(container); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.getElementById('test').trigger('tapstart'); + + expect(container.style.cursor).toEqual('grab'); + }); + }); + }); + + describe('when a tap ends', () => { + it('sets the cursor to the default', () => { + const cy = cytoscape({}); + const container = ({ + style: { cursor: 'grabbing' }, + } as unknown) as HTMLElement; + jest.spyOn(cy, 'container').mockReturnValueOnce(container); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.trigger('tapend'); + + expect(container.style.cursor).toEqual('grab'); + }); + + describe('when the target is a node', () => { + it('does not change the cursor', () => { + const cy = cytoscape({ elements: [{ data: { id: 'test' } }] }); + const container = ({ + style: { cursor: 'pointer' }, + } as unknown) as HTMLElement; + jest.spyOn(cy, 'container').mockReturnValueOnce(container); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.getElementById('test').trigger('tapend'); + + expect(container.style.cursor).toEqual('pointer'); + }); + }); + }); + + describe('when debug is enabled', () => { + it('logs a debug message', () => { + const cy = cytoscape({ elements: [{ data: { id: 'test' } }] }); + (useUiTracker as jest.Mock).mockReturnValueOnce(() => {}); + jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce('true'); + const debug = jest + .spyOn(window.console, 'debug') + .mockReturnValueOnce(undefined); + + renderHook(() => useCytoscapeEventHandlers({ cy, theme })); + cy.getElementById('test').trigger('select'); + + expect(debug).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts index e8c6a3165ce9..a9125a13fc6f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/use_cytoscape_event_handlers.ts @@ -8,7 +8,7 @@ import cytoscape from 'cytoscape'; import { debounce } from 'lodash'; import { useEffect } from 'react'; import { EuiTheme, useUiTracker } from '../../../../../observability/public'; -import { getAnimationOptions, getNodeHeight } from './cytoscapeOptions'; +import { getAnimationOptions, getNodeHeight } from './cytoscape_options'; /* * @notice @@ -66,6 +66,24 @@ function getLayoutOptions({ }; } +function setCursor(cursor: string, event: cytoscape.EventObjectCore) { + const container = event.cy.container(); + + if (container) { + container.style.cursor = cursor; + } +} + +function resetConnectedEdgeStyle( + cytoscapeInstance: cytoscape.Core, + node?: cytoscape.NodeSingular +) { + cytoscapeInstance.edges().removeClass('highlight'); + if (node) { + node.connectedEdges().addClass('highlight'); + } +} + export function useCytoscapeEventHandlers({ cy, serviceName, @@ -80,16 +98,6 @@ export function useCytoscapeEventHandlers({ useEffect(() => { const nodeHeight = getNodeHeight(theme); - const resetConnectedEdgeStyle = ( - cytoscapeInstance: cytoscape.Core, - node?: cytoscape.NodeSingular - ) => { - cytoscapeInstance.edges().removeClass('highlight'); - if (node) { - node.connectedEdges().addClass('highlight'); - } - }; - const dataHandler: cytoscape.EventHandler = (event, fit) => { if (serviceName) { const node = event.cy.getElementById(serviceName); @@ -123,11 +131,17 @@ export function useCytoscapeEventHandlers({ ); const mouseoverHandler: cytoscape.EventHandler = (event) => { + if (event.target.isNode()) { + setCursor('pointer', event); + } + trackNodeEdgeHover(); event.target.addClass('hover'); event.target.connectedEdges().addClass('nodeHover'); }; const mouseoutHandler: cytoscape.EventHandler = (event) => { + setCursor('grab', event); + event.target.removeClass('hover'); event.target.connectedEdges().removeClass('nodeHover'); }; @@ -148,17 +162,37 @@ export function useCytoscapeEventHandlers({ console.debug('cytoscape:', event); } }; - const dragHandler: cytoscape.EventHandler = (event) => { + setCursor('grabbing', event); + applyCubicBezierStyles(event.target.connectedEdges()); if (!event.target.data('hasBeenDragged')) { event.target.data('hasBeenDragged', true); } }; + const dragfreeHandler: cytoscape.EventHandler = (event) => { + setCursor('pointer', event); + }; + const tapstartHandler: cytoscape.EventHandler = (event) => { + // Onle set cursot to "grabbing" if the target doesn't have an "isNode" + // property (meaning it's the canvas) or if "isNode" is false (meaning + // it's an edge.) + if (!event.target.isNode || !event.target.isNode()) { + setCursor('grabbing', event); + } + }; + const tapendHandler: cytoscape.EventHandler = (event) => { + if (!event.target.isNode || !event.target.isNode()) { + setCursor('grab', event); + } + }; if (cy) { - cy.on('custom:data drag layoutstop select unselect', debugHandler); + cy.on( + 'custom:data drag dragfree layoutstop select tapstart tapend unselect', + debugHandler + ); cy.on('custom:data', dataHandler); cy.on('layoutstop', layoutstopHandler); cy.on('mouseover', 'edge, node', mouseoverHandler); @@ -166,12 +200,15 @@ export function useCytoscapeEventHandlers({ cy.on('select', 'node', selectHandler); cy.on('unselect', 'node', unselectHandler); cy.on('drag', 'node', dragHandler); + cy.on('dragfree', 'node', dragfreeHandler); + cy.on('tapstart', tapstartHandler); + cy.on('tapend', tapendHandler); } return () => { if (cy) { cy.removeListener( - 'custom:data drag layoutstop select unselect', + 'custom:data drag dragfree layoutstop select tapstart tapend unselect', undefined, debugHandler ); @@ -182,6 +219,9 @@ export function useCytoscapeEventHandlers({ cy.removeListener('select', 'node', selectHandler); cy.removeListener('unselect', 'node', unselectHandler); cy.removeListener('drag', 'node', dragHandler); + cy.removeListener('dragfree', 'node', dragfreeHandler); + cy.removeListener('tapstart', undefined, tapstartHandler); + cy.removeListener('tapend', undefined, tapendHandler); } }; }, [cy, serviceName, trackApmEvent, theme]); 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..3d1572689c5b 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 @@ -53,6 +53,17 @@ const AppLink = styled(TransactionOverviewLink)` ${truncate('100%')}; `; +const ToolTipWrapper = styled.span` + width: 100%; + .apmServiceList__serviceNameTooltip { + width: 100%; + .apmServiceList__serviceNameContainer { + // removes 24px referent to the icon placed on the left side of the text. + width: calc(100% - 24px); + } + } +`; + export const SERVICE_COLUMNS: Array> = [ { field: 'healthStatus', @@ -77,24 +88,27 @@ export const SERVICE_COLUMNS: Array> = [ width: '40%', sortable: true, render: (_, { serviceName, agentName }) => ( - - - {agentName && ( - - + + + + {agentName && ( + + + + )} + + + {formatString(serviceName)} + - )} - - - {formatString(serviceName)} - - - - + + + ), }, { @@ -153,7 +167,7 @@ export const SERVICE_COLUMNS: Array> = [ width: px(unit * 10), }, { - field: 'errorsPerMinute', + field: 'transactionErrorRate', name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { defaultMessage: 'Error rate %', }), @@ -191,18 +205,20 @@ export function ServiceList({ items, noItemsMessage }: Props) { const columns = displayHealthStatus ? SERVICE_COLUMNS : SERVICE_COLUMNS.filter((column) => column.field !== 'healthStatus'); + const initialSortField = displayHealthStatus + ? 'healthStatus' + : 'transactionsPerMinute'; return ( { // For healthStatus, sort items by healthStatus first, then by TPM - return sortField === 'healthStatus' ? orderBy( itemsToSort, @@ -220,13 +236,15 @@ export function ServiceList({ items, noItemsMessage }: Props) { itemsToSort, (item) => { switch (sortField) { + // Use `?? -1` here so `undefined` will appear after/before `0`. + // In the table this will make the "N/A" items always at the + // bottom/top. case 'avgResponseTime': - return item.avgResponseTime?.value ?? 0; + return item.avgResponseTime?.value ?? -1; case 'transactionsPerMinute': - return item.transactionsPerMinute?.value ?? 0; + return item.transactionsPerMinute?.value ?? -1; case 'transactionErrorRate': - return item.transactionErrorRate?.value ?? 0; - + return item.transactionErrorRate?.value ?? -1; default: return item[sortField as keyof typeof item]; } diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/service_list.test.tsx new file mode 100644 index 000000000000..73777c2221a5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/service_list.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { ServiceHealthStatus } from '../../../../../common/service_health_status'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; +import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers'; +import { ServiceList, SERVICE_COLUMNS } from './'; +import props from './__fixtures__/props.json'; + +function Wrapper({ children }: { children?: ReactNode }) { + return ( + + {children} + + ); +} + +describe('ServiceList', () => { + beforeAll(() => { + mockMoment(); + }); + + it('renders empty state', () => { + expect(() => + renderWithTheme(, { wrapper: Wrapper }) + ).not.toThrowError(); + }); + + it('renders with data', () => { + expect(() => + renderWithTheme( + , + { wrapper: Wrapper } + ) + ).not.toThrowError(); + }); + + it('renders columns correctly', () => { + const service: any = { + serviceName: 'opbeans-python', + agentName: 'python', + transactionsPerMinute: { + value: 86.93333333333334, + timeseries: [], + }, + errorsPerMinute: { + value: 12.6, + timeseries: [], + }, + avgResponseTime: { + value: 91535.42944785276, + timeseries: [], + }, + environments: ['test'], + }; + const renderedColumns = SERVICE_COLUMNS.map((c) => + c.render!(service[c.field!], service) + ); + + expect(renderedColumns[0]).toMatchInlineSnapshot(` + + `); + }); + + describe('without ML data', () => { + it('does not render the health column', () => { + const { queryByText } = renderWithTheme( + , + { + wrapper: Wrapper, + } + ); + const healthHeading = queryByText('Health'); + + expect(healthHeading).toBeNull(); + }); + + it('sorts by transactions per minute', async () => { + const { findByTitle } = renderWithTheme( + , + { + wrapper: Wrapper, + } + ); + + expect( + await findByTitle('Trans. per minute; Sorted in descending order') + ).toBeInTheDocument(); + }); + }); + + describe('with ML data', () => { + it('renders the health column', async () => { + const { findByTitle } = renderWithTheme( + ({ + ...item, + healthStatus: ServiceHealthStatus.warning, + }) + )} + />, + { wrapper: Wrapper } + ); + + expect( + await findByTitle('Health; Sorted in descending order') + ).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/service_overview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/service_overview.test.tsx.snap index ee3a4fce0dba..611ee4e07134 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/service_overview.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/service_overview.test.tsx.snap @@ -128,7 +128,7 @@ NodeList [ exports[`Service Overview -> View should render services, when list is not empty 1`] = ` NodeList [ - .c0 { + .c1 { font-size: 16px; max-width: 100%; white-space: nowrap; @@ -136,6 +136,18 @@ NodeList [ text-overflow: ellipsis; } +.c0 { + width: 100%; +} + +.c0 .apmServiceList__serviceNameTooltip { + width: 100%; +} + +.c0 .apmServiceList__serviceNameTooltip .apmServiceList__serviceNameContainer { + width: calc(100% - 24px); +} + @@ -181,32 +193,36 @@ NodeList [ class="euiTableCellContent euiTableCellContent--overflowingContent" > - -
+
@@ -401,7 +417,7 @@ NodeList [
, - .c0 { + .c1 { font-size: 16px; max-width: 100%; white-space: nowrap; @@ -409,6 +425,18 @@ NodeList [ text-overflow: ellipsis; } +.c0 { + width: 100%; +} + +.c0 .apmServiceList__serviceNameTooltip { + width: 100%; +} + +.c0 .apmServiceList__serviceNameTooltip .apmServiceList__serviceNameContainer { + width: calc(100% - 24px); +} + @@ -454,32 +482,36 @@ NodeList [ class="euiTableCellContent euiTableCellContent--overflowingContent" > - -
+
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/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 75d8842d4843..d597b65040ce 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -55,7 +55,7 @@ export const APM_FEATURE = { read: [], }, alerting: { - all: Object.values(AlertType), + read: Object.values(AlertType), }, management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 4cb0c4c750dd..5ea3714e81b6 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -58,14 +58,27 @@ export async function getServicesItems({ }), ]); - const allMetrics = [ - ...transactionDurationAverages, - ...agentNames, - ...transactionRates, - ...transactionErrorRates, - ...environments, - ...healthStatuses, - ]; + const apmServiceMetrics = joinByKey( + [ + ...transactionDurationAverages, + ...agentNames, + ...transactionRates, + ...transactionErrorRates, + ...environments, + ], + 'serviceName' + ); + + const apmServices = apmServiceMetrics.map(({ serviceName }) => serviceName); + + // make sure to exclude health statuses from services + // that are not found in APM data + + const matchedHealthStatuses = healthStatuses.filter(({ serviceName }) => + apmServices.includes(serviceName) + ); + + const allMetrics = [...apmServiceMetrics, ...matchedHealthStatuses]; return joinByKey(allMetrics, 'serviceName'); } diff --git a/x-pack/plugins/audit_trail/kibana.json b/x-pack/plugins/audit_trail/kibana.json deleted file mode 100644 index ce92e232ec13..000000000000 --- a/x-pack/plugins/audit_trail/kibana.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "auditTrail", - "version": "8.0.0", - "kibanaVersion": "kibana", - "configPath": ["xpack", "audit_trail"], - "server": true, - "ui": false, - "requiredPlugins": ["licensing", "security"], - "optionalPlugins": ["spaces"] -} diff --git a/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts b/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts deleted file mode 100644 index 76ca3e56fe83..000000000000 --- a/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts +++ /dev/null @@ -1,65 +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 { Subject } from 'rxjs'; - -import { AuditTrailClient } from './audit_trail_client'; -import { AuditEvent } from '../types'; - -import { httpServerMock } from '../../../../../src/core/server/mocks'; -import { securityMock } from '../../../security/server/mocks'; -import { spacesMock } from '../../../spaces/server/mocks'; - -describe('AuditTrailClient', () => { - let client: AuditTrailClient; - let event$: Subject; - const deps = { - getCurrentUser: securityMock.createSetup().authc.getCurrentUser, - getSpaceId: spacesMock.createSetup().spacesService.getSpaceId, - }; - - beforeEach(() => { - event$ = new Subject(); - client = new AuditTrailClient( - httpServerMock.createKibanaRequest({ - kibanaRequestState: { requestId: 'request id alpha', requestUuid: 'ignore-me' }, - }), - event$, - deps - ); - }); - - afterEach(() => { - event$.complete(); - }); - - describe('#withAuditScope', () => { - it('registers upper level scope', (done) => { - client.withAuditScope('scope_name'); - event$.subscribe((event) => { - expect(event.scope).toBe('scope_name'); - done(); - }); - client.add({ message: 'message', type: 'type' }); - }); - - it('populates requestId', (done) => { - client.withAuditScope('scope_name'); - event$.subscribe((event) => { - expect(event.requestId).toBe('request id alpha'); - done(); - }); - client.add({ message: 'message', type: 'type' }); - }); - - it('throws an exception if tries to re-write a scope', () => { - client.withAuditScope('scope_name'); - expect(() => client.withAuditScope('another_scope_name')).toThrowErrorMatchingInlineSnapshot( - `"Audit scope is already set to: scope_name"` - ); - }); - }); -}); diff --git a/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts b/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts deleted file mode 100644 index e5022234af9d..000000000000 --- a/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts +++ /dev/null @@ -1,47 +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 { Subject } from 'rxjs'; -import { KibanaRequest, Auditor, AuditableEvent } from 'src/core/server'; -import { AuditEvent } from '../types'; - -import { SecurityPluginSetup } from '../../../security/server'; -import { SpacesPluginSetup } from '../../../spaces/server'; - -interface Deps { - getCurrentUser: SecurityPluginSetup['authc']['getCurrentUser']; - getSpaceId?: SpacesPluginSetup['spacesService']['getSpaceId']; -} - -export class AuditTrailClient implements Auditor { - private scope?: string; - constructor( - private readonly request: KibanaRequest, - private readonly event$: Subject, - private readonly deps: Deps - ) {} - - public withAuditScope(name: string) { - if (this.scope !== undefined) { - throw new Error(`Audit scope is already set to: ${this.scope}`); - } - this.scope = name; - } - - public add(event: AuditableEvent) { - const user = this.deps.getCurrentUser(this.request); - // doesn't use getSpace since it's async operation calling ES - const spaceId = this.deps.getSpaceId ? this.deps.getSpaceId(this.request) : undefined; - - this.event$.next({ - message: event.message, - type: event.type, - user: user?.username, - space: spaceId, - scope: this.scope, - requestId: this.request.id, - }); - } -} diff --git a/x-pack/plugins/audit_trail/server/config.test.ts b/x-pack/plugins/audit_trail/server/config.test.ts deleted file mode 100644 index 65dfc9f589ec..000000000000 --- a/x-pack/plugins/audit_trail/server/config.test.ts +++ /dev/null @@ -1,56 +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 { config } from './config'; - -describe('config schema', () => { - it('generates proper defaults', () => { - expect(config.schema.validate({})).toEqual({ - enabled: false, - logger: { - enabled: false, - }, - }); - }); - - it('accepts an appender', () => { - const appender = config.schema.validate({ - appender: { - kind: 'file', - path: '/path/to/file.txt', - layout: { - kind: 'json', - }, - }, - logger: { - enabled: false, - }, - }).appender; - - expect(appender).toEqual({ - kind: 'file', - path: '/path/to/file.txt', - layout: { - kind: 'json', - }, - }); - }); - - it('rejects an appender if not fully configured', () => { - expect(() => - config.schema.validate({ - // no layout configured - appender: { - kind: 'file', - path: '/path/to/file.txt', - }, - logger: { - enabled: false, - }, - }) - ).toThrow(); - }); -}); diff --git a/x-pack/plugins/audit_trail/server/config.ts b/x-pack/plugins/audit_trail/server/config.ts deleted file mode 100644 index 7b05c04c2236..000000000000 --- a/x-pack/plugins/audit_trail/server/config.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginConfigDescriptor, config as coreConfig } from '../../../../src/core/server'; - -const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: false }), - appender: schema.maybe(coreConfig.logging.appenders), - logger: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - }), -}); - -export type AuditTrailConfigType = TypeOf; - -export const config: PluginConfigDescriptor = { - schema: configSchema, -}; diff --git a/x-pack/plugins/audit_trail/server/index.ts b/x-pack/plugins/audit_trail/server/index.ts deleted file mode 100644 index 7db48823a0e2..000000000000 --- a/x-pack/plugins/audit_trail/server/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PluginInitializerContext } from 'src/core/server'; -import { AuditTrailPlugin } from './plugin'; - -export { config } from './config'; -export const plugin = (initializerContext: PluginInitializerContext) => { - return new AuditTrailPlugin(initializerContext); -}; diff --git a/x-pack/plugins/audit_trail/server/plugin.test.ts b/x-pack/plugins/audit_trail/server/plugin.test.ts deleted file mode 100644 index fa5fd1bcc1e1..000000000000 --- a/x-pack/plugins/audit_trail/server/plugin.test.ts +++ /dev/null @@ -1,125 +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 { first } from 'rxjs/operators'; -import { AuditTrailPlugin } from './plugin'; -import { coreMock } from '../../../../src/core/server/mocks'; - -import { securityMock } from '../../security/server/mocks'; -import { spacesMock } from '../../spaces/server/mocks'; - -describe('AuditTrail plugin', () => { - describe('#setup', () => { - let plugin: AuditTrailPlugin; - let pluginInitContextMock: ReturnType; - let coreSetup: ReturnType; - - const deps = { - security: securityMock.createSetup(), - spaces: spacesMock.createSetup(), - }; - - beforeEach(() => { - pluginInitContextMock = coreMock.createPluginInitializerContext(); - plugin = new AuditTrailPlugin(pluginInitContextMock); - coreSetup = coreMock.createSetup(); - }); - - afterEach(async () => { - await plugin.stop(); - }); - - it('registers AuditTrail factory', async () => { - pluginInitContextMock = coreMock.createPluginInitializerContext(); - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - expect(coreSetup.auditTrail.register).toHaveBeenCalledTimes(1); - }); - - describe('logger', () => { - it('registers a custom logger', async () => { - pluginInitContextMock = coreMock.createPluginInitializerContext(); - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - - expect(coreSetup.logging.configure).toHaveBeenCalledTimes(1); - }); - - it('disables logging if config.logger.enabled: false', async () => { - const config = { - logger: { - enabled: false, - }, - }; - pluginInitContextMock = coreMock.createPluginInitializerContext(config); - - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - - const args = coreSetup.logging.configure.mock.calls[0][0]; - const value = await args.pipe(first()).toPromise(); - expect(value.loggers?.every((l) => l.level === 'off')).toBe(true); - }); - it('logs with DEBUG level if config.logger.enabled: true', async () => { - const config = { - logger: { - enabled: true, - }, - }; - pluginInitContextMock = coreMock.createPluginInitializerContext(config); - - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - - const args = coreSetup.logging.configure.mock.calls[0][0]; - const value = await args.pipe(first()).toPromise(); - expect(value.loggers?.every((l) => l.level === 'debug')).toBe(true); - }); - it('uses appender adjusted via config', async () => { - const config = { - appender: { - kind: 'file', - path: '/path/to/file.txt', - }, - logger: { - enabled: true, - }, - }; - pluginInitContextMock = coreMock.createPluginInitializerContext(config); - - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - - const args = coreSetup.logging.configure.mock.calls[0][0]; - const value = await args.pipe(first()).toPromise(); - expect(value.appenders).toEqual({ auditTrailAppender: config.appender }); - }); - it('falls back to the default appender if not configured', async () => { - const config = { - logger: { - enabled: true, - }, - }; - pluginInitContextMock = coreMock.createPluginInitializerContext(config); - - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - - const args = coreSetup.logging.configure.mock.calls[0][0]; - const value = await args.pipe(first()).toPromise(); - expect(value.appenders).toEqual({ - auditTrailAppender: { - kind: 'console', - layout: { - kind: 'pattern', - highlight: true, - }, - }, - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/audit_trail/server/plugin.ts b/x-pack/plugins/audit_trail/server/plugin.ts deleted file mode 100644 index cf423f230aef..000000000000 --- a/x-pack/plugins/audit_trail/server/plugin.ts +++ /dev/null @@ -1,97 +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 { Observable, Subject } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { - AppenderConfigType, - CoreSetup, - CoreStart, - KibanaRequest, - Logger, - LoggerContextConfigInput, - Plugin, - PluginInitializerContext, -} from 'src/core/server'; - -import { AuditEvent } from './types'; -import { AuditTrailClient } from './client/audit_trail_client'; -import { AuditTrailConfigType } from './config'; - -import { SecurityPluginSetup } from '../../security/server'; -import { SpacesPluginSetup } from '../../spaces/server'; -import { LicensingPluginStart } from '../../licensing/server'; - -interface DepsSetup { - security: SecurityPluginSetup; - spaces?: SpacesPluginSetup; -} - -interface DepStart { - licensing: LicensingPluginStart; -} - -export class AuditTrailPlugin implements Plugin { - private readonly logger: Logger; - private readonly config$: Observable; - private readonly event$ = new Subject(); - - constructor(private readonly context: PluginInitializerContext) { - this.logger = this.context.logger.get(); - this.config$ = this.context.config.create(); - } - - public setup(core: CoreSetup, deps: DepsSetup) { - const depsApi = { - getCurrentUser: deps.security.authc.getCurrentUser, - getSpaceId: deps.spaces?.spacesService.getSpaceId, - }; - - this.event$.subscribe(({ message, ...other }) => this.logger.debug(message, other)); - - core.auditTrail.register({ - asScoped: (request: KibanaRequest) => { - return new AuditTrailClient(request, this.event$, depsApi); - }, - }); - - core.logging.configure( - this.config$.pipe( - map((config) => ({ - appenders: { - auditTrailAppender: this.getAppender(config), - }, - loggers: [ - { - // plugins.auditTrail prepended automatically - context: '', - // do not pipe in root log if disabled - level: config.logger.enabled ? 'debug' : 'off', - appenders: ['auditTrailAppender'], - }, - ], - })) - ) - ); - } - - private getAppender(config: AuditTrailConfigType): AppenderConfigType { - return ( - config.appender ?? { - kind: 'console', - layout: { - kind: 'pattern', - highlight: true, - }, - } - ); - } - - public start(core: CoreStart, deps: DepStart) {} - public stop() { - this.event$.complete(); - } -} 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/public/components/datatable/datatable.tsx b/x-pack/plugins/canvas/public/components/datatable/datatable.tsx index bd343b15758b..cd8a0e91510a 100644 --- a/x-pack/plugins/canvas/public/components/datatable/datatable.tsx +++ b/x-pack/plugins/canvas/public/components/datatable/datatable.tsx @@ -9,11 +9,9 @@ import PropTypes from 'prop-types'; import { EuiIcon, EuiPagination } from '@elastic/eui'; import moment from 'moment'; import { Paginate } from '../paginate'; -import { Datatable as DatatableType, DatatableColumn } from '../../../types'; +import { Datatable as DatatableType, DatatableColumn, DatatableColumnType } from '../../../types'; -type IconType = 'string' | 'number' | 'date' | 'boolean' | 'null'; - -const getIcon = (type: IconType) => { +const getIcon = (type: DatatableColumnType | null) => { if (type === null) { return; } diff --git a/x-pack/plugins/canvas/scripts/storybook.js b/x-pack/plugins/canvas/scripts/storybook.js index 23703810569b..3b06b7e7e211 100644 --- a/x-pack/plugins/canvas/scripts/storybook.js +++ b/x-pack/plugins/canvas/scripts/storybook.js @@ -8,6 +8,8 @@ const path = require('path'); const fs = require('fs'); const del = require('del'); const { run } = require('@kbn/dev-utils'); +// This is included in the main Kibana package.json +// eslint-disable-next-line import/no-extraneous-dependencies const storybook = require('@storybook/react/standalone'); const execa = require('execa'); const { DLL_OUTPUT } = require('./../storybook/constants'); diff --git a/x-pack/plugins/canvas/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/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index 1e993f9c5461..420923972ed6 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -111,6 +111,7 @@ addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots initStoryshots({ configPath: path.resolve(__dirname, './../storybook'), + framework: 'react', test: multiSnapshotWithOptions({}), // Don't snapshot tests that start with 'redux' storyNameRegex: /^((?!.*?redux).)*$/, diff --git a/x-pack/plugins/canvas/types/state.ts b/x-pack/plugins/canvas/types/state.ts index e9b580f81e66..60407b78ab5e 100644 --- a/x-pack/plugins/canvas/types/state.ts +++ b/x-pack/plugins/canvas/types/state.ts @@ -10,7 +10,6 @@ import { ExpressionImage, ExpressionFunction, KibanaContext, - KibanaDatatable, PointSeries, Render, Style, @@ -49,7 +48,6 @@ type ExpressionType = | ExpressionValueFilter | ExpressionImage | KibanaContext - | KibanaDatatable | PointSeries | Style | Range; 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/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts index bd276bc91ca3..ce35b9975041 100644 --- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -62,6 +62,45 @@ export const getActions = (): FindActionResult[] => [ isPreconfigured: false, referencedByCount: 0, }, + { + id: '456', + actionTypeId: '.jira', + name: 'Connector without isCaseOwned', + config: { + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + apiUrl: 'https://elastic.jira.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: '789', + actionTypeId: '.resilient', + name: 'Connector without mapping', + config: { + apiUrl: 'https://elastic.resilient.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, ]; export const newConfiguration: CasesConfigureRequest = { diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts index d7a01ef06986..ee4dcc8e81b9 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts @@ -15,7 +15,6 @@ import { import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initCaseConfigureGetActionConnector } from './get_connectors'; -import { getActions } from '../../__mocks__/request_responses'; import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; describe('GET connectors', () => { @@ -24,7 +23,7 @@ describe('GET connectors', () => { routeHandler = await createRoute(initCaseConfigureGetActionConnector, 'get'); }); - it('returns the connectors', async () => { + it('returns case owned connectors', async () => { const req = httpServerMock.createKibanaRequest({ path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, method: 'get', @@ -38,9 +37,67 @@ describe('GET connectors', () => { const res = await routeHandler(context, req, kibanaResponseFactory); expect(res.status).toEqual(200); - expect(res.payload).toEqual( - getActions().filter((action) => action.actionTypeId === '.servicenow') - ); + expect(res.payload).toEqual([ + { + id: '123', + actionTypeId: '.servicenow', + name: 'ServiceNow', + config: { + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + apiUrl: 'https://dev102283.service-now.com', + isCaseOwned: true, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: '456', + actionTypeId: '.jira', + name: 'Connector without isCaseOwned', + config: { + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + apiUrl: 'https://elastic.jira.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, + ]); }); it('it throws an error when actions client is null', async () => { diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 545ccf82c3d7..c3e565a404e9 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -7,15 +7,44 @@ import Boom from 'boom'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FindActionResult } from '../../../../../../actions/server/types'; import { CASE_CONFIGURE_CONNECTORS_URL, - SUPPORTED_CONNECTORS, SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID, } from '../../../../../common/constants'; +/** + * We need to take into account connectors that have been created within cases and + * they do not have the isCaseOwned field. Checking for the existence of + * the mapping attribute ensures that the connector is indeed a case connector. + * Cases connector should always have a mapping. + */ + +interface CaseAction extends FindActionResult { + config?: { + isCaseOwned?: boolean; + incidentConfiguration?: Record; + }; +} + +const isCaseOwned = (action: CaseAction): boolean => { + if ( + [SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( + action.actionTypeId + ) + ) { + if (action.config?.isCaseOwned === true || action.config?.incidentConfiguration?.mapping) { + return true; + } + } + + return false; +}; + /* * Be aware that this api will only return 20 connectors */ @@ -34,18 +63,7 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou throw Boom.notFound('Action client have not been found'); } - const results = (await actionsClient.getAll()).filter( - (action) => - SUPPORTED_CONNECTORS.includes(action.actionTypeId) && - // Need this filtering temporary to display only Case owned ServiceNow connectors - (![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( - action.actionTypeId - ) || - ([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( - action.actionTypeId - ) && - action.config?.isCaseOwned === true)) - ); + const results = (await actionsClient.getAll()).filter(isCaseOwned); return response.ok({ body: results }); } catch (error) { return response.customError(wrapError(error)); 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/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index ccc93316482c..43ad4a9ed9b8 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -40,6 +40,7 @@ export class DataEnhancedPlugin uiSettings: core.uiSettings, startServices: core.getStartServices(), usageCollector: data.search.usageCollector, + session: data.search.session, }); data.__enhance({ diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index 6e34e4c1964c..3187b41a2c55 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -9,6 +9,7 @@ import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; import { AbortError, UI_SETTINGS } from '../../../../../src/plugins/data/common'; import { SearchTimeoutError } from 'src/plugins/data/public'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; const timeTravel = (msToRun = 0) => { jest.advanceTimersByTime(msToRun); @@ -43,6 +44,7 @@ describe('EnhancedSearchInterceptor', () => { beforeEach(() => { mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); + const dataPluginMockStart = dataPluginMock.createStartContract(); mockCoreSetup.uiSettings.get.mockImplementation((name: string) => { switch (name) { @@ -77,6 +79,7 @@ describe('EnhancedSearchInterceptor', () => { http: mockCoreSetup.http, uiSettings: mockCoreSetup.uiSettings, usageCollector: mockUsageCollector, + session: dataPluginMockStart.search.session, }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index cca87c85e326..aee32a7c6275 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -98,7 +98,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { if (id !== undefined) { this.deps.http.delete(`/internal/search/${strategy}/${id}`); } - return throwError(this.handleSearchError(e, request, timeoutSignal, options?.abortSignal)); + return throwError(this.handleSearchError(e, request, timeoutSignal, options)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); diff --git a/x-pack/plugins/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/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts index 64af67aefa4b..79d380991f5f 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts @@ -6,6 +6,7 @@ import { UrlDrilldown, ActionContext, Config } from './url_drilldown'; import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public/lib/embeddables'; +import { DatatableColumnType } from '../../../../../../src/plugins/expressions/common'; const mockDataPoints = [ { @@ -15,12 +16,17 @@ const mockDataPoints = [ name: 'test', id: '1-1', meta: { - type: 'histogram', - indexPatternId: 'logstash-*', - aggConfigParams: { - field: 'bytes', - interval: 30, - otherBucket: true, + type: 'number' as DatatableColumnType, + field: 'bytes', + index: 'logstash-*', + sourceParams: { + indexPatternId: 'logstash-*', + type: 'histogram', + params: { + field: 'bytes', + interval: 30, + otherBucket: true, + }, }, }, }, diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts index bb1baf5b9642..6989819da2b0 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts @@ -9,6 +9,7 @@ import { getMockEventScope, ValueClickTriggerEventScope, } from './url_drilldown_scope'; +import { DatatableColumnType } from '../../../../../../src/plugins/expressions/common'; const createPoint = ({ field, @@ -23,10 +24,12 @@ const createPoint = ({ name: field, id: '1-1', meta: { - type: 'histogram', - indexPatternId: 'logstash-*', - aggConfigParams: { - field, + type: 'date' as DatatableColumnType, + field, + source: 'esaggs', + sourceParams: { + type: 'histogram', + indexPatternId: 'logstash-*', interval: 30, otherBucket: true, }, diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts index 15a9a3ba77d8..0f66cb144c96 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts @@ -131,7 +131,7 @@ function getEventScopeFromRangeSelectTriggerContext( const { table, column: columnIndex, range } = eventScopeInput.data; const column = table.columns[columnIndex]; return cleanEmptyKeys({ - key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string, + key: toPrimitiveOrUndefined(column?.meta.field) as string, from: toPrimitiveOrUndefined(range[0]) as string | number | undefined, to: toPrimitiveOrUndefined(range[range.length - 1]) as string | number | undefined, }); @@ -145,7 +145,7 @@ function getEventScopeFromValueClickTriggerContext( const column = table.columns[columnIndex]; return { value: toPrimitiveOrUndefined(value) as Primitive, - key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string | undefined, + key: column?.meta?.field, }; }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts index de14a79dd0dd..4f3e7e9f2b5a 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditLogger, AuthenticatedUser } from '../../../security/server'; +import { LegacyAuditLogger, AuthenticatedUser } from '../../../security/server'; import { SavedObjectDescriptor, descriptorToArray } from '../crypto'; /** * Represents all audit events the plugin can log. */ export class EncryptedSavedObjectsAuditLogger { - constructor(private readonly logger: AuditLogger = { log() {} }) {} + constructor(private readonly logger: LegacyAuditLogger = { log() {} }) {} public encryptAttributeFailure( attributeName: string, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/groups.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/groups.mock.ts new file mode 100644 index 000000000000..c4b129d870ea --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/groups.mock.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { contentSources } from './content_sources.mock'; +import { users } from './users.mock'; + +export const groups = [ + { + id: '123', + name: 'group', + createdAt: '2020-10-06', + updatedAt: '2020-10-06', + users, + usersCount: users.length, + color: 'motherofpearl', + contentSources, + canEditGroup: true, + canDeleteGroup: true, + }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 6aa4cf59ab46..25544b4a9bb6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -76,6 +76,7 @@ describe('WorkplaceSearchConfigured', () => { shallow(); expect(initializeAppData).not.toHaveBeenCalled(); + expect(mockKibanaValues.renderHeaderActions).not.toHaveBeenCalled(); }); it('renders ErrorState', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index a3c7f7d48a61..e22b9c6282f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -38,11 +38,10 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { useEffect(() => { if (!hasInitialized) { initializeAppData(props); + renderHeaderActions(WorkplaceSearchHeaderActions); } }, [hasInitialized]); - renderHeaderActions(WorkplaceSearchHeaderActions); - return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx new file mode 100644 index 000000000000..59216126a237 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockValues, setMockActions } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AddGroupModal } from './add_group_modal'; + +import { EuiModal, EuiOverlayMask } from '@elastic/eui'; + +describe('AddGroupModal', () => { + const closeNewGroupModal = jest.fn(); + const saveNewGroup = jest.fn(); + const setNewGroupName = jest.fn(); + + beforeEach(() => { + setMockValues({ + newGroupNameErrors: [], + newGroupName: 'foo', + }); + + setMockActions({ + closeNewGroupModal, + saveNewGroup, + setNewGroupName, + }); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiModal)).toHaveLength(1); + expect(wrapper.find(EuiOverlayMask)).toHaveLength(1); + }); + + it('updates the input value', () => { + const wrapper = shallow(); + + const input = wrapper.find('[data-test-subj="AddGroupInput"]'); + input.simulate('change', { target: { value: 'bar' } }); + + expect(setNewGroupName).toHaveBeenCalledWith('bar'); + }); + + it('submits the form', () => { + const wrapper = shallow(); + + const simulatedEvent = { + form: 0, + target: { getAttribute: () => '_self' }, + preventDefault: jest.fn(), + }; + + const form = wrapper.find('form'); + form.simulate('submit', simulatedEvent); + expect(simulatedEvent.preventDefault).toHaveBeenCalled(); + expect(saveNewGroup).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx new file mode 100644 index 000000000000..6a781f52c9e9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ClearFiltersLink } from './clear_filters_link'; + +import { EuiLink } from '@elastic/eui'; + +describe('ClearFiltersLink', () => { + const resetGroupsFilters = jest.fn(); + + beforeEach(() => { + setMockActions({ + resetGroupsFilters, + }); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiLink)).toHaveLength(1); + }); + + it('handles click', () => { + const wrapper = shallow(); + + const button = wrapper.find(EuiLink); + button.simulate('click'); + + expect(resetGroupsFilters).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx new file mode 100644 index 000000000000..f23a0c8d1404 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx @@ -0,0 +1,121 @@ +/* + * 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 '../../../../__mocks__/kea.mock'; + +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiFieldSearch, EuiFilterSelectItem, EuiCard, EuiPopoverTitle } from '@elastic/eui'; + +import { FilterableUsersList } from './filterable_users_list'; + +import { IUser } from '../../../types'; + +const mockSetState = jest.fn(); +const useStateMock: any = (initState: any) => [initState, mockSetState]; + +const addFilteredUser = jest.fn(); +const removeFilteredUser = jest.fn(); + +const props = { + users, + addFilteredUser, + removeFilteredUser, +}; + +describe('FilterableUsersList', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldSearch)).toHaveLength(1); + expect(wrapper.find(EuiCard)).toHaveLength(0); + }); + + it('updates the input value and renders zero users card', () => { + jest.spyOn(React, 'useState').mockImplementation(useStateMock); + const _users = [ + users[0], + { + ...users[0], + id: 'asdfa', + email: 'user@example.co', + name: null, + }, + ]; + + const wrapper = shallow(); + + const input = wrapper.find(EuiFieldSearch); + input.simulate('change', { target: { value: 'bar' } }); + + expect(wrapper.find(EuiCard)).toHaveLength(1); + expect(wrapper.find(EuiFilterSelectItem)).toHaveLength(0); + }); + + it('handles adding and removing users', () => { + const _users = [ + users[0], + { + ...users[0], + id: 'asdfa', + }, + ]; + + const wrapper = shallow( + + ); + const firstItem = wrapper.find(EuiFilterSelectItem).first(); + firstItem.simulate('click'); + + expect(removeFilteredUser).toHaveBeenCalled(); + expect(addFilteredUser).not.toHaveBeenCalled(); + + const secondItem = wrapper.find(EuiFilterSelectItem).last(); + secondItem.simulate('click'); + + expect(addFilteredUser).toHaveBeenCalled(); + }); + + it('renders loading when no users', () => { + const wrapper = shallow( + loading} /> + ); + const card = wrapper.find(EuiCard); + + expect((card.prop('description') as any).props.children).toEqual('loading'); + }); + + it('handles hidden users when count is higher than 20', () => { + const _users = [] as IUser[]; + const NUM_TOTAL_USERS = 30; + const NUM_VISIBLE_USERS = 20; + + [...Array(NUM_TOTAL_USERS)].forEach((_, i) => { + _users.push({ + ...users[0], + id: i.toString(), + }); + }); + + const wrapper = shallow(); + + expect(wrapper.find(EuiFilterSelectItem)).toHaveLength(NUM_VISIBLE_USERS); + }); + + it('renders elements wrapped when popover', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiPopoverTitle)).toHaveLength(1); + expect(wrapper.find('.euiFilterSelect__items')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx new file mode 100644 index 000000000000..215a0e3eecdd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions } from '../../../../__mocks__'; +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { FilterableUsersPopover } from './filterable_users_popover'; +import { FilterableUsersList } from './filterable_users_list'; + +import { EuiFilterGroup, EuiPopover } from '@elastic/eui'; + +const closePopover = jest.fn(); +const addFilteredUser = jest.fn(); +const removeFilteredUser = jest.fn(); + +const props = { + users, + closePopover, + isPopoverOpen: false, + button: <>, +}; + +describe('FilterableUsersPopover', () => { + beforeEach(() => { + setMockActions({ + addFilteredUser, + removeFilteredUser, + }); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFilterGroup)).toHaveLength(1); + expect(wrapper.find(EuiPopover)).toHaveLength(1); + expect(wrapper.find(FilterableUsersList)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx new file mode 100644 index 000000000000..2826d740d533 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx @@ -0,0 +1,85 @@ +/* + * 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 '../../../../__mocks__/kea.mock'; + +import { setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { GroupManagerModal } from './group_manager_modal'; + +import { EuiOverlayMask, EuiModal, EuiEmptyPrompt } from '@elastic/eui'; + +const hideModal = jest.fn(); +const selectAll = jest.fn(); +const saveItems = jest.fn(); + +const props = { + children: <>, + label: 'shared content sources', + allItems: [], + numSelected: 1, + hideModal, + selectAll, + saveItems, +}; + +const mockValues = { + group: groups[0], + contentSources, + managerModalFormErrors: [], +}; + +describe('GroupManagerModal', () => { + beforeEach(() => { + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiModal)).toHaveLength(1); + expect(wrapper.find(EuiOverlayMask)).toHaveLength(1); + }); + + it('renders empty state', () => { + setMockValues({ ...mockValues, contentSources: [] }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + + it('selects all items when clicked', () => { + const wrapper = shallow(); + + const button = wrapper.find('[data-test-subj="SelectAllGroups"]'); + button.simulate('click'); + + expect(selectAll).toHaveBeenCalledWith([]); + }); + + it('deselects all items when clicked', () => { + const wrapper = shallow(); + + const button = wrapper.find('[data-test-subj="SelectAllGroups"]'); + button.simulate('click'); + + expect(selectAll).toHaveBeenCalledWith([{}]); + }); + + it('handles cancel when clicked', () => { + const wrapper = shallow(); + + const button = wrapper.find('[data-test-subj="CloseGroupsModal"]'); + button.simulate('click'); + + expect(hideModal).toHaveBeenCalledWith(groups[0]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx index db576808b66e..c91516edf7b1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx @@ -139,7 +139,7 @@ export const GroupManagerModal: React.FC = ({ - + {i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSelectAllToggle', { @@ -152,7 +152,9 @@ export const GroupManagerModal: React.FC = ({ - {CANCEL_BUTTON_TEXT} + + {CANCEL_BUTTON_TEXT} + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx new file mode 100644 index 000000000000..acb2fcfbaaa3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { + GroupOverview, + EMPTY_SOURCES_DESCRIPTION, + EMPTY_USERS_DESCRIPTION, +} from './group_overview'; + +import { ContentSection } from '../../../components/shared/content_section'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { SourcesTable } from '../../../components/shared/sources_table'; +import { Loading } from '../../../components/shared/loading'; + +import { EuiFieldText } from '@elastic/eui'; + +const deleteGroup = jest.fn(); +const showSharedSourcesModal = jest.fn(); +const showManageUsersModal = jest.fn(); +const showConfirmDeleteModal = jest.fn(); +const hideConfirmDeleteModal = jest.fn(); +const updateGroupName = jest.fn(); +const onGroupNameInputChange = jest.fn(); + +const mockValues = { + group: groups[0], + groupNameInputValue: '', + dataLoading: false, + confirmDeleteModalVisible: true, +}; + +describe('GroupOverview', () => { + beforeEach(() => { + setMockActions({ + deleteGroup, + showSharedSourcesModal, + showManageUsersModal, + showConfirmDeleteModal, + hideConfirmDeleteModal, + updateGroupName, + onGroupNameInputChange, + }); + setMockValues(mockValues); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ContentSection)).toHaveLength(4); + expect(wrapper.find(ViewContentHeader)).toHaveLength(1); + expect(wrapper.find(SourcesTable)).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('updates the input value', () => { + const wrapper = shallow(); + + const input = wrapper.find(EuiFieldText); + input.simulate('change', { target: { value: 'bar' } }); + + expect(onGroupNameInputChange).toHaveBeenCalledWith('bar'); + }); + + it('submits the form', () => { + const wrapper = shallow(); + + const simulatedEvent = { + form: 0, + target: { getAttribute: () => '_self' }, + preventDefault: jest.fn(), + }; + + const form = wrapper.find('form'); + form.simulate('submit', simulatedEvent); + expect(simulatedEvent.preventDefault).toHaveBeenCalled(); + expect(updateGroupName).toHaveBeenCalled(); + }); + + it('renders empty state messages', () => { + setMockValues({ + ...mockValues, + group: { + ...groups[0], + users: [], + contentSources: [], + }, + }); + + const wrapper = shallow(); + const sourcesSection = wrapper.find('[data-test-subj="GroupContentSourcesSection"]') as any; + const usersSection = wrapper.find('[data-test-subj="GroupUsersSection"]') as any; + + expect(sourcesSection.prop('description')).toEqual(EMPTY_SOURCES_DESCRIPTION); + expect(usersSection.prop('description')).toEqual(EMPTY_USERS_DESCRIPTION); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx index 1c7a01a1d9a4..fd97f1c0a03c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx @@ -33,7 +33,7 @@ import { GroupUsersTable } from './group_users_table'; import { GroupLogic, MAX_NAME_LENGTH } from '../group_logic'; -const EMPTY_SOURCES_DESCRIPTION = i18n.translate( +export const EMPTY_SOURCES_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription', { defaultMessage: 'No content sources are shared with this group.', @@ -45,7 +45,7 @@ const GROUP_USERS_DESCRIPTION = i18n.translate( defaultMessage: 'Members will be able to search over the group’s sources.', } ); -const EMPTY_USERS_DESCRIPTION = i18n.translate( +export const EMPTY_USERS_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.overview.emptyUsersDescription', { defaultMessage: 'There are no users in this group.', @@ -180,6 +180,7 @@ export const GroupOverview: React.FC = () => { title="Group content sources" description={hasContentSources ? GROUP_SOURCES_DESCRIPTION : EMPTY_SOURCES_DESCRIPTION} action={manageSourcesButton} + data-test-subj="GroupContentSourcesSection" > {hasContentSources && sourcesTable} @@ -190,6 +191,7 @@ export const GroupOverview: React.FC = () => { title="Group users" description={hasUsers ? GROUP_USERS_DESCRIPTION : EMPTY_USERS_DESCRIPTION} action={manageUsersButton} + data-test-subj="GroupUsersSection" > {hasUsers && } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx new file mode 100644 index 000000000000..c7eea8ab64d4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import moment from 'moment'; + +import { GroupRow, NO_USERS_MESSAGE, NO_SOURCES_MESSAGE } from './group_row'; +import { GroupUsers } from './group_users'; + +import { EuiTableRow } from '@elastic/eui'; + +describe('GroupRow', () => { + beforeEach(() => { + setMockValues({ isFederatedAuth: true }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(1); + }); + + it('renders group users', () => { + setMockValues({ isFederatedAuth: false }); + const wrapper = shallow(); + + expect(wrapper.find(GroupUsers)).toHaveLength(1); + }); + + it('renders fromNow date string when in range', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('small').text()).toEqual('Last updated 7 days ago.'); + }); + + it('renders formatted date string when out of range', () => { + const wrapper = shallow(); + + expect(wrapper.find('small').text()).toEqual('Last updated January 1, 2020.'); + }); + + it('renders empty users message when no users present', () => { + setMockValues({ isFederatedAuth: false }); + const wrapper = shallow(); + + expect(wrapper.find('.user-group__accounts').text()).toEqual(NO_USERS_MESSAGE); + }); + + it('renders empty sources message when no sources present', () => { + const wrapper = shallow(); + + expect(wrapper.find('.user-group__sources').text()).toEqual(NO_SOURCES_MESSAGE); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx index 9c7276372cf5..9642d48af55f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx @@ -24,13 +24,13 @@ import { GroupSources } from './group_sources'; import { GroupUsers } from './group_users'; const DAYS_CUTOFF = 8; -const NO_SOURCES_MESSAGE = i18n.translate( +export const NO_SOURCES_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage', { defaultMessage: 'No shared content sources', } ); -const NO_USERS_MESSAGE = i18n.translate( +export const NO_USERS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage', { defaultMessage: 'No users', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx new file mode 100644 index 000000000000..9493e52e08b8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; +import { SourceOptionItem } from './source_option_item'; + +import { EuiFilterGroup } from '@elastic/eui'; + +const onButtonClick = jest.fn(); +const closePopover = jest.fn(); + +const props = { + isPopoverOpen: true, + numOptions: 1, + groupSources: contentSources, + onButtonClick, + closePopover, +}; + +describe('GroupRowSourcesDropdown', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SourceOptionItem)).toHaveLength(2); + expect(wrapper.find(EuiFilterGroup)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx new file mode 100644 index 000000000000..039a2620f1fd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx @@ -0,0 +1,66 @@ +/* + * 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 '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow, mount } from 'enzyme'; + +import { EuiLoadingContent, EuiButtonEmpty } from '@elastic/eui'; + +import { GroupRowUsersDropdown } from './group_row_users_dropdown'; +import { FilterableUsersPopover } from './filterable_users_popover'; + +const fetchGroupUsers = jest.fn(); +const onButtonClick = jest.fn(); +const closePopover = jest.fn(); + +const props = { + isPopoverOpen: true, + numOptions: 1, + groupId: '123', + onButtonClick, + closePopover, +}; +describe('GroupRowUsersDropdown', () => { + beforeEach(() => { + setMockActions({ fetchGroupUsers }); + setMockValues({ + allGroupUsers: users, + allGroupUsersLoading: false, + }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(FilterableUsersPopover)).toHaveLength(1); + }); + + it('handles toggle click', () => { + const wrapper = mount(); + + const button = wrapper.find(EuiButtonEmpty); + button.simulate('click'); + + expect(fetchGroupUsers).toHaveBeenCalledWith('123'); + expect(onButtonClick).toHaveBeenCalled(); + }); + + it('handles loading state', () => { + setMockValues({ + allGroupUsers: users, + allGroupUsersLoading: true, + }); + const wrapper = shallow(); + const popover = wrapper.find(FilterableUsersPopover); + + expect(popover.prop('allGroupUsersLoading')).toEqual(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx new file mode 100644 index 000000000000..81639327f4ba --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Loading } from '../../../components/shared/loading'; + +import { GroupSourcePrioritization } from './group_source_prioritization'; + +import { EuiTable, EuiEmptyPrompt, EuiRange } from '@elastic/eui'; + +const updatePriority = jest.fn(); +const saveGroupSourcePrioritization = jest.fn(); +const showSharedSourcesModal = jest.fn(); + +const mockValues = { + group: groups[0], + activeSourcePriorities: [ + { + [groups[0].id]: 1, + }, + ], + dataLoading: false, + groupPrioritiesUnchanged: true, +}; + +describe('GroupSourcePrioritization', () => { + beforeEach(() => { + setMockActions({ + updatePriority, + saveGroupSourcePrioritization, + showSharedSourcesModal, + }); + + setMockValues(mockValues); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTable)).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('renders empty state', () => { + setMockValues({ + ...mockValues, + group: { + ...groups[0], + contentSources: [], + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + + it('handles slider change', () => { + const wrapper = shallow(); + + const slider = wrapper.find(EuiRange).first(); + slider.simulate('change', { target: { value: 2 } }); + + expect(updatePriority).toHaveBeenCalledWith('123', 2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx new file mode 100644 index 000000000000..38c56aefebaf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { GroupSources } from './group_sources'; +import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; + +import { SourceIcon } from '../../../components/shared/source_icon'; + +import { IContentSourceDetails } from '../../../types'; + +describe('GroupSources', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SourceIcon)).toHaveLength(2); + }); + + it('handles hidden sources when count is higer than 10', () => { + const sources = [] as IContentSourceDetails[]; + const NUM_TOTAL_SOURCES = 10; + + [...Array(NUM_TOTAL_SOURCES)].forEach((_, i) => { + sources.push({ + ...contentSources[0], + id: i.toString(), + }); + }); + + const wrapper = shallow(); + + // These were needed for 100% test coverage. + wrapper.find(GroupRowSourcesDropdown).invoke('onButtonClick')(); + wrapper.find(GroupRowSourcesDropdown).invoke('closePopover')(); + + expect(wrapper.find(GroupRowSourcesDropdown)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx new file mode 100644 index 000000000000..7ddecc21c22c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { GroupSubNav } from './group_sub_nav'; + +import { SideNavLink } from '../../../../shared/layout'; + +describe('GroupSubNav', () => { + it('renders empty when no group id present', () => { + setMockValues({ group: {} }); + const wrapper = shallow(); + + expect(wrapper.find(SideNavLink)).toHaveLength(0); + }); + + it('renders nav items', () => { + setMockValues({ group: { id: '1' } }); + const wrapper = shallow(); + + expect(wrapper.find(SideNavLink)).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx new file mode 100644 index 000000000000..6a635eacf258 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 '../../../../__mocks__/kea.mock'; + +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { IUser } from '../../../types'; + +import { GroupUsers } from './group_users'; +import { GroupRowUsersDropdown } from './group_row_users_dropdown'; + +import { UserIcon } from '../../../components/shared/user_icon'; + +const props = { + groupUsers: users, + usersCount: 1, + groupId: '123', +}; + +describe('GroupUsers', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(UserIcon)).toHaveLength(1); + }); + + it('handles hidden users when count is higher than 20', () => { + const _users = [] as IUser[]; + const NUM_TOTAL_USERS = 20; + + [...Array(NUM_TOTAL_USERS)].forEach((_, i) => { + _users.push({ + ...users[0], + id: i.toString(), + }); + }); + + const wrapper = shallow(); + + // These were needed for 100% test coverage. + wrapper.find(GroupRowUsersDropdown).invoke('onButtonClick')(); + wrapper.find(GroupRowUsersDropdown).invoke('closePopover')(); + + expect(wrapper.find(GroupRowUsersDropdown)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx new file mode 100644 index 000000000000..8747a838689c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx @@ -0,0 +1,62 @@ +/* + * 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 '../../../../__mocks__/kea.mock'; + +import { setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { IUser } from '../../../types'; + +import { GroupUsersTable } from './group_users_table'; +import { TableHeader } from '../../../../shared/table_header'; + +import { EuiTable, EuiTablePagination } from '@elastic/eui'; + +const group = groups[0]; + +describe('GroupUsersTable', () => { + it('renders', () => { + setMockValues({ isFederatedAuth: true, group }); + const wrapper = shallow(); + + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(TableHeader).prop('headerItems')).toHaveLength(1); + }); + + it('adds header item for non-federated auth', () => { + setMockValues({ isFederatedAuth: false, group }); + const wrapper = shallow(); + + expect(wrapper.find(TableHeader).prop('headerItems')).toHaveLength(2); + }); + + it('renders pagination', () => { + const users = [] as IUser[]; + const NUM_TOTAL_USERS = 20; + + [...Array(NUM_TOTAL_USERS)].forEach((_, i) => { + users.push({ + ...group.users[0], + id: i.toString(), + }); + }); + + setMockValues({ isFederatedAuth: true, group: { users } }); + const wrapper = shallow(); + const pagination = wrapper.find(EuiTablePagination); + + // This was needed for 100% test coverage. The tests pass and 100% coverage + // is achieved with this line, but TypeScript complains anyway, so ignoring line. + // @ts-ignore + pagination.invoke('onChangePage')(1); + + expect(pagination).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx new file mode 100644 index 000000000000..38d035cbca90 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import { DEFAULT_META } from '../../../../shared/constants'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; + +import { GroupsTable } from './groups_table'; +import { GroupRow } from './group_row'; +import { ClearFiltersLink } from './clear_filters_link'; + +import { EuiTable, EuiTableHeaderCell } from '@elastic/eui'; + +const setActivePage = jest.fn(); + +const mockValues = { + groupsMeta: DEFAULT_META, + groups, + hasFiltersSet: false, + isFederatedAuth: true, +}; + +describe('GroupsTable', () => { + beforeEach(() => { + setMockActions({ setActivePage }); + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(GroupRow)).toHaveLength(1); + }); + + it('renders extra header for non-federated auth', () => { + setMockValues({ ...mockValues, isFederatedAuth: false }); + const wrapper = shallow(); + + expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(4); + }); + + it('handles pagination', () => { + setMockValues({ + ...mockValues, + groupsMeta: { + page: { + current: 1, + size: 10, + total_pages: 3, + total_results: 30, + }, + }, + }); + + const wrapper = shallow(); + wrapper.find(TablePaginationBar).first().invoke('onChangePage')(1); + + expect(setActivePage).toHaveBeenCalledWith(2); + expect(wrapper.find(TablePaginationBar)).toHaveLength(2); + }); + + it('renders clear filters link when filters set', () => { + setMockValues({ ...mockValues, hasFiltersSet: true }); + const wrapper = shallow(); + + expect(wrapper.find(ClearFiltersLink)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx new file mode 100644 index 000000000000..34f748e8a716 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ManageUsersModal } from './manage_users_modal'; +import { FilterableUsersList } from './filterable_users_list'; +import { GroupManagerModal } from './group_manager_modal'; + +const addGroupUser = jest.fn(); +const removeGroupUser = jest.fn(); +const selectAllUsers = jest.fn(); +const hideManageUsersModal = jest.fn(); +const saveGroupUsers = jest.fn(); + +describe('ManageUsersModal', () => { + it('renders', () => { + setMockActions({ + addGroupUser, + removeGroupUser, + selectAllUsers, + hideManageUsersModal, + saveGroupUsers, + }); + + setMockValues({ + users, + selectedGroupUsers: [], + }); + + const wrapper = shallow(); + + expect(wrapper.find(FilterableUsersList)).toHaveLength(1); + expect(wrapper.find(GroupManagerModal)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx new file mode 100644 index 000000000000..8c5ead2509d9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SharedSourcesModal } from './shared_sources_modal'; +import { GroupManagerModal } from './group_manager_modal'; +import { SourcesList } from './sources_list'; + +const group = groups[0]; + +const addGroupSource = jest.fn(); +const selectAllSources = jest.fn(); +const hideSharedSourcesModal = jest.fn(); +const removeGroupSource = jest.fn(); +const saveGroupSources = jest.fn(); + +describe('SharedSourcesModal', () => { + it('renders', () => { + setMockActions({ + addGroupSource, + selectAllSources, + hideSharedSourcesModal, + removeGroupSource, + saveGroupSources, + }); + + setMockValues({ + group, + selectedGroupSources: [], + contentSources: group.contentSources, + }); + + const wrapper = shallow(); + + expect(wrapper.find(SourcesList)).toHaveLength(1); + expect(wrapper.find(GroupManagerModal)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx new file mode 100644 index 000000000000..8a3901f5462d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.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 '../../../../__mocks__/kea.mock'; + +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SourceOptionItem } from './source_option_item'; + +import { TruncatedContent } from '../../../../shared/truncate'; + +import { SourceIcon } from '../../../components/shared/source_icon'; + +describe('SourceOptionItem', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(TruncatedContent)).toHaveLength(1); + expect(wrapper.find(SourceIcon)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx new file mode 100644 index 000000000000..05754f3846bb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SourcesList } from './sources_list'; + +import { EuiFilterSelectItem } from '@elastic/eui'; + +const addFilteredSource = jest.fn(); +const removeFilteredSource = jest.fn(); + +const props = { + contentSources, + filteredSources: [], + addFilteredSource, + removeFilteredSource, +}; + +describe('SourcesList', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFilterSelectItem)).toHaveLength(2); + }); + + it('handles adding item click when item unchecked', () => { + const wrapper = shallow(); + wrapper.find(EuiFilterSelectItem).first().simulate('click'); + + expect(addFilteredSource).toHaveBeenCalled(); + }); + + it('handles removing item click when item checked', () => { + const wrapper = shallow(); + wrapper.find(EuiFilterSelectItem).first().simulate('click'); + + expect(removeFilteredSource).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx new file mode 100644 index 000000000000..e75feb425492 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.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 '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TableFilterSourcesDropdown } from './table_filter_sources_dropdown'; + +import { SourcesList } from './sources_list'; + +const addFilteredSource = jest.fn(); +const removeFilteredSource = jest.fn(); +const toggleFilterSourcesDropdown = jest.fn(); +const closeFilterSourcesDropdown = jest.fn(); + +describe('TableFilterSourcesDropdown', () => { + it('renders', () => { + setMockActions({ + addFilteredSource, + removeFilteredSource, + toggleFilterSourcesDropdown, + closeFilterSourcesDropdown, + }); + + setMockValues({ contentSources, filterSourcesDropdownOpen: false, filteredSources: [] }); + + const wrapper = shallow(); + + expect(wrapper.find(SourcesList)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx new file mode 100644 index 000000000000..9d461e06a77e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TableFilterUsersDropdown } from './table_filter_users_dropdown'; +import { FilterableUsersPopover } from './filterable_users_popover'; + +const closeFilterUsersDropdown = jest.fn(); +const toggleFilterUsersDropdown = jest.fn(); + +describe('TableFilterUsersDropdown', () => { + it('renders', () => { + setMockActions({ closeFilterUsersDropdown, toggleFilterUsersDropdown }); + setMockValues({ users, filteredUsers: [], filterUsersDropdownOpen: false }); + + const wrapper = shallow(); + + expect(wrapper.find(FilterableUsersPopover)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx new file mode 100644 index 000000000000..80662bc0974a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TableFilters } from './table_filters'; + +import { EuiFieldSearch } from '@elastic/eui'; +import { TableFilterSourcesDropdown } from './table_filter_sources_dropdown'; +import { TableFilterUsersDropdown } from './table_filter_users_dropdown'; + +const setFilterValue = jest.fn(); + +describe('TableFilters', () => { + beforeEach(() => { + setMockValues({ filterValue: '', isFederatedAuth: true }); + setMockActions({ setFilterValue }); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldSearch)).toHaveLength(1); + expect(wrapper.find(TableFilterSourcesDropdown)).toHaveLength(1); + expect(wrapper.find(TableFilterUsersDropdown)).toHaveLength(0); + }); + + it('renders for non-federated Auth', () => { + setMockValues({ filterValue: '', isFederatedAuth: false }); + const wrapper = shallow(); + + expect(wrapper.find(TableFilterUsersDropdown)).toHaveLength(1); + }); + + it('handles search input value change', () => { + const wrapper = shallow(); + const input = wrapper.find(EuiFieldSearch); + input.simulate('change', { target: { value: 'bar' } }); + + expect(setFilterValue).toHaveBeenCalledWith('bar'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx new file mode 100644 index 000000000000..72611f254d01 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { UserOptionItem } from './user_option_item'; +import { UserIcon } from '../../../components/shared/user_icon'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +const user = users[0]; + +describe('UserOptionItem', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(UserIcon)).toHaveLength(1); + expect(wrapper.find(EuiFlexGroup)).toHaveLength(1); + expect(wrapper.find(EuiFlexItem)).toHaveLength(2); + }); + + it('falls back to email when name not present', () => { + const wrapper = shallow(); + const nameItem = wrapper.find(EuiFlexItem).last(); + + expect(nameItem.prop('children')).toEqual(user.email); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index d8cb24b34496..e55f997a6b51 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -325,6 +325,10 @@ describe('EnterpriseSearchRequestHandler', () => { expect(mockLogger.error).toHaveBeenCalled(); }); + it('errors when receiving a 401 response', async () => { + EnterpriseSearchAPI.mockReturn({}, { status: 401 }); + }); + it('errors when redirected to /login', async () => { EnterpriseSearchAPI.mockReturn({}, { url: 'http://localhost:3002/login' }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index cb28cd2b90f4..ad6d936ac0c3 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -84,8 +84,12 @@ export class EnterpriseSearchRequestHandler { // Handle response headers this.setResponseHeaders(apiResponse); - // Handle authentication redirects - if (apiResponse.url.endsWith('/login') || apiResponse.url.endsWith('/ent/select')) { + // Handle unauthenticated users / authentication redirects + if ( + apiResponse.status === 401 || + apiResponse.url.endsWith('/login') || + apiResponse.url.endsWith('/ent/select') + ) { return this.handleAuthenticationError(response); } @@ -213,6 +217,10 @@ export class EnterpriseSearchRequestHandler { return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); } + /** + * Note: Kibana auto logs users out when it receives a 401 response, so we want to catch and + * return 401 responses from Enterprise Search as a 502 so Kibana sessions aren't interrupted + */ handleAuthenticationError(response: KibanaResponseFactory) { const errorMessage = 'Cannot authenticate Enterprise Search user'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap index 59d1723c3948..a25af12ff7f5 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap @@ -552,7 +552,9 @@ exports[`extend index management ilm summary extension should return extension w 2018-12-07 13:02:55 - +
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index 4dff70518c11..bd845b0a7d9a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -10,6 +10,23 @@ export const POLICY_NAME = 'my_policy'; export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy'; export const NEW_SNAPSHOT_POLICY_NAME = 'my_new_snapshot_policy'; +export const DEFAULT_POLICY: PolicyFromES = { + version: 1, + modified_date: Date.now().toString(), + policy: { + name: '', + phases: { + hot: { + min_age: '123ms', + actions: { + rollover: {}, + }, + }, + }, + }, + name: '', +}; + export const DELETE_PHASE_POLICY: PolicyFromES = { version: 1, modified_date: Date.now().toString(), @@ -19,8 +36,12 @@ export const DELETE_PHASE_POLICY: PolicyFromES = { min_age: '0ms', actions: { rollover: { + max_age: '30d', max_size: '50gb', }, + set_priority: { + priority: 100, + }, }, }, delete: { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 6365bb8caa96..0cfccba76130 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils'; +import { registerTestBed, TestBedConfig } from '../../../../../test_utils'; import { POLICY_NAME } from './constants'; import { TestSubjects } from '../helpers'; @@ -43,39 +43,110 @@ const testBedConfig: TestBedConfig = { }, }; -const initTestBed = registerTestBed(EditPolicy, testBedConfig); +const initTestBed = registerTestBed(EditPolicy, testBedConfig); -export interface EditPolicyTestBed extends TestBed { - actions: { - setWaitForSnapshotPolicy: (snapshotPolicyName: string) => void; - savePolicy: () => void; - }; -} +type SetupReturn = ReturnType; + +export type EditPolicyTestBed = SetupReturn extends Promise ? U : SetupReturn; -export const setup = async (): Promise => { +export const setup = async () => { const testBed = await initTestBed(); + const { find, component } = testBed; + const setWaitForSnapshotPolicy = async (snapshotPolicyName: string) => { - const { component } = testBed; act(() => { - testBed.find('snapshotPolicyCombobox').simulate('change', [{ label: snapshotPolicyName }]); + find('snapshotPolicyCombobox').simulate('change', [{ label: snapshotPolicyName }]); }); component.update(); }; const savePolicy = async () => { - const { component, find } = testBed; await act(async () => { find('savePolicyButton').simulate('click'); }); component.update(); }; + const toggleRollover = async (checked: boolean) => { + await act(async () => { + find('rolloverSwitch').simulate('click', { target: { checked } }); + }); + component.update(); + }; + + const setMaxSize = async (value: string, units?: string) => { + await act(async () => { + find('hot-selectedMaxSizeStored').simulate('change', { target: { value } }); + if (units) { + find('hot-selectedMaxSizeStoredUnits.select').simulate('change', { + target: { value: units }, + }); + } + }); + component.update(); + }; + + const setMaxDocs = async (value: string) => { + await act(async () => { + find('hot-selectedMaxDocuments').simulate('change', { target: { value } }); + }); + component.update(); + }; + + const setMaxAge = async (value: string, units?: string) => { + await act(async () => { + find('hot-selectedMaxAge').simulate('change', { target: { value } }); + if (units) { + find('hot-selectedMaxAgeUnits.select').simulate('change', { target: { value: units } }); + } + }); + component.update(); + }; + + const toggleForceMerge = (phase: string) => async (checked: boolean) => { + await act(async () => { + find(`${phase}-forceMergeSwitch`).simulate('click', { target: { checked } }); + }); + component.update(); + }; + + const setForcemergeSegmentsCount = (phase: string) => async (value: string) => { + await act(async () => { + find(`${phase}-selectedForceMergeSegments`).simulate('change', { target: { value } }); + }); + component.update(); + }; + + const setBestCompression = (phase: string) => async (checked: boolean) => { + await act(async () => { + find(`${phase}-bestCompression`).simulate('click', { target: { checked } }); + }); + component.update(); + }; + + const setIndexPriority = (phase: string) => async (value: string) => { + await act(async () => { + find(`${phase}-phaseIndexPriority`).simulate('change', { target: { value } }); + }); + component.update(); + }; + return { ...testBed, actions: { setWaitForSnapshotPolicy, savePolicy, + hot: { + setMaxSize, + setMaxDocs, + setMaxAge, + toggleRollover, + toggleForceMerge: toggleForceMerge('hot'), + setForcemergeSegments: setForcemergeSegmentsCount('hot'), + setBestCompression: setBestCompression('hot'), + setIndexPriority: setIndexPriority('hot'), + }, }, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index b465afb8b5d9..3cbc2d982566 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -10,7 +10,12 @@ import { setupEnvironment } from '../helpers/setup_environment'; import { EditPolicyTestBed, setup } from './edit_policy.helpers'; import { API_BASE_PATH } from '../../../common/constants'; -import { DELETE_PHASE_POLICY, NEW_SNAPSHOT_POLICY_NAME, SNAPSHOT_POLICY_NAME } from './constants'; +import { + DELETE_PHASE_POLICY, + NEW_SNAPSHOT_POLICY_NAME, + SNAPSHOT_POLICY_NAME, + DEFAULT_POLICY, +} from './constants'; window.scrollTo = jest.fn(); @@ -21,6 +26,83 @@ describe('', () => { server.restore(); }); + describe('hot phase', () => { + describe('serialization', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('setting all values', async () => { + const { actions } = testBed; + + await actions.hot.setMaxSize('123', 'mb'); + await actions.hot.setMaxDocs('123'); + await actions.hot.setMaxAge('123', 'h'); + await actions.hot.toggleForceMerge(true); + await actions.hot.setForcemergeSegments('123'); + await actions.hot.setBestCompression(true); + await actions.hot.setIndexPriority('123'); + + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toMatchInlineSnapshot(` + Object { + "name": "my_policy", + "phases": Object { + "hot": Object { + "actions": Object { + "forcemerge": Object { + "index_codec": "best_compression", + "max_num_segments": 123, + }, + "rollover": Object { + "max_age": "123h", + "max_docs": 123, + "max_size": "123mb", + }, + "set_priority": Object { + "priority": 123, + }, + }, + "min_age": "0ms", + }, + }, + } + `); + }); + + test('disabling rollover', async () => { + const { actions } = testBed; + await actions.hot.toggleRollover(false); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toMatchInlineSnapshot(` + Object { + "name": "my_policy", + "phases": Object { + "hot": Object { + "actions": Object { + "set_priority": Object { + "priority": 100, + }, + }, + "min_age": "0ms", + }, + }, + } + `); + }); + }); + }); + describe('delete phase', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([DELETE_PHASE_POLICY]); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts index 7b227f822fa9..c6d27ca890b5 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts @@ -9,4 +9,12 @@ export type TestSubjects = | 'savePolicyButton' | 'customPolicyCallout' | 'noPoliciesCallout' - | 'policiesErrorCallout'; + | 'policiesErrorCallout' + | 'rolloverSwitch' + | 'rolloverSettingsRequired' + | 'hot-selectedMaxSizeStored' + | 'hot-selectedMaxSizeStoredUnits' + | 'hot-selectedMaxDocuments' + | 'hot-selectedMaxAge' + | 'hot-selectedMaxAgeUnits' + | string; 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..d9af20763657 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'; @@ -31,15 +33,12 @@ import { positiveNumbersAboveZeroErrorMessage, positiveNumberRequiredMessage, numberRequiredMessage, - maximumAgeRequiredMessage, - maximumSizeRequiredMessage, policyNameRequiredMessage, policyNameStartsWithUnderscoreErrorMessage, policyNameContainsCommaErrorMessage, policyNameContainsSpaceErrorMessage, policyNameMustBeDifferentErrorMessage, policyNameAlreadyUsedErrorMessage, - maximumDocumentsRequiredMessage, } from '../../public/application/services/policies/policy_validation'; import { editPolicyHelpers } from './helpers'; @@ -114,8 +113,10 @@ const expectedErrorMessages = (rendered: ReactWrapper, expectedMessages: string[ expect(foundErrorMessage).toBe(true); }); }; -const noRollover = (rendered: ReactWrapper) => { - findTestSubject(rendered, 'rolloverSwitch').simulate('click'); +const noRollover = async (rendered: ReactWrapper) => { + await act(async () => { + findTestSubject(rendered, 'rolloverSwitch').simulate('click'); + }); rendered.update(); }; const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => { @@ -131,7 +132,7 @@ const setPhaseAfter = (rendered: ReactWrapper, phase: string, after: string | nu afterInput.simulate('change', { target: { value: after } }); rendered.update(); }; -const setPhaseIndexPriority = ( +const setPhaseIndexPriorityLegacy = ( rendered: ReactWrapper, phase: string, priority: string | number @@ -140,15 +141,53 @@ const setPhaseIndexPriority = ( priorityInput.simulate('change', { target: { value: priority } }); rendered.update(); }; -const save = (rendered: ReactWrapper) => { +const setPhaseIndexPriority = async ( + rendered: ReactWrapper, + phase: string, + priority: string | number +) => { + const priorityInput = findTestSubject(rendered, `${phase}-phaseIndexPriority`); + await act(async () => { + priorityInput.simulate('change', { target: { value: priority } }); + }); + rendered.update(); +}; +const save = async (rendered: ReactWrapper) => { const saveButton = findTestSubject(rendered, 'savePolicyButton'); - saveButton.simulate('click'); + await act(async () => { + saveButton.simulate('click'); + }); rendered.update(); }; describe('edit policy', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + + /** + * The form lib has a short delay (setTimeout) before running and rendering + * any validation errors. This helper advances timers and can trigger component + * state changes. + */ + const waitForFormLibValidation = () => { + act(() => { + jest.advanceTimersByTime(1000); + }); + }; + beforeEach(() => { component = ( - + + + ); ({ http } = editPolicyHelpers.setup()); @@ -157,27 +196,27 @@ describe('edit policy', () => { httpRequestsMockHelpers.setPoliciesResponse(policies); }); describe('top level form', () => { - test('should show error when trying to save empty form', () => { + test('should show error when trying to save empty form', async () => { const rendered = mountWithIntl(component); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [policyNameRequiredMessage]); }); - test('should show error when trying to save policy name with space', () => { + test('should show error when trying to save policy name with space', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'my policy'); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [policyNameContainsSpaceErrorMessage]); }); - test('should show error when trying to save policy name that is already used', () => { + test('should show error when trying to save policy name that is already used', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'testy0'); rendered.update(); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [policyNameAlreadyUsedErrorMessage]); }); - test('should show error when trying to save as new policy but using the same name', () => { + test('should show error when trying to save as new policy but using the same name', async () => { component = ( { findTestSubject(rendered, 'saveAsNewSwitch').simulate('click'); rendered.update(); setPolicyName(rendered, 'testy0'); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [policyNameMustBeDifferentErrorMessage]); }); - test('should show error when trying to save policy name with comma', () => { + test('should show error when trying to save policy name with comma', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'my,policy'); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [policyNameContainsCommaErrorMessage]); }); - test('should show error when trying to save policy name starting with underscore', () => { + test('should show error when trying to save policy name starting with underscore', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, '_mypolicy'); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [policyNameStartsWithUnderscoreErrorMessage]); }); - test('should show correct json in policy flyout', () => { + test('should show correct json in policy flyout', async () => { const rendered = mountWithIntl(component); - findTestSubject(rendered, 'requestButton').simulate('click'); + + await act(async () => { + findTestSubject(rendered, 'requestButton').simulate('click'); + }); + rendered.update(); const json = rendered.find(`code`).text(); const expected = `PUT _ilm/policy/\n${JSON.stringify( { policy: { phases: { hot: { - min_age: '0ms', actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, set_priority: { priority: 100, }, + rollover: { + max_size: '50gb', + max_age: '30d', + }, }, + min_age: '0ms', }, }, }, @@ -237,55 +280,66 @@ describe('edit policy', () => { }); }); describe('hot phase', () => { - test('should show errors when trying to save with no max size and no max age', () => { + test('should show errors when trying to save with no max size and no max age', async () => { const rendered = mountWithIntl(component); + expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeFalsy(); setPolicyName(rendered, 'mypolicy'); - const maxSizeInput = rendered.find(`input#hot-selectedMaxSizeStored`); - maxSizeInput.simulate('change', { target: { value: '' } }); - const maxAgeInput = rendered.find(`input#hot-selectedMaxAge`); - maxAgeInput.simulate('change', { target: { value: '' } }); + const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); + await act(async () => { + maxSizeInput.simulate('change', { target: { value: '' } }); + }); + waitForFormLibValidation(); + const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); + await act(async () => { + maxAgeInput.simulate('change', { target: { value: '' } }); + }); + waitForFormLibValidation(); rendered.update(); - save(rendered); - expectedErrorMessages(rendered, [ - maximumSizeRequiredMessage, - maximumAgeRequiredMessage, - maximumDocumentsRequiredMessage, - ]); + await save(rendered); + expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeTruthy(); }); - test('should show number above 0 required error when trying to save with -1 for max size', () => { + test('should show number above 0 required error when trying to save with -1 for max size', async () => { const rendered = mountWithIntl(component); setPolicyName(rendered, 'mypolicy'); - const maxSizeInput = rendered.find(`input#hot-selectedMaxSizeStored`); - maxSizeInput.simulate('change', { target: { value: -1 } }); + const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); + await act(async () => { + maxSizeInput.simulate('change', { target: { value: '-1' } }); + }); + waitForFormLibValidation(); rendered.update(); - save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); - test('should show number above 0 required error when trying to save with 0 for max size', () => { + test('should show number above 0 required error when trying to save with 0 for max size', async () => { const rendered = mountWithIntl(component); setPolicyName(rendered, 'mypolicy'); - const maxSizeInput = rendered.find(`input#hot-selectedMaxSizeStored`); - maxSizeInput.simulate('change', { target: { value: 0 } }); + const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); + await act(async () => { + maxSizeInput.simulate('change', { target: { value: '-1' } }); + }); + waitForFormLibValidation(); rendered.update(); - save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); - test('should show number above 0 required error when trying to save with -1 for max age', () => { + test('should show number above 0 required error when trying to save with -1 for max age', async () => { const rendered = mountWithIntl(component); setPolicyName(rendered, 'mypolicy'); - const maxSizeInput = rendered.find(`input#hot-selectedMaxAge`); - maxSizeInput.simulate('change', { target: { value: -1 } }); + const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); + await act(async () => { + maxAgeInput.simulate('change', { target: { value: '-1' } }); + }); + waitForFormLibValidation(); rendered.update(); - save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); - test('should show number above 0 required error when trying to save with 0 for max age', () => { + test('should show number above 0 required error when trying to save with 0 for max age', async () => { const rendered = mountWithIntl(component); setPolicyName(rendered, 'mypolicy'); - const maxSizeInput = rendered.find(`input#hot-selectedMaxAge`); - maxSizeInput.simulate('change', { target: { value: 0 } }); + const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); + await act(async () => { + maxAgeInput.simulate('change', { target: { value: '0' } }); + }); + waitForFormLibValidation(); rendered.update(); - save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); test('should show forcemerge input when rollover enabled', () => { @@ -293,22 +347,27 @@ describe('edit policy', () => { setPolicyName(rendered, 'mypolicy'); expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeTruthy(); }); - test('should hide forcemerge input when rollover is disabled', () => { + test('should hide forcemerge input when rollover is disabled', async () => { const rendered = mountWithIntl(component); setPolicyName(rendered, 'mypolicy'); - noRollover(rendered); + await noRollover(rendered); + waitForFormLibValidation(); rendered.update(); expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeFalsy(); }); test('should show positive number required above zero error when trying to save hot phase with 0 for force merge', async () => { const rendered = mountWithIntl(component); setPolicyName(rendered, 'mypolicy'); - findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); + act(() => { + findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); + }); rendered.update(); const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); - forcemergeInput.simulate('change', { target: { value: '0' } }); + await act(async () => { + forcemergeInput.simulate('change', { target: { value: '0' } }); + }); + waitForFormLibValidation(); rendered.update(); - save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); test('should show positive number above 0 required error when trying to save hot phase with -1 for force merge', async () => { @@ -317,18 +376,22 @@ describe('edit policy', () => { findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); rendered.update(); const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); - forcemergeInput.simulate('change', { target: { value: '-1' } }); + await act(async () => { + forcemergeInput.simulate('change', { target: { value: '-1' } }); + }); + waitForFormLibValidation(); rendered.update(); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); - test('should show positive number required error when trying to save with -1 for index priority', () => { + test('should show positive number required error when trying to save with -1 for index priority', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - setPhaseIndexPriority(rendered, 'hot', '-1'); - save(rendered); - expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); + await setPhaseIndexPriority(rendered, 'hot', '-1'); + waitForFormLibValidation(); + rendered.update(); + expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); }); describe('warm phase', () => { @@ -342,44 +405,44 @@ describe('edit policy', () => { test('should show number required error when trying to save empty warm phase', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', ''); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [numberRequiredMessage]); }); test('should allow 0 for phase timing', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', '0'); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, []); }); test('should show positive number required error when trying to save warm phase with -1 for after', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', '-1'); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); test('should show positive number required error when trying to save warm phase with -1 for index priority', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', '1'); - setPhaseIndexPriority(rendered, 'warm', '-1'); - save(rendered); + setPhaseIndexPriorityLegacy(rendered, 'warm', '-1'); + await save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); findTestSubject(rendered, 'shrinkSwitch').simulate('click'); @@ -388,12 +451,12 @@ describe('edit policy', () => { const shrinkInput = rendered.find('input#warm-selectedPrimaryShardCount'); shrinkInput.simulate('change', { target: { value: '0' } }); rendered.update(); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', '1'); @@ -402,12 +465,12 @@ describe('edit policy', () => { const shrinkInput = rendered.find('input#warm-selectedPrimaryShardCount'); shrinkInput.simulate('change', { target: { value: '-1' } }); rendered.update(); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', '1'); @@ -416,12 +479,12 @@ describe('edit policy', () => { const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); forcemergeInput.simulate('change', { target: { value: '0' } }); rendered.update(); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); setPhaseAfter(rendered, 'warm', '1'); @@ -430,13 +493,13 @@ describe('edit policy', () => { const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); forcemergeInput.simulate('change', { target: { value: '-1' } }); rendered.update(); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); }); test('should show spinner for node attributes input when loading', async () => { server.respondImmediately = false; const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); @@ -447,9 +510,10 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -459,7 +523,7 @@ describe('edit policy', () => { }); test('should show node attributes input when attributes exist', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -471,7 +535,7 @@ describe('edit policy', () => { }); test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -495,9 +559,10 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -507,9 +572,10 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -519,9 +585,10 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -538,26 +605,26 @@ describe('edit policy', () => { }); test('should allow 0 for phase timing', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); setPhaseAfter(rendered, 'cold', '0'); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, []); }); test('should show positive number required error when trying to save cold phase with -1 for after', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); setPhaseAfter(rendered, 'cold', '-1'); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); test('should show spinner for node attributes input when loading', async () => { server.respondImmediately = false; const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); @@ -568,9 +635,10 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -580,7 +648,7 @@ describe('edit policy', () => { }); test('should show node attributes input when attributes exist', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -592,7 +660,7 @@ describe('edit policy', () => { }); test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -614,21 +682,22 @@ describe('edit policy', () => { }); test('should show positive number required error when trying to save with -1 for index priority', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); setPhaseAfter(rendered, 'cold', '1'); - setPhaseIndexPriority(rendered, 'cold', '-1'); - save(rendered); + setPhaseIndexPriorityLegacy(rendered, 'cold', '-1'); + await save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); test('should show default allocation warning when no node roles are found', async () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -638,9 +707,10 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -650,9 +720,10 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -662,21 +733,121 @@ describe('edit policy', () => { describe('delete phase', () => { test('should allow 0 for phase timing', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'delete'); setPhaseAfter(rendered, 'delete', '0'); - save(rendered); + await save(rendered); expectedErrorMessages(rendered, []); }); test('should show positive number required error when trying to save delete phase with -1 for after', async () => { const rendered = mountWithIntl(component); - noRollover(rendered); + await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'delete'); setPhaseAfter(rendered, 'delete', '-1'); - save(rendered); + await save(rendered); 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); + await 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); + await 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); + await 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); + await 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/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index f7c1b810ecbc..152c5e4e9e0d 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -107,10 +107,9 @@ export interface ForcemergeAction { index_codec?: 'best_compression'; } -export interface Policy { +export interface LegacyPolicy { name: string; phases: { - hot: HotPhase; warm: WarmPhase; cold: ColdPhase; delete: DeletePhase; @@ -155,18 +154,6 @@ export interface PhaseWithForcemergeAction { bestCompressionEnabled: boolean; } -export interface HotPhase - extends CommonPhaseSettings, - PhaseWithIndexPriority, - PhaseWithForcemergeAction { - rolloverEnabled: boolean; - selectedMaxSizeStored: string; - selectedMaxSizeStoredUnits: string; - selectedMaxDocuments: string; - selectedMaxAge: string; - selectedMaxAgeUnits: string; -} - export interface WarmPhase extends CommonPhaseSettings, PhaseWithMinAge, 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/constants/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts index 61c197f2ba14..53f6b30a01c3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts @@ -5,4 +5,5 @@ */ export * from './policy'; + export * from './ui_metric'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index 1cd81b55ea17..c919331082ec 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -8,22 +8,24 @@ import { SerializedPhase, ColdPhase, DeletePhase, - HotPhase, WarmPhase, + SerializedPolicy, } from '../../../common/types'; -export const defaultNewHotPhase: HotPhase = { - phaseEnabled: true, - rolloverEnabled: true, - selectedMaxAge: '30', - selectedMaxAgeUnits: 'd', - selectedMaxSizeStored: '50', - selectedMaxSizeStoredUnits: 'gb', - forceMergeEnabled: false, - selectedForceMergeSegments: '', - bestCompressionEnabled: false, - phaseIndexPriority: '100', - selectedMaxDocuments: '', +export const defaultSetPriority: string = '100'; + +export const defaultPolicy: SerializedPolicy = { + name: '', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + }, + }, + }, }; export const defaultNewWarmPhase: WarmPhase = { 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/components/forcemerge.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx index c079c7a19ce9..0b0dbe273c02 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * PLEASE NOTE: This component is currently duplicated. A version of this component wired up with + * the form lib lives in ./phases/shared + */ + import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx index 9db40ebf5521..ed7ca6041767 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx @@ -9,7 +9,7 @@ import { EuiFormRow, EuiFormRowProps } from '@elastic/eui'; type Props = EuiFormRowProps & { isShowingErrors: boolean; - errors?: string[]; + errors?: string | string[] | null; }; export const ErrableFormRow: React.FunctionComponent = ({ @@ -18,8 +18,13 @@ export const ErrableFormRow: React.FunctionComponent = ({ children, ...rest }) => { + const _errors = errors ? (Array.isArray(errors) ? errors : [errors]) : undefined; return ( - 0} error={errors} {...rest}> + 0)} + error={errors} + {...rest} + > {Children.map(children, (child) => cloneElement(child as ReactElement, { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index c39545112ee5..04d9a6ef3cf6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -22,3 +22,5 @@ export { } from './data_tier_allocation'; export { DescribedFormField } from './described_form_field'; export { Forcemerge } from './forcemerge'; + +export * from './phases'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx index 2e70ef255524..6fcf35b79928 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx @@ -212,7 +212,7 @@ export const MinAgeInput = ({ { + onChange={(e) => { setPhaseData(selectedMinimumAgeProperty, e.target.value); }} min={0} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx similarity index 95% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx index b9b1b8b663ec..7ed8a94403a9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx @@ -10,8 +10,11 @@ import { i18n } from '@kbn/i18n'; import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui'; -import { ColdPhase as ColdPhaseInterface, Phases } from '../../../../../common/types'; -import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; +import { ColdPhase as ColdPhaseInterface, Phases } from '../../../../../../common/types'; + +import { useFormData } from '../../../../../shared_imports'; + +import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; import { LearnMoreLink, @@ -22,9 +25,9 @@ import { SetPriorityInput, MinAgeInput, DescribedFormField, -} from '../components'; +} from '../'; -import { DataTierAllocationField } from './shared'; +import { DataTierAllocationField, useRolloverPath } from './shared'; const i18nTexts = { freezeLabel: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { @@ -46,15 +49,17 @@ interface Props { phaseData: ColdPhaseInterface; isShowingErrors: boolean; errors?: PhaseValidationErrors; - hotPhaseRolloverEnabled: boolean; } export const ColdPhase: FunctionComponent = ({ setPhaseData, phaseData, errors, isShowingErrors, - hotPhaseRolloverEnabled, }) => { + const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({ + watch: [useRolloverPath], + }); + return (
<> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx new file mode 100644 index 000000000000..59e4738657be --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx @@ -0,0 +1,155 @@ +/* + * 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, { FunctionComponent, Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; + +import { DeletePhase as DeletePhaseInterface, Phases } from '../../../../../../common/types'; + +import { useFormData } from '../../../../../shared_imports'; + +import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; + +import { + ActiveBadge, + LearnMoreLink, + OptionalLabel, + PhaseErrorMessage, + MinAgeInput, + SnapshotPolicies, +} from '../'; +import { useRolloverPath } from './shared'; + +const deleteProperty: keyof Phases = 'delete'; +const phaseProperty = (propertyName: keyof DeletePhaseInterface) => propertyName; + +interface Props { + setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void; + phaseData: DeletePhaseInterface; + isShowingErrors: boolean; + errors?: PhaseValidationErrors; + getUrlForApp: ( + appId: string, + options?: { + path?: string; + absolute?: boolean; + } + ) => string; +} + +export const DeletePhase: FunctionComponent = ({ + setPhaseData, + phaseData, + errors, + isShowingErrors, + getUrlForApp, +}) => { + const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({ + watch: [useRolloverPath], + }); + + return ( +
+ +

+ +

{' '} + {phaseData.phaseEnabled && !isShowingErrors ? : null} + +
+ } + titleSize="s" + description={ + +

+ +

+ + } + id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} + onChange={(e) => { + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); + }} + aria-controls="deletePhaseContent" + /> +
+ } + fullWidth + > + {phaseData.phaseEnabled ? ( + + errors={errors} + phaseData={phaseData} + phase={deleteProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + rolloverEnabled={hotPhaseRolloverEnabled} + /> + ) : ( +
+ )} + + {phaseData.phaseEnabled ? ( + + + + } + description={ + + {' '} + + + } + titleSize="xs" + fullWidth + > + + + + + } + > + setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)} + getUrlForApp={getUrlForApp} + /> + + + ) : null} +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/constants.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/constants.ts new file mode 100644 index 000000000000..e438fa470c0c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/constants.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const maxSizeStoredUnits = [ + { + value: 'gb', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.gigabytesLabel', { + defaultMessage: 'gigabytes', + }), + }, + { + value: 'mb', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.megabytesLabel', { + defaultMessage: 'megabytes', + }), + }, + { + value: 'b', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.bytesLabel', { + defaultMessage: 'bytes', + }), + }, + { + value: 'kb', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.kilobytesLabel', { + defaultMessage: 'kilobytes', + }), + }, + { + value: 'tb', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.terabytesLabel', { + defaultMessage: 'terabytes', + }), + }, + { + value: 'pb', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.petabytesLabel', { + defaultMessage: 'petabytes', + }), + }, +]; + +export const maxAgeUnits = [ + { + value: 'd', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.daysLabel', { + defaultMessage: 'days', + }), + }, + { + value: 'h', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.hoursLabel', { + defaultMessage: 'hours', + }), + }, + { + value: 'm', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.minutesLabel', { + defaultMessage: 'minutes', + }), + }, + { + value: 's', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.secondsLabel', { + defaultMessage: 'seconds', + }), + }, + { + value: 'ms', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.millisecondsLabel', { + defaultMessage: 'milliseconds', + }), + }, + { + value: 'micros', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.microsecondsLabel', { + defaultMessage: 'microseconds', + }), + }, + { + value: 'nanos', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.nanosecondsLabel', { + defaultMessage: 'nanoseconds', + }), + }, +]; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx new file mode 100644 index 000000000000..ae84c0afe975 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -0,0 +1,235 @@ +/* + * 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, { Fragment, FunctionComponent, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiDescribedFormGroup, + EuiCallOut, +} from '@elastic/eui'; + +import { Phases } from '../../../../../../../common/types'; + +import { + useFormContext, + useFormData, + UseField, + SelectField, + ToggleField, + NumericField, +} from '../../../../../../shared_imports'; + +import { ROLLOVER_EMPTY_VALIDATION } from '../../../form_validations'; + +import { ROLLOVER_FORM_PATHS } from '../../../constants'; + +import { LearnMoreLink, ActiveBadge, PhaseErrorMessage } from '../../'; + +import { Forcemerge, SetPriorityInput } from '../shared'; + +import { maxSizeStoredUnits, maxAgeUnits } from './constants'; + +import { i18nTexts } from './i18n_texts'; + +import { useRolloverPath } from '../shared'; + +const hotProperty: keyof Phases = 'hot'; + +export const HotPhase: FunctionComponent<{ setWarmPhaseOnRollover: (v: boolean) => void }> = ({ + setWarmPhaseOnRollover, +}) => { + const [{ [useRolloverPath]: isRolloverEnabled }] = useFormData({ watch: [useRolloverPath] }); + const form = useFormContext(); + + const isShowingErrors = form.isValid === false; + const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false); + + useEffect(() => { + setWarmPhaseOnRollover(isRolloverEnabled ?? false); + }, [setWarmPhaseOnRollover, isRolloverEnabled]); + + return ( + <> + +

+ +

{' '} + {isShowingErrors ? null : } + +
+ } + titleSize="s" + description={ + +

+ +

+
+ } + fullWidth + > + + key="_meta.hot.useRollover" + path="_meta.hot.useRollover" + component={ToggleField} + componentProps={{ + hasEmptyLabelSpace: true, + fullWidth: false, + helpText: ( + <> +

+ +

+ + } + docPath="indices-rollover-index.html" + /> + + + ), + euiFieldProps: { + 'data-test-subj': 'rolloverSwitch', + }, + }} + /> + {isRolloverEnabled && ( + <> + + {showEmptyRolloverFieldsError && ( + <> + +
{i18nTexts.rollOverConfigurationCallout.body}
+
+ + + )} + + + + {(field) => { + const showErrorCallout = field.errors.some( + (e) => e.validationType === ROLLOVER_EMPTY_VALIDATION + ); + if (showErrorCallout !== showEmptyRolloverFieldsError) { + setShowEmptyRolloverFieldsError(showErrorCallout); + } + return ( + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + )} + + {isRolloverEnabled && } + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/i18n_texts.ts new file mode 100644 index 000000000000..6423b12b86dd --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/i18n_texts.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const i18nTexts = { + maximumAgeRequiredMessage: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError', + { + defaultMessage: 'A maximum age is required.', + } + ), + maximumSizeRequiredMessage: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError', + { + defaultMessage: 'A maximum index size is required.', + } + ), + maximumDocumentsRequiredMessage: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError', + { + defaultMessage: 'Maximum documents is required.', + } + ), + rollOverConfigurationCallout: { + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.rolloverConfigurationError.title', { + defaultMessage: 'Invalid rollover configuration', + }), + body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.rolloverConfigurationError.body', { + defaultMessage: + 'A value for one of maximum size, maximum documents, or maximum age is required.', + }), + }, +}; diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/index.ts similarity index 85% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/index.ts index abf060aca8c0..325e35f98593 100644 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { default } from './jquery_flot'; +export { HotPhase } from './hot_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts similarity index 99% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts index 8d1ace595049..076c16e87e8d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts @@ -5,6 +5,9 @@ */ export { HotPhase } from './hot_phase'; + export { WarmPhase } from './warm_phase'; + export { ColdPhase } from './cold_phase'; + export { DeletePhase } from './delete_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx new file mode 100644 index 000000000000..de7f321e5f15 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx @@ -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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +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'; +import { isNodeRoleFirstPreference } from '../../../../../lib/data_tiers/is_node_role_first_preference'; + +import { + DataTierAllocation, + DefaultAllocationNotice, + NoNodeAttributesWarning, + NodesDataProvider, + CloudDataTierCallout, +} from '../../data_tier_allocation'; + +const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', { + defaultMessage: 'Data allocation', + }), +}; + +interface Props { + description: React.ReactNode; + phase: PhaseWithAllocation; + setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void; + isShowingErrors: boolean; + errors?: PhaseValidationErrors; + phaseData: PhaseWithAllocationAction; +} + +/** + * Top-level layout control for the data tier allocation field. + */ +export const DataTierAllocationField: FunctionComponent = ({ + description, + phase, + phaseData, + setPhaseData, + isShowingErrors, + errors, +}) => { + const { + services: { cloud }, + } = useKibana(); + + return ( + + {({ nodesByRoles, nodesByAttributes, isUsingDeprecatedDataRoleConfig }) => { + const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); + + 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 ( + <> + + + + ); + } + + const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles); + if ( + allocationNodeRole === 'none' || + !isNodeRoleFirstPreference(phase, allocationNodeRole) + ) { + return ( + <> + + + + ); + } + break; + case 'custom': + if (!hasNodeAttrs) { + return ( + <> + + + + ); + } + break; + default: + return null; + } + }; + + return ( + {i18nTexts.title}} + description={description} + fullWidth + > + + <> + + + {/* Data tier related warnings and call-to-action notices */} + {renderNotice()} + + + + ); + }} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx new file mode 100644 index 000000000000..987133fd652a --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx @@ -0,0 +1,95 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiDescribedFormGroup, EuiSpacer, EuiTextColor } from '@elastic/eui'; +import React from 'react'; + +import { Phases } from '../../../../../../../common/types'; + +import { UseField, ToggleField, NumericField, useFormData } from '../../../../../../shared_imports'; + +import { i18nTexts } from '../../../i18n_texts'; + +import { LearnMoreLink } from '../../'; + +interface Props { + phase: keyof Phases & string; +} + +const forceMergeEnabledPath = '_meta.hot.forceMergeEnabled'; + +export const Forcemerge: React.FunctionComponent = ({ phase }) => { + const [{ [forceMergeEnabledPath]: forceMergeEnabled }] = useFormData({ + watch: [forceMergeEnabledPath], + }); + return ( + + + + } + description={ + + {' '} + + + } + titleSize="xs" + fullWidth + > + + +
+ {forceMergeEnabled && ( + <> + + + + )} +
+
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts new file mode 100644 index 000000000000..3b94d36a977d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { useRolloverPath } from '../../../constants'; + +export { DataTierAllocationField } from './data_tier_allocation_field'; + +export { Forcemerge } from './forcemerge_field'; + +export { SetPriorityInput } from './set_priority_input'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/set_priority_input.tsx new file mode 100644 index 000000000000..0f7ca4d52f88 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/set_priority_input.tsx @@ -0,0 +1,62 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui'; + +import { PhaseWithIndexPriority, Phases } from '../../../../../../../common/types'; + +import { UseField, NumericField } from '../../../../../../shared_imports'; + +import { propertyof } from '../../../../../services/policies/policy_validation'; + +import { LearnMoreLink } from '../../'; + +interface Props { + phase: keyof Phases & string; +} + +export const SetPriorityInput: FunctionComponent = ({ phase }) => { + const phaseIndexPriorityProperty = propertyof('phaseIndexPriority'); + return ( + + + + } + description={ + + {' '} + + + } + titleSize="xs" + fullWidth + > + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase.tsx similarity index 96% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase.tsx index b837eed1256c..bd0b380bdc17 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase.tsx @@ -18,8 +18,12 @@ import { EuiDescribedFormGroup, } from '@elastic/eui'; -import { Phases, WarmPhase as WarmPhaseInterface } from '../../../../../common/types'; -import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; +import { useFormData } from '../../../../../shared_imports'; +import { Phases, WarmPhase as WarmPhaseInterface } from '../../../../../../common/types'; +import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; + +import { useRolloverPath } from './shared'; + import { LearnMoreLink, ActiveBadge, @@ -30,7 +34,8 @@ import { MinAgeInput, DescribedFormField, Forcemerge, -} from '../components'; +} from '../'; + import { DataTierAllocationField } from './shared'; const i18nTexts = { @@ -61,15 +66,16 @@ interface Props { phaseData: WarmPhaseInterface; isShowingErrors: boolean; errors?: PhaseValidationErrors; - hotPhaseRolloverEnabled: boolean; } export const WarmPhase: FunctionComponent = ({ setPhaseData, phaseData, errors, isShowingErrors, - hotPhaseRolloverEnabled, }) => { + const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({ + watch: [useRolloverPath], + }); return (
<> @@ -132,7 +138,7 @@ export const WarmPhase: FunctionComponent = ({ /> ) : null} - {!phaseData.warmPhaseOnRollover ? ( + {!phaseData.warmPhaseOnRollover || !hotPhaseRolloverEnabled ? ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx index 98d2409ffea6..e9ce193118b3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -17,36 +18,108 @@ import { EuiSpacer, EuiText, EuiTitle, + EuiCallOut, + EuiLoadingSpinner, } from '@elastic/eui'; -import { Policy, PolicyFromES } from '../../../../../common/types'; -import { serializePolicy } from '../../../services/policies/policy_serialization'; + +import { SerializedPolicy } from '../../../../../common/types'; + +import { useFormContext, useFormData } from '../../../../shared_imports'; interface Props { + legacyPolicy: SerializedPolicy; close: () => void; - policy: Policy; - existingPolicy?: PolicyFromES; policyName: string; } export const PolicyJsonFlyout: React.FunctionComponent = ({ - close, - policy, policyName, - existingPolicy, + close, + legacyPolicy, }) => { - const { phases } = serializePolicy(policy, existingPolicy?.policy); - const json = JSON.stringify( - { - policy: { - phases, - }, - }, - null, - 2 - ); + /** + * policy === undefined: we are checking validity + * policy === null: we have determined the policy is invalid + * policy === {@link SerializedPolicy} we have determined the policy is valid + */ + const [policy, setPolicy] = useState(undefined); + + const form = useFormContext(); + const [formData, getFormData] = useFormData(); + + useEffect(() => { + (async function checkPolicy() { + setPolicy(undefined); + if (await form.validate()) { + const p = getFormData() as SerializedPolicy; + setPolicy({ + ...legacyPolicy, + phases: { + ...legacyPolicy.phases, + hot: p.phases.hot, + }, + }); + } else { + setPolicy(null); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [form, legacyPolicy, formData]); - const endpoint = `PUT _ilm/policy/${policyName || ''}`; - const request = `${endpoint}\n${json}`; + let content: React.ReactNode; + switch (policy) { + case undefined: + content = ; + break; + case null: + content = ( + + {i18n.translate('xpack.indexLifecycleMgmt.policyJsonFlyout.validationErrorCallout.body', { + defaultMessage: 'To view the JSON for this policy address all validation errors.', + })} + + ); + break; + default: + const { phases } = policy; + + const json = JSON.stringify( + { + policy: { + phases, + }, + }, + null, + 2 + ); + + const endpoint = `PUT _ilm/policy/${policyName || ''}`; + const request = `${endpoint}\n${json}`; + content = ( + <> + +

+ +

+
+ + + {request} + + + ); + break; + } return ( @@ -69,22 +142,7 @@ export const PolicyJsonFlyout: React.FunctionComponent = ({ - - -

- -

-
- - - - - {request} - -
+ {content} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx index 7f839fc94918..5efbfabdf093 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx @@ -3,6 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +/** + * PLEASE NOTE: This component is currently duplicated. A version of this component wired up with + * the form lib lives in ./phases/shared + */ + import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts new file mode 100644 index 000000000000..a5d5f1c62847 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const useRolloverPath = '_meta.hot.useRollover'; + +/** + * These strings describe the path to their respective values in the serialized + * ILM form. + */ +export const ROLLOVER_FORM_PATHS = { + maxDocs: 'phases.hot.actions.rollover.max_docs', + maxAge: 'phases.hot.actions.rollover.max_age', + maxSize: 'phases.hot.actions.rollover.max_size', +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts new file mode 100644 index 000000000000..bb24eea64ec8 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { produce } from 'immer'; + +import { SerializedPolicy } from '../../../../common/types'; + +import { splitSizeAndUnits } from '../../services/policies/policy_serialization'; + +import { FormInternal } from './types'; + +export const deserializer = (policy: SerializedPolicy): FormInternal => + produce( + { + ...policy, + _meta: { + hot: { + useRollover: Boolean(policy.phases.hot?.actions?.rollover), + forceMergeEnabled: Boolean(policy.phases.hot?.actions?.forcemerge), + bestCompression: + policy.phases.hot?.actions?.forcemerge?.index_codec === 'best_compression', + }, + }, + }, + (draft) => { + if (draft.phases.hot?.actions?.rollover) { + if (draft.phases.hot.actions.rollover.max_size) { + const maxSize = splitSizeAndUnits(draft.phases.hot.actions.rollover.max_size); + draft.phases.hot.actions.rollover.max_size = maxSize.size; + draft._meta.hot.maxStorageSizeUnit = maxSize.units; + } + + if (draft.phases.hot.actions.rollover.max_age) { + const maxAge = splitSizeAndUnits(draft.phases.hot.actions.rollover.max_age); + draft.phases.hot.actions.rollover.max_age = maxAge.size; + draft._meta.hot.maxAgeUnit = maxAge.units; + } + } + } + ); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index 67e8e42cf6fd..8f8b0447f378 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useEffect, useState, useCallback } from 'react'; +import React, { Fragment, useEffect, useState, useCallback, useMemo } from 'react'; + import { RouteComponentProps } from 'react-router-dom'; + import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -27,23 +29,43 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; + +import { useForm, Form } from '../../../shared_imports'; + import { toasts } from '../../services/notification'; -import { Phases, Policy, PolicyFromES } from '../../../../common/types'; +import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types'; + +import { defaultPolicy } from '../../constants'; + import { validatePolicy, ValidationErrors, findFirstError, } from '../../services/policies/policy_validation'; + import { savePolicy } from '../../services/policies/policy_save'; + import { deserializePolicy, getPolicyByName, initializeNewPolicy, + legacySerializePolicy, } from '../../services/policies/policy_serialization'; -import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components'; -import { ColdPhase, DeletePhase, HotPhase, WarmPhase } from './phases'; +import { + ErrableFormRow, + LearnMoreLink, + PolicyJsonFlyout, + ColdPhase, + DeletePhase, + HotPhase, + WarmPhase, +} from './components'; + +import { schema } from './form_schema'; +import { deserializer } from './deserializer'; +import { createSerializer } from './serializer'; export interface Props { policies: PolicyFromES[]; @@ -57,6 +79,20 @@ export interface Props { ) => string; history: RouteComponentProps['history']; } + +const mergeAllSerializedPolicies = ( + serializedPolicy: SerializedPolicy, + legacySerializedPolicy: SerializedPolicy +): SerializedPolicy => { + return { + ...legacySerializedPolicy, + phases: { + ...legacySerializedPolicy.phases, + hot: serializedPolicy.phases.hot, + }, + }; +}; + export const EditPolicy: React.FunctionComponent = ({ policies, policyName, @@ -73,7 +109,18 @@ export const EditPolicy: React.FunctionComponent = ({ const existingPolicy = getPolicyByName(policies, policyName); - const [policy, setPolicy] = useState( + const serializer = useMemo(() => { + return createSerializer(existingPolicy?.policy); + }, [existingPolicy?.policy]); + + const { form } = useForm({ + schema, + defaultValue: existingPolicy?.policy ?? defaultPolicy, + deserializer, + serializer, + }); + + const [policy, setPolicy] = useState(() => existingPolicy ? deserializePolicy(existingPolicy) : initializeNewPolicy(policyName) ); @@ -85,9 +132,26 @@ export const EditPolicy: React.FunctionComponent = ({ history.push('/policies'); }; + const setWarmPhaseOnRollover = useCallback( + (value: boolean) => { + setPolicy((p) => ({ + ...p, + phases: { + ...p.phases, + warm: { + ...p.phases.warm, + warmPhaseOnRollover: value, + }, + }, + })); + }, + [setPolicy] + ); + const submit = async () => { setIsShowingErrors(true); - const [isValid, validationErrors] = validatePolicy( + const { data: formLibPolicy, isValid: newIsValid } = await form.submit(); + const [legacyIsValid, validationErrors] = validatePolicy( saveAsNew, policy, policies, @@ -95,20 +159,30 @@ export const EditPolicy: React.FunctionComponent = ({ ); setErrors(validationErrors); + const isValid = legacyIsValid && newIsValid; + if (!isValid) { toasts.addDanger( i18n.translate('xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage', { defaultMessage: 'Please fix the errors on this page.', }) ); - const firstError = findFirstError(validationErrors); - const errorRowId = `${firstError ? firstError.replace('.', '-') : ''}-row`; - const element = document.getElementById(errorRowId); - if (element) { - element.scrollIntoView({ block: 'center', inline: 'nearest' }); + // This functionality will not be required for once form lib is fully adopted for this form + // because errors are reported as fields are edited. + if (!legacyIsValid) { + const firstError = findFirstError(validationErrors); + const errorRowId = `${firstError ? firstError.replace('.', '-') : ''}-row`; + const element = document.getElementById(errorRowId); + if (element) { + element.scrollIntoView({ block: 'center', inline: 'nearest' }); + } } } else { - const success = await savePolicy(policy, isNewPolicy || saveAsNew, existingPolicy); + const readSerializedPolicy = () => { + const legacySerializedPolicy = legacySerializePolicy(policy, existingPolicy?.policy); + return mergeAllSerializedPolicies(formLibPolicy, legacySerializedPolicy); + }; + const success = await savePolicy(readSerializedPolicy, isNewPolicy || saveAsNew); if (success) { backToPolicyList(); } @@ -120,7 +194,7 @@ export const EditPolicy: React.FunctionComponent = ({ }; const setPhaseData = useCallback( - (phase: keyof Phases, key: string, value: any) => { + (phase: keyof LegacyPolicy['phases'], key: string, value: any) => { setPolicy((nextPolicy) => ({ ...nextPolicy, phases: { @@ -132,10 +206,6 @@ export const EditPolicy: React.FunctionComponent = ({ [setPolicy] ); - const setHotPhaseData = useCallback( - (key: string, value: any) => setPhaseData('hot', key, value), - [setPhaseData] - ); const setWarmPhaseData = useCallback( (key: string, value: any) => setPhaseData('warm', key, value), [setPhaseData] @@ -149,23 +219,6 @@ export const EditPolicy: React.FunctionComponent = ({ [setPhaseData] ); - const setWarmPhaseOnRollover = (value: boolean) => { - setPolicy({ - ...policy, - phases: { - ...policy.phases, - hot: { - ...policy.phases.hot, - rolloverEnabled: value, - }, - warm: { - ...policy.phases.warm, - warmPhaseOnRollover: value, - }, - }, - }); - }; - return ( @@ -188,215 +241,210 @@ export const EditPolicy: React.FunctionComponent = ({
- - -

- + +

+ {' '} - - } - /> -

-
+ />{' '} + + } + /> +

+ - + - {isNewPolicy ? null : ( - - -

- + {isNewPolicy ? null : ( + + +

+ + + + .{' '} - - .{' '} - +

+
+ + + + { + setSaveAsNew(e.target.checked); + }} + label={ + + + + } /> -

- - - - - { - setSaveAsNew(e.target.checked); - }} - label={ - + +
+ )} + + {saveAsNew || isNewPolicy ? ( + + - } - /> - - - )} - - {saveAsNew || isNewPolicy ? ( - - +
+ } + titleSize="s" + fullWidth + > + - -
- } - titleSize="s" - fullWidth - > - + { + setPolicy({ ...policy, name: e.target.value }); + }} /> - } - > - { - setPolicy({ ...policy, name: e.target.value }); - }} - /> - - - ) : null} - - - - 0} - setPhaseData={setHotPhaseData} - phaseData={policy.phases.hot} - setWarmPhaseOnRollover={setWarmPhaseOnRollover} - /> - - - - 0} - setPhaseData={setWarmPhaseData} - phaseData={policy.phases.warm} - hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} - /> - - - - 0} - setPhaseData={setColdPhaseData} - phaseData={policy.phases.cold} - hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} - /> - - - - 0} - getUrlForApp={getUrlForApp} - setPhaseData={setDeletePhaseData} - phaseData={policy.phases.delete} - hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} - /> - - - - - - - - - {saveAsNew ? ( - - ) : ( + + + ) : null} + + + + + + + + 0} + setPhaseData={setWarmPhaseData} + phaseData={policy.phases.warm} + /> + + + + 0} + setPhaseData={setColdPhaseData} + phaseData={policy.phases.cold} + /> + + + + 0 + } + getUrlForApp={getUrlForApp} + setPhaseData={setDeletePhaseData} + phaseData={policy.phases.delete} + /> + + + + + + + + + {saveAsNew ? ( + + ) : ( + + )} + + + + + - )} - - - - - + + + + + + + + {isShowingPolicyJsonFlyout ? ( - - - - - - - - {isShowingPolicyJsonFlyout ? ( - - ) : ( - - )} - - - - - {isShowingPolicyJsonFlyout ? ( - setIsShowingPolicyJsonFlyout(false)} - /> - ) : null} + ) : ( + + )} + + + + + {isShowingPolicyJsonFlyout ? ( + setIsShowingPolicyJsonFlyout(false)} + /> + ) : null} +
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts new file mode 100644 index 000000000000..806164c8b0da --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts @@ -0,0 +1,129 @@ +/* + * 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 { FormSchema, fieldValidators } from '../../../shared_imports'; +import { defaultSetPriority } from '../../constants'; + +import { FormInternal } from './types'; +import { ifExistsNumberGreaterThanZero, rolloverThresholdsValidator } from './form_validations'; +import { i18nTexts } from './i18n_texts'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + _meta: { + hot: { + useRollover: { + defaultValue: true, + label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', { + defaultMessage: 'Enable rollover', + }), + }, + maxStorageSizeUnit: { + defaultValue: 'gb', + }, + maxAgeUnit: { + defaultValue: 'd', + }, + forceMergeEnabled: { + label: i18nTexts.editPolicy.forceMergeEnabledFieldLabel, + }, + bestCompression: { + label: i18n.translate('xpack.indexLifecycleMgmt.forcemerge.bestCompressionLabel', { + defaultMessage: 'Compress stored fields', + }), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.forceMerge.bestCompressionText', + { + defaultMessage: + 'Use higher compression for stored fields at the cost of slower performance.', + } + ), + }, + }, + }, + phases: { + hot: { + actions: { + rollover: { + max_age: { + label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumAgeLabel', { + defaultMessage: 'Maximum age', + }), + validations: [ + { + validator: rolloverThresholdsValidator, + }, + { + validator: ifExistsNumberGreaterThanZero, + }, + ], + }, + max_docs: { + label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumDocumentsLabel', { + defaultMessage: 'Maximum documents', + }), + validations: [ + { + validator: rolloverThresholdsValidator, + }, + { + validator: ifExistsNumberGreaterThanZero, + }, + ], + serializer: (v: string): any => (v ? parseInt(v, 10) : undefined), + }, + max_size: { + label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeLabel', { + defaultMessage: 'Maximum index size', + }), + validations: [ + { + validator: rolloverThresholdsValidator, + }, + { + validator: ifExistsNumberGreaterThanZero, + }, + ], + }, + }, + forcemerge: { + max_num_segments: { + label: i18n.translate('xpack.indexLifecycleMgmt.forceMerge.numberOfSegmentsLabel', { + defaultMessage: 'Number of segments', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError', + { defaultMessage: 'A value for number of segments is required.' } + ) + ), + }, + { + validator: ifExistsNumberGreaterThanZero, + }, + ], + serializer: (v: string): any => (v ? parseInt(v, 10) : undefined), + }, + }, + set_priority: { + priority: { + defaultValue: defaultSetPriority as any, + label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.indexPriorityLabel', { + defaultMessage: 'Index priority (optional)', + }), + validations: [{ validator: ifExistsNumberGreaterThanZero }], + serializer: (v: string): any => (v ? parseInt(v, 10) : undefined), + }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts new file mode 100644 index 000000000000..b937ea204313 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { fieldValidators, ValidationFunc } from '../../../shared_imports'; + +import { i18nTexts } from './components/phases/hot_phase/i18n_texts'; + +import { ROLLOVER_FORM_PATHS } from './constants'; + +const { numberGreaterThanField } = fieldValidators; + +export const positiveNumberRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.numberAboveZeroRequiredError', + { + defaultMessage: 'Only numbers above 0 are allowed.', + } +); + +export const ifExistsNumberGreaterThanZero: ValidationFunc = (arg) => { + if (arg.value) { + return numberGreaterThanField({ + than: 0, + message: positiveNumberRequiredMessage, + })({ + ...arg, + value: parseInt(arg.value, 10), + }); + } +}; + +/** + * A special validation type used to keep track of validation errors for + * the rollover threshold values not being set (e.g., age and doc count) + */ +export const ROLLOVER_EMPTY_VALIDATION = 'EMPTY'; + +/** + * An ILM policy requires that for rollover a value must be set for one of the threshold values. + * + * This validator checks that and updates form values by setting errors states imperatively to + * indicate this error state. + */ +export const rolloverThresholdsValidator: ValidationFunc = ({ form }) => { + const fields = form.getFields(); + if ( + !( + fields[ROLLOVER_FORM_PATHS.maxAge].value || + fields[ROLLOVER_FORM_PATHS.maxDocs].value || + fields[ROLLOVER_FORM_PATHS.maxSize].value + ) + ) { + fields[ROLLOVER_FORM_PATHS.maxAge].setErrors([ + { validationType: ROLLOVER_EMPTY_VALIDATION, message: i18nTexts.maximumAgeRequiredMessage }, + ]); + fields[ROLLOVER_FORM_PATHS.maxDocs].setErrors([ + { + validationType: ROLLOVER_EMPTY_VALIDATION, + message: i18nTexts.maximumDocumentsRequiredMessage, + }, + ]); + fields[ROLLOVER_FORM_PATHS.maxSize].setErrors([ + { validationType: ROLLOVER_EMPTY_VALIDATION, message: i18nTexts.maximumSizeRequiredMessage }, + ]); + } else { + fields[ROLLOVER_FORM_PATHS.maxAge].clearErrors(ROLLOVER_EMPTY_VALIDATION); + fields[ROLLOVER_FORM_PATHS.maxDocs].clearErrors(ROLLOVER_EMPTY_VALIDATION); + fields[ROLLOVER_FORM_PATHS.maxSize].clearErrors(ROLLOVER_EMPTY_VALIDATION); + } +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts new file mode 100644 index 000000000000..31bb10b402d2 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const i18nTexts = { + editPolicy: { + forceMergeEnabledFieldLabel: i18n.translate('xpack.indexLifecycleMgmt.forcemerge.enableLabel', { + defaultMessage: 'Force merge data', + }), + }, +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx deleted file mode 100644 index 11adebdd094b..000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx +++ /dev/null @@ -1,153 +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, { PureComponent, Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; - -import { DeletePhase as DeletePhaseInterface, Phases } from '../../../../../common/types'; -import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; - -import { - ActiveBadge, - LearnMoreLink, - OptionalLabel, - PhaseErrorMessage, - MinAgeInput, - SnapshotPolicies, -} from '../components'; - -const deleteProperty: keyof Phases = 'delete'; -const phaseProperty = (propertyName: keyof DeletePhaseInterface) => propertyName; - -interface Props { - setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void; - phaseData: DeletePhaseInterface; - isShowingErrors: boolean; - errors?: PhaseValidationErrors; - hotPhaseRolloverEnabled: boolean; - getUrlForApp: ( - appId: string, - options?: { - path?: string; - absolute?: boolean; - } - ) => string; -} - -export class DeletePhase extends PureComponent { - render() { - const { - setPhaseData, - phaseData, - errors, - isShowingErrors, - hotPhaseRolloverEnabled, - getUrlForApp, - } = this.props; - - return ( -
- -

- -

{' '} - {phaseData.phaseEnabled && !isShowingErrors ? : null} - -
- } - titleSize="s" - description={ - -

- -

- - } - id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`} - checked={phaseData.phaseEnabled} - onChange={(e) => { - setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); - }} - aria-controls="deletePhaseContent" - /> -
- } - fullWidth - > - {phaseData.phaseEnabled ? ( - - errors={errors} - phaseData={phaseData} - phase={deleteProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - rolloverEnabled={hotPhaseRolloverEnabled} - /> - ) : ( -
- )} - - {phaseData.phaseEnabled ? ( - - - - } - description={ - - {' '} - - - } - titleSize="xs" - fullWidth - > - - - - - } - > - setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)} - getUrlForApp={getUrlForApp} - /> - - - ) : null} -
- ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx deleted file mode 100644 index 7682b9248808..000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx +++ /dev/null @@ -1,336 +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, { Fragment, PureComponent } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiFieldNumber, - EuiSelect, - EuiSwitch, - EuiFormRow, - EuiDescribedFormGroup, -} from '@elastic/eui'; - -import { HotPhase as HotPhaseInterface, Phases } from '../../../../../common/types'; -import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; - -import { - LearnMoreLink, - ActiveBadge, - PhaseErrorMessage, - ErrableFormRow, - SetPriorityInput, - Forcemerge, -} from '../components'; - -const maxSizeStoredUnits = [ - { - value: 'gb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.gigabytesLabel', { - defaultMessage: 'gigabytes', - }), - }, - { - value: 'mb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.megabytesLabel', { - defaultMessage: 'megabytes', - }), - }, - { - value: 'b', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.bytesLabel', { - defaultMessage: 'bytes', - }), - }, - { - value: 'kb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.kilobytesLabel', { - defaultMessage: 'kilobytes', - }), - }, - { - value: 'tb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.terabytesLabel', { - defaultMessage: 'terabytes', - }), - }, - { - value: 'pb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.petabytesLabel', { - defaultMessage: 'petabytes', - }), - }, -]; - -const maxAgeUnits = [ - { - value: 'd', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.daysLabel', { - defaultMessage: 'days', - }), - }, - { - value: 'h', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.hoursLabel', { - defaultMessage: 'hours', - }), - }, - { - value: 'm', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.minutesLabel', { - defaultMessage: 'minutes', - }), - }, - { - value: 's', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.secondsLabel', { - defaultMessage: 'seconds', - }), - }, - { - value: 'ms', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.millisecondsLabel', { - defaultMessage: 'milliseconds', - }), - }, - { - value: 'micros', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.microsecondsLabel', { - defaultMessage: 'microseconds', - }), - }, - { - value: 'nanos', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.nanosecondsLabel', { - defaultMessage: 'nanoseconds', - }), - }, -]; -const hotProperty: keyof Phases = 'hot'; -const phaseProperty = (propertyName: keyof HotPhaseInterface) => propertyName; - -interface Props { - errors?: PhaseValidationErrors; - isShowingErrors: boolean; - phaseData: HotPhaseInterface; - setPhaseData: (key: keyof HotPhaseInterface & string, value: string | boolean) => void; - setWarmPhaseOnRollover: (value: boolean) => void; -} - -export class HotPhase extends PureComponent { - render() { - const { setPhaseData, phaseData, isShowingErrors, errors, setWarmPhaseOnRollover } = this.props; - - return ( - - -

- -

{' '} - {isShowingErrors ? null : } - -
- } - titleSize="s" - description={ - -

- -

-
- } - fullWidth - > - -

- -

- - } - docPath="indices-rollover-index.html" - /> - - - } - > - { - setWarmPhaseOnRollover(e.target.checked); - }} - label={i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', { - defaultMessage: 'Enable rollover', - })} - /> -
- {phaseData.rolloverEnabled ? ( - - - - - - { - setPhaseData(phaseProperty('selectedMaxSizeStored'), e.target.value); - }} - min={1} - /> - - - - - { - setPhaseData(phaseProperty('selectedMaxSizeStoredUnits'), e.target.value); - }} - options={maxSizeStoredUnits} - /> - - - - - - - - { - setPhaseData(phaseProperty('selectedMaxDocuments'), e.target.value); - }} - min={1} - /> - - - - - - - - { - setPhaseData(phaseProperty('selectedMaxAge'), e.target.value); - }} - min={1} - /> - - - - - { - setPhaseData(phaseProperty('selectedMaxAgeUnits'), e.target.value); - }} - options={maxAgeUnits} - /> - - - - - ) : null} - - {phaseData.rolloverEnabled ? ( - - ) : null} - - errors={errors} - phaseData={phaseData} - phase={hotProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - /> - - ); - } -} 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 deleted file mode 100644 index 623d443a1db0..000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx +++ /dev/null @@ -1,108 +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, { FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; - -import { PhaseWithAllocationAction, PhaseWithAllocation } from '../../../../../../common/types'; -import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; -import { getAvailableNodeRoleForPhase } from '../../../../lib/data_tiers'; -import { isNodeRoleFirstPreference } from '../../../../lib/data_tiers/is_node_role_first_preference'; - -import { - DataTierAllocation, - DefaultAllocationNotice, - NoNodeAttributesWarning, - NodesDataProvider, -} from '../../components/data_tier_allocation'; - -const i18nTexts = { - title: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', { - defaultMessage: 'Data allocation', - }), -}; - -interface Props { - description: React.ReactNode; - phase: PhaseWithAllocation; - setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void; - isShowingErrors: boolean; - errors?: PhaseValidationErrors; - phaseData: PhaseWithAllocationAction; -} - -/** - * Top-level layout control for the data tier allocation field. - */ -export const DataTierAllocationField: FunctionComponent = ({ - description, - phase, - phaseData, - setPhaseData, - isShowingErrors, - errors, -}) => { - return ( - - {(nodesData) => { - const hasNodeAttrs = Boolean(Object.keys(nodesData.nodesByAttributes ?? {}).length); - - const renderDefaultAllocationNotice = () => { - if (phaseData.dataTierAllocationType !== 'default') { - return null; - } - - const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesData.nodesByRoles); - if ( - allocationNodeRole !== 'none' && - isNodeRoleFirstPreference(phase, allocationNodeRole) - ) { - return null; - } - - return ; - }; - - const renderNodeAttributesWarning = () => { - if (phaseData.dataTierAllocationType !== 'custom') { - return null; - } - if (hasNodeAttrs) { - return null; - } - return ; - }; - - return ( - {i18nTexts.title}} - description={description} - fullWidth - > - - <> - - - {/* Data tier related warnings */} - {renderDefaultAllocationNotice()} - {renderNodeAttributesWarning()} - - - - ); - }} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts new file mode 100644 index 000000000000..e0e1ad44f155 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SerializedPolicy } from '../../../../common/types'; + +import { FormInternal } from './types'; + +export const createSerializer = (originalPolicy?: SerializedPolicy) => ( + data: FormInternal +): SerializedPolicy => { + const { _meta, ...rest } = data; + + if (!rest.phases || !rest.phases.hot) { + rest.phases = { hot: { actions: {} } }; + } + + if (rest.phases.hot) { + rest.phases.hot.min_age = originalPolicy?.phases.hot?.min_age ?? '0ms'; + } + + if (rest.phases.hot?.actions) { + if (rest.phases.hot.actions?.rollover && _meta.hot.useRollover) { + if (rest.phases.hot.actions.rollover.max_age) { + rest.phases.hot.actions.rollover.max_age = `${rest.phases.hot.actions.rollover.max_age}${_meta.hot.maxAgeUnit}`; + } + + if (rest.phases.hot.actions.rollover.max_size) { + rest.phases.hot.actions.rollover.max_size = `${rest.phases.hot.actions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`; + } + + if (_meta.hot.bestCompression && rest.phases.hot.actions?.forcemerge) { + rest.phases.hot.actions.forcemerge.index_codec = 'best_compression'; + } + } else { + delete rest.phases.hot.actions?.rollover; + } + } + + return rest; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts new file mode 100644 index 000000000000..dba56eb8ecbf --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.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 { SerializedPolicy } from '../../../../common/types'; + +/** + * Describes the shape of data after deserialization. + */ +export interface FormInternal extends SerializedPolicy { + /** + * This is a special internal-only field that is used to display or hide + * certain form fields which affects what is ultimately serialized. + */ + _meta: { + hot: { + useRollover: boolean; + forceMergeEnabled: boolean; + bestCompression: boolean; + maxStorageSizeUnit?: string; + maxAgeUnit?: string; + }; + }; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts deleted file mode 100644 index 3bb9165c7d4f..000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts +++ /dev/null @@ -1,190 +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 { HotPhase, SerializedHotPhase } from '../../../../common/types'; -import { serializedPhaseInitialization } from '../../constants'; -import { isNumber, splitSizeAndUnits } from './policy_serialization'; -import { - maximumAgeRequiredMessage, - maximumDocumentsRequiredMessage, - maximumSizeRequiredMessage, - numberRequiredMessage, - PhaseValidationErrors, - positiveNumberRequiredMessage, - positiveNumbersAboveZeroErrorMessage, -} from './policy_validation'; - -const hotPhaseInitialization: HotPhase = { - phaseEnabled: false, - rolloverEnabled: false, - selectedMaxAge: '', - selectedMaxAgeUnits: 'd', - selectedMaxSizeStored: '', - selectedMaxSizeStoredUnits: 'gb', - forceMergeEnabled: false, - selectedForceMergeSegments: '', - bestCompressionEnabled: false, - phaseIndexPriority: '', - selectedMaxDocuments: '', -}; - -export const hotPhaseFromES = (phaseSerialized?: SerializedHotPhase): HotPhase => { - const phase: HotPhase = { ...hotPhaseInitialization }; - - if (phaseSerialized === undefined || phaseSerialized === null) { - return phase; - } - - phase.phaseEnabled = true; - - if (phaseSerialized.actions) { - const actions = phaseSerialized.actions; - - if (actions.rollover) { - const rollover = actions.rollover; - phase.rolloverEnabled = true; - if (rollover.max_age) { - const { size: maxAge, units: maxAgeUnits } = splitSizeAndUnits(rollover.max_age); - phase.selectedMaxAge = maxAge; - phase.selectedMaxAgeUnits = maxAgeUnits; - } - if (rollover.max_size) { - const { size: maxSize, units: maxSizeUnits } = splitSizeAndUnits(rollover.max_size); - phase.selectedMaxSizeStored = maxSize; - phase.selectedMaxSizeStoredUnits = maxSizeUnits; - } - if (rollover.max_docs) { - phase.selectedMaxDocuments = rollover.max_docs.toString(); - } - } - - if (actions.forcemerge) { - const forcemerge = actions.forcemerge; - phase.forceMergeEnabled = true; - phase.selectedForceMergeSegments = forcemerge.max_num_segments.toString(); - // only accepted value for index_codec - phase.bestCompressionEnabled = forcemerge.index_codec === 'best_compression'; - } - - if (actions.set_priority) { - phase.phaseIndexPriority = actions.set_priority.priority - ? actions.set_priority.priority.toString() - : ''; - } - } - - return phase; -}; - -export const hotPhaseToES = ( - phase: HotPhase, - originalPhase?: SerializedHotPhase -): SerializedHotPhase => { - if (!originalPhase) { - originalPhase = { ...serializedPhaseInitialization }; - } - - const esPhase = { ...originalPhase }; - - esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; - - if (phase.rolloverEnabled) { - if (!esPhase.actions.rollover) { - esPhase.actions.rollover = {}; - } - if (isNumber(phase.selectedMaxAge)) { - esPhase.actions.rollover.max_age = `${phase.selectedMaxAge}${phase.selectedMaxAgeUnits}`; - } - if (isNumber(phase.selectedMaxSizeStored)) { - esPhase.actions.rollover.max_size = `${phase.selectedMaxSizeStored}${phase.selectedMaxSizeStoredUnits}`; - } - if (isNumber(phase.selectedMaxDocuments)) { - esPhase.actions.rollover.max_docs = parseInt(phase.selectedMaxDocuments, 10); - } - if (phase.forceMergeEnabled && isNumber(phase.selectedForceMergeSegments)) { - esPhase.actions.forcemerge = { - max_num_segments: parseInt(phase.selectedForceMergeSegments, 10), - }; - if (phase.bestCompressionEnabled) { - // only accepted value for index_codec - esPhase.actions.forcemerge.index_codec = 'best_compression'; - } - } else { - delete esPhase.actions.forcemerge; - } - } else { - delete esPhase.actions.rollover; - // forcemerge is only allowed if rollover is enabled - if (esPhase.actions.forcemerge) { - delete esPhase.actions.forcemerge; - } - } - - if (isNumber(phase.phaseIndexPriority)) { - esPhase.actions.set_priority = { - priority: parseInt(phase.phaseIndexPriority, 10), - }; - } else { - delete esPhase.actions.set_priority; - } - - return esPhase; -}; - -export const validateHotPhase = (phase: HotPhase): PhaseValidationErrors => { - if (!phase.phaseEnabled) { - return {}; - } - - const phaseErrors = {} as PhaseValidationErrors; - - // index priority is optional, but if it's set, it needs to be a positive number - if (phase.phaseIndexPriority) { - if (!isNumber(phase.phaseIndexPriority)) { - phaseErrors.phaseIndexPriority = [numberRequiredMessage]; - } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { - phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; - } - } - - // if rollover is enabled - if (phase.rolloverEnabled) { - // either max_age, max_size or max_documents need to be set - if ( - !isNumber(phase.selectedMaxAge) && - !isNumber(phase.selectedMaxSizeStored) && - !isNumber(phase.selectedMaxDocuments) - ) { - phaseErrors.selectedMaxAge = [maximumAgeRequiredMessage]; - phaseErrors.selectedMaxSizeStored = [maximumSizeRequiredMessage]; - phaseErrors.selectedMaxDocuments = [maximumDocumentsRequiredMessage]; - } - - // max age, max size and max docs need to be above zero if set - if (isNumber(phase.selectedMaxAge) && parseInt(phase.selectedMaxAge, 10) < 1) { - phaseErrors.selectedMaxAge = [positiveNumbersAboveZeroErrorMessage]; - } - if (isNumber(phase.selectedMaxSizeStored) && parseInt(phase.selectedMaxSizeStored, 10) < 1) { - phaseErrors.selectedMaxSizeStored = [positiveNumbersAboveZeroErrorMessage]; - } - if (isNumber(phase.selectedMaxDocuments) && parseInt(phase.selectedMaxDocuments, 10) < 1) { - phaseErrors.selectedMaxDocuments = [positiveNumbersAboveZeroErrorMessage]; - } - - // if forcemerge is enabled, force merge segments needs to be a number above zero - if (phase.forceMergeEnabled) { - if (!isNumber(phase.selectedForceMergeSegments)) { - phaseErrors.selectedForceMergeSegments = [numberRequiredMessage]; - } else if (parseInt(phase.selectedForceMergeSegments, 10) < 1) { - phaseErrors.selectedForceMergeSegments = [positiveNumbersAboveZeroErrorMessage]; - } - } - } - - return { - ...phaseErrors, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts index a96b6f57a0f9..9cf622e830cb 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts @@ -7,26 +7,25 @@ import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; -import { Policy, PolicyFromES } from '../../../../common/types'; +import { SerializedPolicy } from '../../../../common/types'; import { savePolicy as savePolicyApi } from '../api'; import { showApiError } from '../api_errors'; import { getUiMetricsForPhases, trackUiMetric } from '../ui_metric'; import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants'; import { toasts } from '../notification'; -import { serializePolicy } from './policy_serialization'; export const savePolicy = async ( - policy: Policy, - isNew: boolean, - originalEsPolicy?: PolicyFromES + readSerializedPolicy: () => SerializedPolicy, + isNew: boolean ): Promise => { - const serializedPolicy = serializePolicy(policy, originalEsPolicy?.policy); + const serializedPolicy = readSerializedPolicy(); + try { await savePolicyApi(serializedPolicy); } catch (err) { const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage', { defaultMessage: 'Error saving lifecycle policy {lifecycleName}', - values: { lifecycleName: policy.name }, + values: { lifecycleName: serializedPolicy.name }, }); showApiError(err, title); return false; @@ -46,7 +45,7 @@ export const savePolicy = async ( : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.updatedMessage', { defaultMessage: 'Updated', }), - lifecycleName: policy.name, + lifecycleName: serializedPolicy.name, }, }); toasts.addSuccess(message); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts index 71ae9b26e290..5c7f04986827 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts @@ -6,24 +6,18 @@ // Prefer importing entire lodash library, e.g. import { get } from "lodash" // eslint-disable-next-line no-restricted-imports import cloneDeep from 'lodash/cloneDeep'; -import { deserializePolicy, serializePolicy } from './policy_serialization'; -import { - defaultNewColdPhase, - defaultNewDeletePhase, - defaultNewHotPhase, - defaultNewWarmPhase, -} from '../../constants'; +import { deserializePolicy, legacySerializePolicy } from './policy_serialization'; +import { defaultNewColdPhase, defaultNewDeletePhase, defaultNewWarmPhase } from '../../constants'; import { DataTierAllocationType } from '../../../../common/types'; import { coldPhaseInitialization } from './cold_phase'; describe('Policy serialization', () => { test('serialize a policy using "default" data allocation', () => { expect( - serializePolicy( + legacySerializePolicy( { name: 'test', phases: { - hot: { ...defaultNewHotPhase }, warm: { ...defaultNewWarmPhase, dataTierAllocationType: 'default', @@ -56,17 +50,6 @@ describe('Policy serialization', () => { ).toEqual({ name: 'test', phases: { - hot: { - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - set_priority: { - priority: 100, - }, - }, - }, warm: { actions: { set_priority: { @@ -88,11 +71,10 @@ describe('Policy serialization', () => { test('serialize a policy using "custom" data allocation', () => { expect( - serializePolicy( + legacySerializePolicy( { name: 'test', phases: { - hot: { ...defaultNewHotPhase }, warm: { ...defaultNewWarmPhase, dataTierAllocationType: 'custom', @@ -136,17 +118,6 @@ describe('Policy serialization', () => { ).toEqual({ name: 'test', phases: { - hot: { - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - set_priority: { - priority: 100, - }, - }, - }, warm: { actions: { allocate: { @@ -182,11 +153,10 @@ describe('Policy serialization', () => { test('serialize a policy using "custom" data allocation with no node attributes', () => { expect( - serializePolicy( + legacySerializePolicy( { name: 'test', phases: { - hot: { ...defaultNewHotPhase }, warm: { ...defaultNewWarmPhase, dataTierAllocationType: 'custom', @@ -219,17 +189,6 @@ describe('Policy serialization', () => { // There should be no allocation action in any phases... name: 'test', phases: { - hot: { - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - set_priority: { - priority: 100, - }, - }, - }, warm: { actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } }, @@ -253,11 +212,10 @@ describe('Policy serialization', () => { test('serialize a policy using "none" data allocation with no node attributes', () => { expect( - serializePolicy( + legacySerializePolicy( { name: 'test', phases: { - hot: { ...defaultNewHotPhase }, warm: { ...defaultNewWarmPhase, dataTierAllocationType: 'none', @@ -290,17 +248,6 @@ describe('Policy serialization', () => { // There should be no allocation action in any phases... name: 'test', phases: { - hot: { - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - set_priority: { - priority: 100, - }, - }, - }, warm: { actions: { migrate: { @@ -330,7 +277,6 @@ describe('Policy serialization', () => { const originalPolicy = { name: 'test', phases: { - hot: { actions: {} }, warm: { actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, }, @@ -345,7 +291,6 @@ describe('Policy serialization', () => { const deserializedPolicy = { name: 'test', phases: { - hot: { ...defaultNewHotPhase }, warm: { ...defaultNewWarmPhase, dataTierAllocationType: 'none' as DataTierAllocationType, @@ -363,26 +308,20 @@ describe('Policy serialization', () => { }, }; - serializePolicy(deserializedPolicy, originalPolicy); + legacySerializePolicy(deserializedPolicy, originalPolicy); deserializedPolicy.phases.warm.dataTierAllocationType = 'custom'; - serializePolicy(deserializedPolicy, originalPolicy); + legacySerializePolicy(deserializedPolicy, originalPolicy); deserializedPolicy.phases.warm.dataTierAllocationType = 'default'; - serializePolicy(deserializedPolicy, originalPolicy); + legacySerializePolicy(deserializedPolicy, originalPolicy); expect(originalPolicy).toEqual(originalClone); }); test('serialize a policy using "best_compression" codec for forcemerge', () => { expect( - serializePolicy( + legacySerializePolicy( { name: 'test', phases: { - hot: { - ...defaultNewHotPhase, - forceMergeEnabled: true, - selectedForceMergeSegments: '1', - bestCompressionEnabled: true, - }, warm: { ...defaultNewWarmPhase, phaseEnabled: true, @@ -406,21 +345,6 @@ describe('Policy serialization', () => { ).toEqual({ name: 'test', phases: { - hot: { - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - forcemerge: { - max_num_segments: 1, - index_codec: 'best_compression', - }, - set_priority: { - priority: 100, - }, - }, - }, warm: { actions: { forcemerge: { @@ -477,12 +401,6 @@ describe('Policy serialization', () => { ).toEqual({ name: 'test', phases: { - hot: { - ...defaultNewHotPhase, - forceMergeEnabled: true, - selectedForceMergeSegments: '1', - bestCompressionEnabled: true, - }, warm: { ...defaultNewWarmPhase, warmPhaseOnRollover: false, @@ -501,16 +419,10 @@ describe('Policy serialization', () => { test('delete "best_compression" codec for forcemerge if disabled in UI', () => { expect( - serializePolicy( + legacySerializePolicy( { name: 'test', phases: { - hot: { - ...defaultNewHotPhase, - forceMergeEnabled: true, - selectedForceMergeSegments: '1', - bestCompressionEnabled: false, - }, warm: { ...defaultNewWarmPhase, phaseEnabled: true, @@ -527,7 +439,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - hot: { actions: {} }, warm: { actions: { forcemerge: { @@ -542,20 +453,6 @@ describe('Policy serialization', () => { ).toEqual({ name: 'test', phases: { - hot: { - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - forcemerge: { - max_num_segments: 1, - }, - set_priority: { - priority: 100, - }, - }, - }, warm: { actions: { forcemerge: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts index 996b2e8c371b..0dce7efce462 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -4,17 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Policy, PolicyFromES, SerializedPolicy } from '../../../../common/types'; +import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types'; import { defaultNewColdPhase, defaultNewDeletePhase, - defaultNewHotPhase, defaultNewWarmPhase, serializedPhaseInitialization, } from '../../constants'; -import { hotPhaseFromES, hotPhaseToES } from './hot_phase'; import { warmPhaseFromES, warmPhaseToES } from './warm_phase'; import { coldPhaseFromES, coldPhaseToES } from './cold_phase'; import { deletePhaseFromES, deletePhaseToES } from './delete_phase'; @@ -46,11 +44,10 @@ export const getPolicyByName = ( } }; -export const initializeNewPolicy = (newPolicyName: string = ''): Policy => { +export const initializeNewPolicy = (newPolicyName: string = ''): LegacyPolicy => { return { name: newPolicyName, phases: { - hot: { ...defaultNewHotPhase }, warm: { ...defaultNewWarmPhase }, cold: { ...defaultNewColdPhase }, delete: { ...defaultNewDeletePhase }, @@ -58,7 +55,7 @@ export const initializeNewPolicy = (newPolicyName: string = ''): Policy => { }; }; -export const deserializePolicy = (policy: PolicyFromES): Policy => { +export const deserializePolicy = (policy: PolicyFromES): LegacyPolicy => { const { name, policy: { phases }, @@ -67,7 +64,6 @@ export const deserializePolicy = (policy: PolicyFromES): Policy => { return { name, phases: { - hot: hotPhaseFromES(phases.hot), warm: warmPhaseFromES(phases.warm), cold: coldPhaseFromES(phases.cold), delete: deletePhaseFromES(phases.delete), @@ -75,8 +71,8 @@ export const deserializePolicy = (policy: PolicyFromES): Policy => { }; }; -export const serializePolicy = ( - policy: Policy, +export const legacySerializePolicy = ( + policy: LegacyPolicy, originalEsPolicy: SerializedPolicy = { name: policy.name, phases: { hot: { ...serializedPhaseInitialization } }, @@ -84,7 +80,7 @@ export const serializePolicy = ( ): SerializedPolicy => { const serializedPolicy = { name: policy.name, - phases: { hot: hotPhaseToES(policy.phases.hot, originalEsPolicy.phases.hot) }, + phases: {}, } as SerializedPolicy; if (policy.phases.warm.phaseEnabled) { serializedPolicy.phases.warm = warmPhaseToES(policy.phases.warm, originalEsPolicy.phases.warm); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts index ffd3c01ab001..eeceb97c409f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -8,12 +8,10 @@ import { i18n } from '@kbn/i18n'; import { ColdPhase, DeletePhase, - HotPhase, - Policy, + LegacyPolicy, PolicyFromES, WarmPhase, } from '../../../../common/types'; -import { validateHotPhase } from './hot_phase'; import { validateWarmPhase } from './warm_phase'; import { validateColdPhase } from './cold_phase'; import { validateDeletePhase } from './delete_phase'; @@ -35,27 +33,6 @@ export const positiveNumberRequiredMessage = i18n.translate( } ); -export const maximumAgeRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError', - { - defaultMessage: 'A maximum age is required.', - } -); - -export const maximumSizeRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError', - { - defaultMessage: 'A maximum index size is required.', - } -); - -export const maximumDocumentsRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError', - { - defaultMessage: 'Maximum documents is required.', - } -); - export const positiveNumbersAboveZeroErrorMessage = i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError', { @@ -112,7 +89,6 @@ export type PhaseValidationErrors = { }; export interface ValidationErrors { - hot: PhaseValidationErrors; warm: PhaseValidationErrors; cold: PhaseValidationErrors; delete: PhaseValidationErrors; @@ -121,7 +97,7 @@ export interface ValidationErrors { export const validatePolicy = ( saveAsNew: boolean, - policy: Policy, + policy: LegacyPolicy, policies: PolicyFromES[], originalPolicyName: string ): [boolean, ValidationErrors] => { @@ -152,13 +128,11 @@ export const validatePolicy = ( } } - const hotPhaseErrors = validateHotPhase(policy.phases.hot); const warmPhaseErrors = validateWarmPhase(policy.phases.warm); const coldPhaseErrors = validateColdPhase(policy.phases.cold); const deletePhaseErrors = validateDeletePhase(policy.phases.delete); const isValid = policyNameErrors.length === 0 && - Object.keys(hotPhaseErrors).length === 0 && Object.keys(warmPhaseErrors).length === 0 && Object.keys(coldPhaseErrors).length === 0 && Object.keys(deletePhaseErrors).length === 0; @@ -166,7 +140,6 @@ export const validatePolicy = ( isValid, { policyName: [...policyNameErrors], - hot: hotPhaseErrors, warm: warmPhaseErrors, cold: coldPhaseErrors, delete: deletePhaseErrors, @@ -183,9 +156,6 @@ export const findFirstError = (errors?: ValidationErrors): string | undefined => return propertyof('policyName'); } - if (Object.keys(errors.hot).length > 0) { - return `${propertyof('hot')}.${Object.keys(errors.hot)[0]}`; - } if (Object.keys(errors.warm).length > 0) { return `${propertyof('warm')}.${Object.keys(errors.warm)[0]}`; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts index aeb2c8ce917c..ea5c5619da58 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts @@ -14,8 +14,8 @@ import { UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_WARM_PHASE, defaultNewColdPhase, - defaultNewHotPhase, defaultNewWarmPhase, + defaultSetPriority, } from '../constants'; import { Phases } from '../../../common/types'; @@ -45,8 +45,7 @@ export function getUiMetricsForPhases(phases: Phases): string[] { const isHotPhasePriorityChanged = phases.hot && phases.hot.actions.set_priority && - phases.hot.actions.set_priority.priority !== - parseInt(defaultNewHotPhase.phaseIndexPriority, 10); + phases.hot.actions.set_priority.priority !== parseInt(defaultSetPriority, 10); const isWarmPhasePriorityChanged = phases.warm && diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx index ce36a3650c2f..d711863c309e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx @@ -143,7 +143,7 @@ export class IndexLifecycleSummary extends Component { ); return ( - + { } content = content || '-'; const cell = ( - <> + {label} {content} - + ); if (arrayIndex % 2 === 0) { rows.left.push(cell); 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..dc3e1b1d1b62 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AppServicesContext } from './types'; +import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public'; + +export { + useForm, + useFormData, + Form, + UseField, + FieldConfig, + OnFormUpdateArg, + ValidationFunc, + getFieldValidityAndErrorMessage, + useFormContext, + FormSchema, +} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +export { + ToggleField, + NumericField, + SelectField, +} from '../../../../src/plugins/es_ui_shared/static/forms/components'; + +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/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 40037d0c1e77..e87f4aa17c0e 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -14,6 +14,7 @@ import { PluginInitializerContext, LegacyAPICaller, } from 'src/core/server'; +import { handleEsError } from './shared_imports'; import { Index as IndexWithoutIlm } from '../../index_management/common/types'; import { PLUGIN } from '../common/constants'; @@ -99,6 +100,9 @@ export class IndexLifecycleManagementServerPlugin implements Plugin { @@ -47,15 +51,8 @@ export function registerAddPolicyRoute({ router, license }: RouteDependencies) { alias ); return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts index a83a3fa1378c..15c3e7b866c7 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts @@ -26,7 +26,11 @@ const bodySchema = schema.object({ indexNames: schema.arrayOf(schema.string()), }); -export function registerRemoveRoute({ router, license }: RouteDependencies) { +export function registerRemoveRoute({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { router.post( { path: addBasePath('/index/remove'), validate: { body: bodySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -36,15 +40,8 @@ export function registerRemoveRoute({ router, license }: RouteDependencies) { try { await removeLifecycle(context.core.elasticsearch.client.asCurrentUser, indexNames); return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts index cdcf5ed4b7ac..28bced0fb5a8 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts @@ -27,7 +27,7 @@ const bodySchema = schema.object({ indexNames: schema.arrayOf(schema.string()), }); -export function registerRetryRoute({ router, license }: RouteDependencies) { +export function registerRetryRoute({ router, license, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/index/retry'), validate: { body: bodySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -37,15 +37,8 @@ export function registerRetryRoute({ router, license }: RouteDependencies) { try { await retryLifecycle(context.core.elasticsearch.client.asCurrentUser, indexNames); return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); 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_details_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts index 57034af324ed..41b93ba59e3f 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts @@ -29,7 +29,11 @@ const paramsSchema = schema.object({ nodeAttrs: schema.string(), }); -export function registerDetailsRoute({ router, license }: RouteDependencies) { +export function registerDetailsRoute({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { router.get( { path: addBasePath('/nodes/{nodeAttrs}/details'), validate: { params: paramsSchema } }, license.guardApiRoute(async (context, request, response) => { @@ -40,15 +44,8 @@ export function registerDetailsRoute({ router, license }: RouteDependencies) { const statsResponse = await context.core.elasticsearch.client.asCurrentUser.nodes.stats(); const okResponse = { body: findMatchingNodes(statsResponse.body, nodeAttrs) }; return response.ok(okResponse); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); 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..53955d93c1e0 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,18 +39,35 @@ 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 ); } -export function registerListRoute({ router, config, license }: RouteDependencies) { +export function registerListRoute({ + router, + config, + license, + lib: { handleEsError }, +}: RouteDependencies) { const { filteredNodeAttributes } = config; const NODE_ATTRS_KEYS_TO_IGNORE: string[] = [ @@ -64,23 +86,22 @@ 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 }); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index 359b275622f0..d8e40e3b3041 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -134,7 +134,11 @@ const bodySchema = schema.object({ }), }); -export function registerCreateRoute({ router, license }: RouteDependencies) { +export function registerCreateRoute({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { router.post( { path: addBasePath('/policies'), validate: { body: bodySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -144,15 +148,8 @@ export function registerCreateRoute({ router, license }: RouteDependencies) { try { await createPolicy(context.core.elasticsearch.client.asCurrentUser, name, phases); return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts index cb394c12c46f..b0363cb7c3bc 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts @@ -23,7 +23,11 @@ const paramsSchema = schema.object({ policyNames: schema.string(), }); -export function registerDeleteRoute({ router, license }: RouteDependencies) { +export function registerDeleteRoute({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { router.delete( { path: addBasePath('/policies/{policyNames}'), validate: { params: paramsSchema } }, license.guardApiRoute(async (context, request, response) => { @@ -33,15 +37,8 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) { try { await deletePolicies(context.core.elasticsearch.client.asCurrentUser, policyNames); return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts index 8cbea2566637..fc5f369e588f 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts @@ -57,7 +57,7 @@ const querySchema = schema.object({ withIndices: schema.boolean({ defaultValue: false }), }); -export function registerFetchRoute({ router, license }: RouteDependencies) { +export function registerFetchRoute({ router, license, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/policies'), validate: { query: querySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -75,15 +75,8 @@ export function registerFetchRoute({ router, license }: RouteDependencies) { await addLinkedIndices(asCurrentUser, policiesMap); } return response.ok({ body: formatPolicies(policiesMap) }); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts index 869be3d55704..00afc31c03ba 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts @@ -14,7 +14,7 @@ async function fetchSnapshotPolicies(client: ElasticsearchClient): Promise return response.body; } -export function registerFetchRoute({ router, license }: RouteDependencies) { +export function registerFetchRoute({ router, license, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/snapshot_policies'), validate: false }, license.guardApiRoute(async (context, request, response) => { @@ -23,15 +23,8 @@ export function registerFetchRoute({ router, license }: RouteDependencies) { context.core.elasticsearch.client.asCurrentUser ); return response.ok({ body: Object.keys(policiesByName) }); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts index 7e7f3f1f725f..667491ef4af6 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts @@ -92,7 +92,11 @@ const querySchema = schema.object({ legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); -export function registerAddPolicyRoute({ router, license }: RouteDependencies) { +export function registerAddPolicyRoute({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { router.post( { path: addBasePath('/template'), validate: { body: bodySchema, query: querySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -118,15 +122,8 @@ export function registerAddPolicyRoute({ router, license }: RouteDependencies) { }); } return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts index fbd102d3be1e..35860d217732 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts @@ -80,7 +80,7 @@ const querySchema = schema.object({ legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); -export function registerFetchRoute({ router, license }: RouteDependencies) { +export function registerFetchRoute({ router, license, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/templates'), validate: { query: querySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -92,15 +92,8 @@ export function registerFetchRoute({ router, license }: RouteDependencies) { ); const okResponse = { body: filterTemplates(templates, isLegacy) }; return response.ok(okResponse); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts new file mode 100644 index 000000000000..068cddcee4c8 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/index_lifecycle_management/server/types.ts b/x-pack/plugins/index_lifecycle_management/server/types.ts index e34dc8e4b1a5..8de7a01f1feb 100644 --- a/x-pack/plugins/index_lifecycle_management/server/types.ts +++ b/x-pack/plugins/index_lifecycle_management/server/types.ts @@ -11,6 +11,7 @@ import { LicensingPluginSetup } from '../../licensing/server'; import { IndexManagementPluginSetup } from '../../index_management/server'; import { License } from './services'; import { IndexLifecycleManagementConfig } from './config'; +import { handleEsError } from './shared_imports'; export interface Dependencies { licensing: LicensingPluginSetup; @@ -22,4 +23,7 @@ export interface RouteDependencies { router: IRouter; config: IndexLifecycleManagementConfig; license: License; + lib: { + handleEsError: typeof handleEsError; + }; } 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/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 444530c4d79f..3767144a1b79 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -51,7 +51,7 @@ export const METRICS_FEATURE = { read: ['infrastructure-ui-source', 'index-pattern'], }, alerting: { - all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + read: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], }, management: { insightsAndAlerting: ['triggersActions'], @@ -92,7 +92,7 @@ export const LOGS_FEATURE = { catalogue: ['infralogging', 'logs'], api: ['infra'], alerting: { - all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], }, savedObject: { all: [], diff --git a/x-pack/plugins/ingest_manager/common/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..dc61f4898478 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,84 @@ 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 + ); + }); + it('returns false if agent reports upgradeable, with agent snapshot version === kibana version', () => { + expect( + isAgentUpgradeable(getAgent({ version: '7.9.0-SNAPSHOT', upgradeable: true }), '7.9.0') + ).toBe(false); + }); + it('returns false if agent reports upgradeable, with agent version === kibana snapshot version', () => { + expect( + isAgentUpgradeable(getAgent({ version: '7.9.0', upgradeable: true }), '7.9.0-SNAPSHOT') + ).toBe(false); + }); + it('returns true if agent reports upgradeable, with agent snapshot version < kibana snapshot version', () => { + expect( + isAgentUpgradeable( + getAgent({ version: '7.9.0-SNAPSHOT', upgradeable: true }), + '8.0.0-SNAPSHOT' + ) + ).toBe(true); + }); + it('returns false if agent reports upgradeable, with agent snapshot version === kibana snapshot version', () => { + expect( + isAgentUpgradeable( + getAgent({ version: '8.0.0-SNAPSHOT', upgradeable: true }), + '8.0.0-SNAPSHOT' + ) + ).toBe(false); + }); + it('returns true if agent reports upgradeable, with agent version < kibana snapshot version', () => { + expect( + isAgentUpgradeable(getAgent({ version: '7.9.0', upgradeable: true }), '8.0.0-SNAPSHOT') + ).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..b93e5d99543f 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,9 +13,13 @@ export function isAgentUpgradeable(agent: Agent, kibanaVersion: string) { } else { return false; } - const kibanaVersionParsed = semver.parse(kibanaVersion); - const agentVersionParsed = semver.parse(agentVersion); - if (!agentVersionParsed || !kibanaVersionParsed) return false; + if (agent.unenrollment_started_at || agent.unenrolled_at) return false; if (!agent.local_metadata.elastic.agent.upgradeable) return false; - return semver.lt(agentVersionParsed, kibanaVersionParsed); + + // make sure versions are only the number before comparison + const agentVersionNumber = semver.coerce(agentVersion); + if (!agentVersionNumber) throw new Error('agent version is invalid'); + const kibanaVersionNumber = semver.coerce(kibanaVersion); + if (!kibanaVersionNumber) throw new Error('kibana version is invalid'); + return semver.lt(agentVersionNumber, kibanaVersionNumber); } 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..a8b986be048a 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -25,13 +25,13 @@ 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 }), - registryUrl: schema.maybe(schema.uri()), - registryProxyUrl: schema.maybe(schema.uri()), + registryUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), + registryProxyUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), agents: schema.object({ enabled: schema.boolean({ defaultValue: true }), tlsCheckDisabled: schema.boolean({ defaultValue: false }), diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts index 9c6b50b6d8f0..60dc7c6ee5f2 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts @@ -6,6 +6,7 @@ import { RequestHandler } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; +import semver from 'semver'; import { AgentSOAttributes, PostAgentUpgradeResponse, @@ -26,17 +27,18 @@ export const postAgentUpgradeHandler: RequestHandler< > = async (context, request, response) => { const soClient = context.core.savedObjects.client; const { version, source_uri: sourceUri } = request.body; - - // temporarily only allow upgrading to the same version as the installed kibana version const kibanaVersion = appContextService.getKibanaVersion(); - if (kibanaVersion !== version) { + try { + checkVersionIsSame(version, kibanaVersion); + } catch (err) { return response.customError({ statusCode: 400, body: { - message: `cannot upgrade agent to ${version} because it is different than the installed kibana version ${kibanaVersion}`, + message: err.message, }, }); } + const agentSO = await soClient.get( AGENT_SAVED_OBJECT_TYPE, request.params.agentId @@ -82,14 +84,14 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< > = async (context, request, response) => { const soClient = context.core.savedObjects.client; const { version, source_uri: sourceUri, agents } = request.body; - - // temporarily only allow upgrading to the same version as the installed kibana version const kibanaVersion = appContextService.getKibanaVersion(); - if (kibanaVersion !== version) { + try { + checkVersionIsSame(version, kibanaVersion); + } catch (err) { return response.customError({ statusCode: 400, body: { - message: `cannot upgrade agent to ${version} because it is different than the installed kibana version ${kibanaVersion}`, + message: err.message, }, }); } @@ -115,3 +117,17 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< return defaultIngestErrorHandler({ error, response }); } }; + +export const checkVersionIsSame = (version: string, kibanaVersion: string) => { + // get version number only in case "-SNAPSHOT" is in it + const kibanaVersionNumber = semver.coerce(kibanaVersion)?.version; + if (!kibanaVersionNumber) throw new Error(`kibanaVersion ${kibanaVersionNumber} is not valid`); + const versionToUpgradeNumber = semver.coerce(version)?.version; + if (!versionToUpgradeNumber) + throw new Error(`version to upgrade ${versionToUpgradeNumber} is not valid`); + // temporarily only allow upgrading to the same version as the installed kibana version + if (kibanaVersionNumber !== versionToUpgradeNumber) + throw new Error( + `cannot upgrade agent to ${versionToUpgradeNumber} because it is different than the installed kibana version ${kibanaVersionNumber}` + ); +}; 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/elasticsearch/transform/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts index 02d5dfc64d07..5b5583a121e5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts @@ -7,6 +7,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { CallESAsCurrentUser, ElasticsearchAssetType, EsAssetReference } from '../../../../types'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common/constants'; +import { appContextService } from '../../../app_context'; export const stopTransforms = async (transformIds: string[], callCluster: CallESAsCurrentUser) => { for (const transformId of transformIds) { @@ -28,7 +29,7 @@ export const deleteTransforms = async ( // get the index the transform const transformResponse: { count: number; - transforms: Array<{ + transforms?: Array<{ dest: { index: string; }; @@ -36,6 +37,7 @@ export const deleteTransforms = async ( } = await callCluster('transport.request', { method: 'GET', path: `/_transform/${transformId}`, + ignore: [404], }); await stopTransforms([transformId], callCluster); @@ -46,13 +48,17 @@ export const deleteTransforms = async ( ignore: [404], }); - // expect this to be 1 - for (const transform of transformResponse.transforms) { - await callCluster('transport.request', { - method: 'DELETE', - path: `/${transform?.dest?.index}`, - ignore: [404], - }); + if (transformResponse?.transforms) { + // expect this to be 1 + for (const transform of transformResponse.transforms) { + await callCluster('transport.request', { + method: 'DELETE', + path: `/${transform?.dest?.index}`, + ignore: [404], + }); + } + } else { + appContextService.getLogger().warn(`cannot find transform for ${transformId}`); } }) ); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts index 768c6af1d891..2bf0ad12856f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts @@ -160,6 +160,7 @@ describe('test transform install', () => { { method: 'GET', path: '/_transform/endpoint.metadata_current-default-0.15.0-dev.0', + ignore: [404], }, ], [ @@ -446,6 +447,7 @@ describe('test transform install', () => { { method: 'GET', path: '/_transform/endpoint.metadata-current-default-0.15.0-dev.0', + ignore: [404], }, ], [ 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/ingest_manager/server/services/epm/registry/registry_url.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts index ff9a7871a7db..efc25cc2efb5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts @@ -28,12 +28,14 @@ const getDefaultRegistryUrl = (): string => { } }; -// Custom registry URL is currently only for internal Elastic development and is unsupported export const getRegistryUrl = (): string => { const customUrl = appContextService.getConfig()?.registryUrl; const isEnterprise = licenseService.isEnterprise(); if (customUrl && isEnterprise) { + appContextService + .getLogger() + .info('Custom registry url is an experimental feature and is unsupported.'); return customUrl; } diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index d30ab5962667..1f4525db09ad 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -6,6 +6,7 @@ export const PLUGIN_ID = 'lens'; export const LENS_EMBEDDABLE_TYPE = 'lens'; +export const DOC_TYPE = 'lens'; export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; export const BASE_API_URL = '/api/lens'; export const LENS_EDIT_BY_VALUE = 'edit_by_value'; diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index e9e6bf43d9f1..25813aa96569 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -11,7 +11,8 @@ import { act } from 'react-dom/test-utils'; import { App } from './app'; import { LensAppProps, LensAppServices } from './types'; import { EditorFrameInstance } from '../types'; -import { Document, DOC_TYPE } from '../persistence'; +import { Document } from '../persistence'; +import { DOC_TYPE } from '../../common'; import { mount } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; import { diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 90ba74decfd8..3943cbc54f0b 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -37,7 +37,7 @@ export async function mountApp( mountProps: { createEditorFrame: EditorFrameStart['createInstance']; getByValueFeatureFlag: () => Promise; - attributeService: LensAttributeService; + attributeService: () => Promise; } ) { const { createEditorFrame, getByValueFeatureFlag, attributeService } = mountProps; @@ -54,7 +54,7 @@ export async function mountApp( data, storage, navigation, - attributeService, + attributeService: await attributeService(), http: coreStart.http, chrome: coreStart.chrome, overlays: coreStart.overlays, diff --git a/x-pack/plugins/lens/public/async_services.ts b/x-pack/plugins/lens/public/async_services.ts index 09b9233197d2..891e5b92ed4d 100644 --- a/x-pack/plugins/lens/public/async_services.ts +++ b/x-pack/plugins/lens/public/async_services.ts @@ -23,3 +23,5 @@ export * from './indexpattern_datasource/indexpattern'; export * from './editor_frame_service/editor_frame'; export * from './editor_frame_service/embeddable'; export * from './app_plugin/mounter'; +export * from './lens_attribute_service'; +export * from './lens_ui_telemetry'; diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx index eb00cf93ccd3..c95f6085b479 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -13,20 +13,50 @@ import { DatatableProps } from './expression'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; import { IFieldFormat } from '../../../../../src/plugins/data/public'; import { IAggType } from 'src/plugins/data/public'; -const onClickValue = jest.fn(); import { EmptyPlaceholder } from '../shared_components'; import { LensIconChartDatatable } from '../assets/chart_datatable'; function sampleArgs() { + const indexPatternId = 'indexPatternId'; const data: LensMultiTable = { type: 'lens_multitable', tables: { l1: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a', meta: { type: 'terms' } }, - { id: 'b', name: 'b', meta: { type: 'date_histogram', aggConfigParams: { field: 'b' } } }, - { id: 'c', name: 'c', meta: { type: 'count' } }, + { + id: 'a', + name: 'a', + meta: { + type: 'string', + source: 'esaggs', + field: 'a', + sourceParams: { type: 'terms', indexPatternId }, + }, + }, + { + id: 'b', + name: 'b', + meta: { + type: 'date', + field: 'b', + source: 'esaggs', + sourceParams: { + type: 'date_histogram', + indexPatternId, + }, + }, + }, + { + id: 'c', + name: 'c', + meta: { + type: 'number', + source: 'esaggs', + field: 'c', + sourceParams: { indexPatternId, type: 'count' }, + }, + }, ], rows: [{ a: 'shoes', b: 1588024800000, c: 3 }], }, @@ -45,6 +75,11 @@ function sampleArgs() { } describe('datatable_expression', () => { + let onClickValue: jest.Mock; + beforeEach(() => { + onClickValue = jest.fn(); + }); + describe('datatable renders', () => { test('it renders with the specified data and args', () => { const { data, args } = sampleArgs(); @@ -106,7 +141,7 @@ describe('datatable_expression', () => { }, ], negate: true, - timeFieldName: undefined, + timeFieldName: 'a', }); }); @@ -150,10 +185,27 @@ describe('datatable_expression', () => { type: 'lens_multitable', tables: { l1: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a', meta: { type: 'date_range', aggConfigParams: { field: 'a' } } }, - { id: 'b', name: 'b', meta: { type: 'count' } }, + { + id: 'a', + name: 'a', + meta: { + type: 'date', + source: 'esaggs', + field: 'a', + sourceParams: { type: 'date_range', indexPatternId: 'a' }, + }, + }, + { + id: 'b', + name: 'b', + meta: { + type: 'number', + source: 'esaggs', + sourceParams: { type: 'count', indexPatternId: 'a' }, + }, + }, ], rows: [{ a: 1588024800000, b: 3 }], }, diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index af1773b41359..6502e0769781 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -166,15 +166,15 @@ export function DatatableComponent(props: DatatableRenderProps) { const formatters: Record> = {}; firstTable.columns.forEach((column) => { - formatters[column.id] = props.formatFactory(column.formatHint); + formatters[column.id] = props.formatFactory(column.meta?.params); }); const { onClickValue } = props; const handleFilterClick = useMemo( () => (field: string, value: unknown, colIndex: number, negate: boolean = false) => { const col = firstTable.columns[colIndex]; - const isDate = col.meta?.type === 'date_histogram' || col.meta?.type === 'date_range'; - const timeFieldName = negate && isDate ? undefined : col?.meta?.aggConfigParams?.field; + const isDate = col.meta?.type === 'date'; + const timeFieldName = negate && isDate ? undefined : col?.meta?.field; const rowIndex = firstTable.rows.findIndex((row) => row[field] === value); const data: LensFilterEvent['data'] = { @@ -196,7 +196,10 @@ export function DatatableComponent(props: DatatableRenderProps) { const bucketColumns = firstTable.columns .filter((col) => { - return col?.meta?.type && props.getType(col.meta.type)?.type === 'buckets'; + return ( + col?.meta?.sourceParams?.type && + props.getType(col.meta.sourceParams.type as string)?.type === 'buckets' + ); }) .map((col) => col.id); @@ -230,7 +233,7 @@ export function DatatableComponent(props: DatatableRenderProps) { name: (col && col.name) || '', render: (value: unknown) => { const formattedValue = formatters[field]?.convert(value); - const fieldName = col?.meta?.aggConfigParams?.field; + const fieldName = col?.meta?.field; if (filterable) { return ( 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/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 1297c1da6e1b..d245b7f2fcde 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -33,13 +33,13 @@ import { SavedObjectEmbeddableInput, ReferenceOrValueEmbeddable, } from '../../../../../../src/plugins/embeddable/public'; -import { DOC_TYPE, Document, injectFilterReferences } from '../../persistence'; +import { Document, injectFilterReferences } from '../../persistence'; import { ExpressionWrapper } from './expression_wrapper'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; import { isLensBrushEvent, isLensFilterEvent } from '../../types'; import { IndexPatternsContract } from '../../../../../../src/plugins/data/public'; -import { getEditPath } from '../../../common'; +import { getEditPath, DOC_TYPE } from '../../../common'; import { IBasePath } from '../../../../../../src/core/public'; import { LensAttributeService } from '../../lens_attribute_service'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 35d120e5c4f4..65e9c22d24ea 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -18,10 +18,10 @@ import { IContainer, } from '../../../../../../src/plugins/embeddable/public'; import { LensByReferenceInput, LensEmbeddableInput } from './embeddable'; -import { DOC_TYPE } from '../../persistence'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; import { Document } from '../../persistence/saved_object_store'; import { LensAttributeService } from '../../lens_attribute_service'; +import { DOC_TYPE } from '../../../common'; export interface LensEmbeddableStartServices { timefilter: TimefilterContract; 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/editor_frame_service/format_column.ts b/x-pack/plugins/lens/public/editor_frame_service/format_column.ts index b95139a00ec5..2da6e7195a5e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/format_column.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/format_column.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunctionDefinition, KibanaDatatable } from 'src/plugins/expressions/public'; +import { ExpressionFunctionDefinition, Datatable } from 'src/plugins/expressions/public'; interface FormatColumn { format: string; @@ -41,12 +41,12 @@ const supportedFormats: Record = { name: 'lens_format_column', - type: 'kibana_datatable', + type: 'datatable', help: '', args: { format: { @@ -64,7 +64,7 @@ export const formatColumn: ExpressionFunctionDefinition< help: '', }, }, - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], fn(input, { format, columnId, decimals }: FormatColumn) { return { ...input, @@ -73,15 +73,23 @@ export const formatColumn: ExpressionFunctionDefinition< if (supportedFormats[format]) { return { ...col, - formatHint: { - id: format, - params: { pattern: supportedFormats[format].decimalsToPattern(decimals) }, + meta: { + ...col.meta, + params: { + id: format, + params: { pattern: supportedFormats[format].decimalsToPattern(decimals) }, + }, }, }; } else { return { ...col, - formatHint: { id: format, params: {} }, + meta: { + ...col.meta, + params: { + id: format, + }, + }, }; } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts index b3da722de5f3..5afabb9a5236 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts @@ -6,15 +6,15 @@ import moment from 'moment'; import { mergeTables } from './merge_tables'; -import { KibanaDatatable } from 'src/plugins/expressions'; +import { Datatable } from 'src/plugins/expressions'; describe('lens_merge_tables', () => { it('should produce a row with the nested table as defined', () => { - const sampleTable1: KibanaDatatable = { - type: 'kibana_datatable', + const sampleTable1: Datatable = { + type: 'datatable', columns: [ - { id: 'bucket', name: 'A' }, - { id: 'count', name: 'Count' }, + { id: 'bucket', name: 'A', meta: { type: 'string' } }, + { id: 'count', name: 'Count', meta: { type: 'number' } }, ], rows: [ { bucket: 'a', count: 5 }, @@ -22,11 +22,11 @@ describe('lens_merge_tables', () => { ], }; - const sampleTable2: KibanaDatatable = { - type: 'kibana_datatable', + const sampleTable2: Datatable = { + type: 'datatable', columns: [ - { id: 'bucket', name: 'C' }, - { id: 'avg', name: 'Average' }, + { id: 'bucket', name: 'C', meta: { type: 'string' } }, + { id: 'avg', name: 'Average', meta: { type: 'number' } }, ], rows: [ { bucket: 'a', avg: 2.5 }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts index 7c10ee4a57fa..e4f7b07084ea 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts @@ -6,9 +6,9 @@ import { i18n } from '@kbn/i18n'; import { + Datatable, ExpressionFunctionDefinition, ExpressionValueSearchContext, - KibanaDatatable, } from 'src/plugins/expressions/public'; import { search } from '../../../../../src/plugins/data/public'; const { toAbsoluteDates } = search.aggs; @@ -17,7 +17,7 @@ import { LensMultiTable } from '../types'; interface MergeTables { layerIds: string[]; - tables: KibanaDatatable[]; + tables: Datatable[]; } export const mergeTables: ExpressionFunctionDefinition< @@ -38,14 +38,14 @@ export const mergeTables: ExpressionFunctionDefinition< multi: true, }, tables: { - types: ['kibana_datatable'], + types: ['datatable'], help: '', multi: true, }, }, inputTypes: ['kibana_context', 'null'], fn(input, { layerIds, tables }) { - const resultTables: Record = {}; + const resultTables: Record = {}; tables.forEach((table, index) => { resultTables[layerIds[index]] = table; }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 54250c3bd930..e2a382133cb3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -83,7 +83,7 @@ export class EditorFrameService { public setup( core: CoreSetup, plugins: EditorFrameSetupPlugins, - getAttributeService: () => LensAttributeService + getAttributeService: () => Promise ): EditorFrameSetup { plugins.expressions.registerFunction(() => mergeTables); plugins.expressions.registerFunction(() => formatColumn); @@ -91,7 +91,7 @@ export class EditorFrameService { const getStartServices = async (): Promise => { const [coreStart, deps] = await core.getStartServices(); return { - attributeService: getAttributeService(), + attributeService: await getAttributeService(), capabilities: coreStart.application.capabilities, coreHttp: coreStart.http, timefilter: deps.data.query.timefilter.timefilter, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts index 0b7e4e6b3e22..47687ef10f88 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts @@ -17,4 +17,5 @@ export const { sortByField, hasField, updateLayerIndexPattern, + mergeLayer, } = actual; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 8b0c9011f2c2..310548e5ab81 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -16,7 +16,8 @@ import { EuiListGroupItemProps, EuiFormLabel, } from '@elastic/eui'; -import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from './dimension_panel'; +import { IndexPatternDimensionEditorProps } from './dimension_panel'; +import { OperationSupportMatrix } from './operation_support'; import { IndexPatternColumn, OperationType } from '../indexpattern'; import { operationDefinitionMap, @@ -24,7 +25,7 @@ import { buildColumn, changeField, } from '../operations'; -import { deleteColumn, changeColumn, updateColumnParam } from '../state_helpers'; +import { deleteColumn, changeColumn, updateColumnParam, mergeLayer } from '../state_helpers'; import { FieldSelect } from './field_select'; import { hasField, fieldIsInvalid } from '../utils'; import { BucketNestingEditor } from './bucket_nesting_editor'; @@ -264,7 +265,11 @@ export function DimensionEditor(props: DimensionEditorProps) { 3 ? 'lnsIndexPatternDimensionEditor__columns' : ''} gutterSize="none" - listItems={sideNavItems} + listItems={ + // add a padding item containing a non breakable space if the number of operations is not even + // otherwise the column layout will break within an element + sideNavItems.length % 2 === 1 ? [...sideNavItems, { label: '\u00a0' }] : sideNavItems + } maxWidth={false} />
@@ -390,12 +395,11 @@ export function DimensionEditor(props: DimensionEditorProps) { { - setState({ - ...state, - layers: { - ...state.layers, - [layerId]: { - ...state.layers[layerId], + setState( + mergeLayer({ + state, + layerId, + newLayer: { columns: { ...state.layers[layerId].columns, [columnId]: { @@ -405,8 +409,8 @@ export function DimensionEditor(props: DimensionEditorProps) { }, }, }, - }, - }); + }) + ); }} /> )} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index d15825718682..829bd333ce2c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -13,11 +13,7 @@ import { changeColumn } from '../state_helpers'; import { IndexPatternDimensionEditorComponent, IndexPatternDimensionEditorProps, - onDrop, - canHandleDrop, } from './dimension_panel'; -import { DragContextState } from '../../drag_drop'; -import { createMockedDragDropContext } from '../mocks'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; @@ -110,7 +106,6 @@ describe('IndexPatternDimensionEditorPanel', () => { let state: IndexPatternPrivateState; let setState: jest.Mock; let defaultProps: IndexPatternDimensionEditorProps; - let dragDropContext: DragContextState; function getStateWithColumns(columns: Record) { return { ...state, layers: { first: { ...state.layers.first, columns } } }; @@ -154,8 +149,6 @@ describe('IndexPatternDimensionEditorPanel', () => { setState = jest.fn(); - dragDropContext = createMockedDragDropContext(); - defaultProps = { state, setState, @@ -186,210 +179,489 @@ describe('IndexPatternDimensionEditorPanel', () => { jest.clearAllMocks(); }); - describe('Editor component', () => { - let wrapper: ReactWrapper | ShallowWrapper; + let wrapper: ReactWrapper | ShallowWrapper; - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - } - }); + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); - it('should call the filterOperations function', () => { - const filterOperations = jest.fn().mockReturnValue(true); + it('should call the filterOperations function', () => { + const filterOperations = jest.fn().mockReturnValue(true); - wrapper = shallow( - - ); + wrapper = shallow( + + ); + + expect(filterOperations).toBeCalled(); + }); + + it('should show field select', () => { + wrapper = mount(); + + expect( + wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') + ).toHaveLength(1); + }); + + it('should not show field select on fieldless operation', () => { + wrapper = mount( + + ); + + expect( + wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') + ).toHaveLength(0); + }); + + it('should not show any choices if the filter returns false', () => { + wrapper = mount( + false} + /> + ); + + expect( + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')! + .prop('options')! + ).toHaveLength(0); + }); + + it('should list all field names and document as a whole in prioritized order', () => { + wrapper = mount(); + + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); + + expect(options).toHaveLength(2); + + expect(options![0].label).toEqual('Records'); + expect(options![1].options!.map(({ label }) => label)).toEqual([ + 'timestampLabel', + 'bytes', + 'memory', + 'source', + ]); + }); + + it('should hide fields that have no data', () => { + const props = { + ...defaultProps, + state: { + ...defaultProps.state, + existingFields: { + 'my-fake-index-pattern': { + timestamp: true, + source: true, + }, + }, + }, + }; + wrapper = mount(); + + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); + + expect(options![1].options!.map(({ label }) => label)).toEqual(['timestampLabel', 'source']); + }); + + it('should indicate fields which are incompatible for the operation of the current column', () => { + wrapper = mount( + + ); + + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); + + expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records'); + + expect( + options![1].options!.filter(({ label }) => label === 'timestampLabel')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); + + it('should indicate operations which are incompatible for the field of the current column', () => { + wrapper = mount( + + ); + + const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; + + expect(items.find(({ label }) => label === 'Minimum')!['data-test-subj']).not.toContain( + 'incompatible' + ); + + expect(items.find(({ label }) => label === 'Date histogram')!['data-test-subj']).toContain( + 'incompatible' + ); + + // Fieldless operation is compatible with field + expect(items.find(({ label }) => label === 'Filters')!['data-test-subj']).toContain( + 'compatible' + ); + }); + + it('should keep the operation when switching to another field compatible with this operation', () => { + const initialState: IndexPatternPrivateState = getStateWithColumns({ col1: bytesColumn }); + + wrapper = mount( + + ); + + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'memory')!; - expect(filterOperations).toBeCalled(); + act(() => { + comboBox.prop('onChange')!([option]); }); - it('should show field select', () => { - wrapper = mount(); + expect(setState).toHaveBeenCalledWith({ + ...initialState, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'max', + sourceField: 'memory', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, + }, + }, + }); + }); - expect( - wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') - ).toHaveLength(1); + it('should switch operations when selecting a field that requires another operation', () => { + wrapper = mount(); + + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + + act(() => { + comboBox.prop('onChange')!([option]); }); - it('should not show field select on fieldless operation', () => { - wrapper = mount( - - ); + it('should keep the field when switching to another operation compatible for this field', () => { + wrapper = mount( + + ); - expect( - wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') - ).toHaveLength(0); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - it('should not show any choices if the filter returns false', () => { - wrapper = mount( - false} - /> - ); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'min', + sourceField: 'bytes', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, + }, + }, + }); + }); - expect( - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')! - .prop('options')! - ).toHaveLength(0); + it('should not set the state if selecting the currently active operation', () => { + wrapper = mount(); + + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); }); - it('should list all field names and document as a whole in prioritized order', () => { - wrapper = mount(); + expect(setState).not.toHaveBeenCalled(); + }); - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + it('should update label and custom label flag on label input changes', () => { + wrapper = mount(); + + act(() => { + wrapper + .find('input[data-test-subj="indexPattern-label-edit"]') + .simulate('change', { target: { value: 'New Label' } }); + }); + + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'New Label', + customLabel: true, + // Other parts of this don't matter for this test + }), + }, + }, + }, + }); + }); - expect(options).toHaveLength(2); + it('should not keep the label as long as it is the default label', () => { + wrapper = mount( + + ); - expect(options![0].label).toEqual('Records'); - expect(options![1].options!.map(({ label }) => label)).toEqual([ - 'timestampLabel', - 'bytes', - 'memory', - 'source', - ]); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - it('should hide fields that have no data', () => { - const props = { - ...defaultProps, - state: { - ...defaultProps.state, - existingFields: { - 'my-fake-index-pattern': { - timestamp: true, - source: true, - }, + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'Minimum of bytes', + }), }, }, - }; - wrapper = mount(); + }, + }); + }); - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + it('should keep the label on operation change if it is custom', () => { + wrapper = mount( + + ); - expect(options![1].options!.map(({ label }) => label)).toEqual(['timestampLabel', 'source']); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - it('should indicate fields which are incompatible for the operation of the current column', () => { - wrapper = mount( - - ); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'Custom label', + customLabel: true, + }), + }, + }, + }, + }); + }); - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + describe('transient invalid state', () => { + it('should not set the state if selecting an operation incompatible with the current field', () => { + wrapper = mount(); + + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + }); - expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records'); + expect(setState).not.toHaveBeenCalled(); + }); + + it('should show error message in invalid state', () => { + wrapper = mount(); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); expect( - options![1].options!.filter(({ label }) => label === 'timestampLabel')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] - ).not.toContain('Incompatible'); + wrapper.find('[data-test-subj="indexPattern-field-selection-row"]').first().prop('error') + ).toBeDefined(); + + expect(setState).not.toHaveBeenCalled(); + }); + + it('should leave error state if a compatible operation is selected', () => { + wrapper = mount(); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); + + it('should leave error state if the original operation is re-selected', () => { + wrapper = mount(); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); + + it('should leave error state when switching from incomplete state to fieldless operation', () => { + wrapper = mount(); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-filters incompatible"]') + .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); }); - it('should indicate operations which are incompatible for the field of the current column', () => { + it('should leave error state when re-selecting the original fieldless function', () => { wrapper = mount( ); - const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); - expect(items.find(({ label }) => label === 'Minimum')!['data-test-subj']).not.toContain( - 'incompatible' - ); - - expect(items.find(({ label }) => label === 'Date histogram')!['data-test-subj']).toContain( - 'incompatible' - ); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-filters"]').simulate('click'); - // Fieldless operation is compatible with field - expect(items.find(({ label }) => label === 'Filters')!['data-test-subj']).toContain( - 'compatible' - ); + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); }); - it('should keep the operation when switching to another field compatible with this operation', () => { - const initialState: IndexPatternPrivateState = getStateWithColumns({ col1: bytesColumn }); + it('should indicate fields compatible with selected operation', () => { + wrapper = mount(); - wrapper = mount( - - ); + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); - const comboBox = wrapper + const options = wrapper .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'memory')!; + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - act(() => { - comboBox.prop('onChange')!([option]); - }); + expect(options![0]['data-test-subj']).toContain('Incompatible'); - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'max', - sourceField: 'memory', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), - }, - }, - }, - }); + expect( + options![1].options!.filter(({ label }) => label === 'timestampLabel')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] + ).not.toContain('Incompatible'); }); - it('should switch operations when selecting a field that requires another operation', () => { - wrapper = mount(); + it('should select compatible operation if field not compatible with selected operation', () => { + wrapper = mount(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); const comboBox = wrapper .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + .filter('[data-test-subj="indexPattern-dimension-field"]'); + const options = comboBox.prop('options'); + // options[1][2] is a `source` field of type `string` which doesn't support `avg` operation act(() => { - comboBox.prop('onChange')!([option]); + comboBox.prop('onChange')!([options![1].options![2]]); }); expect(setState).toHaveBeenCalledWith({ @@ -399,788 +671,96 @@ describe('IndexPatternDimensionEditorPanel', () => { ...state.layers.first, columns: { ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'terms', + col2: expect.objectContaining({ sourceField: 'source', + operationType: 'terms', // Other parts of this don't matter for this test }), }, + columnOrder: ['col1', 'col2'], }, }, }); }); - it('should keep the field when switching to another operation compatible for this field', () => { + it('should select the Records field when count is selected', () => { wrapper = mount( ); - act(() => { - wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'min', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), - }, - }, - }, - }); - }); - - it('should not set the state if selecting the currently active operation', () => { - wrapper = mount(); - - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); - }); - - expect(setState).not.toHaveBeenCalled(); - }); - - it('should update label and custom label flag on label input changes', () => { - wrapper = mount(); - - act(() => { - wrapper - .find('input[data-test-subj="indexPattern-label-edit"]') - .simulate('change', { target: { value: 'New Label' } }); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - label: 'New Label', - customLabel: true, - // Other parts of this don't matter for this test - }), - }, - }, - }, - }); - }); - - it('should not keep the label as long as it is the default label', () => { - wrapper = mount( - - ); - - act(() => { - wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - label: 'Minimum of bytes', - }), - }, - }, - }, - }); - }); - - it('should keep the label on operation change if it is custom', () => { - wrapper = mount( - - ); - - act(() => { - wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - label: 'Custom label', - customLabel: true, - }), - }, - }, - }, - }); - }); - - describe('transient invalid state', () => { - it('should not set the state if selecting an operation incompatible with the current field', () => { - wrapper = mount(); - - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - }); - - expect(setState).not.toHaveBeenCalled(); - }); - - it('should show error message in invalid state', () => { - wrapper = mount(); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - - expect( - wrapper.find('[data-test-subj="indexPattern-field-selection-row"]').first().prop('error') - ).toBeDefined(); - - expect(setState).not.toHaveBeenCalled(); - }); - - it('should leave error state if a compatible operation is selected', () => { - wrapper = mount(); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); - - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); - - it('should leave error state if the original operation is re-selected', () => { - wrapper = mount(); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); - - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); - - it('should leave error state when switching from incomplete state to fieldless operation', () => { - wrapper = mount(); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-filters incompatible"]') - .simulate('click'); - - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); - - it('should leave error state when re-selecting the original fieldless function', () => { - wrapper = mount( - - ); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-filters"]') - .simulate('click'); - - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); - - it('should indicate fields compatible with selected operation', () => { - wrapper = mount(); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).toContain('Incompatible'); - - expect( - options![1].options!.filter(({ label }) => label === 'timestampLabel')[0][ - 'data-test-subj' - ] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); - - it('should select compatible operation if field not compatible with selected operation', () => { - wrapper = mount( - - ); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]'); - const options = comboBox.prop('options'); - - // options[1][2] is a `source` field of type `string` which doesn't support `avg` operation - act(() => { - comboBox.prop('onChange')!([options![1].options![2]]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should select the Records field when count is selected', () => { - wrapper = mount( - - ); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-count incompatible"]') - .simulate('click'); - - const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; - expect(newColumnState.operationType).toEqual('count'); - expect(newColumnState.sourceField).toEqual('Records'); - }); - - it('should indicate document and field compatibility with selected document operation', () => { - wrapper = mount( - - ); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).toContain('Incompatible'); - - expect( - options![1].options!.filter(({ label }) => label === 'timestampLabel')[0][ - 'data-test-subj' - ] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); - - it('should set datasource state if compatible field is selected for operation', () => { - wrapper = mount(); - - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - }); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox - .prop('options')![1] - .options!.find(({ label }) => label === 'source')!; - - act(() => { - comboBox.prop('onChange')!([option]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', - }), - }, - }, - }, - }); - }); - }); - - it('should render invalid field if field reference is broken', () => { - wrapper = mount( - - ); - - expect(wrapper.find(EuiComboBox).prop('selectedOptions')).toEqual([ - { - label: 'nonexistent', - value: { type: 'field', field: 'nonexistent' }, - }, - ]); - }); - - it('should support selecting the operation before the field', () => { - wrapper = mount(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]'); - const options = comboBox.prop('options'); - - act(() => { - comboBox.prop('onChange')!([options![1].options![0]]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should select operation directly if only one field is possible', () => { - const initialState = { - ...state, - indexPatterns: { - 1: { - ...state.indexPatterns['1'], - fields: state.indexPatterns['1'].fields.filter((field) => field.name !== 'memory'), - }, - }, - }; - - wrapper = mount( - - ); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...initialState.layers.first, - columns: { - ...initialState.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should select operation directly if only document is possible', () => { - wrapper = mount(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - operationType: 'count', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should indicate compatible fields when selecting the operation first', () => { - wrapper = mount(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).toContain('Incompatible'); - - expect( - options![1].options!.filter(({ label }) => label === 'timestampLabel')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'bytes')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); - - it('should indicate document compatibility when document operation is selected', () => { - wrapper = mount( - - ); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).not.toContain('Incompatible'); - - options![1].options!.map((operation) => - expect(operation['data-test-subj']).toContain('Incompatible') - ); - }); - - it('should show all operations that are not filtered out', () => { - wrapper = mount( - !op.isBucketed && op.dataType === 'number'} - /> - ); - - const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; - - expect(items.map(({ label }: { label: React.ReactNode }) => label)).toEqual([ - 'Average', - 'Count', - 'Maximum', - 'Minimum', - 'Sum', - 'Unique count', - ]); - }); - - it('should add a column on selection of a field', () => { - wrapper = mount(); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options![0]; - - act(() => { - comboBox.prop('onChange')!([option]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should use helper function when changing the function', () => { - const initialState: IndexPatternPrivateState = getStateWithColumns({ - col1: bytesColumn, - }); - wrapper = mount( - - ); - - act(() => { - wrapper.find('[data-test-subj="lns-indexPatternDimension-min"]').first().prop('onClick')!( - {} as React.MouseEvent<{}, MouseEvent> - ); - }); - - expect(changeColumn).toHaveBeenCalledWith({ - state: initialState, - columnId: 'col1', - layerId: 'first', - newColumn: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'min', - }), - }); - }); - - it('should clear the dimension when removing the selection in field combobox', () => { - wrapper = mount(); - - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('onChange')!([]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - indexPatternId: '1', - columns: {}, - columnOrder: [], - }, - }, - }); - }); - - it('allows custom format', () => { - const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ - col1: { - label: 'Average of memory', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'memory', - }, - }); - - wrapper = mount( - - ); - - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: 'bytes', label: 'Bytes' }]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - params: { - format: { id: 'bytes', params: { decimals: 2 } }, - }, - }), - }, - }, - }, - }); + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-count incompatible"]') + .simulate('click'); + + const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; + expect(newColumnState.operationType).toEqual('count'); + expect(newColumnState.sourceField).toEqual('Records'); }); - it('keeps decimal places while switching', () => { - const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ - col1: { - label: 'Average of memory', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'memory', - params: { - format: { id: 'bytes', params: { decimals: 0 } }, - }, - }, - }); + it('should indicate document and field compatibility with selected document operation', () => { wrapper = mount( - + ); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: '', label: 'Default' }]); - }); + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: 'number', label: 'Number' }]); - }); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); + + expect(options![0]['data-test-subj']).toContain('Incompatible'); expect( - wrapper - .find(EuiRange) - .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .prop('value') - ).toEqual(0); + options![1].options!.filter(({ label }) => label === 'timestampLabel')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] + ).not.toContain('Incompatible'); }); - it('allows custom format with number of decimal places', () => { - const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ - col1: { - label: 'Average of memory', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'memory', - params: { - format: { id: 'bytes', params: { decimals: 2 } }, - }, - }, + it('should set datasource state if compatible field is selected for operation', () => { + wrapper = mount(); + + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); }); - wrapper = mount( - - ); + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; act(() => { - wrapper - .find(EuiRange) - .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .prop('onChange')!({ currentTarget: { value: '0' } }); + comboBox.prop('onChange')!([option]); }); expect(setState).toHaveBeenCalledWith({ @@ -1191,9 +771,8 @@ describe('IndexPatternDimensionEditorPanel', () => { columns: { ...state.layers.first.columns, col1: expect.objectContaining({ - params: { - format: { id: 'bytes', params: { decimals: 0 } }, - }, + sourceField: 'source', + operationType: 'terms', }), }, }, @@ -1202,447 +781,404 @@ describe('IndexPatternDimensionEditorPanel', () => { }); }); - describe('Drag and drop', () => { - function dragDropState(): IndexPatternPrivateState { - return { - indexPatternRefs: [], - existingFields: {}, - indexPatterns: { - foo: { - id: 'foo', - title: 'Foo pattern', - hasRestrictions: false, - fields: [ - { - aggregatable: true, - name: 'bar', - displayName: 'bar', - searchable: true, - type: 'number', - }, - { - aggregatable: true, - name: 'mystring', - displayName: 'mystring', - searchable: true, - type: 'string', - }, - ], - }, - }, - currentIndexPatternId: '1', - isFirstExistenceFetch: false, - layers: { - myLayer: { - indexPatternId: 'foo', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Date histogram of timestamp', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - params: { - interval: '1d', - }, - sourceField: 'timestamp', + it('should render invalid field if field reference is broken', () => { + wrapper = mount( + + ); - it('is not droppable if no drag is happening', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext, - state: dragDropState(), - layerId: 'myLayer', - }) - ).toBe(false); - }); + expect(wrapper.find(EuiComboBox).prop('selectedOptions')).toEqual([ + { + label: 'nonexistent', + value: { type: 'field', field: 'nonexistent' }, + }, + ]); + }); - it('is not droppable if the dragged item has no field', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { name: 'bar' }, - }, - }) - ).toBe(false); + it('should support selecting the operation before the field', () => { + wrapper = mount(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]'); + const options = comboBox.prop('options'); + + act(() => { + comboBox.prop('onChange')!([options![1].options![0]]); }); - it('is not droppable if field is not supported by filterOperations', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { - indexPatternId: 'foo', - field: { type: 'string', name: 'mystring', aggregatable: true }, - }, + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'avg', + // Other parts of this don't matter for this test + }), }, - state: dragDropState(), - filterOperations: () => false, - layerId: 'myLayer', - }) - ).toBe(false); + columnOrder: ['col1', 'col2'], + }, + }, }); + }); - it('is droppable if the field is supported by filterOperations', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo', - }, + it('should select operation directly if only one field is possible', () => { + const initialState = { + ...state, + indexPatterns: { + 1: { + ...state.indexPatterns['1'], + fields: state.indexPatterns['1'].fields.filter((field) => field.name !== 'memory'), + }, + }, + }; + + wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + + expect(setState).toHaveBeenCalledWith({ + ...initialState, + layers: { + first: { + ...initialState.layers.first, + columns: { + ...initialState.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'avg', + // Other parts of this don't matter for this test + }), }, - state: dragDropState(), - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - layerId: 'myLayer', - }) - ).toBe(true); + columnOrder: ['col1', 'col2'], + }, + }, }); + }); - it('is not droppable if the field belongs to another index pattern', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo2', - }, + it('should select operation directly if only document is possible', () => { + wrapper = mount(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + operationType: 'count', + // Other parts of this don't matter for this test + }), }, - state: dragDropState(), - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - layerId: 'myLayer', - }) - ).toBe(false); + columnOrder: ['col1', 'col2'], + }, + }, }); + }); - it('is droppable if the dragged column is compatible', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'a', - layerId: 'myLayer', - }, + it('should indicate compatible fields when selecting the operation first', () => { + wrapper = mount(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); + + expect(options![0]['data-test-subj']).toContain('Incompatible'); + + expect( + options![1].options!.filter(({ label }) => label === 'timestampLabel')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'bytes')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); + + it('should indicate document compatibility when document operation is selected', () => { + wrapper = mount( + true, - layerId: 'myLayer', - }) - ).toBe(true); + })} + columnId={'col2'} + /> + ); + + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); + + expect(options![0]['data-test-subj']).not.toContain('Incompatible'); + + options![1].options!.map((operation) => + expect(operation['data-test-subj']).toContain('Incompatible') + ); + }); + + it('should show all operations that are not filtered out', () => { + wrapper = mount( + !op.isBucketed && op.dataType === 'number'} + /> + ); + + const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; + + expect(items.map(({ label }: { label: React.ReactNode }) => label)).toEqual([ + 'Average', + 'Count', + 'Maximum', + 'Median', + 'Minimum', + 'Sum', + 'Unique count', + '\u00a0', + ]); + }); + + it('should add a column on selection of a field', () => { + wrapper = mount(); + + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options![0]; + + act(() => { + comboBox.prop('onChange')!([option]); }); - it('is not droppable if the dragged column is the same as the current column', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'a', - layerId: 'myLayer', - }, + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + // Other parts of this don't matter for this test + }), }, - state: dragDropState(), - columnId: 'col1', - filterOperations: (op: OperationMetadata) => true, - layerId: 'myLayer', - }) - ).toBe(false); + columnOrder: ['col1', 'col2'], + }, + }, }); + }); - it('is not droppable if the dragged column is incompatible', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'a', - layerId: 'myLayer', - }, - }, - state: dragDropState(), - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - layerId: 'myLayer', - }) - ).toBe(false); + it('should use helper function when changing the function', () => { + const initialState: IndexPatternPrivateState = getStateWithColumns({ + col1: bytesColumn, }); + wrapper = mount( + + ); - it('appends the dropped column when a field is dropped', () => { - const dragging = { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo', - }; - const testState = dragDropState(); - - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, - droppedItem: dragging, - state: testState, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - layerId: 'myLayer', - }); + act(() => { + wrapper.find('[data-test-subj="lns-indexPatternDimension-min"]').first().prop('onClick')!( + {} as React.MouseEvent<{}, MouseEvent> + ); + }); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - myLayer: { - ...testState.layers.myLayer, - columnOrder: ['col1', 'col2'], - columns: { - ...testState.layers.myLayer.columns, - col2: expect.objectContaining({ - dataType: 'number', - sourceField: 'bar', - }), - }, - }, - }, - }); + expect(changeColumn).toHaveBeenCalledWith({ + state: initialState, + columnId: 'col1', + layerId: 'first', + newColumn: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'min', + }), }); + }); - it('selects the specific operation that was valid on drop', () => { - const dragging = { - field: { type: 'string', name: 'mystring', aggregatable: true }, - indexPatternId: 'foo', - }; - const testState = dragDropState(); - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, - droppedItem: dragging, - state: testState, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed, - layerId: 'myLayer', - }); + it('should clear the dimension when removing the selection in field combobox', () => { + wrapper = mount(); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - myLayer: { - ...testState.layers.myLayer, - columnOrder: ['col1', 'col2'], - columns: { - ...testState.layers.myLayer.columns, - col2: expect.objectContaining({ - dataType: 'string', - sourceField: 'mystring', - }), - }, - }, - }, - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('onChange')!([]); }); - it('updates a column when a field is dropped', () => { - const dragging = { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo', - }; - const testState = dragDropState(); - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + indexPatternId: '1', + columns: {}, + columnOrder: [], }, - droppedItem: dragging, - state: testState, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - layerId: 'myLayer', - }); + }, + }); + }); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - myLayer: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - dataType: 'number', - sourceField: 'bar', - }), - }), - }), - }, - }); + it('allows custom format', () => { + const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Average of memory', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'memory', + }, }); - it('does not set the size of the terms aggregation', () => { - const dragging = { - field: { type: 'string', name: 'mystring', aggregatable: true }, - indexPatternId: 'foo', - }; - const testState = dragDropState(); - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, - droppedItem: dragging, - state: testState, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed, - layerId: 'myLayer', - }); + wrapper = mount( + + ); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - myLayer: { - ...testState.layers.myLayer, - columnOrder: ['col1', 'col2'], - columns: { - ...testState.layers.myLayer.columns, - col2: expect.objectContaining({ - operationType: 'terms', - params: expect.objectContaining({ size: 3 }), - }), - }, - }, - }, - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: 'bytes', label: 'Bytes' }]); }); - it('updates the column id when moving an operation to an empty dimension', () => { - const dragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'myLayer', - }; - const testState = dragDropState(); - - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + params: { + format: { id: 'bytes', params: { decimals: 2 } }, + }, + }), + }, }, - droppedItem: dragging, - state: testState, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => true, - layerId: 'myLayer', - }); + }, + }); + }); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - myLayer: { - ...testState.layers.myLayer, - columnOrder: ['col2'], - columns: { - col2: testState.layers.myLayer.columns.col1, - }, - }, + it('keeps decimal places while switching', () => { + const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Average of memory', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'memory', + params: { + format: { id: 'bytes', params: { decimals: 0 } }, }, - }); + }, }); + wrapper = mount( + + ); - it('replaces an operation when moving to a populated dimension', () => { - const dragging = { - columnId: 'col2', - groupId: 'a', - layerId: 'myLayer', - }; - const testState = dragDropState(); - testState.layers.myLayer = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: testState.layers.myLayer.columns.col1, + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: '', label: 'Default' }]); + }); - col2: { - label: 'Top values of src', - dataType: 'string', - isBucketed: true, + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: 'number', label: 'Number' }]); + }); - // Private - operationType: 'terms', - params: { - orderBy: { type: 'column', columnId: 'col3' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'src', - }, - col3: { - label: 'Count', - dataType: 'number', - isBucketed: false, + expect( + wrapper + .find(EuiRange) + .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .prop('value') + ).toEqual(0); + }); - // Private - operationType: 'count', - sourceField: 'Records', - }, + it('allows custom format with number of decimal places', () => { + const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Average of memory', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'memory', + params: { + format: { id: 'bytes', params: { decimals: 2 } }, }, - }; + }, + }); - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, - droppedItem: dragging, - state: testState, - columnId: 'col1', - filterOperations: (op: OperationMetadata) => true, - layerId: 'myLayer', - }); + wrapper = mount( + + ); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - myLayer: { - ...testState.layers.myLayer, - columnOrder: ['col1', 'col3'], - columns: { - col1: testState.layers.myLayer.columns.col2, - col3: testState.layers.myLayer.columns.col3, - }, + act(() => { + wrapper + .find(EuiRange) + .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .prop('onChange')!({ currentTarget: { value: '0' } }); + }); + + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + params: { + format: { id: 'bytes', params: { decimals: 0 } }, + }, + }), }, }, - }); + }, }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index ff6840bc16a5..2efdedceb4db 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -4,28 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiLink, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { - DatasourceDimensionTriggerProps, - DatasourceDimensionEditorProps, - DatasourceDimensionDropProps, - DatasourceDimensionDropHandlerProps, - isDraggedOperation, -} from '../../types'; +import { DatasourceDimensionTriggerProps, DatasourceDimensionEditorProps } from '../../types'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; -import { IndexPatternColumn, OperationType } from '../indexpattern'; -import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations'; +import { IndexPatternColumn } from '../indexpattern'; +import { fieldIsInvalid } from '../utils'; +import { IndexPatternPrivateState } from '../types'; import { DimensionEditor } from './dimension_editor'; -import { changeColumn } from '../state_helpers'; -import { isDraggedField, hasField, fieldIsInvalid } from '../utils'; -import { IndexPatternPrivateState, IndexPatternField } from '../types'; -import { trackUiEvent } from '../../lens_ui_telemetry'; import { DateRange } from '../../../common'; +import { getOperationSupportMatrix } from './operation_support'; export type IndexPatternDimensionTriggerProps = DatasourceDimensionTriggerProps< IndexPatternPrivateState @@ -46,189 +37,6 @@ export type IndexPatternDimensionEditorProps = DatasourceDimensionEditorProps< dateRange: DateRange; }; -export interface OperationSupportMatrix { - operationByField: Partial>; - operationWithoutField: OperationType[]; - fieldByOperation: Partial>; -} - -type Props = Pick< - DatasourceDimensionDropProps, - 'layerId' | 'columnId' | 'state' | 'filterOperations' ->; - -// TODO: This code has historically been memoized, as a potentially performance -// sensitive task. If we can add memoization without breaking the behavior, we should. -const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => { - const layerId = props.layerId; - const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; - - const filteredOperationsByMetadata = getAvailableOperationsByMetadata( - currentIndexPattern - ).filter((operation) => props.filterOperations(operation.operationMetaData)); - - const supportedOperationsByField: Partial> = {}; - const supportedOperationsWithoutField: OperationType[] = []; - const supportedFieldsByOperation: Partial> = {}; - - filteredOperationsByMetadata.forEach(({ operations }) => { - operations.forEach((operation) => { - if (operation.type === 'field') { - if (supportedOperationsByField[operation.field]) { - supportedOperationsByField[operation.field]!.push(operation.operationType); - } else { - supportedOperationsByField[operation.field] = [operation.operationType]; - } - - if (supportedFieldsByOperation[operation.operationType]) { - supportedFieldsByOperation[operation.operationType]!.push(operation.field); - } else { - supportedFieldsByOperation[operation.operationType] = [operation.field]; - } - } else if (operation.type === 'none') { - supportedOperationsWithoutField.push(operation.operationType); - } - }); - }); - return { - operationByField: _.mapValues(supportedOperationsByField, _.uniq), - operationWithoutField: _.uniq(supportedOperationsWithoutField), - fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), - }; -}; - -export function canHandleDrop(props: DatasourceDimensionDropProps) { - const operationSupportMatrix = getOperationSupportMatrix(props); - - const { dragging } = props.dragDropContext; - const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; - - function hasOperationForField(field: IndexPatternField) { - return Boolean(operationSupportMatrix.operationByField[field.name]); - } - - if (isDraggedField(dragging)) { - return ( - layerIndexPatternId === dragging.indexPatternId && - Boolean(hasOperationForField(dragging.field)) - ); - } - - if ( - isDraggedOperation(dragging) && - dragging.layerId === props.layerId && - props.columnId !== dragging.columnId - ) { - const op = props.state.layers[props.layerId].columns[dragging.columnId]; - return props.filterOperations(op); - } - return false; -} - -export function onDrop(props: DatasourceDimensionDropHandlerProps) { - const operationSupportMatrix = getOperationSupportMatrix(props); - const droppedItem = props.droppedItem; - - function hasOperationForField(field: IndexPatternField) { - return Boolean(operationSupportMatrix.operationByField[field.name]); - } - - if (isDraggedOperation(droppedItem) && droppedItem.layerId === props.layerId) { - const layer = props.state.layers[props.layerId]; - const op = { ...layer.columns[droppedItem.columnId] }; - if (!props.filterOperations(op)) { - return false; - } - - const newColumns = { ...layer.columns }; - delete newColumns[droppedItem.columnId]; - newColumns[props.columnId] = op; - - const newColumnOrder = [...layer.columnOrder]; - const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId); - const newIndex = newColumnOrder.findIndex((c) => c === props.columnId); - - if (newIndex === -1) { - newColumnOrder[oldIndex] = props.columnId; - } else { - newColumnOrder.splice(oldIndex, 1); - } - - // Time to replace - props.setState({ - ...props.state, - layers: { - ...props.state.layers, - [props.layerId]: { - ...layer, - columnOrder: newColumnOrder, - columns: newColumns, - }, - }, - }); - return { deleted: droppedItem.columnId }; - } - - if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { - // TODO: What do we do if we couldn't find a column? - return false; - } - - const operationsForNewField = operationSupportMatrix.operationByField[droppedItem.field.name]; - - const layerId = props.layerId; - const selectedColumn: IndexPatternColumn | null = - props.state.layers[layerId].columns[props.columnId] || null; - const currentIndexPattern = - props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; - - // We need to check if dragging in a new field, was just a field change on the same - // index pattern and on the same operations (therefore checking if the new field supports - // our previous operation) - const hasFieldChanged = - selectedColumn && - hasField(selectedColumn) && - selectedColumn.sourceField !== droppedItem.field.name && - operationsForNewField && - operationsForNewField.includes(selectedColumn.operationType); - - if (!operationsForNewField || operationsForNewField.length === 0) { - return false; - } - - // If only the field has changed use the onFieldChange method on the operation to get the - // new column, otherwise use the regular buildColumn to get a new column. - const newColumn = hasFieldChanged - ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) - : buildColumn({ - op: operationsForNewField[0], - columns: props.state.layers[props.layerId].columns, - indexPattern: currentIndexPattern, - layerId, - suggestedPriority: props.suggestedPriority, - field: droppedItem.field, - previousColumn: selectedColumn, - }); - - trackUiEvent('drop_onto_dimension'); - const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length); - trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); - - props.setState( - changeColumn({ - state: props.state, - layerId, - columnId: props.columnId, - newColumn, - // If the field has changed, the onFieldChange method needs to take care of everything including moving - // over params. If we create a new column above we want changeColumn to move over params. - keepParams: !hasFieldChanged, - }) - ); - - return true; -} - function wrapOnDot(str?: string) { // u200B is a non-width white-space character, which allows // the browser to efficiently word-wrap right after the dot diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts new file mode 100644 index 000000000000..f943246ebc48 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -0,0 +1,594 @@ +/* + * 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 { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; +import { IndexPatternDimensionEditorProps } from './dimension_panel'; +import { onDrop, canHandleDrop } from './droppable'; +import { DragContextState } from '../../drag_drop'; +import { createMockedDragDropContext } from '../mocks'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { IndexPatternPrivateState } from '../types'; +import { documentField } from '../document_field'; +import { OperationMetadata } from '../../types'; + +jest.mock('../state_helpers'); + +const expectedIndexPatterns = { + 1: { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasExistence: true, + hasRestrictions: false, + fields: [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + documentField, + ], + }, +}; + +/** + * The datasource exposes four main pieces of code which are tested at + * an integration test level. The main reason for this fairly high level + * of testing is that there is a lot of UI logic that isn't easily + * unit tested, such as the transient invalid state. + * + * - Dimension trigger: Not tested here + * - Dimension editor component: First half of the tests + * + * - canHandleDrop: Tests for dropping of fields or other dimensions + * - onDrop: Correct application of drop logic + */ +describe('IndexPatternDimensionEditorPanel', () => { + let state: IndexPatternPrivateState; + let setState: jest.Mock; + let defaultProps: IndexPatternDimensionEditorProps; + let dragDropContext: DragContextState; + + beforeEach(() => { + state = { + indexPatternRefs: [], + indexPatterns: expectedIndexPatterns, + currentIndexPatternId: '1', + isFirstExistenceFetch: false, + existingFields: { + 'my-fake-index-pattern': { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + }, + }, + }, + }, + }; + + setState = jest.fn(); + + dragDropContext = createMockedDragDropContext(); + + defaultProps = { + state, + setState, + dateRange: { fromDate: 'now-1d', toDate: 'now' }, + columnId: 'col1', + layerId: 'first', + uniqueLabel: 'stuff', + filterOperations: () => true, + storage: {} as IStorageWrapper, + uiSettings: {} as IUiSettingsClient, + savedObjectsClient: {} as SavedObjectsClientContract, + http: {} as HttpSetup, + data: ({ + fieldFormats: ({ + getType: jest.fn().mockReturnValue({ + id: 'number', + title: 'Number', + }), + getDefaultType: jest.fn().mockReturnValue({ + id: 'bytes', + title: 'Bytes', + }), + } as unknown) as DataPublicPluginStart['fieldFormats'], + } as unknown) as DataPublicPluginStart, + core: {} as CoreSetup, + }; + + jest.clearAllMocks(); + }); + + function dragDropState(): IndexPatternPrivateState { + return { + indexPatternRefs: [], + existingFields: {}, + indexPatterns: { + foo: { + id: 'foo', + title: 'Foo pattern', + hasRestrictions: false, + fields: [ + { + aggregatable: true, + name: 'bar', + displayName: 'bar', + searchable: true, + type: 'number', + }, + { + aggregatable: true, + name: 'mystring', + displayName: 'mystring', + searchable: true, + type: 'string', + }, + ], + }, + }, + currentIndexPatternId: '1', + isFirstExistenceFetch: false, + layers: { + myLayer: { + indexPatternId: 'foo', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + }, + }, + }, + }, + }; + } + + it('is not droppable if no drag is happening', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext, + state: dragDropState(), + layerId: 'myLayer', + }) + ).toBe(false); + }); + + it('is not droppable if the dragged item has no field', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { name: 'bar' }, + }, + }) + ).toBe(false); + }); + + it('is not droppable if field is not supported by filterOperations', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + indexPatternId: 'foo', + field: { type: 'string', name: 'mystring', aggregatable: true }, + }, + }, + state: dragDropState(), + filterOperations: () => false, + layerId: 'myLayer', + }) + ).toBe(false); + }); + + it('is droppable if the field is supported by filterOperations', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + field: { type: 'number', name: 'bar', aggregatable: true }, + indexPatternId: 'foo', + }, + }, + state: dragDropState(), + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(true); + }); + + it('is not droppable if the field belongs to another index pattern', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + field: { type: 'number', name: 'bar', aggregatable: true }, + indexPatternId: 'foo2', + }, + }, + state: dragDropState(), + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(false); + }); + + it('is droppable if the dragged column is compatible', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }, + }, + state: dragDropState(), + columnId: 'col2', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }) + ).toBe(true); + }); + + it('is not droppable if the dragged column is the same as the current column', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }, + }, + state: dragDropState(), + columnId: 'col1', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }) + ).toBe(false); + }); + + it('is not droppable if the dragged column is incompatible', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }, + }, + state: dragDropState(), + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(false); + }); + + it('appends the dropped column when a field is dropped', () => { + const dragging = { + field: { type: 'number', name: 'bar', aggregatable: true }, + indexPatternId: 'foo', + }; + const testState = dragDropState(); + + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col1', 'col2'], + columns: { + ...testState.layers.myLayer.columns, + col2: expect.objectContaining({ + dataType: 'number', + sourceField: 'bar', + }), + }, + }, + }, + }); + }); + + it('selects the specific operation that was valid on drop', () => { + const dragging = { + field: { type: 'string', name: 'mystring', aggregatable: true }, + indexPatternId: 'foo', + }; + const testState = dragDropState(); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed, + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col1', 'col2'], + columns: { + ...testState.layers.myLayer.columns, + col2: expect.objectContaining({ + dataType: 'string', + sourceField: 'mystring', + }), + }, + }, + }, + }); + }); + + it('updates a column when a field is dropped', () => { + const dragging = { + field: { type: 'number', name: 'bar', aggregatable: true }, + indexPatternId: 'foo', + }; + const testState = dragDropState(); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + dataType: 'number', + sourceField: 'bar', + }), + }), + }), + }, + }); + }); + + it('does not set the size of the terms aggregation', () => { + const dragging = { + field: { type: 'string', name: 'mystring', aggregatable: true }, + indexPatternId: 'foo', + }; + const testState = dragDropState(); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed, + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col1', 'col2'], + columns: { + ...testState.layers.myLayer.columns, + col2: expect.objectContaining({ + operationType: 'terms', + params: expect.objectContaining({ size: 3 }), + }), + }, + }, + }, + }); + }); + + it('updates the column id when moving an operation to an empty dimension', () => { + const dragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }; + const testState = dragDropState(); + + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col2'], + columns: { + col2: testState.layers.myLayer.columns.col1, + }, + }, + }, + }); + }); + + it('replaces an operation when moving to a populated dimension', () => { + const dragging = { + columnId: 'col2', + groupId: 'a', + layerId: 'myLayer', + }; + const testState = dragDropState(); + testState.layers.myLayer = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.myLayer.columns.col1, + + col2: { + label: 'Top values of src', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col3' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'src', + }, + col3: { + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + sourceField: 'Records', + }, + }, + }; + + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col1', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col1', 'col3'], + columns: { + col1: testState.layers.myLayer.columns.col2, + col3: testState.layers.myLayer.columns.col3, + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts new file mode 100644 index 000000000000..01674a7411b9 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -0,0 +1,149 @@ +/* + * 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 { + DatasourceDimensionDropProps, + DatasourceDimensionDropHandlerProps, + isDraggedOperation, +} from '../../types'; +import { IndexPatternColumn } from '../indexpattern'; +import { buildColumn, changeField } from '../operations'; +import { changeColumn, mergeLayer } from '../state_helpers'; +import { isDraggedField, hasField } from '../utils'; +import { IndexPatternPrivateState, IndexPatternField } from '../types'; +import { trackUiEvent } from '../../lens_ui_telemetry'; +import { getOperationSupportMatrix } from './operation_support'; + +export function canHandleDrop(props: DatasourceDimensionDropProps) { + const operationSupportMatrix = getOperationSupportMatrix(props); + + const { dragging } = props.dragDropContext; + const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; + + function hasOperationForField(field: IndexPatternField) { + return Boolean(operationSupportMatrix.operationByField[field.name]); + } + + if (isDraggedField(dragging)) { + return ( + layerIndexPatternId === dragging.indexPatternId && + Boolean(hasOperationForField(dragging.field)) + ); + } + + if ( + isDraggedOperation(dragging) && + dragging.layerId === props.layerId && + props.columnId !== dragging.columnId + ) { + const op = props.state.layers[props.layerId].columns[dragging.columnId]; + return props.filterOperations(op); + } + return false; +} + +export function onDrop(props: DatasourceDimensionDropHandlerProps) { + const operationSupportMatrix = getOperationSupportMatrix(props); + const droppedItem = props.droppedItem; + + function hasOperationForField(field: IndexPatternField) { + return Boolean(operationSupportMatrix.operationByField[field.name]); + } + + if (isDraggedOperation(droppedItem) && droppedItem.layerId === props.layerId) { + const layer = props.state.layers[props.layerId]; + const op = { ...layer.columns[droppedItem.columnId] }; + if (!props.filterOperations(op)) { + return false; + } + + const newColumns = { ...layer.columns }; + delete newColumns[droppedItem.columnId]; + newColumns[props.columnId] = op; + + const newColumnOrder = [...layer.columnOrder]; + const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId); + const newIndex = newColumnOrder.findIndex((c) => c === props.columnId); + + if (newIndex === -1) { + newColumnOrder[oldIndex] = props.columnId; + } else { + newColumnOrder.splice(oldIndex, 1); + } + + // Time to replace + props.setState( + mergeLayer({ + state: props.state, + layerId: props.layerId, + newLayer: { + columnOrder: newColumnOrder, + columns: newColumns, + }, + }) + ); + return { deleted: droppedItem.columnId }; + } + + if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { + // TODO: What do we do if we couldn't find a column? + return false; + } + + const operationsForNewField = operationSupportMatrix.operationByField[droppedItem.field.name]; + + const layerId = props.layerId; + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + const currentIndexPattern = + props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; + + // We need to check if dragging in a new field, was just a field change on the same + // index pattern and on the same operations (therefore checking if the new field supports + // our previous operation) + const hasFieldChanged = + selectedColumn && + hasField(selectedColumn) && + selectedColumn.sourceField !== droppedItem.field.name && + operationsForNewField && + operationsForNewField.includes(selectedColumn.operationType); + + if (!operationsForNewField || operationsForNewField.length === 0) { + return false; + } + + // If only the field has changed use the onFieldChange method on the operation to get the + // new column, otherwise use the regular buildColumn to get a new column. + const newColumn = hasFieldChanged + ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) + : buildColumn({ + op: operationsForNewField[0], + columns: props.state.layers[props.layerId].columns, + indexPattern: currentIndexPattern, + layerId, + suggestedPriority: props.suggestedPriority, + field: droppedItem.field, + previousColumn: selectedColumn, + }); + + trackUiEvent('drop_onto_dimension'); + const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length); + trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); + + props.setState( + changeColumn({ + state: props.state, + layerId, + columnId: props.columnId, + newColumn, + // If the field has changed, the onFieldChange method needs to take care of everything including moving + // over params. If we create a new column above we want changeColumn to move over params. + keepParams: !hasFieldChanged, + }) + ); + + return true; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index c2e179fd13a2..b1b77f193012 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -20,7 +20,7 @@ import { EuiHighlight } from '@elastic/eui'; import { OperationType } from '../indexpattern'; import { LensFieldIcon } from '../lens_field_icon'; import { DataType } from '../../types'; -import { OperationSupportMatrix } from './dimension_panel'; +import { OperationSupportMatrix } from './operation_support'; import { IndexPattern, IndexPatternField, IndexPatternPrivateState } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { fieldExists } from '../pure_helpers'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts index 88e5588ce0e0..92a91d5b5086 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts @@ -5,3 +5,5 @@ */ export * from './dimension_panel'; +export * from './droppable'; +export * from './operation_support'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts new file mode 100644 index 000000000000..2ea28da20155 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.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 _ from 'lodash'; +import { DatasourceDimensionDropProps } from '../../types'; +import { OperationType } from '../indexpattern'; +import { getAvailableOperationsByMetadata } from '../operations'; +import { IndexPatternPrivateState } from '../types'; + +export interface OperationSupportMatrix { + operationByField: Partial>; + operationWithoutField: OperationType[]; + fieldByOperation: Partial>; +} + +type Props = Pick< + DatasourceDimensionDropProps, + 'layerId' | 'columnId' | 'state' | 'filterOperations' +>; + +// TODO: This code has historically been memoized, as a potentially performance +// sensitive task. If we can add memoization without breaking the behavior, we should. +export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => { + const layerId = props.layerId; + const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; + + const filteredOperationsByMetadata = getAvailableOperationsByMetadata( + currentIndexPattern + ).filter((operation) => props.filterOperations(operation.operationMetaData)); + + const supportedOperationsByField: Partial> = {}; + const supportedOperationsWithoutField: OperationType[] = []; + const supportedFieldsByOperation: Partial> = {}; + + filteredOperationsByMetadata.forEach(({ operations }) => { + operations.forEach((operation) => { + if (operation.type === 'field') { + supportedOperationsByField[operation.field] = [ + ...(supportedOperationsByField[operation.field] ?? []), + operation.operationType, + ]; + + supportedFieldsByOperation[operation.operationType] = [ + ...(supportedFieldsByOperation[operation.operationType] ?? []), + operation.field, + ]; + } else if (operation.type === 'none') { + supportedOperationsWithoutField.push(operation.operationType); + } + }); + }); + return { + operationByField: _.mapValues(supportedOperationsByField, _.uniq), + operationWithoutField: _.uniq(supportedOperationsWithoutField), + fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), + }; +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index a480cfe40898..c8cb9fcb33ba 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -1381,6 +1381,126 @@ describe('IndexPattern Data Source suggestions', () => { ); }); + it('does not create an over time suggestion if tables with numeric buckets with time dimension', async () => { + const initialState = testInitialState(); + const state: IndexPatternPrivateState = { + ...initialState, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['colb', 'cola'], + columns: { + cola: { + dataType: 'number', + isBucketed: false, + sourceField: 'dest', + label: 'Unique count of dest', + operationType: 'cardinality', + }, + colb: { + label: 'My Op', + dataType: 'number', + isBucketed: true, + operationType: 'range', + sourceField: 'bytes', + scale: 'interval', + params: { + type: 'histogram', + maxBars: 100, + ranges: [], + }, + }, + }, + }, + }, + }; + + expect(getDatasourceSuggestionsFromCurrentState(state)).not.toContainEqual( + expect.objectContaining({ + table: { + isMultiRow: true, + label: 'Over time', + layerId: 'first', + }, + }) + ); + }); + + it('adds date histogram over default time field for custom range intervals', async () => { + const initialState = testInitialState(); + const state: IndexPatternPrivateState = { + ...initialState, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['colb', 'cola'], + columns: { + cola: { + dataType: 'number', + isBucketed: false, + sourceField: 'dest', + label: 'Unique count of dest', + operationType: 'cardinality', + }, + colb: { + label: 'My Custom Range', + dataType: 'string', + isBucketed: true, + operationType: 'range', + sourceField: 'bytes', + scale: 'ordinal', + params: { + type: 'range', + maxBars: 100, + ranges: [{ from: 1, to: 2, label: '' }], + }, + }, + }, + }, + }, + }; + + expect(getDatasourceSuggestionsFromCurrentState(state)).toContainEqual( + expect.objectContaining({ + table: { + changeType: 'extended', + columns: [ + { + columnId: 'colb', + operation: { + dataType: 'string', + isBucketed: true, + label: 'My Custom Range', + scale: 'ordinal', + }, + }, + { + columnId: 'id1', + operation: { + dataType: 'date', + isBucketed: true, + label: 'timestampLabel', + scale: 'interval', + }, + }, + { + columnId: 'cola', + operation: { + dataType: 'number', + isBucketed: false, + label: 'Unique count of dest', + scale: undefined, + }, + }, + ], + isMultiRow: true, + label: 'Over time', + layerId: 'first', + }, + }) + ); + }); + it('does not create an over time suggestion if there is no default time field', async () => { const initialState = testInitialState(); const state: IndexPatternPrivateState = { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index c7eeef178c25..098569d1f460 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -455,6 +455,10 @@ export function getDatasourceSuggestionsFromCurrentState( ({ name }) => name === indexPattern.timeFieldName ); + const hasNumericDimension = + buckets.length === 1 && + buckets.some((columnId) => layer.columns[columnId].dataType === 'number'); + const suggestions: Array> = []; if (metrics.length === 0) { // intermediary chart without metric, don't try to suggest reduced versions @@ -482,7 +486,9 @@ export function getDatasourceSuggestionsFromCurrentState( } else { suggestions.push(...createSimplifiedTableSuggestions(state, layerId)); - if (!timeDimension && timeField) { + // base range intervals are of number dataType. + // Custom range/intervals have a different dataType so they still receive the Over Time suggestion + if (!timeDimension && timeField && !hasNumericDimension) { // suggest current configuration over time if there is a default time field // and no time dimension yet suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId, timeField)); @@ -653,9 +659,13 @@ function createSuggestionWithDefaultDateHistogram( field: timeField, suggestedPriority: undefined, }); + const updatedLayer = { indexPatternId: layer.indexPatternId, - columns: { ...layer.columns, [newId]: timeColumn }, + columns: { + ...layer.columns, + [newId]: timeColumn, + }, columnOrder: [...buckets, newId, ...metrics], }; return buildSuggestion({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 38aec866ca5c..735015492bd5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -18,6 +18,8 @@ import { SumIndexPatternColumn, maxOperation, MaxIndexPatternColumn, + medianOperation, + MedianIndexPatternColumn, } from './metrics'; import { dateHistogramOperation, DateHistogramIndexPatternColumn } from './date_histogram'; import { countOperation, CountIndexPatternColumn } from './count'; @@ -43,6 +45,7 @@ export type IndexPatternColumn = | AvgIndexPatternColumn | CardinalityIndexPatternColumn | SumIndexPatternColumn + | MedianIndexPatternColumn | CountIndexPatternColumn; export type FieldBasedIndexPatternColumn = Extract; @@ -59,6 +62,7 @@ const internalOperationDefinitions = [ averageOperation, cardinalityOperation, sumOperation, + medianOperation, countOperation, rangeOperation, ]; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index c02f7bcb7d2c..1d3ecc165ce7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -87,6 +87,7 @@ export type SumIndexPatternColumn = MetricColumn<'sum'>; export type AvgIndexPatternColumn = MetricColumn<'avg'>; export type MinIndexPatternColumn = MetricColumn<'min'>; export type MaxIndexPatternColumn = MetricColumn<'max'>; +export type MedianIndexPatternColumn = MetricColumn<'median'>; export const minOperation = buildMetricOperation({ type: 'min', @@ -137,3 +138,15 @@ export const sumOperation = buildMetricOperation({ values: { name }, }), }); + +export const medianOperation = buildMetricOperation({ + type: 'median', + displayName: i18n.translate('xpack.lens.indexPattern.median', { + defaultMessage: 'Median', + }), + ofName: (name) => + i18n.translate('xpack.lens.indexPattern.medianOf', { + defaultMessage: 'Median of {name}', + values: { name }, + }), +}); 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/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index c1bd4b84099b..6808bc724f26 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -315,12 +315,12 @@ describe('getOperationTypesForField', () => { }, Object { "field": "bytes", - "operationType": "min", + "operationType": "max", "type": "field", }, Object { "field": "bytes", - "operationType": "max", + "operationType": "min", "type": "field", }, Object { @@ -338,6 +338,11 @@ describe('getOperationTypesForField', () => { "operationType": "cardinality", "type": "field", }, + Object { + "field": "bytes", + "operationType": "median", + "type": "field", + }, ], }, ] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts index 4bfd6a4f93c7..43285d657dd4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts @@ -5,16 +5,16 @@ */ import { renameColumns } from './rename_columns'; -import { KibanaDatatable } from '../../../../../src/plugins/expressions/public'; +import { Datatable } from '../../../../../src/plugins/expressions/public'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; describe('rename_columns', () => { it('should rename columns of a given datatable', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', + const input: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, + { id: 'a', name: 'A', meta: { type: 'number' } }, + { id: 'b', name: 'B', meta: { type: 'number' } }, ], rows: [ { a: 1, b: 2 }, @@ -46,10 +46,16 @@ describe('rename_columns', () => { "columns": Array [ Object { "id": "b", + "meta": Object { + "type": "number", + }, "name": "Austrailia", }, Object { "id": "c", + "meta": Object { + "type": "number", + }, "name": "Boomerang", }, ], @@ -71,15 +77,15 @@ describe('rename_columns', () => { "c": 8, }, ], - "type": "kibana_datatable", + "type": "datatable", } `); }); it('should replace "" with a visible value', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', - columns: [{ id: 'a', name: 'A' }], + const input: Datatable = { + type: 'datatable', + columns: [{ id: 'a', name: 'A', meta: { type: 'string' } }], rows: [{ a: '' }], }; @@ -100,11 +106,11 @@ describe('rename_columns', () => { }); it('should keep columns which are not mapped', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', + const input: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, + { id: 'a', name: 'A', meta: { type: 'number' } }, + { id: 'b', name: 'B', meta: { type: 'number' } }, ], rows: [ { a: 1, b: 2 }, @@ -129,10 +135,16 @@ describe('rename_columns', () => { "columns": Array [ Object { "id": "a", + "meta": Object { + "type": "number", + }, "name": "A", }, Object { "id": "c", + "meta": Object { + "type": "number", + }, "name": "Catamaran", }, ], @@ -154,17 +166,17 @@ describe('rename_columns', () => { "c": 8, }, ], - "type": "kibana_datatable", + "type": "datatable", } `); }); it('should rename date histograms', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', + const input: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'banana per 30 seconds' }, + { id: 'a', name: 'A', meta: { type: 'number' } }, + { id: 'b', name: 'banana per 30 seconds', meta: { type: 'number' } }, ], rows: [ { a: 1, b: 2 }, @@ -189,10 +201,16 @@ describe('rename_columns', () => { "columns": Array [ Object { "id": "a", + "meta": Object { + "type": "number", + }, "name": "A", }, Object { "id": "c", + "meta": Object { + "type": "number", + }, "name": "Apple per 30 seconds", }, ], @@ -214,7 +232,7 @@ describe('rename_columns', () => { "c": 8, }, ], - "type": "kibana_datatable", + "type": "datatable", } `); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts index bf938a3e05ef..74f143225e29 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts @@ -5,11 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - KibanaDatatableColumn, -} from 'src/plugins/expressions'; +import { ExpressionFunctionDefinition, Datatable, DatatableColumn } from 'src/plugins/expressions'; import { IndexPatternColumn } from './operations'; interface RemapArgs { @@ -20,12 +16,12 @@ export type OriginalColumn = { id: string } & IndexPatternColumn; export const renameColumns: ExpressionFunctionDefinition< 'lens_rename_columns', - KibanaDatatable, + Datatable, RemapArgs, - KibanaDatatable + Datatable > = { name: 'lens_rename_columns', - type: 'kibana_datatable', + type: 'datatable', help: i18n.translate('xpack.lens.functions.renameColumns.help', { defaultMessage: 'A helper to rename the columns of a datatable', }), @@ -38,12 +34,12 @@ export const renameColumns: ExpressionFunctionDefinition< }), }, }, - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], fn(data, { idMap: encodedIdMap }) { const idMap = JSON.parse(encodedIdMap) as Record; return { - type: 'kibana_datatable', + type: 'datatable', rows: data.rows.map((row) => { const mappedRow: Record = {}; Object.entries(idMap).forEach(([fromId, toId]) => { @@ -77,7 +73,7 @@ export const renameColumns: ExpressionFunctionDefinition< }, }; -function getColumnName(originalColumn: OriginalColumn, newColumn: KibanaDatatableColumn) { +function getColumnName(originalColumn: OriginalColumn, newColumn: DatatableColumn) { if (originalColumn && originalColumn.operationType === 'date_histogram') { const fieldName = originalColumn.sourceField; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts index 7b6eb11efc49..da90a2ce5fce 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts @@ -452,22 +452,32 @@ describe('state_helpers', () => { describe('getColumnOrder', () => { it('should work for empty columns', () => { - expect(getColumnOrder({})).toEqual([]); + expect( + getColumnOrder({ + indexPatternId: '', + columnOrder: [], + columns: {}, + }) + ).toEqual([]); }); it('should work for one column', () => { expect( getColumnOrder({ - col1: { - label: 'Value of timestamp', - dataType: 'string', - isBucketed: false, - - // Private - operationType: 'date_histogram', - sourceField: 'timestamp', - params: { - interval: 'h', + columnOrder: [], + indexPatternId: '', + columns: { + col1: { + label: 'Value of timestamp', + dataType: 'string', + isBucketed: false, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'h', + }, }, }, }) @@ -477,41 +487,45 @@ describe('state_helpers', () => { it('should put any number of aggregations before metrics', () => { expect( getColumnOrder({ - col1: { - label: 'Top values of category', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - sourceField: 'category', - params: { - size: 5, - orderBy: { - type: 'alphabetical', + columnOrder: [], + indexPatternId: '', + columns: { + col1: { + label: 'Top values of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'category', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'asc', }, - orderDirection: 'asc', }, - }, - col2: { - label: 'Average of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'avg', - sourceField: 'bytes', - }, - col3: { - label: 'Date histogram of timestamp', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - sourceField: 'timestamp', - params: { - interval: '1d', + col2: { + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'avg', + sourceField: 'bytes', + }, + col3: { + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: '1d', + }, }, }, }) @@ -521,44 +535,48 @@ describe('state_helpers', () => { it('should reorder aggregations based on suggested priority', () => { expect( getColumnOrder({ - col1: { - label: 'Top values of category', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - sourceField: 'category', - params: { - size: 5, - orderBy: { - type: 'alphabetical', + indexPatternId: '', + columnOrder: [], + columns: { + col1: { + label: 'Top values of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'category', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'asc', }, - orderDirection: 'asc', + suggestedPriority: 2, }, - suggestedPriority: 2, - }, - col2: { - label: 'Average of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'avg', - sourceField: 'bytes', - suggestedPriority: 0, - }, - col3: { - label: 'Date histogram of timestamp', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - sourceField: 'timestamp', - suggestedPriority: 1, - params: { - interval: '1d', + col2: { + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'avg', + sourceField: 'bytes', + suggestedPriority: 0, + }, + col3: { + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + suggestedPriority: 1, + params: { + interval: '1d', + }, }, }, }) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts index c977a7e0fa37..2e92d4ad8f88 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts @@ -25,25 +25,24 @@ export function updateColumnParam column === currentColumn )![0]; - return { - ...state, - layers: { - ...state.layers, - [layerId]: { - ...state.layers[layerId], - columns: { - ...state.layers[layerId].columns, - [columnId]: { - ...currentColumn, - params: { - ...currentColumn.params, - [paramName]: value, - }, + const layer = state.layers[layerId]; + + return mergeLayer({ + state, + layerId, + newLayer: { + columns: { + ...layer.columns, + [columnId]: { + ...currentColumn, + params: { + ...currentColumn.params, + [paramName]: value, }, }, }, }, - }; + }); } function adjustColumnReferencesForChangedColumn( @@ -91,25 +90,29 @@ export function changeColumn({ updatedColumn.label = oldColumn.label; } + const layer = { + ...state.layers[layerId], + }; + const newColumns = adjustColumnReferencesForChangedColumn( { - ...state.layers[layerId].columns, + ...layer.columns, [columnId]: updatedColumn, }, columnId ); - return { - ...state, - layers: { - ...state.layers, - [layerId]: { - ...state.layers[layerId], - columnOrder: getColumnOrder(newColumns), + return mergeLayer({ + state, + layerId, + newLayer: { + columnOrder: getColumnOrder({ + ...layer, columns: newColumns, - }, + }), + columns: newColumns, }, - }; + }); } export function deleteColumn({ @@ -125,24 +128,26 @@ export function deleteColumn({ delete hypotheticalColumns[columnId]; const newColumns = adjustColumnReferencesForChangedColumn(hypotheticalColumns, columnId); + const layer = { + ...state.layers[layerId], + columns: newColumns, + }; - return { - ...state, - layers: { - ...state.layers, - [layerId]: { - ...state.layers[layerId], - columnOrder: getColumnOrder(newColumns), - columns: newColumns, - }, + return mergeLayer({ + state, + layerId, + newLayer: { + ...layer, + columnOrder: getColumnOrder(layer), }, - }; + }); } -export function getColumnOrder(columns: Record): string[] { - const entries = Object.entries(columns); - - const [aggregations, metrics] = _.partition(entries, ([id, col]) => col.isBucketed); +export function getColumnOrder(layer: IndexPatternLayer): string[] { + const [aggregations, metrics] = _.partition( + Object.entries(layer.columns), + ([id, col]) => col.isBucketed + ); return aggregations .sort(([id, col], [id2, col2]) => { @@ -156,6 +161,24 @@ export function getColumnOrder(columns: Record): str .concat(metrics.map(([id]) => id)); } +export function mergeLayer({ + state, + layerId, + newLayer, +}: { + state: IndexPatternPrivateState; + layerId: string; + newLayer: Partial; +}) { + return { + ...state, + layers: { + ...state.layers, + [layerId]: { ...state.layers[layerId], ...newLayer }, + }, + }; +} + export function updateLayerIndexPattern( layer: IndexPatternLayer, newIndexPattern: IndexPattern diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts index 9f6feee2877a..a08e8c277742 100644 --- a/x-pack/plugins/lens/public/lens_attribute_service.ts +++ b/x-pack/plugins/lens/public/lens_attribute_service.ts @@ -12,8 +12,9 @@ import { LensByValueInput, LensByReferenceInput, } from './editor_frame_service/embeddable/embeddable'; -import { SavedObjectIndexStore, DOC_TYPE } from './persistence'; +import { SavedObjectIndexStore } from './persistence'; import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; +import { DOC_TYPE } from '../common'; export type LensAttributeService = AttributeService< LensSavedObjectAttributes, diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx index 7e80fcc06dff..88ce026fc269 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx @@ -17,11 +17,11 @@ function sampleArgs() { type: 'lens_multitable', tables: { l1: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'string' } }, + { id: 'b', name: 'b', meta: { type: 'string' } }, + { id: 'c', name: 'c', meta: { type: 'number' } }, ], rows: [{ a: 10110, b: 2, c: 3 }], }, diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.tsx index 58814f62da60..6522a4c45794 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.tsx @@ -136,8 +136,8 @@ export function MetricChart({ } const value = - column && column.formatHint - ? formatFactory(column.formatHint).convert(row[accessor]) + column && column.meta?.params + ? formatFactory(column.meta?.params).convert(row[accessor]) : Number(Number(row[accessor]).toFixed(3)).toString(); return ( diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index c6b3fd2cc0f6..89c2d73539d5 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -10,7 +10,7 @@ import { SavedObjectReference, } from 'kibana/public'; import { Query } from '../../../../../src/plugins/data/public'; -import { PersistableFilter } from '../../common'; +import { DOC_TYPE, PersistableFilter } from '../../common'; export interface Document { savedObjectId?: string; @@ -27,8 +27,6 @@ export interface Document { references: SavedObjectReference[]; } -export const DOC_TYPE = 'lens'; - export interface DocumentSaver { save: (vis: Document) => Promise<{ savedObjectId: string }>; } diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index ac952e307758..8ab1a8b5a58d 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -31,11 +31,11 @@ describe('PieVisualization component', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, ], rows: [ { a: 6, b: 2, c: 'I', d: 'Row 1' }, @@ -138,14 +138,23 @@ describe('PieVisualization component', () => { "columns": Array [ Object { "id": "a", + "meta": Object { + "type": "number", + }, "name": "a", }, Object { "id": "b", + "meta": Object { + "type": "number", + }, "name": "b", }, Object { "id": "c", + "meta": Object { + "type": "string", + }, "name": "c", }, ], @@ -163,7 +172,7 @@ describe('PieVisualization component', () => { "d": "Row 2", }, ], - "type": "kibana_datatable", + "type": "datatable", }, "value": 6, }, diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 8de810f9aa5d..cb2458a76967 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -68,7 +68,7 @@ export function PieComponent( if (!hideLabels) { firstTable.columns.forEach((column) => { - formatters[column.id] = props.formatFactory(column.formatHint); + formatters[column.id] = props.formatFactory(column.meta.params); }); } @@ -108,7 +108,7 @@ export function PieComponent( if (hideLabels || d === EMPTY_SLICE) { return ''; } - if (col.formatHint) { + if (col.meta.params) { return formatters[col.id].convert(d) ?? ''; } return String(d); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts index 8b94ff3236a4..d9ccda2a99ab 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaDatatable } from 'src/plugins/expressions/public'; +import { Datatable } from 'src/plugins/expressions/public'; import { getSliceValueWithFallback, getFilterContext } from './render_helpers'; +import { ColumnGroups } from './types'; describe('render helpers', () => { describe('#getSliceValueWithFallback', () => { describe('without fallback', () => { - const columnGroups = [ - { col: { id: 'a', name: 'A' }, metrics: [] }, - { col: { id: 'b', name: 'C' }, metrics: [] }, + const columnGroups: ColumnGroups = [ + { col: { id: 'a', name: 'A', meta: { type: 'string' } }, metrics: [] }, + { col: { id: 'b', name: 'C', meta: { type: 'string' } }, metrics: [] }, ]; it('returns the metric when positive number', () => { @@ -20,6 +21,7 @@ describe('render helpers', () => { getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: 5 }, columnGroups, { id: 'c', name: 'C', + meta: { type: 'number' }, }) ).toEqual(5); }); @@ -29,6 +31,7 @@ describe('render helpers', () => { getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: -100 }, columnGroups, { id: 'c', name: 'C', + meta: { type: 'number' }, }) ).toEqual(-100); }); @@ -38,15 +41,19 @@ describe('render helpers', () => { getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: 0 }, columnGroups, { id: 'c', name: 'C', + meta: { type: 'number' }, }) ).toEqual(Number.EPSILON); }); }); describe('fallback behavior', () => { - const columnGroups = [ - { col: { id: 'a', name: 'A' }, metrics: [{ id: 'a_subtotal', name: '' }] }, - { col: { id: 'b', name: 'C' }, metrics: [] }, + const columnGroups: ColumnGroups = [ + { + col: { id: 'a', name: 'A', meta: { type: 'string' } }, + metrics: [{ id: 'a_subtotal', name: '', meta: { type: 'number' } }], + }, + { col: { id: 'b', name: 'C', meta: { type: 'string' } }, metrics: [] }, ]; it('falls back to metric from previous column if available', () => { @@ -54,7 +61,7 @@ describe('render helpers', () => { getSliceValueWithFallback( { a: 'Cat', a_subtotal: 5, b: 'Home', c: undefined }, columnGroups, - { id: 'c', name: 'C' } + { id: 'c', name: 'C', meta: { type: 'number' } } ) ).toEqual(5); }); @@ -64,7 +71,7 @@ describe('render helpers', () => { getSliceValueWithFallback( { a: 'Cat', a_subtotal: 0, b: 'Home', c: undefined }, columnGroups, - { id: 'c', name: 'C' } + { id: 'c', name: 'C', meta: { type: 'number' } } ) ).toEqual(Number.EPSILON); }); @@ -74,7 +81,7 @@ describe('render helpers', () => { getSliceValueWithFallback( { a: 'Cat', a_subtotal: undefined, b: 'Home', c: undefined }, columnGroups, - { id: 'c', name: 'C' } + { id: 'c', name: 'C', meta: { type: 'number' } } ) ).toEqual(Number.EPSILON); }); @@ -83,11 +90,11 @@ describe('render helpers', () => { describe('#getFilterContext', () => { it('handles single slice click for single ring', () => { - const table: KibanaDatatable = { - type: 'kibana_datatable', + const table: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, + { id: 'a', name: 'A', meta: { type: 'string' } }, + { id: 'b', name: 'B', meta: { type: 'number' } }, ], rows: [ { a: 'Hi', b: 2 }, @@ -108,12 +115,12 @@ describe('render helpers', () => { }); it('handles single slice click with 2 rings', () => { - const table: KibanaDatatable = { - type: 'kibana_datatable', + const table: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, - { id: 'c', name: 'C' }, + { id: 'a', name: 'A', meta: { type: 'string' } }, + { id: 'b', name: 'B', meta: { type: 'string' } }, + { id: 'c', name: 'C', meta: { type: 'number' } }, ], rows: [ { a: 'Hi', b: 'Two', c: 2 }, @@ -134,12 +141,12 @@ describe('render helpers', () => { }); it('finds right row for multi slice click', () => { - const table: KibanaDatatable = { - type: 'kibana_datatable', + const table: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, - { id: 'c', name: 'C' }, + { id: 'a', name: 'A', meta: { type: 'string' } }, + { id: 'b', name: 'B', meta: { type: 'string' } }, + { id: 'c', name: 'C', meta: { type: 'number' } }, ], rows: [ { a: 'Hi', b: 'Two', c: 2 }, diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts index aafbb477bab2..26b4f9ccda85 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts @@ -5,14 +5,14 @@ */ import { Datum, LayerValue } from '@elastic/charts'; -import { KibanaDatatable, KibanaDatatableColumn } from 'src/plugins/expressions/public'; +import { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; import { ColumnGroups } from './types'; import { LensFilterEvent } from '../types'; export function getSliceValueWithFallback( d: Datum, reverseGroups: ColumnGroups, - metricColumn: KibanaDatatableColumn + metricColumn: DatatableColumn ) { if (typeof d[metricColumn.id] === 'number' && d[metricColumn.id] !== 0) { return d[metricColumn.id]; @@ -27,7 +27,7 @@ export function getSliceValueWithFallback( export function getFilterContext( clickedLayers: LayerValue[], layerColumnIds: string[], - table: KibanaDatatable + table: Datatable ): LensFilterEvent['data'] { const matchingIndex = table.rows.findIndex((row) => clickedLayers.every((layer, index) => { diff --git a/x-pack/plugins/lens/public/pie_visualization/types.ts b/x-pack/plugins/lens/public/pie_visualization/types.ts index 603c80aa0006..0596e54870a9 100644 --- a/x-pack/plugins/lens/public/pie_visualization/types.ts +++ b/x-pack/plugins/lens/public/pie_visualization/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaDatatableColumn } from 'src/plugins/expressions/public'; +import { DatatableColumn } from 'src/plugins/expressions/public'; import { LensMultiTable } from '../types'; export interface SharedLayerState { @@ -40,6 +40,6 @@ export interface PieExpressionProps { } export type ColumnGroups = Array<{ - col: KibanaDatatableColumn; - metrics: KibanaDatatableColumn[]; + col: DatatableColumn; + metrics: DatatableColumn[]; }>; diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index ef84ca2698ee..533efcbfe427 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -26,7 +26,6 @@ import { DatatableVisualizationPluginSetupPlugins, } from './datatable_visualization'; import { PieVisualization, PieVisualizationPluginSetupPlugins } from './pie_visualization'; -import { stopReportManager } from './lens_ui_telemetry'; import { AppNavLinkStatus } from '../../../../src/core/public'; import { @@ -40,7 +39,7 @@ import { getLensAliasConfig } from './vis_type_alias'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; import { getSearchProvider } from './search_provider'; -import { getLensAttributeService, LensAttributeService } from './lens_attribute_service'; +import { LensAttributeService } from './lens_attribute_service'; export interface LensPluginSetupDependencies { urlForwarding: UrlForwardingSetup; @@ -64,12 +63,14 @@ export class LensPlugin { private datatableVisualization: DatatableVisualization; private editorFrameService: EditorFrameService; private createEditorFrame: EditorFrameStart['createInstance'] | null = null; - private attributeService: LensAttributeService | null = null; + private attributeService: (() => Promise) | null = null; private indexpatternDatasource: IndexPatternDatasource; private xyVisualization: XyVisualization; private metricVisualization: MetricVisualization; private pieVisualization: PieVisualization; + private stopReportManager?: () => void; + constructor() { this.datatableVisualization = new DatatableVisualization(); this.editorFrameService = new EditorFrameService(); @@ -91,6 +92,11 @@ export class LensPlugin { globalSearch, }: LensPluginSetupDependencies ) { + this.attributeService = async () => { + const { getLensAttributeService } = await import('./async_services'); + const [coreStart, startDependencies] = await core.getStartServices(); + return getLensAttributeService(coreStart, startDependencies); + }; const editorFrameSetupInterface = this.editorFrameService.setup( core, { @@ -98,7 +104,7 @@ export class LensPlugin { embeddable, expressions, }, - () => this.attributeService! + this.attributeService ); const dependencies: IndexPatternDatasourceSetupPlugins & XyVisualizationPluginSetupPlugins & @@ -131,7 +137,8 @@ export class LensPlugin { title: NOT_INTERNATIONALIZED_PRODUCT_NAME, navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { - const { mountApp } = await import('./async_services'); + const { mountApp, stopReportManager } = await import('./async_services'); + this.stopReportManager = stopReportManager; return mountApp(core, params, { createEditorFrame: this.createEditorFrame!, attributeService: this.attributeService!, @@ -158,7 +165,6 @@ export class LensPlugin { } start(core: CoreStart, startDependencies: LensPluginStartDependencies) { - this.attributeService = getLensAttributeService(core, startDependencies); this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; // unregisters the Visualize action and registers the lens one if (startDependencies.uiActions.hasAction(ACTION_VISUALIZE_FIELD)) { @@ -171,6 +177,8 @@ export class LensPlugin { } stop() { - stopReportManager(); + if (this.stopReportManager) { + this.stopReportManager(); + } } } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 2b9ca5a2425f..e70436163b23 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -11,7 +11,7 @@ import { SavedObjectReference } from 'kibana/public'; import { ExpressionRendererEvent, IInterpreterRenderHandlers, - KibanaDatatable, + Datatable, SerializedFieldFormat, } from '../../../../src/plugins/expressions/public'; import { DragContextState } from './drag_drop'; @@ -304,7 +304,7 @@ export interface OperationMetadata { export interface LensMultiTable { type: 'lens_multitable'; - tables: Record; + tables: Record; dateRange?: { fromDate: Date; toDate: Date; diff --git a/x-pack/plugins/lens/public/utils.test.ts b/x-pack/plugins/lens/public/utils.test.ts index 170579b7c551..59b81fd3d113 100644 --- a/x-pack/plugins/lens/public/utils.test.ts +++ b/x-pack/plugins/lens/public/utils.test.ts @@ -6,10 +6,12 @@ import { LensFilterEvent } from './types'; import { desanitizeFilterContext } from './utils'; +import { Datatable } from '../../../../src/plugins/expressions/common'; describe('desanitizeFilterContext', () => { it(`When filtered value equals '(empty)' replaces it with '' in table and in value.`, () => { - const table = { + const table: Datatable = { + type: 'datatable', rows: [ { 'f903668f-1175-4705-a5bd-713259d10326': 1589414640000, @@ -35,14 +37,17 @@ describe('desanitizeFilterContext', () => { { id: 'f903668f-1175-4705-a5bd-713259d10326', name: 'order_date per 30 seconds', + meta: { type: 'date' }, }, { id: '5d5446b2-72e8-4f86-91e0-88380f0fa14c', name: 'Top values of customer_phone', + meta: { type: 'string' }, }, { id: '9f0b6f88-c399-43a0-a993-0ad943c9af25', name: 'Count of records', + meta: { type: 'number' }, }, ], }; @@ -102,6 +107,7 @@ describe('desanitizeFilterContext', () => { }, ], columns: table.columns, + type: 'datatable', }, }, ], diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 171707dcb9d2..0461e600d2b4 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -14,7 +14,7 @@ export const desanitizeFilterContext = ( const emptyTextValue = i18n.translate('xpack.lens.indexpattern.emptyTextColumnValue', { defaultMessage: '(empty)', }); - return { + const result: LensFilterEvent['data'] = { ...context, data: context.data.map((point) => point.value === emptyTextValue @@ -36,4 +36,8 @@ export const desanitizeFilterContext = ( : point ), }; + if (context.timeFieldName) { + result.timeFieldName = context.timeFieldName; + } + return result; }; diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts index 15c08d17e49c..a823a6370270 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts @@ -5,13 +5,13 @@ */ import { LayerArgs } from './types'; -import { KibanaDatatable } from '../../../../../src/plugins/expressions/public'; +import { Datatable } from '../../../../../src/plugins/expressions/public'; import { getAxesConfiguration } from './axes_configuration'; describe('axes_configuration', () => { - const tables: Record = { + const tables: Record = { first: { - type: 'kibana_datatable', + type: 'datatable', rows: [ { xAccessorId: 1585758120000, @@ -99,48 +99,60 @@ describe('axes_configuration', () => { id: 'xAccessorId', name: 'order_date per minute', meta: { - type: 'date_histogram', - indexPatternId: 'indexPatternId', - aggConfigParams: { - field: 'order_date', - timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, - useNormalizedEsInterval: true, - scaleMetricValues: false, - interval: '1m', - drop_partials: false, - min_doc_count: 0, - extended_bounds: {}, + type: 'date', + field: 'order_date', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'date_histogram', + params: { + field: 'order_date', + timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '1m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, }, + params: { params: { id: 'date', params: { pattern: 'HH:mm' } } }, }, - formatHint: { id: 'date', params: { pattern: 'HH:mm' } }, }, { id: 'splitAccessorId', name: 'Top values of category.keyword', meta: { - type: 'terms', - indexPatternId: 'indexPatternId', - aggConfigParams: { - field: 'category.keyword', - orderBy: 'yAccessorId', - order: 'desc', - size: 3, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', + type: 'string', + field: 'category.keyword', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'terms', + params: { + field: 'category.keyword', + orderBy: 'yAccessorId', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, }, - }, - formatHint: { - id: 'terms', params: { - id: 'string', - otherBucketLabel: 'Other', - missingBucketLabel: 'Missing', - parsedUrl: { - origin: 'http://localhost:5601', - pathname: '/jiy/app/kibana', - basePath: '/jiy', + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/jiy/app/kibana', + basePath: '/jiy', + }, }, }, }, @@ -149,41 +161,57 @@ describe('axes_configuration', () => { id: 'yAccessorId', name: 'Count of records', meta: { - type: 'count', - indexPatternId: 'indexPatternId', - aggConfigParams: {}, + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'count', + }, + params: { id: 'number' }, }, - formatHint: { id: 'number' }, }, { id: 'yAccessorId2', name: 'Other column', meta: { - type: 'average', - indexPatternId: 'indexPatternId', - aggConfigParams: {}, + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'average', + }, + params: { id: 'bytes' }, }, - formatHint: { id: 'bytes' }, }, { id: 'yAccessorId3', name: 'Other column', meta: { - type: 'average', - indexPatternId: 'indexPatternId', - aggConfigParams: {}, + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'average', + }, + params: { id: 'currency' }, }, - formatHint: { id: 'currency' }, }, { id: 'yAccessorId4', name: 'Other column', meta: { - type: 'average', - indexPatternId: 'indexPatternId', - aggConfigParams: {}, + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'average', + }, + params: { id: 'currency' }, }, - formatHint: { id: 'currency' }, }, ], }, diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts index 876baaabb57c..3c312abf1fd9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts +++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts @@ -5,10 +5,7 @@ */ import { LayerConfig } from './types'; -import { - KibanaDatatable, - SerializedFieldFormat, -} from '../../../../../src/plugins/expressions/public'; +import { Datatable, SerializedFieldFormat } from '../../../../../src/plugins/expressions/public'; import { IFieldFormat } from '../../../../../src/plugins/data/public'; interface FormattedMetric { @@ -34,7 +31,7 @@ export function isFormatterCompatible( export function getAxesConfiguration( layers: LayerConfig[], shouldRotate: boolean, - tables?: Record, + tables?: Record, formatFactory?: (mapping: SerializedFieldFormat) => IFieldFormat ): GroupsConfiguration { const series: { auto: FormattedMetric[]; left: FormattedMetric[]; right: FormattedMetric[] } = { @@ -50,7 +47,7 @@ export function getAxesConfiguration( layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode || 'auto'; let formatter: SerializedFieldFormat = table?.columns.find((column) => column.id === accessor) - ?.formatHint || { id: 'number' }; + ?.meta?.params || { id: 'number' }; if (layer.seriesType.includes('percentage') && formatter.id !== 'percent') { formatter = { id: 'percent', diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index e7da850983de..9e937399a796 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/charts'; import { xyChart, XYChart } from './expression'; import { LensMultiTable } from '../types'; -import { KibanaDatatable, KibanaDatatableRow } from '../../../../../src/plugins/expressions/public'; +import { Datatable, DatatableRow } from '../../../../../src/plugins/expressions/public'; import React from 'react'; import { shallow } from 'enzyme'; import { @@ -46,7 +46,7 @@ const dateHistogramData: LensMultiTable = { type: 'lens_multitable', tables: { timeLayer: { - type: 'kibana_datatable', + type: 'datatable', rows: [ { xAccessorId: 1585758120000, @@ -104,48 +104,60 @@ const dateHistogramData: LensMultiTable = { id: 'xAccessorId', name: 'order_date per minute', meta: { - type: 'date_histogram', - indexPatternId: 'indexPatternId', - aggConfigParams: { - field: 'order_date', - timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, - useNormalizedEsInterval: true, - scaleMetricValues: false, - interval: '1m', - drop_partials: false, - min_doc_count: 0, - extended_bounds: {}, + type: 'date', + field: 'order_date', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'date_histogram', + params: { + field: 'order_date', + timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '1m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, }, + params: { id: 'date', params: { pattern: 'HH:mm' } }, }, - formatHint: { id: 'date', params: { pattern: 'HH:mm' } }, }, { id: 'splitAccessorId', name: 'Top values of category.keyword', meta: { - type: 'terms', - indexPatternId: 'indexPatternId', - aggConfigParams: { - field: 'category.keyword', - orderBy: 'yAccessorId', - order: 'desc', - size: 3, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', + type: 'string', + field: 'category.keyword', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'terms', + params: { + field: 'category.keyword', + orderBy: 'yAccessorId', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, }, - }, - formatHint: { - id: 'terms', params: { - id: 'string', - otherBucketLabel: 'Other', - missingBucketLabel: 'Missing', - parsedUrl: { - origin: 'http://localhost:5601', - pathname: '/jiy/app/kibana', - basePath: '/jiy', + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/jiy/app/kibana', + basePath: '/jiy', + }, }, }, }, @@ -154,11 +166,15 @@ const dateHistogramData: LensMultiTable = { id: 'yAccessorId', name: 'Count of records', meta: { - type: 'count', - indexPatternId: 'indexPatternId', - aggConfigParams: {}, + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + params: {}, + }, + params: { id: 'number' }, }, - formatHint: { id: 'number' }, }, ], }, @@ -181,22 +197,30 @@ const dateHistogramLayer: LayerArgs = { accessors: ['yAccessorId'], }; -const createSampleDatatableWithRows = (rows: KibanaDatatableRow[]): KibanaDatatable => ({ - type: 'kibana_datatable', +const createSampleDatatableWithRows = (rows: DatatableRow[]): Datatable => ({ + type: 'datatable', columns: [ { id: 'a', name: 'a', - formatHint: { id: 'number', params: { pattern: '0,0.000' } }, + meta: { type: 'number', params: { id: 'number', params: { pattern: '0,0.000' } } }, + }, + { + id: 'b', + name: 'b', + meta: { type: 'number', params: { id: 'number', params: { pattern: '000,0' } } }, }, - { id: 'b', name: 'b', formatHint: { id: 'number', params: { pattern: '000,0' } } }, { id: 'c', name: 'c', - formatHint: { id: 'string' }, - meta: { type: 'date-histogram', aggConfigParams: { interval: 'auto' } }, + meta: { + type: 'date', + field: 'order_date', + sourceParams: { type: 'date-histogram', params: { interval: 'auto' } }, + params: { id: 'string' }, + }, }, - { id: 'd', name: 'ColD', formatHint: { id: 'string' } }, + { id: 'd', name: 'ColD', meta: { type: 'string' } }, ], rows, }); @@ -347,12 +371,12 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, - { id: 'd', name: 'd' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, + { id: 'd', name: 'd', meta: { type: 'string' } }, ], rows: [ { a: 1, b: 2, c: 'I', d: 'Row 1' }, @@ -365,12 +389,12 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, - { id: 'd', name: 'd', formatHint: { id: 'custom' } }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, + { id: 'd', name: 'd', meta: { type: 'string', params: { id: 'custom' } } }, ], rows: [ { a: 1, b: 2, c: 'I', d: 'Row 1' }, @@ -542,12 +566,12 @@ describe('xy_expression', () => { ); expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": undefined, - } - `); + Object { + "max": 1546491600000, + "min": 1546405200000, + "minInterval": undefined, + } + `); }); test('it generates correct xDomain for a layer with single value and layer with multiple value data (1-n)', () => { const data: LensMultiTable = { @@ -625,12 +649,12 @@ describe('xy_expression', () => { ); expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": undefined, - } - `); + Object { + "max": 1546491600000, + "min": 1546405200000, + "minInterval": undefined, + } + `); }); }); @@ -792,7 +816,7 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { numberLayer: { - type: 'kibana_datatable', + type: 'datatable', rows: [ { xAccessorId: 5, @@ -815,10 +839,12 @@ describe('xy_expression', () => { { id: 'xAccessorId', name: 'bytes', + meta: { type: 'number' }, }, { id: 'yAccessorId', name: 'Count of records', + meta: { type: 'number' }, }, ], }, @@ -1737,11 +1763,11 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, ], rows: [ { a: undefined, b: 2, c: 'I', d: 'Row 1' }, @@ -1749,11 +1775,11 @@ describe('xy_expression', () => { ], }, second: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, ], rows: [ { a: undefined, b: undefined, c: undefined }, @@ -1831,11 +1857,11 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'number' } }, ], rows: [ { a: 0, b: 2, c: 5 }, @@ -1903,11 +1929,11 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, ], rows: [{ a: 1, b: 5, c: 'J' }], }, diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index dad1d31ced71..4a2c13e1e352 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -26,7 +26,8 @@ import { ExpressionFunctionDefinition, ExpressionRenderDefinition, ExpressionValueSearchContext, - KibanaDatatable, + Datatable, + DatatableRow, } from 'src/plugins/expressions/public'; import { IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -271,7 +272,7 @@ export function XYChart({ const xAxisColumn = data.tables[filteredLayers[0].layerId].columns.find( ({ id }) => id === filteredLayers[0].xAccessor ); - const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.formatHint); + const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.meta?.params); const layersAlreadyFormatted: Record = {}; // This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers const safeXAccessorLabelRenderer = (value: unknown): string => @@ -330,8 +331,8 @@ export function XYChart({ // add minInterval only for single point in domain if (data.dateRange && isSingleTimestampInXDomain()) { - if (xAxisColumn?.meta?.aggConfigParams?.interval !== 'auto') - return parseInterval(xAxisColumn?.meta?.aggConfigParams?.interval)?.asMilliseconds(); + const params = xAxisColumn?.meta?.sourceParams?.params as Record; + if (params?.interval !== 'auto') return parseInterval(params?.interval)?.asMilliseconds(); const { fromDate, toDate } = data.dateRange; const duration = moment(toDate).diff(moment(fromDate)); @@ -417,8 +418,9 @@ export function XYChart({ const xAxisColumnIndex = table.columns.findIndex( (el) => el.id === filteredLayers[0].xAccessor ); + const timeFieldName = isTimeViz - ? table.columns[xAxisColumnIndex]?.meta?.aggConfigParams?.field + ? table.columns[xAxisColumnIndex]?.meta?.field : undefined; const context: LensBrushEvent['data'] = { @@ -471,8 +473,7 @@ export function XYChart({ }); } - const xAxisFieldName = table.columns.find((el) => el.id === layer.xAccessor)?.meta - ?.aggConfigParams?.field; + const xAxisFieldName = table.columns.find((el) => el.id === layer.xAccessor)?.meta?.field; const timeFieldName = xDomain && xAxisFieldName; const context: LensFilterEvent['data'] = { @@ -552,14 +553,14 @@ export function XYChart({ // what if row values are not primitive? That is the case of, for instance, Ranges // remaps them to their serialized version with the formatHint metadata // In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on - const tableConverted: KibanaDatatable = { + const tableConverted: Datatable = { ...table, - rows: table.rows.map((row) => { + rows: table.rows.map((row: DatatableRow) => { const newRow = { ...row }; for (const column of table.columns) { const record = newRow[column.id]; if (record && !isPrimitive(record)) { - newRow[column.id] = formatFactory(column.formatHint).convert(record); + newRow[column.id] = formatFactory(column.meta.params).convert(record); } } return newRow; @@ -634,7 +635,7 @@ export function XYChart({ }, }, name(d) { - const splitHint = table.columns.find((col) => col.id === splitAccessor)?.formatHint; + const splitHint = table.columns.find((col) => col.id === splitAccessor)?.meta?.params; // For multiple y series, the name of the operation is used on each, either: // * Key - Y name 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..47b97af3071a 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, }); } } @@ -117,8 +126,6 @@ function flipSeriesType(seriesType: SeriesType) { return 'bar_stacked'; case 'bar': return 'bar_horizontal'; - case 'bar_horizontal_stacked': - return 'bar_stacked'; case 'bar_horizontal_percentage_stacked': return 'bar_percentage_stacked'; case 'bar_percentage_stacked': @@ -190,6 +197,7 @@ function getSuggestionsForLayer({ currentState, tableLabel, keptLayerIds, + requestedSeriesType, }: { layerId: string; changeType: TableChangeType; @@ -199,9 +207,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/common/elasticsearch_util/elasticsearch_geo_utils.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js index be214e3b01e6..813d01ff9086 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js @@ -175,6 +175,16 @@ export function convertESShapeToGeojsonGeometry(value) { geoJson.type = GEO_JSON_TYPE.GEOMETRY_COLLECTION; break; case 'envelope': + // format defined here https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html#_envelope + const polygon = formatEnvelopeAsPolygon({ + minLon: geoJson.coordinates[0][0], + maxLon: geoJson.coordinates[1][0], + minLat: geoJson.coordinates[1][1], + maxLat: geoJson.coordinates[0][1], + }); + geoJson.type = polygon.type; + geoJson.coordinates = polygon.coordinates; + break; case 'circle': const errorMessage = i18n.translate( 'xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage', diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index a8d5d650740c..ccab57dd1833 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -250,6 +250,30 @@ describe('geoShapeToGeometry', () => { expect(shapes[0].coordinates).toEqual(coordinates); }); + it('Should convert envelope to geojson', () => { + const coordinates = [ + [100.0, 1.0], + [101.0, 0.0], + ]; + const value = { + type: 'envelope', + coordinates: coordinates, + }; + const shapes = []; + geoShapeToGeometry(value, shapes); + expect(shapes.length).toBe(1); + expect(shapes[0].type).toBe('Polygon'); + expect(shapes[0].coordinates).toEqual([ + [ + [100, 1], + [100, 0], + [101, 0], + [101, 1], + [100, 1], + ], + ]); + }); + it('Should convert array of values', () => { const linestringCoordinates = [ [-77.03653, 38.897676], diff --git a/x-pack/plugins/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/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index e3bcc53fe697..9dc0814e3a3e 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -12,7 +12,10 @@ import { AppMountParameters, CoreStart, HttpStart } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + RedirectAppLinks, +} from '../../../../../src/plugins/kibana_react/public'; import { setDependencyCache, clearCache } from './util/dependency_cache'; import { setLicenseCache } from './license'; import { MlSetupDependencies, MlStartDependencies } from '../plugin'; @@ -21,7 +24,6 @@ import { MlRouter } from './routing'; import { mlApiServicesProvider } from './services/ml_api_service'; import { HttpService } from './services/http_service'; import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../common/constants/ml_url_generator'; - export type MlDependencies = Omit & MlStartDependencies; @@ -80,13 +82,17 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { const I18nContext = coreStart.i18n.Context; return ( - - - - - + /** RedirectAppLinks intercepts all tags to use navigateToUrl + * avoiding full page reload **/ + + + + + + + ); }; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 7eb280c6247c..100b2afcc97c 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -273,7 +273,7 @@ class AnnotationsTableUI extends Component { timeRange, refreshInterval: { display: 'Off', - pause: false, + pause: true, value: 0, }, jobIds: [job.job_id], diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 079d56da60e5..6361c0274c3d 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -30,7 +30,6 @@ import { formatHumanReadableDateTimeSeconds } from '../../../../common/util/date import { getIndexPatternIdFromName } from '../../util/index_utils'; import { replaceStringTokens } from '../../util/string_utils'; import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../common/constants/ml_url_generator'; -import { PLUGIN_ID } from '../../../../common/constants/app'; /* * Component for rendering the links menu inside a cell in the anomalies table. */ @@ -147,8 +146,6 @@ class LinksMenuUI extends Component { viewSeries = async () => { const { services: { - application: { navigateToApp }, - share: { urlGenerators: { getUrlGenerator }, }, @@ -185,13 +182,13 @@ class LinksMenuUI extends Component { } const singleMetricViewerLink = await mlUrlGenerator.createUrl({ - excludeBasePath: true, + excludeBasePath: false, page: ML_PAGES.SINGLE_METRIC_VIEWER, pageState: { jobIds: [record.job_id], refreshInterval: { display: 'Off', - pause: false, + pause: true, value: 0, }, timeRange: { @@ -211,9 +208,7 @@ class LinksMenuUI extends Component { }, }, }); - await navigateToApp(PLUGIN_ID, { - path: singleMetricViewerLink, - }); + window.open(singleMetricViewerLink, '_blank'); }; viewExamples = () => { @@ -307,7 +302,7 @@ class LinksMenuUI extends Component { const _g = rison.encode({ refreshInterval: { display: 'Off', - pause: false, + pause: true, value: 0, }, time: { 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/data_grid/use_data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx index b97ddb269098..08b2d48a982d 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; @@ -93,10 +93,8 @@ export const useDataGrid = ( [columns] ); - return { - chartsVisible, - chartsButtonVisible: true, - columnsWithCharts: columns.map((c, index) => { + const columnsWithCharts = useMemo(() => { + const updatedColumns = columns.map((c, index) => { const chartData = columnCharts.find((cd) => cd.id === c.id); return { @@ -110,7 +108,32 @@ export const useDataGrid = ( /> ) : undefined, }; - }), + }); + + // Sort the columns to be in line with the current order of visible columns. + // EuiDataGrid misses a callback for the order of all available columns, so + // this only can retain the order of visible columns. + return updatedColumns.sort((a, b) => { + // This will always move visible columns above invisible ones. + if (visibleColumns.indexOf(a.id) === -1 && visibleColumns.indexOf(b.id) > -1) { + return 1; + } + if (visibleColumns.indexOf(b.id) === -1 && visibleColumns.indexOf(a.id) > -1) { + return -1; + } + if (visibleColumns.indexOf(a.id) === -1 && visibleColumns.indexOf(b.id) === -1) { + return a.id.localeCompare(b.id); + } + + // If both columns are visible sort by their visible sorting order. + return visibleColumns.indexOf(a.id) - visibleColumns.indexOf(b.id); + }); + }, [columns, columnCharts, chartsVisible, JSON.stringify(visibleColumns)]); + + return { + chartsVisible, + chartsButtonVisible: true, + columnsWithCharts, errorMessage, invalidSortingColumnns, noDataMessage, diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index 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..7d1a616d5711 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; +import React, { FC, Fragment, useState, useEffect, useCallback } from 'react'; import { Subscription } from 'rxjs'; import { EuiSuperDatePicker, OnRefreshProps } from '@elastic/eui'; import { TimeRange, TimeHistoryContract } from 'src/plugins/data/public'; @@ -48,13 +48,15 @@ export const DatePickerWrapper: FC = () => { const [globalState, setGlobalState] = useUrlState('_g'); const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(history); - const [refreshInterval, setRefreshInterval] = useState( - globalState?.refreshInterval ?? timefilter.getRefreshInterval() + const refreshInterval: RefreshInterval = + globalState?.refreshInterval ?? timefilter.getRefreshInterval(); + + const setRefreshInterval = useCallback( + (refreshIntervalUpdate: RefreshInterval) => { + setGlobalState('refreshInterval', refreshIntervalUpdate); + }, + [setGlobalState] ); - useEffect(() => { - setGlobalState({ refreshInterval }); - timefilter.setRefreshInterval(refreshInterval); - }, [refreshInterval?.pause, refreshInterval?.value]); const [time, setTime] = useState(timefilter.getTime()); const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(getRecentlyUsedRanges()); @@ -71,15 +73,28 @@ export const DatePickerWrapper: FC = () => { const subscriptions = new Subscription(); const refreshIntervalUpdate$ = timefilter.getRefreshIntervalUpdate$(); if (refreshIntervalUpdate$ !== undefined) { - subscriptions.add(refreshIntervalUpdate$.subscribe(timefilterUpdateListener)); + subscriptions.add( + refreshIntervalUpdate$.subscribe((r) => { + setRefreshInterval(timefilter.getRefreshInterval()); + }) + ); } const timeUpdate$ = timefilter.getTimeUpdate$(); if (timeUpdate$ !== undefined) { - subscriptions.add(timeUpdate$.subscribe(timefilterUpdateListener)); + subscriptions.add( + timeUpdate$.subscribe((v) => { + setTime(timefilter.getTime()); + }) + ); } const enabledUpdated$ = timefilter.getEnabledUpdated$(); if (enabledUpdated$ !== undefined) { - subscriptions.add(enabledUpdated$.subscribe(timefilterUpdateListener)); + subscriptions.add( + enabledUpdated$.subscribe((w) => { + setIsAutoRefreshSelectorEnabled(timefilter.isAutoRefreshSelectorEnabled()); + setIsTimeRangeSelectorEnabled(timefilter.isTimeRangeSelectorEnabled()); + }) + ); } return function cleanup() { @@ -87,18 +102,6 @@ export const DatePickerWrapper: FC = () => { }; }, []); - useEffect(() => { - // Force re-render with up-to-date values when isTimeRangeSelectorEnabled/isAutoRefreshSelectorEnabled are changed. - timefilterUpdateListener(); - }, [isTimeRangeSelectorEnabled, isAutoRefreshSelectorEnabled]); - - function timefilterUpdateListener() { - setTime(timefilter.getTime()); - setRefreshInterval(timefilter.getRefreshInterval()); - setIsAutoRefreshSelectorEnabled(timefilter.isAutoRefreshSelectorEnabled()); - setIsTimeRangeSelectorEnabled(timefilter.isTimeRangeSelectorEnabled()); - } - function updateFilter({ start, end }: Duration) { const newTime = { from: start, to: end }; // Update timefilter for controllers listening for changes diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx index e7026508a90b..3da5bcec4c6c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx @@ -22,7 +22,7 @@ import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; import { ANALYTICS_STEPS } from '../../page'; function getStringValue(value: number | undefined) { - return value !== undefined ? `${value}` : UNSET_CONFIG_ITEM; + return typeof value === 'number' ? `${value}` : UNSET_CONFIG_ITEM; } export interface ListItems { @@ -135,7 +135,12 @@ export const AdvancedStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.numTopClasses', { defaultMessage: 'Top classes', }), - description: `${numTopClasses}`, + description: + numTopClasses === -1 + ? i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.allClasses', { + defaultMessage: 'All classes', + }) + : getStringValue(numTopClasses), }); } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx index 1aad5dfd9cfb..7747fe5746c2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx @@ -5,13 +5,17 @@ */ import React, { FC, Fragment, useMemo, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiAccordion, + EuiComboBox, + EuiComboBoxOptionOption, EuiFieldNumber, EuiFieldText, EuiFlexGrid, EuiFlexItem, EuiFormRow, + EuiLink, EuiSelect, EuiSpacer, EuiTitle, @@ -25,12 +29,93 @@ import { NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN, ANALYSIS_ADVANCED_FIELDS, } from '../../../../common/analytics'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../analytics_management/hooks/use_create_analytics_form/state'; import { ANALYTICS_STEPS } from '../../page'; import { fetchExplainData } from '../shared'; import { ContinueButton } from '../continue_button'; import { OutlierHyperParameters } from './outlier_hyper_parameters'; +const defaultNumTopClassesOption: EuiComboBoxOptionOption = { + label: i18n.translate('xpack.ml.dataframe.analytics.create.allClassesLabel', { + defaultMessage: 'All classes', + }), + value: -1, +}; +const numClassesTypeMessage = ( + +); + +function getZeroClassesMessage(elasaticUrl: string, version: string) { + return ( + + {i18n.translate('xpack.ml.dataframe.analytics.create.aucRocLabel', { + defaultMessage: 'AUC ROC', + })} + + ), + }} + /> + ); +} + +function getTopClassesHelpText(currentNumTopClasses?: number) { + if (currentNumTopClasses === -1) { + return ( + <> + {' '} + + + ); + } + return ( + + ); +} + +function getSelectedNumTomClassesOption(currentNumTopClasses?: number) { + const option: EuiComboBoxOptionOption[] = []; + if (currentNumTopClasses === -1) { + option.push(defaultNumTopClassesOption); + } else if (currentNumTopClasses !== undefined) { + option.push({ + label: `${currentNumTopClasses}`, + }); + } + return option; +} + +function isInvalidNumTopClasses(currentNumTopClasses?: number) { + // Only valid if undefined or a whole integer >= -1 + return ( + currentNumTopClasses !== undefined && + (isNaN(currentNumTopClasses) || + currentNumTopClasses < -1 || + currentNumTopClasses - Math.floor(currentNumTopClasses) !== 0) + ); +} + export function getNumberValue(value?: number) { return value === undefined ? '' : +value; } @@ -47,6 +132,11 @@ export const AdvancedStepForm: FC = ({ const [advancedParamErrors, setAdvancedParamErrors] = useState({}); const [fetchingAdvancedParamErrors, setFetchingAdvancedParamErrors] = useState(false); + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + const { setEstimatedModelMemoryLimit, setFormState } = actions; const { form, isJobCreated } = state; const { @@ -71,6 +161,21 @@ export const AdvancedStepForm: FC = ({ randomizeSeed, } = form; + const [numTopClassesOptions, setNumTopClassesOptions] = useState([ + defaultNumTopClassesOption, + ]); + const [numTopClassesSelectedOptions, setNumTopClassesSelectedOptions] = useState< + EuiComboBoxOptionOption[] + >(getSelectedNumTomClassesOption(numTopClasses)); + + const selectedNumTopClasses = + numTopClassesSelectedOptions[0] && + ((numTopClassesSelectedOptions[0].value ?? Number(numTopClassesSelectedOptions[0].label)) as + | number + | undefined); + + const selectedNumTopClassesIsInvalid = isInvalidNumTopClasses(selectedNumTopClasses); + const mmlErrors = useMemo(() => getModelMemoryLimitErrors(modelMemoryLimitValidationResult), [ modelMemoryLimitValidationResult, ]); @@ -84,6 +189,7 @@ export const AdvancedStepForm: FC = ({ modelMemoryLimitValidationResult.required === true); const isStepInvalid = + selectedNumTopClassesIsInvalid || mmlInvalid || Object.keys(advancedParamErrors).length > 0 || fetchingAdvancedParamErrors === true || @@ -309,15 +415,16 @@ export const AdvancedStepForm: FC = ({ label={i18n.translate('xpack.ml.dataframe.analytics.create.numTopClassesLabel', { defaultMessage: 'Top classes', })} - helpText={i18n.translate( - 'xpack.ml.dataframe.analytics.create.numTopClassesHelpText', - { - defaultMessage: - 'The number of categories for which the predicted probabilities are reported.', - } - )} + helpText={getTopClassesHelpText(selectedNumTopClasses)} + isInvalid={selectedNumTopClasses === 0 || selectedNumTopClassesIsInvalid} + error={[ + ...(selectedNumTopClasses === 0 + ? [getZeroClassesMessage(ELASTIC_WEBSITE_URL, DOC_LINK_VERSION)] + : []), + ...(selectedNumTopClassesIsInvalid ? [numClassesTypeMessage] : []), + ]} > - = ({ 'The number of categories for which the predicted probabilities are reported', } )} + singleSelection={true} + options={numTopClassesOptions} + selectedOptions={numTopClassesSelectedOptions} + onCreateOption={(input: string, flattenedOptions = []) => { + const normalizedInput = input.trim().toLowerCase(); + + if (normalizedInput === '') { + return; + } + + const newOption = { + label: input, + }; + + if ( + flattenedOptions.findIndex( + (option) => option.label.trim().toLowerCase() === normalizedInput + ) === -1 + ) { + setNumTopClassesOptions([...numTopClassesOptions, newOption]); + } + + setNumTopClassesSelectedOptions([newOption]); + }} + onChange={(selectedOptions) => { + setNumTopClassesSelectedOptions(selectedOptions); + }} + isClearable={true} + isInvalid={selectedNumTopClasses !== undefined && selectedNumTopClasses < -1} data-test-subj="mlAnalyticsCreateJobWizardnumTopClassesInput" - min={0} - onChange={(e) => - setFormState({ - numTopClasses: e.target.value === '' ? undefined : +e.target.value, - }) - } - step={1} - value={getNumberValue(numTopClasses)} /> @@ -440,6 +568,10 @@ export const AdvancedStepForm: FC = ({ { + setFormState({ + numTopClasses: + selectedNumTopClassesIsInvalid === false ? selectedNumTopClasses : undefined, + }); setCurrentStep(ANALYTICS_STEPS.DETAILS); }} /> 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/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx index b03777fef6bd..de4d1a97f248 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -144,12 +144,19 @@ export const ExplorationPageWrapper: FC = ({ )} - {isLoadingJobConfig === true && totalFeatureImportance === undefined && } - {isLoadingJobConfig === false && totalFeatureImportance !== undefined && ( - <> - - - )} + {isLoadingJobConfig === true && + jobConfig !== undefined && + totalFeatureImportance === undefined && } + {isLoadingJobConfig === false && + jobConfig !== undefined && + totalFeatureImportance !== undefined && ( + <> + + + )} {isLoadingJobConfig === true && jobConfig === undefined && } {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index 06bcdfd364d6..c837fcbacdd5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -149,11 +149,11 @@ export const ExplorationQueryBar: FC = ({ placeholder={ searchInput.language === SEARCH_QUERY_LANGUAGE.KUERY ? i18n.translate('xpack.ml.stepDefineForm.queryPlaceholderKql', { - defaultMessage: 'e.g. {example}', + defaultMessage: 'Search for e.g. {example}', values: { example: 'method : "GET" or status : "404"' }, }) : i18n.translate('xpack.ml.stepDefineForm.queryPlaceholderLucene', { - defaultMessage: 'e.g. {example}', + defaultMessage: 'Search for e.g. {example}', values: { example: 'method:GET OR status:404' }, }) } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts index 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/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index eded8e82a791..88aa06808e8a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -52,17 +52,21 @@ export const useOutlierData = ( const needsDestIndexFields = indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; - const columns: EuiDataGridColumn[] = []; - - if (jobConfig !== undefined && indexPattern !== undefined) { - const resultsField = jobConfig.dest.results_field; - const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); - columns.push( - ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => - sortExplorationResultsFields(a.id, b.id, jobConfig) - ) - ); - } + const columns = useMemo(() => { + const newColumns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined && indexPattern !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + newColumns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + return newColumns; + }, [jobConfig, indexPattern]); const dataGrid = useDataGrid( columns, @@ -124,7 +128,10 @@ export const useOutlierData = ( }, [ dataGrid.chartsVisible, jobConfig?.dest.index, - JSON.stringify([searchQuery, dataGrid.visibleColumns]), + // Only trigger when search or the visible columns changes. + // We're only interested in the visible columns but not their order, that's + // why we sort for comparison (and copying it via spread to avoid sort in place). + JSON.stringify([searchQuery, [...dataGrid.visibleColumns].sort()]), ]); const colorRange = useColorRange( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx index b754e2eeba34..4da96c595bdf 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx @@ -32,6 +32,9 @@ import { import { useMlKibana } from '../../../../../contexts/kibana'; import { ExpandableSection } from '../expandable_section'; +import { DataFrameAnalyticsConfig } from '../../../../../../../common/types/data_frame_analytics'; +import { getAnalysisType } from '../../../../common'; +import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; const { euiColorMediumShade } = euiVars; const axisColor = euiColorMediumShade; @@ -69,6 +72,7 @@ const theme: PartialTheme = { export interface FeatureImportanceSummaryPanelProps { totalFeatureImportance: TotalFeatureImportance[]; + jobConfig: DataFrameAnalyticsConfig; } const tooltipContent = i18n.translate( @@ -88,6 +92,7 @@ const calculateTotalMeanImportance = (featureClass: ClassificationTotalFeatureIm export const FeatureImportanceSummaryPanel: FC = ({ totalFeatureImportance, + jobConfig, }) => { const { services: { docLinks }, @@ -189,13 +194,52 @@ export const FeatureImportanceSummaryPanel: FC Number(d.toPrecision(3)).toString(), []); // do not expand by default if no feature importance data - const hasTotalFeatureImportance = useMemo(() => totalFeatureImportance.length > 1, [ - totalFeatureImportance, - ]); + const noDataCallOut = useMemo(() => { + // if no total feature importance data + if (totalFeatureImportance.length === 0) { + // check if it's because num_top_feature_importance_values is set to 0 + if ( + (jobConfig?.analysis && isRegressionAnalysis(jobConfig?.analysis)) || + isClassificationAnalysis(jobConfig?.analysis) + ) { + const analysisType = getAnalysisType(jobConfig.analysis); + if ( + analysisType !== 'unknown' && + jobConfig.analysis[analysisType].num_top_feature_importance_values === 0 + ) { + return ( + + } + /> + ); + } else { + // or is it because the data is uniform + return ( + + } + /> + ); + } + } + } + return undefined; + }, [totalFeatureImportance, jobConfig]); return ( <> - } - /> + noDataCallOut ? ( + noDataCallOut ) : ( ({ nNeighbors: undefined, numTopFeatureImportanceValues: DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, numTopFeatureImportanceValuesValid: true, - numTopClasses: 2, + numTopClasses: -1, outlierFraction: undefined, predictionFieldName: undefined, previousJobType: null, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx index 374d434708f4..22e8c2af452c 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState, useEffect, useCallback } from 'react'; +import React, { FC, useState, useEffect } from 'react'; import moment from 'moment'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; @@ -12,7 +12,7 @@ import { ml } from '../../../../services/ml_api_service'; import { isFullLicense } from '../../../../license'; import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; -import { useMlKibana, useMlUrlGenerator, useNavigateToPath } from '../../../../contexts/kibana'; +import { useMlKibana, useMlUrlGenerator } from '../../../../contexts/kibana'; import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; import { MlCommonGlobalState } from '../../../../../../common/types/ml_url_generator'; import { @@ -45,12 +45,16 @@ export const ResultsLinks: FC = ({ const [globalState, setGlobalState] = useState(); const [discoverLink, setDiscoverLink] = useState(''); + const [indexManagementLink, setIndexManagementLink] = useState(''); + const [indexPatternManagementLink, setIndexPatternManagementLink] = useState(''); + const [dataVisualizerLink, setDataVisualizerLink] = useState(''); + const [createJobsSelectTypePage, setCreateJobsSelectTypePage] = useState(''); + const mlUrlGenerator = useMlUrlGenerator(); - const navigateToPath = useNavigateToPath(); const { services: { - application: { navigateToApp, navigateToUrl }, + application: { getUrlForApp }, share: { urlGenerators: { getUrlGenerator }, }, @@ -84,35 +88,52 @@ export const ResultsLinks: FC = ({ setDiscoverLink(discoverUrl); } }; + + const getDataVisualizerLink = async (): Promise => { + const _dataVisualizerLink = await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, + pageState: { + index: indexPatternId, + globalState, + }, + }); + if (!unmounted) { + setDataVisualizerLink(_dataVisualizerLink); + } + }; + const getADCreateJobsSelectTypePage = async (): Promise => { + const _createJobsSelectTypePage = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, + pageState: { + index: indexPatternId, + globalState, + }, + }); + if (!unmounted) { + setCreateJobsSelectTypePage(_createJobsSelectTypePage); + } + }; + getDiscoverUrl(); + getDataVisualizerLink(); + getADCreateJobsSelectTypePage(); + + if (!unmounted) { + setIndexManagementLink( + getUrlForApp('management', { path: '/data/index_management/indices' }) + ); + setIndexPatternManagementLink( + getUrlForApp('management', { + path: `/kibana/indexPatterns${createIndexPattern ? `/patterns/${indexPatternId}` : ''}`, + }) + ); + } return () => { unmounted = true; }; }, [indexPatternId, getUrlGenerator]); - const openInDataVisualizer = useCallback(async () => { - const path = await mlUrlGenerator.createUrl({ - page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, - pageState: { - index: indexPatternId, - globalState, - }, - }); - await navigateToPath(path); - }, [indexPatternId, globalState]); - - const redirectToADCreateJobsSelectTypePage = useCallback(async () => { - const path = await mlUrlGenerator.createUrl({ - page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, - pageState: { - index: indexPatternId, - globalState, - }, - }); - await navigateToPath(path); - }, [indexPatternId, globalState]); - useEffect(() => { setShowCreateJobLink(checkPermission('canCreateJob') && mlNodesAvailable()); updateTimeValues(); @@ -148,23 +169,6 @@ export const ResultsLinks: FC = ({ } } - function openInDiscover(e: React.MouseEvent) { - e.preventDefault(); - navigateToUrl(discoverLink); - } - - function openIndexManagement(e: React.MouseEvent) { - e.preventDefault(); - navigateToApp('management', { path: '/data/index_management/indices' }); - } - - function openIndexPatternManagement(e: React.MouseEvent) { - e.preventDefault(); - navigateToApp('management', { - path: `/kibana/indexPatterns${createIndexPattern ? `/patterns/${indexPatternId}` : ''}`, - }); - } - return ( {createIndexPattern && discoverLink && ( @@ -178,7 +182,7 @@ export const ResultsLinks: FC = ({ /> } description="" - onClick={openInDiscover} + href={discoverLink} /> )} @@ -186,7 +190,8 @@ export const ResultsLinks: FC = ({ {isFullLicense() === true && timeFieldName !== undefined && showCreateJobLink && - createIndexPattern && ( + createIndexPattern && + createJobsSelectTypePage && ( } @@ -197,12 +202,12 @@ export const ResultsLinks: FC = ({ /> } description="" - onClick={redirectToADCreateJobsSelectTypePage} + href={createJobsSelectTypePage} /> )} - {createIndexPattern && ( + {createIndexPattern && dataVisualizerLink && ( } @@ -213,38 +218,42 @@ export const ResultsLinks: FC = ({ /> } description="" - onClick={openInDataVisualizer} + href={dataVisualizerLink} /> )} - - } - title={ - - } - description="" - onClick={openIndexManagement} - /> - + {indexManagementLink && ( + + } + title={ + + } + description="" + href={indexManagementLink} + /> + + )} - - } - title={ - - } - description="" - onClick={openIndexPatternManagement} - /> - + {indexPatternManagementLink && ( + + } + title={ + + } + description="" + href={indexPatternManagementLink} + /> + + )} } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 8f03b1903800..9c04e8187cd3 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -30,7 +30,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { MlTooltipComponent } from '../../components/chart_tooltip'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator'; -import { PLUGIN_ID } from '../../../../common/constants/app'; import { addItemToRecentlyAccessed } from '../../util/recently_accessed'; const textTooManyBuckets = i18n.translate('xpack.ml.explorer.charts.tooManyBucketsDescription', { @@ -55,21 +54,12 @@ function getChartId(series) { } // Wrapper for a single explorer chart -function ExplorerChartContainer({ - series, - severity, - tooManyBuckets, - wrapLabel, - navigateToApp, - mlUrlGenerator, -}) { +function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel, mlUrlGenerator }) { const redirectToSingleMetricViewer = useCallback(async () => { const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series); addItemToRecentlyAccessed('timeseriesexplorer', series.jobId, singleMetricViewerLink); - await navigateToApp(PLUGIN_ID, { - path: singleMetricViewerLink, - }); + window.open(singleMetricViewerLink, '_blank'); }, [mlUrlGenerator]); const { detectorLabel, entityFields } = series; 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/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js index 73b212b97b4c..82e38b297138 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js @@ -8,9 +8,9 @@ import React from 'react'; import { detectorToString } from '../../../../util/string_utils'; import { formatValues, filterObjects } from './format_values'; import { i18n } from '@kbn/i18n'; -import { Link } from 'react-router-dom'; +import { EuiLink } from '@elastic/eui'; -export function extractJobDetails(job) { +export function extractJobDetails(job, basePath) { if (Object.keys(job).length === 0) { return {}; } @@ -61,7 +61,9 @@ export function extractJobDetails(job) { if (job.calendars) { calendars.items = job.calendars.map((c) => [ '', - {c}, + + {c} + , ]); // remove the calendars list from the general section // so not to show it twice. diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index 44ebde634714..933c7150f44d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -136,7 +136,7 @@ export class ForecastsTableUI extends Component { }, refreshInterval: { display: 'Off', - pause: false, + pause: true, value: 0, }, jobIds: [this.props.job.job_id], diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index 9a5cea62cf6f..e826684fb395 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -19,8 +19,9 @@ import { ForecastsTable } from './forecasts_table'; import { JobDetailsPane } from './job_details_pane'; import { JobMessagesPane } from './job_messages_pane'; import { i18n } from '@kbn/i18n'; +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -export class JobDetails extends Component { +export class JobDetailsUI extends Component { constructor(props) { super(props); @@ -39,7 +40,14 @@ export class JobDetails extends Component { } render() { + console.log('this.props', this.props); const { job } = this.state; + const { + services: { + http: { basePath }, + }, + } = this.props.kibana; + if (job === undefined) { return (
@@ -62,7 +70,7 @@ export class JobDetails extends Component { modelSizeStats, jobTimingStats, datafeedTimingStats, - } = extractJobDetails(job); + } = extractJobDetails(job, basePath); const { showFullDetails, refreshJobList } = this.props; const tabs = [ @@ -197,7 +205,7 @@ export class JobDetails extends Component { } } } -JobDetails.propTypes = { +JobDetailsUI.propTypes = { jobId: PropTypes.string.isRequired, job: PropTypes.object, addYourself: PropTypes.func.isRequired, @@ -205,3 +213,5 @@ JobDetails.propTypes = { showFullDetails: PropTypes.bool, refreshJobList: PropTypes.func, }; + +export const JobDetails = withKibana(JobDetailsUI); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx index 0e84619899d7..f1c82dbb83eb 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiLink } from '@elastic/eui'; -import { useMlKibana, useMlUrlGenerator } from '../../../../contexts/kibana'; +import { useMlUrlGenerator } from '../../../../contexts/kibana'; import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; import { AnomalyDetectionQueryState } from '../../../../../../common/types/ml_url_generator'; // @ts-ignore @@ -28,34 +28,40 @@ function isGroupIdLink(props: JobIdLink | GroupIdLink): props is GroupIdLink { } export const AnomalyDetectionJobIdLink = (props: AnomalyDetectionJobIdLinkProps) => { const mlUrlGenerator = useMlUrlGenerator(); - const { - services: { - application: { navigateToUrl }, - }, - } = useMlKibana(); + const [href, setHref] = useState(''); + + useEffect(() => { + let isCancelled = false; + const generateLink = async () => { + const pageState: AnomalyDetectionQueryState = {}; + if (isGroupIdLink(props)) { + pageState.groupIds = [props.groupId]; + } else { + pageState.jobId = props.id; + } + const url = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState, + }); + if (!isCancelled) { + setHref(url); + } + }; + generateLink(); + return () => { + isCancelled = true; + }; + }, [props, mlUrlGenerator]); - const redirectToJobsManagementPage = async () => { - const pageState: AnomalyDetectionQueryState = {}; - if (isGroupIdLink(props)) { - pageState.groupIds = [props.groupId]; - } else { - pageState.jobId = props.id; - } - const url = await mlUrlGenerator.createUrl({ - page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, - pageState, - }); - await navigateToUrl(url); - }; if (isGroupIdLink(props)) { return ( - redirectToJobsManagementPage()}> + ); } else { return ( - redirectToJobsManagementPage()}> + {props.id} ); diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 9c9096dfdfc2..61dfea8897e8 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -24,7 +24,10 @@ import { import { ManagementAppMountParams } from '../../../../../../../../../src/plugins/management/public/'; import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities'; -import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + RedirectAppLinks, +} from '../../../../../../../../../src/plugins/kibana_react/public'; import { getDocLinks } from '../../../../util/dependency_cache'; // @ts-ignore undeclared module @@ -137,54 +140,56 @@ export const JobsListPage: FC<{ } return ( - - - - - - - -

- {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', { - defaultMessage: 'Machine Learning Jobs', - })} -

-
- - - {currentTabId === 'anomaly_detection_jobs' - ? anomalyDetectionDocsLabel - : analyticsDocsLabel} - - -
-
- - - - {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', { - defaultMessage: 'View machine learning analytics and anomaly detection jobs.', - })} - - - - {renderTabs()} -
-
-
-
+ + + + + + + + +

+ {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', { + defaultMessage: 'Machine Learning Jobs', + })} +

+
+ + + {currentTabId === 'anomaly_detection_jobs' + ? anomalyDetectionDocsLabel + : analyticsDocsLabel} + + +
+
+ + + + {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', { + defaultMessage: 'View machine learning analytics and anomaly detection jobs.', + })} + + + + {renderTabs()} +
+
+
+
+
); }; 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/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 03588872d6be..e4cf43ac9172 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -98,7 +98,7 @@ export const TimeSeriesExplorerUrlStateManager: FC { - if (refresh !== undefined) { + if (refresh !== undefined && refresh.lastRefresh !== lastRefresh) { setLastRefresh(refresh?.lastRefresh); if (refresh.timeRange !== undefined) { @@ -109,7 +109,7 @@ export const TimeSeriesExplorerUrlStateManager: FC { - if (selectedJobIds !== undefined && previousSelectedJobIds !== undefined) { - setLastRefresh(Date.now()); - appStateHandler(APP_STATE_ACTION.CLEAR); - } - const validatedJobId = validateJobSelection(jobsWithTimeRange, selectedJobIds, setGlobalState); - if (typeof validatedJobId === 'string') { - setSelectedJobId(validatedJobId); - } - }, [JSON.stringify(selectedJobIds)]); - // Next we get globalState and appState information to pass it on as props later. // If a job change is going on, we fall back to defaults (as if appState was already cleared), // otherwise the page could break. @@ -216,9 +204,21 @@ export const TimeSeriesExplorerUrlStateManager: FC { + if (selectedJobIds !== undefined && previousSelectedJobIds !== undefined) { + setLastRefresh(Date.now()); + appStateHandler(APP_STATE_ACTION.CLEAR); + } + const validatedJobId = validateJobSelection(jobsWithTimeRange, selectedJobIds, setGlobalState); + if (typeof validatedJobId === 'string') { + setSelectedJobId(validatedJobId); + } + }, [JSON.stringify(selectedJobIds)]); + const boundsMinMs = bounds?.min?.valueOf(); const boundsMaxMs = bounds?.max?.valueOf(); diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index 4aa1f7ef81d5..cdcd4a7ab732 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -805,7 +805,7 @@ function createResultsUrl(jobIds, start, end, resultsPage, mode = 'absolute') { } path += `?_g=(ml:(jobIds:!(${idString}))`; - path += `,refreshInterval:(display:Off,pause:!f,value:0),time:(from:'${from}'`; + path += `,refreshInterval:(display:Off,pause:!t,value:0),time:(from:'${from}'`; path += `,to:'${to}'`; if (mode === 'invalid') { path += `,mode:invalid`; 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/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index ca55bb10b13d..d774b0b759ec 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -239,7 +239,7 @@ export async function getExploreSeriesLink(mlUrlGenerator, series) { jobIds: [series.jobId], refreshInterval: { display: 'Off', - pause: false, + pause: true, value: 0, }, timeRange: { @@ -260,7 +260,7 @@ export async function getExploreSeriesLink(mlUrlGenerator, series) { }, }, }, - excludeBasePath: true, + excludeBasePath: false, }); return url; } diff --git a/x-pack/plugins/ml/public/application/util/url_state.test.tsx b/x-pack/plugins/ml/public/application/util/url_state.test.tsx index 9c0336964855..292167462136 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.test.tsx +++ b/x-pack/plugins/ml/public/application/util/url_state.test.tsx @@ -24,7 +24,7 @@ describe('getUrlState', () => { test('properly decode url with _g and _a', () => { expect( parseUrlState( - "?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d" + "?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!t,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d" ) ).toEqual({ _a: { @@ -45,7 +45,7 @@ describe('getUrlState', () => { }, refreshInterval: { display: 'Off', - pause: false, + pause: true, value: 0, }, time: { 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/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts index fdd725355062..824eeab7245b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -7,12 +7,16 @@ import { fetchStatus } from './fetch_status'; import { AlertUiState, AlertState } from '../../alerts/types'; import { AlertSeverity } from '../../../common/enums'; -import { ALERT_CPU_USAGE, ALERT_CLUSTER_HEALTH } from '../../../common/constants'; +import { + ALERT_CPU_USAGE, + ALERT_CLUSTER_HEALTH, + ALERT_DISK_USAGE, + ALERT_MISSING_MONITORING_DATA, +} from '../../../common/constants'; describe('fetchStatus', () => { const alertType = ALERT_CPU_USAGE; const alertTypes = [alertType]; - const log = { warn: jest.fn() }; const start = 0; const end = 0; const id = 1; @@ -53,6 +57,7 @@ describe('fetchStatus', () => { afterEach(() => { (alertsClient.find as jest.Mock).mockClear(); (alertsClient.getAlertState as jest.Mock).mockClear(); + alertStates.length = 0; }); it('should fetch from the alerts client', async () => { @@ -62,8 +67,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(status).toEqual({ monitoring_alert_cpu_usage: { @@ -98,8 +102,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(Object.values(status).length).toBe(1); expect(Object.keys(status)).toEqual(alertTypes); @@ -126,8 +129,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, customStart, - customEnd, - log as any + customEnd ); expect(Object.values(status).length).toBe(1); expect(Object.keys(status)).toEqual(alertTypes); @@ -141,8 +143,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect((alertsClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe( `alert.attributes.alertTypeId:${alertType}` @@ -160,8 +161,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(status[alertType].states.length).toEqual(0); }); @@ -178,8 +178,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(status).toEqual({}); }); @@ -197,9 +196,51 @@ describe('fetchStatus', () => { [ALERT_CLUSTER_HEALTH], defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(customLicenseService.getWatcherFeature).toHaveBeenCalled(); }); + + it('should sort the alerts', async () => { + const customAlertsClient = { + find: jest.fn(() => ({ + total: 1, + data: [ + { + id, + }, + ], + })), + getAlertState: jest.fn(() => ({ + alertInstances: { + abc: { + state: { + alertStates: [ + { + cluster: defaultClusterState, + ui: { + ...defaultUiState, + isFiring: true, + }, + }, + ], + }, + }, + }, + })), + }; + const status = await fetchStatus( + customAlertsClient as any, + licenseService as any, + [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], + defaultClusterState.clusterUuid, + start, + end + ); + expect(Object.keys(status)).toEqual([ + ALERT_CPU_USAGE, + ALERT_DISK_USAGE, + ALERT_MISSING_MONITORING_DATA, + ]); + }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 49e688fafbee..ed49f42e4908 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -18,8 +18,9 @@ export async function fetchStatus( clusterUuid: string, start: number, end: number, - filters: CommonAlertFilter[] + filters: CommonAlertFilter[] = [] ): Promise<{ [type: string]: CommonAlertStatus }> { + const types: Array<{ type: string; result: CommonAlertStatus }> = []; const byType: { [type: string]: CommonAlertStatus } = {}; await Promise.all( (alertTypes || ALERTS).map(async (type) => { @@ -39,7 +40,7 @@ export async function fetchStatus( alert: serialized, }; - byType[type] = result; + types.push({ type, result }); const id = alert.getId(); if (!id) { @@ -75,5 +76,10 @@ export async function fetchStatus( }) ); + types.sort((a, b) => (a.type === b.type ? 0 : a.type.length > b.type.length ? 1 : -1)); + for (const { type, result } of types) { + byType[type] = result; + } + return byType; } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts index 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 { + return ( + <> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 22b97f45db18..18895f9e623e 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -13,7 +13,7 @@ import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; -interface Props { +export interface Props { apiClient: ReportingAPIClient; toasts: ToastsSetup; reportType: string; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content_lazy.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content_lazy.tsx new file mode 100644 index 000000000000..45a7d60a6096 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content_lazy.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { lazy, Suspense, FC } from 'react'; +import { PanelSpinner } from './panel_spinner'; +import type { Props } from './reporting_panel_content'; + +const LazyComponent = lazy(() => + import('./reporting_panel_content').then(({ ReportingPanelContent }) => ({ + default: ReportingPanelContent, + })) +); + +export const ReportingPanelContent: FC> = (props) => { + return ( + }> + + + ); +}; diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx index 4a62ab2b7650..ff81ced43e0b 100644 --- a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx @@ -12,7 +12,7 @@ import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ReportingPanelContent } from './reporting_panel_content'; -interface Props { +export interface Props { apiClient: ReportingAPIClient; toasts: ToastsSetup; reportType: string; diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content_lazy.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content_lazy.tsx new file mode 100644 index 000000000000..52080e16dd6a --- /dev/null +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content_lazy.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { lazy, Suspense, FC } from 'react'; +import { PanelSpinner } from './panel_spinner'; +import type { Props } from './screen_capture_panel_content'; + +const LazyComponent = lazy(() => + import('./screen_capture_panel_content').then(({ ScreenCapturePanelContent }) => ({ + default: ScreenCapturePanelContent, + })) +); + +export const ScreenCapturePanelContent: FC = (props) => { + return ( + }> + + + ); +}; diff --git a/x-pack/plugins/reporting/public/mount_management_section.tsx b/x-pack/plugins/reporting/public/mount_management_section.tsx new file mode 100644 index 000000000000..ac737e4a318a --- /dev/null +++ b/x-pack/plugins/reporting/public/mount_management_section.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { CoreSetup, CoreStart } from 'src/core/public'; +import { Observable } from 'rxjs'; +import { ReportListing } from './components/report_listing'; +import { ManagementAppMountParams } from '../../../../src/plugins/management/public'; +import { ILicense } from '../../licensing/public'; +import { ClientConfigType } from './plugin'; +import { ReportingAPIClient } from './lib/reporting_api_client'; + +export async function mountManagementSection( + coreSetup: CoreSetup, + coreStart: CoreStart, + license$: Observable, + pollConfig: ClientConfigType['poll'], + apiClient: ReportingAPIClient, + params: ManagementAppMountParams +) { + render( + + + , + params.element + ); + + return () => { + unmountComponentAtNode(params.element); + }; +} diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.ts similarity index 78% rename from x-pack/plugins/reporting/public/plugin.tsx rename to x-pack/plugins/reporting/public/plugin.ts index cc5964f73798..33f4fd4abf72 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -5,9 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; -import React from 'react'; -import ReactDOM from 'react-dom'; import * as Rx from 'rxjs'; import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; import { @@ -17,21 +14,21 @@ import { Plugin, PluginInitializerContext, } from 'src/core/public'; -import { UiActionsSetup } from 'src/plugins/ui_actions/public'; +import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup, + HomePublicPluginStart, } from '../../../../src/plugins/home/public'; -import { ManagementSetup } from '../../../../src/plugins/management/public'; -import { SharePluginSetup } from '../../../../src/plugins/share/public'; -import { LicensingPluginSetup } from '../../licensing/public'; +import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; +import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; +import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { durationToNumber } from '../common/schema_utils'; import { JobId, ReportingConfigType } from '../common/types'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; import { JobSummarySet } from './'; import { getGeneralErrorToast } from './components'; -import { ReportListing } from './components/report_listing'; import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; @@ -60,7 +57,25 @@ function handleError(notifications: NotificationsSetup, err: Error): Rx.Observab return Rx.of({ completed: [], failed: [] }); } -export class ReportingPublicPlugin implements Plugin { +export interface ReportingPublicPluginSetupDendencies { + home: HomePublicPluginSetup; + management: ManagementSetup; + licensing: LicensingPluginSetup; + uiActions: UiActionsSetup; + share: SharePluginSetup; +} + +export interface ReportingPublicPluginStartDendencies { + home: HomePublicPluginStart; + management: ManagementStart; + licensing: LicensingPluginStart; + uiActions: UiActionsStart; + share: SharePluginStart; +} + +export class ReportingPublicPlugin + implements + Plugin { private config: ClientConfigType; private readonly stop$ = new Rx.ReplaySubject(1); private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { @@ -76,19 +91,7 @@ export class ReportingPublicPlugin implements Plugin { public setup( core: CoreSetup, - { - home, - management, - licensing, - uiActions, - share, - }: { - home: HomePublicPluginSetup; - management: ManagementSetup; - licensing: LicensingPluginSetup; - uiActions: UiActionsSetup; - share: SharePluginSetup; - } + { home, management, licensing, uiActions, share }: ReportingPublicPluginSetupDendencies ) { const { http, @@ -119,24 +122,19 @@ export class ReportingPublicPlugin implements Plugin { title: this.title, order: 1, mount: async (params) => { - const [start] = await getStartServices(); params.setBreadcrumbs([{ text: this.breadcrumbText }]); - ReactDOM.render( - - - , - params.element + const [[start], { mountManagementSection }] = await Promise.all([ + getStartServices(), + import('./mount_management_section'), + ]); + return await mountManagementSection( + core, + start, + license$, + this.config.poll, + apiClient, + params ); - - return () => { - ReactDOM.unmountComponentAtNode(params.element); - }; }, }); diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 451d907199c4..e90d6786b58f 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -11,7 +11,7 @@ import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; import { ShareContext } from '../../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../../licensing/public'; import { JobParamsCSV, SearchRequest } from '../../server/export_types/csv/types'; -import { ReportingPanelContent } from '../components/reporting_panel_content'; +import { ReportingPanelContent } from '../components/reporting_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index 2dab66187bb2..d17d4af3c010 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -13,7 +13,7 @@ import { LicensingPluginSetup } from '../../../licensing/public'; import { LayoutParams } from '../../common/types'; import { JobParamsPNG } from '../../server/export_types/png/types'; import { JobParamsPDF } from '../../server/export_types/printable_pdf/types'; -import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content'; +import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index 8276e8b49d34..7a21c5a1f610 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -45,7 +45,15 @@ const QueueSchema = schema.object({ const RulesSchema = schema.object({ allow: schema.boolean(), host: schema.maybe(schema.string()), - protocol: schema.maybe(schema.string()), + protocol: schema.maybe( + schema.string({ + validate(value) { + if (!/:$/.test(value)) { + return 'must end in colon'; + } + }, + }) + ), }); const CaptureSchema = schema.object({ diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index c7a1c79748b5..abd86d51fb6b 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -195,21 +195,21 @@ export class ReportingCore { return scopedUiSettingsService; } - public getSpaceId(request: KibanaRequest): string | undefined { + public getSpaceId(request: KibanaRequest, logger = this.logger): string | undefined { const spacesService = this.getPluginSetupDeps().spaces?.spacesService; if (spacesService) { const spaceId = spacesService?.getSpaceId(request); if (spaceId !== DEFAULT_SPACE_ID) { - this.logger.info(`Request uses Space ID: ` + spaceId); + logger.info(`Request uses Space ID: ${spaceId}`); return spaceId; } else { - this.logger.info(`Request uses default Space`); + logger.debug(`Request uses default Space`); } } } - public getFakeRequest(baseRequest: object, spaceId?: string) { + public getFakeRequest(baseRequest: object, spaceId: string | undefined, logger = this.logger) { const fakeRequest = KibanaRequest.from({ path: '/', route: { settings: {} }, @@ -221,7 +221,7 @@ export class ReportingCore { const spacesService = this.getPluginSetupDeps().spaces?.spacesService; if (spacesService) { if (spaceId && spaceId !== DEFAULT_SPACE_ID) { - this.logger.info(`Generating request for space: ` + spaceId); + logger.info(`Generating request for space: ${spaceId}`); this.getPluginSetupDeps().basePath.set(fakeRequest, `/s/${spaceId}`); } } @@ -229,11 +229,11 @@ export class ReportingCore { return fakeRequest; } - public async getUiSettingsClient(request: KibanaRequest) { + public async getUiSettingsClient(request: KibanaRequest, logger = this.logger) { const spacesService = this.getPluginSetupDeps().spaces?.spacesService; - const spaceId = this.getSpaceId(request); + const spaceId = this.getSpaceId(request, logger); if (spacesService && spaceId) { - this.logger.info(`Creating UI Settings Client for space: ${spaceId}`); + logger.info(`Creating UI Settings Client for space: ${spaceId}`); } const savedObjectsClient = await this.getSavedObjectsClient(request); return await this.getUiSettingsServiceFactory(savedObjectsClient); diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index cb60b218818f..5b98a198b7d1 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CSV_JOB_TYPE } from '../../../constants'; import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { IndexPatternSavedObject, JobParamsCSV, TaskPayloadCSV } from './types'; @@ -11,7 +12,9 @@ import { IndexPatternSavedObject, JobParamsCSV, TaskPayloadCSV } from './types'; export const createJobFnFactory: CreateJobFnFactory> = 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/README.md b/x-pack/plugins/security/README.md index 068f19ba9482..b93be0269536 100644 --- a/x-pack/plugins/security/README.md +++ b/x-pack/plugins/security/README.md @@ -1,3 +1,92 @@ # Kibana Security Plugin -See [Configuring security in Kibana](https://www.elastic.co/guide/en/kibana/current/using-kibana-with-security.html). +See [Configuring security in +Kibana](https://www.elastic.co/guide/en/kibana/current/using-kibana-with-security.html). + +## Audit logging + +### Example + +```typescript +const auditLogger = securitySetup.audit.asScoped(request); +auditLogger.log({ + message: 'User is updating dashboard [id=123]', + event: { + action: 'saved_object_update', + category: EventCategory.DATABASE, + type: EventType.CHANGE, + outcome: EventOutcome.UNKNOWN, + }, + kibana: { + saved_object: { type: 'dashboard', id: '123' }, + }, +}); +``` + +### What events should be logged? + +The purpose of an audit log is to support compliance, accountability and +security by capturing who performed an action, what action was performed and +when it occurred. It is not the purpose of an audit log to aid with debugging +the system or provide usage statistics. + +**Kibana guidelines:** + +Each API call to Kibana will result in a record in the audit log that captures +general information about the request (`http_request` event). + +In addition to that, any operation that is performed on a resource owned by +Kibana (e.g. saved objects) and that falls in the following categories, should +be included in the audit log: + +- System access (incl. failed attempts due to authentication errors) +- Data reads (incl. failed attempts due to authorisation errors) +- Data writes (incl. failed attempts due to authorisation errors) + +If Kibana does not own the resource (e.g. when running queries against user +indices), then auditing responsibilities are deferred to Elasticsearch and no +additional events will be logged. + +**Examples:** + +For a list of audit events that Kibana currently logs see: +`docs/user/security/audit-logging.asciidoc` + +### When should an event be logged? + +Due to the asynchronous nature of most operations in Kibana, there is an +inherent tradeoff between the following logging approaches: + +- Logging the **intention** before performing an operation, leading to false + positives if the operation fails downstream. +- Logging the **outcome** after completing an operation, leading to missing + records if Kibana crashes before the response is received. +- Logging **both**, intention and outcome, leading to unnecessary duplication + and noisy/difficult to analyse logs. + +**Kibana guidelines:** + +- **Write operations** should be logged immediately after all authorisation + checks have passed, but before the response is received (logging the + intention). This ensures that a record of every operation is persisted even in + case of an unexpected error. +- **Read operations**, on the other hand, should be logged after the operation + completed (logging the outcome) since we won't know what resources were + accessed before receiving the response. +- Be explicit about the timing and outcome of an action in your messaging. (e.g. + "User has logged in" vs. "User is creating dashboard") + +### Can an action trigger multiple events? + +- A request to Kibana can perform a combination of different operations, each of + which should be captured as separate events. +- Operations that are performed on multiple resources (**bulk operations**) + should be logged as separate events, one for each resource. +- Actions that kick off **background tasks** should be logged as separate + events, one for creating the task and another one for executing it. +- **Internal checks**, which have been carried out in order to perform an + operation, or **errors** that occured as a result of an operation should be + logged as an outcome of the operation itself, using the ECS `event.outcome` + and `error` fields, instead of logging a separate event. +- Multiple events that were part of the same request can be correlated in the + audit log using the ECS `trace.id` property. 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/audit/audit_events.test.ts b/x-pack/plugins/security/server/audit/audit_events.test.ts new file mode 100644 index 000000000000..ae40429eea1b --- /dev/null +++ b/x-pack/plugins/security/server/audit/audit_events.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 { + EventOutcome, + SavedObjectAction, + savedObjectEvent, + userLoginEvent, + httpRequestEvent, +} from './audit_events'; +import { AuthenticationResult } from '../authentication'; +import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; +import { httpServerMock } from 'src/core/server/mocks'; + +describe('#savedObjectEvent', () => { + test('creates event with `unknown` outcome', () => { + expect( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' }, + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "saved_object_create", + "category": "database", + "outcome": "unknown", + "type": "creation", + }, + "kibana": Object { + "add_to_spaces": undefined, + "delete_from_spaces": undefined, + "saved_object": Object { + "id": "SAVED_OBJECT_ID", + "type": "dashboard", + }, + }, + "message": "User is creating dashboard [id=SAVED_OBJECT_ID]", + } + `); + }); + + test('creates event with `success` outcome', () => { + expect( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' }, + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "saved_object_create", + "category": "database", + "outcome": "success", + "type": "creation", + }, + "kibana": Object { + "add_to_spaces": undefined, + "delete_from_spaces": undefined, + "saved_object": Object { + "id": "SAVED_OBJECT_ID", + "type": "dashboard", + }, + }, + "message": "User has created dashboard [id=SAVED_OBJECT_ID]", + } + `); + }); + + test('creates event with `failure` outcome', () => { + expect( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' }, + error: new Error('ERROR_MESSAGE'), + }) + ).toMatchInlineSnapshot(` + Object { + "error": Object { + "code": "Error", + "message": "ERROR_MESSAGE", + }, + "event": Object { + "action": "saved_object_create", + "category": "database", + "outcome": "failure", + "type": "creation", + }, + "kibana": Object { + "add_to_spaces": undefined, + "delete_from_spaces": undefined, + "saved_object": Object { + "id": "SAVED_OBJECT_ID", + "type": "dashboard", + }, + }, + "message": "Failed attempt to create dashboard [id=SAVED_OBJECT_ID]", + } + `); + }); +}); + +describe('#userLoginEvent', () => { + test('creates event with `success` outcome', () => { + expect( + userLoginEvent({ + authenticationResult: AuthenticationResult.succeeded(mockAuthenticatedUser()), + authenticationProvider: 'basic1', + authenticationType: 'basic', + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "user_login", + "category": "authentication", + "outcome": "success", + }, + "kibana": Object { + "authentication_provider": "basic1", + "authentication_realm": "native1", + "authentication_type": "basic", + "lookup_realm": "native1", + "space_id": undefined, + }, + "message": "User [user] has logged in using basic provider [name=basic1]", + "user": Object { + "name": "user", + "roles": Array [ + "user-role", + ], + }, + } + `); + }); + + test('creates event with `failure` outcome', () => { + expect( + userLoginEvent({ + authenticationResult: AuthenticationResult.failed(new Error('Not Authorized')), + authenticationProvider: 'basic1', + authenticationType: 'basic', + }) + ).toMatchInlineSnapshot(` + Object { + "error": Object { + "code": "Error", + "message": "Not Authorized", + }, + "event": Object { + "action": "user_login", + "category": "authentication", + "outcome": "failure", + }, + "kibana": Object { + "authentication_provider": "basic1", + "authentication_realm": undefined, + "authentication_type": "basic", + "lookup_realm": undefined, + "space_id": undefined, + }, + "message": "Failed attempt to login using basic provider [name=basic1]", + "user": undefined, + } + `); + }); +}); + +describe('#httpRequestEvent', () => { + test('creates event with `unknown` outcome', () => { + expect( + httpRequestEvent({ + request: httpServerMock.createKibanaRequest(), + }) + ).toMatchInlineSnapshot(` + Object { + "event": Object { + "action": "http_request", + "category": "web", + "outcome": "unknown", + }, + "http": Object { + "request": Object { + "method": "get", + }, + }, + "message": "User is requesting [/path] endpoint", + "url": Object { + "domain": undefined, + "path": "/path", + "port": undefined, + "query": undefined, + "scheme": undefined, + }, + } + `); + }); +}); diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts new file mode 100644 index 000000000000..48a3b1e7e85b --- /dev/null +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -0,0 +1,244 @@ +/* + * 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 { KibanaRequest } from 'src/core/server'; +import { AuthenticationResult } from '../authentication/authentication_result'; + +/** + * Audit event schema using ECS format. + * https://www.elastic.co/guide/en/ecs/1.5/index.html + * @public + */ +export interface AuditEvent { + /** + * Human readable message describing action, outcome and user. + * + * @example + * User [jdoe] logged in using basic provider [name=basic1] + */ + message: string; + event: { + action: string; + category?: EventCategory; + type?: EventType; + outcome?: EventOutcome; + module?: string; + dataset?: string; + }; + user?: { + name: string; + email?: string; + full_name?: string; + hash?: string; + roles?: readonly string[]; + }; + kibana?: { + /** + * Current space id of the request. + */ + space_id?: string; + /** + * Saved object that was created, changed, deleted or accessed as part of the action. + */ + saved_object?: { + type: string; + id?: string; + }; + /** + * Any additional event specific fields. + */ + [x: string]: any; + }; + error?: { + code?: string; + message?: string; + }; + http?: { + request?: { + method?: string; + body?: { + content: string; + }; + }; + response?: { + status_code?: number; + }; + }; + url?: { + domain?: string; + full?: string; + path?: string; + port?: number; + query?: string; + scheme?: string; + }; +} + +export enum EventCategory { + DATABASE = 'database', + WEB = 'web', + IAM = 'iam', + AUTHENTICATION = 'authentication', + PROCESS = 'process', +} + +export enum EventType { + USER = 'user', + GROUP = 'group', + CREATION = 'creation', + ACCESS = 'access', + CHANGE = 'change', + DELETION = 'deletion', +} + +export enum EventOutcome { + SUCCESS = 'success', + FAILURE = 'failure', + UNKNOWN = 'unknown', +} + +export interface HttpRequestParams { + request: KibanaRequest; +} + +export function httpRequestEvent({ request }: HttpRequestParams): AuditEvent { + const { pathname, search } = request.url; + + return { + message: `User is requesting [${pathname}] endpoint`, + event: { + action: 'http_request', + category: EventCategory.WEB, + outcome: EventOutcome.UNKNOWN, + }, + http: { + request: { + method: request.route.method, + }, + }, + url: { + domain: request.url.hostname, + path: pathname, + port: request.url.port ? parseInt(request.url.port, 10) : undefined, + query: search?.slice(1) || undefined, + scheme: request.url.protocol, + }, + }; +} + +export interface UserLoginParams { + authenticationResult: AuthenticationResult; + authenticationProvider?: string; + authenticationType?: string; +} + +export function userLoginEvent({ + authenticationResult, + authenticationProvider, + authenticationType, +}: UserLoginParams): AuditEvent { + return { + message: authenticationResult.user + ? `User [${authenticationResult.user.username}] has logged in using ${authenticationType} provider [name=${authenticationProvider}]` + : `Failed attempt to login using ${authenticationType} provider [name=${authenticationProvider}]`, + event: { + action: 'user_login', + category: EventCategory.AUTHENTICATION, + outcome: authenticationResult.user ? EventOutcome.SUCCESS : EventOutcome.FAILURE, + }, + user: authenticationResult.user && { + name: authenticationResult.user.username, + roles: authenticationResult.user.roles, + }, + kibana: { + space_id: undefined, // Ensure this does not get populated by audit service + authentication_provider: authenticationProvider, + authentication_type: authenticationType, + authentication_realm: authenticationResult.user?.authentication_realm.name, + lookup_realm: authenticationResult.user?.lookup_realm.name, + }, + error: authenticationResult.error && { + code: authenticationResult.error.name, + message: authenticationResult.error.message, + }, + }; +} + +export enum SavedObjectAction { + CREATE = 'saved_object_create', + GET = 'saved_object_get', + UPDATE = 'saved_object_update', + DELETE = 'saved_object_delete', + FIND = 'saved_object_find', + ADD_TO_SPACES = 'saved_object_add_to_spaces', + DELETE_FROM_SPACES = 'saved_object_delete_from_spaces', +} + +const eventVerbs = { + saved_object_create: ['create', 'creating', 'created'], + saved_object_get: ['access', 'accessing', 'accessed'], + saved_object_update: ['update', 'updating', 'updated'], + saved_object_delete: ['delete', 'deleting', 'deleted'], + saved_object_find: ['access', 'accessing', 'accessed'], + saved_object_add_to_spaces: ['update', 'updating', 'updated'], + saved_object_delete_from_spaces: ['update', 'updating', 'updated'], +}; + +const eventTypes = { + saved_object_create: EventType.CREATION, + saved_object_get: EventType.ACCESS, + saved_object_update: EventType.CHANGE, + saved_object_delete: EventType.DELETION, + saved_object_find: EventType.ACCESS, + saved_object_add_to_spaces: EventType.CHANGE, + saved_object_delete_from_spaces: EventType.CHANGE, +}; + +export interface SavedObjectParams { + action: SavedObjectAction; + outcome?: EventOutcome; + savedObject?: Required['kibana']>['saved_object']; + addToSpaces?: readonly string[]; + deleteFromSpaces?: readonly string[]; + error?: Error; +} + +export function savedObjectEvent({ + action, + savedObject, + addToSpaces, + deleteFromSpaces, + outcome, + error, +}: SavedObjectParams): AuditEvent { + const doc = savedObject ? `${savedObject.type} [id=${savedObject.id}]` : 'saved objects'; + const [present, progressive, past] = eventVerbs[action]; + const message = error + ? `Failed attempt to ${present} ${doc}` + : outcome === 'unknown' + ? `User is ${progressive} ${doc}` + : `User has ${past} ${doc}`; + const type = eventTypes[action]; + + return { + message, + event: { + action, + category: EventCategory.DATABASE, + type, + outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS), + }, + kibana: { + saved_object: savedObject, + add_to_spaces: addToSpaces, + delete_from_spaces: deleteFromSpaces, + }, + error: error && { + code: error.name, + message: error.message, + }, + }; +} diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts index b2d866d07ff8..e0dd98c7de63 100644 --- a/x-pack/plugins/security/server/audit/audit_service.test.ts +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -3,163 +3,492 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { AuditService } from './audit_service'; -import { loggingSystemMock } from 'src/core/server/mocks'; +import { AuditService, filterEvent, createLoggingConfig } from './audit_service'; +import { AuditEvent, EventCategory, EventType, EventOutcome } from './audit_events'; +import { + coreMock, + loggingSystemMock, + httpServiceMock, + httpServerMock, +} from 'src/core/server/mocks'; import { licenseMock } from '../../common/licensing/index.mock'; import { ConfigSchema, ConfigType } from '../config'; import { SecurityLicenseFeatures } from '../../common/licensing'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable, of } from 'rxjs'; const createConfig = (settings: Partial) => { return ConfigSchema.validate(settings); }; -const config = createConfig({ - enabled: true, +const logger = loggingSystemMock.createLogger(); +const license = licenseMock.create(); +const config = createConfig({ enabled: true }); +const { logging } = coreMock.createSetup(); +const http = httpServiceMock.createSetupContract(); +const getCurrentUser = jest.fn().mockReturnValue({ username: 'jdoe', roles: ['admin'] }); +const getSpaceId = jest.fn().mockReturnValue('default'); + +beforeEach(() => { + logger.info.mockClear(); + logging.configure.mockClear(); + http.registerOnPostAuth.mockClear(); }); describe('#setup', () => { it('returns the expected contract', () => { - const logger = loggingSystemMock.createLogger(); const auditService = new AuditService(logger); - const license = licenseMock.create(); - expect(auditService.setup({ license, config })).toMatchInlineSnapshot(` + expect( + auditService.setup({ + license, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }) + ).toMatchInlineSnapshot(` Object { + "asScoped": [Function], "getLogger": [Function], } `); }); + + it('configures logging correctly when using ecs logger', async () => { + new AuditService(logger).setup({ + license, + config: { + enabled: true, + appender: { + kind: 'console', + layout: { + kind: 'pattern', + }, + }, + }, + logging, + http, + getCurrentUser, + getSpaceId, + }); + expect(logging.configure).toHaveBeenCalledWith(expect.any(Observable)); + }); + + it('registers post auth hook', () => { + new AuditService(logger).setup({ + license, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); + expect(http.registerOnPostAuth).toHaveBeenCalledWith(expect.any(Function)); + }); }); -test(`calls the underlying logger with the provided message and requisite tags`, () => { - const pluginId = 'foo'; +describe('#asScoped', () => { + it('logs event enriched with meta data', async () => { + const audit = new AuditService(logger).setup({ + license, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); + const request = httpServerMock.createKibanaRequest({ + kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, + }); + + audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); + expect(logger.info).toHaveBeenCalledWith('MESSAGE', { + event: { action: 'ACTION' }, + kibana: { space_id: 'default' }, + message: 'MESSAGE', + trace: { id: 'REQUEST_ID' }, + user: { name: 'jdoe', roles: ['admin'] }, + }); + }); + + it('does not log to audit logger if event matches ignore filter', async () => { + const audit = new AuditService(logger).setup({ + license, + config: { + enabled: true, + ignore_filters: [{ actions: ['ACTION'] }], + }, + logging, + http, + getCurrentUser, + getSpaceId, + }); + const request = httpServerMock.createKibanaRequest({ + kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, + }); + + audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); + expect(logger.info).not.toHaveBeenCalled(); + }); +}); - const logger = loggingSystemMock.createLogger(); - const license = licenseMock.create(); - license.features$ = new BehaviorSubject({ - allowAuditLogging: true, - } as SecurityLicenseFeatures).asObservable(); +describe('#createLoggingConfig', () => { + test('sets log level to `info` when audit logging is enabled and appender is defined', async () => { + const features$ = of({ + allowAuditLogging: true, + }); + + const loggingConfig = await features$ + .pipe( + createLoggingConfig({ + enabled: true, + appender: { + kind: 'console', + layout: { + kind: 'pattern', + }, + }, + }) + ) + .toPromise(); + + expect(loggingConfig).toMatchInlineSnapshot(` + Object { + "appenders": Object { + "auditTrailAppender": Object { + "kind": "console", + "layout": Object { + "kind": "pattern", + }, + }, + }, + "loggers": Array [ + Object { + "appenders": Array [ + "auditTrailAppender", + ], + "context": "audit.ecs", + "level": "info", + }, + ], + } + `); + }); - const auditService = new AuditService(logger).setup({ license, config }); + test('sets log level to `off` when audit logging is disabled', async () => { + const features$ = of({ + allowAuditLogging: true, + }); + + const loggingConfig = await features$ + .pipe( + createLoggingConfig({ + enabled: false, + appender: { + kind: 'console', + layout: { + kind: 'pattern', + }, + }, + }) + ) + .toPromise(); + + expect(loggingConfig.loggers![0].level).toEqual('off'); + }); - const auditLogger = auditService.getLogger(pluginId); + test('sets log level to `off` when appender is not defined', async () => { + const features$ = of({ + allowAuditLogging: true, + }); - const eventType = 'bar'; - const message = 'this is my audit message'; - auditLogger.log(eventType, message); + const loggingConfig = await features$ + .pipe( + createLoggingConfig({ + enabled: true, + }) + ) + .toPromise(); - expect(logger.info).toHaveBeenCalledTimes(1); - expect(logger.info).toHaveBeenCalledWith(message, { - eventType, - tags: [pluginId, eventType], + expect(loggingConfig.loggers![0].level).toEqual('off'); + }); + + test('sets log level to `off` when license does not allow audit logging', async () => { + const features$ = of({ + allowAuditLogging: false, + }); + + const loggingConfig = await features$ + .pipe( + createLoggingConfig({ + enabled: true, + appender: { + kind: 'console', + layout: { + kind: 'pattern', + }, + }, + }) + ) + .toPromise(); + + expect(loggingConfig.loggers![0].level).toEqual('off'); }); }); -test(`calls the underlying logger with the provided metadata`, () => { - const pluginId = 'foo'; +describe('#filterEvent', () => { + const event: AuditEvent = { + message: 'this is my audit message', + event: { + action: 'http_request', + category: EventCategory.WEB, + type: EventType.ACCESS, + outcome: EventOutcome.SUCCESS, + }, + user: { + name: 'jdoe', + }, + kibana: { + space_id: 'default', + }, + }; + + test('keeps event when ignore filters are undefined or empty', () => { + expect(filterEvent(event, undefined)).toBeTruthy(); + expect(filterEvent(event, [])).toBeTruthy(); + }); - const logger = loggingSystemMock.createLogger(); - const license = licenseMock.create(); - license.features$ = new BehaviorSubject({ - allowAuditLogging: true, - } as SecurityLicenseFeatures).asObservable(); + test('filters event correctly when a single match is found per criteria', () => { + expect(filterEvent(event, [{ actions: ['NO_MATCH'] }])).toBeTruthy(); + expect(filterEvent(event, [{ actions: ['NO_MATCH', 'http_request'] }])).toBeFalsy(); + expect(filterEvent(event, [{ categories: ['NO_MATCH', 'web'] }])).toBeFalsy(); + expect(filterEvent(event, [{ types: ['NO_MATCH', 'access'] }])).toBeFalsy(); + expect(filterEvent(event, [{ outcomes: ['NO_MATCH', 'success'] }])).toBeFalsy(); + expect(filterEvent(event, [{ spaces: ['NO_MATCH', 'default'] }])).toBeFalsy(); + }); - const auditService = new AuditService(logger).setup({ license, config }); + test('keeps event when one criteria per rule does not match', () => { + expect( + filterEvent(event, [ + { + actions: ['NO_MATCH'], + categories: ['web'], + types: ['access'], + outcomes: ['success'], + spaces: ['default'], + }, + { + actions: ['http_request'], + categories: ['NO_MATCH'], + types: ['access'], + outcomes: ['success'], + spaces: ['default'], + }, + { + actions: ['http_request'], + categories: ['web'], + types: ['NO_MATCH'], + outcomes: ['success'], + spaces: ['default'], + }, + { + actions: ['http_request'], + categories: ['web'], + types: ['access'], + outcomes: ['NO_MATCH'], + spaces: ['default'], + }, + { + actions: ['http_request'], + categories: ['web'], + types: ['access'], + outcomes: ['success'], + spaces: ['NO_MATCH'], + }, + ]) + ).toBeTruthy(); + }); - const auditLogger = auditService.getLogger(pluginId); + test('filters out event when all criteria in a single rule match', () => { + expect( + filterEvent(event, [ + { + actions: ['NO_MATCH'], + categories: ['NO_MATCH'], + types: ['NO_MATCH'], + outcomes: ['NO_MATCH'], + spaces: ['NO_MATCH'], + }, + { + actions: ['http_request'], + categories: ['web'], + types: ['access'], + outcomes: ['success'], + spaces: ['default'], + }, + ]) + ).toBeFalsy(); + }); +}); - const eventType = 'bar'; - const message = 'this is my audit message'; - const metadata = Object.freeze({ - property1: 'value1', - property2: false, - property3: 123, +describe('#getLogger', () => { + test('calls the underlying logger with the provided message and requisite tags', () => { + const pluginId = 'foo'; + + const licenseWithFeatures = licenseMock.create(); + licenseWithFeatures.features$ = new BehaviorSubject({ + allowAuditLogging: true, + } as SecurityLicenseFeatures).asObservable(); + + const auditService = new AuditService(logger).setup({ + license: licenseWithFeatures, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); + + const auditLogger = auditService.getLogger(pluginId); + + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); + + expect(logger.info).toHaveBeenCalledTimes(1); + expect(logger.info).toHaveBeenCalledWith(message, { + eventType, + tags: [pluginId, eventType], + }); }); - auditLogger.log(eventType, message, metadata); - expect(logger.info).toHaveBeenCalledTimes(1); - expect(logger.info).toHaveBeenCalledWith(message, { - eventType, - tags: [pluginId, eventType], - property1: 'value1', - property2: false, - property3: 123, + test('calls the underlying logger with the provided metadata', () => { + const pluginId = 'foo'; + + const licenseWithFeatures = licenseMock.create(); + licenseWithFeatures.features$ = new BehaviorSubject({ + allowAuditLogging: true, + } as SecurityLicenseFeatures).asObservable(); + + const auditService = new AuditService(logger).setup({ + license: licenseWithFeatures, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); + + const auditLogger = auditService.getLogger(pluginId); + + const eventType = 'bar'; + const message = 'this is my audit message'; + const metadata = Object.freeze({ + property1: 'value1', + property2: false, + property3: 123, + }); + auditLogger.log(eventType, message, metadata); + + expect(logger.info).toHaveBeenCalledTimes(1); + expect(logger.info).toHaveBeenCalledWith(message, { + eventType, + tags: [pluginId, eventType], + property1: 'value1', + property2: false, + property3: 123, + }); }); -}); -test(`does not call the underlying logger if license does not support audit logging`, () => { - const pluginId = 'foo'; + test('does not call the underlying logger if license does not support audit logging', () => { + const pluginId = 'foo'; - const logger = loggingSystemMock.createLogger(); - const license = licenseMock.create(); - license.features$ = new BehaviorSubject({ - allowAuditLogging: false, - } as SecurityLicenseFeatures).asObservable(); + const licenseWithFeatures = licenseMock.create(); + licenseWithFeatures.features$ = new BehaviorSubject({ + allowAuditLogging: false, + } as SecurityLicenseFeatures).asObservable(); - const auditService = new AuditService(logger).setup({ license, config }); + const auditService = new AuditService(logger).setup({ + license: licenseWithFeatures, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); - const auditLogger = auditService.getLogger(pluginId); + const auditLogger = auditService.getLogger(pluginId); - const eventType = 'bar'; - const message = 'this is my audit message'; - auditLogger.log(eventType, message); + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); - expect(logger.info).not.toHaveBeenCalled(); -}); + expect(logger.info).not.toHaveBeenCalled(); + }); -test(`does not call the underlying logger if security audit logging is not enabled`, () => { - const pluginId = 'foo'; + test('does not call the underlying logger if security audit logging is not enabled', () => { + const pluginId = 'foo'; - const logger = loggingSystemMock.createLogger(); - const license = licenseMock.create(); - license.features$ = new BehaviorSubject({ - allowAuditLogging: true, - } as SecurityLicenseFeatures).asObservable(); + const licenseWithFeatures = licenseMock.create(); + licenseWithFeatures.features$ = new BehaviorSubject({ + allowAuditLogging: true, + } as SecurityLicenseFeatures).asObservable(); - const auditService = new AuditService(logger).setup({ - license, - config: createConfig({ - enabled: false, - }), - }); + const auditService = new AuditService(logger).setup({ + license: licenseWithFeatures, + config: createConfig({ + enabled: false, + }), + logging, + http, + getCurrentUser, + getSpaceId, + }); - const auditLogger = auditService.getLogger(pluginId); + const auditLogger = auditService.getLogger(pluginId); - const eventType = 'bar'; - const message = 'this is my audit message'; - auditLogger.log(eventType, message); + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); - expect(logger.info).not.toHaveBeenCalled(); -}); + expect(logger.info).not.toHaveBeenCalled(); + }); -test(`calls the underlying logger after license upgrade`, () => { - const pluginId = 'foo'; + test('calls the underlying logger after license upgrade', () => { + const pluginId = 'foo'; - const logger = loggingSystemMock.createLogger(); - const license = licenseMock.create(); + const licenseWithFeatures = licenseMock.create(); - const features$ = new BehaviorSubject({ - allowAuditLogging: false, - } as SecurityLicenseFeatures); + const features$ = new BehaviorSubject({ + allowAuditLogging: false, + } as SecurityLicenseFeatures); - license.features$ = features$.asObservable(); + licenseWithFeatures.features$ = features$.asObservable(); - const auditService = new AuditService(logger).setup({ license, config }); + const auditService = new AuditService(logger).setup({ + license: licenseWithFeatures, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); - const auditLogger = auditService.getLogger(pluginId); + const auditLogger = auditService.getLogger(pluginId); - const eventType = 'bar'; - const message = 'this is my audit message'; - auditLogger.log(eventType, message); + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); - expect(logger.info).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); - // perform license upgrade - features$.next({ - allowAuditLogging: true, - } as SecurityLicenseFeatures); + // perform license upgrade + features$.next({ + allowAuditLogging: true, + } as SecurityLicenseFeatures); - auditLogger.log(eventType, message); + auditLogger.log(eventType, message); - expect(logger.info).toHaveBeenCalledTimes(1); + expect(logger.info).toHaveBeenCalledTimes(1); + }); }); diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts index 93e69fd2601e..31c7e28be3b8 100644 --- a/x-pack/plugins/security/server/audit/audit_service.ts +++ b/x-pack/plugins/security/server/audit/audit_service.ts @@ -5,53 +5,182 @@ */ import { Subscription } from 'rxjs'; -import { Logger } from '../../../../../src/core/server'; -import { SecurityLicense } from '../../common/licensing'; +import { map, distinctUntilKeyChanged } from 'rxjs/operators'; +import { + Logger, + LoggingServiceSetup, + KibanaRequest, + HttpServiceSetup, + LoggerContextConfigInput, +} from '../../../../../src/core/server'; +import { SecurityLicense, SecurityLicenseFeatures } from '../../common/licensing'; import { ConfigType } from '../config'; +import { SpacesPluginSetup } from '../../../spaces/server'; +import { AuditEvent, httpRequestEvent } from './audit_events'; +import { SecurityPluginSetup } from '..'; -export interface AuditLogger { +/** + * @deprecated + */ +export interface LegacyAuditLogger { log: (eventType: string, message: string, data?: Record) => void; } +export interface AuditLogger { + log: (event: AuditEvent) => void; +} + +interface AuditLogMeta extends AuditEvent { + session?: { + id: string; + }; + trace: { + id: string; + }; +} + export interface AuditServiceSetup { - getLogger: (id?: string) => AuditLogger; + asScoped: (request: KibanaRequest) => AuditLogger; + getLogger: (id?: string) => LegacyAuditLogger; } interface AuditServiceSetupParams { license: SecurityLicense; config: ConfigType['audit']; + logging: Pick; + http: Pick; + getCurrentUser( + request: KibanaRequest + ): ReturnType | undefined; + getSpaceId( + request: KibanaRequest + ): ReturnType | undefined; } export class AuditService { + /** + * @deprecated + */ private licenseFeaturesSubscription?: Subscription; - private auditLoggingEnabled = false; + /** + * @deprecated + */ + private allowAuditLogging = false; + + private ecsLogger: Logger; - constructor(private readonly logger: Logger) {} + constructor(private readonly logger: Logger) { + this.ecsLogger = logger.get('ecs'); + } - setup({ license, config }: AuditServiceSetupParams): AuditServiceSetup { - if (config.enabled) { + setup({ + license, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }: AuditServiceSetupParams): AuditServiceSetup { + if (config.enabled && !config.appender) { this.licenseFeaturesSubscription = license.features$.subscribe(({ allowAuditLogging }) => { - this.auditLoggingEnabled = allowAuditLogging; + this.allowAuditLogging = allowAuditLogging; }); } - return { - getLogger: (id?: string): AuditLogger => { - return { - log: (eventType: string, message: string, data?: Record) => { - if (!this.auditLoggingEnabled) { - return; - } - - this.logger.info(message, { - tags: id ? [id, eventType] : [eventType], - eventType, - ...data, - }); + // Configure logging during setup and when license changes + logging.configure( + license.features$.pipe( + distinctUntilKeyChanged('allowAuditLogging'), + createLoggingConfig(config) + ) + ); + + /** + * Creates an {@link AuditLogger} scoped to the current request. + * + * @example + * ```typescript + * const auditLogger = securitySetup.audit.asScoped(request); + * auditLogger.log(event); + * ``` + */ + const asScoped = (request: KibanaRequest): AuditLogger => { + /** + * Logs an {@link AuditEvent} and automatically adds meta data about the + * current user, space and correlation id. + * + * Guidelines around what events should be logged and how they should be + * structured can be found in: `/x-pack/plugins/security/README.md` + * + * @example + * ```typescript + * const auditLogger = securitySetup.audit.asScoped(request); + * auditLogger.log({ + * message: 'User is updating dashboard [id=123]', + * event: { + * action: 'saved_object_update', + * outcome: 'unknown' + * }, + * kibana: { + * saved_object: { type: 'dashboard', id: '123' } + * }, + * }); + * ``` + */ + const log = (event: AuditEvent) => { + const user = getCurrentUser(request); + const spaceId = getSpaceId(request); + const meta: AuditLogMeta = { + ...event, + user: + (user && { + name: user.username, + roles: user.roles, + }) || + event.user, + kibana: { + space_id: spaceId, + ...event.kibana, + }, + trace: { + id: request.id, }, }; - }, + if (filterEvent(meta, config.ignore_filters)) { + this.ecsLogger.info(event.message!, meta); + } + }; + return { log }; }; + + /** + * @deprecated + * Use `audit.asScoped(request)` method instead to create an audit logger + */ + const getLogger = (id?: string): LegacyAuditLogger => { + return { + log: (eventType: string, message: string, data?: Record) => { + if (!this.allowAuditLogging) { + return; + } + + this.logger.info(message, { + tags: id ? [id, eventType] : [eventType], + eventType, + ...data, + }); + }, + }; + }; + + http.registerOnPostAuth((request, response, t) => { + if (request.auth.isAuthenticated) { + asScoped(request).log(httpRequestEvent({ request })); + } + return t.next(); + }); + + return { asScoped, getLogger }; } stop() { @@ -61,3 +190,40 @@ export class AuditService { } } } + +export const createLoggingConfig = (config: ConfigType['audit']) => + map, LoggerContextConfigInput>((features) => ({ + appenders: { + auditTrailAppender: config.appender ?? { + kind: 'console', + layout: { + kind: 'pattern', + highlight: true, + }, + }, + }, + loggers: [ + { + context: 'audit.ecs', + level: config.enabled && config.appender && features.allowAuditLogging ? 'info' : 'off', + appenders: ['auditTrailAppender'], + }, + ], + })); + +export function filterEvent( + event: AuditEvent, + ignoreFilters: ConfigType['audit']['ignore_filters'] +) { + if (ignoreFilters) { + return !ignoreFilters.some( + (rule) => + (!rule.actions || rule.actions.includes(event.event.action)) && + (!rule.categories || rule.categories.includes(event.event.category!)) && + (!rule.types || rule.types.includes(event.event.type!)) && + (!rule.outcomes || rule.outcomes.includes(event.event.outcome!)) && + (!rule.spaces || rule.spaces.includes(event.kibana?.space_id!)) + ); + } + return true; +} diff --git a/x-pack/plugins/security/server/audit/index.mock.ts b/x-pack/plugins/security/server/audit/index.mock.ts index 07341cc06e88..cf95fbbffa96 100644 --- a/x-pack/plugins/security/server/audit/index.mock.ts +++ b/x-pack/plugins/security/server/audit/index.mock.ts @@ -21,6 +21,9 @@ export const auditServiceMock = { create() { return { getLogger: jest.fn(), + asScoped: jest.fn().mockReturnValue({ + log: jest.fn(), + }), } as jest.Mocked>; }, }; diff --git a/x-pack/plugins/security/server/audit/index.ts b/x-pack/plugins/security/server/audit/index.ts index 3db160c703e3..09f3df8b310e 100644 --- a/x-pack/plugins/security/server/audit/index.ts +++ b/x-pack/plugins/security/server/audit/index.ts @@ -4,5 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export { AuditService, AuditServiceSetup, AuditLogger } from './audit_service'; +export { AuditService, AuditServiceSetup, AuditLogger, LegacyAuditLogger } from './audit_service'; +export { + AuditEvent, + EventCategory, + EventType, + EventOutcome, + userLoginEvent, + httpRequestEvent, + savedObjectEvent, + SavedObjectAction, +} from './audit_events'; export { SecurityAuditLogger } from './security_audit_logger'; diff --git a/x-pack/plugins/security/server/audit/security_audit_logger.ts b/x-pack/plugins/security/server/audit/security_audit_logger.ts index 87f7201f8566..ee81f5f330f4 100644 --- a/x-pack/plugins/security/server/audit/security_audit_logger.ts +++ b/x-pack/plugins/security/server/audit/security_audit_logger.ts @@ -5,11 +5,17 @@ */ import { AuthenticationProvider } from '../../common/types'; -import { AuditLogger } from './audit_service'; +import { LegacyAuditLogger } from './audit_service'; +/** + * @deprecated + */ export class SecurityAuditLogger { - constructor(private readonly logger: AuditLogger) {} + constructor(private readonly logger: LegacyAuditLogger) {} + /** + * @deprecated + */ savedObjectsAuthorizationFailure( username: string, action: string, @@ -37,6 +43,9 @@ export class SecurityAuditLogger { ); } + /** + * @deprecated + */ savedObjectsAuthorizationSuccess( username: string, action: string, @@ -59,6 +68,9 @@ export class SecurityAuditLogger { ); } + /** + * @deprecated + */ accessAgreementAcknowledged(username: string, provider: AuthenticationProvider) { this.logger.log( 'access_agreement_acknowledged', diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 4f52ebe3065a..e5bb00cdc056 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -19,7 +19,7 @@ import { } from '../../../../../src/core/server/mocks'; import { licenseMock } from '../../common/licensing/index.mock'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; -import { securityAuditLoggerMock } from '../audit/index.mock'; +import { auditServiceMock, securityAuditLoggerMock } from '../audit/index.mock'; import { sessionMock } from '../session_management/index.mock'; import { SecurityLicenseFeatures } from '../../common/licensing'; import { ConfigSchema, createConfig } from '../config'; @@ -40,7 +40,8 @@ function getMockOptions({ selector?: AuthenticatorOptions['config']['authc']['selector']; } = {}) { return { - auditLogger: securityAuditLoggerMock.create(), + legacyAuditLogger: securityAuditLoggerMock.create(), + audit: auditServiceMock.create(), getCurrentUser: jest.fn(), clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), basePath: httpServiceMock.createSetupContract().basePath, @@ -215,9 +216,15 @@ describe('Authenticator', () => { let authenticator: Authenticator; let mockOptions: ReturnType; let mockSessVal: SessionValue; + const auditLogger = { + log: jest.fn(), + }; + beforeEach(() => { + auditLogger.log.mockClear(); mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } }); mockOptions.session.get.mockResolvedValue(null); + mockOptions.audit.asScoped.mockReturnValue(auditLogger); mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } }); authenticator = new Authenticator(mockOptions); @@ -280,6 +287,49 @@ describe('Authenticator', () => { ); }); + it('adds audit event when successful.', async () => { + const request = httpServerMock.createKibanaRequest(); + const user = mockAuthenticatedUser(); + mockBasicAuthenticationProvider.login.mockResolvedValue( + AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) + ); + await authenticator.login(request, { provider: { type: 'basic' }, value: {} }); + + expect(auditLogger.log).toHaveBeenCalledTimes(1); + expect(auditLogger.log).toHaveBeenCalledWith( + expect.objectContaining({ + event: { action: 'user_login', category: 'authentication', outcome: 'success' }, + }) + ); + }); + + it('adds audit event when not successful.', async () => { + const request = httpServerMock.createKibanaRequest(); + const failureReason = new Error('Not Authorized'); + mockBasicAuthenticationProvider.login.mockResolvedValue( + AuthenticationResult.failed(failureReason) + ); + await authenticator.login(request, { provider: { type: 'basic' }, value: {} }); + + expect(auditLogger.log).toHaveBeenCalledTimes(1); + expect(auditLogger.log).toHaveBeenCalledWith( + expect.objectContaining({ + event: { action: 'user_login', category: 'authentication', outcome: 'failure' }, + }) + ); + }); + + it('does not add audit event when not handled.', async () => { + const request = httpServerMock.createKibanaRequest(); + await expect( + authenticator.login(request, { provider: { type: 'token' }, value: {} }) + ).resolves.toEqual(AuthenticationResult.notHandled()); + + await authenticator.login(request, { provider: { name: 'basic2' }, value: {} }); + + expect(auditLogger.log).not.toHaveBeenCalled(); + }); + it('creates session whenever authentication provider returns state', async () => { const user = mockAuthenticatedUser(); const request = httpServerMock.createKibanaRequest(); @@ -1859,11 +1909,14 @@ describe('Authenticator', () => { accessAgreementAcknowledged: true, }); - expect(mockOptions.auditLogger.accessAgreementAcknowledged).toHaveBeenCalledTimes(1); - expect(mockOptions.auditLogger.accessAgreementAcknowledged).toHaveBeenCalledWith('user', { - type: 'basic', - name: 'basic1', - }); + expect(mockOptions.legacyAuditLogger.accessAgreementAcknowledged).toHaveBeenCalledTimes(1); + expect(mockOptions.legacyAuditLogger.accessAgreementAcknowledged).toHaveBeenCalledWith( + 'user', + { + type: 'basic', + name: 'basic1', + } + ); expect( mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index b8ec6258eb0d..0523ebaffb9d 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -13,7 +13,7 @@ import { import { SecurityLicense } from '../../common/licensing'; import { AuthenticatedUser } from '../../common/model'; import { AuthenticationProvider } from '../../common/types'; -import { SecurityAuditLogger } from '../audit'; +import { SecurityAuditLogger, AuditServiceSetup, userLoginEvent } from '../audit'; import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; import { SecurityFeatureUsageServiceStart } from '../feature_usage'; @@ -59,7 +59,8 @@ export interface ProviderLoginAttempt { } export interface AuthenticatorOptions { - auditLogger: SecurityAuditLogger; + legacyAuditLogger: SecurityAuditLogger; + audit: AuditServiceSetup; getFeatureUsageService: () => SecurityFeatureUsageServiceStart; getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; config: Pick; @@ -293,6 +294,20 @@ export class Authenticator { existingSessionValue, }); + // Checking for presence of `user` object to determine success state rather than + // `success()` method since that indicates a successful authentication and `redirect()` + // could also (but does not always) authenticate a user successfully (e.g. SAML flow) + if (authenticationResult.user || authenticationResult.failed()) { + const auditLogger = this.options.audit.asScoped(request); + auditLogger.log( + userLoginEvent({ + authenticationResult, + authenticationProvider: providerName, + authenticationType: provider.type, + }) + ); + } + return this.handlePreAccessRedirects( request, authenticationResult, @@ -421,7 +436,7 @@ export class Authenticator { accessAgreementAcknowledged: true, }); - this.options.auditLogger.accessAgreementAcknowledged( + this.options.legacyAuditLogger.accessAgreementAcknowledged( currentUser.username, existingSessionValue.provider ); diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index 263ea5c4e504..6f8f17a0a3c7 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -18,7 +18,7 @@ import { } from '../../../../../src/core/server/mocks'; import { licenseMock } from '../../common/licensing/index.mock'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; -import { securityAuditLoggerMock } from '../audit/index.mock'; +import { auditServiceMock, securityAuditLoggerMock } from '../audit/index.mock'; import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock'; import { sessionMock } from '../session_management/session.mock'; @@ -42,13 +42,14 @@ import { InvalidateAPIKeyParams, } from './api_keys'; import { SecurityLicense } from '../../common/licensing'; -import { SecurityAuditLogger } from '../audit'; +import { AuditServiceSetup, SecurityAuditLogger } from '../audit'; import { SecurityFeatureUsageServiceStart } from '../feature_usage'; import { Session } from '../session_management'; describe('setupAuthentication()', () => { let mockSetupAuthenticationParams: { - auditLogger: jest.Mocked; + legacyAuditLogger: jest.Mocked; + audit: jest.Mocked; config: ConfigType; loggers: LoggerFactory; http: jest.Mocked; @@ -60,7 +61,8 @@ describe('setupAuthentication()', () => { let mockScopedClusterClient: jest.Mocked>; beforeEach(() => { mockSetupAuthenticationParams = { - auditLogger: securityAuditLoggerMock.create(), + legacyAuditLogger: securityAuditLoggerMock.create(), + audit: auditServiceMock.create(), http: coreMock.createSetup().http, config: createConfig( ConfigSchema.validate({ diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 431c82fb28a6..ab8e42a6a72d 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -12,7 +12,7 @@ import { } from '../../../../../src/core/server'; import { SecurityLicense } from '../../common/licensing'; import { AuthenticatedUser } from '../../common/model'; -import { SecurityAuditLogger } from '../audit'; +import { SecurityAuditLogger, AuditServiceSetup } from '../audit'; import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; import { SecurityFeatureUsageServiceStart } from '../feature_usage'; @@ -45,7 +45,8 @@ export { } from './http_authentication'; interface SetupAuthenticationParams { - auditLogger: SecurityAuditLogger; + legacyAuditLogger: SecurityAuditLogger; + audit: AuditServiceSetup; getFeatureUsageService: () => SecurityFeatureUsageServiceStart; http: HttpServiceSetup; clusterClient: ILegacyClusterClient; @@ -58,7 +59,8 @@ interface SetupAuthenticationParams { export type Authentication = UnwrapPromise>; export async function setupAuthentication({ - auditLogger, + legacyAuditLogger: auditLogger, + audit, getFeatureUsageService, http, clusterClient, @@ -82,7 +84,8 @@ export async function setupAuthentication({ }; const authenticator = new Authenticator({ - auditLogger, + legacyAuditLogger: auditLogger, + audit, loggers, clusterClient, basePath: http.basePath, diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 093a7643fbf6..32b8708d2b38 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('crypto', () => ({ randomBytes: jest.fn() })); +jest.mock('crypto', () => ({ + randomBytes: jest.fn(), + constants: jest.requireActual('crypto').constants, +})); import { loggingSystemMock } from '../../../../src/core/server/mocks'; import { createConfig, ConfigSchema } from './config'; @@ -150,31 +153,23 @@ describe('config schema', () => { }); it('should throw error if xpack.security.encryptionKey is less than 32 characters', () => { - expect(() => - ConfigSchema.validate({ encryptionKey: 'foo' }) - ).toThrowErrorMatchingInlineSnapshot( - `"[encryptionKey]: value has length [3] but it must have a minimum length of [32]."` + expect(() => ConfigSchema.validate({ encryptionKey: 'foo' })).toThrow( + '[encryptionKey]: value has length [3] but it must have a minimum length of [32].' ); - expect(() => - ConfigSchema.validate({ encryptionKey: 'foo' }, { dist: true }) - ).toThrowErrorMatchingInlineSnapshot( - `"[encryptionKey]: value has length [3] but it must have a minimum length of [32]."` + expect(() => ConfigSchema.validate({ encryptionKey: 'foo' }, { dist: true })).toThrow( + '[encryptionKey]: value has length [3] but it must have a minimum length of [32].' ); }); describe('authc.oidc', () => { it(`returns a validation error when authc.providers is "['oidc']" and realm is unspecified`, async () => { - expect(() => - ConfigSchema.validate({ authc: { providers: ['oidc'] } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[authc.oidc.realm]: expected value of type [string] but got [undefined]"` + expect(() => ConfigSchema.validate({ authc: { providers: ['oidc'] } })).toThrow( + '[authc.oidc.realm]: expected value of type [string] but got [undefined]' ); - expect(() => - ConfigSchema.validate({ authc: { providers: ['oidc'], oidc: {} } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[authc.oidc.realm]: expected value of type [string] but got [undefined]"` + expect(() => ConfigSchema.validate({ authc: { providers: ['oidc'], oidc: {} } })).toThrow( + '[authc.oidc.realm]: expected value of type [string] but got [undefined]' ); }); @@ -204,10 +199,8 @@ describe('config schema', () => { }); it(`returns a validation error when authc.providers is "['oidc', 'basic']" and realm is unspecified`, async () => { - expect(() => - ConfigSchema.validate({ authc: { providers: ['oidc', 'basic'] } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[authc.oidc.realm]: expected value of type [string] but got [undefined]"` + expect(() => ConfigSchema.validate({ authc: { providers: ['oidc', 'basic'] } })).toThrow( + '[authc.oidc.realm]: expected value of type [string] but got [undefined]' ); }); @@ -240,22 +233,18 @@ describe('config schema', () => { it(`realm is not allowed when authc.providers is "['basic']"`, async () => { expect(() => ConfigSchema.validate({ authc: { providers: ['basic'], oidc: { realm: 'realm-1' } } }) - ).toThrowErrorMatchingInlineSnapshot(`"[authc.oidc]: a value wasn't expected to be present"`); + ).toThrow("[authc.oidc]: a value wasn't expected to be present"); }); }); describe('authc.saml', () => { it('fails if authc.providers includes `saml`, but `saml.realm` is not specified', async () => { - expect(() => - ConfigSchema.validate({ authc: { providers: ['saml'] } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[authc.saml.realm]: expected value of type [string] but got [undefined]"` + expect(() => ConfigSchema.validate({ authc: { providers: ['saml'] } })).toThrow( + '[authc.saml.realm]: expected value of type [string] but got [undefined]' ); - expect(() => - ConfigSchema.validate({ authc: { providers: ['saml'], saml: {} } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[authc.saml.realm]: expected value of type [string] but got [undefined]"` + expect(() => ConfigSchema.validate({ authc: { providers: ['saml'], saml: {} } })).toThrow( + '[authc.saml.realm]: expected value of type [string] but got [undefined]' ); expect( @@ -285,7 +274,7 @@ describe('config schema', () => { it('`realm` is not allowed if saml provider is not enabled', async () => { expect(() => ConfigSchema.validate({ authc: { providers: ['basic'], saml: { realm: 'realm-1' } } }) - ).toThrowErrorMatchingInlineSnapshot(`"[authc.saml]: a value wasn't expected to be present"`); + ).toThrow("[authc.saml]: a value wasn't expected to be present"); }); it('`maxRedirectURLSize` accepts any positive value that can coerce to `ByteSizeValue`', async () => { @@ -360,11 +349,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { basic: { basic1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.basic.basic1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.basic.basic1.order]: expected value of type [number] but got [undefined]' + ); }); it('cannot be hidden from selector', () => { @@ -374,11 +361,9 @@ describe('config schema', () => { providers: { basic: { basic1: { order: 0, showInSelector: false } } }, }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.basic.basic1.showInSelector]: \`basic\` provider only supports \`true\` in \`showInSelector\`." -`); + ).toThrow( + '[authc.providers.1.basic.basic1.showInSelector]: `basic` provider only supports `true` in `showInSelector`.' + ); }); it('can have only provider of this type', () => { @@ -386,11 +371,7 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { basic: { basic1: { order: 0 }, basic2: { order: 1 } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.basic]: Only one \\"basic\\" provider can be configured." -`); + ).toThrow('[authc.providers.1.basic]: Only one "basic" provider can be configured'); }); it('can be successfully validated', () => { @@ -420,11 +401,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { token: { token1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.token.token1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.token.token1.order]: expected value of type [number] but got [undefined]' + ); }); it('cannot be hidden from selector', () => { @@ -434,11 +413,9 @@ describe('config schema', () => { providers: { token: { token1: { order: 0, showInSelector: false } } }, }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.token.token1.showInSelector]: \`token\` provider only supports \`true\` in \`showInSelector\`." -`); + ).toThrow( + '[authc.providers.1.token.token1.showInSelector]: `token` provider only supports `true` in `showInSelector`.' + ); }); it('can have only provider of this type', () => { @@ -446,11 +423,7 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { token: { token1: { order: 0 }, token2: { order: 1 } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.token]: Only one \\"token\\" provider can be configured." -`); + ).toThrow('[authc.providers.1.token]: Only one "token" provider can be configured'); }); it('can be successfully validated', () => { @@ -480,11 +453,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { pki: { pki1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.pki.pki1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.pki.pki1.order]: expected value of type [number] but got [undefined]' + ); }); it('can have only provider of this type', () => { @@ -492,11 +463,7 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { pki: { pki1: { order: 0 }, pki2: { order: 1 } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.pki]: Only one \\"pki\\" provider can be configured." -`); + ).toThrow('[authc.providers.1.pki]: Only one "pki" provider can be configured'); }); it('can be successfully validated', () => { @@ -524,11 +491,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { kerberos: { kerberos1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.kerberos.kerberos1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.kerberos.kerberos1.order]: expected value of type [number] but got [undefined]' + ); }); it('can have only provider of this type', () => { @@ -538,11 +503,7 @@ describe('config schema', () => { providers: { kerberos: { kerberos1: { order: 0 }, kerberos2: { order: 1 } } }, }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.kerberos]: Only one \\"kerberos\\" provider can be configured." -`); + ).toThrow('[authc.providers.1.kerberos]: Only one "kerberos" provider can be configured'); }); it('can be successfully validated', () => { @@ -570,11 +531,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { oidc: { oidc1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.oidc.oidc1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.oidc.oidc1.order]: expected value of type [number] but got [undefined]' + ); }); it('requires `realm`', () => { @@ -582,11 +541,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { oidc: { oidc1: { order: 0 } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.oidc.oidc1.realm]: expected value of type [string] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.oidc.oidc1.realm]: expected value of type [string] but got [undefined]' + ); }); it('can be successfully validated', () => { @@ -625,11 +582,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { saml: { saml1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.saml.saml1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.saml.saml1.order]: expected value of type [number] but got [undefined]' + ); }); it('requires `realm`', () => { @@ -637,11 +592,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { saml: { saml1: { order: 0 } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.saml.saml1.realm]: expected value of type [string] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.saml.saml1.realm]: expected value of type [string] but got [undefined]' + ); }); it('can be successfully validated', () => { @@ -703,11 +656,9 @@ describe('config schema', () => { }, }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1]: Found multiple providers configured with the same name \\"provider1\\": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider1]" -`); + ).toThrow( + '[authc.providers.1]: Found multiple providers configured with the same name "provider1": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider1]' + ); }); it('`order` should be unique across all provider types', () => { @@ -723,11 +674,9 @@ describe('config schema', () => { }, }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1]: Found multiple providers configured with the same order \\"0\\": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider2]" -`); + ).toThrow( + '[authc.providers.1]: Found multiple providers configured with the same order "0": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider2]' + ); }); it('can be successfully validated with multiple providers ignoring uniqueness violations in disabled ones', () => { @@ -792,10 +741,8 @@ describe('config schema', () => { describe('session', () => { it('should throw error if xpack.security.session.cleanupInterval is less than 10 seconds', () => { - expect(() => - ConfigSchema.validate({ session: { cleanupInterval: '9s' } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[session.cleanupInterval]: the value must be greater or equal to 10 seconds."` + expect(() => ConfigSchema.validate({ session: { cleanupInterval: '9s' } })).toThrow( + '[session.cleanupInterval]: the value must be greater or equal to 10 seconds.' ); }); }); @@ -1091,4 +1038,55 @@ describe('createConfig()', () => { ] `); }); + + it('accepts an audit appender', () => { + expect( + ConfigSchema.validate({ + audit: { + appender: { + kind: 'file', + path: '/path/to/file.txt', + layout: { + kind: 'json', + }, + }, + }, + }).audit.appender + ).toMatchInlineSnapshot(` + Object { + "kind": "file", + "layout": Object { + "kind": "json", + }, + "path": "/path/to/file.txt", + } + `); + }); + + it('rejects an appender if not fully configured', () => { + expect(() => + ConfigSchema.validate({ + audit: { + // no layout configured + appender: { + kind: 'file', + path: '/path/to/file.txt', + }, + }, + }) + ).toThrow('[audit.appender.2.kind]: expected value to equal [legacy-appender]'); + }); + + it('rejects an ignore_filter when no appender is configured', () => { + expect(() => + ConfigSchema.validate({ + audit: { + enabled: true, + ignore_filters: [{ actions: ['some_action'] }], + }, + }) + ).toThrow( + '[audit]: xpack.security.audit.ignore_filters can only be used with the ECS audit logger. To enable the ECS audit logger, specify where you want to write the audit events using xpack.security.audit.appender.' + ); + }); }); diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 9ccbdac5e09f..80b46a67ce01 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -7,7 +7,7 @@ import crypto from 'crypto'; import { schema, Type, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { Logger } from '../../../../src/core/server'; +import { Logger, config as coreConfig } from '../../../../src/core/server'; export type ConfigType = ReturnType; @@ -198,9 +198,30 @@ export const ConfigSchema = schema.object({ schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey'] }), }), }), - audit: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - }), + audit: schema.object( + { + enabled: schema.boolean({ defaultValue: false }), + appender: schema.maybe(coreConfig.logging.appenders), + ignore_filters: schema.maybe( + schema.arrayOf( + schema.object({ + actions: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + categories: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + types: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + outcomes: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + spaces: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + }) + ) + ), + }, + { + validate: (auditConfig) => { + if (auditConfig.ignore_filters && !auditConfig.appender) { + return 'xpack.security.audit.ignore_filters can only be used with the ECS audit logger. To enable the ECS audit logger, specify where you want to write the audit events using xpack.security.audit.appender.'; + } + }, + } + ), }); export function createConfig( diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 00ad96211590..04db65f88cda 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -27,7 +27,7 @@ export { SAMLLogin, OIDCLogin, } from './authentication'; -export { AuditLogger } from './audit'; +export { LegacyAuditLogger } from './audit'; export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 9088d4f08d0e..9b08ba8c275f 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -55,6 +55,7 @@ describe('Security Plugin', () => { await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(` Object { "audit": Object { + "asScoped": [Function], "getLogger": [Function], }, "authc": Object { diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 5edc4c235727..52283290ba7b 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -67,7 +67,7 @@ export interface SecurityPluginSetup { 'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode' >; license: SecurityLicense; - audit: Pick; + audit: AuditServiceSetup; /** * If Spaces plugin is available it's supposed to register its SpacesService with Security plugin @@ -101,6 +101,7 @@ export class Plugin { private readonly logger: Logger; private spacesService?: SpacesService | symbol = Symbol('not accessed'); private securityLicenseService?: SecurityLicenseService; + private authc?: Authentication; private readonly featureUsageService = new SecurityFeatureUsageService(); private featureUsageServiceStart?: SecurityFeatureUsageServiceStart; @@ -176,8 +177,15 @@ export class Plugin { registerSecurityUsageCollector({ usageCollection, config, license }); - const audit = this.auditService.setup({ license, config: config.audit }); - const auditLogger = new SecurityAuditLogger(audit.getLogger()); + const audit = this.auditService.setup({ + license, + config: config.audit, + logging: core.logging, + http: core.http, + getSpaceId: (request) => this.getSpacesService()?.getSpaceId(request), + getCurrentUser: (request) => this.authc?.getCurrentUser(request), + }); + const legacyAuditLogger = new SecurityAuditLogger(audit.getLogger()); const { session } = this.sessionManagementService.setup({ config, @@ -187,8 +195,9 @@ export class Plugin { taskManager, }); - const authc = await setupAuthentication({ - auditLogger, + this.authc = await setupAuthentication({ + legacyAuditLogger, + audit, getFeatureUsageService: this.getFeatureUsageService, http: core.http, clusterClient, @@ -209,11 +218,12 @@ export class Plugin { buildNumber: this.initializerContext.env.packageInfo.buildNum, getSpacesService: this.getSpacesService, features, - getCurrentUser: authc.getCurrentUser, + getCurrentUser: this.authc.getCurrentUser, }); setupSavedObjects({ - auditLogger, + legacyAuditLogger, + audit, authz, savedObjects: core.savedObjects, getSpacesService: this.getSpacesService, @@ -226,7 +236,7 @@ export class Plugin { logger: this.initializerContext.logger.get('routes'), clusterClient, config, - authc, + authc: this.authc, authz, license, session, @@ -239,17 +249,18 @@ export class Plugin { return deepFreeze({ audit: { + asScoped: audit.asScoped, getLogger: audit.getLogger, }, authc: { - isAuthenticated: authc.isAuthenticated, - getCurrentUser: authc.getCurrentUser, - areAPIKeysEnabled: authc.areAPIKeysEnabled, - createAPIKey: authc.createAPIKey, - invalidateAPIKey: authc.invalidateAPIKey, - grantAPIKeyAsInternalUser: authc.grantAPIKeyAsInternalUser, - invalidateAPIKeyAsInternalUser: authc.invalidateAPIKeyAsInternalUser, + isAuthenticated: this.authc.isAuthenticated, + getCurrentUser: this.authc.getCurrentUser, + areAPIKeysEnabled: this.authc.areAPIKeysEnabled, + createAPIKey: this.authc.createAPIKey, + invalidateAPIKey: this.authc.invalidateAPIKey, + grantAPIKeyAsInternalUser: this.authc.grantAPIKeyAsInternalUser, + invalidateAPIKeyAsInternalUser: this.authc.invalidateAPIKeyAsInternalUser, }, authz: { diff --git a/x-pack/plugins/security/server/saved_objects/index.ts b/x-pack/plugins/security/server/saved_objects/index.ts index 6acfd06a0309..16c935e04893 100644 --- a/x-pack/plugins/security/server/saved_objects/index.ts +++ b/x-pack/plugins/security/server/saved_objects/index.ts @@ -12,11 +12,12 @@ import { } from '../../../../../src/core/server'; import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; import { AuthorizationServiceSetup } from '../authorization'; -import { SecurityAuditLogger } from '../audit'; +import { SecurityAuditLogger, AuditServiceSetup } from '../audit'; import { SpacesService } from '../plugin'; interface SetupSavedObjectsParams { - auditLogger: SecurityAuditLogger; + legacyAuditLogger: SecurityAuditLogger; + audit: AuditServiceSetup; authz: Pick< AuthorizationServiceSetup, 'mode' | 'actions' | 'checkSavedObjectsPrivilegesWithRequest' @@ -26,7 +27,8 @@ interface SetupSavedObjectsParams { } export function setupSavedObjects({ - auditLogger, + legacyAuditLogger, + audit, authz, savedObjects, getSpacesService, @@ -50,7 +52,8 @@ export function setupSavedObjects({ return authz.mode.useRbacForRequest(kibanaRequest) ? new SecureSavedObjectsClientWrapper({ actions: authz.actions, - auditLogger, + legacyAuditLogger, + auditLogger: audit.asScoped(kibanaRequest), baseClient: client, checkSavedObjectsPrivilegesAsCurrentUser: authz.checkSavedObjectsPrivilegesWithRequest( kibanaRequest diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index af1aaf16f7fe..8136553e4a62 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -6,10 +6,11 @@ import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; import { Actions } from '../authorization'; -import { securityAuditLoggerMock } from '../audit/index.mock'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { securityAuditLoggerMock, auditServiceMock } from '../audit/index.mock'; +import { savedObjectsClientMock, httpServerMock } from '../../../../../src/core/server/mocks'; import { SavedObjectsClientContract } from 'kibana/server'; import { SavedObjectActions } from '../authorization/actions/saved_object'; +import { AuditEvent, EventOutcome } from '../audit'; let clientOpts: ReturnType; let client: SecureSavedObjectsClientWrapper; @@ -38,7 +39,8 @@ const createSecureSavedObjectsClientWrapperOptions = () => { checkSavedObjectsPrivilegesAsCurrentUser: jest.fn(), errors, getSpacesService, - auditLogger: securityAuditLoggerMock.create(), + legacyAuditLogger: securityAuditLoggerMock.create(), + auditLogger: auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()), forbiddenError, generalError, }; @@ -53,8 +55,8 @@ const expectGeneralError = async (fn: Function, args: Record) => { clientOpts.generalError ); expect(clientOpts.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }; /** @@ -84,8 +86,8 @@ const expectForbiddenError = async (fn: Function, args: Record, act const spaceIds = [spaceId]; expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( USERNAME, action ?? ACTION, types, @@ -93,7 +95,7 @@ const expectForbiddenError = async (fn: Function, args: Record, act missing, args ); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }; const expectSuccess = async (fn: Function, args: Record, action?: string) => { @@ -105,9 +107,9 @@ const expectSuccess = async (fn: Function, args: Record, action?: s const types = getCalls.map((x) => x[0]); const spaceIds = args.options?.namespaces || [args.options?.namespace || 'default']; - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( USERNAME, action ?? ACTION, types, @@ -176,6 +178,26 @@ const expectObjectNamespaceFiltering = async ( ); }; +const expectAuditEvent = ( + action: AuditEvent['event']['action'], + outcome: AuditEvent['event']['outcome'], + savedObject?: Required['kibana']['saved_object'] +) => { + expect(clientOpts.auditLogger.log).toHaveBeenCalledWith( + expect.objectContaining({ + event: expect.objectContaining({ + action, + outcome, + }), + kibana: savedObject + ? expect.objectContaining({ + saved_object: savedObject, + }) + : expect.anything(), + }) + ); +}; + const expectObjectsNamespaceFiltering = async (fn: Function, args: Record) => { clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( getMockCheckPrivilegesSuccess // privilege check for authorization @@ -200,15 +222,13 @@ const expectObjectsNamespaceFiltering = async (fn: Function, args: Record { ); expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect( + clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure + ).toHaveBeenCalledWith( USERNAME, 'addToNamespacesCreate', [type], @@ -308,7 +330,7 @@ describe('#addToNamespaces', () => { [{ privilege, spaceId: newNs1 }], { id, type, namespaces, options: {} } ); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); test(`throws decorated ForbiddenError when unauthorized to update in current space`, async () => { @@ -324,9 +346,9 @@ describe('#addToNamespaces', () => { ); expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); expect( - clientOpts.auditLogger.savedObjectsAuthorizationFailure + clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure ).toHaveBeenLastCalledWith( USERNAME, 'addToNamespacesUpdate', @@ -335,7 +357,7 @@ describe('#addToNamespaces', () => { [{ privilege, spaceId: currentNs }], { id, type, namespaces, options: {} } ); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); }); test(`returns result of baseClient.addToNamespaces when authorized`, async () => { @@ -345,9 +367,9 @@ describe('#addToNamespaces', () => { const result = await client.addToNamespaces(type, id, namespaces); expect(result).toBe(apiCallReturnValue); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(2); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(2); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( 1, USERNAME, 'addToNamespacesCreate', // action for privilege check is 'share_to_space', but auditAction is 'addToNamespacesCreate' @@ -355,7 +377,7 @@ describe('#addToNamespaces', () => { namespaces.sort(), { type, id, namespaces, options: {} } ); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( 2, USERNAME, 'addToNamespacesUpdate', // action for privilege check is 'share_to_space', but auditAction is 'addToNamespacesUpdate' @@ -392,12 +414,28 @@ describe('#addToNamespaces', () => { // this operation is unique because it requires two privilege checks before it executes await expectObjectNamespaceFiltering(client.addToNamespaces, { type, id, namespaces }, 2); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.addToNamespaces.mockReturnValue(apiCallReturnValue as any); + await client.addToNamespaces(type, id, namespaces); + + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_add_to_spaces', EventOutcome.UNKNOWN, { type, id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.addToNamespaces(type, id, namespaces)).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_add_to_spaces', EventOutcome.FAILURE, { type, id }); + }); }); describe('#bulkCreate', () => { const attributes = { some: 'attr' }; - const obj1 = Object.freeze({ type: 'foo', otherThing: 'sup', attributes }); - const obj2 = Object.freeze({ type: 'bar', otherThing: 'everyone', attributes }); + const obj1 = Object.freeze({ type: 'foo', id: 'sup', attributes }); + const obj2 = Object.freeze({ type: 'bar', id: 'everyone', attributes }); const namespace = 'some-ns'; test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { @@ -445,6 +483,25 @@ describe('#bulkCreate', () => { const options = { namespace }; await expectObjectsNamespaceFiltering(client.bulkCreate, { objects, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkCreate.mockReturnValue(apiCallReturnValue as any); + const objects = [obj1, obj2]; + const options = { namespace }; + await expectSuccess(client.bulkCreate, { objects, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_create', EventOutcome.UNKNOWN, { type: obj1.type, id: obj1.id }); + expectAuditEvent('saved_object_create', EventOutcome.UNKNOWN, { type: obj2.type, id: obj2.id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.bulkCreate([obj1, obj2], { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_create', EventOutcome.FAILURE, { type: obj1.type, id: obj1.id }); + expectAuditEvent('saved_object_create', EventOutcome.FAILURE, { type: obj2.type, id: obj2.id }); + }); }); describe('#bulkGet', () => { @@ -484,6 +541,25 @@ describe('#bulkGet', () => { const options = { namespace }; await expectObjectsNamespaceFiltering(client.bulkGet, { objects, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkGet.mockReturnValue(apiCallReturnValue as any); + const objects = [obj1, obj2]; + const options = { namespace }; + await expectSuccess(client.bulkGet, { objects, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_get', EventOutcome.SUCCESS, obj1); + expectAuditEvent('saved_object_get', EventOutcome.SUCCESS, obj2); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.bulkGet([obj1, obj2], { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_get', EventOutcome.FAILURE, obj1); + expectAuditEvent('saved_object_get', EventOutcome.FAILURE, obj2); + }); }); describe('#bulkUpdate', () => { @@ -534,6 +610,25 @@ describe('#bulkUpdate', () => { const options = { namespace }; await expectObjectsNamespaceFiltering(client.bulkUpdate, { objects, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkUpdate.mockReturnValue(apiCallReturnValue as any); + const objects = [obj1, obj2]; + const options = { namespace }; + await expectSuccess(client.bulkUpdate, { objects, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_update', EventOutcome.UNKNOWN, { type: obj1.type, id: obj1.id }); + expectAuditEvent('saved_object_update', EventOutcome.UNKNOWN, { type: obj2.type, id: obj2.id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.bulkUpdate([obj1, obj2], { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_update', EventOutcome.FAILURE, { type: obj1.type, id: obj1.id }); + expectAuditEvent('saved_object_update', EventOutcome.FAILURE, { type: obj2.type, id: obj2.id }); + }); }); describe('#checkConflicts', () => { @@ -614,6 +709,22 @@ describe('#create', () => { const options = { namespace }; await expectObjectNamespaceFiltering(client.create, { type, attributes, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.create.mockResolvedValue(apiCallReturnValue as any); + const options = { namespace }; + await expectSuccess(client.create, { type, attributes, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_create', EventOutcome.UNKNOWN, { type }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.create(type, attributes, { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_create', EventOutcome.FAILURE, { type }); + }); }); describe('#delete', () => { @@ -643,6 +754,22 @@ describe('#delete', () => { const options = { namespace }; await expectPrivilegeCheck(client.delete, { type, id, options }, namespace); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.delete.mockReturnValue(apiCallReturnValue as any); + const options = { namespace }; + await expectSuccess(client.delete, { type, id, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_delete', EventOutcome.UNKNOWN, { type, id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.delete(type, id)).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_delete', EventOutcome.FAILURE, { type, id }); + }); }); describe('#find', () => { @@ -663,8 +790,10 @@ describe('#find', () => { const result = await client.find(options); expect(clientOpts.baseClient.find).not.toHaveBeenCalled(); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect( + clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure + ).toHaveBeenCalledWith( USERNAME, 'find', [type1], @@ -759,6 +888,27 @@ describe('#find', () => { const options = { type: [type1, type2], namespaces }; await expectObjectsNamespaceFiltering(client.find, { options }); }); + + test(`adds audit event when successful`, async () => { + const obj1 = { type: 'foo', id: 'sup' }; + const obj2 = { type: 'bar', id: 'everyone' }; + const apiCallReturnValue = { saved_objects: [obj1, obj2], foo: 'bar' }; + clientOpts.baseClient.find.mockReturnValue(apiCallReturnValue as any); + const options = Object.freeze({ type: type1, namespaces: ['some-ns'] }); + await expectSuccess(client.find, { options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_find', EventOutcome.SUCCESS, obj1); + expectAuditEvent('saved_object_find', EventOutcome.SUCCESS, obj2); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + await client.find({ type: type1 }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_find', EventOutcome.FAILURE); + }); }); describe('#get', () => { @@ -793,6 +943,22 @@ describe('#get', () => { const options = { namespace }; await expectObjectNamespaceFiltering(client.get, { type, id, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.get.mockReturnValue(apiCallReturnValue as any); + const options = { namespace }; + await expectSuccess(client.get, { type, id, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_get', EventOutcome.SUCCESS, { type, id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.get(type, id, { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_get', EventOutcome.FAILURE, { type, id }); + }); }); describe('#deleteFromNamespaces', () => { @@ -817,8 +983,8 @@ describe('#deleteFromNamespaces', () => { ); expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( USERNAME, 'deleteFromNamespaces', // action for privilege check is 'share_to_space', but auditAction is 'deleteFromNamespaces' [type], @@ -826,7 +992,7 @@ describe('#deleteFromNamespaces', () => { [{ privilege, spaceId: namespace1 }], { type, id, namespaces, options: {} } ); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); test(`returns result of baseClient.deleteFromNamespaces when authorized`, async () => { @@ -836,9 +1002,9 @@ describe('#deleteFromNamespaces', () => { const result = await client.deleteFromNamespaces(type, id, namespaces); expect(result).toBe(apiCallReturnValue); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( USERNAME, 'deleteFromNamespaces', // action for privilege check is 'share_to_space', but auditAction is 'deleteFromNamespaces' [type], @@ -864,6 +1030,21 @@ describe('#deleteFromNamespaces', () => { test(`filters namespaces that the user doesn't have access to`, async () => { await expectObjectNamespaceFiltering(client.deleteFromNamespaces, { type, id, namespaces }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(apiCallReturnValue as any); + await client.deleteFromNamespaces(type, id, namespaces); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_delete_from_spaces', EventOutcome.UNKNOWN, { type, id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_delete_from_spaces', EventOutcome.FAILURE, { type, id }); + }); }); describe('#update', () => { @@ -899,6 +1080,22 @@ describe('#update', () => { const options = { namespace }; await expectObjectNamespaceFiltering(client.update, { type, id, attributes, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.update.mockReturnValue(apiCallReturnValue as any); + const options = { namespace }; + await expectSuccess(client.update, { type, id, attributes, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_update', EventOutcome.UNKNOWN, { type, id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.update(type, id, attributes, { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_update', EventOutcome.FAILURE, { type, id }); + }); }); describe('other', () => { diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index d94dac942845..c7a3f31cc517 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -23,10 +23,12 @@ import { SecurityAuditLogger } from '../audit'; import { Actions, CheckSavedObjectsPrivileges } from '../authorization'; import { CheckPrivilegesResponse } from '../authorization/types'; import { SpacesService } from '../plugin'; +import { AuditLogger, EventOutcome, SavedObjectAction, savedObjectEvent } from '../audit'; interface SecureSavedObjectsClientWrapperOptions { actions: Actions; - auditLogger: SecurityAuditLogger; + legacyAuditLogger: SecurityAuditLogger; + auditLogger: AuditLogger; baseClient: SavedObjectsClientContract; errors: SavedObjectsClientContract['errors']; checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; @@ -58,7 +60,8 @@ interface EnsureAuthorizedTypeResult { export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContract { private readonly actions: Actions; - private readonly auditLogger: PublicMethodsOf; + private readonly legacyAuditLogger: PublicMethodsOf; + private readonly auditLogger: AuditLogger; private readonly baseClient: SavedObjectsClientContract; private readonly checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; private getSpacesService: () => SpacesService | undefined; @@ -66,6 +69,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra constructor({ actions, + legacyAuditLogger, auditLogger, baseClient, checkSavedObjectsPrivilegesAsCurrentUser, @@ -74,6 +78,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra }: SecureSavedObjectsClientWrapperOptions) { this.errors = errors; this.actions = actions; + this.legacyAuditLogger = legacyAuditLogger; this.auditLogger = auditLogger; this.baseClient = baseClient; this.checkSavedObjectsPrivilegesAsCurrentUser = checkSavedObjectsPrivilegesAsCurrentUser; @@ -85,9 +90,27 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra attributes: T = {} as T, options: SavedObjectsCreateOptions = {} ) { - const args = { type, attributes, options }; - const namespaces = [options.namespace, ...(options.initialNamespaces || [])]; - await this.ensureAuthorized(type, 'create', namespaces, { args }); + try { + const args = { type, attributes, options }; + const namespaces = [options.namespace, ...(options.initialNamespaces || [])]; + await this.ensureAuthorized(type, 'create', namespaces, { args }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + savedObject: { type, id: options.id }, + error, + }) + ); + throw error; + } + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id: options.id }, + }) + ); const savedObject = await this.baseClient.create(type, attributes, options); return await this.redactSavedObjectNamespaces(savedObject); @@ -112,25 +135,65 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: Array>, options: SavedObjectsBaseOptions = {} ) { - const args = { objects, options }; - const namespaces = objects.reduce( - (acc, { initialNamespaces = [] }) => { - return acc.concat(initialNamespaces); - }, - [options.namespace] - ); + try { + const args = { objects, options }; + const namespaces = objects.reduce( + (acc, { initialNamespaces = [] }) => { + return acc.concat(initialNamespaces); + }, + [options.namespace] + ); - await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_create', namespaces, { - args, - }); + await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_create', namespaces, { + args, + }); + } catch (error) { + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + savedObject: { type, id }, + error, + }) + ) + ); + throw error; + } + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + }) + ) + ); const response = await this.baseClient.bulkCreate(objects, options); return await this.redactSavedObjectsNamespaces(response); } public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { - const args = { type, id, options }; - await this.ensureAuthorized(type, 'delete', options.namespace, { args }); + try { + const args = { type, id, options }; + await this.ensureAuthorized(type, 'delete', options.namespace, { args }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.DELETE, + savedObject: { type, id }, + error, + }) + ); + throw error; + } + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.DELETE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + }) + ); return await this.baseClient.delete(type, id, options); } @@ -145,6 +208,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra `_find across namespaces is not permitted when the Spaces plugin is disabled.` ); } + const args = { options }; const { status, typeMap } = await this.ensureAuthorized( options.type, @@ -155,6 +219,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra if (status === 'unauthorized') { // return empty response + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.FIND, + error: new Error(status), + }) + ); return SavedObjectsUtils.createEmptyFindResponse(options); } @@ -163,11 +233,22 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra isGloballyAuthorized ? acc.set(type, options.namespaces) : acc.set(type, authorizedSpaces), new Map() ); + const response = await this.baseClient.find({ ...options, typeToNamespacesMap: undefined, // if the user is fully authorized, use `undefined` as the typeToNamespacesMap to prevent privilege escalation ...(status === 'partially_authorized' && { typeToNamespacesMap, type: '', namespaces: [] }), // the repository requires that `type` and `namespaces` must be empty if `typeToNamespacesMap` is defined }); + + response.saved_objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.FIND, + savedObject: { type, id }, + }) + ) + ); + return await this.redactSavedObjectsNamespaces(response); } @@ -175,20 +256,67 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ) { - const args = { objects, options }; - await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_get', options.namespace, { - args, - }); + try { + const args = { objects, options }; + await this.ensureAuthorized( + this.getUniqueObjectTypes(objects), + 'bulk_get', + options.namespace, + { + args, + } + ); + } catch (error) { + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.GET, + savedObject: { type, id }, + error, + }) + ) + ); + throw error; + } const response = await this.baseClient.bulkGet(objects, options); + + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.GET, + savedObject: { type, id }, + }) + ) + ); + return await this.redactSavedObjectsNamespaces(response); } public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { - const args = { type, id, options }; - await this.ensureAuthorized(type, 'get', options.namespace, { args }); + try { + const args = { type, id, options }; + await this.ensureAuthorized(type, 'get', options.namespace, { args }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.GET, + savedObject: { type, id }, + error, + }) + ); + throw error; + } const savedObject = await this.baseClient.get(type, id, options); + + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.GET, + savedObject: { type, id }, + }) + ); + return await this.redactSavedObjectNamespaces(savedObject); } @@ -198,8 +326,26 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra attributes: Partial, options: SavedObjectsUpdateOptions = {} ) { - const args = { type, id, attributes, options }; - await this.ensureAuthorized(type, 'update', options.namespace, { args }); + try { + const args = { type, id, attributes, options }; + await this.ensureAuthorized(type, 'update', options.namespace, { args }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE, + savedObject: { type, id }, + error, + }) + ); + throw error; + } + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + }) + ); const savedObject = await this.baseClient.update(type, id, attributes, options); return await this.redactSavedObjectNamespaces(savedObject); @@ -211,25 +357,45 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra namespaces: string[], options: SavedObjectsAddToNamespacesOptions = {} ) { - const args = { type, id, namespaces, options }; - const { namespace } = options; - // To share an object, the user must have the "share_to_space" permission in each of the destination namespaces. - await this.ensureAuthorized(type, 'share_to_space', namespaces, { - args, - auditAction: 'addToNamespacesCreate', - }); - - // To share an object, the user must also have the "share_to_space" permission in one or more of the source namespaces. Because the - // `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "share_to_space" permission in - // the current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation - // will result in a 404 error. - await this.ensureAuthorized(type, 'share_to_space', namespace, { - args, - auditAction: 'addToNamespacesUpdate', - }); + try { + const args = { type, id, namespaces, options }; + const { namespace } = options; + // To share an object, the user must have the "share_to_space" permission in each of the destination namespaces. + await this.ensureAuthorized(type, 'share_to_space', namespaces, { + args, + auditAction: 'addToNamespacesCreate', + }); + + // To share an object, the user must also have the "share_to_space" permission in one or more of the source namespaces. Because the + // `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "share_to_space" permission in + // the current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation + // will result in a 404 error. + await this.ensureAuthorized(type, 'share_to_space', namespace, { + args, + auditAction: 'addToNamespacesUpdate', + }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.ADD_TO_SPACES, + savedObject: { type, id }, + addToSpaces: namespaces, + error, + }) + ); + throw error; + } + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.ADD_TO_SPACES, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + addToSpaces: namespaces, + }) + ); - const result = await this.baseClient.addToNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(result); + const response = await this.baseClient.addToNamespaces(type, id, namespaces, options); + return await this.redactSavedObjectNamespaces(response); } public async deleteFromNamespaces( @@ -238,31 +404,73 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra namespaces: string[], options: SavedObjectsDeleteFromNamespacesOptions = {} ) { - const args = { type, id, namespaces, options }; - // To un-share an object, the user must have the "share_to_space" permission in each of the target namespaces. - await this.ensureAuthorized(type, 'share_to_space', namespaces, { - args, - auditAction: 'deleteFromNamespaces', - }); + try { + const args = { type, id, namespaces, options }; + // To un-share an object, the user must have the "share_to_space" permission in each of the target namespaces. + await this.ensureAuthorized(type, 'share_to_space', namespaces, { + args, + auditAction: 'deleteFromNamespaces', + }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.DELETE_FROM_SPACES, + savedObject: { type, id }, + deleteFromSpaces: namespaces, + error, + }) + ); + throw error; + } + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.DELETE_FROM_SPACES, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + deleteFromSpaces: namespaces, + }) + ); - const result = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(result); + const response = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options); + return await this.redactSavedObjectNamespaces(response); } public async bulkUpdate( objects: Array> = [], options: SavedObjectsBaseOptions = {} ) { - const objectNamespaces = objects - // The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace; - // in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so. - .filter(({ namespace }) => namespace !== undefined) - .map(({ namespace }) => namespace!); - const namespaces = [options?.namespace, ...objectNamespaces]; - const args = { objects, options }; - await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { - args, - }); + try { + const objectNamespaces = objects + // The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace; + // in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so. + .filter(({ namespace }) => namespace !== undefined) + .map(({ namespace }) => namespace!); + const namespaces = [options?.namespace, ...objectNamespaces]; + const args = { objects, options }; + await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { + args, + }); + } catch (error) { + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE, + savedObject: { type, id }, + error, + }) + ) + ); + throw error; + } + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + }) + ) + ); const response = await this.baseClient.bulkUpdate(objects, options); return await this.redactSavedObjectsNamespaces(response); @@ -316,7 +524,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const logAuthorizationFailure = () => { - this.auditLogger.savedObjectsAuthorizationFailure( + this.legacyAuditLogger.savedObjectsAuthorizationFailure( username, auditAction, types, @@ -326,7 +534,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); }; const logAuthorizationSuccess = (typeArray: string[], spaceIdArray: string[]) => { - this.auditLogger.savedObjectsAuthorizationSuccess( + this.legacyAuditLogger.savedObjectsAuthorizationSuccess( username, auditAction, typeArray, 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/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index 7e3b3d125fb5..66119e098238 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -26,6 +26,53 @@ interface Node { parent_entity_id?: string; } +describe('data generator data streams', () => { + // these tests cast the result of the generate methods so that we can specifically compare the `data_stream` fields + it('creates a generator with default data streams', () => { + const generator = new EndpointDocGenerator('seed'); + expect(generator.generateHostMetadata().data_stream).toEqual({ + type: 'metrics', + dataset: 'endpoint.metadata', + namespace: 'default', + }); + expect(generator.generatePolicyResponse().data_stream).toEqual({ + type: 'metrics', + dataset: 'endpoint.policy', + namespace: 'default', + }); + expect(generator.generateEvent().data_stream).toEqual({ + type: 'logs', + dataset: 'endpoint.events.process', + namespace: 'default', + }); + expect(generator.generateAlert().data_stream).toEqual({ + type: 'logs', + dataset: 'endpoint.alerts', + namespace: 'default', + }); + }); + + it('creates a generator with custom data streams', () => { + const metadataDataStream = { type: 'meta', dataset: 'dataset', namespace: 'name' }; + const policyDataStream = { type: 'policy', dataset: 'fake', namespace: 'something' }; + const eventsDataStream = { type: 'events', dataset: 'events stuff', namespace: 'name' }; + const alertsDataStream = { type: 'alerts', dataset: 'alerts stuff', namespace: 'name' }; + const generator = new EndpointDocGenerator('seed'); + expect(generator.generateHostMetadata(0, metadataDataStream).data_stream).toStrictEqual( + metadataDataStream + ); + expect(generator.generatePolicyResponse({ policyDataStream }).data_stream).toStrictEqual( + policyDataStream + ); + expect(generator.generateEvent({ eventsDataStream }).data_stream).toStrictEqual( + eventsDataStream + ); + expect(generator.generateAlert({ alertsDataStream }).data_stream).toStrictEqual( + alertsDataStream + ); + }); +}); + describe('data generator', () => { let generator: EndpointDocGenerator; beforeEach(() => { @@ -69,7 +116,7 @@ describe('data generator', () => { it('creates policy response documents', () => { const timestamp = new Date().getTime(); - const hostPolicyResponse = generator.generatePolicyResponse(timestamp); + const hostPolicyResponse = generator.generatePolicyResponse({ ts: timestamp }); expect(hostPolicyResponse['@timestamp']).toEqual(timestamp); expect(hostPolicyResponse.event.created).toEqual(timestamp); expect(hostPolicyResponse.Endpoint).not.toBeNull(); @@ -80,7 +127,7 @@ describe('data generator', () => { it('creates alert event documents', () => { const timestamp = new Date().getTime(); - const alert = generator.generateAlert(timestamp); + const alert = generator.generateAlert({ ts: timestamp }); expect(alert['@timestamp']).toEqual(timestamp); expect(alert.event?.action).not.toBeNull(); expect(alert.Endpoint).not.toBeNull(); diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index f0254616e6c9..07b230ffc6cc 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -7,6 +7,7 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; import { AlertEvent, + DataStream, EndpointStatus, Host, HostMetadata, @@ -59,6 +60,7 @@ interface EventOptions { pid?: number; parentPid?: number; extensions?: object; + eventsDataStream?: DataStream; } const Windows: OSFields[] = [ @@ -330,6 +332,8 @@ export interface TreeOptions { percentTerminated?: number; alwaysGenMaxChildrenPerNode?: boolean; ancestryArraySize?: number; + eventsDataStream?: DataStream; + alertsDataStream?: DataStream; } type TreeOptionDefaults = Required; @@ -351,19 +355,51 @@ export function getTreeOptionsWithDef(options?: TreeOptions): TreeOptionDefaults percentTerminated: options?.percentTerminated ?? 100, alwaysGenMaxChildrenPerNode: options?.alwaysGenMaxChildrenPerNode ?? false, ancestryArraySize: options?.ancestryArraySize ?? ANCESTRY_LIMIT, + eventsDataStream: options?.eventsDataStream ?? eventsDefaultDataStream, + alertsDataStream: options?.alertsDataStream ?? alertsDefaultDataStream, }; } +const metadataDefaultDataStream = { + type: 'metrics', + dataset: 'endpoint.metadata', + namespace: 'default', +}; + +const policyDefaultDataStream = { + type: 'metrics', + dataset: 'endpoint.policy', + namespace: 'default', +}; + +const eventsDefaultDataStream = { + type: 'logs', + dataset: 'endpoint.events.process', + namespace: 'default', +}; + +const alertsDefaultDataStream = { + type: 'logs', + dataset: 'endpoint.alerts', + namespace: 'default', +}; + export class EndpointDocGenerator { commonInfo: HostInfo; random: seedrandom.prng; sequence: number = 0; + /** + * The EndpointDocGenerator parameters + * + * @param seed either a string to seed the random number generator or a random number generator function + */ constructor(seed: string | seedrandom.prng = Math.random().toString()) { if (typeof seed === 'string') { this.random = seedrandom(seed); } else { this.random = seed; } + this.commonInfo = this.createHostData(); } @@ -383,6 +419,21 @@ export class EndpointDocGenerator { this.commonInfo.Endpoint.policy.applied.status = this.randomChoice(POLICY_RESPONSE_STATUSES); } + /** + * Parses an index and returns the data stream fields extracted from the index. + * + * @param index the index name to parse into the data stream parts + */ + public static createDataStreamFromIndex(index: string): DataStream { + // e.g. logs-endpoint.events.network-default + const parts = index.split('-'); + return { + type: parts[0], // logs + dataset: parts[1], // endpoint.events.network + namespace: parts[2], // default + }; + } + private createHostData(): HostInfo { const hostName = this.randomHostname(); return { @@ -417,8 +468,12 @@ export class EndpointDocGenerator { /** * Creates a host metadata document * @param ts - Timestamp to put in the event + * @param metadataDataStream the values to populate the data_stream fields when generating metadata documents */ - public generateHostMetadata(ts = new Date().getTime()): HostMetadata { + public generateHostMetadata( + ts = new Date().getTime(), + metadataDataStream = metadataDefaultDataStream + ): HostMetadata { return { '@timestamp': ts, event: { @@ -432,6 +487,7 @@ export class EndpointDocGenerator { dataset: 'endpoint.metadata', }, ...this.commonInfo, + data_stream: metadataDataStream, }; } @@ -441,15 +497,24 @@ export class EndpointDocGenerator { * @param entityID - entityID of the originating process * @param parentEntityID - optional entityID of the parent process, if it exists * @param ancestry - an array of ancestors for the generated alert + * @param alertsDataStream the values to populate the data_stream fields when generating alert documents */ - public generateAlert( + public generateAlert({ ts = new Date().getTime(), entityID = this.randomString(10), - parentEntityID?: string, - ancestry: string[] = [] - ): AlertEvent { + parentEntityID, + ancestry = [], + alertsDataStream = alertsDefaultDataStream, + }: { + ts?: number; + entityID?: string; + parentEntityID?: string; + ancestry?: string[]; + alertsDataStream?: DataStream; + } = {}): AlertEvent { return { ...this.commonInfo, + data_stream: alertsDataStream, '@timestamp': ts, ecs: { version: '1.4.0', @@ -598,6 +663,7 @@ export class EndpointDocGenerator { return {}; })(options.eventCategory); return { + data_stream: options?.eventsDataStream ?? eventsDefaultDataStream, '@timestamp': options.timestamp ? options.timestamp : new Date().getTime(), agent: { ...this.commonInfo.agent, type: 'endpoint' }, ecs: { @@ -813,6 +879,7 @@ export class EndpointDocGenerator { const startDate = new Date().getTime(); const root = this.generateEvent({ timestamp: startDate + 1000, + eventsDataStream: opts.eventsDataStream, }); events.push(root); let ancestor = root; @@ -824,18 +891,24 @@ export class EndpointDocGenerator { secBeforeAlert: number, eventList: Event[] ) => { - for (const relatedAlert of this.relatedAlertsGenerator(node, alertsPerNode, secBeforeAlert)) { + for (const relatedAlert of this.relatedAlertsGenerator({ + node, + relatedAlerts: alertsPerNode, + alertCreationTime: secBeforeAlert, + alertsDataStream: opts.alertsDataStream, + })) { eventList.push(relatedAlert); } }; const addRelatedEvents = (node: Event, secBeforeEvent: number, eventList: Event[]) => { - for (const relatedEvent of this.relatedEventsGenerator( + for (const relatedEvent of this.relatedEventsGenerator({ node, - opts.relatedEvents, - secBeforeEvent, - opts.relatedEventsOrdered - )) { + relatedEvents: opts.relatedEvents, + processDuration: secBeforeEvent, + ordered: opts.relatedEventsOrdered, + eventsDataStream: opts.eventsDataStream, + })) { eventList.push(relatedEvent); } }; @@ -857,6 +930,7 @@ export class EndpointDocGenerator { parentEntityID: parentEntityIDSafeVersion(root), eventCategory: ['process'], eventType: ['end'], + eventsDataStream: opts.eventsDataStream, }) ); } @@ -877,6 +951,7 @@ export class EndpointDocGenerator { ancestryArrayLimit: opts.ancestryArraySize, parentPid: firstNonNullValue(ancestor.process?.pid), pid: this.randomN(5000), + eventsDataStream: opts.eventsDataStream, }); events.push(ancestor); timestamp = timestamp + 1000; @@ -892,6 +967,7 @@ export class EndpointDocGenerator { eventType: ['end'], ancestry: ancestryArray(ancestor), ancestryArrayLimit: opts.ancestryArraySize, + eventsDataStream: opts.eventsDataStream, }) ); } @@ -912,12 +988,13 @@ export class EndpointDocGenerator { timestamp = timestamp + 1000; events.push( - this.generateAlert( - timestamp, - entityIDSafeVersion(ancestor), - parentEntityIDSafeVersion(ancestor), - ancestryArray(ancestor) - ) + this.generateAlert({ + ts: timestamp, + entityID: entityIDSafeVersion(ancestor), + parentEntityID: parentEntityIDSafeVersion(ancestor), + ancestry: ancestryArray(ancestor), + alertsDataStream: opts.alertsDataStream, + }) ); return events; } @@ -973,6 +1050,7 @@ export class EndpointDocGenerator { parentEntityID: currentStateEntityID, ancestry, ancestryArrayLimit: opts.ancestryArraySize, + eventsDataStream: opts.eventsDataStream, }); maxChildren = this.randomN(opts.children + 1); @@ -996,16 +1074,23 @@ export class EndpointDocGenerator { eventType: ['end'], ancestry, ancestryArrayLimit: opts.ancestryArraySize, + eventsDataStream: opts.eventsDataStream, }); } if (this.randomN(100) < opts.percentWithRelated) { - yield* this.relatedEventsGenerator( - child, - opts.relatedEvents, + yield* this.relatedEventsGenerator({ + node: child, + relatedEvents: opts.relatedEvents, processDuration, - opts.relatedEventsOrdered - ); - yield* this.relatedAlertsGenerator(child, opts.relatedAlerts, processDuration); + ordered: opts.relatedEventsOrdered, + eventsDataStream: opts.eventsDataStream, + }); + yield* this.relatedAlertsGenerator({ + node: child, + relatedAlerts: opts.relatedAlerts, + alertCreationTime: processDuration, + alertsDataStream: opts.alertsDataStream, + }); } } } @@ -1019,12 +1104,19 @@ export class EndpointDocGenerator { * @param ordered - if true the events will have an increasing timestamp, otherwise their timestamp will be random but * guaranteed to be greater than or equal to the originating event */ - public *relatedEventsGenerator( - node: Event, - relatedEvents: RelatedEventInfo[] | number = 10, - processDuration: number = 6 * 3600, - ordered: boolean = false - ) { + public *relatedEventsGenerator({ + node, + relatedEvents = 10, + processDuration = 6 * 3600, + ordered = false, + eventsDataStream = eventsDefaultDataStream, + }: { + node: Event; + relatedEvents?: RelatedEventInfo[] | number; + processDuration?: number; + ordered?: boolean; + eventsDataStream?: DataStream; + }) { let relatedEventsInfo: RelatedEventInfo[]; const nodeTimestamp = timestampSafeVersion(node) ?? 0; let ts = nodeTimestamp + 1; @@ -1056,6 +1148,7 @@ export class EndpointDocGenerator { eventCategory: eventInfo.category, eventType: eventInfo.creationType, ancestry: ancestryArray(node), + eventsDataStream, }); } } @@ -1067,19 +1160,26 @@ export class EndpointDocGenerator { * @param relatedAlerts - number which defines the number of related alerts to create * @param alertCreationTime - maximum number of seconds after process event that related alert timestamp can be */ - public *relatedAlertsGenerator( - node: Event, - relatedAlerts: number = 3, - alertCreationTime: number = 6 * 3600 - ) { + public *relatedAlertsGenerator({ + node, + relatedAlerts = 3, + alertCreationTime = 6 * 3600, + alertsDataStream = alertsDefaultDataStream, + }: { + node: Event; + relatedAlerts: number; + alertCreationTime: number; + alertsDataStream: DataStream; + }) { for (let i = 0; i < relatedAlerts; i++) { const ts = (timestampSafeVersion(node) ?? 0) + this.randomN(alertCreationTime) * 1000; - yield this.generateAlert( + yield this.generateAlert({ ts, - entityIDSafeVersion(node), - parentEntityIDSafeVersion(node), - ancestryArray(node) - ); + entityID: entityIDSafeVersion(node), + parentEntityID: parentEntityIDSafeVersion(node), + ancestry: ancestryArray(node), + alertsDataStream, + }); } } @@ -1227,15 +1327,21 @@ export class EndpointDocGenerator { /** * Generates a Host Policy response message */ - public generatePolicyResponse( + public generatePolicyResponse({ ts = new Date().getTime(), - allStatus?: HostPolicyResponseActionStatus - ): HostPolicyResponse { + allStatus, + policyDataStream = policyDefaultDataStream, + }: { + ts?: number; + allStatus?: HostPolicyResponseActionStatus; + policyDataStream?: DataStream; + } = {}): HostPolicyResponse { const policyVersion = this.seededUUIDv4(); const status = () => { return allStatus || this.randomHostPolicyResponseActionStatus(); }; return { + data_stream: policyDataStream, '@timestamp': ts, agent: { id: this.commonInfo.agent.id, diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index bf3d12f231c8..c0c70f9ca11a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -52,10 +52,9 @@ export async function indexHostsAndAlerts( const epmEndpointPackage = await getEndpointPackageInfo(kbnClient); // Keep a map of host applied policy ids (fake) to real ingest package configs (policy record) const realPolicies: Record = {}; - for (let i = 0; i < numHosts; i++) { const generator = new EndpointDocGenerator(random); - await indexHostDocs( + await indexHostDocs({ numDocs, client, kbnClient, @@ -63,10 +62,17 @@ export async function indexHostsAndAlerts( epmEndpointPackage, metadataIndex, policyResponseIndex, - fleet, - generator - ); - await indexAlerts(client, eventIndex, alertIndex, generator, alertsPerHost, options); + enrollFleet: fleet, + generator, + }); + await indexAlerts({ + client, + eventIndex, + alertIndex, + generator, + numAlerts: alertsPerHost, + options, + }); } await client.indices.refresh({ index: eventIndex, @@ -81,17 +87,27 @@ function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } -async function indexHostDocs( - numDocs: number, - client: Client, - kbnClient: KbnClientWithApiKeySupport, - realPolicies: Record, - epmEndpointPackage: GetPackagesResponse['response'][0], - metadataIndex: string, - policyResponseIndex: string, - enrollFleet: boolean, - generator: EndpointDocGenerator -) { +async function indexHostDocs({ + numDocs, + client, + kbnClient, + realPolicies, + epmEndpointPackage, + metadataIndex, + policyResponseIndex, + enrollFleet, + generator, +}: { + numDocs: number; + client: Client; + kbnClient: KbnClientWithApiKeySupport; + realPolicies: Record; + epmEndpointPackage: GetPackagesResponse['response'][0]; + metadataIndex: string; + policyResponseIndex: string; + enrollFleet: boolean; + generator: EndpointDocGenerator; +}) { const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents const timestamp = new Date().getTime(); let hostMetadata: HostMetadata; @@ -102,7 +118,10 @@ async function indexHostDocs( generator.updateHostData(); generator.updateHostPolicyData(); - hostMetadata = generator.generateHostMetadata(timestamp - timeBetweenDocs * (numDocs - j - 1)); + hostMetadata = generator.generateHostMetadata( + timestamp - timeBetweenDocs * (numDocs - j - 1), + EndpointDocGenerator.createDataStreamFromIndex(metadataIndex) + ); if (enrollFleet) { const { id: appliedPolicyId, name: appliedPolicyName } = hostMetadata.Endpoint.policy.applied; @@ -156,20 +175,30 @@ async function indexHostDocs( }); await client.index({ index: policyResponseIndex, - body: generator.generatePolicyResponse(timestamp - timeBetweenDocs * (numDocs - j - 1)), + body: generator.generatePolicyResponse({ + ts: timestamp - timeBetweenDocs * (numDocs - j - 1), + policyDataStream: EndpointDocGenerator.createDataStreamFromIndex(policyResponseIndex), + }), op_type: 'create', }); } } -async function indexAlerts( - client: Client, - eventIndex: string, - alertIndex: string, - generator: EndpointDocGenerator, - numAlerts: number, - options: TreeOptions = {} -) { +async function indexAlerts({ + client, + eventIndex, + alertIndex, + generator, + numAlerts, + options = {}, +}: { + client: Client; + eventIndex: string; + alertIndex: string; + generator: EndpointDocGenerator; + numAlerts: number; + options: TreeOptions; +}) { const alertGenerator = generator.alertsGenerator(numAlerts, options); let result = alertGenerator.next(); while (!result.done) { diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts b/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts index 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/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 510f1833b793..f2033e064ef7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -300,6 +300,15 @@ export interface HostResultList { query_strategy_version: MetadataQueryStrategyVersions; } +/** + * The data_stream fields in an elasticsearch document. + */ +export interface DataStream { + dataset: string; + namespace: string; + type: string; +} + /** * Operating System metadata. */ @@ -556,6 +565,7 @@ export type HostMetadata = Immutable<{ version: string; }; host: Host; + data_stream: DataStream; }>; export interface LegacyEndpointEvent { @@ -675,6 +685,11 @@ export type SafeEndpointEvent = Partial<{ version: ECSField; type: ECSField; }>; + data_stream: Partial<{ + type: ECSField; + dataset: ECSField; + namespace: ECSField; + }>; ecs: Partial<{ version: ECSField; }>; @@ -1002,6 +1017,7 @@ interface HostPolicyResponseAppliedArtifact { */ export interface HostPolicyResponse { '@timestamp': number; + data_stream: DataStream; elastic: { agent: { id: string; diff --git a/x-pack/plugins/security_solution/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..41665cf6d20a 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'); @@ -168,7 +168,7 @@ describe('Custom detection rules creation', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${newRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${newRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', newRule.severity); @@ -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('')); } @@ -328,7 +328,7 @@ describe('Custom detection rules deletion and edition', () => { fillAboutRule(editedRule); saveEditedRule(); - cy.get(RULE_NAME_HEADER).should('have.text', `${editedRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${editedRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', editedRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', editedRule.severity); @@ -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..5502f35d6f0f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts @@ -131,7 +131,7 @@ describe.skip('Detection rules, EQL', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${eqlRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${eqlRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', eqlRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', eqlRule.severity); @@ -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_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts index 49ec6381cbc8..0f34e7d71e5f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts @@ -115,7 +115,7 @@ describe('Detection rules, machine learning', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${machineLearningRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${machineLearningRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', machineLearningRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', machineLearningRule.severity); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts index 090012de7253..edf7305f6916 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts @@ -132,7 +132,7 @@ describe('Detection rules, override', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${newOverrideRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${newOverrideRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newOverrideRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', newOverrideRule.severity); @@ -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..5095e856e3f6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -129,7 +129,7 @@ describe('Detection rules, threshold', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${newThresholdRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${newThresholdRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThresholdRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', newThresholdRule.severity); @@ -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/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts index a45b1fd18a4b..ec3887ad7262 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts @@ -60,7 +60,7 @@ describe('Cases', () => { createNewCaseWithTimeline(case1); backToCases(); - cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases Beta'); + cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases'); cy.get(ALL_CASES_OPEN_CASES_STATS).should('have.text', 'Open cases1'); cy.get(ALL_CASES_CLOSED_CASES_STATS).should('have.text', 'Closed cases0'); cy.get(ALL_CASES_OPEN_CASES_COUNT).should('have.text', 'Open cases (1)'); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index 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_template_creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_template_creation.spec.ts index 91255d6110d5..377b2100b36c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_template_creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_template_creation.spec.ts @@ -12,8 +12,8 @@ import { NOTES_BUTTON, NOTES_COUNT, NOTES_TEXT_AREA, + PIN_EVENT, TIMELINE_DESCRIPTION, - // TIMELINE_FILTER, TIMELINE_QUERY, TIMELINE_TITLE, } from '../screens/timeline'; @@ -35,7 +35,7 @@ import { closeTimeline, createNewTimelineTemplate, markAsFavorite, - openTimelineFromSettings, + openTimelineTemplateFromSettings, populateTimeline, waitForTimelineChanges, } from '../tasks/timeline'; @@ -43,8 +43,7 @@ import { openTimeline } from '../tasks/timelines'; import { OVERVIEW_URL } from '../urls/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/79967 -describe.skip('Timeline Templates', () => { +describe('Timeline Templates', () => { before(() => { cy.server(); cy.route('PATCH', '**/api/timeline').as('timeline'); @@ -56,12 +55,11 @@ describe.skip('Timeline Templates', () => { createNewTimelineTemplate(); populateTimeline(); addFilter(timeline.filter); - // To fix - // cy.get(PIN_EVENT).should( - // 'have.attr', - // 'aria-label', - // 'This event may not be pinned while editing a template timeline' - // ); + cy.get(PIN_EVENT).should( + 'have.attr', + 'aria-label', + 'This event may not be pinned while editing a template timeline' + ); cy.get(LOCKED_ICON).should('be.visible'); addNameToTimeline(timeline.title); @@ -77,7 +75,7 @@ describe.skip('Timeline Templates', () => { waitForTimelineChanges(); createNewTimelineTemplate(); closeTimeline(); - openTimelineFromSettings(); + openTimelineTemplateFromSettings(timelineId); cy.contains(timeline.title).should('exist'); cy.get(TIMELINES_DESCRIPTION).first().should('have.text', timeline.description); diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index 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/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index e397dd9b5a41..98e6502ffe94 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -58,6 +58,9 @@ export const NOTES_COUNT = '[data-test-subj="timeline-notes-count"]'; export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; +export const OPEN_TIMELINE_TEMPLATE_ICON = + '[data-test-subj="open-timeline-modal-body-filter-template"]'; + export const PIN_EVENT = '[data-test-subj="pin"]'; export const PROVIDER_BADGE = '[data-test-subj="providerBadge"]'; @@ -98,6 +101,8 @@ export const TIMELINE_FILTER = (filter: TimelineFilter) => { export const TIMELINE_FILTER_FIELD = '[data-test-subj="filterFieldSuggestionList"]'; +export const TIMELINE_TITLE_BY_ID = (id: string) => `[data-test-subj="title-${id}"]`; + export const TIMELINE_FILTER_OPERATOR = '[data-test-subj="filterOperatorList"]'; export const TIMELINE_FILTER_VALUE = diff --git a/x-pack/plugins/security_solution/cypress/tasks/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/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 7c9c95427a4d..b10179338548 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -41,9 +41,11 @@ import { TIMELINE_INSPECT_BUTTON, TIMELINE_SETTINGS_ICON, TIMELINE_TITLE, + TIMELINE_TITLE_BY_ID, TIMESTAMP_TOGGLE_FIELD, TOGGLE_TIMELINE_EXPAND_EVENT, CREATE_NEW_TIMELINE_TEMPLATE, + OPEN_TIMELINE_TEMPLATE_ICON, } from '../screens/timeline'; import { TIMELINES_TABLE } from '../screens/timelines'; @@ -69,8 +71,7 @@ export const addNotesToTimeline = (notes: string) => { export const addFilter = (filter: TimelineFilter) => { cy.get(ADD_FILTER).click(); - cy.get(TIMELINE_FILTER_FIELD).type(filter.field); - cy.get(COMBO_BOX).contains(filter.field).click(); + cy.get(TIMELINE_FILTER_FIELD).type(`${filter.field}{downarrow}{enter}`); cy.get(TIMELINE_FILTER_OPERATOR).type(filter.operator); cy.get(COMBO_BOX).contains(filter.operator).click(); if (filter.operator !== 'exists') { @@ -146,6 +147,12 @@ export const openTimelineFromSettings = () => { cy.get(OPEN_TIMELINE_ICON).click({ force: true }); }; +export const openTimelineTemplateFromSettings = (id: string) => { + openTimelineFromSettings(); + cy.get(OPEN_TIMELINE_TEMPLATE_ICON).click({ force: true }); + cy.get(TIMELINE_TITLE_BY_ID(id)).click({ force: true }); +}; + export const openTimelineSettings = () => { cy.get(TIMELINE_SETTINGS_ICON).trigger('click', { force: true }); }; diff --git a/x-pack/plugins/security_solution/public/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/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index 14c42697dcbb..3e3d21b9926d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -12,7 +12,6 @@ import { CommentRequest } from '../../../../../case/common/api'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; -import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover'; import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { Form, useForm, UseField, useFormData } from '../../../shared_imports'; @@ -61,10 +60,7 @@ export const AddComment = React.memo( setFieldValue, ]); - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - comment, - onCommentChange - ); + const { handleCursorChange } = useInsertTimeline(comment, onCommentChange); const addQuote = useCallback( (quote) => { @@ -116,13 +112,6 @@ export const AddComment = React.memo( {i18n.ADD_COMMENT} ), - topRightContent: ( - - ), }} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx index 4f7b17a730b6..5e4db16d6d9c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx @@ -7,18 +7,9 @@ import React from 'react'; import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page'; -import * as i18n from './translations'; const CaseHeaderPageComponent: React.FC = (props) => ( ); -CaseHeaderPageComponent.defaultProps = { - badgeOptions: { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, - }, -}; - export const CaseHeaderPage = React.memo(CaseHeaderPageComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts b/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts deleted file mode 100644 index 8cdc287b1584..000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const PAGE_BADGE_LABEL = i18n.translate( - 'xpack.securitySolution.case.caseView.pageBadgeLabel', - { - defaultMessage: 'Beta', - } -); - -export const PAGE_BADGE_TOOLTIP = i18n.translate( - 'xpack.securitySolution.case.caseView.pageBadgeTooltip', - { - defaultMessage: - 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', - } -); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index b7a80bcf6633..42633c5d2ccf 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -28,7 +28,6 @@ import { } from '../../../shared_imports'; import { usePostCase } from '../../containers/use_post_case'; import { schema, FormProps } from './schema'; -import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover'; import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; import { useGetTags } from '../../containers/use_get_tags'; @@ -136,10 +135,7 @@ export const Create = React.memo(() => { setFieldValue, ]); - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - description, - onDescriptionChange - ); + const { handleCursorChange } = useInsertTimeline(description, onDescriptionChange); const handleTimelineClick = useTimelineClick(); @@ -221,20 +217,13 @@ export const Create = React.memo(() => { isDisabled: isLoading, onClickTimeline: handleTimelineClick, onCursorPositionUpdate: handleCursorChange, - topRightContent: ( - - ), }} /> ), }), - [isLoading, options, handleCursorChange, handleTimelineClick, handleOnTimelineChange] + [isLoading, options, handleCursorChange, handleTimelineClick] ); const secondStep = useMemo( diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts b/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts index 54c46f064aa7..05a05ac6dd58 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts @@ -36,14 +36,14 @@ export const GET_ISSUE_API_ERROR = (id: string) => export const SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL = i18n.translate( 'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); export const SEARCH_ISSUES_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap index 61e9dd04d910..801443119217 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap @@ -1,22 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`item_details_card ItemDetailsAction should render correctly 1`] = ` - - + - - primary - - - + primary + + `; exports[`item_details_card ItemDetailsCard should render correctly with actions 1`] = ` @@ -24,103 +23,98 @@ exports[`item_details_card ItemDetailsCard should render correctly with actions paddingSize="none" > - + + + + + + + + - - - - - - - - + some text + + some node + +
+
+ + + primary + + + - some text - - some node - + secondary + - - - - primary - - - - - secondary - - - - - danger - - - + danger + - +
- + `; @@ -130,66 +124,61 @@ exports[`item_details_card ItemDetailsCard should render correctly with no actio paddingSize="none" > - + + + + + + + + - - - - - - - - + some text + + some node + +
+ + - - some text - - some node - - - - - - - + gutterSize="s" + justifyContent="flexEnd" + /> + - + `; @@ -200,11 +189,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.stories.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx index e9d1825658be..74f31a623969 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx @@ -21,7 +21,7 @@ storiesOf('Components/ItemDetailsCard', module).add('default', () => { - {'content text'} + {'content text '} {'content node'} diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx index 9105514b7580..37003961d67d 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} ) ); @@ -84,11 +81,17 @@ ItemDetailsPropertySummary.displayName = 'ItemPropertySummary'; export const ItemDetailsAction: FC> = memo( ({ children, ...rest }) => ( - - - {children} - - + <> + + {children} + + ) ); @@ -102,32 +105,30 @@ export const ItemDetailsCard: FC = memo(({ children }) => { return ( - - - - - - {childElements.get(ItemDetailsPropertySummary)} - - - - - {childElements.get(OTHER_NODES)} - {childElements.has(ItemDetailsAction) && ( - - - {childElements.get(ItemDetailsAction)?.map((action, index) => ( - - {action} - - ))} - - - )} - - + + + + {childElements.get(ItemDetailsPropertySummary)} + + + + + +
{childElements.get(OTHER_NODES)}
+
+ {childElements.has(ItemDetailsAction) && ( + + + {childElements.get(ItemDetailsAction)?.map((action, index) => ( + + {action} + + ))} + + + )}
-
+
); diff --git a/x-pack/plugins/security_solution/public/common/components/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/common/store/sourcerer/model.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts index 93f7ff95dfb0..18aa4e65a03c 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts @@ -56,7 +56,7 @@ export const initSourcererScope = { errorMessage: null, indexPattern: EMPTY_INDEX_PATTERN, indicesExist: true, - loading: true, + loading: false, selectedPatterns: [], }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_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/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 47da1e93cf00..bfc104b10523 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -21,6 +21,7 @@ import { CreateTimeline, UpdateTimelineLoading } from './types'; import { Ecs } from '../../../../common/ecs'; import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import { ISearchStart } from '../../../../../../../src/plugins/data/public'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; jest.mock('apollo-client'); @@ -29,7 +30,7 @@ describe('alert actions', () => { const unix = moment(anchor).valueOf(); let createTimeline: CreateTimeline; let updateTimelineIsLoading: UpdateTimelineLoading; - let searchStrategyClient: ISearchStart; + let searchStrategyClient: jest.Mocked; let clock: sinon.SinonFakeTimers; beforeEach(() => { @@ -42,11 +43,13 @@ describe('alert actions', () => { createTimeline = jest.fn() as jest.Mocked; updateTimelineIsLoading = jest.fn() as jest.Mocked; + searchStrategyClient = { aggs: {} as ISearchStart['aggs'], showError: jest.fn(), search: jest.fn().mockResolvedValue({ data: mockTimelineDetails }), searchSource: {} as ISearchStart['searchSource'], + session: dataPluginMock.createStartContract().search.session, }; jest.spyOn(apolloClient, 'query').mockImplementation((obj) => { diff --git a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx index 1a2deb059ad4..293ed4d488c7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx @@ -7,20 +7,11 @@ import React from 'react'; import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page'; -import * as i18n from './translations'; const DetectionEngineHeaderPageComponent: React.FC = (props) => ( ); -DetectionEngineHeaderPageComponent.defaultProps = { - badgeOptions: { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, - }, -}; - export const DetectionEngineHeaderPage = React.memo(DetectionEngineHeaderPageComponent); DetectionEngineHeaderPage.displayName = 'DetectionEngineHeaderPage'; diff --git a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts deleted file mode 100644 index f59be1692380..000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const PAGE_BADGE_LABEL = i18n.translate( - 'xpack.securitySolution.detectionEngine.headerPage.pageBadgeLabel', - { - defaultMessage: 'Beta', - } -); - -export const PAGE_BADGE_TOOLTIP = i18n.translate( - 'xpack.securitySolution.detectionEngine.headerPage.pageBadgeTooltip', - { - defaultMessage: - 'Alerts is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/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/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx index a41da908085b..75ab1524c5c0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx @@ -5,10 +5,12 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { waitFor } from '@testing-library/react'; +import { shallow, mount, ReactWrapper } from 'enzyme'; import '../../../../common/mock/match_media'; import { PrePackagedRulesPrompt } from './load_empty_prompt'; +import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -23,16 +25,94 @@ jest.mock('react-router-dom', () => { jest.mock('../../../../common/components/link_to'); +jest.mock('../../../containers/detection_engine/rules/api', () => ({ + getPrePackagedRulesStatus: jest.fn().mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }), + createPrepackagedRules: jest.fn(), +})); + +const props = { + createPrePackagedRules: jest.fn(), + loading: false, + userHasNoPermissions: false, + 'data-test-subj': 'load-prebuilt-rules', +}; + describe('PrePackagedRulesPrompt', () => { it('renders correctly', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('EmptyPrompt')).toHaveLength(1); }); }); + +describe('LoadPrebuiltRulesAndTemplatesButton', () => { + it('renders correct button with correct text - Load Elastic prebuilt rules and timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount(); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( + 'Load Elastic prebuilt rules and timeline templates' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt rules', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount(); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( + 'Load Elastic prebuilt rules' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount(); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( + 'Load Elastic prebuilt timeline templates' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx index 99968cd4d9fe..64b3cfa3aa99 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import React, { memo, useCallback } from 'react'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { memo, useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; @@ -14,6 +14,8 @@ import * as i18n from './translations'; import { LinkButton } from '../../../../common/components/links'; import { SecurityPageName } from '../../../../app/types'; import { useFormatUrl } from '../../../../common/components/link_to'; +import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; +import { useUserData } from '../../user_info'; const EmptyPrompt = styled(EuiEmptyPrompt)` align-self: center; /* Corrects horizontal centering in IE11 */ @@ -46,24 +48,36 @@ const PrePackagedRulesPromptComponent: React.FC = ( [history] ); + const [ + { isSignalIndexExists, isAuthenticated, hasEncryptionKey, canUserCRUD, hasIndexWrite }, + ] = useUserData(); + + const { getLoadPrebuiltRulesAndTemplatesButton } = usePrePackagedRules({ + canUserCRUD, + hasIndexWrite, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + }); + + const loadPrebuiltRulesAndTemplatesButton = useMemo( + () => + getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: userHasNoPermissions, + onClick: handlePreBuiltCreation, + fill: true, + 'data-test-subj': 'load-prebuilt-rules', + }), + [getLoadPrebuiltRulesAndTemplatesButton, handlePreBuiltCreation, userHasNoPermissions] + ); + return ( {i18n.PRE_BUILT_TITLE}} body={

{i18n.PRE_BUILT_MSG}

} actions={ - - - {i18n.PRE_BUILT_ACTION} - - + {loadPrebuiltRulesAndTemplatesButton} 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/detections/containers/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts index f878b40b99dc..721790a36b27 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts @@ -40,3 +40,57 @@ export const TAG_FETCH_FAILURE = i18n.translate( defaultMessage: 'Failed to fetch Tags', } ); + +export const LOAD_PREPACKAGED_RULES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton', + { + defaultMessage: 'Load Elastic prebuilt rules', + } +); + +export const LOAD_PREPACKAGED_TIMELINE_TEMPLATES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.loadPrePackagedTimelineTemplatesButton', + { + defaultMessage: 'Load Elastic prebuilt timeline templates', + } +); + +export const LOAD_PREPACKAGED_RULES_AND_TEMPLATES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesAndTemplatesButton', + { + defaultMessage: 'Load Elastic prebuilt rules and timeline templates', + } +); + +export const RELOAD_MISSING_PREPACKAGED_RULES = (missingRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton', + { + values: { missingRules }, + defaultMessage: + 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} ', + } + ); + +export const RELOAD_MISSING_PREPACKAGED_TIMELINES = (missingTimelines: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton', + { + values: { missingTimelines }, + defaultMessage: + 'Install {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', + } + ); + +export const RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES = ( + missingRules: number, + missingTimelines: number +) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton', + { + values: { missingRules, missingTimelines }, + defaultMessage: + 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} and {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', + } + ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 92d46a785b03..7f74e9258449 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -32,6 +32,10 @@ describe('usePrePackagedRules', () => { await waitForNextUpdate(); expect(result.current).toEqual({ + getLoadPrebuiltRulesAndTemplatesButton: + result.current.getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton: + result.current.getReloadPrebuiltRulesAndTemplatesButton, createPrePackagedRules: null, loading: true, loadingCreatePrePackagedRules: false, @@ -63,6 +67,10 @@ describe('usePrePackagedRules', () => { await waitForNextUpdate(); expect(result.current).toEqual({ + getLoadPrebuiltRulesAndTemplatesButton: + result.current.getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton: + result.current.getReloadPrebuiltRulesAndTemplatesButton, createPrePackagedRules: result.current.createPrePackagedRules, loading: false, loadingCreatePrePackagedRules: false, @@ -100,6 +108,10 @@ describe('usePrePackagedRules', () => { expect(resp).toEqual(true); expect(spyOnCreatePrepackagedRules).toHaveBeenCalled(); expect(result.current).toEqual({ + getLoadPrebuiltRulesAndTemplatesButton: + result.current.getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton: + result.current.getReloadPrebuiltRulesAndTemplatesButton, createPrePackagedRules: result.current.createPrePackagedRules, loading: false, loadingCreatePrePackagedRules: false, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx index d82d97883a3d..4d19f44bcfc8 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; +import { EuiButton } from '@elastic/eui'; import { errorToToaster, @@ -14,6 +15,11 @@ import { import { getPrePackagedRulesStatus, createPrepackagedRules } from './api'; import * as i18n from './translations'; +import { + getPrePackagedRuleStatus, + getPrePackagedTimelineStatus, +} from '../../../pages/detection_engine/rules/helpers'; + type Func = () => void; export type CreatePreBuiltRules = () => Promise; @@ -23,6 +29,23 @@ interface ReturnPrePackagedTimelines { timelinesNotUpdated: number | null; } +type GetLoadPrebuiltRulesAndTemplatesButton = (args: { + isDisabled: boolean; + onClick: () => void; + fill?: boolean; + 'data-test-subj'?: string; +}) => React.ReactNode | null; + +type GetReloadPrebuiltRulesAndTemplatesButton = ({ + isDisabled, + onClick, + fill, +}: { + isDisabled: boolean; + onClick: () => void; + fill?: boolean; +}) => React.ReactNode | null; + interface ReturnPrePackagedRules { createPrePackagedRules: null | CreatePreBuiltRules; loading: boolean; @@ -32,6 +55,8 @@ interface ReturnPrePackagedRules { rulesInstalled: number | null; rulesNotInstalled: number | null; rulesNotUpdated: number | null; + getLoadPrebuiltRulesAndTemplatesButton: GetLoadPrebuiltRulesAndTemplatesButton; + getReloadPrebuiltRulesAndTemplatesButton: GetReloadPrebuiltRulesAndTemplatesButton; } export type ReturnPrePackagedRulesAndTimelines = ReturnPrePackagedRules & @@ -89,7 +114,6 @@ export const usePrePackagedRules = ({ const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); - useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); @@ -100,7 +124,6 @@ export const usePrePackagedRules = ({ const prePackagedRuleStatusResponse = await getPrePackagedRulesStatus({ signal: abortCtrl.signal, }); - if (isSubscribed) { setPrepackagedDataStatus({ createPrePackagedRules: createElasticRules, @@ -225,9 +248,108 @@ export const usePrePackagedRules = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [canUserCRUD, hasIndexWrite, isAuthenticated, hasEncryptionKey, isSignalIndexExists]); + const prePackagedRuleStatus = useMemo( + () => + getPrePackagedRuleStatus( + prepackagedDataStatus.rulesInstalled, + prepackagedDataStatus.rulesNotInstalled, + prepackagedDataStatus.rulesNotUpdated + ), + [ + prepackagedDataStatus.rulesInstalled, + prepackagedDataStatus.rulesNotInstalled, + prepackagedDataStatus.rulesNotUpdated, + ] + ); + + const prePackagedTimelineStatus = useMemo( + () => + getPrePackagedTimelineStatus( + prepackagedDataStatus.timelinesInstalled, + prepackagedDataStatus.timelinesNotInstalled, + prepackagedDataStatus.timelinesNotUpdated + ), + [ + prepackagedDataStatus.timelinesInstalled, + prepackagedDataStatus.timelinesNotInstalled, + prepackagedDataStatus.timelinesNotUpdated, + ] + ); + const getLoadPrebuiltRulesAndTemplatesButton = useCallback( + ({ isDisabled, onClick, fill, 'data-test-subj': dataTestSubj = 'loadPrebuiltRulesBtn' }) => { + return prePackagedRuleStatus === 'ruleNotInstalled' || + prePackagedTimelineStatus === 'timelinesNotInstalled' ? ( + + {prePackagedRuleStatus === 'ruleNotInstalled' && + prePackagedTimelineStatus === 'timelinesNotInstalled' && + i18n.LOAD_PREPACKAGED_RULES_AND_TEMPLATES} + + {prePackagedRuleStatus === 'ruleNotInstalled' && + prePackagedTimelineStatus !== 'timelinesNotInstalled' && + i18n.LOAD_PREPACKAGED_RULES} + + {prePackagedRuleStatus !== 'ruleNotInstalled' && + prePackagedTimelineStatus === 'timelinesNotInstalled' && + i18n.LOAD_PREPACKAGED_TIMELINE_TEMPLATES} + + ) : null; + }, + [loadingCreatePrePackagedRules, prePackagedRuleStatus, prePackagedTimelineStatus] + ); + + const getMissingRulesOrTimelinesButtonTitle = useCallback( + (missingRules: number, missingTimelines: number) => { + if (missingRules > 0 && missingTimelines === 0) + return i18n.RELOAD_MISSING_PREPACKAGED_RULES(missingRules); + else if (missingRules === 0 && missingTimelines > 0) + return i18n.RELOAD_MISSING_PREPACKAGED_TIMELINES(missingTimelines); + else if (missingRules > 0 && missingTimelines > 0) + return i18n.RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES(missingRules, missingTimelines); + }, + [] + ); + + const getReloadPrebuiltRulesAndTemplatesButton = useCallback( + ({ isDisabled, onClick, fill = false }) => { + return prePackagedRuleStatus === 'someRuleUninstall' || + prePackagedTimelineStatus === 'someTimelineUninstall' ? ( + + {getMissingRulesOrTimelinesButtonTitle( + prepackagedDataStatus.rulesNotInstalled ?? 0, + prepackagedDataStatus.timelinesNotInstalled ?? 0 + )} + + ) : null; + }, + [ + getMissingRulesOrTimelinesButtonTitle, + loadingCreatePrePackagedRules, + prePackagedRuleStatus, + prePackagedTimelineStatus, + prepackagedDataStatus.rulesNotInstalled, + prepackagedDataStatus.timelinesNotInstalled, + ] + ); + return { loading, loadingCreatePrePackagedRules, ...prepackagedDataStatus, + getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 886a24dd7cbe..58e61c5b4748 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -5,13 +5,14 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount, ReactWrapper } from 'enzyme'; import '../../../../common/mock/match_media'; import { RulesPage } from './index'; import { useUserData } from '../../../components/user_info'; -import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; - +import { waitFor } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -26,16 +27,164 @@ jest.mock('react-router-dom', () => { jest.mock('../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../common/components/link_to'); jest.mock('../../../components/user_info'); -jest.mock('../../../containers/detection_engine/rules'); +jest.mock('../../../../common/components/toasters', () => { + const actual = jest.requireActual('../../../../common/components/toasters'); + return { + ...actual, + errorToToaster: jest.fn(), + useStateToaster: jest.fn().mockReturnValue([jest.fn(), jest.fn()]), + displaySuccessToast: jest.fn(), + }; +}); + +jest.mock('../../../containers/detection_engine/rules/api', () => ({ + getPrePackagedRulesStatus: jest.fn().mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }), + createPrepackagedRules: jest.fn(), +})); + +jest.mock('../../../../common/lib/kibana', () => { + return { + useToast: jest.fn(), + useHttp: jest.fn(), + }; +}); + +jest.mock('../../../components/value_lists_management_modal', () => { + return { + ValueListsModal: jest.fn().mockReturnValue(
), + }; +}); + +jest.mock('./all', () => { + return { + AllRules: jest.fn().mockReturnValue(
), + }; +}); + +jest.mock('../../../../common/utils/route/spy_routes', () => { + return { + SpyRoute: jest.fn().mockReturnValue(
), + }; +}); + +jest.mock('../../../components/rules/pre_packaged_rules/update_callout', () => { + return { + UpdatePrePackagedRulesCallOut: jest.fn().mockReturnValue(
), + }; +}); describe('RulesPage', () => { beforeAll(() => { (useUserData as jest.Mock).mockReturnValue([{}]); - (usePrePackagedRules as jest.Mock).mockReturnValue({}); }); - it('renders correctly', () => { + + it('renders AllRules', () => { const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="all-rules"]').exists()).toEqual(true); + }); + + it('renders correct button with correct text - Load Elastic prebuilt rules and timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual( + 'Load Elastic prebuilt rules and timeline templates' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt rules', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); + + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual( + 'Load Elastic prebuilt rules' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual( + 'Load Elastic prebuilt timeline templates' + ); + }); + }); + + it('renders a callout - Update Elastic prebuilt rules', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 2, + rules_installed: 1, + rules_not_updated: 1, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); - expect(wrapper.find('AllRules')).toHaveLength(1); + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="update-callout-button"]').exists()).toEqual(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 53c82569f94a..8c7cb6a0d928 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules'; @@ -70,6 +70,8 @@ const RulesPageComponent: React.FC = () => { timelinesInstalled, timelinesNotInstalled, timelinesNotUpdated, + getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton, } = usePrePackagedRules({ canUserCRUD, hasIndexWrite, @@ -113,18 +115,6 @@ const RulesPageComponent: React.FC = () => { refreshRulesData.current = refreshRule; }, []); - const getMissingRulesOrTimelinesButtonTitle = useCallback( - (missingRules: number, missingTimelines: number) => { - if (missingRules > 0 && missingTimelines === 0) - return i18n.RELOAD_MISSING_PREPACKAGED_RULES(missingRules); - else if (missingRules === 0 && missingTimelines > 0) - return i18n.RELOAD_MISSING_PREPACKAGED_TIMELINES(missingTimelines); - else if (missingRules > 0 && missingTimelines > 0) - return i18n.RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES(missingRules, missingTimelines); - }, - [] - ); - const goToNewRule = useCallback( (ev) => { ev.preventDefault(); @@ -133,6 +123,24 @@ const RulesPageComponent: React.FC = () => { [history] ); + const loadPrebuiltRulesAndTemplatesButton = useMemo( + () => + getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: userHasNoPermissions(canUserCRUD) || loading, + onClick: handleCreatePrePackagedRules, + }), + [canUserCRUD, getLoadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading] + ); + + const reloadPrebuiltRulesAndTemplatesButton = useMemo( + () => + getReloadPrebuiltRulesAndTemplatesButton({ + isDisabled: userHasNoPermissions(canUserCRUD) || loading, + onClick: handleCreatePrePackagedRules, + }), + [canUserCRUD, getReloadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading] + ); + if ( redirectToDetections( isSignalIndexExists, @@ -177,35 +185,11 @@ const RulesPageComponent: React.FC = () => { title={i18n.PAGE_TITLE} > - {(prePackagedRuleStatus === 'ruleNotInstalled' || - prePackagedTimelineStatus === 'timelinesNotInstalled') && ( - - - {i18n.LOAD_PREPACKAGED_RULES} - - + {loadPrebuiltRulesAndTemplatesButton && ( + {loadPrebuiltRulesAndTemplatesButton} )} - {(prePackagedRuleStatus === 'someRuleUninstall' || - prePackagedTimelineStatus === 'someTimelineUninstall') && ( - - - {getMissingRulesOrTimelinesButtonTitle( - rulesNotInstalled ?? 0, - timelinesNotInstalled ?? 0 - )} - - + {reloadPrebuiltRulesAndTemplatesButton && ( + {reloadPrebuiltRulesAndTemplatesButton} )} @@ -247,6 +231,7 @@ const RulesPageComponent: React.FC = () => { {(prePackagedRuleStatus === 'ruleNeedUpdate' || prePackagedTimelineStatus === 'timelineNeedUpdate') && ( { )} - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton', - { - values: { missingRules }, - defaultMessage: - 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} ', - } - ); - -export const RELOAD_MISSING_PREPACKAGED_TIMELINES = (missingTimelines: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton', - { - values: { missingTimelines }, - defaultMessage: - 'Install {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', - } - ); - -export const RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES = ( - missingRules: number, - missingTimelines: number -) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton', - { - values: { missingRules, missingTimelines }, - defaultMessage: - 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} and {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', - } - ); - export const IMPORT_RULE_BTN_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.components.importRuleModal.importRuleTitle', { diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 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..d785e3b3a131 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -397,13 +397,13 @@ describe('when on the list page', () => { describe('when there is a selected host in the url', () => { let hostDetails: HostInfo; - let agentId: string; + let elasticAgentId: string; let renderAndWaitForData: () => Promise>; const mockEndpointListApi = (mockedPolicyResponse?: HostPolicyResponse) => { const { // eslint-disable-next-line @typescript-eslint/naming-convention host_status, - metadata: { host, ...details }, + metadata: { agent, ...details }, // eslint-disable-next-line @typescript-eslint/naming-convention query_strategy_version, } = mockEndpointDetailsApiResult(); @@ -412,15 +412,15 @@ describe('when on the list page', () => { host_status, metadata: { ...details, - host: { - ...host, + agent: { + ...agent, id: '1', }, }, query_strategy_version, }; - agentId = hostDetails.metadata.elastic.agent.id; + elasticAgentId = hostDetails.metadata.elastic.agent.id; const policy = docGenerator.generatePolicyPackagePolicy(); policy.id = hostDetails.metadata.Endpoint.policy.applied.id; @@ -618,7 +618,7 @@ describe('when on the list page', () => { expect(linkToReassign).not.toBeNull(); expect(linkToReassign.textContent).toEqual('Reassign Policy'); expect(linkToReassign.getAttribute('href')).toEqual( - `/app/ingestManager#/fleet/agents/${agentId}/activity?openReassignFlyout=true` + `/app/ingestManager#/fleet/agents/${elasticAgentId}/activity?openReassignFlyout=true` ); }); @@ -702,10 +702,10 @@ describe('when on the list page', () => { }); it('should not show any numbered badges if all actions are successful', () => { - const policyResponse = docGenerator.generatePolicyResponse( - new Date().getTime(), - HostPolicyResponseActionStatus.success - ); + const policyResponse = docGenerator.generatePolicyResponse({ + ts: new Date().getTime(), + allStatus: HostPolicyResponseActionStatus.success, + }); reactTestingLibrary.act(() => { store.dispatch({ type: 'serverReturnedEndpointPolicyResponse', diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index b514707da6f6..c5d3c3c25313 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -261,11 +261,11 @@ export const EndpointList = () => { return [ { - field: 'metadata.host', + field: 'metadata', name: i18n.translate('xpack.securitySolution.endpoint.list.hostname', { defaultMessage: 'Hostname', }), - render: ({ hostname, id }: HostInfo['metadata']['host']) => { + render: ({ host: { hostname }, agent: { id } }: HostInfo['metadata']) => { const toRoutePath = getEndpointDetailsPath( { ...queryParams, @@ -342,7 +342,7 @@ export const EndpointList = () => { const toRoutePath = getEndpointDetailsPath({ name: 'endpointPolicyResponse', ...queryParams, - selected_endpoint: item.metadata.host.id, + selected_endpoint: item.metadata.agent.id, }); const toRouteUrl = formatUrl(toRoutePath); return ( @@ -561,7 +561,7 @@ export const EndpointList = () => { return ( { )} 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`] = `
+
@@ -103,251 +124,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 0 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 0 - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -363,251 +375,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 1 - -
-
- OS -
-
- - Mac OS - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 1 - -
-
-
-
+ trusted app 1 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 1 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -623,251 +626,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 2 - -
-
- OS -
-
- - Linux - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 2 - -
-
-
-
+ trusted app 2 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 2 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -883,251 +877,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 3 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 3 - -
-
-
-
+ trusted app 3 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 3 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -1143,251 +1128,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 4 - -
-
- OS -
-
- - Mac OS - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 4 - -
-
-
-
+ trusted app 4 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 4 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -1403,251 +1379,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 5 - -
-
- OS -
-
- - Linux - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 5 - -
-
-
-
+ trusted app 5 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 5 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -1663,251 +1630,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 6 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 6 - -
-
-
-
+ trusted app 6 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 6 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -1923,251 +1881,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
+ Name + +
+ + + trusted app 7 + + +
+
-
+
+ -
- Name -
-
- - trusted app 7 - -
-
- OS -
-
- - Mac OS - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 7 - -
-
-
-
+ Mac OS + + + +
-
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 7 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -2183,251 +2132,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 8 - -
-
- OS -
-
- - Linux - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 8 - -
-
-
-
+ trusted app 8 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 8 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -2443,251 +2383,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 9 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 9 - -
-
-
-
+ trusted app 9 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 9 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -2701,6 +2632,9 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
+
+
No items found
+
@@ -2959,7 +2899,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
+
@@ -2981,251 +2924,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 0 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 0 - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -3241,251 +3175,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 1 - -
-
- OS -
-
- - Mac OS - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 1 - -
-
-
-
+ trusted app 1 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 1 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -3501,251 +3426,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 2 - -
-
- OS -
-
- - Linux - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 2 - -
-
-
-
+ trusted app 2 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 2 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -3761,251 +3677,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 3 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 3 - -
-
-
-
+ trusted app 3 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 3 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4021,251 +3928,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 4 - -
-
- OS -
-
- - Mac OS - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 4 - -
-
-
-
+ trusted app 4 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 4 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4281,251 +4179,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 5 - -
-
- OS -
-
- - Linux - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 5 - -
-
-
-
+ trusted app 5 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 5 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4541,251 +4430,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 6 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 6 - -
-
-
-
+ trusted app 6 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 6 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4801,251 +4681,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 7 - -
-
- OS -
-
- - Mac OS - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 7 - -
-
-
-
+ trusted app 7 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 7 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -5061,251 +4932,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 8 - -
-
- OS -
-
- - Linux - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 8 - -
-
-
-
+ trusted app 8 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 8 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -5321,251 +5183,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 9 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 9 - -
-
-
-
+ trusted app 9 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 9 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -5579,6 +5432,9 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
+
+
@@ -5823,251 +5682,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 0 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 0 - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6083,251 +5933,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 1 - -
-
- OS -
-
- - Mac OS - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 1 - -
-
-
-
+ trusted app 1 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 1 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6343,251 +6184,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 2 - -
-
- OS -
-
- - Linux - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 2 - -
-
-
-
+ trusted app 2 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 2 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6603,251 +6435,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - trusted app 3 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 3 - -
-
-
-
+ trusted app 3 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 3 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6863,251 +6686,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 4 - -
-
- OS -
-
- - Mac OS - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 4 - -
-
-
-
+ trusted app 4 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 4 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -7123,251 +6937,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 5 - -
-
- OS -
-
- - Linux - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 5 - -
-
-
-
+ trusted app 5 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 5 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -7383,251 +7188,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - trusted app 6 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 6 - -
-
-
-
+ trusted app 6 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 6 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ + + Remove + + +
@@ -7643,251 +7439,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 7 - -
-
- OS -
-
- - Mac OS - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 7 - -
-
-
-
+ trusted app 7 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 7 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -7903,251 +7690,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 8 - -
-
- OS -
-
- - Linux - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 8 - -
-
-
-
+ trusted app 8 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 8 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -8163,251 +7941,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - trusted app 9 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 9 - -
-
-
-
+ trusted app 9 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 9 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -8421,6 +8190,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..5f652b39ffd5 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 +
@@ -769,248 +779,239 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - trusted app 0 - -
-
- OS -
-
- - Windows - -
-
- Date Created -
-
- - 1 minute ago - -
-
- Created By -
-
- - someone - -
-
- Description -
-
- - Trusted App 0 - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -1033,12 +1034,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 1 + + trusted app 1 +
@@ -1053,7 +1056,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -1152,12 +1163,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 2 + + trusted app 2 +
@@ -1172,7 +1185,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -1271,12 +1292,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 3 + + trusted app 3 +
@@ -1291,7 +1314,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -1390,12 +1421,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 4 + + trusted app 4 +
@@ -1410,7 +1443,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -1509,12 +1550,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 5 + + trusted app 5 +
@@ -1529,7 +1572,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -1628,12 +1679,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 6 + + trusted app 6 +
@@ -1648,7 +1701,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -1747,12 +1808,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 7 + + trusted app 7 +
@@ -1767,7 +1830,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -1866,12 +1937,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 8 + + trusted app 8 +
@@ -1886,7 +1959,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -1985,12 +2066,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 9 + + trusted app 9 +
@@ -2005,7 +2088,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -2104,12 +2195,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 10 + + trusted app 10 +
@@ -2124,7 +2217,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -2223,12 +2324,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 11 + + trusted app 11 +
@@ -2243,7 +2346,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -2342,12 +2453,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 12 + + trusted app 12 +
@@ -2362,7 +2475,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -2461,12 +2582,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 13 + + trusted app 13 +
@@ -2481,7 +2604,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -2580,12 +2711,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 14 + + trusted app 14 +
@@ -2600,7 +2733,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -2699,12 +2840,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 15 + + trusted app 15 +
@@ -2719,7 +2862,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -2818,12 +2969,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 16 + + trusted app 16 +
@@ -2838,7 +2991,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -2937,12 +3098,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 17 + + trusted app 17 +
@@ -2957,7 +3120,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -3056,12 +3227,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 18 + + trusted app 18 +
@@ -3076,7 +3249,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -3175,12 +3356,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 19 + + trusted app 19 +
@@ -3195,7 +3378,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -3654,12 +3845,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 0 + + trusted app 0 +
@@ -3674,7 +3867,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -3773,12 +3974,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 1 + + trusted app 1 +
@@ -3793,7 +3996,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -3892,12 +4103,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 2 + + trusted app 2 +
@@ -3912,7 +4125,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -4011,12 +4232,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 3 + + trusted app 3 +
@@ -4031,7 +4254,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -4130,12 +4361,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 4 + + trusted app 4 +
@@ -4150,7 +4383,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -4249,12 +4490,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 5 + + trusted app 5 +
@@ -4269,7 +4512,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -4368,12 +4619,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 6 + + trusted app 6 +
@@ -4388,7 +4641,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -4487,12 +4748,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 7 + + trusted app 7 +
@@ -4507,7 +4770,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -4606,12 +4877,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 8 + + trusted app 8 +
@@ -4626,7 +4899,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -4725,12 +5006,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 9 + + trusted app 9 +
@@ -4745,7 +5028,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -4844,12 +5135,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 10 + + trusted app 10 +
@@ -4864,7 +5157,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -4963,12 +5264,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 11 + + trusted app 11 +
@@ -4983,7 +5286,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -5082,12 +5393,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 12 + + trusted app 12 +
@@ -5102,7 +5415,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -5201,12 +5522,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 13 + + trusted app 13 +
@@ -5221,7 +5544,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -5320,12 +5651,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 14 + + trusted app 14 +
@@ -5340,7 +5673,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -5439,12 +5780,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 15 + + trusted app 15 +
@@ -5459,7 +5802,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -5558,12 +5909,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 16 + + trusted app 16 +
@@ -5578,7 +5931,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -5677,12 +6038,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 17 + + trusted app 17 +
@@ -5697,7 +6060,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -5796,12 +6167,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 18 + + trusted app 18 +
@@ -5816,7 +6189,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -5915,12 +6296,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 19 + + trusted app 19 +
@@ -5935,7 +6318,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -6552,12 +6943,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 0 + + trusted app 0 +
@@ -6572,7 +6965,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -6671,12 +7072,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 1 + + trusted app 1 +
@@ -6691,7 +7094,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -6790,12 +7201,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 2 + + trusted app 2 +
@@ -6810,7 +7223,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -6909,12 +7330,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 3 + + trusted app 3 +
@@ -6929,7 +7352,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -7028,12 +7459,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 4 + + trusted app 4 +
@@ -7048,7 +7481,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -7147,12 +7588,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 5 + + trusted app 5 +
@@ -7167,7 +7610,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -7266,12 +7717,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 6 + + trusted app 6 +
@@ -7286,7 +7739,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -7385,12 +7846,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 7 + + trusted app 7 +
@@ -7405,7 +7868,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -7504,12 +7975,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 8 + + trusted app 8 +
@@ -7524,7 +7997,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -7623,12 +8104,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 9 + + trusted app 9 +
@@ -7643,7 +8126,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -7742,12 +8233,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 10 + + trusted app 10 +
@@ -7762,7 +8255,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -7861,12 +8362,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 11 + + trusted app 11 +
@@ -7881,7 +8384,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -7980,12 +8491,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 12 + + trusted app 12 +
@@ -8000,7 +8513,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -8099,12 +8620,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 13 + + trusted app 13 +
@@ -8119,7 +8642,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -8218,12 +8749,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 14 + + trusted app 14 +
@@ -8238,7 +8771,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -8337,12 +8878,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 15 + + trusted app 15 +
@@ -8357,7 +8900,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -8456,12 +9007,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 16 + + trusted app 16 +
@@ -8476,7 +9029,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -8575,12 +9136,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 17 + + trusted app 17 +
@@ -8595,7 +9158,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -8694,12 +9265,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 18 + + trusted app 18 +
@@ -8714,7 +9287,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -8813,12 +9394,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 19 + + trusted app 19 +
@@ -8833,7 +9416,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -9292,12 +9883,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 0 + + trusted app 0 +
@@ -9312,7 +9905,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -9411,12 +10012,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 1 + + trusted app 1 +
@@ -9431,7 +10034,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -9530,12 +10141,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 2 + + trusted app 2 +
@@ -9550,7 +10163,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -9649,12 +10270,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 3 + + trusted app 3 +
@@ -9669,7 +10292,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -9768,12 +10399,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 4 + + trusted app 4 +
@@ -9788,7 +10421,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -9887,12 +10528,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 5 + + trusted app 5 +
@@ -9907,7 +10550,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -10006,12 +10657,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 6 + + trusted app 6 +
@@ -10026,7 +10679,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -10125,12 +10786,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 7 + + trusted app 7 +
@@ -10145,7 +10808,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -10244,12 +10915,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 8 + + trusted app 8 +
@@ -10264,7 +10937,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -10363,12 +11044,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 9 + + trusted app 9 +
@@ -10383,7 +11066,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -10482,12 +11173,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 10 + + trusted app 10 +
@@ -10502,7 +11195,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -10601,12 +11302,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 11 + + trusted app 11 +
@@ -10621,7 +11324,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -10720,12 +11431,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 12 + + trusted app 12 +
@@ -10740,7 +11453,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -10839,12 +11560,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 13 + + trusted app 13 +
@@ -10859,7 +11582,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -10958,12 +11689,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 14 + + trusted app 14 +
@@ -10978,7 +11711,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -11077,12 +11818,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 15 + + trusted app 15 +
@@ -11097,7 +11840,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -11196,12 +11947,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 16 + + trusted app 16 +
@@ -11216,7 +11969,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -11315,12 +12076,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 17 + + trusted app 17 +
@@ -11335,7 +12098,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -11434,12 +12205,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 18 + + trusted app 18 +
@@ -11454,7 +12227,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -11553,12 +12334,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 19 + + trusted app 19 +
@@ -11573,7 +12356,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/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 03bde8c7673b..822c57b92b4e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -67,7 +67,7 @@ export const TrustedAppsPage = memo(() => { return ( { (createEmbeddable as jest.Mock).mockResolvedValue(mockCreateEmbeddable); - let wrapper: ReactWrapper; - await act(async () => { - wrapper = mount( - - - - ); - }); + const wrapper: ReactWrapper = mount( + + + + ); - wrapper!.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(true); - expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + }); }); test('renders IndexPatternsMissingPrompt', async () => { @@ -132,20 +131,18 @@ describe('EmbeddedMapComponent', () => { (createEmbeddable as jest.Mock).mockResolvedValue(mockCreateEmbeddable); - let wrapper: ReactWrapper; - await act(async () => { - wrapper = mount( - - - - ); - }); - - wrapper!.update(); + const wrapper: ReactWrapper = mount( + + + + ); + await waitFor(() => { + wrapper.update(); - expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(true); - expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + }); }); test('renders Loader', async () => { @@ -154,19 +151,17 @@ describe('EmbeddedMapComponent', () => { (createEmbeddable as jest.Mock).mockResolvedValue(null); - let wrapper: ReactWrapper; - await act(async () => { - wrapper = mount( - - - - ); - }); - - wrapper!.update(); + const wrapper: ReactWrapper = mount( + + + + ); + await waitFor(() => { + wrapper.update(); - expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="loading-panel"]').exists()).toEqual(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx index 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/embedded_map_helpers.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx index 0c6b90ec2b9d..c503690e776a 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx @@ -12,9 +12,12 @@ import { mockAPMRegexIndexPattern, mockAPMTransactionIndexPattern, mockAuditbeatIndexPattern, + mockCCSGlobIndexPattern, + mockCommaFilebeatAuditbeatCCSGlobIndexPattern, + mockCommaFilebeatAuditbeatGlobIndexPattern, + mockCommaFilebeatExclusionGlobIndexPattern, mockFilebeatIndexPattern, mockGlobIndexPattern, - mockCCSGlobIndexPattern, } from './__mocks__/mock'; const mockEmbeddable = embeddablePluginMock.createStartContract(); @@ -122,5 +125,44 @@ describe('embedded_map_helpers', () => { }); expect(matchingIndexPatterns).toEqual([mockFilebeatIndexPattern]); }); + + test('matches on comma separated Kibana index pattern', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [ + mockCommaFilebeatAuditbeatGlobIndexPattern, + mockAuditbeatIndexPattern, + ], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([ + mockCommaFilebeatAuditbeatGlobIndexPattern, + mockAuditbeatIndexPattern, + ]); + }); + + test('matches on excluded comma separated Kibana index pattern', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [ + mockCommaFilebeatExclusionGlobIndexPattern, + mockAuditbeatIndexPattern, + ], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([ + mockCommaFilebeatExclusionGlobIndexPattern, + mockAuditbeatIndexPattern, + ]); + }); + + test('matches on CCS comma separated Kibana index pattern', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [ + mockCommaFilebeatAuditbeatCCSGlobIndexPattern, + mockAuditbeatIndexPattern, + ], + siemDefaultIndices: ['cluster2:filebeat-*', 'cluster1:auditbeat-*'], + }); + expect(matchingIndexPatterns).toEqual([mockCommaFilebeatAuditbeatCCSGlobIndexPattern]); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx index 25928197590e..4ac759ea534e 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx @@ -150,7 +150,15 @@ export const findMatchingIndexPatterns = ({ const pattern = kip.attributes.title; return ( !ignoredIndexPatterns.includes(pattern) && - siemDefaultIndices.some((sdi) => minimatch(sdi, pattern)) + siemDefaultIndices.some((sdi) => { + const splitPattern = pattern.split(',') ?? []; + return splitPattern.length > 1 + ? splitPattern.some((p) => { + const isMatch = minimatch(sdi, p); + return isMatch && p.charAt(0) === '-' ? false : isMatch; + }) + : minimatch(sdi, pattern); + }) ); }); } catch { diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap index 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/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index 777a7292e9c2..411e4b3e3a5b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -25,6 +25,7 @@ const StyledEdgeLine = styled.div` return `${fontSize(props.magFactorX, 12, 8.5)}px`; }}; background-color: ${(props) => props.resolverEdgeColor}; + z-index: 10; `; interface StyledElapsedTime { diff --git a/x-pack/plugins/security_solution/public/resolver/view/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..7a3657fe9351 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -62,6 +62,7 @@ const StyledDescriptionText = styled.div` text-align: left; text-transform: uppercase; width: fit-content; + z-index: 45; `; const StyledOuterGroup = styled.g` @@ -311,6 +312,7 @@ const UnstyledProcessEventDot = React.memo( outline: 'transparent', border: 'none', pointerEvents: 'none', + zIndex: 30, }} > @@ -382,7 +384,7 @@ const UnstyledProcessEventDot = React.memo( />
= 2 ? 'euiButton' : 'euiButton euiButton--small'} + className={'euiButton euiButton--small'} id={labelHTMLID} onClick={handleClick} onFocus={handleFocus} @@ -391,6 +393,7 @@ const UnstyledProcessEventDot = React.memo( backgroundColor: colorMap.resolverBackground, alignSelf: 'flex-start', padding: 0, + zIndex: 45, }} > ( 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/styles.tsx b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx index 7b94445ee083..714e7e71787a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx @@ -17,6 +17,7 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)` border: none; display: flex; flex-flow: column; + z-index: auto; &.options { font-size: 0.8rem; @@ -26,8 +27,20 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)` position: absolute; top: 4.5em; overflow-x: visible; - width: 12em; - z-index: 2; + width: 24ch; + z-index: auto; + } + + &.options::after { + position: absolute; + content: ''; + width: 100%; + height: 100%; + left: 0; + top: 0; + z-index: 20; + backdrop-filter: blur(2px); + pointer-events: none; } &.options .item { @@ -37,6 +50,7 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)` width: fit-content; border-radius: 2px; line-height: 0.8; + z-index: 40; } &.options .item button { @@ -114,6 +128,8 @@ export const GraphContainer = styled.div` display: flex; flex-grow: 1; contain: layout; + position: relative; + z-index: 0; `; /** 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/edit_data_provider/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx index 72386a2b287f..2bc202c65f6a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx @@ -15,7 +15,6 @@ import { EuiFormRow, EuiPanel, EuiSpacer, - EuiToolTip, } from '@elastic/eui'; import React, { useEffect, useMemo, useState, useCallback } from 'react'; import styled from 'styled-components'; @@ -193,18 +192,16 @@ export const StatefulEditDataProvider = React.memo( - 0 ? updatedField[0].label : null}> - - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx index 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/open_timeline/export_timeline/export_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx index d0cfbaccde7d..31051a51a58d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx @@ -12,7 +12,7 @@ import { mockSelectedTimeline } from './mocks'; import * as i18n from '../translations'; import { ReactWrapper, mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; +import { waitFor } from '@testing-library/react'; import { useParams } from 'react-router-dom'; jest.mock('../translations', () => { @@ -102,15 +102,14 @@ describe('TimelineDownloader', () => { ...defaultTestProps, }; - await act(() => { - wrapper = mount(); - }); - - wrapper.update(); + wrapper = mount(); + await waitFor(() => { + wrapper.update(); - expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( - i18n.SUCCESSFULLY_EXPORTED_TIMELINES - ); + expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( + i18n.SUCCESSFULLY_EXPORTED_TIMELINES + ); + }); }); test('With correct toast message on success for exported templates', async () => { @@ -119,15 +118,15 @@ describe('TimelineDownloader', () => { }; (useParams as jest.Mock).mockReturnValue({ tabName: 'template' }); - await act(() => { - wrapper = mount(); - }); + wrapper = mount(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( - i18n.SUCCESSFULLY_EXPORTED_TIMELINES - ); + expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( + i18n.SUCCESSFULLY_EXPORTED_TIMELINES + ); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 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/insert_timeline_popover/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx deleted file mode 100644 index bfd76f0a98d4..000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import { InsertTimelinePopoverComponent } from '.'; - -const onTimelineChange = jest.fn(); -const props = { - isDisabled: false, - onTimelineChange, -}; - -describe('Insert timeline popover ', () => { - it('it renders', () => { - const wrapper = mount(); - expect(wrapper.find('[data-test-subj="insert-timeline-popover"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx deleted file mode 100644 index 11ad54321da8..000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx +++ /dev/null @@ -1,95 +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 { EuiButtonIcon, EuiPopover, EuiSelectableOption, EuiToolTip } from '@elastic/eui'; -import React, { memo, useCallback, useMemo, useState } from 'react'; - -import { OpenTimelineResult } from '../../open_timeline/types'; -import { SelectableTimeline } from '../selectable_timeline'; -import * as i18n from '../translations'; -import { TimelineType } from '../../../../../common/types/timeline'; - -interface InsertTimelinePopoverProps { - isDisabled: boolean; - hideUntitled?: boolean; - onTimelineChange: ( - timelineTitle: string, - timelineId: string | null, - graphEventId?: string - ) => void; -} - -type Props = InsertTimelinePopoverProps; - -export const InsertTimelinePopoverComponent: React.FC = ({ - isDisabled, - hideUntitled = false, - onTimelineChange, -}) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const handleClosePopover = useCallback(() => { - setIsPopoverOpen(false); - }, []); - - const handleOpenPopover = useCallback(() => { - setIsPopoverOpen(true); - }, []); - - const insertTimelineButton = useMemo( - () => ( - {i18n.INSERT_TIMELINE}

}> - -
- ), - [handleOpenPopover, isDisabled] - ); - - const handleGetSelectableOptions = useCallback( - ({ timelines }) => [ - ...timelines.map( - (t: OpenTimelineResult, index: number) => - ({ - description: t.description, - favorite: t.favorite, - label: t.title, - id: t.savedObjectId, - key: `${t.title}-${index}`, - title: t.title, - checked: undefined, - } as EuiSelectableOption) - ), - ], - [] - ); - - return ( - - - - ); -}; - -export const InsertTimelinePopover = memo(InsertTimelinePopoverComponent); 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/timelines/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx index 641a4105e1af..a80576c7237f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx @@ -16,7 +16,7 @@ import { EuiFilterGroup, EuiFilterButton, } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; +import { isEmpty, debounce } from 'lodash/fp'; import React, { memo, useCallback, useMemo, useState, useEffect, useRef } from 'react'; import styled from 'styled-components'; @@ -120,9 +120,14 @@ const SelectableTimelineComponent: React.FC = ({ const selectableListOuterRef = useRef(null); const selectableListInnerRef = useRef(null); - const onSearchTimeline = useCallback((val) => { - setSearchTimelineValue(val); - }, []); + const debouncedSetSearchTimelineValue = useMemo(() => debounce(500, setSearchTimelineValue), []); + + const onSearchTimeline = useCallback( + (val) => { + debouncedSetSearchTimelineValue(val); + }, + [debouncedSetSearchTimelineValue] + ); const handleOnToggleOnlyFavorites = useCallback(() => { setOnlyFavorites(!onlyFavorites); @@ -238,7 +243,7 @@ const SelectableTimelineComponent: React.FC = ({ isLoading: loading, placeholder: useMemo(() => i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER(timelineType), [timelineType]), onSearch: onSearchTimeline, - incremental: false, + incremental: true, inputRef: (node: HTMLInputElement | null) => { setSearchRef(node); }, 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/scripts/endpoint/resolver_generator_script.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts index 9fa2257afd41..c513c4576d89 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts @@ -10,7 +10,7 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { KbnClient, ToolingLog } from '@kbn/dev-utils'; import { AxiosResponse } from 'axios'; import { indexHostsAndAlerts } from '../../common/endpoint/index_data'; -import { ANCESTRY_LIMIT } from '../../common/endpoint/generate_data'; +import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data'; import { AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../../ingest_manager/common/constants'; import { CreateFleetSetupResponse, @@ -250,6 +250,8 @@ async function main() { percentTerminated: argv.percentTerminated, alwaysGenMaxChildrenPerNode: argv.maxChildrenPerNode, ancestryArraySize: argv.ancestryArraySize, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(argv.eventIndex), + alertsDataStream: EndpointDocGenerator.createDataStreamFromIndex(argv.alertIndex), } ); console.log(`Creating and indexing documents took: ${new Date().getTime() - startTime}ms`); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 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/endpoint/routes/resolver/utils/pagination.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts index 55965e5a9c70..d5297072388e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts @@ -20,7 +20,7 @@ describe('Pagination', () => { }; describe('cursor', () => { const root = generator.generateEvent(); - const events = Array.from(generator.relatedEventsGenerator(root, 5)); + const events = Array.from(generator.relatedEventsGenerator({ node: root, relatedEvents: 5 })); it('does build a cursor when received the same number of events as was requested', () => { expect(PaginationBuilder.buildCursorRequestLimit(4, events)).not.toBeNull(); diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts index 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/rules/get_rules_to_update.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts index 5b2eb24bcdcd..a21f861c1f34 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts @@ -4,102 +4,470 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRulesToUpdate } from './get_rules_to_update'; +import { filterInstalledRules, getRulesToUpdate, mergeExceptionLists } from './get_rules_to_update'; import { getResult } from '../routes/__mocks__/request_responses'; import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; describe('get_rules_to_update', () => { - test('should return empty array if both rule sets are empty', () => { - const update = getRulesToUpdate([], []); - expect(update).toEqual([]); - }); + describe('get_rules_to_update', () => { + test('should return empty array if both rule sets are empty', () => { + const update = getRulesToUpdate([], []); + expect(update).toEqual([]); + }); - test('should return empty array if the id of the two rules do not match', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 2; + test('should return empty array if the rule_id of the two rules do not match', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-2'; - installedRule.params.version = 1; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([]); - }); + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-2'; + installedRule.params.version = 1; + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([]); + }); - test('should return empty array if the id of file system rule is less than the installed version', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 1; + test('should return empty array if the version of file system rule is less than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-1'; - installedRule.params.version = 2; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([]); - }); + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 2; + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([]); + }); - test('should return empty array if the id of file system rule is the same as the installed version', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 1; + test('should return empty array if the version of file system rule is the same as the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-1'; - installedRule.params.version = 1; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([]); - }); + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([]); + }); + + test('should return the rule to update if the version of file system rule is greater than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + installedRule.params.exceptionsList = []; + + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([ruleFromFileSystem]); + }); + + test('should return 1 rule out of 2 to update if the version of file system rule is greater than the installed version of just one', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = []; + + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule1, installedRule2]); + expect(update).toEqual([ruleFromFileSystem]); + }); + + test('should return 2 rules out of 2 to update if the version of file system rule is greater than the installed version of both', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem2.rule_id = 'rule-2'; + ruleFromFileSystem2.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = []; + + const update = getRulesToUpdate( + [ruleFromFileSystem1, ruleFromFileSystem2], + [installedRule1, installedRule2] + ); + expect(update).toEqual([ruleFromFileSystem1, ruleFromFileSystem2]); + }); + + test('should add back an exception_list if it was removed by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); + + test('should not remove an additional exception_list if an additional one was added by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'second_exception_list', + list_id: 'some-other-id', + namespace_type: 'single', + type: 'detection', + }, + ]; + + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual([ + ...ruleFromFileSystem1.exceptions_list, + ...installedRule1.params.exceptionsList, + ]); + }); + + test('should not remove an existing exception_list if they are the same between the current installed one and the upgraded one', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; - test('should return the rule to update if the id of file system rule is greater than the installed version', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 2; + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-1'; - installedRule.params.version = 1; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([ruleFromFileSystem]); + test('should not remove an existing exception_list if the rule has an empty exceptions list', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList); + }); + + test('should not remove an existing exception_list if the rule has an empty exceptions list for multiple rules', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem2.exceptions_list = []; + ruleFromFileSystem2.rule_id = 'rule-2'; + ruleFromFileSystem2.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const [update1, update2] = getRulesToUpdate( + [ruleFromFileSystem1, ruleFromFileSystem2], + [installedRule1, installedRule2] + ); + expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList); + expect(update2.exceptions_list).toEqual(installedRule2.params.exceptionsList); + }); + + test('should not remove an existing exception_list if the rule has an empty exceptions list for mixed rules', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem2.exceptions_list = []; + ruleFromFileSystem2.rule_id = 'rule-2'; + ruleFromFileSystem2.version = 2; + ruleFromFileSystem2.exceptions_list = [ + { + id: 'second_list', + list_id: 'second_list', + namespace_type: 'single', + type: 'detection', + }, + ]; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const [update1, update2] = getRulesToUpdate( + [ruleFromFileSystem1, ruleFromFileSystem2], + [installedRule1, installedRule2] + ); + expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList); + expect(update2.exceptions_list).toEqual([ + ...ruleFromFileSystem2.exceptions_list, + ...installedRule2.params.exceptionsList, + ]); + }); }); - test('should return 1 rule out of 2 to update if the id of file system rule is greater than the installed version of just one', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 2; + describe('filterInstalledRules', () => { + test('should return "false" if the id of the two rules do not match', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-2'; + installedRule.params.version = 1; + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(false); + }); - const installedRule1 = getResult(); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; + test('should return "false" if the version of file system rule is less than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; - const installedRule2 = getResult(); - installedRule2.params.ruleId = 'rule-2'; - installedRule2.params.version = 1; + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 2; + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(false); + }); - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule1, installedRule2]); - expect(update).toEqual([ruleFromFileSystem]); + test('should return "false" if the version of file system rule is the same as the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(false); + }); + + test('should return "true" to update if the version of file system rule is greater than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + installedRule.params.exceptionsList = []; + + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(true); + }); }); - test('should return 2 rules out of 2 to update if the id of file system rule is greater than the installed version of both', () => { - const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem1.rule_id = 'rule-1'; - ruleFromFileSystem1.version = 2; + describe('mergeExceptionLists', () => { + test('should add back an exception_list if it was removed by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); + + test('should not remove an additional exception_list if an additional one was added by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'second_exception_list', + list_id: 'some-other-id', + namespace_type: 'single', + type: 'detection', + }, + ]; + + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual([ + ...ruleFromFileSystem1.exceptions_list, + ...installedRule1.params.exceptionsList, + ]); + }); + + test('should not remove an existing exception_list if they are the same between the current installed one and the upgraded one', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; - const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem2.rule_id = 'rule-2'; - ruleFromFileSystem2.version = 2; + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); - const installedRule1 = getResult(); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; + test('should not remove an existing exception_list if the rule has an empty exceptions list', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; - const installedRule2 = getResult(); - installedRule2.params.ruleId = 'rule-2'; - installedRule2.params.version = 1; + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; - const update = getRulesToUpdate( - [ruleFromFileSystem1, ruleFromFileSystem2], - [installedRule1, installedRule2] - ); - expect(update).toEqual([ruleFromFileSystem1, ruleFromFileSystem2]); + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts index 577ad44789bd..28a58ea49b90 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts @@ -7,15 +7,67 @@ import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { RuleAlertType } from './types'; +/** + * Returns the rules to update by doing a compare to the rules from the file system against + * the installed rules already. This also merges exception list items between the two since + * exception list items can exist on both rules to update and already installed rules. + * @param rulesFromFileSystem The rules on the file system to check against installed + * @param installedRules The installed rules + */ export const getRulesToUpdate = ( rulesFromFileSystem: AddPrepackagedRulesSchemaDecoded[], installedRules: RuleAlertType[] ): AddPrepackagedRulesSchemaDecoded[] => { - return rulesFromFileSystem.filter((rule) => - installedRules.some((installedRule) => { - return ( - rule.rule_id === installedRule.params.ruleId && rule.version > installedRule.params.version + return rulesFromFileSystem + .filter((ruleFromFileSystem) => filterInstalledRules(ruleFromFileSystem, installedRules)) + .map((ruleFromFileSystem) => mergeExceptionLists(ruleFromFileSystem, installedRules)); +}; + +/** + * Filters rules from the file system that do not match the installed rules so you only + * get back rules that are going to be updated + * @param ruleFromFileSystem The rules from the file system to check if any are updates + * @param installedRules The installed rules to compare against for updates + */ +export const filterInstalledRules = ( + ruleFromFileSystem: AddPrepackagedRulesSchemaDecoded, + installedRules: RuleAlertType[] +): boolean => { + return installedRules.some((installedRule) => { + return ( + ruleFromFileSystem.rule_id === installedRule.params.ruleId && + ruleFromFileSystem.version > installedRule.params.version + ); + }); +}; + +/** + * Given a rule from the file system and the set of installed rules this will merge the exception lists + * from the installed rules onto the rules from the file system. + * @param ruleFromFileSystem The rules from the file system that might have exceptions_lists + * @param installedRules The installed rules which might have user driven exceptions_lists + */ +export const mergeExceptionLists = ( + ruleFromFileSystem: AddPrepackagedRulesSchemaDecoded, + installedRules: RuleAlertType[] +): AddPrepackagedRulesSchemaDecoded => { + if (ruleFromFileSystem.exceptions_list != null) { + const installedRule = installedRules.find( + (ruleToFind) => ruleToFind.params.ruleId === ruleFromFileSystem.rule_id + ); + if (installedRule != null && installedRule.params.exceptionsList != null) { + const installedExceptionList = installedRule.params.exceptionsList; + const fileSystemExceptions = ruleFromFileSystem.exceptions_list.filter((potentialDuplicate) => + installedExceptionList.every((item) => item.list_id !== potentialDuplicate.list_id) ); - }) - ); + return { + ...ruleFromFileSystem, + exceptions_list: [...fileSystemExceptions, ...installedRule.params.exceptionsList], + }; + } else { + return ruleFromFileSystem; + } + } else { + return ruleFromFileSystem; + } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index 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/common/lib/spaces_url_parser.test.ts b/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts index b25d79c0a690..2b34bc77ec68 100644 --- a/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts +++ b/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts @@ -10,41 +10,76 @@ describe('getSpaceIdFromPath', () => { describe('without a serverBasePath defined', () => { test('it identifies the space url context', () => { const basePath = `/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath)).toEqual('my-awesome-space-lives-here'); + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: 'my-awesome-space-lives-here', + pathHasExplicitSpaceIdentifier: true, + }); }); test('ignores space identifiers in the middle of the path', () => { const basePath = `/this/is/a/crazy/path/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath)).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); }); test('it handles base url without a space url context', () => { const basePath = `/this/is/a/crazy/path/s`; - expect(getSpaceIdFromPath(basePath)).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); + }); + + test('it identifies the space url context with the default space', () => { + const basePath = `/s/${DEFAULT_SPACE_ID}`; + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: true, + }); }); }); describe('with a serverBasePath defined', () => { test('it identifies the space url context', () => { const basePath = `/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath, '/')).toEqual('my-awesome-space-lives-here'); + expect(getSpaceIdFromPath(basePath, '/')).toEqual({ + spaceId: 'my-awesome-space-lives-here', + pathHasExplicitSpaceIdentifier: true, + }); }); test('it identifies the space url context following the server base path', () => { const basePath = `/server-base-path-here/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath, '/server-base-path-here')).toEqual( - 'my-awesome-space-lives-here' - ); + expect(getSpaceIdFromPath(basePath, '/server-base-path-here')).toEqual({ + spaceId: 'my-awesome-space-lives-here', + pathHasExplicitSpaceIdentifier: true, + }); }); test('ignores space identifiers in the middle of the path', () => { const basePath = `/this/is/a/crazy/path/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath, '/this/is/a')).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath, '/this/is/a')).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); + }); + + test('it identifies the space url context with the default space following the server base path', () => { + const basePath = `/server-base-path-here/s/${DEFAULT_SPACE_ID}`; + expect(getSpaceIdFromPath(basePath, '/server-base-path-here')).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: true, + }); }); test('it handles base url without a space url context', () => { const basePath = `/this/is/a/crazy/path/s`; - expect(getSpaceIdFromPath(basePath, basePath)).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath, basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); }); }); }); diff --git a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts index 994ec7c59cb6..be950e6a651e 100644 --- a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts +++ b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts @@ -5,20 +5,22 @@ */ import { DEFAULT_SPACE_ID } from '../constants'; +const spaceContextRegex = /^\/s\/([a-z0-9_\-]+)/; + export function getSpaceIdFromPath( requestBasePath: string = '/', serverBasePath: string = '/' -): string { - let pathToCheck: string = requestBasePath; +): { spaceId: string; pathHasExplicitSpaceIdentifier: boolean } { + const pathToCheck: string = stripServerBasePath(requestBasePath, serverBasePath); - if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { - pathToCheck = requestBasePath.substr(serverBasePath.length); - } // Look for `/s/space-url-context` in the base path - const matchResult = pathToCheck.match(/^\/s\/([a-z0-9_\-]+)/); + const matchResult = pathToCheck.match(spaceContextRegex); if (!matchResult || matchResult.length === 0) { - return DEFAULT_SPACE_ID; + return { + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }; } // Ignoring first result, we only want the capture group result at index 1 @@ -28,7 +30,10 @@ export function getSpaceIdFromPath( throw new Error(`Unable to determine Space ID from request path: ${requestBasePath}`); } - return spaceId; + return { + spaceId, + pathHasExplicitSpaceIdentifier: true, + }; } export function addSpaceIdToPath( @@ -45,3 +50,10 @@ export function addSpaceIdToPath( } return `${basePath}${requestedPath}`; } + +function stripServerBasePath(requestBasePath: string, serverBasePath: string) { + if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { + return requestBasePath.substr(serverBasePath.length); + } + return requestBasePath; +} diff --git a/x-pack/plugins/spaces/server/lib/audit_logger.ts b/x-pack/plugins/spaces/server/lib/audit_logger.ts index da7c3886277c..8110e3fbc662 100644 --- a/x-pack/plugins/spaces/server/lib/audit_logger.ts +++ b/x-pack/plugins/spaces/server/lib/audit_logger.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditLogger } from '../../../security/server'; +import { LegacyAuditLogger } from '../../../security/server'; export class SpacesAuditLogger { - private readonly auditLogger: AuditLogger; + private readonly auditLogger: LegacyAuditLogger; - constructor(auditLogger: AuditLogger = { log() {} }) { + constructor(auditLogger: LegacyAuditLogger = { log() {} }) { this.auditLogger = auditLogger; } public spacesAuthorizationFailure(username: string, action: string, spaceIds?: string[]) { diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts index 4b3a5d662f12..6408803c2114 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts @@ -10,7 +10,6 @@ import { CoreSetup, } from 'src/core/server'; import { format } from 'url'; -import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { modifyUrl } from '../utils/url'; import { getSpaceIdFromPath } from '../../../common'; @@ -28,9 +27,9 @@ export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDep // If navigating within the context of a space, then we store the Space's URL Context on the request, // and rewrite the request to not include the space identifier in the URL. - const spaceId = getSpaceIdFromPath(path, serverBasePath); + const { spaceId, pathHasExplicitSpaceIdentifier } = getSpaceIdFromPath(path, serverBasePath); - if (spaceId !== DEFAULT_SPACE_ID) { + if (pathHasExplicitSpaceIdentifier) { const reqBasePath = `/s/${spaceId}`; http.basePath.set(request, reqBasePath); diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index b341d76c8664..b48bf971d0c1 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -58,7 +58,7 @@ const createService = async (serverBasePath: string = '') => { serverBasePath, } as HttpServiceSetup['basePath']; httpSetup.basePath.get = jest.fn().mockImplementation((request: KibanaRequest) => { - const spaceId = getSpaceIdFromPath(request.url.path); + const { spaceId } = getSpaceIdFromPath(request.url.path); if (spaceId !== DEFAULT_SPACE_ID) { return `/s/${spaceId}`; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index cf181a78efcb..3630675a7ed3 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -63,7 +63,7 @@ export class SpacesService { ? (request as Record).getBasePath() : http.basePath.get(request); - const spaceId = getSpaceIdFromPath(basePath, http.basePath.serverBasePath); + const { spaceId } = getSpaceIdFromPath(basePath, http.basePath.serverBasePath); return spaceId; }; diff --git a/x-pack/plugins/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 fb2d5e07030a..cc611e124ea7 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -17,6 +17,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 { Logger } from './types'; @@ -149,6 +150,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, @@ -156,7 +164,7 @@ export class TaskManager { this.pool = new TaskPool({ logger: this.logger, - maxWorkers: opts.config.max_workers, + maxWorkers$: maxWorkersConfiguration$, }); const { @@ -166,7 +174,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 92374908c60f..44f5f5648c2a 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 acd19bd75f7a..15261be3d89a 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -121,6 +121,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; @@ -171,11 +172,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); } @@ -333,12 +340,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, @@ -362,8 +379,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, @@ -373,8 +393,11 @@ export class TaskStore { { refresh: false, } - ) - ).saved_objects; + )); + } catch (e) { + this.errors$.next(e); + throw e; + } return updatedSavedObjects.map((updatedSavedObject, index) => isSavedObjectsUpdateResponse(updatedSavedObject) @@ -404,7 +427,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; + } } /** @@ -414,7 +442,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); } /** @@ -438,14 +473,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; @@ -464,17 +505,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/app/sections/transform_management/components/transform_list/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx index 4478edab0dba..de45322d0498 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx @@ -26,6 +26,23 @@ function getItemDescription(value: any) { return value.toString(); } +/** + * Creates a deterministic number based hash out of a string. + */ +export function stringHash(str: string): number { + let hash = 0; + let chr = 0; + if (str.length === 0) { + return hash; + } + for (let i = 0; i < str.length; i++) { + chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr; // eslint-disable-line no-bitwise + hash |= 0; // eslint-disable-line no-bitwise + } + return hash < 0 ? hash * -2 : hash; +} + interface Item { title: string; description: any; @@ -162,9 +179,11 @@ export const ExpandedRow: FC = ({ item }) => { position: 'left', }; + const tabId = stringHash(item.id); + const tabs = [ { - id: `transform-details-tab-${item.id}`, + id: `transform-details-tab-${tabId}`, 'data-test-subj': 'transformDetailsTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformDetailsLabel', @@ -175,7 +194,7 @@ export const ExpandedRow: FC = ({ item }) => { content: , }, { - id: `transform-stats-tab-${item.id}`, + id: `transform-stats-tab-${tabId}`, 'data-test-subj': 'transformStatsTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformStatsLabel', @@ -186,13 +205,13 @@ export const ExpandedRow: FC = ({ item }) => { content: , }, { - id: `transform-json-tab-${item.id}`, + id: `transform-json-tab-${tabId}`, 'data-test-subj': 'transformJsonTab', name: 'JSON', content: , }, { - id: `transform-messages-tab-${item.id}`, + id: `transform-messages-tab-${tabId}`, 'data-test-subj': 'transformMessagesTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformMessagesLabel', @@ -203,7 +222,7 @@ export const ExpandedRow: FC = ({ item }) => { content: , }, { - id: `transform-preview-tab-${item.id}`, + id: `transform-preview-tab-${tabId}`, 'data-test-subj': 'transformPreviewTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformPreviewLabel', 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/transform/readme.md b/x-pack/plugins/transform/readme.md new file mode 100644 index 000000000000..2ee2a7b70c5f --- /dev/null +++ b/x-pack/plugins/transform/readme.md @@ -0,0 +1,117 @@ +# Documentation for Transforms UI developers + +This plugin provides access to the transforms features provided by Elastic. + +## Requirements + +To use the transforms feature, you must have at least a Basic license. For more +info, refer to +[Set up transforms](https://www.elastic.co/guide/en/elasticsearch/reference/current/transform-setup.html). + + +## Setup local environment + +### Kibana + +1. Fork and clone the [Kibana repo](https://github.com/elastic/kibana). + +1. Install `nvm`, `node`, `yarn` (for example, by using Homebrew). See + [Install dependencies](https://www.elastic.co/guide/en/kibana/master/development-getting-started.html#_install_dependencies). + +1. Make sure that Elasticsearch is deployed and running on `localhost:9200`. + +1. Navigate to the directory of the `kibana` repository on your machine. + +1. Fetch the latest changes from the repository. + +1. Checkout the branch of the version you want to use. For example, if you want + to use a 7.9 version, run `git checkout 7.9`. (Your Elasticsearch and Kibana + instances need to be the same version.) + +1. Run `nvm use`. The response shows the Node version that the environment uses. + If you need to update your Node version, the response message contains the + command you need to run to do it. + +1. Run `yarn kbn bootstrap`. It takes all the dependencies in the code and + installs/checks them. It is recommended to use it every time when you switch + between branches. + +1. Make a copy of `kibana.yml` and save as `kibana.dev.yml`. (Git will not track + the changes in `kibana.dev.yml` but yarn will use it.) + +1. Provide the appropriate password and user name in `kibana.dev.yml`. + +1. Run `yarn start` to start Kibana. + +1. Go to http://localhost:560x/xxx (check the terminal message for the exact + path). + +For more details, refer to this [getting started](https://www.elastic.co/guide/en/kibana/master/development-getting-started.html) page. + +### Adding sample data to Kibana + +Kibana has sample data sets that you can add to your setup so that you can test +different configurations on sample data. + +1. Click the Elastic logo in the upper left hand corner of your browser to + navigate to the Kibana home page. + +1. Click *Load a data set and a Kibana dashboard*. + +1. Pick a data set or feel free to click *Add* on all of the available sample + data sets. + +These data sets are now ready to be used for creating transforms in Kibana. + +## Running tests + +### Jest tests + +Run the test following jest tests from `kibana/x-pack`. + +New snapshots, all plugins: + +``` +node scripts/jest +``` + +Update snapshots for the transform plugin: + +``` +node scripts/jest plugins/transform -u +``` + +Update snapshots for a specific directory only: + +``` +node scripts/jest x-pack/plugins/transform/public/app/sections +``` + +Run tests with verbose output: + +``` +node scripts/jest plugins/transform --verbose +``` + +### Functional tests + +Before running the test server, make sure to quit all other instances of +Elasticsearch. + +1. From one terminal, in the x-pack directory, run: + + node scripts/functional_tests_server.js --config test/functional/config.js + + This command starts an Elasticsearch and Kibana instance that the tests will be run against. + +1. In another tab, run the following command to perform API integration tests (from the x-pack directory): + + node scripts/functional_test_runner.js --include-tag transform --config test/api_integration/config + + The transform API integration tests are located in `x-pack/test/api_integration/apis/transform`. + +1. In another tab, run the following command to perform UI functional tests (from the x-pack directory): + + node scripts/functional_test_runner.js --include-tag transform + + The transform functional tests are located in `x-pack/test/functional/apps/transform`. diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bcdc9e013366..d4498a626ab9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -91,6 +91,7 @@ "advancedSettings.categoryNames.timelionLabel": "Timelion", "advancedSettings.categoryNames.visualizationsLabel": "可視化", "advancedSettings.categorySearchLabel": "カテゴリー", + "advancedSettings.featureCatalogueTitle": "日付形式の変更、ダークモードの有効化など、Kibanaエクスペリエンスをカスタマイズします。", "advancedSettings.field.changeImageLinkAriaLabel": "{ariaName} を変更", "advancedSettings.field.changeImageLinkText": "画像を変更", "advancedSettings.field.codeEditorSyntaxErrorMessage": "無効な JSON 構文", @@ -123,6 +124,8 @@ "advancedSettings.pageTitle": "設定", "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", "advancedSettings.searchBarAriaLabel": "高度な設定を検索", + "advancedSettings.voiceAnnouncement.ariaLabel": "詳細設定結果情報", + "advancedSettings.voiceAnnouncement.noSearchResultScreenReaderMessage": "{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other {# オプション}}があります。", "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other {# オプション}}があります。", "apmOss.tutorial.apmAgents.statusCheck.btnLabel": "エージェントステータスを確認", "apmOss.tutorial.apmAgents.statusCheck.errorMessage": "エージェントからまだデータを受け取っていません", @@ -207,6 +210,7 @@ "apmOss.tutorial.nodeClient.configure.commands.setRequiredServiceNameComment": "package.json からサービス名を上書きします", "apmOss.tutorial.nodeClient.configure.commands.useIfApmRequiresTokenComment": "APM Server にトークンが必要な場合に使います", "apmOss.tutorial.nodeClient.configure.textPost": "[Babel/ES モジュール]({babelEsModulesLink}) との使用を含む高度な用途に関しては、 [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.nodeClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APMサービスは「serviceName」に基づいてプログラムで作成されます。このエージェントはさまざまなフレームワークをサポートしていますが、カスタムスタックで使用することもできます。", "apmOss.tutorial.nodeClient.configure.title": "エージェントの構成", "apmOss.tutorial.nodeClient.install.textPre": "Node.js 用の APM エージェントをアプリケーションに依存関係としてインストール。", "apmOss.tutorial.nodeClient.install.title": "APM エージェントのインストール", @@ -255,8 +259,8 @@ "console.autocomplete.addMethodMetaText": "メソド", "console.consoleDisplayName": "コンソール", "console.consoleMenu.copyAsCurlMessage": "リクエストが URL としてコピーされました", - "console.devToolsDescription": "cURL をスキップしこの JSON インスタンスを使って、データに直接アクセスします。", - "console.devToolsTitle": "コンソール", + "console.devToolsDescription": "コンソールでデータを操作するには、cURLをスキップして、JSONインターフェイスを使用します。", + "console.devToolsTitle": "Elasticsearch APIとの連携", "console.exampleOutputTextarea": "開発ツールコンソールエディターの例", "console.helpPage.keyboardCommands.autoIndentDescription": "現在のリクエストを自動インデントします", "console.helpPage.keyboardCommands.closeAutoCompleteMenuDescription": "自動入力メニューを閉じます", @@ -451,8 +455,70 @@ "core.fatalErrors.somethingWentWrongTitle": "何か問題が発生", "core.fatalErrors.tryRefreshingPageDescription": "ページを更新してみてください。うまくいかない場合は、前のページに戻るか、セッションデータを消去してください。", "core.notifications.errorToast.closeModal": "閉じる", + "core.notifications.globalToast.ariaLabel": "通知メッセージリスト", "core.notifications.unableUpdateUISettingNotificationMessageTitle": "UI 設定を更新できません", + "core.status.greenTitle": "緑", + "core.status.redTitle": "赤", + "core.status.yellowTitle": "黄", + "core.statusPage.loadStatus.serverIsDownErrorMessage": "サーバーステータスのリクエストに失敗しました。サーバーがダウンしている可能性があります。", + "core.statusPage.loadStatus.serverStatusCodeErrorMessage": "サーバーステータスのリクエストに失敗しました。ステータスコード:{responseStatus}", + "core.statusPage.metricsTiles.columns.heapTotalHeader": "ヒープ合計", + "core.statusPage.metricsTiles.columns.heapUsedHeader": "使用ヒープ", + "core.statusPage.metricsTiles.columns.loadHeader": "読み込み", + "core.statusPage.metricsTiles.columns.requestsPerSecHeader": "1秒あたりのリクエスト", + "core.statusPage.metricsTiles.columns.resTimeAvgHeader": "平均応答時間", + "core.statusPage.metricsTiles.columns.resTimeMaxHeader": "最長応答時間", + "core.statusPage.serverStatus.statusTitle": "Kibanaのステータス: {kibanaStatus}", + "core.statusPage.statusApp.loadingErrorText": "ステータスの読み込み中にエラーが発生しました", + "core.statusPage.statusApp.statusActions.buildText": "{buildNum}を作成", + "core.statusPage.statusApp.statusActions.commitText": "{buildSha}を確定", + "core.statusPage.statusApp.statusTitle": "プラグインステータス", + "core.statusPage.statusTable.columns.idHeader": "ID", + "core.statusPage.statusTable.columns.statusHeader": "ステータス", "core.toasts.errorToast.seeFullError": "完全なエラーを表示", + "core.ui_settings.params.darkModeText": "Kibana UIのダークモードを有効にします。この設定を適用するにはページの更新が必要です。", + "core.ui_settings.params.darkModeTitle": "ダークモード", + "core.ui_settings.params.dateFormat.dayOfWeekText": "週の初めの曜日を設定します", + "core.ui_settings.params.dateFormat.dayOfWeekTitle": "曜日", + "core.ui_settings.params.dateFormat.optionsLinkText": "フォーマット", + "core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "ISO8601間隔", + "core.ui_settings.params.dateFormat.scaledText": "時間ベースのデータが順番にレンダリングされ、フォーマットされたタイムスタンプが測定値の間隔に適応すべき状況で使用されるフォーマットを定義する値です。キーは{intervalsLink}です。", + "core.ui_settings.params.dateFormat.scaledTitle": "スケーリングされたデータフォーマットです", + "core.ui_settings.params.dateFormat.timezone.invalidValidationMessage": "無効なタイムゾーン:{timezone}", + "core.ui_settings.params.dateFormat.timezoneText": "使用されるタイムゾーンです。{defaultOption}ではご使用のブラウザーにより検知されたタイムゾーンが使用されます。", + "core.ui_settings.params.dateFormat.timezoneTitle": "データフォーマットのタイムゾーン", + "core.ui_settings.params.dateFormatText": "きちんとフォーマットされたデータを表示する際、この{formatLink}を使用します", + "core.ui_settings.params.dateFormatTitle": "データフォーマット", + "core.ui_settings.params.dateNanosFormatText": "Elasticsearchの{dateNanosLink}データタイプに使用されます", + "core.ui_settings.params.dateNanosFormatTitle": "ナノ秒フォーマットでの日付", + "core.ui_settings.params.dateNanosLinkTitle": "date_nanos", + "core.ui_settings.params.dayOfWeekText.invalidValidationMessage": "無効な曜日:{dayOfWeek}", + "core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "相対URLでなければなりません。", + "core.ui_settings.params.defaultRoute.defaultRouteText": "この設定は、Kibana起動時のデフォルトのルートを設定します。この設定で、Kibana起動時のランディングページを変更できます。ルートは相対URLでなければなりません。", + "core.ui_settings.params.defaultRoute.defaultRouteTitle": "デフォルトのルート", + "core.ui_settings.params.disableAnimationsText": "Kibana UIの不要なアニメーションをオフにします。変更を適用するにはページを更新してください。", + "core.ui_settings.params.disableAnimationsTitle": "アニメーションを無効にする", + "core.ui_settings.params.maxCellHeightText": "表のセルが使用する高さの上限です。この切り捨てを無効にするには0に設定します", + "core.ui_settings.params.maxCellHeightTitle": "表のセルの高さの上限", + "core.ui_settings.params.notifications.banner.markdownLinkText": "マークダウン対応", + "core.ui_settings.params.notifications.bannerLifetimeText": "バナー通知が画面に表示される時間(ミリ秒単位)です。{infinityValue}に設定すると、カウントダウンが無効になります。", + "core.ui_settings.params.notifications.bannerLifetimeTitle": "バナー通知時間", + "core.ui_settings.params.notifications.bannerText": "すべてのユーザーへの一時的な通知を目的としたカスタムバナーです。{markdownLink}", + "core.ui_settings.params.notifications.bannerTitle": "カスタムバナー通知", + "core.ui_settings.params.notifications.errorLifetimeText": "エラー通知が画面に表示される時間(ミリ秒単位)です。{infinityValue}に設定すると、無効になります。", + "core.ui_settings.params.notifications.errorLifetimeTitle": "エラー通知時間", + "core.ui_settings.params.notifications.infoLifetimeText": "情報通知が画面に表示される時間(ミリ秒単位)です。{infinityValue}に設定すると、無効になります。", + "core.ui_settings.params.notifications.infoLifetimeTitle": "情報通知時間", + "core.ui_settings.params.notifications.warningLifetimeText": "警告通知が画面に表示される時間(ミリ秒単位)です。{infinityValue}に設定すると、無効になります。", + "core.ui_settings.params.notifications.warningLifetimeTitle": "警告通知時間", + "core.ui_settings.params.pageNavigationDesc": "ナビゲーションのスタイルを変更", + "core.ui_settings.params.pageNavigationLegacy": "レガシー", + "core.ui_settings.params.pageNavigationModern": "モダン", + "core.ui_settings.params.pageNavigationName": "サイドナビゲーションスタイル", + "core.ui_settings.params.storeUrlText": "URLが長くなりすぎるためブラウザーが対応できない場合があります。セッションストレージにURLの一部を保存することでこの問題に対処できるかどうかをテストしています。結果を教えてください!", + "core.ui_settings.params.storeUrlTitle": "セッションストレージにURLを格納", + "core.ui_settings.params.themeVersionText": "現在のバージョンと次のバージョンのKibanaで使用されるテーマを切り替えます。この設定を適用するにはページの更新が必要です。", + "core.ui_settings.params.themeVersionTitle": "テーマバージョン", "core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "ホームページに移動", "core.ui.chrome.headerGlobalNav.helpMenuAskElasticTitle": "Elasticに確認する", "core.ui.chrome.headerGlobalNav.helpMenuButtonAriaLabel": "ヘルプメニュー", @@ -479,6 +545,7 @@ "core.ui.kibanaNavList.label": "Kibana", "core.ui.legacyBrowserMessage": "このElasticインストレーションは、現在ご使用のブラウザが満たしていない厳格なセキュリティ要件が有効になっています。", "core.ui.legacyBrowserTitle": "ブラウザをアップグレードしてください", + "core.ui.loadingIndicatorAriaLabel": "コンテンツを読み込んでいます", "core.ui.managementNavList.label": "管理", "core.ui.observabilityNavList.label": "オブザーバビリティ", "core.ui.overlays.banner.attentionTitle": "注意", @@ -498,11 +565,8 @@ "core.ui.securityNavList.label": "セキュリティ", "core.ui.welcomeErrorMessage": "Elasticが正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。", "core.ui.welcomeMessage": "Elasticの読み込み中", - "core.status.greenTitle": "緑", - "core.status.redTitle": "赤", - "core.status.yellowTitle": "黄色", "dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化", - "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全画面", + "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "パネルを最大化", "dashboard.addExistingVisualizationLinkText": "既存のユーザーを追加", "dashboard.addNewVisualizationText": "またはこのダッシュボードに新規オブジェクトを追加", "dashboard.addPanel.noMatchingObjectsMessage": "一致するオブジェクトが見つかりませんでした。", @@ -532,6 +596,7 @@ "dashboard.emptyDashboardTitle": "このダッシュボードは空です。", "dashboard.factory.displayName": "ダッシュボード", "dashboard.featureCatalogue.dashboardDescription": "ビジュアライゼーションと保存された検索のコレクションの表示と共有を行います。", + "dashboard.featureCatalogue.dashboardSubtitle": "ダッシュボードでデータを分析します。", "dashboard.featureCatalogue.dashboardTitle": "ダッシュボード", "dashboard.fillDashboardTitle": "このダッシュボードは空です。コンテンツを追加しましょう!", "dashboard.helpMenu.appName": "ダッシュボード", @@ -550,13 +615,17 @@ "dashboard.listing.table.entityName": "ダッシュボード", "dashboard.listing.table.entityNamePlural": "ダッシュボード", "dashboard.listing.table.titleColumnName": "タイトル", + "dashboard.panel.AddToLibrary": "ライブラリに追加", "dashboard.panel.clonedToast": "クローンパネル", "dashboard.panel.clonePanel": "パネルのクローン", "dashboard.panel.invalidData": "URLの無効なデータ", + "dashboard.panel.LibraryNotification": "ライブラリ", + "dashboard.panel.libraryNotification.toolTip": "このパネルはライブラリ項目にリンクされています。パネルを編集すると、他のダッシュボードに影響する場合があります。", "dashboard.panel.removePanel.replacePanel": "パネルの交換", "dashboard.panel.title.clonedTag": "コピー", "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません", "dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません:{key}", + "dashboard.panel.unlinkFromLibrary": "ライブラリ項目からのリンクを解除", "dashboard.placeholder.factory.displayName": "プレースホルダー", "dashboard.savedDashboard.newDashboardTitle": "新規ダッシュボード", "dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "このダッシュボードに時刻が保存されていないため、同期できません。", @@ -673,6 +742,8 @@ "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "時間フィルターのデフォルト更新間隔「値」はミリ秒で指定する必要があります。", "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "タイムピッカーの更新間隔", "data.advancedSettings.timepicker.thisWeek": "今週", + "data.advancedSettings.timepicker.timeDefaultsText": "時間フィルターが選択されずにKibanaが起動した際に使用される時間フィルターです", + "data.advancedSettings.timepicker.timeDefaultsTitle": "デフォルトのタイムピッカー", "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "data.common.kql.errors.endOfInputText": "インプットの終わり", @@ -799,11 +870,14 @@ "data.functions.indexPatternLoad.id.help": "読み込むインデックスパターンID", "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "data.indexPatterns.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title} (ID: {id})", + "data.indexPatterns.fetchFieldSaveErrorTitle": "インデックスパターンのフィールド取得後の保存中にエラーが発生 {title}(ID: {id})", "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", "data.noDataPopover.content": "この時間範囲にはデータが含まれていません表示するフィールドを増やし、グラフを作成するには、時間範囲を広げるか、調整してください。", "data.noDataPopover.dismissAction": "今後表示しない", "data.noDataPopover.subtitle": "ヒント", "data.noDataPopover.title": "空のデータセット", + "data.painlessError.buttonTxt": "スクリプトを編集", + "data.painlessError.painlessScriptedFieldErrorMessage": "Painlessスクリプトの実行エラー:「{script}」。", "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "無効なカレンダー間隔:{interval}、1よりも大きな値が必要です", "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "無効な間隔フォーマット:{interval}", "data.query.queryBar.comboboxAriaLabel": "{pageType} ページの検索とフィルタリング", @@ -896,6 +970,7 @@ "data.search.aggs.buckets.histogram.interval.help": "このアグリゲーションで使用する間隔", "data.search.aggs.buckets.histogram.intervalBase.help": "このアグリゲーションで使用するIntervalBase", "data.search.aggs.buckets.histogram.json.help": "アグリゲーションがElasticsearchに送信されるときに含める高度なJSON", + "data.search.aggs.buckets.histogram.maxBars.help": "間隔を計算して、この数の棒を取得します", "data.search.aggs.buckets.histogram.minDocCount.help": "このアグリゲーションでmin_doc_countを使用するかどうかを指定します", "data.search.aggs.buckets.histogram.schema.help": "このアグリゲーションで使用するスキーマ", "data.search.aggs.buckets.histogramTitle": "ヒストグラム", @@ -926,6 +1001,12 @@ "data.search.aggs.buckets.range.ranges.help": "このアグリゲーションで使用するシリアル化された範囲。", "data.search.aggs.buckets.range.schema.help": "このアグリゲーションで使用するスキーマ", "data.search.aggs.buckets.rangeTitle": "範囲", + "data.search.aggs.buckets.shardDelay.customLabel.help": "このアグリゲーションのカスタムラベルを表します", + "data.search.aggs.buckets.shardDelay.delay.help": "処理するシャード間の遅延(ミリ秒)。", + "data.search.aggs.buckets.shardDelay.enabled.help": "このアグリゲーションが有効かどうかを指定します", + "data.search.aggs.buckets.shardDelay.id.help": "このアグリゲーションのID", + "data.search.aggs.buckets.shardDelay.json.help": "アグリゲーションがElasticsearchに送信されるときに含める高度なJSON", + "data.search.aggs.buckets.shardDelay.schema.help": "このアグリゲーションで使用するスキーマ", "data.search.aggs.buckets.significantTerms.customLabel.help": "このアグリゲーションのカスタムラベルを表します", "data.search.aggs.buckets.significantTerms.enabled.help": "このアグリゲーションが有効かどうかを指定します", "data.search.aggs.buckets.significantTerms.exclude.help": "結果から除外する特定のバケット値", @@ -973,6 +1054,7 @@ "data.search.aggs.function.buckets.histogram.help": "ヒストグラムアグリゲーションのシリアル化されたアグリゲーション構成を生成します", "data.search.aggs.function.buckets.ipRange.help": "IP範囲アグリゲーションのシリアル化されたアグリゲーション構成を生成します", "data.search.aggs.function.buckets.range.help": "範囲アグリゲーションのシリアル化されたアグリゲーション構成を生成します", + "data.search.aggs.function.buckets.shardDelay.help": "シャード遅延アグリゲーションのシリアル化されたアグリゲーション構成を生成します", "data.search.aggs.function.buckets.significantTerms.help": "重要な用語アグリゲーションのシリアル化されたアグリゲーション構成を生成します", "data.search.aggs.function.buckets.terms.help": "用語アグリゲーションのシリアル化されたアグリゲーション構成を生成します", "data.search.aggs.function.metrics.avg.help": "平均値アグリゲーションのシリアル化されたアグリゲーション構成を生成します", @@ -1200,9 +1282,25 @@ "data.search.aggs.metrics.uniqueCountTitle": "ユニークカウント", "data.search.aggs.otherBucket.labelForMissingValuesLabel": "欠測値のラベル", "data.search.aggs.otherBucket.labelForOtherBucketLabel": "他のバケットのラベル", + "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "「{aggType}」アグリゲーションで使用するには、保存されたフィールド「{fieldParameter}」が無効です。新しいフィールドを選択してください。", "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} は必須パラメーターです", "data.search.aggs.percentageOfLabel": "{label} の割合", "data.search.aggs.string.customLabel": "カスタムラベル", + "data.search.dataRequest.title": "データ", + "data.search.es_search.dataRequest.description": "このリクエストはElasticsearchにクエリし、ビジュアライゼーション用のデータを取得します。", + "data.search.es_search.hitsDescription": "クエリにより返されたドキュメントの数です。", + "data.search.es_search.hitsLabel": "ヒット数", + "data.search.es_search.hitsTotalDescription": "クエリに一致するドキュメントの数です。", + "data.search.es_search.hitsTotalLabel": "ヒット数 (合計)", + "data.search.es_search.indexPatternDescription": "Elasticsearchインデックスに接続したインデックスパターンです。", + "data.search.es_search.indexPatternLabel": "インデックスパターン", + "data.search.es_search.queryTimeDescription": "クエリの処理の所要時間です。リクエストの送信やブラウザーでのパースの時間は含まれません。", + "data.search.es_search.queryTimeLabel": "クエリ時間", + "data.search.es_search.queryTimeValue": "{queryTime}ms", + "data.search.esdsl.help": "Elasticsearchリクエストを実行", + "data.search.esdsl.index.help": "クエリするElasticsearchインデックス", + "data.search.esdsl.q.help": "クエリDSL", + "data.search.esdsl.size.help": "Elasticsearch検索APIサイズパラメーター", "data.search.searchBar.savedQueryDescriptionLabelText": "説明", "data.search.searchBar.savedQueryDescriptionText": "再度使用するクエリテキストとフィルターを保存します。", "data.search.searchBar.savedQueryForm.titleConflictText": "タイトルが既に保存されているクエリに使用されています", @@ -1261,7 +1359,12 @@ "data.search.searchSource.requestTimeDescription": "ブラウザから Elasticsearch にリクエストが送信され返されるまでの所要時間です。リクエストがキューで待機していた時間は含まれません。", "data.search.searchSource.requestTimeLabel": "リクエスト時間", "data.search.searchSource.requestTimeValue": "{requestTime}ms", + "data.search.timeoutContactAdmin": "クエリがタイムアウトしました。実行時間を延長するには、システム管理者に問い合わせてください。", + "data.search.timeoutIncreaseSetting": "クエリがタイムアウトしました。検索タイムアウト詳細設定で実行時間を延長します。", + "data.search.timeoutIncreaseSettingActionText": "設定を編集", "data.search.unableToGetSavedQueryToastTitle": "保存したクエリ {savedQueryId} を読み込めません", + "data.search.upgradeLicense": "クエリがタイムアウトしました。無料のベーシックティアではクエリがタイムアウトすることはありません。", + "data.search.upgradeLicenseActionText": "今すぐアップグレード", "devTools.badge.readOnly.text": "読み込み専用", "devTools.badge.readOnly.tooltip": "を保存できませんでした", "devTools.devToolsTitle": "開発ツール", @@ -1277,6 +1380,8 @@ "discover.advancedSettings.context.tieBreakerFieldsTitle": "タイブレーカーフィールド", "discover.advancedSettings.defaultColumnsText": "デフォルトでディスカバリタブに表示される列です", "discover.advancedSettings.defaultColumnsTitle": "デフォルトの列", + "discover.advancedSettings.discover.modifyColumnsOnSwitchText": "新しいインデックスパターンで使用できない列を削除します。", + "discover.advancedSettings.discover.modifyColumnsOnSwitchTitle": "インデックスパターンを変更するときに列を修正", "discover.advancedSettings.docTableHideTimeColumnText": "ディスカバリと、ダッシュボードのすべての保存された検索で、「時刻」列を非表示にします。", "discover.advancedSettings.docTableHideTimeColumnTitle": "「時刻」列を非表示", "discover.advancedSettings.fieldsPopularLimitText": "最も頻繁に使用されるフィールドのトップNを表示します", @@ -1313,6 +1418,7 @@ "discover.context.unableToLoadDocumentDescription": "ドキュメントを読み込めません", "discover.discoverBreadcrumbTitle": "発見", "discover.discoverDescription": "ドキュメントにクエリをかけたりフィルターを適用することで、データをインタラクティブに閲覧できます。", + "discover.discoverSubtitle": "インサイトを検索して見つけます。", "discover.discoverTitle": "発見", "discover.doc.couldNotFindDocumentsDescription": "そのIDに一致するドキュメントがありません。", "discover.doc.failedToExecuteQueryDescription": "検索の実行に失敗しました", @@ -1367,12 +1473,16 @@ "discover.embeddable.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。", "discover.embeddable.search.displayName": "検索", "discover.fieldChooser.detailViews.emptyStringText": "空の文字列", + "discover.fieldChooser.detailViews.existsText": "存在する", "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"", "discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "{field}を除外:\"{value}\"", "discover.fieldChooser.detailViews.recordsText": "記録", "discover.fieldChooser.detailViews.visualizeLinkText": "可視化", "discover.fieldChooser.discoverField.addButtonAriaLabel": "{field}を表に追加", + "discover.fieldChooser.discoverField.addFieldTooltip": "フィールドを列として追加", + "discover.fieldChooser.discoverField.fieldTopValuesLabel": "トップ5の値", "discover.fieldChooser.discoverField.removeButtonAriaLabel": "{field}を表から削除", + "discover.fieldChooser.discoverField.removeFieldTooltip": "フィールドを表から削除", "discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription": "スクリプトフィールドは実行に時間がかかる場合があります。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "ジオフィールドは分析できません。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "オブジェクトフィールドは分析できません。", @@ -1411,7 +1521,7 @@ "discover.histogramOfFoundDocumentsAriaLabel": "検出されたドキュメントのヒストグラム", "discover.hitsPluralTitle": "{hits, plural, one {ヒット} other {ヒット}}", "discover.howToChangeTheTimeTooltip": "時刻を変更するには、上記のグローバル時刻フィルターを使用します。", - "discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの{sampleSize}件のドキュメントです。他の結果を表示するには検索条件を絞ってください。 ", + "discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの{sampleSize}件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", "discover.inspectorRequestDataTitle": "データ", "discover.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。", "discover.localMenu.inspectTitle": "検査", @@ -1481,6 +1591,7 @@ "embeddableApi.panel.labelError": "エラー", "embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabel": "パネルオプション", "embeddableApi.panel.optionsMenu.panelOptionsButtonEnhancedAriaLabel": "{title} のパネルオプション", + "embeddableApi.panel.placeholderTitle": "[タイトルなし]", "embeddableApi.panel.removePanel.displayName": "ダッシュボードから削除", "embeddableApi.samples.contactCard.displayName": "連絡先カード", "embeddableApi.samples.filterableContainer.displayName": "フィルター可能なダッシュボード", @@ -1561,12 +1672,17 @@ "expressions.functions.kibana_context.savedSearchId.help": "クエリとフィルターに使用する保存検索ID を指定します。", "expressions.functions.kibana_context.timeRange.help": "Kibana 時間範囲フィルターを指定します", "expressions.functions.kibana.help": "Kibana グローバルコンテキストを取得します", - "expressions.functions.var.help": "Kibana グローバルコンテキストを更新", - "expressions.functions.var.name.help": "変数の名前を指定", - "expressions.functions.varset.help": "Kibana グローバルコンテキストを更新", - "expressions.functions.varset.name.help": "変数の名前を指定", - "expressions.functions.varset.val.help": "変数の値を指定指定がない場合、インプットコンテキストが使用されます", + "expressions.functions.theme.args.defaultHelpText": "テーマ情報がない場合のデフォルト値。", + "expressions.functions.theme.args.variableHelpText": "読み取るテーマ変数名。", + "expressions.functions.themeHelpText": "テーマ設定を読み取ります。", + "expressions.functions.var.help": "Kibanaグローバルコンテキストを更新します。", + "expressions.functions.var.name.help": "変数の名前を指定します。", + "expressions.functions.varset.help": "Kibanaグローバルコンテキストを更新します。", + "expressions.functions.varset.name.help": "変数の名前を指定します。", + "expressions.functions.varset.val.help": "変数の値を指定します。指定しないと、入力コンテキストが使用されます。", "expressions.types.number.fromStringConversionErrorMessage": "\"{string}\" ストリンクを数字に変換できません", + "home.addData.sampleDataButtonLabel": "サンプルデータを試す", + "home.addData.sectionTitle": "データを取り込む", "home.breadcrumbs.addDataTitle": "データの追加", "home.breadcrumbs.homeTitle": "ホーム", "home.dataManagementDisableCollection": " 収集を停止するには、] ", @@ -1582,10 +1698,12 @@ "home.directory.tabs.otherTitle": "その他", "home.exploreButtonLabel": "独りで閲覧", "home.exploreYourDataDescription": "すべてのステップを終えたら、データ閲覧準備の完了です。", - "home.letsStartDescription": "クラスターにデータがありません。サンプルデータやダッシュボードで試すこともできますし、いきなり独自のデータを使用することもできます。", - "home.letsStartTitle": "始めましょう", + "home.header.title": "ホーム", + "home.letsStartDescription": "任意のソースからクラスターにデータを追加して、リアルタイムでデータを分析して可視化します。当社のソリューションを使用すれば、どこからでも検索を追加し、エコシステムを監視して、セキュリティの脅威から保護することができます。", + "home.letsStartTitle": "データを追加して開始する", "home.loadTutorials.requestFailedErrorMessage": "リクエスト失敗、ステータスコード: {status}", "home.loadTutorials.unableToLoadErrorMessage": "チュートリアルが読み込めません。", + "home.manageData.sectionTitle": "データを管理", "home.pageTitle": "ホーム", "home.recentlyAccessed.recentlyViewedTitle": "最近閲覧", "home.sampleData.ecommerceSpec.averageSalesPriceTitle": "[e コマース] 平均販売価格", @@ -1598,6 +1716,7 @@ "home.sampleData.ecommerceSpec.revenueDashboardTitle": "[e コマース] 収益ダッシュボード", "home.sampleData.ecommerceSpec.salesByCategoryTitle": "[e コマース] カテゴリーごとの売上", "home.sampleData.ecommerceSpec.salesByGenderTitle": "[e コマース] 性別ごとの売上", + "home.sampleData.ecommerceSpec.salesCountMapTitle": "[eコマース] 売上カウントマップ", "home.sampleData.ecommerceSpec.soldProductsPerDayTitle": "[e コマース] 1 日の販売製品", "home.sampleData.ecommerceSpec.topSellingProductsTitle": "[e コマース] トップセラー製品", "home.sampleData.ecommerceSpec.totalRevenueTitle": "[e コマース] 合計収益", @@ -1610,6 +1729,7 @@ "home.sampleData.flightsSpec.delayBucketsTitle": "[フライト] 遅延バケット", "home.sampleData.flightsSpec.delaysAndCancellationsTitle": "[フライト] 遅延・欠航", "home.sampleData.flightsSpec.delayTypeTitle": "[フライト] 遅延タイプ", + "home.sampleData.flightsSpec.departuresCountMapTitle": "[フライト] 出発カウントマップ", "home.sampleData.flightsSpec.destinationWeatherTitle": "[フライト] 目的地の天候", "home.sampleData.flightsSpec.flightCancellationsTitle": "[フライト] フライト欠航", "home.sampleData.flightsSpec.flightCountAndAverageTicketPriceTitle": "[フライト] カウントと平均運賃", @@ -1634,6 +1754,7 @@ "home.sampleData.logsSpec.sourceAndDestinationSankeyChartTitle": "[ログ] ソースと行先のサンキーダイアグラム", "home.sampleData.logsSpec.uniqueVisitorsTitle": "[ログ] ユニークビジターと平均バイトの比較", "home.sampleData.logsSpec.visitorOSTitle": "[ログ] OS 別のビジター", + "home.sampleData.logsSpec.visitorsMapTitle": "[ログ] ビジターマップ", "home.sampleData.logsSpec.webTrafficDescription": "Elastic Web サイトのサンプル Webトラフィックログデータを分析します", "home.sampleData.logsSpec.webTrafficTitle": "[ログ] Web トラフィック", "home.sampleData.logsSpecDescription": "Web ログを監視するサンプルデータ、ビジュアライゼーション、ダッシュボードです。", @@ -1657,7 +1778,8 @@ "home.sampleDataSetCard.removingButtonLabel": "削除中", "home.sampleDataSetCard.viewDataButtonAriaLabel": "{datasetName} を表示", "home.sampleDataSetCard.viewDataButtonLabel": "データを表示", - "home.tryButtonLabel": "サンプルデータを試す", + "home.solutionsSection.sectionTitle": "ソリューションを選択", + "home.tryButtonLabel": "データの追加", "home.tutorial.addDataToKibanaTitle": "データの追加", "home.tutorial.card.sampleDataDescription": "これらの「ワンクリック」データセットで Kibana の探索を始めましょう。", "home.tutorial.card.sampleDataTitle": "サンプルデータ", @@ -1689,6 +1811,8 @@ "home.tutorial.tabs.securitySolutionTitle": "セキュリティ", "home.tutorial.unexpectedStatusCheckStateErrorDescription": "予期せぬステータス確認ステータス {statusCheckState}", "home.tutorial.unhandledInstructionTypeErrorDescription": "予期せぬ指示タイプ {visibleInstructions}", + "home.tutorialDirectory.featureCatalogueDescription": "一般的なアプリやサービスからデータを取り込みます。", + "home.tutorialDirectory.featureCatalogueTitle": "データの追加", "home.tutorials.activemqLogs.artifacts.dashboards.linkLabel": "ActiveMQ アプリケーションイベント", "home.tutorials.activemqLogs.longDescription": "Filebeat で ActiveMQ ログを収集します。[詳細]({learnMoreLink})", "home.tutorials.activemqLogs.nameTitle": "ActiveMQ ログ", @@ -1768,15 +1892,15 @@ "home.tutorials.common.auditbeatInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.auditbeatInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.auditbeatInstructions.install.debTextPost": "32 ビットパッケージをお探しですか?[ダウンロードページ]({linkUrl}) をご覧ください。", - "home.tutorials.common.auditbeatInstructions.install.debTextPre": "Auditbeat は初めてですか?[入門ガイド]({linkUrl}) をご覧ください。", + "home.tutorials.common.auditbeatInstructions.install.debTextPre": "Auditbeatは初めてですか?[クイックスタート]({linkUrl})を参照してください。", "home.tutorials.common.auditbeatInstructions.install.debTitle": "Auditbeat のダウンロードとインストール", - "home.tutorials.common.auditbeatInstructions.install.osxTextPre": "Auditbeat は初めてですか?[入門ガイド]({linkUrl}) をご覧ください。", + "home.tutorials.common.auditbeatInstructions.install.osxTextPre": "Auditbeatは初めてですか?[クイックスタート]({linkUrl})を参照してください。", "home.tutorials.common.auditbeatInstructions.install.osxTitle": "Auditbeat のダウンロードとインストール", "home.tutorials.common.auditbeatInstructions.install.rpmTextPost": "32 ビットパッケージをお探しですか?[ダウンロードページ]({linkUrl}) をご覧ください。", - "home.tutorials.common.auditbeatInstructions.install.rpmTextPre": "Auditbeat は初めてですか?[入門ガイド]({linkUrl}) をご覧ください。", + "home.tutorials.common.auditbeatInstructions.install.rpmTextPre": "Auditbeatは初めてですか?[クイックスタート]({linkUrl})を参照してください。", "home.tutorials.common.auditbeatInstructions.install.rpmTitle": "Auditbeat のダウンロードとインストール", "home.tutorials.common.auditbeatInstructions.install.windowsTextPost": "{auditbeatPath} ファイルの {propertyName} を Elasticsearch のインストールに設定します。", - "home.tutorials.common.auditbeatInstructions.install.windowsTextPre": "Auditbeat は初めてですか?[入門ガイド]({guideLinkUrl}) をご覧ください。\n 1.[ダウンロード]({auditbeatLinkUrl}) ページから Auditbeat Windows zip ファイルをダウンロードします。\n 2.zip ファイルのコンテンツを {folderPath} に解凍します。\n 3.「{directoryName}」ディレクトリの名前を「Auditbeat」に変更します。\n 4.管理者として PowerShell プロンプトを開きます (PowerShell アイコンを右クリックして「管理者として実行」を選択します)。Windows XP をご使用の場合、PowerShell のダウンロードとインストールが必要な場合があります。\n 5.PowerShell プロンプトで次のコマンドを実行し、Auditbeat を Windows サービスとしてインストールします。", + "home.tutorials.common.auditbeatInstructions.install.windowsTextPre": "Auditbeatは初めてですか?[クイックスタート]({guideLinkUrl})を参照してください。\n 1.[ダウンロード]({auditbeatLinkUrl})ページからAuditbeat Windows zipファイルをダウンロードします。\n 2.zipファイルのコンテンツを{folderPath}に解凍します。\n 3.「{directoryName}」ディレクトリの名前を「Auditbeat」に変更します。\n 4.管理者としてPowerShellプロンプトを開きます (PowerShellアイコンを右クリックして「管理者として実行」を選択します)。Windows XPをご使用の場合、PowerShellのダウンロードとインストールが必要な場合があります。\n 5.PowerShellプロンプトで次のコマンドを実行し、AuditbeatをWindowsサービスとしてインストールします。", "home.tutorials.common.auditbeatInstructions.install.windowsTitle": "Auditbeat のダウンロードとインストール", "home.tutorials.common.auditbeatInstructions.start.debTextPre": "「setup」コマンドで Kibana のダッシュボードを読み込みます。ダッシュボードが既にセットアップされている場合、このコマンドは省略します。", "home.tutorials.common.auditbeatInstructions.start.debTitle": "Auditbeat を起動", @@ -1826,15 +1950,15 @@ "home.tutorials.common.filebeatInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.filebeatInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.filebeatInstructions.install.debTextPost": "32 ビットパッケージをお探しですか?[ダウンロードページ]({linkUrl}) をご覧ください。", - "home.tutorials.common.filebeatInstructions.install.debTextPre": "Filebeat は初めてですか?[入門ガイド]({linkUrl}) をご覧ください。", + "home.tutorials.common.filebeatInstructions.install.debTextPre": "Filebeatは初めてですか?[クイックスタート]({linkUrl})を参照してください。", "home.tutorials.common.filebeatInstructions.install.debTitle": "Filebeat のダウンロードとインストール", - "home.tutorials.common.filebeatInstructions.install.osxTextPre": "Filebeat は初めてですか?[入門ガイド]({linkUrl}) をご覧ください。", + "home.tutorials.common.filebeatInstructions.install.osxTextPre": "Filebeatは初めてですか?[クイックスタート]({linkUrl})を参照してください。", "home.tutorials.common.filebeatInstructions.install.osxTitle": "Filebeat のダウンロードとインストール", "home.tutorials.common.filebeatInstructions.install.rpmTextPost": "32 ビットパッケージをお探しですか?[ダウンロードページ]({linkUrl}) をご覧ください。", - "home.tutorials.common.filebeatInstructions.install.rpmTextPre": "Filebeat は初めてですか?[入門ガイド]({linkUrl}) をご覧ください。", + "home.tutorials.common.filebeatInstructions.install.rpmTextPre": "Filebeatは初めてですか?[クイックスタート]({linkUrl})を参照してください。", "home.tutorials.common.filebeatInstructions.install.rpmTitle": "Filebeat のダウンロードとインストール", "home.tutorials.common.filebeatInstructions.install.windowsTextPost": "{filebeatPath} ファイルの {propertyName} を Elasticsearch のインストールに設定します。", - "home.tutorials.common.filebeatInstructions.install.windowsTextPre": "Filebeat は初めてですか?[入門ガイド]({guideLinkUrl}) をご覧ください。\n 1.[ダウンロード]({filebeatLinkUrl}) ページから Auditbeat Windows zip ファイルをダウンロードします。\n 2.zip ファイルのコンテンツを {folderPath} に解凍します。\n 3.「{directoryName}」ディレクトリの名前を「Filebeat」に変更します。\n 4.管理者として PowerShell プロンプトを開きます (PowerShell アイコンを右クリックして「管理者として実行」を選択します)。Windows XP をご使用の場合、PowerShell のダウンロードとインストールが必要な場合があります。\n 5.PowerShell プロンプトで次のコマンドを実行し、Filebeat を Windows サービスとしてインストールします。", + "home.tutorials.common.filebeatInstructions.install.windowsTextPre": "Filebeatは初めてですか?[クイックスタート]({guideLinkUrl})を参照してください。\n 1.[ダウンロード]({filebeatLinkUrl})ページからAuditbeat Windows zipファイルをダウンロードします。\n 2.zipファイルのコンテンツを{folderPath}に解凍します。\n 3.「{directoryName}」ディレクトリの名前を「Filebeat」に変更します。\n 4.管理者としてPowerShellプロンプトを開きます (PowerShellアイコンを右クリックして「管理者として実行」を選択します)。Windows XPをご使用の場合、PowerShellのダウンロードとインストールが必要な場合があります。\n 5.PowerShellプロンプトで次のコマンドを実行し、FilebeatをWindowsサービスとしてインストールします。", "home.tutorials.common.filebeatInstructions.install.windowsTitle": "Filebeat のダウンロードとインストール", "home.tutorials.common.filebeatInstructions.start.debTextPre": "「setup」コマンドで Kibana のダッシュボードを読み込みます。ダッシュボードが既にセットアップされている場合、このコマンドは省略します。", "home.tutorials.common.filebeatInstructions.start.debTitle": "Filebeat を起動します", @@ -1873,11 +1997,11 @@ "home.tutorials.common.functionbeatInstructions.deploy.osxTitle": "Functionbeat を AWS Lambda にデプロイ", "home.tutorials.common.functionbeatInstructions.deploy.windowsTextPre": "これにより Functionbeat が Lambda 関数としてインストールされます「setup」コマンドで Elasticsearch の構成を確認し、Kibana インデックスパターンを読み込みます。通常このコマンドを省いても大丈夫です。", "home.tutorials.common.functionbeatInstructions.deploy.windowsTitle": "Functionbeat を AWS Lambda にデプロイ", - "home.tutorials.common.functionbeatInstructions.install.linuxTextPre": "Functionbeat は初めてですか?[入門ガイド]({link}) をご覧ください。", + "home.tutorials.common.functionbeatInstructions.install.linuxTextPre": "Functionbeatは初めてですか?[クイックスタート]({link})を参照してください。", "home.tutorials.common.functionbeatInstructions.install.linuxTitle": "Functionbeat のダウンロードとインストール", - "home.tutorials.common.functionbeatInstructions.install.osxTextPre": "Functionbeat は初めてですか?[入門ガイド]({link}) をご覧ください。", + "home.tutorials.common.functionbeatInstructions.install.osxTextPre": "Functionbeatは初めてですか?[クイックスタート]({link})を参照してください。", "home.tutorials.common.functionbeatInstructions.install.osxTitle": "Functionbeat のダウンロードとインストール", - "home.tutorials.common.functionbeatInstructions.install.windowsTextPre": "Functionbeat は初めてですか?[入門ガイド]({functionbeatLink}) をご覧ください。\n 1.[ダウンロード]({elasticLink}) ページから Functionbeat Windows zip ファイルをダウンロードします。\n 2.zip ファイルのコンテンツを {folderPath} に解凍します。\n 3.「{directoryName} ディレクトリの名前を「Functionbeat」に変更します。\n 4.管理者として PowerShell プロンプトを開きます (PowerShell アイコンを右クリックして「管理者として実行」を選択します)。Windows XP をご使用の場合、PowerShell のダウンロードとインストールが必要な場合があります。\n 5.PowerShell プロンプトから、Functionbeat ディレクトリに移動します:", + "home.tutorials.common.functionbeatInstructions.install.windowsTextPre": "Functionbeatは初めてですか?[クイックスタート]({functionbeatLink})を参照してください。\n 1.[ダウンロード]({elasticLink})ページからFunctionbeat Windows zipファイルをダウンロードします。\n 2.zipファイルのコンテンツを{folderPath}に解凍します。\n 3.「{directoryName} ディレクトリの名前を「Functionbeat」に変更します。\n 4.管理者としてPowerShellプロンプトを開きます (PowerShellアイコンを右クリックして「管理者として実行」を選択します)。Windows XPをご使用の場合、PowerShellのダウンロードとインストールが必要な場合があります。\n 5.PowerShellプロンプトから、Functionbeatディレクトリに移動します:", "home.tutorials.common.functionbeatInstructions.install.windowsTitle": "Functionbeat のダウンロードとインストール", "home.tutorials.common.functionbeatStatusCheck.buttonLabel": "データを確認してください", "home.tutorials.common.functionbeatStatusCheck.errorText": "Functionbeat からまだデータを受け取っていません", @@ -1920,13 +2044,13 @@ "home.tutorials.common.heartbeatInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.heartbeatInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.heartbeatInstructions.install.debTextPost": "32 ビットパッケージをお探しですか?[ダウンロードページ]({link}) をご覧ください。", - "home.tutorials.common.heartbeatInstructions.install.debTextPre": "Heartbeat は初めてですか?[入門ガイド]({link}) をご覧ください。", + "home.tutorials.common.heartbeatInstructions.install.debTextPre": "Heartbeatは初めてですか?[クイックスタート]({link})を参照してください。", "home.tutorials.common.heartbeatInstructions.install.debTitle": "Heartbeat のダウンロードとインストール", - "home.tutorials.common.heartbeatInstructions.install.osxTextPre": "Heartbeat は初めてですか?[入門ガイド]({link}) をご覧ください。", + "home.tutorials.common.heartbeatInstructions.install.osxTextPre": "Heartbeatは初めてですか?[クイックスタート]({link})を参照してください。", "home.tutorials.common.heartbeatInstructions.install.osxTitle": "Heartbeat のダウンロードとインストール", - "home.tutorials.common.heartbeatInstructions.install.rpmTextPre": "Heartbeat は初めてですか?[入門ガイド]({link}) をご覧ください。", + "home.tutorials.common.heartbeatInstructions.install.rpmTextPre": "Heartbeatは初めてですか?[クイックスタート]({link})を参照してください。", "home.tutorials.common.heartbeatInstructions.install.rpmTitle": "Heartbeat のダウンロードとインストール", - "home.tutorials.common.heartbeatInstructions.install.windowsTextPre": "Heartbeat は初めてですか?[入門ガイド]({heartbeatLink}) をご覧ください。\n 1.[ダウンロード]({elasticLink}) ページから Heartbeat Windows zip ファイルをダウンロードします。\n 2.zip ファイルのコンテンツを {folderPath} に解凍します。\n 3.「{directoryName} ディレクトリの名前を「Heartbeat」に変更します。\n 4.管理者として PowerShell プロンプトを開きます (PowerShell アイコンを右クリックして「管理者として実行」を選択します)。Windows XP をご使用の場合、PowerShell のダウンロードとインストールが必要な場合があります。\n 5.PowerShell プロンプトで次のコマンドを実行し、Heartbeat を Windows サービスとしてインストールします。", + "home.tutorials.common.heartbeatInstructions.install.windowsTextPre": "Heartbeatは初めてですか?[クイックスタート]({heartbeatLink})を参照してください。\n 1.[ダウンロード]({elasticLink})ページからHeartbeat Windows zipファイルをダウンロードします。\n 2.zipファイルのコンテンツを{folderPath}に解凍します。\n 3.「{directoryName} ディレクトリの名前を「Heartbeat」に変更します。\n 4.管理者としてPowerShellプロンプトを開きます (PowerShellアイコンを右クリックして「管理者として実行」を選択します)。Windows XPをご使用の場合、PowerShellのダウンロードとインストールが必要な場合があります。\n 5.PowerShellプロンプトで次のコマンドを実行し、HeartbeatをWindowsサービスとしてインストールします。", "home.tutorials.common.heartbeatInstructions.install.windowsTitle": "Heartbeat のダウンロードとインストール", "home.tutorials.common.heartbeatInstructions.start.debTextPre": "「setup」コマンドで Kibana のインデックスパターンを読み込みます。", "home.tutorials.common.heartbeatInstructions.start.debTitle": "Heartbeat を起動します", @@ -1983,14 +2107,14 @@ "home.tutorials.common.metricbeatInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.metricbeatInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.metricbeatInstructions.install.debTextPost": "32 ビットパッケージをお探しですか?[ダウンロードページ]({link}) をご覧ください。", - "home.tutorials.common.metricbeatInstructions.install.debTextPre": "Metricbeat は初めてですか?[入門ガイド]({link}) をご覧ください。", + "home.tutorials.common.metricbeatInstructions.install.debTextPre": "Metricbeatは初めてですか?[クイックスタート]({link})を参照してください。", "home.tutorials.common.metricbeatInstructions.install.debTitle": "Metricbeat のダウンロードとインストール", - "home.tutorials.common.metricbeatInstructions.install.osxTextPre": "Metricbeat は初めてですか?[入門ガイド]({link}) をご覧ください。", + "home.tutorials.common.metricbeatInstructions.install.osxTextPre": "Metricbeatは初めてですか?[クイックスタート]({link})を参照してください。", "home.tutorials.common.metricbeatInstructions.install.osxTitle": "Metricbeat のダウンロードとインストール", - "home.tutorials.common.metricbeatInstructions.install.rpmTextPre": "Metricbeat は初めてですか?[入門ガイド]({link}) をご覧ください。", + "home.tutorials.common.metricbeatInstructions.install.rpmTextPre": "Metricbeatは初めてですか?[クイックスタート]({link})を参照してください。", "home.tutorials.common.metricbeatInstructions.install.rpmTitle": "Metricbeat のダウンロードとインストール", "home.tutorials.common.metricbeatInstructions.install.windowsTextPost": "{path} ファイルの「output.elasticsearch」を Elasticsearch のインストールに設定します。", - "home.tutorials.common.metricbeatInstructions.install.windowsTextPre": "Metricbeat は初めてですか?[入門ガイド]({metricbeatLink}) をご覧ください。\n 1.[ダウンロード]({elasticLink}) ページから Metricbeat Windows zip ファイルをダウンロードします。\n 2.zip ファイルのコンテンツを {folderPath} に解凍します。\n 3.「{directoryName} ディレクトリの名前を「Metricbeat」に変更します。\n 4.管理者として PowerShell プロンプトを開きます (PowerShell アイコンを右クリックして「管理者として実行」を選択します)。Windows XP をご使用の場合、PowerShell のダウンロードとインストールが必要な場合があります。\n 5.PowerShell プロンプトで次のコマンドを実行し、Metricbeat を Windows サービスとしてインストールします。", + "home.tutorials.common.metricbeatInstructions.install.windowsTextPre": "Metricbeatは初めてですか?[クイックスタート]({metricbeatLink})を参照してください。\n 1.[ダウンロード]({elasticLink})ページからMetricbeat Windows zipファイルをダウンロードします。\n 2.zipファイルのコンテンツを{folderPath}に解凍します。\n 3.{directoryName}ディレクトリの名前を「Metricbeat」に変更します。\n 4.管理者としてPowerShellプロンプトを開きます (PowerShellアイコンを右クリックして「管理者として実行」を選択します)。Windows XPをご使用の場合、PowerShellのダウンロードとインストールが必要な場合があります。\n 5.PowerShellプロンプトで次のコマンドを実行し、MetricbeatをWindowsサービスとしてインストールします。", "home.tutorials.common.metricbeatInstructions.install.windowsTitle": "Metricbeat のダウンロードとインストール", "home.tutorials.common.metricbeatInstructions.start.debTextPre": "「setup」コマンドで Kibana のダッシュボードを読み込みます。ダッシュボードが既にセットアップされている場合、このコマンドは省略します。", "home.tutorials.common.metricbeatInstructions.start.debTitle": "Metricbeat を起動します", @@ -2018,7 +2142,7 @@ "home.tutorials.common.winlogbeatInstructions.config.windowsTextPre": "{path} を変更して Elastic Cloud への接続情報を設定します:", "home.tutorials.common.winlogbeatInstructions.config.windowsTitle": "構成を編集する", "home.tutorials.common.winlogbeatInstructions.install.windowsTextPost": "{path} ファイルの「output.elasticsearch」を Elasticsearch のインストールに設定します。", - "home.tutorials.common.winlogbeatInstructions.install.windowsTextPre": "Winlogbeat は初めてですか?[入門ガイド]({winlogbeatLink}) をご覧ください。\n 1.[ダウンロード]({elasticLink}) ページから Winlogbeat Windows zip ファイルをダウンロードします。\n 2.zip ファイルのコンテンツを {folderPath} に解凍します。\n 3.「{directoryName} ディレクトリの名前を「Winlogbeat」に変更します。\n 4.管理者として PowerShell プロンプトを開きます (PowerShell アイコンを右クリックして「管理者として実行」を選択します)。Windows XP をご使用の場合、PowerShell のダウンロードとインストールが必要な場合があります。\n 5.PowerShell プロンプトで次のコマンドを実行し、Winlogbeat を Windows サービスとしてインストールします。", + "home.tutorials.common.winlogbeatInstructions.install.windowsTextPre": "Winlogbeatは初めてですか?[クイックスタート]({winlogbeatLink})を参照してください。\n 1.[ダウンロード]({elasticLink})ページからWinlogbeat Windows zipファイルをダウンロードします。\n 2.zipファイルのコンテンツを{folderPath}に解凍します。\n 3.{directoryName}ディレクトリの名前を「Winlogbeat」に変更します。\n 4.管理者としてPowerShellプロンプトを開きます (PowerShellアイコンを右クリックして「管理者として実行」を選択します)。Windows XPをご使用の場合、PowerShellのダウンロードとインストールが必要な場合があります。\n 5.PowerShellプロンプトで次のコマンドを実行し、WinlogbeatをWindowsサービスとしてインストールします。", "home.tutorials.common.winlogbeatInstructions.install.windowsTitle": "Winlogbeat のダウンロードとインストール", "home.tutorials.common.winlogbeatInstructions.start.windowsTextPre": "「setup」コマンドで Kibana のダッシュボードを読み込みます。ダッシュボードが既にセットアップされている場合、このコマンドは省略します。", "home.tutorials.common.winlogbeatInstructions.start.windowsTitle": "Winlogbeat を起動", @@ -2284,7 +2408,21 @@ "indexPatternManagement.createIndexPattern.betaLabel": "ベータ", "indexPatternManagement.createIndexPattern.description": "インデックスパターンは、{single}または{multiple}データソース、{star}と一致します。", "indexPatternManagement.createIndexPattern.documentation": "ドキュメンテーションを表示", + "indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription": "この機能にはベーシックライセンスが必要です。", + "indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel": "基本", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "新規データを確認", + "indexPatternManagement.createIndexPattern.emptyState.createAnyway": "一部のインデックスは表示されない場合があります。{link}してください。", + "indexPatternManagement.createIndexPattern.emptyState.createAnywayLink": "インデックスパターンを作成します", + "indexPatternManagement.createIndexPattern.emptyState.haveData": "すでにデータがある場合", + "indexPatternManagement.createIndexPattern.emptyState.integrationCardDescription": "さまざまなソースからデータを追加します。", + "indexPatternManagement.createIndexPattern.emptyState.integrationCardTitle": "統合の追加", + "indexPatternManagement.createIndexPattern.emptyState.learnMore": "詳細について", + "indexPatternManagement.createIndexPattern.emptyState.noDataTitle": "Kibanaを試しますか?まずデータが必要です。", + "indexPatternManagement.createIndexPattern.emptyState.readDocs": "ドキュメンテーションを表示", + "indexPatternManagement.createIndexPattern.emptyState.sampleDataCardDescription": "データセットとKibanaダッシュボードを読み込みます。", + "indexPatternManagement.createIndexPattern.emptyState.sampleDataCardTitle": "サンプルデータの追加", + "indexPatternManagement.createIndexPattern.emptyState.uploadCardDescription": "CSV、NDJSON、またはログファイルをインポートします。", + "indexPatternManagement.createIndexPattern.emptyState.uploadCardTitle": "ファイルをアップロード", "indexPatternManagement.createIndexPattern.includeSystemIndicesToggleSwitchLabel": "システムと非表示のインデックスを含める", "indexPatternManagement.createIndexPattern.loadClustersFailMsg": "リモートクラスターの読み込みに失敗", "indexPatternManagement.createIndexPattern.loadIndicesFailMsg": "インデックスの読み込みに失敗", @@ -2306,7 +2444,7 @@ "indexPatternManagement.createIndexPattern.step.status.partialMatchLabel.strongIndicesLabel": "{matchedIndicesLength, plural, one {インデックス} other {# インデックス} }", "indexPatternManagement.createIndexPattern.step.status.successLabel.successDetail": "インデックスパターンは、{sourceCount} {sourceCount, plural, one {個のソース} other {個のソース} }と一致します。", "indexPatternManagement.createIndexPattern.step.warningHeader": "すでに{query}という名前のインデックスパターンがあります。", - "indexPatternManagement.createIndexPattern.stepHeader": "ステップ1/2:インデックスパターンの定義", + "indexPatternManagement.createIndexPattern.stepHeader": "ステップ1/2:インデックスパターンを定義", "indexPatternManagement.createIndexPattern.stepTime.backButton": "戻る", "indexPatternManagement.createIndexPattern.stepTime.createPatternButton": "インデックスパターンを作成", "indexPatternManagement.createIndexPattern.stepTime.creatingLabel": "インデックスパターンを作成中…", @@ -2316,15 +2454,16 @@ "indexPatternManagement.createIndexPattern.stepTime.fieldLabel": "時間フィールド", "indexPatternManagement.createIndexPattern.stepTime.noTimeFieldOptionLabel": "時間フィルターを使用しない", "indexPatternManagement.createIndexPattern.stepTime.noTimeFieldsLabel": "このインデックスパターンに一致するインデックスには時間フィールドがありません。", - "indexPatternManagement.createIndexPattern.stepTime.options.hideButton": "高度なオプションを非表示", + "indexPatternManagement.createIndexPattern.stepTime.options.hideButton": "高度なSIEM設定の非表示化", "indexPatternManagement.createIndexPattern.stepTime.options.patternHeader": "カスタムインデックスパターンID", "indexPatternManagement.createIndexPattern.stepTime.options.patternLabel": "Kibanaはそれぞれのインデックスパターンに固有の識別子を割り当てます。固有のIDを使用しない場合は、カスタムIDを入力してください。", "indexPatternManagement.createIndexPattern.stepTime.options.patternPlaceholder": "custom-index-pattern-id", - "indexPatternManagement.createIndexPattern.stepTime.options.showButton": "高度なオプションを表示", + "indexPatternManagement.createIndexPattern.stepTime.options.showButton": "高度なSIEM設定の表示", "indexPatternManagement.createIndexPattern.stepTime.patterAlreadyExists": "カスタムインデックスパターンIDがすでに存在します。", "indexPatternManagement.createIndexPattern.stepTime.refreshButton": "更新", "indexPatternManagement.createIndexPattern.stepTime.timeDescription": "グローバル時間フィルターで使用するためのプライマリ時間フィールドを選択してください。", "indexPatternManagement.createIndexPattern.stepTimeHeader": "ステップ2/2:設定の構成", + "indexPatternManagement.createIndexPattern.stepTimeLabel": "{indexPattern} {indexPatternName}の設定を指定します。", "indexPatternManagement.createIndexPatternHeader": "{indexPatternName}の作成", "indexPatternManagement.dataStreamLabel": "データストリーム", "indexPatternManagement.date.documentationLabel": "ドキュメント", @@ -2343,6 +2482,7 @@ "indexPatternManagement.duration.decimalPlacesLabel": "小数部分の桁数", "indexPatternManagement.duration.inputFormatLabel": "インプット形式", "indexPatternManagement.duration.outputFormatLabel": "アウトプット形式", + "indexPatternManagement.duration.showSuffixLabel": "接尾辞を表示", "indexPatternManagement.durationErrorMessage": "小数部分の桁数は0から20までの間で指定する必要があります", "indexPatternManagement.editHeader": "{fieldName}を編集", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全アグリゲーションを実行", @@ -2430,10 +2570,15 @@ "indexPatternManagement.editIndexPattern.tabs.fieldsHeader": "フィールド", "indexPatternManagement.editIndexPattern.tabs.scriptedHeader": "スクリプトフィールド", "indexPatternManagement.editIndexPattern.tabs.sourceHeader": "ソースフィルター", - "indexPatternManagement.editIndexPattern.timeFilterHeader": "時間フィルターフィールド名:'{timeFieldName}'", + "indexPatternManagement.editIndexPattern.timeFilterHeader": "時刻フィールド:「{timeFieldName}」", "indexPatternManagement.editIndexPattern.timeFilterLabel.mappingAPILink": "マッピングAPI", "indexPatternManagement.editIndexPattern.timeFilterLabel.timeFilterDetail": "このページは{indexPatternTitle}インデックス内のすべてのフィールドと、Elasticsearchに記録された各フィールドのコアタイプを一覧表示します。フィールドタイプを変更するにはElasticsearchを使用します", "indexPatternManagement.editIndexPatternLiveRegionAriaLabel": "インデックスパターン", + "indexPatternManagement.emptyIndexPatternPrompt.documentation": "ドキュメンテーションを表示", + "indexPatternManagement.emptyIndexPatternPrompt.indexPatternExplanation": "Kibanaでは、検索するインデックスを特定するためにインデックスパターンが必要です。インデックスパターンは、昨日のログデータなど特定のインデックス、またはログデータを含むすべてのインデックスを参照できます。", + "indexPatternManagement.emptyIndexPatternPrompt.learnMore": "詳細について", + "indexPatternManagement.emptyIndexPatternPrompt.nowCreate": "インデックスパターンを作成します。", + "indexPatternManagement.emptyIndexPatternPrompt.youHaveData": "Elasticsearchにデータがあります。", "indexPatternManagement.fieldTypeConflict": "フィールドタイプの矛盾", "indexPatternManagement.formatHeader": "フォーマット", "indexPatternManagement.formatLabel": "フォーマットは、特定の値の表示形式を管理できます。また、値を完全に変更したり、ディスカバリでのハイライト機能を無効にしたりすることも可能です。", @@ -2450,6 +2595,7 @@ "indexPatternManagement.indexPatterns.createFieldBreadcrumb": "フィールドを作成", "indexPatternManagement.indexPatterns.listBreadcrumb": "インデックスパターン", "indexPatternManagement.indexPatternTable.createBtn": "インデックスパターンを作成", + "indexPatternManagement.indexPatternTable.indexPatternExplanation": "Elasticsearchからのデータの取得に役立つインデックスパターンを作成して管理します。", "indexPatternManagement.indexPatternTable.title": "インデックスパターン", "indexPatternManagement.labelTemplate.example.idLabel": "ユーザー#{value}", "indexPatternManagement.labelTemplate.example.output.idLabel": "ユーザー", @@ -2645,6 +2791,8 @@ "kibana_utils.history.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", + "kibana_utils.stateManagement.url.restoreUrlErrorTitle": "URLからの状態の復元エラー", + "kibana_utils.stateManagement.url.saveStateInUrlErrorTitle": "URLでの状態の保存エラー", "kibana-react.dualRangeControl.maxInputAriaLabel": "範囲最大", "kibana-react.dualRangeControl.minInputAriaLabel": "範囲最小", "kibana-react.dualRangeControl.mustSetBothErrorMessage": "下と上の値の両方を設定する必要があります", @@ -2653,6 +2801,14 @@ "kibana-react.exitFullScreenButton.exitFullScreenModeButtonAriaLabel": "全画面モードを終了", "kibana-react.exitFullScreenButton.exitFullScreenModeButtonText": "全画面を終了", "kibana-react.exitFullScreenButton.fullScreenModeDescription": "ESC キーで全画面モードを終了します。", + "kibana-react.kbnOverviewPageHeader.addDataButtonLabel": "データの追加", + "kibana-react.kbnOverviewPageHeader.devToolsButtonLabel": "開発ツール", + "kibana-react.kbnOverviewPageHeader.stackManagementButtonLabel": "管理", + "kibana-react.mountPointPortal.errorMessage": "ポータルコンテンツのレンダリングエラー", + "kibana-react.pageFooter.appDirectoryButtonLabel": "アプリディレクトリを表示", + "kibana-react.pageFooter.changeDefaultRouteSuccessToast": "ランディングページが更新されました", + "kibana-react.pageFooter.changeHomeRouteLink": "ログイン時に別のページを表示", + "kibana-react.pageFooter.makeDefaultRouteLink": "これをランディングページにする", "kibana-react.splitPanel.adjustPanelSizeAriaLabel": "左右のキーを押してパネルサイズを調整します", "kibana-react.tableListView.listing.createNewItemButtonLabel": "Create {entityName}", "kibana-react.tableListView.listing.deleteButtonMessage": "{itemCount} 件の {entityName} を削除", @@ -2672,6 +2828,24 @@ "kibana-react.tableListView.listing.table.editActionDescription": "編集", "kibana-react.tableListView.listing.table.editActionName": "編集", "kibana-react.tableListView.listing.unableToDeleteDangerMessage": "{entityName} を削除できません", + "kibanaOverview.addData.sampleDataButtonLabel": "サンプルデータを試す", + "kibanaOverview.addData.sectionTitle": "データを取り込む", + "kibanaOverview.apps.title": "これらのアプリを検索", + "kibanaOverview.gettingStarted.addDataButtonLabel": "データを追加", + "kibanaOverview.gettingStarted.description": "Kibanaを使用すると、データや方法を可視化する能力が高まります。 1つの質問から始めて、解答から知見を得ることができます。", + "kibanaOverview.gettingStarted.title": "Kibanaの基本操作", + "kibanaOverview.header.title": "Kibana", + "kibanaOverview.kibana.appDescription1": "ダッシュボードでデータを分析します。", + "kibanaOverview.kibana.appDescription2": "インサイトを検索して見つけます。", + "kibanaOverview.kibana.appDescription3": "詳細まで正確なレポートを設計します。", + "kibanaOverview.kibana.appDescription4": "地理的なデータをプロットします。", + "kibanaOverview.kibana.appDescription5": "モデリング、予測、検出を行います。", + "kibanaOverview.kibana.appDescription6": "パターンと関係を明らかにします。", + "kibanaOverview.kibana.solution.subtitle": "可視化&分析", + "kibanaOverview.kibana.solution.title": "Kibana", + "kibanaOverview.manageData.sectionTitle": "データを管理", + "kibanaOverview.more.title": "Elasticではさまざまなことが可能です", + "kibanaOverview.news.title": "新機能", "management.breadcrumb": "スタック管理", "management.landing.header": "Stack Management {version}へようこそ", "management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibanaの設定、その他を管理します。", @@ -2693,9 +2867,19 @@ "management.stackManagement.managementDescription": "Elastic Stack の管理を行うセンターコンソールです。", "management.stackManagement.managementLabel": "スタック管理", "management.stackManagement.title": "スタック管理", + "maps_legacy.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "ディメンションの説明", + "maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionText": "タイルマップに表示されるジオハッシュの最高精度です。7は高い、10は非常に高い、12は最大です。{cellDimensionsLink}", + "maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionTitle": "タイルマップの最高精度", + "maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText": "プロパティ", + "maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsText": "座標マップのWMSマップサーバーサポートのデフォルトの{propertiesLink}です。", + "maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsTitle": "デフォルトのWMSプロパティ", "maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子はdata-updateに対応できるようこのメソッドを導入する必要があります", + "maps_legacy.defaultDistributionMessage": "Mapsを入手するには、ElasticsearchとKibanaの{defaultDistribution}にアップグレードしてください。", "maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel": "データバウンドを合わせる", "maps_legacy.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、ElasticsearchとKibanaの{defaultDistribution}にアップグレードしてください。{ems}ではより多くのズームレベルを無料で利用できます。または、独自のマップサーバーを構成できます。詳細は、{ wms }または{ configSettings}をご覧ください。", + "maps_legacy.legacyMapDeprecationMessage": "Mapsを使用すると、複数のレイヤーとインデックスを追加する、個別のドキュメントをプロットする、データ値から特徴を表現する、ヒートマップ、グリッド、クラスターを追加するなど、さまざまなことが可能です。{getMapsMessage}", + "maps_legacy.legacyMapDeprecationTitle": "{label}は8.0でMapsに移行されます。", + "maps_legacy.openInMapsButtonLabel": "Mapsで表示", "maps_legacy.wmsOptions.attributionStringTip": "右下角の属性文字列", "maps_legacy.wmsOptions.baseLayerSettingsTitle": "ベースレイヤー設定", "maps_legacy.wmsOptions.imageFormatToUseTip": "通常、画像/pngまたは画像/jpegです。サーバーが透明レイヤーを返す場合は。pngを使用します。", @@ -2720,7 +2904,11 @@ "newsfeed.flyoutList.closeButtonLabel": "閉じる", "newsfeed.flyoutList.versionTextLabel": "{version}", "newsfeed.flyoutList.whatsNewTitle": "Elastic の新機能", + "newsfeed.headerButton.readAriaLabel": "ニュースフィードメニュー - すべての項目が既読です", + "newsfeed.headerButton.unreadAriaLabel": "ニュースフィードメニュー - 未読の項目があります", "newsfeed.loadingPrompt.gettingNewsText": "最新ニュースを取得しています...", + "regionMap.advancedSettings.visualization.showRegionMapWarningsText": "用語がマップの形に合わない場合に地域マップに警告を表示するかどうかです。", + "regionMap.advancedSettings.visualization.showRegionMapWarningsTitle": "地域マップに警告を表示", "regionMap.choroplethLayer.downloadingVectorData404ErrorMessage": "{name} の取得時にサーバーから「404」が返されます。指定された場所にファイルが存在することを確認してください。", "regionMap.choroplethLayer.downloadingVectorDataErrorMessage": "{name} ファイルをダウンロードできません。サーバーの CORS 構成で、このホストの Kibana アプリケーションからのリクエストが許可されていることを確認してください。", "regionMap.choroplethLayer.downloadingVectorDataErrorMessageTitle": "ベクトルデータのダウンロード中にエラーが発生しました", @@ -2764,6 +2952,8 @@ "savedObjects.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました", "savedObjects.saveModal.cancelButtonLabel": "キャンセル", "savedObjects.saveModal.descriptionLabel": "説明", + "savedObjects.saveModal.duplicateTitleDescription": "「{title}」を保存すると、タイトルが重複します。", + "savedObjects.saveModal.duplicateTitleLabel": "この{objectType}はすでに存在します", "savedObjects.saveModal.saveAsNewLabel": "新しい {objectType} として保存", "savedObjects.saveModal.saveButtonLabel": "保存", "savedObjects.saveModal.saveTitle": "{objectType} を保存", @@ -2780,6 +2970,14 @@ "savedObjectsManagement.deleteSavedObjectsConfirmModalDescription": "この操作は次の保存されたオブジェクトを削除します:", "savedObjectsManagement.field.offLabel": "オフ", "savedObjectsManagement.field.onLabel": "オン", + "savedObjectsManagement.importSummary.createdCountHeader": "{createdCount}件の新規項目", + "savedObjectsManagement.importSummary.createdOutcomeLabel": "作成済み", + "savedObjectsManagement.importSummary.errorCountHeader": "{errorCount}件のエラー", + "savedObjectsManagement.importSummary.errorOutcomeLabel": "{errorMessage}", + "savedObjectsManagement.importSummary.headerLabelPlural": "{importCount}個のオブジェクトがインポートされました", + "savedObjectsManagement.importSummary.headerLabelSingular": "1個のオブジェクトがインポートされました", + "savedObjectsManagement.importSummary.overwrittenCountHeader": "{overwrittenCount}件上書きされました", + "savedObjectsManagement.importSummary.overwrittenOutcomeLabel": "上書き", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "上書き", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "「{title}」に上書きしてよろしいですか?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "{type}を上書きしますか?", @@ -2836,11 +3034,26 @@ "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription": "影響されるオブジェクトのサンプル", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName": "影響されるオブジェクトのサンプル", "savedObjectsManagement.objectsTable.flyout.resolveImportErrorsFileErrorMessage": "ファイルを処理できませんでした。", + "savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel": "インポートするファイルを選択してください", "savedObjectsManagement.objectsTable.header.exportButtonLabel": "{filteredCount, plural, one{# オブジェクト} other {# オブジェクト}}をエクスポート", "savedObjectsManagement.objectsTable.header.importButtonLabel": "インポート", "savedObjectsManagement.objectsTable.header.refreshButtonLabel": "更新", "savedObjectsManagement.objectsTable.header.savedObjectsTitle": "保存されたオブジェクト", - "savedObjectsManagement.objectsTable.howToDeleteSavedObjectsDescription": "ここから保存された検索などの保存されたオブジェクトを削除できます。保存されたオブジェクトの生データを編集することもできます。通常、オブジェクトは関連付けられたアプリケーションでのみ編集されます。一般的に、この画面ではなく、関連付けられたアプリケーションの使用をお勧めします。", + "savedObjectsManagement.objectsTable.howToDeleteSavedObjectsDescription": "保存されたオブジェクトを管理して共有します。オブジェクトの基本データを編集するには、関連付けられたアプリケーションに移動します。", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.disabledText": "オブジェクトが以前にコピーまたはインポートされたかどうかを確認します。", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.disabledTitle": "既存のオブジェクトを確認", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.enabledText": "このオプションを使用すると、オブジェクトの1つ以上のコピーを作成します。", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.enabledTitle": "ランダムIDで新しいオブジェクトを作成", + "savedObjectsManagement.objectsTable.importModeControl.importOptionsTitle": "インポートオプション", + "savedObjectsManagement.objectsTable.importModeControl.overwrite.disabledLabel": "競合時にアクションを要求", + "savedObjectsManagement.objectsTable.importModeControl.overwrite.enabledLabel": "自動的に競合を上書き", + "savedObjectsManagement.objectsTable.importSummary.unsupportedTypeError": "サポートされていないオブジェクトタイプ", + "savedObjectsManagement.objectsTable.overwriteModal.body.ambiguousConflict": "「{title}」は複数の既存のオブジェクトと競合します。上書きしますか?", + "savedObjectsManagement.objectsTable.overwriteModal.body.conflict": "「{title}」は既存のオブジェクトと競合します。上書きしますか?", + "savedObjectsManagement.objectsTable.overwriteModal.cancelButtonText": "スキップ", + "savedObjectsManagement.objectsTable.overwriteModal.overwriteButtonText": "上書き", + "savedObjectsManagement.objectsTable.overwriteModal.selectControlLabel": "オブジェクトID", + "savedObjectsManagement.objectsTable.overwriteModal.title": "{type}を上書きしますか?", "savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionDescription": "この保存されたオブジェクトを確認してください", "savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionName": "検査", "savedObjectsManagement.objectsTable.relationships.columnActionsName": "アクション", @@ -2872,6 +3085,7 @@ "savedObjectsManagement.objectsTable.table.exportButtonLabel": "エクスポート", "savedObjectsManagement.objectsTable.table.exportPopoverButtonLabel": "エクスポート", "savedObjectsManagement.objectsTable.table.typeFilterName": "タイプ", + "savedObjectsManagement.objectsTable.unableFindSavedObjectNotificationMessage": "保存されたオブジェクトが見つかりません", "savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage": "保存されたオブジェクトが見つかりません", "savedObjectsManagement.parsingFieldErrorMessage": "{fieldName}をインデックスパターン{indexName}用にパース中にエラーが発生しました:{errorMessage}", "savedObjectsManagement.view.cancelButtonAriaLabel": "キャンセル", @@ -2889,7 +3103,11 @@ "savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "このオブジェクトに関連付けられた保存された検索は現在存在しません。", "savedObjectsManagement.view.viewItemButtonLabel": "{title}を表示", "savedObjectsManagement.view.viewItemTitle": "{title}を表示", - "usageCollection.stats.notReadyMessage": "まだ統計が準備できていません。後程再試行してください", + "security.checkup.dismissButtonText": "閉じる", + "security.checkup.dontShowAgain": "今後表示しない", + "security.checkup.insecureClusterMessage": "当社の無料のセキュリティ機能を使用すると、不正アクセスから保護することができます。", + "security.checkup.insecureClusterTitle": "インストールを保護してください", + "security.checkup.learnMoreButtonText": "詳細", "share.advancedSettings.csv.quoteValuesText": "csvエクスポートに値を引用するかどうかです", "share.advancedSettings.csv.quoteValuesTitle": "CSVの値を引用", "share.advancedSettings.csv.separatorText": "エクスポートされた値をこの文字列で区切ります", @@ -2924,6 +3142,7 @@ "telemetry.callout.errorLoadingClusterStatisticsTitle": "クラスター統計の読み込みエラー", "telemetry.callout.errorUnprivilegedUserDescription": "暗号化されていないクラスター統計を表示するアクセス権がありません。", "telemetry.callout.errorUnprivilegedUserTitle": "クラスター統計の表示エラー", + "telemetry.clusterData": "クラスターデータ", "telemetry.optInErrorToastText": "使用状況統計設定の設定中にエラーが発生しました。", "telemetry.optInErrorToastTitle": "エラー", "telemetry.optInNoticeSeenErrorTitle": "エラー", @@ -2933,6 +3152,8 @@ "telemetry.provideUsageStatisticsAriaName": "使用統計を提供", "telemetry.provideUsageStatisticsTitle": "使用統計を提供", "telemetry.readOurUsageDataPrivacyStatementLinkText": "プライバシーポリシー", + "telemetry.securityData": "Endpoint Securityデータ", + "telemetry.seeExamplesOfWhatWeCollect": "当社が収集する{clusterData}および{endpointSecurityData}の例をご覧ください。", "telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。", "telemetry.telemetryConfigAndLinkDescription": "使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。", "telemetry.telemetryConfigDescription": "基本的な機能の利用状況に関する統計情報を提供して、Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。", @@ -2978,6 +3199,8 @@ "timelion.cells.actions.reorderAriaLabel": "ドラッグして並べ替え", "timelion.cells.actions.reorderTooltip": "ドラッグして並べ替え", "timelion.chart.seriesList.noSchemaWarning": "次のパネルタイプは存在しません: {renderType}", + "timelion.deprecation.here": "ダッシュボードに移行します。", + "timelion.deprecation.message": "Timelionアプリは7.0以降で非推奨となっています。8.0では削除される予定です。Timelionワークシートを引き続き使用するには、{timeLionDeprecationLink}。", "timelion.emptyExpressionErrorMessage": "Timelion エラー式が入力されていません", "timelion.expressionInputAriaLabel": "Timelion 式", "timelion.expressionInputPlaceholder": "{esQuery} でのクエリを試してみてください。", @@ -3316,8 +3539,16 @@ "timelion.vis.invalidIntervalErrorMessage": "無効な間隔フォーマット。", "timelion.vis.selectIntervalHelpText": "オプションを選択するかカスタム値を作成します。例30s、20m、24h、2d、1w、1M", "timelion.vis.selectIntervalPlaceholder": "間隔を選択", + "uiActions.actionPanel.more": "詳細", "uiActions.actionPanel.title": "オプション", "uiActions.errors.incompatibleAction": "操作に互換性がありません", + "uiActions.triggers.applyFilterDescription": "Kibanaフィルターが適用されるとき。単一の値または範囲フィルターにすることができます。", + "uiActions.triggers.applyFilterTitle": "フィルターを適用", + "uiActions.triggers.selectRangeDescription": "ビジュアライゼーションでの値の範囲", + "uiActions.triggers.selectRangeTitle": "範囲選択", + "uiActions.triggers.valueClickDescription": "ビジュアライゼーションでデータポイントをクリック", + "uiActions.triggers.valueClickTitle": "シングルクリック", + "usageCollection.stats.notReadyMessage": "まだ統計が準備できていません。しばらくたってから再試行してください。", "visDefaultEditor.advancedToggle.advancedLinkLabel": "高度な設定", "visDefaultEditor.agg.disableAggButtonTooltip": "集約を無効にする", "visDefaultEditor.agg.enableAggButtonTooltip": "集約を有効にする", @@ -3382,9 +3613,13 @@ "visDefaultEditor.controls.ipRangesAriaLabel": "IP 範囲", "visDefaultEditor.controls.jsonInputLabel": "JSON インプット", "visDefaultEditor.controls.jsonInputTooltip": "ここに追加された JSON 形式のプロパティは、すべてこのセクションの Elasticsearch 集約定義に融合されます。用語集約における「shard_size」がその例です。", + "visDefaultEditor.controls.maxBars.autoPlaceholder": "自動", + "visDefaultEditor.controls.maxBars.maxBarsHelpText": "間隔は、使用可能なデータに基づいて、自動的に選択されます。棒の最大数は、詳細設定の{histogramMaxBars}以下でなければなりません。", + "visDefaultEditor.controls.maxBars.maxBarsLabel": "棒の最大数", "visDefaultEditor.controls.metricLabel": "メトリック", "visDefaultEditor.controls.metrics.bucketTitle": "バケット", "visDefaultEditor.controls.metrics.metricTitle": "メトリック", + "visDefaultEditor.controls.numberInterval.autoInteralIsUsed": "自動間隔が使用されます", "visDefaultEditor.controls.numberInterval.minimumIntervalLabel": "最低間隔", "visDefaultEditor.controls.numberInterval.minimumIntervalTooltip": "入力された値により高度な設定の {histogramMaxBars} で指定されたよりも多くのバケットが作成される場合、間隔は自動的にスケーリングされます。", "visDefaultEditor.controls.numberInterval.selectIntervalPlaceholder": "間隔を入力", @@ -3433,6 +3668,7 @@ "visDefaultEditor.controls.timeInterval.scaledHelpText": "現在 {bucketDescription} にスケーリングされています", "visDefaultEditor.controls.timeInterval.selectIntervalPlaceholder": "間隔を選択", "visDefaultEditor.controls.timeInterval.selectOptionHelpText": "オプションを選択するかカスタム値を作成します。例30s、20m、24h、2d、1w、1M", + "visDefaultEditor.controls.useAutoInterval": "自動間隔を使用", "visDefaultEditor.editorConfig.dateHistogram.customInterval.helpText": "構成間隔の倍数でなければなりません: {interval}", "visDefaultEditor.editorConfig.histogram.interval.helpText": "構成間隔の倍数でなければなりません: {interval}", "visDefaultEditor.metrics.wrongLastBucketTypeErrorMessage": "「{type}」メトリック集約を使用する場合、最後のバケット集約は「Date Histogram」または「Histogram」でなければなりません。", @@ -4083,9 +4319,21 @@ "visTypeVega.esQueryParser.shiftMustValueTypeErrorMessage": "{shiftParam} は数値でなければなりません", "visTypeVega.esQueryParser.timefilterValueErrorMessage": "{timefilter} のプロパティは {trueValue}、{minValue}、または {maxValue} に設定する必要があります", "visTypeVega.esQueryParser.unknownUnitValueErrorMessage": "不明な {unitParamName} 値。[{unitParamValues}] の内の 1 つでなければなりません", + "visTypeVega.esQueryParser.unnamedRequest": "無題のリクエスト#{index}", "visTypeVega.esQueryParser.urlBodyValueTypeErrorMessage": "{configName} はオブジェクトでなければなりません", "visTypeVega.esQueryParser.urlContextAndUrlTimefieldMustNotBeUsedErrorMessage": "{urlContext} と {timefield} は {queryParam} が設定されている場合使用できません", "visTypeVega.function.help": "Vega ビジュアライゼーション", + "visTypeVega.inspector.dataSetsLabel": "データセット", + "visTypeVega.inspector.dataViewer.dataSetAriaLabel": "データセット", + "visTypeVega.inspector.dataViewer.gridAriaLabel": "{name}データグリッド", + "visTypeVega.inspector.signalValuesLabel": "単一の値", + "visTypeVega.inspector.signalViewer.gridAriaLabel": "単一の値のデータグリッド", + "visTypeVega.inspector.specLabel": "仕様", + "visTypeVega.inspector.specViewer.copyToClipboardLabel": "クリップボードにコピー", + "visTypeVega.inspector.vegaAdapter.signal": "信号", + "visTypeVega.inspector.vegaAdapter.value": "値", + "visTypeVega.inspector.vegaDebugLabel": "Vegaデバッグ", + "visTypeVega.mapView.experimentalMapLayerInfo": "マップレイヤーはまだ実験段階であり、オフィシャルGA機能のサポートSLAが適用されません。フィードバックがある場合は、{githubLink}で問題を報告してください。", "visTypeVega.mapView.mapStyleNotFoundWarningMessage": "{mapStyleParam} が見つかりませんでした", "visTypeVega.mapView.minZoomAndMaxZoomHaveBeenSwappedWarningMessage": "{minZoomPropertyName} と {maxZoomPropertyName} が交換されました", "visTypeVega.mapView.resettingPropertyToMaxValueWarningMessage": "{name} を {max} にリセットしています", @@ -4093,6 +4341,7 @@ "visTypeVega.type.vegaDescription": "Vega と Vega-Lite を使用してカスタムビジュアライゼーションを作成します。", "visTypeVega.urlParser.dataUrlRequiresUrlParameterInFormErrorMessage": "{dataUrlParam} には「{formLink}」の形で {urlParam} パラメーターが必要です", "visTypeVega.urlParser.urlShouldHaveQuerySubObjectWarningMessage": "{urlObject} を使用するには {subObjectName} サブオブジェクトが必要です", + "visTypeVega.vegaParser.autoSizeDoesNotAllowFalse": "{autoSizeParam}が有効です。無効にするには、{autoSizeParam}を{noneParam}に設定してください", "visTypeVega.vegaParser.baseView.externalUrlsAreNotEnabledErrorMessage": "外部 URL が無効です。{enableExternalUrls} を {kibanaConfigFileName} に追加します", "visTypeVega.vegaParser.baseView.functionIsNotDefinedForGraphErrorMessage": "このグラフには {funcName} が定義されていません", "visTypeVega.vegaParser.baseView.timeValuesTypeErrorMessage": "時間フィルターの設定エラー: 両方の時間の値は相対的または絶対的な日付である必要があります。 {start}、{end}", @@ -4100,6 +4349,7 @@ "visTypeVega.vegaParser.dataExceedsSomeParamsUseTimesLimitErrorMessage": "データには {urlParam}、{valuesParam}、 {sourceParam} の内複数を含めることができません", "visTypeVega.vegaParser.hostConfigIsDeprecatedWarningMessage": "{deprecatedConfigName} は廃止されました。代わりに {newConfigName} を使用してください。", "visTypeVega.vegaParser.hostConfigValueTypeErrorMessage": "{configName} が含まれている場合、オブジェクトでなければなりません", + "visTypeVega.vegaParser.inputSpecDoesNotSpecifySchemaErrorMessage": "仕様に基づき、{schemaParam}フィールドには、\nVega({vegaSchemaUrl}を参照)または\nVega-Lite({vegaLiteSchemaUrl}を参照)の有効なURLを入力する必要があります。\nURLは識別子にすぎません。Kibanaやご使用のブラウザーがこのURLにアクセスすることはありません。", "visTypeVega.vegaParser.invalidVegaSpecErrorMessage": "無効な Vega 仕様", "visTypeVega.vegaParser.kibanaConfigValueTypeErrorMessage": "{configName} が含まれている場合、オブジェクトでなければなりません", "visTypeVega.vegaParser.mapStyleValueTypeWarningMessage": "{mapStyleConfigName} は {mapStyleConfigFirstAllowedValue} か {mapStyleConfigSecondAllowedValue} のどちらかです", @@ -4291,6 +4541,7 @@ "visTypeVislib.thresholdLine.style.dashedText": "鎖線", "visTypeVislib.thresholdLine.style.dotdashedText": "点線", "visTypeVislib.thresholdLine.style.fullText": "完全", + "visTypeVislib.vislib.errors.noResultsFoundTitle": "結果が見つかりませんでした", "visTypeVislib.vislib.heatmap.maxBucketsText": "定義された数列が多すぎます ({nr})。構成されている最大値は {max} です。", "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "値 {legendDataLabel} でフィルタリング", "visTypeVislib.vislib.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", @@ -4307,6 +4558,7 @@ "visualizations.disabledLabVisualizationMessage": "ラボビジュアライゼーションを表示するには、高度な設定でラボモードをオンにしてください。", "visualizations.disabledLabVisualizationTitle": "{title} はラボビジュアライゼーションです。", "visualizations.displayName": "ビジュアライゼーション", + "visualizations.embeddable.placeholderTitle": "プレースホルダータイトル", "visualizations.function.range.from.help": "範囲の開始", "visualizations.function.range.help": "範囲オブジェクトを生成します", "visualizations.function.range.to.help": "範囲の終了", @@ -4333,14 +4585,23 @@ "visualizations.newVisWizard.searchSelection.savedObjectType.search": "保存検索", "visualizations.newVisWizard.selectVisType": "ビジュアライゼーションのタイプを選択してください", "visualizations.newVisWizard.title": "新規ビジュアライゼーション", + "visualizations.noResultsFoundTitle": "結果が見つかりませんでした", "visualizations.savedObjectName": "ビジュアライゼーション", + "visualizations.savingVisualizationFailed.errorMsg": "ビジュアライゼーションの保存が失敗しました", "visualizations.visualizationTypeInvalidMessage": "無効なビジュアライゼーションタイプ \"{visType}\"", "visualize.badge.readOnly.text": "読み取り専用", "visualize.badge.readOnly.tooltip": "ビジュアライゼーションを保存できません", + "visualize.byValue_pageHeading": "{originatingApp}アプリに埋め込まれた{chartType}タイプのビジュアライゼーション", + "visualize.confirmModal.confirmTextDescription": "変更を保存せずにVisualizeエディターから移動しますか?", + "visualize.confirmModal.title": "保存されていない変更", "visualize.createVisualization.failedToLoadErrorMessage": "ビジュアライゼーションを読み込めませんでした", "visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPatternまたはsavedSearchIdが必要です", "visualize.createVisualization.noVisTypeErrorMessage": "有効なビジュアライゼーションタイプを指定してください", + "visualize.dashboard.prefix.breadcrumb": "ダッシュボード", + "visualize.discover.visualizeFieldLabel": "Visualizeフィールド", "visualize.editor.createBreadcrumb": "作成", + "visualize.editor.defaultEditBreadcrumbText": "編集", + "visualize.experimentalVisInfoText": "このビジュアライゼーションはまだ実験段階であり、オフィシャルGA機能のサポートSLAが適用されません。フィードバックがある場合は、{githubLink}で問題を報告してください。", "visualize.helpMenu.appName": "可視化", "visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました", "visualize.listing.betaTitle": "ベータ", @@ -4361,6 +4622,9 @@ "visualize.noMatchRoute.bannerText": "Visualizeアプリケーションはこのルートを認識できません。{route}", "visualize.noMatchRoute.bannerTitleText": "ページが見つかりません", "visualize.pageHeading": "{chartName} {chartType}可視化", + "visualize.topNavMenu.cancelAndReturnButtonTooltip": "完了する前に変更を破棄", + "visualize.topNavMenu.cancelButtonAriaLabel": "変更を保存せずに最後に使用していたアプリに戻る", + "visualize.topNavMenu.cancelButtonLabel": "キャンセル", "visualize.topNavMenu.openInspectorButtonAriaLabel": "ビジュアライゼーションのインスペクターを開く", "visualize.topNavMenu.openInspectorButtonLabel": "検査", "visualize.topNavMenu.openInspectorDisabledButtonTooltip": "このビジュアライゼーションはインスペクターをサポートしていません。", @@ -4391,11 +4655,15 @@ "xpack.actions.builtin.case.connectorApiNullError": "コネクター[apiUrl]が必要です", "xpack.actions.builtin.case.jiraTitle": "Jira", "xpack.actions.builtin.case.resilientTitle": "IBM Resilient", + "xpack.actions.builtin.configuration.apiAllowedHostsError": "コネクターアクションの構成エラー:{message}", "xpack.actions.builtin.email.errorSendingErrorMessage": "エラー送信メールアドレス", "xpack.actions.builtin.emailTitle": "メール", "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "エラーインデックス作成ドキュメント", "xpack.actions.builtin.esIndexTitle": "インデックス", + "xpack.actions.builtin.jira.configuration.apiAllowedHostsError": "コネクターアクションの構成エラー:{message}", + "xpack.actions.builtin.jira.configuration.emptyMapping": "[incidentConfiguration.mapping]:空以外の値が必要ですが空でした", "xpack.actions.builtin.pagerduty.invalidTimestampErrorMessage": "タイムスタンプ\"{timestamp}\"の解析エラー", + "xpack.actions.builtin.pagerduty.missingDedupkeyErrorMessage": "eventActionが「{eventAction}」のときにはDedupKeyが必要です", "xpack.actions.builtin.pagerduty.pagerdutyConfigurationError": "pagerduty アクションの設定エラー: {message}", "xpack.actions.builtin.pagerduty.postingErrorMessage": "pagerduty イベントの投稿エラー", "xpack.actions.builtin.pagerduty.postingRetryErrorMessage": "pagerduty イベントの投稿エラー: http status {status}、後ほど再試行", @@ -4423,39 +4691,18 @@ "xpack.actions.builtin.webhook.webhookConfigurationErrorNoHostname": "Webフックアクションの構成エラーです。URLを解析できません。{err}", "xpack.actions.builtin.webhookTitle": "Web フック", "xpack.actions.disabledActionTypeError": "アクションタイプ \"{actionType}\" は、Kibana 構成 xpack.actions.enabledActionTypes では有効化されません", + "xpack.actions.featureRegistry.actionsFeatureName": "アクションとコネクター", "xpack.actions.serverSideErrors.expirerdLicenseErrorMessage": "{licenseType} ライセンスの期限が切れたのでアクションタイプ {actionTypeId} は無効です。", "xpack.actions.serverSideErrors.invalidLicenseErrorMessage": "{licenseType} ライセンスでサポートされないのでアクションタイプ {actionTypeId} は無効です。ライセンスをアップグレードしてください。", "xpack.actions.serverSideErrors.predefinedActionDeleteDisabled": "あらかじめ構成されたアクション{id}は削除できません。", "xpack.actions.serverSideErrors.predefinedActionUpdateDisabled": "あらかじめ構成されたアクション{id}は更新できません。", "xpack.actions.serverSideErrors.unavailableLicenseErrorMessage": "現時点でライセンス情報を入手できないため、アクションタイプ {actionTypeId} は無効です。", "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "グラフを利用できません。現在ライセンス情報が利用できません。", - "xpack.stackAlerts.indexThreshold.actionGroupThresholdMetTitle": "しきい値一致", - "xpack.stackAlerts.indexThreshold.actionVariableContextDateLabel": "アラートがしきい値を超えた日付。", - "xpack.stackAlerts.indexThreshold.actionVariableContextGroupLabel": "しきい値を超えたグループ。", - "xpack.stackAlerts.indexThreshold.actionVariableContextMessageLabel": "アラートの事前構成メッセージ。", - "xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "アラートの事前構成タイトル。", - "xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "しきい値を超えた値。", - "xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage": "[aggType] が「{aggType}」のときには [aggField] に値が必要です", - "xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "アラート {name} グループ {group} 値 {value} が {date} に {window} にわたってしきい値 {function} を超えました", - "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "アラート {name} グループ {group} がしきい値を超えました", - "xpack.stackAlerts.indexThreshold.alertTypeTitle": "インデックスしきい値", - "xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage": "[dateStart] が [dateEnd] よりも大です", - "xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage": "{fieldName} の無効な {formatName} 形式:「{fieldValue}」", - "xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage": "[interval]: [dateStart] が [dateEnd] と等しくない場合に指定する必要があります", - "xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage": "無効な aggType:「{aggType}」", - "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "無効な thresholdComparator が指定されました: {comparator}", - "xpack.stackAlerts.indexThreshold.invalidDateErrorMessage": "無効な日付 {date}", - "xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage": "無効な期間:「{duration}」", - "xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage": "無効な groupBy:「{groupBy}」", - "xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage": "[termSize]: {maxGroups} 以下でなければなりません。", - "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]: 「{thresholdComparator}」比較子の場合には 2 つの要素が必要です", - "xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage": "無効な timeWindowUnit:「{timeWindowUnit}」", - "xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage": "間隔 {intervals} の計算値が {maxIntervals} よりも大です", - "xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage": "[termField]: [groupBy] がトップのときには termField が必要です", - "xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage": "[termSize]: [groupBy] がトップのときには termSize が必要です", + "xpack.actions.urlAllowedHostsConfigurationError": "ターゲット{field}「{value}」はKibana構成xpack.actions.allowedHostsに追加されていません", "xpack.alerts.alertNavigationRegistry.get.missingNavigationError": "「{consumer}」内のアラートタイプ「{alertType}」のナビゲーションは登録されていません。", "xpack.alerts.alertNavigationRegistry.register.duplicateDefaultError": "「{consumer}」内のデフォルトナビゲーションはすでに登録されています。", "xpack.alerts.alertNavigationRegistry.register.duplicateNavigationError": "「{consumer}」内のアラートタイプ「{alertType}」のナビゲーションはすでに登録されています。", + "xpack.alerts.alertsClient.invalidDate": "パラメーター{field}の無効な日付:「{dateValue}」", "xpack.alerts.alertsClient.validateActions.invalidGroups": "無効なアクショングループ:{groups}", "xpack.alerts.alertTypeRegistry.get.missingAlertTypeError": "アラートタイプ「{id}」は登録されていません。", "xpack.alerts.alertTypeRegistry.register.duplicateAlertTypeError": "アラートタイプ\"{id}\"はすでに登録されています。", @@ -4463,6 +4710,7 @@ "xpack.alerts.appName": "アラート", "xpack.alerts.loadAlertType.missingAlertTypeError": "アラートタイプ「{id}」は登録されていません。", "xpack.alerts.serverSideErrors.unavailableLicenseInformationErrorMessage": "アラートを利用できません。現在ライセンス情報が利用できません。", + "xpack.apm.a.thresholdMet": "しきい値一致", "xpack.apm.addDataButtonLabel": "データの追加", "xpack.apm.agentConfig.allOptionLabel": "すべて", "xpack.apm.agentConfig.apiRequestSize.description": "チャンクエンコーディング(HTTPストリーミング)を経由してAPM ServerインテークAPIに送信されるリクエスト本文の最大合計圧縮サイズ。\nわずかなオーバーシュートの可能性があることに注意してください。\n\n使用できるバイト単位は、「b」、「kb」、「mb」です。「1kb」は「1024b」と等価です。", @@ -4572,7 +4820,29 @@ "xpack.apm.agentMetrics.java.threadCount": "平均カウント", "xpack.apm.agentMetrics.java.threadCountChartTitle": "スレッド数", "xpack.apm.agentMetrics.java.threadCountMax": "最高カウント", - "xpack.apm.alertTypes.transactionDuration": "トランザクション期間", + "xpack.apm.alerting.fields.all_option": "すべて", + "xpack.apm.alerting.fields.environment": "環境", + "xpack.apm.alerting.fields.service": "サービス", + "xpack.apm.alerting.fields.type": "タイプ", + "xpack.apm.alerts.action_variables.environment": "アラートが作成されるトランザクションタイプ", + "xpack.apm.alerts.action_variables.intervalSize": "アラート条件が満たされた期間の長さと単位", + "xpack.apm.alerts.action_variables.serviceName": "アラートが作成されるサービス", + "xpack.apm.alerts.action_variables.threshold": "この値を超えるすべてのトリガーによりアラートが実行されます", + "xpack.apm.alerts.action_variables.transactionType": "アラートが作成されるトランザクションタイプ", + "xpack.apm.alerts.action_variables.triggerValue": "しきい値に達し、アラートをトリガーした値", + "xpack.apm.alerts.anomalySeverity.criticalLabel": "致命的", + "xpack.apm.alerts.anomalySeverity.majorLabel": "メジャー", + "xpack.apm.alerts.anomalySeverity.minor": "マイナー", + "xpack.apm.alerts.anomalySeverity.scoreDetailsDescription": "スコア{value}以上", + "xpack.apm.alerts.anomalySeverity.warningLabel": "警告", + "xpack.apm.alertTypes.errorCount": "エラー数しきい値", + "xpack.apm.alertTypes.errorCount.defaultActionMessage": "次の条件のため、\\{\\{alertName\\}\\}アラートが実行されています。\n\n- サービス名:\\{\\{context.serviceName\\}\\}\n- 環境:\\{\\{context.environment\\}\\}\n- しきい値\\{\\{context.threshold\\}\\}エラー\n- トリガーされた値:過去\\{\\{context.interval\\}\\}に\\{\\{context.triggerValue\\}\\}件のエラー", + "xpack.apm.alertTypes.transactionDuration": "トランザクション期間のしきい値", + "xpack.apm.alertTypes.transactionDuration.defaultActionMessage": "次の条件のため、\\{\\{alertName\\}\\}アラートが実行されています。\n\n- サービス名:\\{\\{context.serviceName\\}\\}\n- タイプ:\\{\\{context.transactionType\\}\\}\n- 環境:\\{\\{context.environment\\}\\}\n- しきい値:\\{\\{context.threshold\\}\\}ミリ秒\n- トリガーされた値:過去\\{\\{context.interval\\}\\}に\\{\\{context.triggerValue\\}\\}", + "xpack.apm.alertTypes.transactionDurationAnomaly": "トランザクション期間異常", + "xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage": "次の条件のため、\\{\\{alertName\\}\\}アラートが実行されています。\n\n- サービス名:\\{\\{context.serviceName\\}\\}\n- タイプ:\\{\\{context.transactionType\\}\\}\n- 環境:\\{\\{context.environment\\}\\}\n- 重要度しきい値:\\{\\{context.threshold\\}\\}%\n- 重要度値:\\{\\{context.thresholdValue\\}\\}\n", + "xpack.apm.alertTypes.transactionErrorRate": "トランザクションエラー率しきい値", + "xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage": "次の条件のため、\\{\\{alertName\\}\\}アラートが実行されています。\n\n- サービス名:\\{\\{context.serviceName\\}\\}\n- タイプ:\\{\\{context.transactionType\\}\\}\n- 環境:\\{\\{context.environment\\}\\}\n- しきい値:\\{\\{context.threshold\\}\\}%\n- トリガーされた値:過去\\{\\{context.interval\\}\\}にエラーの\\{\\{context.triggerValue\\}\\}%", "xpack.apm.anomaly_detection.error.invalid_license": "異常検知を使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。このライセンスがあれば、機械学習を活用して、サービスを監視できます。", "xpack.apm.anomaly_detection.error.missing_read_privileges": "異常検知ジョブを表示するには、機械学習およびAPMの「読み取り」権限が必要です", "xpack.apm.anomaly_detection.error.missing_write_privileges": "異常検知ジョブを作成するには、機械学習およびAPMの「書き込み」権限が必要です", @@ -4586,6 +4856,7 @@ "xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText": "「{currentEnvironment}」環境では、まだ異常検知が有効ではありません。クリックすると、セットアップを続行します。", "xpack.apm.anomalyDetectionSetup.notEnabledText": "異常検知はまだ有効ではありません。クリックすると、セットアップを続行します。", "xpack.apm.apmDescription": "アプリケーション内から自動的に詳細なパフォーマンスメトリックやエラーを集めます。", + "xpack.apm.apply.label": "適用", "xpack.apm.applyFilter": "{title} フィルターを適用", "xpack.apm.applyOptions": "オプションを適用", "xpack.apm.breadcrumb.errorsTitle": "エラー", @@ -4609,6 +4880,11 @@ "xpack.apm.chart.memorySeries.systemAverageLabel": "平均", "xpack.apm.chart.memorySeries.systemMaxLabel": "最高", "xpack.apm.clearFilters": "フィルターを消去", + "xpack.apm.csm.breakdownFilter.browser": "ブラウザー", + "xpack.apm.csm.breakdownFilter.device": "デバイス", + "xpack.apm.csm.breakdownFilter.location": "場所", + "xpack.apm.csm.breakDownFilter.noBreakdown": "内訳なし", + "xpack.apm.csm.breakdownFilter.os": "OS", "xpack.apm.customLink.buttom.create": "カスタムリンクを作成", "xpack.apm.customLink.buttom.create.title": "作成", "xpack.apm.customLink.buttom.manage": "カスタムリンクを管理", @@ -4617,6 +4893,8 @@ "xpack.apm.emptyMessage.noDataFoundLabel": "データが見つかりません。", "xpack.apm.error.prompt.body": "詳細はブラウザの開発者コンソールをご確認ください。", "xpack.apm.error.prompt.title": "申し訳ございませんが、エラーが発生しました :(", + "xpack.apm.errorCountAlert.name": "エラー数しきい値", + "xpack.apm.errorCountAlertTrigger.errors": " エラー", "xpack.apm.errorGroupDetails.avgLabel": "平均", "xpack.apm.errorGroupDetails.culpritLabel": "原因", "xpack.apm.errorGroupDetails.errorGroupTitle": "エラーグループ {errorGroupId}", @@ -4641,11 +4919,12 @@ "xpack.apm.errorsTable.occurrencesColumnLabel": "オカレンス", "xpack.apm.errorsTable.typeColumnLabel": "タイプ", "xpack.apm.errorsTable.unhandledLabel": "未対応", - "xpack.apm.featureRegistry.apmFeatureName": "APM", + "xpack.apm.featureRegistry.apmFeatureName": "APMおよびユーザーエクスペリエンス", "xpack.apm.feedbackMenu.appName": "APM", "xpack.apm.fetcher.error.status": "エラー", "xpack.apm.fetcher.error.title": "リソースの取得中にエラーが発生しました", "xpack.apm.fetcher.error.url": "URL", + "xpack.apm.filter.environment.allLabel": "すべて", "xpack.apm.filter.environment.label": "環境", "xpack.apm.filter.environment.notDefinedLabel": "未定義", "xpack.apm.filter.environment.selectEnvironmentLabel": "環境を選択", @@ -4660,6 +4939,13 @@ "xpack.apm.header.badge.readOnly.tooltip": "を保存できませんでした", "xpack.apm.helpMenu.upgradeAssistantLink": "アップグレードアシスタント", "xpack.apm.histogram.plot.noDataLabel": "この時間範囲のデータがありません。", + "xpack.apm.home.alertsMenu.alerts": "アラート", + "xpack.apm.home.alertsMenu.createAnomalyAlert": "異常アラートを作成", + "xpack.apm.home.alertsMenu.createThresholdAlert": "しきい値アラートを作成", + "xpack.apm.home.alertsMenu.errorCount": "エラー数", + "xpack.apm.home.alertsMenu.transactionDuration": "トランザクション期間", + "xpack.apm.home.alertsMenu.transactionErrorRate": "トランザクションエラー率", + "xpack.apm.home.alertsMenu.viewActiveAlerts": "アクティブアラートを表示", "xpack.apm.home.serviceMapTabLabel": "サービスマップ", "xpack.apm.home.servicesTabLabel": "サービス", "xpack.apm.home.tracesTabLabel": "トレース", @@ -4714,11 +5000,13 @@ "xpack.apm.metrics.plot.noDataLabel": "この時間範囲のデータがありません。", "xpack.apm.metrics.transactionChart.machineLearningLabel": "機械学習:", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "平均期間の周りのストリームには予測バウンドが表示されます。異常スコアが>= 75の場合、注釈が表示されます。", + "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "フィルタリングで検索バーを使用しているときには、機械学習結果が表示されません", "xpack.apm.metrics.transactionChart.pageLoadTimesLabel": "ページ読み込み時間", "xpack.apm.metrics.transactionChart.requestsPerMinuteLabel": "1 分あたりのリクエスト", "xpack.apm.metrics.transactionChart.routeChangeTimesLabel": "ルート変更時間", "xpack.apm.metrics.transactionChart.transactionDurationLabel": "トランザクション時間", "xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel": "1 分あたりのトランザクション数", + "xpack.apm.metrics.transactionChart.viewJob": "ジョブを表示:", "xpack.apm.notAvailableLabel": "N/A", "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "利用可能なデータがありません", "xpack.apm.propertiesTable.agentFeature.noResultFound": "\"{value}\"に対する結果が見つかりませんでした。", @@ -4726,24 +5014,53 @@ "xpack.apm.propertiesTable.tabs.logStacktraceLabel": "スタックトレース", "xpack.apm.propertiesTable.tabs.metadataLabel": "メタデータ", "xpack.apm.propertiesTable.tabs.timelineLabel": "タイムライン", + "xpack.apm.rum.coreVitals.fcp": "初回コンテンツの描画", + "xpack.apm.rum.coreVitals.tbt": "合計ブロック時間", "xpack.apm.rum.dashboard.backend": "バックエンド", "xpack.apm.rum.dashboard.frontend": "フロントエンド", + "xpack.apm.rum.dashboard.impactfulMetrics.highTrafficPages": "高トラフィックページ", + "xpack.apm.rum.dashboard.impactfulMetrics.jsErrors": "JavaScriptエラー", "xpack.apm.rum.dashboard.overall.label": "全体", "xpack.apm.rum.dashboard.pageLoadDistribution.label": "ページ読み込み分布", + "xpack.apm.rum.dashboard.pageLoadDuration.label": "ページ読み込み時間", "xpack.apm.rum.dashboard.pageLoadTime.label": "ページ読み込み時間(秒)", "xpack.apm.rum.dashboard.pageLoadTimes.label": "ページ読み込み時間", "xpack.apm.rum.dashboard.pagesLoaded.label": "ページが読み込まれました", "xpack.apm.rum.dashboard.pageViews": "ページビュー", "xpack.apm.rum.dashboard.resetZoom.label": "ズームをリセット", "xpack.apm.rum.filterGroup.breakdown": "内訳", + "xpack.apm.rum.filterGroup.coreWebVitals": "コアWebバイタル", "xpack.apm.rum.filterGroup.seconds": "秒", "xpack.apm.rum.filterGroup.selectBreakdown": "内訳を選択", + "xpack.apm.rum.filters.searchByUrl": "URLで検索", + "xpack.apm.rum.filters.searchResults": "{total}件の検索結果", + "xpack.apm.rum.filters.select": "選択してください", + "xpack.apm.rum.filters.topPages": "上位のページ", + "xpack.apm.rum.filters.url": "Url", + "xpack.apm.rum.filters.url.loadingResults": "結果を読み込み中", + "xpack.apm.rum.filters.url.noResults": "結果がありません", + "xpack.apm.rum.jsErrors.errorMessage": "エラーメッセージ", + "xpack.apm.rum.jsErrors.errorRate": "エラー率", + "xpack.apm.rum.jsErrors.errorRateValue": "{errorRate} %", + "xpack.apm.rum.jsErrors.impactedPageLoads": "影響を受けるページ読み込み数", + "xpack.apm.rum.jsErrors.totalErrors": "合計エラー数", + "xpack.apm.rum.userExperienceMetrics": "ユーザーエクスペリエンスメトリック", + "xpack.apm.rum.uxMetrics.longestLongTasks": "最長タスク時間", + "xpack.apm.rum.uxMetrics.noOfLongTasks": "時間がかかるタスク数", + "xpack.apm.rum.uxMetrics.sumLongTasks": "時間がかかるタスクの合計時間", "xpack.apm.rum.visitorBreakdown": "アクセスユーザー内訳", + "xpack.apm.rum.visitorBreakdown.browser": "ブラウザー", + "xpack.apm.rum.visitorBreakdown.operatingSystem": "オペレーティングシステム", + "xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration": "平均ページ読み込み時間", + "xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion": "地域別ページ読み込み時間(平均)", "xpack.apm.searchInput.filter": "フィルター...", "xpack.apm.selectPlaceholder": "オプションを選択:", "xpack.apm.serviceDetails.alertsMenu.alerts": "アラート", + "xpack.apm.serviceDetails.alertsMenu.createAnomalyAlert": "異常アラートを作成", "xpack.apm.serviceDetails.alertsMenu.createThresholdAlert": "しきい値アラートを作成", + "xpack.apm.serviceDetails.alertsMenu.errorCount": "エラー数", "xpack.apm.serviceDetails.alertsMenu.transactionDuration": "トランザクション期間", + "xpack.apm.serviceDetails.alertsMenu.transactionErrorRate": "トランザクションエラー率", "xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "アクティブアラートを表示", "xpack.apm.serviceDetails.errorsTabLabel": "エラー", "xpack.apm.serviceDetails.metrics.cpuUsageChartTitle": "CPU 使用状況", @@ -4752,6 +5069,10 @@ "xpack.apm.serviceDetails.metricsTabLabel": "メトリック", "xpack.apm.serviceDetails.nodesTabLabel": "JVM", "xpack.apm.serviceDetails.transactionsTabLabel": "トランザクション", + "xpack.apm.serviceHealthStatus.critical": "重大", + "xpack.apm.serviceHealthStatus.healthy": "正常", + "xpack.apm.serviceHealthStatus.unknown": "不明", + "xpack.apm.serviceHealthStatus.warning": "警告", "xpack.apm.serviceMap.anomalyDetectionPopoverDisabled": "APM設定で異常検知を有効にすると、サービス正常性インジケーターが表示されます。", "xpack.apm.serviceMap.anomalyDetectionPopoverLink": "異常を表示", "xpack.apm.serviceMap.anomalyDetectionPopoverNoData": "選択した時間範囲で、異常スコアを検出できませんでした。異常エクスプローラーで詳細を確認してください。", @@ -4770,7 +5091,10 @@ "xpack.apm.serviceMap.errorRatePopoverStat": "トランザクションエラー率(平均)", "xpack.apm.serviceMap.focusMapButtonText": "焦点マップ", "xpack.apm.serviceMap.invalidLicenseMessage": "サービスマップを利用するには、Elastic Platinum ライセンスが必要です。これにより、APM データとともにアプリケーションスタック全てを可視化することができるようになります。", + "xpack.apm.serviceMap.noServicesPromptDescription": "現在選択されている時間範囲と環境内では、マッピングするサービスが見つかりません。別の範囲を試すか、選択した環境を確認してください。サービスがない場合は、セットアップ手順に従って開始してください。", + "xpack.apm.serviceMap.noServicesPromptTitle": "サービスが利用できません", "xpack.apm.serviceMap.popoverMetrics.noDataText": "選択した環境のデータがありません。別の環境に切り替えてください。", + "xpack.apm.serviceMap.resourceCountLabel": "{count}個のリソース", "xpack.apm.serviceMap.serviceDetailsButtonText": "サービス詳細", "xpack.apm.serviceMap.subtypePopoverStat": "サブタイプ", "xpack.apm.serviceMap.typePopoverStat": "タイプ", @@ -4784,6 +5108,10 @@ "xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningText": "これらのメトリックが所属する JVM を特定できませんでした。7.5 よりも古い APM Server を実行していることが原因である可能性が高いです。この問題は APM Server 7.5 以降にアップグレードすることで解決されます。アップグレードに関する詳細は、{link} をご覧ください。代わりに Kibana クエリバーを使ってホスト名、コンテナー ID、またはその他フィールドでフィルタリングすることもできます。", "xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningTitle": "JVM を特定できませんでした", "xpack.apm.serviceNodeNameMissing": "(空)", + "xpack.apm.serviceOverview.mlNudgeMessage.content": "ML異常検知との統合により、サービスの正常性ステータスを確認できます", + "xpack.apm.serviceOverview.mlNudgeMessage.dismissButton": "メッセージを消去", + "xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton": "詳細", + "xpack.apm.serviceOverview.mlNudgeMessage.title": "異常検知を有効にしてサービスのヘルスを確認", "xpack.apm.serviceOverview.toastText": "現在 Elastic Stack 7.0+ を実行中で、以前のバージョン 6.x からの互換性のないデータを検知しました。このデータを APM で表示するには、移行が必要です。詳細: ", "xpack.apm.serviceOverview.toastTitle": "選択された時間範囲内にレガシーデータが検知されました。", "xpack.apm.serviceOverview.upgradeAssistantLink": "アップグレードアシスタント", @@ -4792,9 +5120,11 @@ "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均応答時間", "xpack.apm.servicesTable.environmentColumnLabel": "環境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 個の環境} other {# 個の環境}}", + "xpack.apm.servicesTable.healthColumnLabel": "ヘルス", "xpack.apm.servicesTable.nameColumnLabel": "名前", "xpack.apm.servicesTable.noServicesLabel": "APM サービスがインストールされていないようです。追加しましょう!", "xpack.apm.servicesTable.notFoundLabel": "サービスが見つかりません", + "xpack.apm.servicesTable.transactionErrorRate": "エラー率%", "xpack.apm.servicesTable.transactionsPerMinuteColumnLabel": "1 分あたりのトランザクション", "xpack.apm.servicesTable.transactionsPerMinuteUnitLabel": "1分あたりトランザクション数", "xpack.apm.servicesTable.UpgradeAssistantLink": "Kibana アップグレードアシスタントで詳細をご覧ください", @@ -4969,10 +5299,14 @@ "xpack.apm.transactionDurationAlert.aggregationType.95th": "95 パーセンタイル", "xpack.apm.transactionDurationAlert.aggregationType.99th": "99 パーセンタイル", "xpack.apm.transactionDurationAlert.aggregationType.avg": "平均", - "xpack.apm.transactionDurationAlert.name": "トランザクション期間", + "xpack.apm.transactionDurationAlert.name": "トランザクション期間のしきい値", "xpack.apm.transactionDurationAlertTrigger.ms": "ms", "xpack.apm.transactionDurationAlertTrigger.when": "タイミング", + "xpack.apm.transactionDurationAnomalyAlert.name": "トランザクション期間異常", + "xpack.apm.transactionDurationAnomalyAlertTrigger.anomalySeverity": "異常と重要度があります", "xpack.apm.transactionDurationLabel": "期間", + "xpack.apm.transactionErrorRateAlert.name": "トランザクションエラー率しきい値", + "xpack.apm.transactionErrorRateAlertTrigger.isAbove": "より大きい", "xpack.apm.transactions.chart.95thPercentileLabel": "95 パーセンタイル", "xpack.apm.transactions.chart.99thPercentileLabel": "99 パーセンタイル", "xpack.apm.transactions.chart.anomalyBoundariesLabel": "異常境界", @@ -4990,8 +5324,19 @@ "xpack.apm.tutorial.elasticCloud.textPre": "APM Server を有効にするには、[the Elastic Cloud console](https://cloud.elastic.co/deployments?q={cloudId}) に移動し、展開設定で APM を有効にします。有効になったら、このページを更新してください。", "xpack.apm.tutorial.elasticCloudInstructions.title": "APM エージェント", "xpack.apm.tutorial.specProvider.artifacts.application.label": "APM を起動", + "xpack.apm.uifilter.badge.removeFilter": "フィルターを削除", "xpack.apm.unitLabel": "単位を選択", "xpack.apm.unsavedChanges": "{unsavedChangesCount, plural, =0{0 未保存変更} one {1 未保存変更} other {# 未保存変更}} ", + "xpack.apm.ux.jsErrors.percent": "{pageLoadPercent} %", + "xpack.apm.ux.localFilters.titles.webApplication": "Webアプリケーション", + "xpack.apm.ux.percentile.50thMedian": "50番目(中央値)", + "xpack.apm.ux.percentile.75th": "75番目", + "xpack.apm.ux.percentile.90th": "90番目", + "xpack.apm.ux.percentile.95th": "95番目", + "xpack.apm.ux.percentile.99th": "99番目", + "xpack.apm.ux.percentile.label": "パーセンタイル", + "xpack.apm.ux.title": "ユーザーエクスペリエンス", + "xpack.apm.ux.visitorBreakdown.noData": "データがありません。", "xpack.apm.version": "バージョン", "xpack.apm.waterfall.exceedsMax": "このトレースの項目数は表示されている範囲を超えています", "xpack.beatsManagement.beat.actionSectionTypeLabel": "タイプ: {beatType}。", @@ -5186,12 +5531,16 @@ "xpack.canvas.argFormArgSimpleForm.requiredTooltip": "この引数は必須です。数値を入力してください。", "xpack.canvas.argFormPendingArgValue.loadingMessage": "読み込み中", "xpack.canvas.argFormSimpleFailure.failureTooltip": "この引数のインターフェースが値を解析できなかったため、フォールバックインプットが使用されています", + "xpack.canvas.asset.confirmModalButtonLabel": "削除", + "xpack.canvas.asset.confirmModalDetail": "このアセットを削除してよろしいですか?", + "xpack.canvas.asset.confirmModalTitle": "アセットの削除", "xpack.canvas.asset.copyAssetTooltip": "ID をクリップボードにコピー", "xpack.canvas.asset.createImageTooltip": "画像エレメントを作成", "xpack.canvas.asset.deleteAssetTooltip": "削除", "xpack.canvas.asset.downloadAssetTooltip": "ダウンロード", "xpack.canvas.asset.thumbnailAltText": "アセットのサムネイル", "xpack.canvas.assetManager.manageButtonLabel": "アセットの管理", + "xpack.canvas.assetModal.copyAssetMessage": "「{id}」をクリップボードにコピーしました", "xpack.canvas.assetModal.emptyAssetsDescription": "アセットをインポートして開始します", "xpack.canvas.assetModal.filePickerPromptText": "画像を選択するかドラッグ &amp; ドロップしてください", "xpack.canvas.assetModal.loadingText": "画像をアップロード中", @@ -5215,6 +5564,7 @@ "xpack.canvas.customElementModal.remainingCharactersDescription": "残り {numberOfRemainingCharacter} 文字", "xpack.canvas.customElementModal.saveButtonLabel": "保存", "xpack.canvas.datasourceDatasourceComponent.changeButtonLabel": "要素データソースの変更", + "xpack.canvas.datasourceDatasourceComponent.expressionArgDescription": "データソースの引数は式で制御されます。式エディターを使用して、データソースを修正します。", "xpack.canvas.datasourceDatasourceComponent.previewButtonLabel": "データをプレビュー", "xpack.canvas.datasourceDatasourceComponent.saveButtonLabel": "保存", "xpack.canvas.datasourceDatasourcePreview.emptyFirstLineDescription": "検索条件に一致するドキュメントが見つかりませんでした。", @@ -5357,6 +5707,7 @@ "xpack.canvas.expressionTypes.argTypes.seriesStyle.styleLabel": "スタイル", "xpack.canvas.expressionTypes.argTypes.seriesStyleLabel": "選択された名前付きの数列のスタイルを設定", "xpack.canvas.expressionTypes.argTypes.seriesStyleTitle": "数列スタイル", + "xpack.canvas.featureCatalogue.canvasSubtitle": "詳細まで正確なレポートを設計します。", "xpack.canvas.functionForm.contextError": "エラー: {errorMessage}", "xpack.canvas.functionForm.functionUnknown.unknownArgumentTypeError": "未知の表現タイプ「{expressionType}」", "xpack.canvas.functions.all.args.conditionHelpText": "確認する条件です。", @@ -5366,25 +5717,25 @@ "xpack.canvas.functions.alterColumn.args.typeHelpText": "列の変換語のタイプです。タイプを変更しない場合は未入力のままにします。", "xpack.canvas.functions.alterColumn.cannotConvertTypeErrorMessage": "「{type}」に変換できません", "xpack.canvas.functions.alterColumn.columnNotFoundErrorMessage": "列が見つかりません: 「{column}」", - "xpack.canvas.functions.alterColumnHelpText": "{list}、{end} などのコアタイプを変換し、列名を変更します。{mapColumnFn} および {staticColumnFn} もご参照ください。", + "xpack.canvas.functions.alterColumnHelpText": "{list}、{end}などのコアタイプを変換し、列名を変更します。{mapColumnFn}および{staticColumnFn}も参照してください。", "xpack.canvas.functions.any.args.conditionHelpText": "確認する条件です。", "xpack.canvas.functions.anyHelpText": "少なくとも 1 つの条件が満たされている場合、{BOOLEAN_TRUE} が返されます。{all_fn} もご参照ください。", - "xpack.canvas.functions.as.args.nameHelpText": "列に付ける名前です", + "xpack.canvas.functions.as.args.nameHelpText": "列に付ける名前です。", "xpack.canvas.functions.asHelpText": "単一の値で {DATATABLE} を作成します。{getCellFn} もご参照ください。", "xpack.canvas.functions.asset.args.id": "読み込むアセットの ID です。", "xpack.canvas.functions.asset.invalidAssetId": "ID「{assetId}」でアセットを取得できませんでした", "xpack.canvas.functions.assetHelpText": "引数値を提供するために、Canvas ワークパッドアセットオブジェクトを取得します。通常画像です。", - "xpack.canvas.functions.axisConfig.args.maxHelpText": "軸に表示する最高値です。数字または新世紀からのミリ秒単位の日付、もしくは {ISO8601} 文字列でなければなりません。", - "xpack.canvas.functions.axisConfig.args.minHelpText": "軸に表示する最低値です。数字または新世紀からのミリ秒単位の日付、もしくは {ISO8601} 文字列でなければなりません。", + "xpack.canvas.functions.axisConfig.args.maxHelpText": "軸に表示する最高値です。数字または新世紀からの日付(ミリ秒単位)、もしくは {ISO8601} 文字列でなければなりません。", + "xpack.canvas.functions.axisConfig.args.minHelpText": "軸に表示する最低値です。数字または新世紀からの日付(ミリ秒単位)、もしくは {ISO8601} 文字列でなければなりません。", "xpack.canvas.functions.axisConfig.args.positionHelpText": "軸ラベルの配置です。例: {list} または {end}。", "xpack.canvas.functions.axisConfig.args.showHelpText": "軸ラベルを表示しますか?", - "xpack.canvas.functions.axisConfig.args.tickSizeHelpText": "目盛り間の増加量です。「数字」軸のみで使用されます", + "xpack.canvas.functions.axisConfig.args.tickSizeHelpText": "目盛間の増加量です。「数字」軸のみで使用されます。", "xpack.canvas.functions.axisConfig.invalidMaxPositionErrorMessage": "無効なデータ文字列: 「{max}」。「max」は数字、ms での日付、または ISO8601 データ文字列出なければなりません", "xpack.canvas.functions.axisConfig.invalidMinDateStringErrorMessage": "無効なデータ文字列: 「{min}」。「min」は数字、ms での日付、または ISO8601 データ文字列出なければなりません", "xpack.canvas.functions.axisConfig.invalidPositionErrorMessage": "無効なポジション: 「{position}」", "xpack.canvas.functions.axisConfigHelpText": "ビジュアライゼーションの軸を構成します。{plotFn} でのみ使用されます。", - "xpack.canvas.functions.case.args.ifHelpText": "この値は、条件が満たされているかを示し、通常部分式を使用します。両方が入力された場合、{IF_ARG} 引数が {WHEN_ARG} 引数を上書きします。", - "xpack.canvas.functions.case.args.thenHelpText": "条件が満たされた際に戻る値です。", + "xpack.canvas.functions.case.args.ifHelpText": "この値は、条件が満たされているかどうかを示します。両方が入力された場合、{IF_ARG}引数が{WHEN_ARG}引数を上書きします。", + "xpack.canvas.functions.case.args.thenHelpText": "条件が満たされた際に返される値です。", "xpack.canvas.functions.case.args.whenHelpText": "等しいかを確認するために {CONTEXT} と比較される値です。{IF_ARG} 引数も指定されている場合、{WHEN_ARG} 引数は無視されます。", "xpack.canvas.functions.caseHelpText": "{switchFn} 関数に渡すため、条件と結果を含めて {case} を作成します。", "xpack.canvas.functions.clearHelpText": "{CONTEXT} を消去し、{TYPE_NULL} を返します。", @@ -5394,7 +5745,7 @@ "xpack.canvas.functions.compare.args.opHelpText": "比較で使用する演算子です: {eq} (equal to)、{gt} (greater than)、{gte} (greater than or equal to)、{lt} (less than)、{lte} (less than or equal to)、{ne} または {neq} (not equal to)", "xpack.canvas.functions.compare.args.toHelpText": "{CONTEXT} と比較される値です。", "xpack.canvas.functions.compare.invalidCompareOperatorErrorMessage": "無効な比較演算子: 「{op}」。{ops} を使用してください", - "xpack.canvas.functions.compareHelpText": "{CONTEXT} を指定された値と比較し、{BOOLEAN_TRUE} または {BOOLEAN_FALSE} を決定します。通常「{ifFn}」または「{caseFn}」と組み合わせて使用されます。{examples} などの基本タイプにのみ使用できます。「{eqFn}」、「{gtFn}」、「{gteFn}」、「{ltFn}」、「{lteFn}」、「{neqFn}」もご参照ください", + "xpack.canvas.functions.compareHelpText": "{CONTEXT}を指定された値と比較し、{BOOLEAN_TRUE}または{BOOLEAN_FALSE}を決定します。通常「{ifFn}」または「{caseFn}」と組み合わせて使用されます。{examples}などの基本タイプにのみ使用できます。{eqFn}、{gtFn}、{gteFn}、{ltFn}、{lteFn}、{neqFn}も参照してください。", "xpack.canvas.functions.containerStyle.args.backgroundColorHelpText": "有効な {CSS} 背景色。", "xpack.canvas.functions.containerStyle.args.backgroundImageHelpText": "有効な {CSS} 背景画像。", "xpack.canvas.functions.containerStyle.args.backgroundRepeatHelpText": "有効な {CSS} 背景繰り返し。", @@ -5412,7 +5763,6 @@ "xpack.canvas.functions.csv.args.newlineHelpText": "行の区切り文字です。", "xpack.canvas.functions.csv.invalidInputCSVErrorMessage": "インプット CSV の解析中にエラーが発生しました。", "xpack.canvas.functions.csvHelpText": "{CSV} インプットから {DATATABLE} を作成します。", - "xpack.canvas.functions.date.args.formatHelpText": "指定された日付文字列の解析に使用される {MOMENTJS} フォーマットです。{url} を参照。", "xpack.canvas.functions.date.args.valueHelpText": "新紀元からのミリ秒に解析するオプションの日付文字列です。日付文字列には、有効な {JS} {date} インプット、または {formatArg} 引数を使用して解析する文字列のどちらかが使用できます。{ISO8601} 文字列を使用するか、フォーマットを提供する必要があります。", "xpack.canvas.functions.date.invalidDateInputErrorMessage": "無効な日付インプット: {date}", "xpack.canvas.functions.dateHelpText": "現在時刻、または指定された文字列から解析された時刻を、新紀元からのミリ秒で返します。", @@ -5426,7 +5776,7 @@ "xpack.canvas.functions.dropdownControl.args.valueColumnHelpText": "ドロップダウンコントロールの固有値を抽出する元の列またはフィールドです。", "xpack.canvas.functions.dropdownControlHelpText": "ドロップダウンフィルターのコントロールエレメントを構成します。", "xpack.canvas.functions.eq.args.valueHelpText": "{CONTEXT} と比較される値です。", - "xpack.canvas.functions.eqHelpText": "{CONTEXT} が引数と等しいかを戻します。", + "xpack.canvas.functions.eqHelpText": "{CONTEXT}が引数と等しいかを戻します。", "xpack.canvas.functions.escount.args.indexHelpText": "インデックスまたはインデックスパターンです。例: {example}。", "xpack.canvas.functions.escount.args.queryHelpText": "{LUCENE} クエリ文字列です。", "xpack.canvas.functions.escountHelpText": "{ELASTICSEARCH} にクエリを実行して、指定されたクエリに一致するヒット数を求めます。", @@ -5446,39 +5796,40 @@ "xpack.canvas.functions.exactly.args.valueHelpText": "ホワイトスペースと大文字・小文字を含め、正確に一致させる値です。", "xpack.canvas.functions.exactlyHelpText": "特定の列をピッタリと正確な値に一致させるフィルターを作成します。", "xpack.canvas.functions.filterrows.args.fnHelpText": "{DATATABLE} の各行に渡す表現式です。表現式は {TYPE_BOOLEAN} に戻ります。{BOOLEAN_TRUE} 値は行を維持し、{BOOLEAN_FALSE} 値は行を削除します。", - "xpack.canvas.functions.filterrowsHelpText": "{DATATABLE} の行を部分式の戻り値に基づきフィルタリングします。", + "xpack.canvas.functions.filterrowsHelpText": "{DATATABLE}の行を部分式の戻り値に基づきフィルタリングします。", "xpack.canvas.functions.filters.args.group": "使用するフィルターグループの名前です。", "xpack.canvas.functions.filters.args.ungrouped": "フィルターグループに属するフィルターを除外しますか?", "xpack.canvas.functions.filtersHelpText": "ワークパッドのエレメントフィルターを他 (通常データソース) で使用できるように集約します。", "xpack.canvas.functions.formatdate.args.formatHelpText": "{MOMENTJS} フォーマットです。例: {example}。{url} を参照。", "xpack.canvas.functions.formatdateHelpText": "{MOMENTJS} を使って {ISO8601} 日付文字列、または新世紀からのミリ秒での日付をフォーマットします。{url} を参照。", "xpack.canvas.functions.formatnumber.args.formatHelpText": "{NUMERALJS} 形式の文字列。例: {example1} または {example2}。", - "xpack.canvas.functions.formatnumberHelpText": "{NUMERALJS} を使って数字をフォーマットされた数字文字列にフォーマットします。", + "xpack.canvas.functions.formatnumberHelpText": "{NUMERALJS}を使って数字をフォーマットされた数字文字列にフォーマットします。", "xpack.canvas.functions.getCell.args.columnHelpText": "値を取得する元の列の名前です。この値は入力されていないと、初めの列から取得されます。", "xpack.canvas.functions.getCell.args.rowHelpText": "行番号で、0 から開始します。", "xpack.canvas.functions.getCell.columnNotFoundErrorMessage": "列が見つかりません: 「{column}」", "xpack.canvas.functions.getCell.rowNotFoundErrorMessage": "行が見つかりません: 「{row}」", - "xpack.canvas.functions.getCellHelpText": "{DATATABLE} から単一のセルを取得します。", + "xpack.canvas.functions.getCellHelpText": "{DATATABLE}から単一のセルを取得します。", "xpack.canvas.functions.gt.args.valueHelpText": "{CONTEXT} と比較される値です。", "xpack.canvas.functions.gte.args.valueHelpText": "{CONTEXT} と比較される値です。", "xpack.canvas.functions.gteHelpText": "{CONTEXT} が引数以上かを戻します。", "xpack.canvas.functions.gtHelpText": "{CONTEXT} が引数よりも大きいかを戻します。", "xpack.canvas.functions.head.args.countHelpText": "{DATATABLE} の初めから取得する行数です。", - "xpack.canvas.functions.headHelpText": "{DATATABLE} から初めの {n} 行を取得します。{tailFn} もご参照ください", + "xpack.canvas.functions.headHelpText": "{DATATABLE}から初めの{n}行を取得します。{tailFn}を参照してください。", "xpack.canvas.functions.if.args.conditionHelpText": "{BOOLEAN_TRUE} または {BOOLEAN_FALSE} で、条件が満たされているかを示し、通常部分式から戻されます。指定されていない場合、元の {CONTEXT} が戻されます。", "xpack.canvas.functions.if.args.elseHelpText": "条件が {BOOLEAN_FALSE} の場合の戻り値です。指定されておらず、条件が満たされていない場合は、元の {CONTEXT} が戻されます。", "xpack.canvas.functions.if.args.thenHelpText": "条件が {BOOLEAN_TRUE} の場合の戻り値です。指定されておらず、条件が満たされている場合は、元の {CONTEXT} が戻されます。", - "xpack.canvas.functions.ifHelpText": "条件付きロジックを実行します", + "xpack.canvas.functions.ifHelpText": "条件付きロジックを実行します。", "xpack.canvas.functions.image.args.dataurlHelpText": "画像の {https} {URL} または {BASE64} データ {URL} です。", "xpack.canvas.functions.image.args.modeHelpText": "{contain} はサイズに合わせて拡大・縮小して画像全体を表示し、{cover} はコンテナーを画像で埋め、必要に応じて両端や下をクロップします。{stretch} は画像の高さと幅をコンテナーの 100% になるよう変更します", "xpack.canvas.functions.image.invalidImageModeErrorMessage": "「mode」は「{contain}」、「{cover}」、または「{stretch}」でなければなりません", "xpack.canvas.functions.imageHelpText": "画像を表示します。画像アセットを {BASE64} データ {URL} として提供するか、部分式で渡します。", - "xpack.canvas.functions.joinRows.args.columnHelpText": "値を結合する元になる列", - "xpack.canvas.functions.joinRows.args.distinctHelpText": "重複する値を削除しますか?", - "xpack.canvas.functions.joinRows.args.quoteHelpText": "値を囲む引用文字", - "xpack.canvas.functions.joinRows.args.separatorHelpText": "行の値の間で使用する区切り文字", + "xpack.canvas.functions.joinRows.args.columnHelpText": "値を抽出する列またはフィールド。", + "xpack.canvas.functions.joinRows.args.distinctHelpText": "一意の値のみを抽出しますか?", + "xpack.canvas.functions.joinRows.args.quoteHelpText": "各抽出された値を囲む引用符文字。", + "xpack.canvas.functions.joinRows.args.separatorHelpText": "各抽出された値の間に挿入される区切り文字。", "xpack.canvas.functions.joinRows.columnNotFoundErrorMessage": "列が見つかりません。'{column}'", - "xpack.canvas.functions.joinRowsHelpText": "データベースの行の値を文字列に結合", + "xpack.canvas.functions.joinRowsHelpText": "「データベース」の行の値を1つの文字列に結合します。", + "xpack.canvas.functions.locationHelpText": "ブラウザーの{geolocationAPI}を使用して現在の位置情報を取得します。パフォーマンスに違いはありますが、比較的正確です。{url}を参照してください。この関数にはユーザー入力が必要であるため、PDFを生成する場合は、{locationFn}を使用しないでください。", "xpack.canvas.functions.lt.args.valueHelpText": "{CONTEXT} と比較される値です。", "xpack.canvas.functions.lte.args.valueHelpText": "{CONTEXT} と比較される値です。", "xpack.canvas.functions.lteHelpText": "{CONTEXT} が引数以下かを戻します。", @@ -5487,15 +5838,17 @@ "xpack.canvas.functions.mapCenterHelpText": "マップの中央座標とズームレベルのオブジェクトに戻ります。", "xpack.canvas.functions.mapColumn.args.expressionHelpText": "単一行 {DATATABLE} として各行に渡される {CANVAS} 表現です。", "xpack.canvas.functions.mapColumn.args.nameHelpText": "結果の列の名前です。", + "xpack.canvas.functions.mapColumnHelpText": "他の列の結果として計算された列を追加します。引数が指定された場合のみ変更が加えられます。{alterColumnFn}と{staticColumnFn}もご参照ください。", "xpack.canvas.functions.markdown.args.contentHelpText": "{MARKDOWN} を含むテキストの文字列です。連結させるには、{stringFn} 関数を複数回渡します。", "xpack.canvas.functions.markdown.args.fontHelpText": "コンテンツの {CSS} フォントプロパティです。例: {fontFamily} または {fontWeight}。", - "xpack.canvas.functions.markdown.args.openLinkHelpText": "新しいタブでリンクを開くための true/false 値。デフォルト値は false です。true に設定するとすべてのリンクが新しいタブで開くようになります。", + "xpack.canvas.functions.markdown.args.openLinkHelpText": "新しいタブでリンクを開くためのtrue/false値。デフォルト値は「false」です。「true」に設定するとすべてのリンクが新しいタブで開くようになります。", "xpack.canvas.functions.markdownHelpText": "{MARKDOWN} テキストをレンダリングするエレメントを追加します。ヒント:単一の数字、メトリック、テキストの段落には {markdownFn} 関数を使います。", "xpack.canvas.functions.math.args.expressionHelpText": "評価された {TINYMATH} 表現です。{TINYMATH_URL} をご覧ください。", "xpack.canvas.functions.math.emptyDatatableErrorMessage": "空のデータベース", "xpack.canvas.functions.math.emptyExpressionErrorMessage": "空の表現", "xpack.canvas.functions.math.executionFailedErrorMessage": "数式の実行に失敗しました。列名を確認してください", "xpack.canvas.functions.math.tooManyResultsErrorMessage": "表現は 1 つの数字を返す必要があります。表現を {mean} または {sum} で囲んでみてください", + "xpack.canvas.functions.mathHelpText": "{TYPE_NUMBER}または{DATATABLE}を{CONTEXT}として使用して、{TINYMATH}数式を解釈します。{DATATABLE}列は列名で表示されます。{CONTEXT}が数字の場合は、{value}と表示されます。", "xpack.canvas.functions.metric.args.labelFontHelpText": "ラベルの {CSS} フォントプロパティです。例: {FONT_FAMILY} または {FONT_WEIGHT}。", "xpack.canvas.functions.metric.args.labelHelpText": "メトリックを説明するテキストです。", "xpack.canvas.functions.metric.args.metricFontHelpText": "メトリックの {CSS} フォントプロパティです。例: {FONT_FAMILY} または {FONT_WEIGHT}。", @@ -5511,32 +5864,36 @@ "xpack.canvas.functions.pie.args.holeHelpText": "円グラフに穴をあけます、0~100 で円グラフの半径のパーセンテージを指定します。", "xpack.canvas.functions.pie.args.labelRadiusHelpText": "ラベルの円の半径として使用する、コンテナーの面積のパーセンテージです。", "xpack.canvas.functions.pie.args.labelsHelpText": "円グラフのラベルを表示しますか?", + "xpack.canvas.functions.pie.args.legendHelpText": "凡例の配置です。例: {legend}、または {BOOLEAN_FALSE}。{BOOLEAN_FALSE}の場合、凡例は非表示になります。", + "xpack.canvas.functions.pie.args.paletteHelpText": "この円グラフに使用されている色を説明する{palette}オブジェクトです。", "xpack.canvas.functions.pie.args.radiusHelpText": "利用可能なスペースのパーセンテージで示された円グラフの半径です (0 から 1 の間)。半径を自動的に設定するには {auto} を使用します。", "xpack.canvas.functions.pie.args.seriesStyleHelpText": "特定の数列のスタイルです", "xpack.canvas.functions.pie.args.tiltHelpText": "「1」 が完全に垂直、「0」が完全に水平を表す傾きのパーセンテージです。", "xpack.canvas.functions.pieHelpText": "円グラフのエレメントを構成します。", "xpack.canvas.functions.plot.args.defaultStyleHelpText": "すべての数列に使用するデフォルトのスタイルです。", "xpack.canvas.functions.plot.args.fontHelpText": "表の {CSS} フォントプロパティです。例: {FONT_FAMILY} または {FONT_WEIGHT}。", + "xpack.canvas.functions.plot.args.legendHelpText": "凡例の配置です。例: {legend}、または {BOOLEAN_FALSE}。{BOOLEAN_FALSE}の場合、凡例は非表示になります。", + "xpack.canvas.functions.plot.args.paletteHelpText": "このチャートに使用される色を説明する{palette}オブジェクトです。", "xpack.canvas.functions.plot.args.seriesStyleHelpText": "特定の数列のスタイルです", "xpack.canvas.functions.plot.args.xaxisHelpText": "軸の構成です。{BOOLEAN_FALSE} の場合、軸は非表示になります。", "xpack.canvas.functions.plot.args.yaxisHelpText": "軸の構成です。{BOOLEAN_FALSE} の場合、軸は非表示になります。", - "xpack.canvas.functions.plotHelpText": "チャートのエレメントを構成します", + "xpack.canvas.functions.plotHelpText": "チャートのエレメントを構成します。", "xpack.canvas.functions.ply.args.byHelpText": "{DATATABLE} を細分する列です。", "xpack.canvas.functions.ply.args.expressionHelpText": "それぞれの結果の {DATATABLE} を渡す先の表現です。アドバイス:表現は {DATATABLE} を返す必要があります。直定数を {DATATABLE} にするには {asFn} を使用します。複数表現が同じ行数を戻す必要があります。異なる行数を戻す必要がある場合は、{plyFn} の別のインスタンスにパイピングします。複数表現が同じ名前の行を戻した場合、最後の行が優先されます。", "xpack.canvas.functions.ply.columnNotFoundErrorMessage": "列が見つかりません: 「{by}」", "xpack.canvas.functions.ply.rowCountMismatchErrorMessage": "すべての表現が同じ行数を返す必要があります。", - "xpack.canvas.functions.plyHelpText": "{DATATABLE} を指定された列の固有値で細分し、表現にその結果となる表を渡し、各表現のアウトプットを結合します。", + "xpack.canvas.functions.plyHelpText": "{DATATABLE}を指定された列の固有値で細分し、表現にその結果となる表を渡し、各表現のアウトプットを結合します。", "xpack.canvas.functions.pointseries.args.colorHelpText": "マークの色を決めるのに使用する表現です。", "xpack.canvas.functions.pointseries.args.sizeHelpText": "マークのサイズです。サポートされているエレメントのみに適用されます。", "xpack.canvas.functions.pointseries.args.textHelpText": "マークに表示するテキストです。サポートされているエレメントのみに適用されます。", - "xpack.canvas.functions.pointseries.args.xHelpText": "X 軸の値です。", - "xpack.canvas.functions.pointseries.args.yHelpText": "Y 軸の値です。", + "xpack.canvas.functions.pointseries.args.xHelpText": "X軸の値です。", + "xpack.canvas.functions.pointseries.args.yHelpText": "Y軸の値です。", "xpack.canvas.functions.pointseries.unwrappedExpressionErrorMessage": "表現は {fn} などの関数で囲む必要があります", "xpack.canvas.functions.pointseriesHelpText": "{DATATABLE} を点の配列モデルに変換します。現在 {TINYMATH} 表現でディメンションのメジャーを区別します。{TINYMATH_URL} をご覧ください。引数に {TINYMATH} 表現が入力された場合、その引数をメジャーとして使用し、そうでない場合はディメンションになります。ディメンションを組み合わせて固有のキーを作成します。その後メジャーはそれらのキーで、指定された {TINYMATH} 関数を使用して複製されます。", "xpack.canvas.functions.progress.args.barColorHelpText": "背景バーの色です。", "xpack.canvas.functions.progress.args.barWeightHelpText": "背景バーの太さです。", "xpack.canvas.functions.progress.args.fontHelpText": "ラベルの {CSS} フォントプロパティです。例: {FONT_FAMILY} または {FONT_WEIGHT}。", - "xpack.canvas.functions.progress.args.labelHelpText": "ラベルの表示・非表示を切り替えるには、{BOOLEAN_TRUE} または {BOOLEAN_FALSE} を使用します。また、ラベルとして表示する文字列を入力することもできます。", + "xpack.canvas.functions.progress.args.labelHelpText": "ラベルの表示・非表示を切り替えるには、{BOOLEAN_TRUE}または{BOOLEAN_FALSE}を使用します。また、ラベルとして表示する文字列を入力することもできます。", "xpack.canvas.functions.progress.args.maxHelpText": "進捗エレメントの最高値です。", "xpack.canvas.functions.progress.args.shapeHelpText": "{list} または {end} を選択します。", "xpack.canvas.functions.progress.args.valueColorHelpText": "進捗バーの色です。", @@ -5547,8 +5904,8 @@ "xpack.canvas.functions.render.args.asHelpText": "レンダリングに使用するエレメントタイプです。代わりに {plotFn} や {shapeFn} などの特殊な関数を使用するほうがいいでしょう。", "xpack.canvas.functions.render.args.containerStyleHelpText": "背景、境界、透明度を含む、コンテナーのスタイルです。", "xpack.canvas.functions.render.args.cssHelpText": "このエレメントの対象となるカスタム {CSS} のブロックです。", - "xpack.canvas.functions.renderHelpText": "{CONTEXT} を特定のエレメントとしてレンダリングし、背景と境界のスタイルなどのエレメントレベルのオプションを設定します。", - "xpack.canvas.functions.repeatImage.args.emptyImageHelpText": "{CONTEXT} と {maxArg} パラメーターの間をこの画像で埋めます。画像アセットは {BASE64} データ {URL} として提供するか、部分式で渡します。", + "xpack.canvas.functions.renderHelpText": "{CONTEXT}を特定のエレメントとしてレンダリングし、背景と境界のスタイルなどのエレメントレベルのオプションを設定します。", + "xpack.canvas.functions.repeatImage.args.emptyImageHelpText": "この画像のエレメントについて、{CONTEXT}および{maxArg}パラメーターの差異を解消します。画像アセットは{BASE64}データ{URL}として提供するか、部分式で渡します。", "xpack.canvas.functions.repeatImage.args.imageHelpText": "繰り返す画像です。画像アセットを {BASE64} データ {URL} として提供するか、部分式で渡します。", "xpack.canvas.functions.repeatImage.args.maxHelpText": "画像が繰り返される最高回数です。", "xpack.canvas.functions.repeatImage.args.sizeHelpText": "画像の高さまたは幅のピクセル単位での最高値です。画像が縦長の場合、この関数は高さを制限します。", @@ -5562,13 +5919,13 @@ "xpack.canvas.functions.revealImage.args.originHelpText": "画像で埋め始める位置です。例: {list} または {end}。", "xpack.canvas.functions.revealImage.invalidPercentErrorMessage": "無効な値: 「{percent}」.パーセンテージは 0 と 1 の間でなければなりません", "xpack.canvas.functions.revealImageHelpText": "画像表示エレメントを構成します。", - "xpack.canvas.functions.rounddate.args.formatHelpText": "バケットに使用する {MOMENTJS} フォーマットです。たとえば、{example} はそれぞれの日付を月単位に繰り上げ・繰り下げします。{url} を参照。", + "xpack.canvas.functions.rounddate.args.formatHelpText": "バケットに使用する{MOMENTJS}フォーマットです。たとえば、{example}は月単位に端数処理されます。{url}を参照してください。", "xpack.canvas.functions.rounddateHelpText": "新世紀からのミリ秒の繰り上げ・繰り下げに {MOMENTJS} を使用し、新世紀からのミリ秒を戻します。", - "xpack.canvas.functions.rowCountHelpText": "行数を戻します。{plyFn} と組み合わせて、固有の列値の数、または固有の列値の組み合わせを求めます。", - "xpack.canvas.functions.savedLens.args.idHelpText": "保存されたレンズオブジェクトのID", + "xpack.canvas.functions.rowCountHelpText": "行数を返します。{plyFn}と組み合わせて、固有の列値の数、または固有の列値の組み合わせを求めます。", + "xpack.canvas.functions.savedLens.args.idHelpText": "保存されたLensビジュアライゼーションオブジェクトの ID", "xpack.canvas.functions.savedLens.args.timerangeHelpText": "含めるデータの時間範囲", - "xpack.canvas.functions.savedLens.args.titleHelpText": "レンズ埋め込み可能オブジェクトのタイトル", - "xpack.canvas.functions.savedLensHelpText": "保存されたレンズオブジェクトの埋め込み可能オブジェクトを返します", + "xpack.canvas.functions.savedLens.args.titleHelpText": "Lensビジュアライゼーションオブジェクトのタイトル", + "xpack.canvas.functions.savedLensHelpText": "保存されたLensビジュアライゼーションオブジェクトの埋め込み可能なオブジェクトを返します", "xpack.canvas.functions.savedMap.args.centerHelpText": "マップが持つ必要のある中央とズームレベル", "xpack.canvas.functions.savedMap.args.hideLayer": "非表示にすべきマップレイヤーのID", "xpack.canvas.functions.savedMap.args.idHelpText": "保存されたマップオブジェクトのID", @@ -5576,20 +5933,21 @@ "xpack.canvas.functions.savedMap.args.timerangeHelpText": "含めるべきデータの時間範囲", "xpack.canvas.functions.savedMap.args.titleHelpText": "マップのタイトル", "xpack.canvas.functions.savedMap.args.zoomHelpText": "マップのズームレベル", - "xpack.canvas.functions.savedMapHelpText": "保存されたマップオブジェクトの埋め込み可能なオブジェクトを返します", + "xpack.canvas.functions.savedMapHelpText": "保存されたマップオブジェクトの埋め込み可能なオブジェクトを返します。", "xpack.canvas.functions.savedSearchHelpText": "保存検索オブジェクトの埋め込み可能なオブジェクトを返します", "xpack.canvas.functions.savedVisualization.args.colorsHelpText": "特定のシリーズに使用する色を指定します", - "xpack.canvas.functions.savedVisualization.args.hideLegendHelpText": "凡例が非表示の場合", - "xpack.canvas.functions.savedVisualization.args.idHelpText": "保存されたビジュアライゼーションオブジェクトの ID", + "xpack.canvas.functions.savedVisualization.args.hideLegendHelpText": "凡例を非表示にするオプションを指定します", + "xpack.canvas.functions.savedVisualization.args.idHelpText": "保存されたビジュアライゼーションオブジェクトのID", "xpack.canvas.functions.savedVisualization.args.timerangeHelpText": "含めるデータの時間範囲", - "xpack.canvas.functions.savedVisualizationHelpText": "保存されたビジュアライゼーションオブジェクトの埋め込み可能なオブジェクトを返します", + "xpack.canvas.functions.savedVisualization.args.titleHelpText": "ビジュアライゼーションオブジェクトのタイトル", + "xpack.canvas.functions.savedVisualizationHelpText": "保存されたビジュアライゼーションオブジェクトの埋め込み可能なオブジェクトを返します。", "xpack.canvas.functions.seriesStyle.args.barsHelpText": "バーの幅です。", "xpack.canvas.functions.seriesStyle.args.colorHelpText": "ラインカラーです。", "xpack.canvas.functions.seriesStyle.args.fillHelpText": "点を埋めますか?", "xpack.canvas.functions.seriesStyle.args.horizontalBarsHelpText": "グラフの棒の方向を水平に設定します。", "xpack.canvas.functions.seriesStyle.args.labelHelpText": "スタイルを適用する数列の名前です。", "xpack.canvas.functions.seriesStyle.args.linesHelpText": "線の幅です。", - "xpack.canvas.functions.seriesStyle.args.pointsHelpText": "線上の点のサイズです", + "xpack.canvas.functions.seriesStyle.args.pointsHelpText": "線上の点のサイズです。", "xpack.canvas.functions.seriesStyle.args.stackHelpText": "数列をスタックするかを指定します。数字はスタック ID です。同じスタック ID の数列は一緒にスタックされます。", "xpack.canvas.functions.seriesStyleHelpText": "チャートの数列のプロパティの説明に使用されるオブジェクトを作成します。グラフ関数内では {plotFn} または {pieFn} のような {seriesStyleFn} を使用します。", "xpack.canvas.functions.shape.args.borderHelpText": "図形の外郭の {SVG} カラーです。", @@ -5598,21 +5956,22 @@ "xpack.canvas.functions.shape.args.maintainAspectHelpText": "図形の元の横縦比を維持しますか?", "xpack.canvas.functions.shape.args.shapeHelpText": "図形を選択します。", "xpack.canvas.functions.shapeHelpText": "図形を作成します。", - "xpack.canvas.functions.sort.args.byHelpText": "並べ替えの基準となる列です。指定されていない場合、「{DATATABLE}」は初めの列で並べられます。", - "xpack.canvas.functions.sort.args.reverseHelpText": "並び順を反転させます。指定されていない場合、「{DATATABLE}」は昇順で並べられます。", + "xpack.canvas.functions.sort.args.byHelpText": "並べ替えの基準となる列です。指定されていない場合、{DATATABLE}は初めの列で並べられます。", + "xpack.canvas.functions.sort.args.reverseHelpText": "並び順を反転させます。指定されていない場合、{DATATABLE}は昇順で並べられます。", + "xpack.canvas.functions.sortHelpText": "{DATATABLE}を指定された列で並べ替えます。", "xpack.canvas.functions.staticColumn.args.nameHelpText": "新しい列の名前です。", "xpack.canvas.functions.staticColumn.args.valueHelpText": "新しい列の各行に挿入する値です。ヒント: 部分式を使用して他の列を静的値にロールアップします。", - "xpack.canvas.functions.staticColumnHelpText": "すべての行に同じ静的値の列を追加します。{alterColumnFn} および {mapColumnFn} もご参照ください。", + "xpack.canvas.functions.staticColumnHelpText": "すべての行に同じ静的値の列を追加します。{alterColumnFn}および{mapColumnFn}も参照してください。", "xpack.canvas.functions.string.args.valueHelpText": "1 つの文字列に結合する値です。必要な場所にスペースを入れてください。", "xpack.canvas.functions.stringHelpText": "すべての引数を 1 つの文字列に連結させます。", - "xpack.canvas.functions.switch.args.caseHelpText": "確認する条件です", + "xpack.canvas.functions.switch.args.caseHelpText": "確認する条件です。", "xpack.canvas.functions.switch.args.defaultHelpText": "条件が一切満たされていないときに戻される値です。指定されておらず、条件が一切満たされている場合は、元の {CONTEXT} が戻されます。", - "xpack.canvas.functions.switchHelpText": "複数条件の条件付きロジックを実行します。{case} を作成し {switchFn} 関数に渡す {caseFn} もご覧ください。", + "xpack.canvas.functions.switchHelpText": "複数条件の条件付きロジックを実行します。{switchFn}関数に渡す{case}を作成する、{caseFn}も参照してください。", "xpack.canvas.functions.table.args.fontHelpText": "表のコンテンツの {CSS} フォントプロパティです。例: {FONT_FAMILY} または {FONT_WEIGHT}。", "xpack.canvas.functions.table.args.paginateHelpText": "ページ付けのコントロールを表示しますか?{BOOLEAN_FALSE} の場合、初めのページだけが表示されます。", "xpack.canvas.functions.table.args.perPageHelpText": "各ページに表示される行数です。", - "xpack.canvas.functions.table.args.showHeaderHelpText": "各列のタイトルと共にヘッダー行を表示するかしないかです。", - "xpack.canvas.functions.tableHelpText": "表エレメントを構成します", + "xpack.canvas.functions.table.args.showHeaderHelpText": "各列のタイトルを含むヘッダー列の表示・非表示を切り替えます。", + "xpack.canvas.functions.tableHelpText": "表エレメントを構成します。", "xpack.canvas.functions.tail.args.countHelpText": "{DATATABLE} の終わりから取得する行数です。", "xpack.canvas.functions.tailHelpText": "{DATATABLE} の終わりから N 行を取得します。{headFn} もご参照ください。", "xpack.canvas.functions.timefilter.args.columnHelpText": "フィルタリングする列またはフィールドです。", @@ -5629,16 +5988,16 @@ "xpack.canvas.functions.timelion.args.query": "Timelion クエリ", "xpack.canvas.functions.timelion.args.timezone": "時間範囲のタイムゾーンです。{MOMENTJS_TIMEZONE_URL} をご覧ください。", "xpack.canvas.functions.timelion.args.to": "時間範囲の終わりの {ELASTICSEARCH} {DATEMATH} 文字列です。", - "xpack.canvas.functions.timelionHelpText": "多くのソースから単独または複数の時系列を抽出するために、Timelion を使用します。", + "xpack.canvas.functions.timelionHelpText": "多くのソースから単独または複数の時系列を抽出するために、Timelionを使用します。", "xpack.canvas.functions.timerange.args.fromHelpText": "時間範囲の開始", "xpack.canvas.functions.timerange.args.toHelpText": "時間範囲の終了", "xpack.canvas.functions.timerangeHelpText": "期間を意味するオブジェクト", "xpack.canvas.functions.to.args.type": "表現言語の既知のデータ型です。", "xpack.canvas.functions.to.missingType": "型キャストを指定する必要があります", - "xpack.canvas.functions.toHelpText": "{CONTEXT} の型を指定された型に明確にキャストします。", + "xpack.canvas.functions.toHelpText": "1つの型から{CONTEXT}の型を指定された型に明確にキャストします。", "xpack.canvas.functions.urlparam.args.defaultHelpText": "{URL} パラメーターが指定されていないときに戻される文字列です。", "xpack.canvas.functions.urlparam.args.paramHelpText": "取得する {URL} ハッシュパラメーターです。", - "xpack.canvas.functions.urlparamHelpText": "表現で使用する {URL} パラメーターを取得します。{urlparamFn} 関数は常に {TYPE_STRING} を戻します。たとえば、値 {value} を {URL} {example} のパラメーター {myVar} から取得できます)。", + "xpack.canvas.functions.urlparamHelpText": "表現で使用する{URL}パラメーターを取得します。{urlparamFn}関数は常に {TYPE_STRING} を戻します。たとえば、値{value}を{URL} {example}のパラメーター{myVar}から取得できます。", "xpack.canvas.groupSettings.multipleElementsActionsDescription": "個々の設定を編集するには、これらのエレメントの選択を解除し、 ({gKey}) を押してグループ化するか、この選択をワークパッド全体で再利用できるように新規エレメントとして保存します。", "xpack.canvas.groupSettings.multipleElementsDescription": "現在複数エレメントが選択されています。", "xpack.canvas.groupSettings.saveGroupDescription": "ワークパッド全体で再利用できるように、このグループを新規エレメントとして保存します。", @@ -5713,7 +6072,11 @@ "xpack.canvas.pageConfig.transitionLabel": "トランジション", "xpack.canvas.pageConfig.transitionPreviewLabel": "プレビュー", "xpack.canvas.pageConfig.transitions.noneDropDownOptionLabel": "なし", + "xpack.canvas.pageManager.addPageTooltip": "新しいページをこのワークパッドに追加", + "xpack.canvas.pageManager.confirmRemoveDescription": "このページを削除してよろしいですか?", + "xpack.canvas.pageManager.confirmRemoveTitle": "ページを削除", "xpack.canvas.pageManager.pageNumberAriaLabel": "ページ {pageNumber} を読み込む", + "xpack.canvas.pageManager.removeButtonLabel": "削除", "xpack.canvas.pagePreviewPageControls.clonePageAriaLabel": "ページのクローンを作成", "xpack.canvas.pagePreviewPageControls.clonePageTooltip": "クローンを作成", "xpack.canvas.pagePreviewPageControls.deletePageAriaLabel": "ページを削除", @@ -5822,6 +6185,7 @@ "xpack.canvas.textStylePicker.styleUnderlineOption": "下線", "xpack.canvas.timePicker.applyButtonLabel": "適用", "xpack.canvas.toolbar.editorButtonLabel": "表現エディター", + "xpack.canvas.toolbar.errorMessage": "ツールバーエラー:{message}", "xpack.canvas.toolbar.nextPageAriaLabel": "次のページ", "xpack.canvas.toolbar.pageButtonLabel": "ページ {pageNum}{rest}", "xpack.canvas.toolbar.previousPageAriaLabel": "前のページ", @@ -6581,6 +6945,8 @@ "xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDeprecation": "この設定はサポートが終了し、Kibana 8.0 では削除されます。", "xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDescription": "ダッシュボード表示専用モードのロールです", "xpack.dashboardMode.uiSettings.dashboardsOnlyRolesTitle": "ダッシュボード専用ロール", + "xpack.data.advancedSettings.searchTimeout": "検索タイムアウト", + "xpack.data.advancedSettings.searchTimeoutDesc": "検索セッションの最大タイムアウトを変更するか、0に設定してタイムアウトを無効にすると、クエリは完了するまで実行されます。", "xpack.data.kueryAutocomplete.andOperatorDescription": "{bothArguments} が true であることを条件とする", "xpack.data.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "両方の引数", "xpack.data.kueryAutocomplete.equalOperatorDescription": "一部の値に{equals}", @@ -6613,11 +6979,26 @@ "xpack.enterpriseSearch.appSearch.enginesOverview.table.column.fieldCount": "フィールドカウント", "xpack.enterpriseSearch.appSearch.enginesOverview.table.column.name": "名前", "xpack.enterpriseSearch.appSearch.enginesOverview.title": "エンジン概要", + "xpack.enterpriseSearch.appSearch.nav.credentials": "資格情報", + "xpack.enterpriseSearch.appSearch.nav.engines": "エンジン", + "xpack.enterpriseSearch.appSearch.nav.roleMappings": "ロールマッピング", + "xpack.enterpriseSearch.appSearch.nav.settings": "アカウント設定", + "xpack.enterpriseSearch.appSearch.productCardDescription": "Elastic App Searchには、強力な検索を設計し、WebサイトやWeb/モバイルアプリケーションにデプロイするための使いやすいツールがあります。", "xpack.enterpriseSearch.appSearch.productCta": "App Searchの起動", + "xpack.enterpriseSearch.appSearch.productDescription": "ダッシュボード、分析、APIを活用し、高度なアプリケーション検索をシンプルにします。", "xpack.enterpriseSearch.appSearch.productName": "App Search", "xpack.enterpriseSearch.appSearch.setupGuide.description": "Elastic App Searchには、強力な検索を設計し、Webサイトやモバイルアプリケーションにデプロイするためのツールがあります。", "xpack.enterpriseSearch.appSearch.setupGuide.notConfigured": "App SearchはまだKibanaインスタンスで構成されていません。", "xpack.enterpriseSearch.appSearch.setupGuide.videoAlt": "App Searchの基本という短い動画では、App Searchを起動して実行する方法について説明します。", + "xpack.enterpriseSearch.appSearch.tokens.admin.description": "資格情報APIとの連携では、非公開管理キーが使用されます。", + "xpack.enterpriseSearch.appSearch.tokens.admin.name": "非公開管理キー", + "xpack.enterpriseSearch.appSearch.tokens.private.description": "1つ以上のエンジンに対する読み取り/書き込みアクセス権を得るために、非公開APIキーが使用されます。", + "xpack.enterpriseSearch.appSearch.tokens.private.name": "非公開APIキー", + "xpack.enterpriseSearch.appSearch.tokens.search.description": "エンドポイントのみの検索では、公開検索キーが使用されます。", + "xpack.enterpriseSearch.appSearch.tokens.search.name": "公開検索キー", + "xpack.enterpriseSearch.enterpriseSearch.setupGuide.description": "場所を問わず、何でも検索。組織を支える多忙なチームのために、パワフルでモダンな検索エクスペリエンスを簡単に導入できます。Webサイトやアプリ、ワークプレイスに事前調整済みの検索をすばやく追加しましょう。何でもシンプルに検索できます。", + "xpack.enterpriseSearch.enterpriseSearch.setupGuide.notConfigured": "エンタープライズサーチはまだKibanaインスタンスで構成されていません。", + "xpack.enterpriseSearch.enterpriseSearch.setupGuide.videoAlt": "エンタープライズ サーチの基本操作", "xpack.enterpriseSearch.errorConnectingState.description1": "ホストURL {enterpriseSearchUrl}では、エンタープライズ サーチへの接続を確立できません", "xpack.enterpriseSearch.errorConnectingState.description2": "ホストURLが{configFile}で正しく構成されていることを確認してください。", "xpack.enterpriseSearch.errorConnectingState.description3": "エンタープライズ サーチサーバーが応答していることを確認してください。", @@ -6627,6 +7008,26 @@ "xpack.enterpriseSearch.errorConnectingState.troubleshootAuth": "ユーザー認証を確認してください。", "xpack.enterpriseSearch.errorConnectingState.troubleshootAuthNative": "Elasticsearchネイティブ認証またはSSO/SAMLを使用して認証する必要があります。", "xpack.enterpriseSearch.errorConnectingState.troubleshootAuthSAML": "SSO/SAMLを使用している場合は、エンタープライズ サーチでSAMLレルムも設定する必要があります。", + "xpack.enterpriseSearch.FeatureCatalogue.description": "厳選されたAPIとツールを使用して検索エクスペリエンスを作成します。", + "xpack.enterpriseSearch.featureCatalogue.subtitle": "すべて検索", + "xpack.enterpriseSearch.featureCatalogueDescription1": "強力な検索エクスペリエンスを構築します。", + "xpack.enterpriseSearch.featureCatalogueDescription2": "ユーザーを関連するデータにつなげます。", + "xpack.enterpriseSearch.featureCatalogueDescription3": "チームの内容を統合します。", + "xpack.enterpriseSearch.nav.hierarchy": "セカンダリ", + "xpack.enterpriseSearch.nav.menu": "メニュー", + "xpack.enterpriseSearch.nav.toggleMenu": "セカンダリナビゲーションを切り替える", + "xpack.enterpriseSearch.navTitle": "概要", + "xpack.enterpriseSearch.notFound.action1": "ダッシュボードに戻す", + "xpack.enterpriseSearch.notFound.action2": "サポートに問い合わせる", + "xpack.enterpriseSearch.notFound.description": "お探しのページは見つかりませんでした。", + "xpack.enterpriseSearch.notFound.title": "エラー", + "xpack.enterpriseSearch.overview.heading": "Elasticエンタープライズサーチへようこそ", + "xpack.enterpriseSearch.overview.productCard.heading": "Elastic {productName}", + "xpack.enterpriseSearch.overview.productCard.launchButton": "{productName}の起動", + "xpack.enterpriseSearch.overview.productCard.setupButton": "{productName}のセットアップ", + "xpack.enterpriseSearch.overview.subheading": "開始する製品を選択", + "xpack.enterpriseSearch.productName": "エンタープライズサーチ", + "xpack.enterpriseSearch.readOnlyMode.warning": "エンタープライズ サーチは読み取り専用モードです。作成、編集、削除などの変更を実行できません。", "xpack.enterpriseSearch.setupGuide.step1.instruction1": "{configFile}ファイルで、{configSetting}を{productName}インスタンスのURLに設定します。例:", "xpack.enterpriseSearch.setupGuide.step1.title": "{productName}ホストURLをKibana構成に追加", "xpack.enterpriseSearch.setupGuide.step2.instruction1": "Kibanaを再起動して、前のステップから構成変更を取得します。", @@ -6642,6 +7043,82 @@ "xpack.enterpriseSearch.troubleshooting.standardAuth.title": "標準認証の{productName}はサポートされていません", "xpack.enterpriseSearch.workplaceSearch.activityFeedEmptyDefault.title": "組織には最近のアクティビティがありません", "xpack.enterpriseSearch.workplaceSearch.activityFeedNamedDefault.title": "{name}には最近のアクティビティがありません", + "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.cancel.action": "キャンセル", + "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.heading": "グループを追加", + "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.submit.action": "グループを追加", + "xpack.enterpriseSearch.workplaceSearch.groups.addGroupForm.action": "グループを作成", + "xpack.enterpriseSearch.workplaceSearch.groups.clearFilters.action": "フィルターを消去", + "xpack.enterpriseSearch.workplaceSearch.groups.contentSourceCountHeading": "{numSources}件の共有コンテンツソース", + "xpack.enterpriseSearch.workplaceSearch.groups.description": "共有コンテンツソースとユーザーをグループに割り当て、さまざまな内部チーム向けに関連する検索エクスペリエンスを作成します。", + "xpack.enterpriseSearch.workplaceSearch.groups.filterGroups.placeholder": "名前でグループをフィルター...", + "xpack.enterpriseSearch.workplaceSearch.groups.filterSources.buttonText": "ソース", + "xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.buttonText": "ユーザー", + "xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.placeholder": "ユーザーをフィルター...", + "xpack.enterpriseSearch.workplaceSearch.groups.groupDeleted": "グループ「{groupName}」が正常に削除されました。", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerCancel": "キャンセル", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerHeaderTitle": "{label}を管理", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSelectAllToggle": "{action}すべて", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSourceEmpty.body": "まだ共有コンテンツソースが追加されていない可能性があります。", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSourceEmpty.title": "おっと!", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerUpdate": "更新", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerUpdateAddSourceButton": "共有ソースを追加", + "xpack.enterpriseSearch.workplaceSearch.groups.groupNotFound": "ID「{groupId}」のグループが見つかりません。", + "xpack.enterpriseSearch.workplaceSearch.groups.groupPrioritizationUpdated": "共有ソース優先度が正常に更新されました", + "xpack.enterpriseSearch.workplaceSearch.groups.groupRenamed": "このグループ名が正常に「{groupName}」に変更されました", + "xpack.enterpriseSearch.workplaceSearch.groups.groupSourcesUpdated": "共有コンテンツソースが正常に更新されました", + "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader": "グループ", + "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.sourcesTableHeader": "コンテンツソース", + "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader": "ユーザー", + "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.emailTableHeader": "メール", + "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.usernameTableHeader": "ユーザー名", + "xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText": "前回更新日時{updatedAt}。", + "xpack.enterpriseSearch.workplaceSearch.groups.groupUsersUpdated": "このグループのユーザーが正常に更新されました", + "xpack.enterpriseSearch.workplaceSearch.groups.heading": "グループを管理", + "xpack.enterpriseSearch.workplaceSearch.groups.inviteUsers.action": "ユーザーを招待", + "xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action": "グループを管理", + "xpack.enterpriseSearch.workplaceSearch.groups.newGroupSavedSuccess": "{groupName}が正常に作成されました", + "xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "共有コンテンツソースがありません", + "xpack.enterpriseSearch.workplaceSearch.groups.noUsersFound": "ユーザーが見つかりません", + "xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "ユーザーがありません", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.cancelRemoveButtonText": "キャンセル", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "{name}を削除", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "グループはWorkplace Searchから削除されます。{name}を削除してよろしいですか?", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "確認", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription": "コンテンツソースはこのグループと共有されていません。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptyUsersDescription": "このグループにはユーザーがありません。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesDescription": "「{name}」グループのすべてのユーザーによって検索可能です。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupUsersDescription": "メンバーはグループのソースを検索できます。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.manageSourcesButtonText": "共有コンテンツソースを管理", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.manageUsersButtonText": "ユーザーの管理", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.nameSectionDescription": "このグループの名前をカスタマイズします。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.nameSectionTitle": "グループ名", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.removeButtonText": "グループを削除", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.removeSectionDescription": "この操作は元に戻すことができません。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.removeSectionTitle": "このグループを削除", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.saveNameButtonText": "名前を保存", + "xpack.enterpriseSearch.workplaceSearch.groups.searchResults.notFoound": "結果が見つかりませんでした。", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.headerActionText": "保存", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.headerDescription": "グループコンテンツソース全体で相対ドキュメント重要度を調整します。", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.headerTitle": "共有コンテンツソースの優先度", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.priorityTableHeader": "関連性優先度", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.sourceTableHeader": "送信元", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.zeroStateBody": "2つ以上のソースを{groupName}と共有し、ソース優先度をカスタマイズします。", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.zeroStateButtonText": "共有コンテンツソースを追加", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.zeroStateTitle": "ソースはこのグループと共有されていません", + "xpack.enterpriseSearch.workplaceSearch.groups.sourcesModalLabel": "共有コンテンツソース", + "xpack.enterpriseSearch.workplaceSearch.groups.sourcesModalTitle": "{groupName}と共有するコンテンツソースを選択", + "xpack.enterpriseSearch.workplaceSearch.groups.userListCount": "{maxVisibleUsers}/{numUsers}ユーザーを表示しています。", + "xpack.enterpriseSearch.workplaceSearch.groups.usersModalLabel": "ユーザー", + "xpack.enterpriseSearch.workplaceSearch.headerActions.searchApplication": "検索アプリケーションに移動", + "xpack.enterpriseSearch.workplaceSearch.nav.groups": "グループ", + "xpack.enterpriseSearch.workplaceSearch.nav.groups.groupOverview": "概要", + "xpack.enterpriseSearch.workplaceSearch.nav.groups.sourcePrioritization": "ソースの優先度", + "xpack.enterpriseSearch.workplaceSearch.nav.overview": "概要", + "xpack.enterpriseSearch.workplaceSearch.nav.personalDashboard": "個人のダッシュボードを表示", + "xpack.enterpriseSearch.workplaceSearch.nav.roleMappings": "ロールマッピング", + "xpack.enterpriseSearch.workplaceSearch.nav.security": "セキュリティ", + "xpack.enterpriseSearch.workplaceSearch.nav.settings": "設定", + "xpack.enterpriseSearch.workplaceSearch.nav.sources": "ソース", "xpack.enterpriseSearch.workplaceSearch.organizationStats.activeUsers": "アクティブなユーザー", "xpack.enterpriseSearch.workplaceSearch.organizationStats.invitations": "招待", "xpack.enterpriseSearch.workplaceSearch.organizationStats.privateSources": "プライベートソース", @@ -6658,7 +7135,9 @@ "xpack.enterpriseSearch.workplaceSearch.overviewOnboardingUsersCard.description": "検索できるように、同僚をこの組織に招待します。", "xpack.enterpriseSearch.workplaceSearch.overviewOnboardingUsersCard.title": "ユーザーと招待", "xpack.enterpriseSearch.workplaceSearch.overviewUsersCard.title": "検索できるように、同僚を招待しました。", + "xpack.enterpriseSearch.workplaceSearch.productCardDescription": "チームのコンテンツをすべて1つの場所に統合します。頻繁に使用される生産性ツールやコラボレーションツールにすぐに接続できます。", "xpack.enterpriseSearch.workplaceSearch.productCta": "Workplace Searchの起動", + "xpack.enterpriseSearch.workplaceSearch.productDescription": "仮想ワークプレイスで使用可能な、すべてのドキュメント、ファイル、ソースを検索します。", "xpack.enterpriseSearch.workplaceSearch.productName": "Workplace Search", "xpack.enterpriseSearch.workplaceSearch.recentActivity.title": "最近のアクティビティ", "xpack.enterpriseSearch.workplaceSearch.recentActivitySourceLink.linkLabel": "ソースを表示", @@ -6668,6 +7147,7 @@ "xpack.enterpriseSearch.workplaceSearch.sourcesOnboardingCard.buttonLabel": "{label}ソースを追加", "xpack.enterpriseSearch.workplaceSearch.sourcesOnboardingCard.description": "{sourcesCount, number}{sourcesCount, plural, one {個の共有ソース} other {個の共有ソース}}を追加しました。検索をご利用ください。", "xpack.enterpriseSearch.workplaceSearch.usersOnboardingCard.buttonLabel": "{label}ユーザーを招待", + "xpack.eventLog.savedObjectProviderRegistry.getProvidersClient.noDefaultProvider": "イベントログにはデフォルトプロバイダーが必要です。", "xpack.features.advancedSettingsFeatureName": "高度な設定", "xpack.features.dashboardFeatureName": "ダッシュボード", "xpack.features.devToolsFeatureName": "開発ツール", @@ -6736,6 +7216,15 @@ "xpack.fileUpload.noIndexSuppliedErrorMessage": "インデックスが指定されていません。", "xpack.fileUpload.patternReader.featuresOmitted": "ジオメトリのない一部の機能は省略されました", "xpack.globalSearch.find.invalidLicenseError": "GlobalSearch APIは、ライセンス状態が無効であるため、無効になっています。{errorMessage}", + "xpack.globalSearchBar.searchBar.mobileSearchButtonAriaLabel": "サイト検索", + "xpack.globalSearchBar.searchBar.noResults": "アプリケーション、ダッシュボード、ビジュアライゼーションなどを検索してみてください。", + "xpack.globalSearchBar.searchBar.noResultsHeading": "結果が見つかりませんでした", + "xpack.globalSearchBar.searchBar.noResultsImageAlt": "ブラックホールの図", + "xpack.globalSearchBar.searchBar.placeholder": "Elasticを検索", + "xpack.globalSearchBar.searchBar.shortcutDescription.macCommandDescription": "コマンド+ /", + "xpack.globalSearchBar.searchBar.shortcutDescription.shortcutDetail": "{shortcutDescription}{commandDescription}", + "xpack.globalSearchBar.searchBar.shortcutDescription.shortcutInstructionDescription": "ショートカット", + "xpack.globalSearchBar.searchBar.shortcutDescription.windowsCommandDescription": "コントロール+ /", "xpack.graph.badge.readOnly.text": "読み込み専用", "xpack.graph.badge.readOnly.tooltip": "Graph ワークスペースを保存できません", "xpack.graph.bar.exploreLabel": "グラフ", @@ -6743,6 +7232,8 @@ "xpack.graph.bar.pickSourceLabel": "データソースを選択", "xpack.graph.bar.pickSourceTooltip": "グラフの関係性を開始するデータソースを選択します。", "xpack.graph.bar.searchFieldPlaceholder": "データを検索してグラフに追加", + "xpack.graph.blocklist.noEntriesDescription": "ブロックされた用語がありません。頂点を選択して、右側のコントロールパネルの{stopSign}をクリックしてブロックします。ブロックされた用語に一致するドキュメントは今後表示されず、関係性が非表示になります。", + "xpack.graph.blocklist.removeButtonAriaLabel": "削除", "xpack.graph.clearWorkspace.confirmButtonLabel": "データソースを変更", "xpack.graph.clearWorkspace.confirmText": "データソースを変更すると、現在のフィールドと頂点がリセットされます。", "xpack.graph.clearWorkspace.modalTitle": "保存されていない変更", @@ -6855,6 +7346,7 @@ "xpack.graph.outlinkEncoders.textPlainTitle": "プレインテキスト", "xpack.graph.pageTitle": "グラフ", "xpack.graph.pluginDescription": "Elasticsearch データの関連性のある関係を浮上させ分析します。", + "xpack.graph.pluginSubtitle": "パターンと関係を明らかにします。", "xpack.graph.sampleData.label": "グラフ", "xpack.graph.savedWorkspace.workspaceNameTitle": "新規グラフワークスペース", "xpack.graph.saveWorkspace.savingErrorMessage": "ワークスペースの保存に失敗しました: {message}", @@ -6879,6 +7371,9 @@ "xpack.graph.settings.advancedSettings.timeoutInputLabel": "タイムアウト (ms)", "xpack.graph.settings.advancedSettings.timeoutUnit": "ms", "xpack.graph.settings.advancedSettingsTitle": "高度な設定", + "xpack.graph.settings.blocklist.blocklistHelpText": "これらの用語は現在ワークスペースに再度表示されないようブラックリストに登録されています", + "xpack.graph.settings.blocklist.clearButtonLabel": "すべて削除", + "xpack.graph.settings.blocklistTitle": "ブラックリスト", "xpack.graph.settings.closeLabel": "閉じる", "xpack.graph.settings.drillDowns.cancelButtonLabel": "キャンセル", "xpack.graph.settings.drillDowns.defaultUrlTemplateTitle": "生ドキュメント", @@ -6969,9 +7464,11 @@ "xpack.grokDebugger.registryProviderTitle": "Grokデバッガー", "xpack.grokDebugger.sampleDataLabel": "サンプルデータ", "xpack.grokDebugger.serverInactiveLicenseError": "Grok Debuggerツールには有効なライセンスが必要です。", + "xpack.grokDebugger.simulate.errorTitle": "シミュレーションエラー", "xpack.grokDebugger.simulateButtonLabel": "シミュレート", "xpack.grokDebugger.structuredDataLabel": "構造化データ", "xpack.grokDebugger.trialLicenseTitle": "トライアル", + "xpack.grokDebugger.unknownErrorTitle": "問題が発生しました", "xpack.idxMgmt.aliasesTab.noAliasesTitle": "エイリアスが定義されていません。", "xpack.idxMgmt.appTitle": "インデックス管理", "xpack.idxMgmt.badgeAriaLabel": "{label}。これをフィルタリングするよう選択。", @@ -7082,6 +7579,8 @@ "xpack.idxMgmt.componentTemplatesSelector.filters.mappingsLabel": "マッピング", "xpack.idxMgmt.componentTemplatesSelector.loadingComponentsDescription": "コンポーネントテンプレートを読み込んでいます...", "xpack.idxMgmt.componentTemplatesSelector.loadingComponentsErrorMessage": "コンポーネントの読み込みエラー", + "xpack.idxMgmt.componentTemplatesSelector.noComponentSelectedLabel-1": "コンポーネントテンプレート基本要素をこのテンプレートに追加します。", + "xpack.idxMgmt.componentTemplatesSelector.noComponentSelectedLabel-2": "コンポーネントテンプレートは指定された順序で適用されます。", "xpack.idxMgmt.componentTemplatesSelector.removeItemIconLabel": "削除", "xpack.idxMgmt.componentTemplatesSelector.searchBox.placeholder": "コンポーネントテンプレートを検索", "xpack.idxMgmt.componentTemplatesSelector.searchResult.emptyPrompt.clearSearchButtonLabel": "検索のクリア", @@ -7098,10 +7597,22 @@ "xpack.idxMgmt.dataStreamDetailPanel.deleteButtonLabel": "データストリームを削除", "xpack.idxMgmt.dataStreamDetailPanel.generationTitle": "生成", "xpack.idxMgmt.dataStreamDetailPanel.generationToolTip": "データストリームに作成されたバッキングインデックスの累積数", + "xpack.idxMgmt.dataStreamDetailPanel.healthTitle": "ヘルス", + "xpack.idxMgmt.dataStreamDetailPanel.healthToolTip": "データストリームの現在のバッキングインデックスのヘルス。", + "xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyContentNoneMessage": "なし", + "xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyTitle": "インデックスライフサイクルポリシー", + "xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyToolTip": "データストリームのデータを管理するインデックスライフサイクルポリシー", + "xpack.idxMgmt.dataStreamDetailPanel.indexTemplateTitle": "インデックステンプレート", + "xpack.idxMgmt.dataStreamDetailPanel.indexTemplateToolTip": "データストリームを構成し、バッキングインデックスを構成するインデックステンプレート", "xpack.idxMgmt.dataStreamDetailPanel.indicesTitle": "インデックス", "xpack.idxMgmt.dataStreamDetailPanel.indicesToolTip": "データストリームの現在のバッキングインデックス", "xpack.idxMgmt.dataStreamDetailPanel.loadingDataStreamDescription": "データストリームを読み込んでいます", "xpack.idxMgmt.dataStreamDetailPanel.loadingDataStreamErrorMessage": "データの読み込み中にエラーが発生", + "xpack.idxMgmt.dataStreamDetailPanel.maxTimeStampNoneMessage": "無し", + "xpack.idxMgmt.dataStreamDetailPanel.maxTimeStampTitle": "最終更新", + "xpack.idxMgmt.dataStreamDetailPanel.maxTimeStampToolTip": "データストリームに追加する最新のドキュメント", + "xpack.idxMgmt.dataStreamDetailPanel.storageSizeTitle": "ストレージサイズ", + "xpack.idxMgmt.dataStreamDetailPanel.storageSizeToolTip": "データストリームのバッキングインデックスにあるすべてのシャードの合計サイズ", "xpack.idxMgmt.dataStreamDetailPanel.timestampFieldTitle": "タイムスタンプフィールド", "xpack.idxMgmt.dataStreamDetailPanel.timestampFieldToolTip": "タイムスタンプフィールドはデータストリームのすべてのドキュメントで共有されます", "xpack.idxMgmt.dataStreamList.dataStreamsDescription": "データストリームは複数のインデックスの時系列データを格納します。{learnMoreLink}", @@ -7118,9 +7629,15 @@ "xpack.idxMgmt.dataStreamList.table.actionDeleteDecription": "このデータストリームを削除", "xpack.idxMgmt.dataStreamList.table.actionDeleteText": "削除", "xpack.idxMgmt.dataStreamList.table.deleteDataStreamsButtonLabel": "{count, plural, one {個のデータストリーム} other {個のデータストリーム}}を削除", + "xpack.idxMgmt.dataStreamList.table.healthColumnTitle": "ヘルス", "xpack.idxMgmt.dataStreamList.table.indicesColumnTitle": "インデックス", + "xpack.idxMgmt.dataStreamList.table.maxTimeStampColumnNoneMessage": "無し", + "xpack.idxMgmt.dataStreamList.table.maxTimeStampColumnTitle": "最終更新", "xpack.idxMgmt.dataStreamList.table.nameColumnTitle": "名前", "xpack.idxMgmt.dataStreamList.table.noDataStreamsMessage": "データストリームが見つかりません", + "xpack.idxMgmt.dataStreamList.table.storageSizeColumnTitle": "ストレージサイズ", + "xpack.idxMgmt.dataStreamListControls.includeStatsSwitchLabel": "統計情報を含める", + "xpack.idxMgmt.dataStreamListControls.includeStatsSwitchToolTip": "統計情報を含めると、再読み込み時間が長くなることがあります", "xpack.idxMgmt.dataStreamListDescription.learnMoreLinkText": "詳細情報", "xpack.idxMgmt.deleteDataStreamsConfirmationModal.cancelButtonLabel": "キャンセル", "xpack.idxMgmt.deleteDataStreamsConfirmationModal.confirmButtonLabel": "{dataStreamsCount, plural, one {個のデータストリーム} other {個のデータストリーム}}を削除", @@ -7346,6 +7863,8 @@ "xpack.idxMgmt.mappingsEditor.configuration.throwErrorsForUnmappedFieldsLabel": "ドキュメントがマッピングされていないフィールドを含む場合に例外を選択する", "xpack.idxMgmt.mappingsEditor.confirmationModal.deleteAliasesDescription": "次のエイリアスも削除されます。", "xpack.idxMgmt.mappingsEditor.confirmationModal.deleteFieldsDescription": "これによって、次のフィールドも削除されます。", + "xpack.idxMgmt.mappingsEditor.constantKeyword.valueFieldDescription": "インデックスのすべてのドキュメントのこのフィールドの値。指定しない場合は、最初のインデックスされたドキュメントで指定された値(デフォルト値)になります。", + "xpack.idxMgmt.mappingsEditor.constantKeyword.valueFieldTitle": "値を設定", "xpack.idxMgmt.mappingsEditor.copyToDocLinkText": "ドキュメントのコピー", "xpack.idxMgmt.mappingsEditor.copyToFieldDescription": "複数のフィールドの値をグループフィールドにコピーします。その後、このグループフィールドは単一のフィールドとしてクエリできます。", "xpack.idxMgmt.mappingsEditor.copyToFieldTitle": "グループフィールドへのコピー", @@ -7363,6 +7882,8 @@ "xpack.idxMgmt.mappingsEditor.dataType.byteLongDescription": "バイトフィールドは、最小値{minValue}と最大値{maxValue}を持つ符号付き8ビット整数を受け入れます。", "xpack.idxMgmt.mappingsEditor.dataType.completionSuggesterDescription": "完了サジェスタ", "xpack.idxMgmt.mappingsEditor.dataType.completionSuggesterLongDescription": "完了サジェスタフィールドは、オートコンプリート機能をサポートしますが、メモリを占有し、低速で構築される特別なデータ構造が必要です。", + "xpack.idxMgmt.mappingsEditor.dataType.constantKeywordDescription": "Constantキーワード", + "xpack.idxMgmt.mappingsEditor.dataType.constantKeywordLongDescription": "Constantキーワードフィールドは、特殊なタイプのキーワードフィールドであり、インデックスのすべてのドキュメントで同じキーワードを含むフィールドで使用されます。{keyword}フィールドと同じクエリと集計をサポートします。", "xpack.idxMgmt.mappingsEditor.dataType.dateDescription": "日付", "xpack.idxMgmt.mappingsEditor.dataType.dateLongDescription": "日付フィールドは、フォーマット設定された日付( \"2015/01/01 12:10:30\")、基準時点からのミリ秒を表す長い数字、および基準時点からの秒を表す整数を含む文字列を受け入れます。複数の日付フォーマットは許可されています。タイムゾーン付きの日付はUTCに変換されます。", "xpack.idxMgmt.mappingsEditor.dataType.dateNanosDescription": "日付 ナノ秒", @@ -7387,6 +7908,8 @@ "xpack.idxMgmt.mappingsEditor.dataType.geoShapeDescription": "地形", "xpack.idxMgmt.mappingsEditor.dataType.halfFloatDescription": "半浮動", "xpack.idxMgmt.mappingsEditor.dataType.halfFloatLongDescription": "半浮動小数点フィールドは、有限値に制限された半精度16ビット浮動小数点数を受け入れます(IEEE 754)。", + "xpack.idxMgmt.mappingsEditor.dataType.histogramDescription": "ヒストグラム", + "xpack.idxMgmt.mappingsEditor.dataType.histogramLongDescription": "ヒストグラムフィールドには、ヒストグラムを表すあらかじめ集計された数値データが格納されます。このフィールドは、集計目的で使用されます。", "xpack.idxMgmt.mappingsEditor.dataType.integerDescription": "整数", "xpack.idxMgmt.mappingsEditor.dataType.integerLongDescription": "整数フィールドは、最小値{minValue}と最大値{maxValue}を持つ符号付きの32ビット整数を受け入れます。", "xpack.idxMgmt.mappingsEditor.dataType.integerRangeDescription": "整数レンジ", @@ -7418,6 +7941,8 @@ "xpack.idxMgmt.mappingsEditor.dataType.percolatorDescription": "パーコレーター", "xpack.idxMgmt.mappingsEditor.dataType.percolatorLongDescription": "パーコレーターデータタイプは、{percolator}を有効にします。", "xpack.idxMgmt.mappingsEditor.dataType.percolatorLongDescription.learnMoreLink": "パーコレーターのクエリ", + "xpack.idxMgmt.mappingsEditor.dataType.pointDescription": "点", + "xpack.idxMgmt.mappingsEditor.dataType.pointLongDescription": "点フィールドでは、2次元平面座標系に該当する{code}ペアを検索できます。", "xpack.idxMgmt.mappingsEditor.dataType.rangeDescription": "範囲", "xpack.idxMgmt.mappingsEditor.dataType.rangeSubtypeDescription": "範囲タイプ", "xpack.idxMgmt.mappingsEditor.dataType.rankFeatureDescription": "ランク特性", @@ -7439,6 +7964,11 @@ "xpack.idxMgmt.mappingsEditor.dataType.textLongDescription.keywordTypeLink": "キーワードデータタイプ", "xpack.idxMgmt.mappingsEditor.dataType.tokenCountDescription": "トークン数", "xpack.idxMgmt.mappingsEditor.dataType.tokenCountLongDescription": "トークン数フィールドは、文字列値を受け入れます。 これらの値は分析され、文字列内のトークン数がインデックスされます。", + "xpack.idxMgmt.mappingsEditor.dataType.versionDescription": "バージョン", + "xpack.idxMgmt.mappingsEditor.dataType.versionLongDescription": "バージョンフィールドは、ソフトウェアバージョン値を処理する際に役立ちます。このフィールドは、重いワイルドカード、正規表現、曖昧検索用に最適化されていません。このようなタイプのクエリでは、{keywordType}を使用してください。", + "xpack.idxMgmt.mappingsEditor.dataType.versionLongDescription.keywordTypeLink": "キーワードデータ型", + "xpack.idxMgmt.mappingsEditor.dataType.wildcardDescription": "ワイルドカード", + "xpack.idxMgmt.mappingsEditor.dataType.wildcardLongDescription": "ワイルドカードフィールドには、ワイルドカードのgrepのようなクエリに最適化された値が格納されます。", "xpack.idxMgmt.mappingsEditor.date.localeFieldTitle": "ロケールの設定", "xpack.idxMgmt.mappingsEditor.dateType.localeFieldDescription": "日付解析時に使用するロケール。言語によって月の名称や略語は異なるため、これが役に立ちます。{root}ロケールに関するデフォルト。", "xpack.idxMgmt.mappingsEditor.dateType.nullValueFieldDescription": "インデックスおよび検索を可能にするため、null値を日付値と入れ替えてください。", @@ -7602,7 +8132,9 @@ "xpack.idxMgmt.mappingsEditor.geoShapeType.orientationFieldTitle": "向きの設定", "xpack.idxMgmt.mappingsEditor.hideErrorsButtonLabel": "エラーの非表示化", "xpack.idxMgmt.mappingsEditor.ignoreAboveDocLinkText": "上記ドキュメントの無視", + "xpack.idxMgmt.mappingsEditor.ignoreAboveFieldDescription": "この値よりも長い文字列はインデックスされません。これは、Luceneの文字制限(8,191 UTF-8 文字)に対する保護に役立ちます。", "xpack.idxMgmt.mappingsEditor.ignoreAboveFieldLabel": "文字数の制限", + "xpack.idxMgmt.mappingsEditor.ignoreAboveFieldTitle": "長さ制限の設定", "xpack.idxMgmt.mappingsEditor.ignoredMalformedFieldDescription": "デフォルトとして、フィールドに対し正しくないデータタイプを含むドキュメントはインデックスされません。有効にした場合、これらのドキュメントはインデックスされますが、正しくないデータタイプを含むフィールドは除外されます。注意: この方法でインデックスされるドキュメントが多すぎる場合、フィールドに対するクエリは有意ではなくなります。", "xpack.idxMgmt.mappingsEditor.ignoredZValueFieldDescription": "3次元点は受け入れられますが、緯度と軽度値のみがインデックスされ、第3の次元は無視されます。", "xpack.idxMgmt.mappingsEditor.ignoreMalformedDocLinkText": "正しくないドキュメンテーションの無視", @@ -7652,6 +8184,10 @@ "xpack.idxMgmt.mappingsEditor.metaFieldDocumentionLink": "さらに詳しく", "xpack.idxMgmt.mappingsEditor.metaFieldEditorAriaLabel": "_meta fieldデータエディタ", "xpack.idxMgmt.mappingsEditor.metaFieldTitle": "_meta field", + "xpack.idxMgmt.mappingsEditor.metaParameterAriaLabel": "メタデータフィールドデータエディター", + "xpack.idxMgmt.mappingsEditor.metaParameterDescription": "フィールドに関する任意の情報。JSONのキーと値のペアとして指定します。", + "xpack.idxMgmt.mappingsEditor.metaParameterDocLinkText": "メタデータドキュメンテーション", + "xpack.idxMgmt.mappingsEditor.metaParameterTitle": "メタデータを設定", "xpack.idxMgmt.mappingsEditor.minSegmentSizeFieldLabel": "最小セグメントサイズ", "xpack.idxMgmt.mappingsEditor.multiFieldBadgeLabel": "{dataType} マルチフィールド", "xpack.idxMgmt.mappingsEditor.multiFieldIntroductionText": "このフィールドはマルチフィールドです。同じフィールドを異なる方法でインデックスするために、マルチフィールドを使用できます。", @@ -7674,11 +8210,18 @@ "xpack.idxMgmt.mappingsEditor.parameters.localeHelpText": "言語、国、およびバリアントを分離し、{hyphen}または{underscore}を使用します。最大で2つのセパレータが許可されます。例: {locale}。", "xpack.idxMgmt.mappingsEditor.parameters.localeLabel": "ロケール", "xpack.idxMgmt.mappingsEditor.parameters.maxInputLengthLabel": "最大入力長さ", + "xpack.idxMgmt.mappingsEditor.parameters.metaFieldEditorArraysNotAllowedError": "配列は使用できません。", + "xpack.idxMgmt.mappingsEditor.parameters.metaFieldEditorJsonError": "無効なJSONです。", + "xpack.idxMgmt.mappingsEditor.parameters.metaFieldEditorOnlyStringValuesAllowedError": "値は文字列でなければなりません。", + "xpack.idxMgmt.mappingsEditor.parameters.metaHelpText": "JSONフォーマットを使用:{code}", + "xpack.idxMgmt.mappingsEditor.parameters.metaLabel": "メタデータ", "xpack.idxMgmt.mappingsEditor.parameters.normalizerHelpText": "インデックス設定に定義されたノーマライザー名。", "xpack.idxMgmt.mappingsEditor.parameters.nullValueIpHelpText": "IPアドレスを受け入れます。", "xpack.idxMgmt.mappingsEditor.parameters.orientationLabel": "向き", "xpack.idxMgmt.mappingsEditor.parameters.pathHelpText": "ルートからターゲットフィールドへの絶対パス。", "xpack.idxMgmt.mappingsEditor.parameters.pathLabel": "フィールドパス", + "xpack.idxMgmt.mappingsEditor.parameters.pointNullValueHelpText": "点は、オブジェクト、文字列、配列または{docsLink} POINTとして表現できます。", + "xpack.idxMgmt.mappingsEditor.parameters.pointWellKnownTextDocumentationLink": "よく知られたテキスト", "xpack.idxMgmt.mappingsEditor.parameters.positionIncrementGapLabel": "位置のインクリメントギャップ", "xpack.idxMgmt.mappingsEditor.parameters.scalingFactorFieldDescription": "値は、インデックス時におけるこの係数と掛け合わされ、最も近い長さ値に丸められます。係数値を高くすると精度が向上しますが、スペース要件も増加します。", "xpack.idxMgmt.mappingsEditor.parameters.scalingFactorFieldTitle": "スケーリングファクター", @@ -7707,13 +8250,19 @@ "xpack.idxMgmt.mappingsEditor.parameters.validations.smallerZeroErrorMessage": "値は0と同じかそれ以上でなければなりません。", "xpack.idxMgmt.mappingsEditor.parameters.validations.spacesNotAllowedErrorMessage": "スペースは使用できません。", "xpack.idxMgmt.mappingsEditor.parameters.validations.typeIsRequiredErrorMessage": "フィールドタイプを指定します。", + "xpack.idxMgmt.mappingsEditor.parameters.valueLabel": "値", "xpack.idxMgmt.mappingsEditor.parameters.wellKnownTextDocumentationLink": "よく知られたテキスト", + "xpack.idxMgmt.mappingsEditor.point.ignoreMalformedFieldDescription": "デフォルトとして、正しくない点を含むドキュメントはインデックスされません。有効にした場合、これらのドキュメントはインデックスされますが、点が正しくないフィールドは除外されます。注意:この方法でインデックスされるドキュメントが多すぎる場合、フィールドに対するクエリは有意ではなくなります。", + "xpack.idxMgmt.mappingsEditor.point.ignoreZValueFieldDescription": "3次元点も使用できますが、xおよびy値のみがインデックスされ、第3の次元は無視されます。", + "xpack.idxMgmt.mappingsEditor.point.nullValueFieldDescription": "インデックスおよび検索を可能にするため、null値を点の値と入れ替えてください。", "xpack.idxMgmt.mappingsEditor.positionIncrementGapDocLinkText": "位置のインクリメントギャップに関するドキュメンテーション", "xpack.idxMgmt.mappingsEditor.positionIncrementGapFieldDescription": "文字列の配列にある各エレメント間に挿入される偽の用語位置の数。", "xpack.idxMgmt.mappingsEditor.positionIncrementGapFieldTitle": "位置のインクリメントギャップの設定", "xpack.idxMgmt.mappingsEditor.positionsErrorMessage": "位置のインクリメントギャップを変更可能にするには、インデックスオプション(「検索可能」トグルボタンの下)を[位置]または[オフセット]に設定する必要があります。", "xpack.idxMgmt.mappingsEditor.positionsErrorTitle": "位置は有効化されていません。", "xpack.idxMgmt.mappingsEditor.predefinedButtonLabel": "内蔵型アナライザーの使用", + "xpack.idxMgmt.mappingsEditor.rankFeature.positiveScoreImpactFieldDescription": "スコアに不利な相関関係となるランク機能により、このフィールドが無効になります。", + "xpack.idxMgmt.mappingsEditor.rankFeature.positiveScoreImpactFieldTitle": "スコアに有利な影響", "xpack.idxMgmt.mappingsEditor.relationshipsTitle": "関係", "xpack.idxMgmt.mappingsEditor.removeFieldButtonLabel": "削除", "xpack.idxMgmt.mappingsEditor.routingDescription": "ドキュメントは、インデックス内の特定のシャードにルーティングできます。カスタムルーティングの使用時は、ドキュメントをインデックスする度にルーティング値を提供することが重要です。そうしない場合、ドキュメントが複数のシャードでインデックスされる可能性があります。 {docsLink}", @@ -7772,6 +8321,15 @@ "xpack.idxMgmt.refreshIndicesAction.successfullyRefreshedIndicesMessage": "[{indexNames}] が更新されました", "xpack.idxMgmt.reloadIndicesAction.indicesPageRefreshFailureMessage": "現在のインデックスページの更新に失敗しました。", "xpack.idxMgmt.settingsTab.noIndexSettingsTitle": "設定が定義されていません。", + "xpack.idxMgmt.simulateTemplate.closeButtonLabel": "閉じる", + "xpack.idxMgmt.simulateTemplate.descriptionText": "これは最終テンプレートであり、選択したコンポーネントテンプレートと追加したオーバーライドに基づいて、一致するインデックスに適用されます。", + "xpack.idxMgmt.simulateTemplate.filters.aliases": "エイリアス", + "xpack.idxMgmt.simulateTemplate.filters.indexSettings": "インデックス設定", + "xpack.idxMgmt.simulateTemplate.filters.label": "含める", + "xpack.idxMgmt.simulateTemplate.filters.mappings": "マッピング", + "xpack.idxMgmt.simulateTemplate.noFilterSelected": "プレビューするオプションを1つ以上選択してください。", + "xpack.idxMgmt.simulateTemplate.title": "インデックステンプレートをプレビュー", + "xpack.idxMgmt.simulateTemplate.updateButtonLabel": "更新", "xpack.idxMgmt.summary.headers.aliases": "エイリアス", "xpack.idxMgmt.summary.headers.deletedDocumentsHeader": "ドキュメントが削除されました", "xpack.idxMgmt.summary.headers.documentsHeader": "ドキュメント数", @@ -7802,6 +8360,8 @@ "xpack.idxMgmt.templateDetails.manageButtonLabel": "管理", "xpack.idxMgmt.templateDetails.manageContextMenuPanelTitle": "テンプレートオプション", "xpack.idxMgmt.templateDetails.mappingsTabTitle": "マッピング", + "xpack.idxMgmt.templateDetails.previewTab.descriptionText": "これは最終テンプレートであり、一致するインデックスに適用されます。", + "xpack.idxMgmt.templateDetails.previewTabTitle": "プレビュー", "xpack.idxMgmt.templateDetails.settingsTabTitle": "設定", "xpack.idxMgmt.templateDetails.summaryTab.componentsDescriptionListTitle": "コンポーネントテンプレート", "xpack.idxMgmt.templateDetails.summaryTab.dataStreamDescriptionListTitle": "データストリーム", @@ -7822,6 +8382,7 @@ "xpack.idxMgmt.templateEdit.systemTemplateWarningDescription": "システムテンプレートは内部オペレーションに不可欠です。", "xpack.idxMgmt.templateEdit.systemTemplateWarningTitle": "システムテンプレートを編集することで、Kibana に重大な障害が生じる可能性があります", "xpack.idxMgmt.templateForm.createButtonLabel": "テンプレートを作成", + "xpack.idxMgmt.templateForm.previewIndexTemplateButtonLabel": "インデックステンプレートをプレビュー", "xpack.idxMgmt.templateForm.saveButtonLabel": "テンプレートを保存", "xpack.idxMgmt.templateForm.saveTemplateError": "テンプレートを作成できません", "xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel": "メタデータを追加", @@ -7853,6 +8414,8 @@ "xpack.idxMgmt.templateForm.stepLogistics.stepTitle": "ロジスティクス", "xpack.idxMgmt.templateForm.stepLogistics.versionDescription": "テンプレートを外部管理システムで識別するための番号です。", "xpack.idxMgmt.templateForm.stepLogistics.versionTitle": "バージョン", + "xpack.idxMgmt.templateForm.stepReview.previewTab.descriptionText": "これは最終テンプレートであり、一致するインデックスに適用されます。コンポーネントテンプレートは指定された順序で適用されます。明示的なマッピング、設定、およびエイリアスにより、コンポーネントテンプレートが無効になります。", + "xpack.idxMgmt.templateForm.stepReview.previewTabTitle": "プレビュー", "xpack.idxMgmt.templateForm.stepReview.requestTab.descriptionText": "このリクエストは次のインデックステンプレートを作成します。", "xpack.idxMgmt.templateForm.stepReview.requestTabTitle": "リクエスト", "xpack.idxMgmt.templateForm.stepReview.stepTitle": "「{templateName}」の詳細の確認", @@ -7925,8 +8488,14 @@ "xpack.indexLifecycleMgmt.activePhaseMessage": "アクティブ", "xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel": "ライフサイクルポリシーを追加", "xpack.indexLifecycleMgmt.appTitle": "インデックスライフサイクルポリシー", + "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody": "ロールに基づく割り当てを使用するには、1つ以上のノードを、コールド、ウォーム、またはホットティアに割り当てます。使用可能なノードがない場合、ポリシーは割り当てを完了できません。", + "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "コールドティアに割り当てられているノードがありません", + "xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。安価なハードウェアのコールドフェーズにデータを格納します。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "インデックスを凍結", + "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription": "レプリカの数を設定します。デフォルトでは前のフェーズと同じです。", "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "複製の数", + "xpack.indexLifecycleMgmt.coldPhase.replicasTitle": "レプリカ", + "xpack.indexLifecycleMgmt.common.dataTier.title": "データ割り当て", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "キャンセル", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "削除", "xpack.indexLifecycleMgmt.confirmDelete.errorMessage": "ポリシー {policyName} の削除中にエラーが発生しました", @@ -7934,10 +8503,26 @@ "xpack.indexLifecycleMgmt.confirmDelete.title": "ポリシー「{name}」が削除されました", "xpack.indexLifecycleMgmt.confirmDelete.undoneWarning": "削除されたポリシーは復元できません。", "xpack.indexLifecycleMgmt.editPolicy.cancelButton": "キャンセル", + "xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription": "属性に基づく割り当てを使用するには、elasticsearch.ymlでカスタムノード属性を定義します。コールドノードが使用されます。", + "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "コールドフェーズを有効にする", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText": "インデックスへのクエリの頻度を減らすことで、大幅に性能が低いハードウェアにシャードを割り当てることができます。クエリが遅いため、複製の数を減らすことができます。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel": "コールドフェーズ", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "凍結されたインデックスはクラスターにほとんどオーバーヘッドがなく、書き込みオペレーションがブロックされます。凍結されたインデックスは検索できますが、クエリが遅くなります。", + "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "インデックスを読み取り専用にし、メモリー消費量を最小化します。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "凍結", + "xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel": "レプリカを設定", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.allocationFieldLabel": "データティアオプション", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "ノード属性に基づいてデータを移動します。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "カスタム", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "コールドティアのノードにデータを移動します。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input": "コールドノードを使用(推奨)", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText": "コールドフェーズにデータを移動しないでください。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input": "オフ", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText": "ノード属性に基づいてデータを移動します。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input": "カスタム", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText": "ウォームティアのノードにデータを移動します。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input": "ウォームノードを使用(推奨)", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText": "ウォームフェーズにデータを移動しないでください。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.input": "オフ", "xpack.indexLifecycleMgmt.editPolicy.createdMessage": "作成されました", "xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage": "インデックスライフサイクルポリシーを作成します", "xpack.indexLifecycleMgmt.editPolicy.creationDaysOptionLabel": "インデックスの作成からの経過日数", @@ -7947,10 +8532,17 @@ "xpack.indexLifecycleMgmt.editPolicy.creationMinutesOptionLabel": "インデックスの作成からの経過時間(分)", "xpack.indexLifecycleMgmt.editPolicy.creationNanoSecondsOptionLabel": "インデックスの作成からの経過時間(ナノ秒)", "xpack.indexLifecycleMgmt.editPolicy.creationSecondsOptionLabel": "インデックスの作成からの経過時間(秒)", + "xpack.indexLifecycleMgmt.editPolicy.dataTierColdLabel": "コールド", + "xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel": "ホット", + "xpack.indexLifecycleMgmt.editPolicy.dataTierWarmLabel": "ウォーム", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.activateWarmPhaseSwitchLabel": "削除フェーズを有効にする", + "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyLink": "新しいポリシーを作成", + "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyMessage": "既存のスナップショットポリシーの名前を入力するか、この名前で{link}。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyTitle": "ポリシー名が見つかりません", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescriptionText": "今後インデックスは必要ありません。 いつ安全に削除できるかを定義できます。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseLabel": "削除フェーズ", + "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedLink": "スナップショットライフサイクルポリシーを作成", + "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedMessage": "{link}して、クラスタースナップショットの作成と削除を自動化します。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedTitle": "スナップショットポリシーが見つかりません", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesLoadedMessage": "このフィールドを更新し、既存のスナップショットポリシーの名前を入力します。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesLoadedTitle": "既存のポリシーを読み込めません", @@ -7962,6 +8554,9 @@ "xpack.indexLifecycleMgmt.editPolicy.editingExistingPolicyExplanationMessage": "すべての変更はこのポリシーに関連付けられているインデックスに影響を及ぼします。代わりに、これらの変更を新規ポリシーに保存することもできます。", "xpack.indexLifecycleMgmt.editPolicy.editingExistingPolicyMessage": "既存のポリシーを編集しています", "xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage": "インデックスライフサイクルポリシー {originalPolicyName} を編集します", + "xpack.indexLifecycleMgmt.editPolicy.forceMerge.bestCompressionText": "格納されたフィールドでは、低パフォーマンスで高圧縮を使用します。", + "xpack.indexLifecycleMgmt.editPolicy.forceMerge.enableExplanationText": "小さなファイルを結合して削除されたファイルを消去することで、シャードのセグメント数を減らします。", + "xpack.indexLifecycleMgmt.editPolicy.forceMerge.enableText": "強制結合", "xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage": "このページのエラーを修正してください。", "xpack.indexLifecycleMgmt.editPolicy.hidePolicyJsonButto": "リクエストを非表示", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescriptionMessage": "このフェーズは必須です。アクティブにクエリを実行しインデックスに書き込んでいます。 更新を高速化するため、大きくなりすぎたり古くなりすぎたりした際にインデックスをロールオーバーできます。", @@ -7973,13 +8568,21 @@ "xpack.indexLifecycleMgmt.editPolicy.learnAboutIndexTemplatesLink": "インデックステンプレートの詳細をご覧ください", "xpack.indexLifecycleMgmt.editPolicy.learnAboutShardAllocationLink": "シャード割り当ての詳細をご覧ください", "xpack.indexLifecycleMgmt.editPolicy.learnAboutTimingText": "タイミングの詳細をご覧ください", + "xpack.indexLifecycleMgmt.editPolicy.lifecyclePoliciesLoadingFailedTitle": "既存のライフサイクルポリシーを読み込めません", + "xpack.indexLifecycleMgmt.editPolicy.lifecyclePoliciesReloadButton": "再試行", "xpack.indexLifecycleMgmt.editPolicy.lifecyclePolicyDescriptionText": "インデックスへのアクティブな書き込みから削除までの、インデックスライフサイクルの 4 つのフェーズを自動化するには、インデックスポリシーを使用します。", "xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError": "最高年齢が必要です。", "xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError": "最高ドキュメント数が必要です。", "xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大インデックスサイズが必要です。", "xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名前", - "xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "シャードの割当をコントロールするノード属性を選択", - "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml でノード属性が構成されていません", + "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.customOption.description": "ノード属性を使用して、シャード割り当てを制御します。{learnMoreLink}。", + "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption": "割り当て構成を修正しない", + "xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "ノード属性を選択", + "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle": "ノード属性を読み込めません", + "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "カスタムノード属性が構成されていません", + "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton": "再試行", + "xpack.indexLifecycleMgmt.editPolicy.nodeDetailsLoadingFailedTitle": "ノード属性詳細を読み込めません", + "xpack.indexLifecycleMgmt.editPolicy.nodeDetailsReloadButton": "再試行", "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字が必要です。", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "コールドフェーズのタイミング", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "コールドフェーズのタイミングの単位", @@ -7988,6 +8591,7 @@ "xpack.indexLifecycleMgmt.editPolicy.phaseErrorMessage": "エラーを修正してください", "xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel": "ウォームフェーズのタイミング", "xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel": "ウォームフェーズのタイミングの単位", + "xpack.indexLifecycleMgmt.editPolicy.policiesLoading": "ポリシーを読み込み中…", "xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError": "このポリシー名は既に使用されています。", "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError": "ポリシー名にはコンマを使用できません。", "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError": "ポリシー名にはスペースを使用できません。", @@ -8012,13 +8616,20 @@ "xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage": "ライフサイクルポリシー「{lifecycleName}」を {verb}", "xpack.indexLifecycleMgmt.editPolicy.updatedMessage": "更新しました", "xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage": "ポリシー名の頭にアンダーラインを使用することはできず、括弧やスペースを含めることもできません。", - "xpack.indexLifecycleMgmt.editPolicy.viewNodeDetailsButton": "この構成に関連付けられているインデックスのリストを表示", + "xpack.indexLifecycleMgmt.editPolicy.viewNodeDetailsButton": "選択した属性のノードを表示", + "xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription": "属性に基づく割り当てを使用するには、elasticsearch.ymlでカスタムノード属性を定義します。ウォームノードが使用されます。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.activateWarmPhaseSwitchLabel": "ウォームフェーズを有効にする", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.indexPriorityExplanationText": "ノードの再起動後にインデックスを復元する優先順位を設定します。優先順位の高いインデックスは優先順位の低いインデックスよりも先に復元されます。", + "xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel": "レプリカを設定", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.shrinkIndexExplanationText": "インデックス情報をプライマリシャードの少ない新規インデックスに縮小します。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.shrinkText": "縮小", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescriptionMessage": "まだインデックスにクエリを実行中ですが、読み取り専用です。性能の低いハードウェアにシャードを割り当てることができます。検索を高速化するために、シャードの数を減らしセグメントを結合することができます。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseLabel": "ウォームフェーズ", + "xpack.indexLifecycleMgmt.featureCatalogueDescription": "ライフサイクルポリシーを定義し、インデックス年齢として自動的に処理を実行します。", + "xpack.indexLifecycleMgmt.featureCatalogueTitle": "インデックスライフサイクルを管理", + "xpack.indexLifecycleMgmt.forcemerge.bestCompressionLabel": "格納されたフィールドを圧縮", + "xpack.indexLifecycleMgmt.forcemerge.enableLabel": "データを強制結合", + "xpack.indexLifecycleMgmt.forceMerge.numberOfSegmentsLabel": "セグメントの数", "xpack.indexLifecycleMgmt.hotPhase.bytesLabel": "バイト", "xpack.indexLifecycleMgmt.hotPhase.daysLabel": "日", "xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel": "ロールオーバーを有効にする", @@ -8065,6 +8676,7 @@ "xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.modalTitle": "「{indexName}」にライフサイクルポリシーを追加", "xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.noPoliciesWarningTitle": "インデックスライフサイクルポリシーが定義されていません", "xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.noPolicySelectedErrorMessage": "ポリシーの選択が必要です。", + "xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyToTemplateConfirmModal.errorLoadingTemplatesButton": "再試行", "xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyToTemplateConfirmModal.indexHasNoAliasesWarningMessage": "このインデックステンプレートには既にポリシー {existingPolicyName} が適用されています。このポリシーを追加するとこの構成が上書きされます。", "xpack.indexLifecycleMgmt.indexManagementTable.removeLifecyclePolicyConfirmModal.cancelButtonText": "キャンセル", "xpack.indexLifecycleMgmt.indexManagementTable.removeLifecyclePolicyConfirmModal.modalTitle": "{count, plural, one {インデックス} other {インデックス}}からライフサイクルポリシーを削除します", @@ -8076,6 +8688,7 @@ "xpack.indexLifecycleMgmt.indexMgmtBanner.filterLabel": "エラーを表示", "xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel": "コールド", "xpack.indexLifecycleMgmt.indexMgmtFilter.deleteLabel": "削除", + "xpack.indexLifecycleMgmt.indexMgmtFilter.frozenLabel": "凍結", "xpack.indexLifecycleMgmt.indexMgmtFilter.hotLabel": "ホット", "xpack.indexLifecycleMgmt.indexMgmtFilter.lifecyclePhaseLabel": "ライフサイクルフェーズ", "xpack.indexLifecycleMgmt.indexMgmtFilter.lifecycleStatusLabel": "ライフサイクルステータス", @@ -8100,10 +8713,12 @@ "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.chooseTemplateLabel": "インデックステンプレート", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.chooseTemplateMessage": "インデックステンプレートを選択してください", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.confirmButton": "ポリシーを追加", + "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.errorLoadingTemplatesTitle": "インデックステンプレートを読み込めません", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.errorMessage": "インデックステンプレート {templateName} へのポリシー {policyName} の追加中にエラーが発生しました", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.explanationText": "これにより、インデックステンプレートと一致するすべてのインデックスにライフサイクルポリシーが適用されます。", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.noTemplateSelectedErrorMessage": "インデックステンプレートの選択が必要です。", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.rolloverAliasLabel": "ロールオーバーインデックスのエイリアス", + "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.showLegacyTemplates": "レガシーインデックステンプレートを表示", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.successMessage": "インデックステンプレート {templateName} にポリシー {policyName} を追加しました", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.templateHasPolicyWarningTitle": "テンプレートに既にポリシーがあります", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title": "インデックステンプレートにポリシー「{name}」 を追加", @@ -8118,6 +8733,9 @@ "xpack.indexLifecycleMgmt.policyTable.headers.modifiedDateHeader": "変更日", "xpack.indexLifecycleMgmt.policyTable.headers.nameHeader": "名前", "xpack.indexLifecycleMgmt.policyTable.headers.versionHeader": "バージョン", + "xpack.indexLifecycleMgmt.policyTable.policiesLoading": "ポリシーを読み込み中…", + "xpack.indexLifecycleMgmt.policyTable.policiesLoadingFailedTitle": "既存のライフサイクルポリシーを読み込めません", + "xpack.indexLifecycleMgmt.policyTable.policiesReloadButton": "再試行", "xpack.indexLifecycleMgmt.policyTable.policyActionsMenu.panelTitle": "ポリシーオプション", "xpack.indexLifecycleMgmt.policyTable.sectionDescription": "インデックスが古くなるにつれ管理します。 インデックスのライフサイクルにおける進捗のタイミングと方法を自動化するポリシーを設定します。", "xpack.indexLifecycleMgmt.policyTable.sectionHeading": "インデックスライフサイクルポリシー", @@ -8127,9 +8745,19 @@ "xpack.indexLifecycleMgmt.removeIndexLifecycleActionButtonLabel": "ライフサイクルポリシーを削除", "xpack.indexLifecycleMgmt.retryIndexLifecycleAction.retriedLifecycleMessage": "ライフサイクルのステップを再試行します {indexNames}", "xpack.indexLifecycleMgmt.retryIndexLifecycleActionButtonLabel": "ライフサイクルのステップを再試行", + "xpack.indexLifecycleMgmt.templateNotFoundMessage": "テンプレート{name}が見つかりません。", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody": "ロールに基づく割り当てを使用するには、1つ以上のノードを、ウォームまたはホットティアに割り当てます。使用可能なノードがない場合、ポリシーは割り当てを完了できません。", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle": "ウォームティアに割り当てられているノードがありません", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold": "このポリシーはコールドフェーズのデータを{tier}ティアノードに移動します。", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold.title": "コールドティアに割り当てられているノードがありません", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "このポリシーはウォームフェーズのデータを{tier}ティアノードに移動します。", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "ウォームティアに割り当てられているノードがありません", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。", "xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel": "ロールオーバー時にウォームフェーズに変更", "xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel": "プライマリシャードの数", + "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription": "レプリカの数を設定します。デフォルトでは前のフェーズと同じです。", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "レプリカの数", + "xpack.indexLifecycleMgmt.warmPhase.replicasTitle": "レプリカ", "xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel": "インデックスを縮小", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", @@ -8139,6 +8767,13 @@ "xpack.infra.alerting.logs.createAlertButton": "アラートの作成", "xpack.infra.alerting.logs.manageAlerts": "アラートを管理", "xpack.infra.alerting.manageAlerts": "アラートを管理", + "xpack.infra.alerts.charts.errorMessage": "問題が発生しました", + "xpack.infra.alerts.charts.loadingMessage": "読み込み中", + "xpack.infra.alerts.charts.noDataMessage": "グラフデータがありません", + "xpack.infra.alerts.timeLabels.days": "日", + "xpack.infra.alerts.timeLabels.hours": "時間", + "xpack.infra.alerts.timeLabels.minutes": "分", + "xpack.infra.alerts.timeLabels.seconds": "秒", "xpack.infra.analysisSetup.actionStepTitle": "MLジョブを作成", "xpack.infra.analysisSetup.configurationStepTitle": "構成", "xpack.infra.analysisSetup.createMlJobButton": "ML ジョブを作成", @@ -8215,6 +8850,7 @@ "xpack.infra.header.infrastructureNavigationTitle": "メトリック", "xpack.infra.header.infrastructureTitle": "メトリック", "xpack.infra.header.logsTitle": "ログ", + "xpack.infra.hideHistory": "履歴を表示しない", "xpack.infra.homePage.documentTitle": "メトリック", "xpack.infra.homePage.inventoryTabTitle": "インベントリ", "xpack.infra.homePage.metricsExplorerTabTitle": "メトリックエクスプローラー", @@ -8243,6 +8879,12 @@ "xpack.infra.inventoryModels.findToolbar.error": "検索しようとしたツールバーは存在しません。", "xpack.infra.inventoryModels.host.singularDisplayName": "ホスト", "xpack.infra.inventoryModels.pod.singularDisplayName": "Kubernetes ポッド", + "xpack.infra.inventoryTimeline.checkNewDataButtonLabel": "新規データを確認", + "xpack.infra.inventoryTimeline.errorTitle": "履歴データを表示できません。", + "xpack.infra.inventoryTimeline.header": "平均{metricLabel}", + "xpack.infra.inventoryTimeline.legend.anomalyLabel": "異常が検知されました", + "xpack.infra.inventoryTimeline.noHistoryDataTitle": "表示する履歴データがありません。", + "xpack.infra.inventoryTimeline.retryButtonLabel": "再試行", "xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage": "{metricId} のモデルには cloudId が必要ですが、{nodeId} に cloudId が指定されていません。", "xpack.infra.kibanaMetrics.invalidInfraMetricErrorMessage": "{id} は有効な InfraMetric ではありません", "xpack.infra.kibanaMetrics.nodeDoesNotExistErrorMessage": "{nodeId} が存在しません。", @@ -8283,12 +8925,20 @@ "xpack.infra.logs.alertFlyout.error.criterionComparatorRequired": "コンパレーターが必要です。", "xpack.infra.logs.alertFlyout.error.criterionFieldRequired": "フィールドが必要です。", "xpack.infra.logs.alertFlyout.error.criterionValueRequired": "値が必要です。", + "xpack.infra.logs.alertFlyout.error.thresholdRequired": "数値しきい値は必須です。", "xpack.infra.logs.alertFlyout.error.timeSizeRequired": "ページサイズが必要です。", "xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix": "With", "xpack.infra.logs.alertFlyout.removeCondition": "条件を削除", "xpack.infra.logs.alertFlyout.sourceStatusError": "申し訳ありません。フィールド情報の読み込み中に問題が発生しました", "xpack.infra.logs.alertFlyout.sourceStatusErrorTryAgain": "再試行", "xpack.infra.logs.alertFlyout.successiveCriterionFieldPrefix": "AND", + "xpack.infra.logs.alertFlyout.thresholdPopoverTitle": "しきい値", + "xpack.infra.logs.alertFlyout.thresholdPrefix": "is", + "xpack.infra.logs.alertFlyout.thresholdTypeCount": "カウント", + "xpack.infra.logs.alertFlyout.thresholdTypeCountSuffix": "ログエントリの", + "xpack.infra.logs.alertFlyout.thresholdTypePrefix": "", + "xpack.infra.logs.alertFlyout.thresholdTypeRatio": "クエリAとクエリBの", + "xpack.infra.logs.alertFlyout.thresholdTypeRatioSuffix": "比率", "xpack.infra.logs.alerting.comparator.eq": "is", "xpack.infra.logs.alerting.comparator.eqNumber": "一致する", "xpack.infra.logs.alerting.comparator.gt": "より多い", @@ -8302,10 +8952,21 @@ "xpack.infra.logs.alerting.comparator.notMatch": "一致しない", "xpack.infra.logs.alerting.comparator.notMatchPhrase": "語句と一致しない", "xpack.infra.logs.alerting.threshold.conditionsActionVariableDescription": "ログエントリが満たす必要がある条件", - "xpack.infra.logs.alerting.threshold.defaultActionMessage": "\\{\\{#context.group\\}\\}\\{\\{context.group\\}\\} - \\{\\{/context.group\\}\\}\\{\\{context.matchingDocuments\\}\\}ログエントリは次の条件と一致しました。\\{\\{context.conditions\\}\\}", + "xpack.infra.logs.alerting.threshold.defaultActionMessage": "\\{\\{^context.isRatio\\}\\}\\{\\{#context.group\\}\\}\\{\\{context.group\\}\\} - \\{\\{/context.group\\}\\}\\{\\{context.matchingDocuments\\}\\}ログエントリが次の条件と一致しました。\\{\\{context.conditions\\}\\}\\{\\{/context.isRatio\\}\\}\\{\\{#context.isRatio\\}\\}\\{\\{#context.group\\}\\}\\{\\{context.group\\}\\} - \\{\\{/context.group\\}\\} \\{\\{context.denominatorConditions\\}\\}と一致するログエントリ数に対する\\{\\{context.numeratorConditions\\}\\}と一致するログエントリ数の比率は\\{\\{context.ratio\\}\\}\\{\\{/context.isRatio\\}\\}でした", + "xpack.infra.logs.alerting.threshold.denominatorConditionsActionVariableDescription": "比率の分母が満たす必要がある条件", "xpack.infra.logs.alerting.threshold.documentCountActionVariableDescription": "指定された条件と一致したログエントリ数", + "xpack.infra.logs.alerting.threshold.everythingSeriesName": "ログエントリ", "xpack.infra.logs.alerting.threshold.fired": "実行", "xpack.infra.logs.alerting.threshold.groupByActionVariableDescription": "アラートのトリガーを実行するグループの名前", + "xpack.infra.logs.alerting.threshold.isRatioActionVariableDescription": "このアラートが比率で構成されていたかどうかを示します", + "xpack.infra.logs.alerting.threshold.numeratorConditionsActionVariableDescription": "比率の分子が満たす必要がある条件", + "xpack.infra.logs.alerting.threshold.ratioActionVariableDescription": "2つのセットの条件の比率値", + "xpack.infra.logs.alerting.threshold.ratioCriteriaQueryAText": "クエリA", + "xpack.infra.logs.alerting.threshold.ratioCriteriaQueryBText": "クエリB", + "xpack.infra.logs.alerting.threshold.timestampActionVariableDescription": "アラートがトリガーされた時点のOTCタイムスタンプ", + "xpack.infra.logs.alerts.dataTimeRangeLabel": "過去{lookback} {timeLabel}のデータ", + "xpack.infra.logs.alerts.dataTimeRangeLabelWithGrouping": "{groupByLabel}でグループ化された、過去{lookback} {timeLabel}のデータ({displayedGroups}/{totalGroups}個のグループを表示)", + "xpack.infra.logs.analsysisSetup.indexQualityWarningTooltipMessage": "これらのインデックスからのログメッセージの分析中に、結果の品質を低下させる可能性がある一部の問題が検出されました。これらのインデックスや問題のあるデータセットを分析から除外することを検討してください。", "xpack.infra.logs.analysis.analyzeInMlButtonLabel": "ML で分析", "xpack.infra.logs.analysis.anomaliesExpandedRowActualRateDescription": "実際", "xpack.infra.logs.analysis.anomaliesExpandedRowActualRateTitle": "{actualCount, plural, one {件のメッセージ} other {件のメッセージ}}", @@ -8338,6 +8999,7 @@ "xpack.infra.logs.analysis.logEntryCategoriesModuleDescription": "機械学習を使用して、ログメッセージを自動的に分類します。", "xpack.infra.logs.analysis.logEntryCategoriesModuleName": "カテゴリー分け", "xpack.infra.logs.analysis.logEntryExamplesViewAnomalyInMlLabel": "機械学習で異常を表示", + "xpack.infra.logs.analysis.logEntryExamplesViewDetailsLabel": "詳細を表示", "xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel": "ストリームで表示", "xpack.infra.logs.analysis.logEntryRateModuleDescription": "機械学習を使用して自動的に異常ログエントリ率を検出します。", "xpack.infra.logs.analysis.logEntryRateModuleName": "ログレート", @@ -8358,6 +9020,7 @@ "xpack.infra.logs.analysis.setupStatusTryAgainButton": "再試行", "xpack.infra.logs.analysis.setupStatusUnknownTitle": "MLジョブのステータスを特定できませんでした。", "xpack.infra.logs.analysis.userManagementButtonLabel": "ユーザーの管理", + "xpack.infra.logs.analysis.viewInMlButtonLabel": "機械学習で表示", "xpack.infra.logs.analysisPage.loadingMessage": "分析ジョブのステータスを確認中…", "xpack.infra.logs.analysisPage.unavailable.mlAppLink": "機械学習アプリ", "xpack.infra.logs.categoryExample.viewInContextText": "コンテキストで表示", @@ -8403,8 +9066,9 @@ "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "ML で分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "ML アプリでこのカテゴリーを分析します。", "xpack.infra.logs.logEntryCategories.categoryColumnTitle": "カテゴリー", - "xpack.infra.logs.logEntryCategories.categoryQualityWarningCalloutMessage": "ログメッセージの分析中に、分類結果の品質を低下させる可能性がある一部の問題が検出されました。", + "xpack.infra.logs.logEntryCategories.categoryQualityWarningCalloutMessage": "ログメッセージの分析中に、分類結果の品質を低下させる可能性がある一部の問題が検出されました。該当するデータセットを分析から除外することを検討してください。", "xpack.infra.logs.logEntryCategories.categoryQUalityWarningCalloutTitle": "品質に関する警告", + "xpack.infra.logs.logEntryCategories.categoryQualityWarningDetailsAccordionButtonLabel": "詳細", "xpack.infra.logs.logEntryCategories.countColumnTitle": "メッセージ数", "xpack.infra.logs.logEntryCategories.datasetColumnTitle": "データセット", "xpack.infra.logs.logEntryCategories.jobStatusLoadingMessage": "分類ジョブのステータスを確認中...", @@ -8595,8 +9259,8 @@ "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "すべてを対象にする", "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データなしの件数:{boldedResultsNumber}", "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber": "{noData, plural, one {件の結果がありました} other {件の結果がありました}}", - "xpack.infra.metrics.alertFlyout.alertPreviewResult": "このアラートは{firedTimes}回発生しました", - "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "過去{lookback}", + "xpack.infra.metrics.alertFlyout.alertPreviewTotalNotifications": "結果として、このアラートは、「{alertThrottle}」に関して選択した[通知間隔]設定に基づいて{notifications}を送信しました。", + "xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber": "{notifs, plural, one {#通知} other {#通知}}", "xpack.infra.metrics.alertFlyout.conditions": "条件", "xpack.infra.metrics.alertFlyout.createAlertPerHelpText": "すべての一意の値についてアラートを作成します。例:「host.id」または「cloud.region」。", "xpack.infra.metrics.alertFlyout.createAlertPerText": "次の単位でアラートを作成(任意)", @@ -8615,7 +9279,7 @@ "xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "タイミング", "xpack.infra.metrics.alertFlyout.filterHelpText": "KQL式を使用して、アラートトリガーの範囲を制限します。", "xpack.infra.metrics.alertFlyout.filterLabel": "フィルター(任意)", - "xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {# 時間} other {# 回数}}", + "xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {#インスタンス} other {#インスタンス}}", "xpack.infra.metrics.alertFlyout.hourLabel": "時間", "xpack.infra.metrics.alertFlyout.lastDayLabel": "昨日", "xpack.infra.metrics.alertFlyout.lastHourLabel": "過去1時間", @@ -8634,6 +9298,7 @@ "xpack.infra.metrics.alertFlyout.weekLabel": "週", "xpack.infra.metrics.alerting.alertStateActionVariableDescription": "現在のアラートの状態", "xpack.infra.metrics.alerting.groupActionVariableDescription": "データを報告するグループの名前", + "xpack.infra.metrics.alerting.inventory.noDataFormattedValue": "[データなし]", "xpack.infra.metrics.alerting.inventory.threshold.defaultActionMessage": "\\{\\{alertName\\}\\} - \\{\\{context.group\\}\\}は状態\\{\\{context.alertState\\}\\}です\n\n理由:\n\\{\\{context.reason\\}\\}\n", "xpack.infra.metrics.alerting.inventory.threshold.fired": "実行", "xpack.infra.metrics.alerting.metricActionVariableDescription": "指定された条件のメトリック名。使用方法:(ctx.metric.condition0、ctx.metric.condition1など)。", @@ -8653,6 +9318,7 @@ "xpack.infra.metrics.alerting.threshold.gtComparator": "より大きい", "xpack.infra.metrics.alerting.threshold.ltComparator": "より小さい", "xpack.infra.metrics.alerting.threshold.noDataAlertReason": "{metric}は過去{interval}にデータを報告していません", + "xpack.infra.metrics.alerting.threshold.noDataFormattedValue": "[データなし]", "xpack.infra.metrics.alerting.threshold.noDataState": "データなし", "xpack.infra.metrics.alerting.threshold.okState": "OK [回復済み]", "xpack.infra.metrics.alerting.threshold.outsideRangeComparator": "の間にない", @@ -8707,6 +9373,7 @@ "xpack.infra.metricsExplorer.emptyChart.body": "チャートをレンダリングできません。", "xpack.infra.metricsExplorer.emptyChart.title": "チャートデータがありません", "xpack.infra.metricsExplorer.errorMessage": "「{message}」によりリクエストは実行されませんでした", + "xpack.infra.metricsExplorer.everything": "すべて", "xpack.infra.metricsExplorer.filterByLabel": "フィルターを追加します", "xpack.infra.metricsExplorer.footerPaginationMessage": "「{groupBy}」でグループ化された{total}件中{length}件のチャートを表示しています。", "xpack.infra.metricsExplorer.groupByAriaLabel": "graph/", @@ -8723,6 +9390,46 @@ "xpack.infra.metricsExplorer.openInTSVB": "ビジュアライザーで開く", "xpack.infra.metricsExplorer.viewNodeDetail": "{name} のメトリックを表示", "xpack.infra.metricsHeaderAddDataButtonLabel": "データの追加", + "xpack.infra.ml.anomalyDetectionButton": "異常検知", + "xpack.infra.ml.anomalyFlyout.anomaliesTabLabel": "異常を表示", + "xpack.infra.ml.anomalyFlyout.create.createButton": "有効にする", + "xpack.infra.ml.anomalyFlyout.create.description": "異常検知は機械学習によって実現されています。次のリソースタイプでは、機械学習ジョブを使用できます。このようなジョブを有効にすると、インフラストラクチャメトリックで異常の検出を開始します。", + "xpack.infra.ml.anomalyFlyout.create.hostDescription": "ホストのメモリー使用状況とネットワークトラフィックの異常を検出します。", + "xpack.infra.ml.anomalyFlyout.create.hostSuccessTitle": "ホスト", + "xpack.infra.ml.anomalyFlyout.create.hostTitle": "ホスト", + "xpack.infra.ml.anomalyFlyout.create.k8sDescription": "Kubernetesポッドのメモリー使用状況とネットワークトラフィックの異常を検出します。", + "xpack.infra.ml.anomalyFlyout.create.k8sSuccessTitle": "Kubernetes", + "xpack.infra.ml.anomalyFlyout.create.k8sTitle": "Kubernetesポッド", + "xpack.infra.ml.anomalyFlyout.create.recreateButton": "ジョブの再作成", + "xpack.infra.ml.anomalyFlyout.enabledCallout": "{target}の異常検知が有効です", + "xpack.infra.ml.anomalyFlyout.flyoutHeader": "機械学習異常検知", + "xpack.infra.ml.anomalyFlyout.jobStatusLoadingMessage": "メトリックジョブのステータスを確認中…", + "xpack.infra.ml.anomalyFlyout.manageJobs": "ジョブの管理", + "xpack.infra.ml.aomalyFlyout.jobSetup.flyoutHeader": "{nodeType}の機械学習を有効にする", + "xpack.infra.ml.metricsHostModuleDescription": "機械学習を使用して自動的に異常ログエントリ率を検出します。", + "xpack.infra.ml.metricsModuleName": "メトリック異常検知", + "xpack.infra.ml.splash.learnMoreLink": "ドキュメンテーションを表示", + "xpack.infra.ml.splash.learnMoreTitle": "詳細について", + "xpack.infra.ml.splash.loadingMessage": "ライセンスを確認しています...", + "xpack.infra.ml.splash.splashImageAlt": "プレースホルダー画像", + "xpack.infra.ml.splash.startTrialCta": "トライアルを開始", + "xpack.infra.ml.splash.startTrialDescription": "無料の試用版には、機械学習機能が含まれており、ログで異常を検出することができます。", + "xpack.infra.ml.splash.startTrialTitle": "異常検知を利用するには、無料の試用版を開始してください", + "xpack.infra.ml.splash.updateSubscriptionCta": "サブスクリプションのアップグレード", + "xpack.infra.ml.splash.updateSubscriptionDescription": "機械学習機能を使用するには、プラチナサブスクリプションが必要です。", + "xpack.infra.ml.splash.updateSubscriptionTitle": "異常検知を利用するには、プラチナサブスクリプションにアップグレードしてください", + "xpack.infra.ml.steps.setupProcess.cancelButton": "キャンセル", + "xpack.infra.ml.steps.setupProcess.description": "ジョブが作成された後は設定を変更できません。ジョブはいつでも再作成できますが、以前に検出された異常は削除されません。", + "xpack.infra.ml.steps.setupProcess.enableButton": "ジョブを有効にする", + "xpack.infra.ml.steps.setupProcess.failureText": "必要なMLジョブの作成中に問題が発生しました。", + "xpack.infra.ml.steps.setupProcess.loadingText": "MLジョブを作成中…", + "xpack.infra.ml.steps.setupProcess.partition.description": "パーティションを使用すると、類似した動作のデータのグループに対して、独立したモデルを作成することができます。たとえば、コンピュータータイプやクラウド可用性ゾーン別に区分することができます。", + "xpack.infra.ml.steps.setupProcess.partition.label": "パーティションフィールド", + "xpack.infra.ml.steps.setupProcess.partition.title": "どのようにしてデータを区分しますか?", + "xpack.infra.ml.steps.setupProcess.tryAgainButton": "再試行", + "xpack.infra.ml.steps.setupProcess.when.description": "デフォルトでは、機械学習ジョブは直近4週間のデータを分析し、無限に実行し続けます。", + "xpack.infra.ml.steps.setupProcess.when.timePicker.label": "開始日", + "xpack.infra.ml.steps.setupProcess.when.title": "いつモデルを開始しますか?", "xpack.infra.node.ariaLabel": "{nodeName}、クリックしてメニューを開きます", "xpack.infra.nodeContextMenu.createAlertLink": "アラートの作成", "xpack.infra.nodeContextMenu.description": "{label} {value} の詳細を表示", @@ -8771,6 +9478,7 @@ "xpack.infra.savedView.searchPlaceholder": "保存されたビューの検索", "xpack.infra.savedView.unknownView": "ビューが選択されていません", "xpack.infra.savedView.updateView": "ビューの更新", + "xpack.infra.showHistory": "履歴を表示", "xpack.infra.snapshot.missingSnapshotMetricError": "{nodeType}の{metric}の集約を使用できません。", "xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "列を追加", "xpack.infra.sourceConfiguration.applySettingsButtonLabel": "申し込む", @@ -8920,38 +9628,59 @@ "xpack.infra.waffle.unableToSelectMetricErrorTitle": "メトリックのオプションまたは値を選択できません", "xpack.infra.waffleTime.autoRefreshButtonLabel": "自動更新", "xpack.infra.waffleTime.stopRefreshingButtonLabel": "更新中止", + "xpack.ingestManager.agentBulkActions.agentsSelected": "{count, plural, one {#個のエージェント} other {#個のエージェント}}が選択されました", + "xpack.ingestManager.agentBulkActions.clearSelection": "選択した項目をクリア", + "xpack.ingestManager.agentBulkActions.reassignPolicy": "新しいポリシーに割り当てる", + "xpack.ingestManager.agentBulkActions.selectAll": "すべてのページのすべての項目を選択", + "xpack.ingestManager.agentBulkActions.totalAgents": "{count, plural, one {#個のエージェント} other {#個のエージェント}}を表示しています", + "xpack.ingestManager.agentBulkActions.totalAgentsWithLimit": "{count}/{total}個のエージェントを表示しています", + "xpack.ingestManager.agentBulkActions.unenrollAgents": "エージェントの登録を解除", + "xpack.ingestManager.agentBulkActions.upgradeAgents": "エージェントをアップグレード", "xpack.ingestManager.agentDetails.actionsButton": "アクション", "xpack.ingestManager.agentDetails.agentDetailsTitle": "エージェント'{id}'", "xpack.ingestManager.agentDetails.agentNotFoundErrorDescription": "エージェントID {agentId}が見つかりません", "xpack.ingestManager.agentDetails.agentNotFoundErrorTitle": "エージェントが見つかりません", + "xpack.ingestManager.agentDetails.agentPolicyLabel": "エージェントポリシー", + "xpack.ingestManager.agentDetails.agentVersionLabel": "エージェントバージョン", "xpack.ingestManager.agentDetails.hostIdLabel": "エージェントID", "xpack.ingestManager.agentDetails.hostNameLabel": "ホスト名", "xpack.ingestManager.agentDetails.localMetadataSectionSubtitle": "メタデータを読み込み中", "xpack.ingestManager.agentDetails.metadataSectionTitle": "メタデータ", "xpack.ingestManager.agentDetails.platformLabel": "プラットフォーム", + "xpack.ingestManager.agentDetails.policyLabel": "ポリシー", + "xpack.ingestManager.agentDetails.releaseLabel": "エージェントリリース", "xpack.ingestManager.agentDetails.statusLabel": "ステータス", "xpack.ingestManager.agentDetails.subTabs.activityLogTab": "アクティビティログ", "xpack.ingestManager.agentDetails.subTabs.detailsTab": "エージェントの詳細", "xpack.ingestManager.agentDetails.unexceptedErrorTitle": "エージェントの読み込み中にエラーが発生しました", + "xpack.ingestManager.agentDetails.upgradeAvailableTooltip": "アップグレードが利用可能です", "xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "ユーザー提供メタデータ", "xpack.ingestManager.agentDetails.versionLabel": "エージェントバージョン", "xpack.ingestManager.agentDetails.viewAgentListTitle": "すべてのエージェントを表示", + "xpack.ingestManager.agentEnrollment.agentDescription": "Elasticエージェントをホストに追加し、データを収集して、Elastic Stackに送信します。", + "xpack.ingestManager.agentEnrollment.agentsNotInitializedText": "エージェントを登録する前に、{link}。", "xpack.ingestManager.agentEnrollment.cancelButtonLabel": "キャンセル", "xpack.ingestManager.agentEnrollment.continueButtonLabel": "続行", + "xpack.ingestManager.agentEnrollment.copyPolicyButton": "クリップボードにコピー", "xpack.ingestManager.agentEnrollment.copyRunInstructionsButton": "クリップボードにコピー", - "xpack.ingestManager.agentEnrollment.downloadDescription": "ホストのコンピューターでElasticエージェントをダウンロードします。Elasticエージェントダウンロードページでは、エージェントバイナリと検証署名にアクセスできます。", - "xpack.ingestManager.agentEnrollment.downloadLink": "elastic.co/downloadsに移動", - "xpack.ingestManager.agentEnrollment.enrollFleetTabLabel": "フリートに登録", - "xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "スタンドアロンモード", + "xpack.ingestManager.agentEnrollment.downloadDescription": "Elasticエージェントダウンロードページでは、エージェントバイナリと検証署名をダウンロードできます。", + "xpack.ingestManager.agentEnrollment.downloadLink": "ダウンロードページに移動", + "xpack.ingestManager.agentEnrollment.downloadPolicyButton": "ダウンロードポリシー", + "xpack.ingestManager.agentEnrollment.enrollFleetTabLabel": "Fleetで登録", + "xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "スタンドアロンで実行", "xpack.ingestManager.agentEnrollment.flyoutTitle": "エージェントの追加", - "xpack.ingestManager.agentEnrollment.managedDescription": "必要なエージェントの数に関係なく、Fleetでは、簡単に一元的に更新を管理し、エージェントにデプロイすることができます。次の手順に従い、Elasticエージェントをダウンロードし、Fleetに登録してください。", - "xpack.ingestManager.agentEnrollment.standaloneDescription": "スタンドアロンモードで実行中のエージェントは、構成を変更したい場合には、手動で更新する必要があります。次の手順に従い、スタンドアロンモードでElasticエージェントをダウンロードし、セットアップしてください。", + "xpack.ingestManager.agentEnrollment.goToDataStreamsLink": "データストリーム", + "xpack.ingestManager.agentEnrollment.managedDescription": "ElasticエージェントをFleetに登録して、自動的に更新をデプロイしたり、一元的にエージェントを管理したりします。", + "xpack.ingestManager.agentEnrollment.setUpAgentsLink": "Elasticエージェントの集中管理を設定", + "xpack.ingestManager.agentEnrollment.standaloneDescription": "Elasticエージェントをスタンドアロンで実行して、エージェントがインストールされているホストで、手動でエージェントを構成および更新します。", + "xpack.ingestManager.agentEnrollment.stepCheckForDataDescription": "エージェントがデータの送信を開始します。{link}に移動して、データを表示してください。", "xpack.ingestManager.agentEnrollment.stepCheckForDataTitle": "データを確認", - "xpack.ingestManager.agentEnrollment.stepConfigureAgentDescription": "この構成をコピーし、Elasticエージェントがインストールされているシステムのファイル{fileName}に置きます。必ず、構成ファイルの{outputSection}セクションで{ESUsernameVariable}と{ESPasswordVariable}を修正し、実際のElasticsearch認証資格情報が使用されるようにしてください。", + "xpack.ingestManager.agentEnrollment.stepChooseAgentPolicyTitle": "エージェントポリシーを選択", + "xpack.ingestManager.agentEnrollment.stepConfigureAgentDescription": "Elasticエージェントがインストールされているホストで、このポリシーを{fileName}にコピーします。Elasticsearch資格情報を使用するには、{fileName}の{outputSection}セクションで、{ESUsernameVariable}と{ESPasswordVariable}を変更します。", "xpack.ingestManager.agentEnrollment.stepConfigureAgentTitle": "エージェントの構成", - "xpack.ingestManager.agentEnrollment.stepDownloadAgentTitle": "Elasticエージェントをダウンロード", + "xpack.ingestManager.agentEnrollment.stepDownloadAgentTitle": "Elasticエージェントをホストにダウンロード", "xpack.ingestManager.agentEnrollment.stepEnrollAndRunAgentTitle": "Elasticエージェントを登録して実行", - "xpack.ingestManager.agentEnrollment.stepRunAgentDescription": "エージェントディレクトリで、次のコマンドを実行し、エージェントを起動します。", + "xpack.ingestManager.agentEnrollment.stepRunAgentDescription": "エージェントのディレクトリから、このコマンドを実行し、Elasticエージェントを、インストール、登録、起動します。このコマンドを再利用すると、複数のホストでエージェントを設定できます。管理者権限が必要です。", "xpack.ingestManager.agentEnrollment.stepRunAgentTitle": "エージェントの起動", "xpack.ingestManager.agentEventsList.collapseDetailsAriaLabel": "詳細を非表示", "xpack.ingestManager.agentEventsList.expandDetailsAriaLabel": "詳細を表示", @@ -8968,11 +9697,13 @@ "xpack.ingestManager.agentEventSubtype.degradedLabel": "劣化", "xpack.ingestManager.agentEventSubtype.failedLabel": "失敗", "xpack.ingestManager.agentEventSubtype.inProgressLabel": "進行中", + "xpack.ingestManager.agentEventSubtype.policyLabel": "ポリシー", "xpack.ingestManager.agentEventSubtype.runningLabel": "実行中", "xpack.ingestManager.agentEventSubtype.startingLabel": "開始中", "xpack.ingestManager.agentEventSubtype.stoppedLabel": "停止", "xpack.ingestManager.agentEventSubtype.stoppingLabel": "停止中", "xpack.ingestManager.agentEventSubtype.unknownLabel": "不明", + "xpack.ingestManager.agentEventSubtype.updatingLabel": "更新中", "xpack.ingestManager.agentEventType.actionLabel": "アクション", "xpack.ingestManager.agentEventType.actionResultLabel": "アクション結果", "xpack.ingestManager.agentEventType.errorLabel": "エラー", @@ -8986,9 +9717,11 @@ "xpack.ingestManager.agentHealth.offlineStatusText": "オフライン", "xpack.ingestManager.agentHealth.onlineStatusText": "オンライン", "xpack.ingestManager.agentHealth.unenrollingStatusText": "登録解除中", + "xpack.ingestManager.agentHealth.updatingStatusText": "更新中", "xpack.ingestManager.agentHealth.warningStatusText": "エラー", "xpack.ingestManager.agentList.actionsColumnTitle": "アクション", "xpack.ingestManager.agentList.addButton": "エージェントの追加", + "xpack.ingestManager.agentList.agentUpgradeLabel": "アップグレードが利用可能です", "xpack.ingestManager.agentList.clearFiltersLinkText": "フィルターを消去", "xpack.ingestManager.agentList.enrollButton": "エージェントの追加", "xpack.ingestManager.agentList.forceUnenrollOneButton": "強制的に登録解除する", @@ -8998,87 +9731,256 @@ "xpack.ingestManager.agentList.noAgentsPrompt": "エージェントが登録されていません", "xpack.ingestManager.agentList.noFilteredAgentsPrompt": "エージェントが見つかりません。{clearFiltersLink}", "xpack.ingestManager.agentList.outOfDateLabel": "最新ではありません", - "xpack.ingestManager.agentList.reassignActionText": "新しいエージェント構成を割り当て", + "xpack.ingestManager.agentList.policyColumnTitle": "エージェントポリシー", + "xpack.ingestManager.agentList.policyFilterText": "エージェントポリシー", + "xpack.ingestManager.agentList.reassignActionText": "新しいポリシーに割り当てる", "xpack.ingestManager.agentList.revisionNumber": "rev. {revNumber}", - "xpack.ingestManager.agentList.showInactiveSwitchLabel": "非アクティブなエージェントを表示", + "xpack.ingestManager.agentList.showInactiveSwitchLabel": "非アクティブ", + "xpack.ingestManager.agentList.showUpgradeableFilterLabel": "アップグレードが利用可能です", "xpack.ingestManager.agentList.statusColumnTitle": "ステータス", "xpack.ingestManager.agentList.statusErrorFilterText": "エラー", "xpack.ingestManager.agentList.statusFilterText": "ステータス", "xpack.ingestManager.agentList.statusOfflineFilterText": "オフライン", "xpack.ingestManager.agentList.statusOnlineFilterText": "オンライン", - "xpack.ingestManager.agentList.unenrollOneButton": "登録解除", + "xpack.ingestManager.agentList.statusUpdatingFilterText": "更新中", + "xpack.ingestManager.agentList.unenrollOneButton": "エージェントの登録解除", + "xpack.ingestManager.agentList.upgradeOneButton": "エージェントをアップグレード", "xpack.ingestManager.agentList.versionTitle": "バージョン", "xpack.ingestManager.agentList.viewActionText": "エージェントを表示", "xpack.ingestManager.agentListStatus.errorLabel": "エラー", "xpack.ingestManager.agentListStatus.offlineLabel": "オフライン", "xpack.ingestManager.agentListStatus.onlineLabel": "オンライン", "xpack.ingestManager.agentListStatus.totalLabel": "エージェント", + "xpack.ingestManager.agentPolicy.confirmModalCalloutDescription": "選択されたエージェントポリシー{policyName}が一部のエージェントですでに使用されていることをFleetが検出しました。このアクションの結果として、Fleetはこのポリシーで使用されているすべてのエージェントに更新をデプロイします。", + "xpack.ingestManager.agentPolicy.confirmModalCancelButtonLabel": "キャンセル", + "xpack.ingestManager.agentPolicy.confirmModalConfirmButtonLabel": "変更を保存してデプロイ", + "xpack.ingestManager.agentPolicy.confirmModalDescription": "このアクションは元に戻せません。続行していいですか?", + "xpack.ingestManager.agentPolicy.confirmModalTitle": "変更を保存してデプロイ", + "xpack.ingestManager.agentPolicy.linkedAgentCountText": "{count, plural, one {#件のエージェント} other {#件のエージェント}}", + "xpack.ingestManager.agentPolicyActionMenu.buttonText": "アクション", + "xpack.ingestManager.agentPolicyActionMenu.copyPolicyActionText": "ポリシーをコピー", + "xpack.ingestManager.agentPolicyActionMenu.enrollAgentActionText": "エージェントの追加", + "xpack.ingestManager.agentPolicyActionMenu.viewPolicyText": "ポリシーを表示", + "xpack.ingestManager.agentPolicyForm.advancedOptionsToggleLabel": "高度なオプション", + "xpack.ingestManager.agentPolicyForm.descriptionFieldLabel": "説明", + "xpack.ingestManager.agentPolicyForm.descriptionFieldPlaceholder": "どのようにこのポリシーを使用しますか?", + "xpack.ingestManager.agentPolicyForm.monitoringDescription": "パフォーマンスのデバッグと追跡のために、エージェントに関するデータを収集します。", + "xpack.ingestManager.agentPolicyForm.monitoringLabel": "アラート監視", + "xpack.ingestManager.agentPolicyForm.monitoringLogsFieldLabel": "エージェントログを収集", + "xpack.ingestManager.agentPolicyForm.monitoringLogsTooltipText": "このポリシーを使用するElasticエージェントからログを収集します。", + "xpack.ingestManager.agentPolicyForm.monitoringMetricsFieldLabel": "エージェントメトリックを収集", + "xpack.ingestManager.agentPolicyForm.monitoringMetricsTooltipText": "このポリシーを使用するElasticエージェントからメトリックを収集します。", + "xpack.ingestManager.agentPolicyForm.nameFieldLabel": "名前", + "xpack.ingestManager.agentPolicyForm.nameFieldPlaceholder": "名前を選択", + "xpack.ingestManager.agentPolicyForm.nameRequiredErrorMessage": "エージェントポリシー名が必要です。", + "xpack.ingestManager.agentPolicyForm.namespaceFieldDescription": "このポリシーを使用する統合にデフォルトの名前空間を適用します。統合はその独自の名前空間を指定できます。", + "xpack.ingestManager.agentPolicyForm.namespaceFieldLabel": "デフォルト名前空間", + "xpack.ingestManager.agentPolicyForm.systemMonitoringFieldLabel": "システム監視", + "xpack.ingestManager.agentPolicyForm.systemMonitoringText": "システムメトリックを収集", + "xpack.ingestManager.agentPolicyForm.systemMonitoringTooltipText": "このオプションを有効にすると、システムメトリックと情報を収集する統合でポリシーをブートストラップできます。", + "xpack.ingestManager.agentPolicyList.actionsColumnTitle": "アクション", + "xpack.ingestManager.agentPolicyList.addButton": "エージェントポリシーを作成", + "xpack.ingestManager.agentPolicyList.agentsColumnTitle": "エージェント", + "xpack.ingestManager.agentPolicyList.clearFiltersLinkText": "フィルターを消去", + "xpack.ingestManager.agentPolicyList.descriptionColumnTitle": "説明", + "xpack.ingestManager.agentPolicyList.loadingAgentPoliciesMessage": "エージェントポリシーの読み込み中...", + "xpack.ingestManager.agentPolicyList.nameColumnTitle": "名前", + "xpack.ingestManager.agentPolicyList.noAgentPoliciesPrompt": "エージェントポリシーがありません", + "xpack.ingestManager.agentPolicyList.noFilteredAgentPoliciesPrompt": "エージェントポリシーが見つかりません。{clearFiltersLink}", + "xpack.ingestManager.agentPolicyList.packagePoliciesCountColumnTitle": "統合", + "xpack.ingestManager.agentPolicyList.pageSubtitle": "エージェントポリシーを使用すると、エージェントとエージェントが収集するデータを管理できます。", + "xpack.ingestManager.agentPolicyList.pageTitle": "エージェントポリシー", + "xpack.ingestManager.agentPolicyList.reloadAgentPoliciesButtonText": "再読み込み", + "xpack.ingestManager.agentPolicyList.revisionNumber": "rev. {revNumber}", + "xpack.ingestManager.agentPolicyList.updatedOnColumnTitle": "最終更新日", + "xpack.ingestManager.agentReassignPolicy.cancelButtonLabel": "キャンセル", + "xpack.ingestManager.agentReassignPolicy.continueButtonLabel": "ポリシーの割り当て", + "xpack.ingestManager.agentReassignPolicy.flyoutDescription": "選択した{count, plural, one {エージェント} other {エージェント}}を割り当てる新しいエージェントポリシーを選択します。", + "xpack.ingestManager.agentReassignPolicy.flyoutTitle": "新しいエージェントポリシーを割り当てる", + "xpack.ingestManager.agentReassignPolicy.policyDescription": "選択したエージェントポリシーは、{count, plural, one {{countValue}個の統合} other {{countValue}個の統合}}のデータを収集します。", + "xpack.ingestManager.agentReassignPolicy.selectPolicyLabel": "エージェントポリシー", + "xpack.ingestManager.agentReassignPolicy.successSingleNotificationTitle": "エージェントポリシーが再割り当てされました", + "xpack.ingestManager.agents.pageSubtitle": "ポリシーの更新を管理し、任意のサイズのエージェントのグループにデプロイします。", + "xpack.ingestManager.agents.pageTitle": "エージェント", "xpack.ingestManager.alphaMessageDescription": "Ingest Managerは本番環境用ではありません。", "xpack.ingestManager.alphaMessageLinkText": "詳細を参照してください。", "xpack.ingestManager.alphaMessageTitle": "ベータリリース", "xpack.ingestManager.alphaMessaging.docsLink": "ドキュメンテーション", - "xpack.ingestManager.alphaMessaging.feedbackText": "{docsLink}をお読みになるか、{forumLink}で質問や回答をすることをお勧めします。", + "xpack.ingestManager.alphaMessaging.feedbackText": "{docsLink}をご覧ください。質問やフィードバックについては、{forumLink}にアクセスしてください。", "xpack.ingestManager.alphaMessaging.flyoutTitle": "このリリースについて", "xpack.ingestManager.alphaMessaging.forumLink": "ディスカッションフォーラム", "xpack.ingestManager.alphaMessaging.introText": "Ingest Managerは開発中であり、本番環境用ではありません。このベータリリースは、ユーザーがIngest Managerと新しいElasticエージェントをテストしてフィードバックを提供することを目的としています。このプラグインには、サポートSLAが適用されません。", "xpack.ingestManager.alphaMessging.closeFlyoutLabel": "閉じる", - "xpack.ingestManager.appNavigation.dataStreamsLinkText": "データセット", + "xpack.ingestManager.appNavigation.agentsLinkText": "エージェント", + "xpack.ingestManager.appNavigation.dataStreamsLinkText": "データストリーム", "xpack.ingestManager.appNavigation.epmLinkText": "統合", "xpack.ingestManager.appNavigation.overviewLinkText": "概要", + "xpack.ingestManager.appNavigation.policiesLinkText": "ポリシー", "xpack.ingestManager.appNavigation.sendFeedbackButton": "フィードバックを送信", "xpack.ingestManager.appNavigation.settingsButton": "設定", - "xpack.ingestManager.appTitle": "Ingest Manager", + "xpack.ingestManager.appTitle": "Fleet", "xpack.ingestManager.betaBadge.labelText": "ベータ", "xpack.ingestManager.betaBadge.tooltipText": "このプラグインは本番環境用ではありません。バグについてはディスカッションフォーラムで報告してください。", + "xpack.ingestManager.breadcrumbs.addPackagePolicyPageTitle": "統合の追加", + "xpack.ingestManager.breadcrumbs.agentsPageTitle": "エージェント", "xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle": "すべて", - "xpack.ingestManager.breadcrumbs.appTitle": "Ingest Manager", - "xpack.ingestManager.breadcrumbs.datastreamsPageTitle": "データセット", + "xpack.ingestManager.breadcrumbs.appTitle": "Fleet", + "xpack.ingestManager.breadcrumbs.datastreamsPageTitle": "データストリーム", + "xpack.ingestManager.breadcrumbs.editPackagePolicyPageTitle": "統合の編集", + "xpack.ingestManager.breadcrumbs.enrollmentTokensPageTitle": "登録トークン", "xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle": "インストール済み", "xpack.ingestManager.breadcrumbs.integrationsPageTitle": "統合", "xpack.ingestManager.breadcrumbs.overviewPageTitle": "概要", + "xpack.ingestManager.breadcrumbs.policiesPageTitle": "ポリシー", + "xpack.ingestManager.copyAgentPolicy.confirmModal.cancelButtonLabel": "キャンセル", + "xpack.ingestManager.copyAgentPolicy.confirmModal.confirmButtonLabel": "ポリシーをコピー", + "xpack.ingestManager.copyAgentPolicy.confirmModal.copyPolicyPrompt": "新しいエージェントポリシーの名前と説明を選択してください。", + "xpack.ingestManager.copyAgentPolicy.confirmModal.copyPolicyTitle": "「{name}」エージェントポリシーをコピー", + "xpack.ingestManager.copyAgentPolicy.confirmModal.defaultNewPolicyName": "{name}(コピー)", + "xpack.ingestManager.copyAgentPolicy.confirmModal.newDescriptionLabel": "説明", + "xpack.ingestManager.copyAgentPolicy.confirmModal.newNameLabel": "新しいポリシー名", + "xpack.ingestManager.copyAgentPolicy.failureNotificationTitle": "エージェントポリシー「{id}」のコピーエラー", + "xpack.ingestManager.copyAgentPolicy.fatalErrorNotificationTitle": "エージェントポリシーのコピーエラー", + "xpack.ingestManager.copyAgentPolicy.successNotificationTitle": "エージェントポリシーがコピーされました", + "xpack.ingestManager.createAgentPolicy.cancelButtonLabel": "キャンセル", + "xpack.ingestManager.createAgentPolicy.errorNotificationTitle": "エージェントポリシーを作成できません", + "xpack.ingestManager.createAgentPolicy.flyoutTitle": "エージェントポリシーを作成", + "xpack.ingestManager.createAgentPolicy.flyoutTitleDescription": "エージェントポリシーは、エージェントのグループ全体にわたる設定を管理する目的で使用されます。エージェントポリシーに統合を追加すると、エージェントで収集するデータを指定できます。エージェントポリシーの編集時には、フリートを使用して、指定したエージェントのグループに更新をデプロイできます。", + "xpack.ingestManager.createAgentPolicy.submitButtonLabel": "エージェントポリシーを作成", + "xpack.ingestManager.createAgentPolicy.successNotificationTitle": "エージェントポリシー「{name}」が作成されました", + "xpack.ingestManager.createPackagePolicy.addedNotificationMessage": "Fleetは'{agentPolicyName}'ポリシーで使用されているすべてのエージェントに更新をデプロイします。", + "xpack.ingestManager.createPackagePolicy.addedNotificationTitle": "「{packagePolicyName}」統合が追加されました。", + "xpack.ingestManager.createPackagePolicy.agentPolicyNameLabel": "エージェントポリシー", + "xpack.ingestManager.createPackagePolicy.cancelButton": "キャンセル", + "xpack.ingestManager.createPackagePolicy.cancelLinkText": "キャンセル", + "xpack.ingestManager.createPackagePolicy.errorOnSaveText": "統合ポリシーにはエラーがあります。保存前に修正してください。", + "xpack.ingestManager.createPackagePolicy.pageDescriptionfromPackage": "次の手順に従い、この統合をエージェントポリシーに追加します。", + "xpack.ingestManager.createPackagePolicy.pageDescriptionfromPolicy": "選択したエージェントポリシーの統合を構成します。", + "xpack.ingestManager.createPackagePolicy.pageTitle": "統合の追加", + "xpack.ingestManager.createPackagePolicy.pageTitleWithPackageName": "{packageName}統合の追加", + "xpack.ingestManager.createPackagePolicy.saveButton": "統合の保存", + "xpack.ingestManager.createPackagePolicy.stepConfigure.advancedOptionsToggleLinkText": "高度なオプション", + "xpack.ingestManager.createPackagePolicy.stepConfigure.errorCountText": "{count, plural, one {件のエラー} other {件のエラー}}", + "xpack.ingestManager.createPackagePolicy.stepConfigure.hideStreamsAriaLabel": "{type}入力を非表示", + "xpack.ingestManager.createPackagePolicy.stepConfigure.inputSettingsDescription": "次の設定は以下のすべての入力に適用されます。", + "xpack.ingestManager.createPackagePolicy.stepConfigure.inputSettingsTitle": "設定", + "xpack.ingestManager.createPackagePolicy.stepConfigure.inputVarFieldOptionalLabel": "オプション", + "xpack.ingestManager.createPackagePolicy.stepConfigure.integrationSettingsSectionDescription": "この統合の使用方法を識別できるように、名前と説明を選択してください。", + "xpack.ingestManager.createPackagePolicy.stepConfigure.integrationSettingsSectionTitle": "統合設定", + "xpack.ingestManager.createPackagePolicy.stepConfigure.noPolicyOptionsMessage": "構成するものがありません", + "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyDescriptionInputLabel": "説明", + "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyNameInputLabel": "統合名", + "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyNamespaceInputLabel": "名前空間", + "xpack.ingestManager.createPackagePolicy.stepConfigure.showStreamsAriaLabel": "{type}入力を表示", + "xpack.ingestManager.createPackagePolicy.stepConfigure.toggleAdvancedOptionsButtonText": "高度なオプション", + "xpack.ingestManager.createPackagePolicy.stepConfigurePackagePolicyTitle": "統合の構成", + "xpack.ingestManager.createPackagePolicy.stepSelectAgentPolicyTitle": "エージェントポリシーを選択", + "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingPackagesTitle": "統合の読み込みエラー", + "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingPolicyTitle": "エージェントポリシー情報の読み込みエラー", + "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択した統合の読み込みエラー", + "xpack.ingestManager.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder": "統合を検索", + "xpack.ingestManager.createPackagePolicy.stepSelectPackageTitle": "統合を選択", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.addButton": "エージェントポリシーを作成", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsCountText": "{count, plural, one {#個のエージェント} other {#個のエージェント}}が登録されました", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsDescriptionText": "選択したエージェントポリシーで{count, plural, one {#個のエージェント} other {#個のエージェント}}が登録されました。", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.agentPolicyLabel": "エージェントポリシー", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.agentPolicyPlaceholderText": "この統合を追加するエージェントポリシーを選択", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingAgentPoliciesTitle": "エージェントポリシーの読み込みエラー", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingPackageTitle": "パッケージ情報の読み込みエラー", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingSelectedAgentPolicyTitle": "選択したエージェントポリシーの読み込みエラー", "xpack.ingestManager.dataStreamList.actionsColumnTitle": "アクション", "xpack.ingestManager.dataStreamList.datasetColumnTitle": "データセット", "xpack.ingestManager.dataStreamList.integrationColumnTitle": "統合", "xpack.ingestManager.dataStreamList.lastActivityColumnTitle": "前回のアクティビティ", - "xpack.ingestManager.dataStreamList.loadingDataStreamsMessage": "データセットを読み込んでいます...", + "xpack.ingestManager.dataStreamList.loadingDataStreamsMessage": "データストリームを読み込んでいます...", "xpack.ingestManager.dataStreamList.namespaceColumnTitle": "名前空間", - "xpack.ingestManager.dataStreamList.noDataStreamsPrompt": "データセットなし", - "xpack.ingestManager.dataStreamList.noFilteredDataStreamsMessage": "一致するデータセットが見つかりません", + "xpack.ingestManager.dataStreamList.noDataStreamsPrompt": "データストリームがありません", + "xpack.ingestManager.dataStreamList.noFilteredDataStreamsMessage": "一致するデータストリームが見つかりません", "xpack.ingestManager.dataStreamList.pageSubtitle": "エージェントが作成したデータを管理します。", - "xpack.ingestManager.dataStreamList.pageTitle": "データセット", + "xpack.ingestManager.dataStreamList.pageTitle": "データストリーム", "xpack.ingestManager.dataStreamList.reloadDataStreamsButtonText": "再読み込み", - "xpack.ingestManager.dataStreamList.searchPlaceholderTitle": "データセットのフィルタリング", + "xpack.ingestManager.dataStreamList.searchPlaceholderTitle": "データストリームをフィルター", "xpack.ingestManager.dataStreamList.sizeColumnTitle": "サイズ", "xpack.ingestManager.dataStreamList.typeColumnTitle": "タイプ", "xpack.ingestManager.dataStreamList.viewDashboardActionText": "ダッシュボードを表示", "xpack.ingestManager.dataStreamList.viewDashboardsActionText": "ダッシュボードを表示", "xpack.ingestManager.dataStreamList.viewDashboardsPanelTitle": "ダッシュボードを表示", "xpack.ingestManager.defaultSearchPlaceholderText": "検索", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {#個のエージェントは} other {#個のエージェントは}}このエージェントポリシーに割り当てられました。このポリシーを削除する前に、これらのエージェントの割り当てを解除します。", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.affectedAgentsTitle": "使用中のポリシー", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.cancelButtonLabel": "キャンセル", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.confirmButtonLabel": "ポリシーを削除", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.deletePolicyTitle": "このエージェントポリシーを削除しますか?", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.irreversibleMessage": "この操作は元に戻すことができません。", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.loadingAgentsCountMessage": "影響があるエージェントの数を確認中...", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.loadingButtonLabel": "読み込み中...", + "xpack.ingestManager.deleteAgentPolicy.failureSingleNotificationTitle": "エージェントポリシー「{id}」の削除エラー", + "xpack.ingestManager.deleteAgentPolicy.fatalErrorNotificationTitle": "エージェントポリシーの削除エラー", + "xpack.ingestManager.deleteAgentPolicy.successSingleNotificationTitle": "エージェントポリシー「{id}」が削除されました", + "xpack.ingestManager.deletePackagePolicy.confirmModal.affectedAgentsMessage": "{agentPolicyName}が一部のエージェントですでに使用されていることをFleetが検出しました。", + "xpack.ingestManager.deletePackagePolicy.confirmModal.affectedAgentsTitle": "このアクションは {agentsCount} {agentsCount, plural, one {# エージェント} other {# エージェント}}に影響します", + "xpack.ingestManager.deletePackagePolicy.confirmModal.cancelButtonLabel": "キャンセル", + "xpack.ingestManager.deletePackagePolicy.confirmModal.deleteMultipleTitle": "{count, plural, one {個の統合} other {個の統合}}を削除しますか?", + "xpack.ingestManager.deletePackagePolicy.confirmModal.generalMessage": "このアクションは元に戻せません。続行していいですか?", + "xpack.ingestManager.deletePackagePolicy.confirmModal.loadingAgentsCountMessage": "影響があるエージェントを確認中...", + "xpack.ingestManager.deletePackagePolicy.confirmModal.loadingButtonLabel": "読み込み中...", + "xpack.ingestManager.deletePackagePolicy.failureMultipleNotificationTitle": "{count}個の統合の削除エラー", + "xpack.ingestManager.deletePackagePolicy.failureSingleNotificationTitle": "統合「{id}」の削除エラー", + "xpack.ingestManager.deletePackagePolicy.fatalErrorNotificationTitle": "統合の削除エラー", + "xpack.ingestManager.deletePackagePolicy.successMultipleNotificationTitle": "{count}個の統合を削除しました", + "xpack.ingestManager.deletePackagePolicy.successSingleNotificationTitle": "統合「{id}」を削除しました", "xpack.ingestManager.disabledSecurityDescription": "Elastic Fleet を使用するには、Kibana と Elasticsearch でセキュリティを有効にする必要があります。", "xpack.ingestManager.disabledSecurityTitle": "セキュリティが有効ではありません", + "xpack.ingestManager.editAgentPolicy.cancelButtonText": "キャンセル", + "xpack.ingestManager.editAgentPolicy.errorNotificationTitle": "エージェントポリシーを更新できません", + "xpack.ingestManager.editAgentPolicy.saveButtonText": "変更を保存", + "xpack.ingestManager.editAgentPolicy.savingButtonText": "保存中…", + "xpack.ingestManager.editAgentPolicy.successNotificationTitle": "正常に「{name}」設定を更新しました", + "xpack.ingestManager.editAgentPolicy.unsavedChangesText": "保存されていない変更があります", + "xpack.ingestManager.editPackagePolicy.cancelButton": "キャンセル", + "xpack.ingestManager.editPackagePolicy.errorLoadingDataMessage": "この統合情報の読み込みエラーが発生しました", + "xpack.ingestManager.editPackagePolicy.errorLoadingDataTitle": "データの読み込み中にエラーが発生", + "xpack.ingestManager.editPackagePolicy.failedConflictNotificationMessage": "データが最新ではありません。最新のポリシーを取得するには、ページを更新してください。", + "xpack.ingestManager.editPackagePolicy.failedNotificationTitle": "「{packagePolicyName}」の更新エラー", + "xpack.ingestManager.editPackagePolicy.pageDescription": "統合設定を修正し、選択したエージェントポリシーに変更をデプロイします。", + "xpack.ingestManager.editPackagePolicy.pageTitle": "統合の編集", + "xpack.ingestManager.editPackagePolicy.pageTitleWithPackageName": "{packageName}統合の編集", + "xpack.ingestManager.editPackagePolicy.saveButton": "統合の保存", + "xpack.ingestManager.editPackagePolicy.updatedNotificationMessage": "Fleetは'{agentPolicyName}'ポリシーで使用されているすべてのエージェントに更新をデプロイします", + "xpack.ingestManager.editPackagePolicy.updatedNotificationTitle": "正常に「{packagePolicyName}」を更新しました", "xpack.ingestManager.enrollemntAPIKeyList.emptyMessage": "登録トークンが見つかりません。", "xpack.ingestManager.enrollemntAPIKeyList.loadingTokensMessage": "登録トークンを読み込んでいます...", - "xpack.ingestManager.enrollmentInstructions.descriptionText": "エージェントのディレクトリから、該当するコマンドを実行し、Elasticエージェントを登録して起動します。再度これらのコマンドを実行すれば、複数のコンピューターでエージェントを設定できます。登録ステップは必ずシステムで管理者権限をもつユーザーとして実行してください。", + "xpack.ingestManager.enrollmentInstructions.descriptionText": "エージェントのディレクトリから、該当するコマンドを実行し、Elasticエージェントをインストール、登録、起動します。これらのコマンドを再利用すると、複数のホストでエージェントを設定できます。管理者権限が必要です。", + "xpack.ingestManager.enrollmentInstructions.linuxMacOSTitle": "Linux、MacOS", + "xpack.ingestManager.enrollmentInstructions.moreInstructionsLink": "Elasticエージェントドキュメント", + "xpack.ingestManager.enrollmentInstructions.moreInstructionsText": "手順とオプションの詳細については、{link}を参照してください。", "xpack.ingestManager.enrollmentInstructions.windowsTitle": "Windows", + "xpack.ingestManager.enrollmentStepAgentPolicy.enrollmentTokenSelectLabel": "登録トークン", + "xpack.ingestManager.enrollmentStepAgentPolicy.policySelectAriaLabel": "エージェントポリシー", + "xpack.ingestManager.enrollmentStepAgentPolicy.policySelectLabel": "エージェントポリシー", + "xpack.ingestManager.enrollmentStepAgentPolicy.showAuthenticationSettingsButton": "認証設定", "xpack.ingestManager.enrollmentTokenDeleteModal.cancelButton": "キャンセル", - "xpack.ingestManager.enrollmentTokenDeleteModal.deleteButton": "削除", - "xpack.ingestManager.enrollmentTokenDeleteModal.description": "{keyName}を削除してよろしいですか?", - "xpack.ingestManager.enrollmentTokenDeleteModal.title": "登録トークンを削除", + "xpack.ingestManager.enrollmentTokenDeleteModal.deleteButton": "登録トークンを取り消し", + "xpack.ingestManager.enrollmentTokenDeleteModal.description": "{keyName}を取り消してよろしいですか?このトークンを使用するエージェントは、ポリシーにアクセスしたり、データを送信したりできなくなります。 ", + "xpack.ingestManager.enrollmentTokenDeleteModal.title": "登録トークンを取り消し", "xpack.ingestManager.enrollmentTokensList.actionsTitle": "アクション", "xpack.ingestManager.enrollmentTokensList.activeTitle": "アクティブ", "xpack.ingestManager.enrollmentTokensList.createdAtTitle": "作成日時", "xpack.ingestManager.enrollmentTokensList.hideTokenButtonLabel": "トークンを非表示", "xpack.ingestManager.enrollmentTokensList.nameTitle": "名前", - "xpack.ingestManager.enrollmentTokensList.newKeyButton": "新しい登録トークン", - "xpack.ingestManager.enrollmentTokensList.pageDescription": "これは、エージェントを登録するために使用できる登録トークンのリストです。", + "xpack.ingestManager.enrollmentTokensList.newKeyButton": "登録トークンを作成", + "xpack.ingestManager.enrollmentTokensList.pageDescription": "登録トークンを作成して取り消します。登録トークンを使用すると、1つ以上のエージェントをFleetに登録し、データを送信できます。", + "xpack.ingestManager.enrollmentTokensList.policyTitle": "エージェントポリシー", "xpack.ingestManager.enrollmentTokensList.revokeTokenButtonLabel": "トークンを取り消す", "xpack.ingestManager.enrollmentTokensList.secretTitle": "シークレット", "xpack.ingestManager.enrollmentTokensList.showTokenButtonLabel": "トークンを表示", + "xpack.ingestManager.epm.addPackagePolicyButtonText": "{packageName}の追加", "xpack.ingestManager.epm.assetGroupTitle": "{assetType}アセット", "xpack.ingestManager.epm.browseAllButtonText": "すべての統合を参照", "xpack.ingestManager.epm.illustrationAltText": "統合の例", "xpack.ingestManager.epm.loadingIntegrationErrorTitle": "統合詳細の読み込みエラー", "xpack.ingestManager.epm.packageDetailsNav.overviewLinkText": "概要", + "xpack.ingestManager.epm.packageDetailsNav.packagePoliciesLinkText": "使用", "xpack.ingestManager.epm.packageDetailsNav.settingsLinkText": "設定", "xpack.ingestManager.epm.pageSubtitle": "一般的なアプリやサービスの統合を参照する", "xpack.ingestManager.epm.pageTitle": "統合", @@ -9098,13 +10000,15 @@ "xpack.ingestManager.epmList.noPackagesFoundPlaceholder": "パッケージが見つかりません", "xpack.ingestManager.epmList.searchPackagesPlaceholder": "統合を検索", "xpack.ingestManager.epmList.updatesAvailableFilterLinkText": "更新が可能です", + "xpack.ingestManager.featureCatalogueDescription": "Elasticエージェントと統合のFleetを追加して管理します。", + "xpack.ingestManager.featureCatalogueTitle": "Elasticエージェントの追加", "xpack.ingestManager.genericActionsMenuText": "開く", "xpack.ingestManager.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "メッセージを消去", - "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeText": "Elasticエージェントでは、シンプルかつ統合された方法で、ログ、メトリック、他の種類のデータの監視をホストに追加することができます。複数のBeatsと他のエージェントをインストールする必要はありません。このため、インフラストラクチャ全体での構成のデプロイが簡単で高速になりました。詳細については、{blogPostLink}をお読みください。", + "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeText": "Elasticエージェントでは、シンプルかつ統合された方法で、ログ、メトリック、他の種類のデータの監視をホストに追加することができます。複数のBeatsと他のエージェントをインストールする必要はありません。このため、インフラストラクチャ全体でのポリシーのデプロイが簡単で高速になりました。詳細については、{blogPostLink}をお読みください。", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "発表ブログ投稿", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix} ElasticエージェントおよびIngest Managerベータ", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeTitle.newPrefix": "新規:", - "xpack.ingestManager.homeIntegration.tutorialModule.noticeText": "{notePrefix} このモジュールの新しいバージョンは、Ingest Managerベータの{availableAsIntegrationLink}です。エージェント構成と新しいElasticエージェントの詳細については、{blogPostLink}をお読みください。", + "xpack.ingestManager.homeIntegration.tutorialModule.noticeText": "{notePrefix}このモジュールの新しいバージョンは、Ingest Managerベータの{availableAsIntegrationLink}です。エージェントポリシーと新しいElasticエージェントの詳細については、{blogPostLink}をお読みください。", "xpack.ingestManager.homeIntegration.tutorialModule.noticeText.blogPostLink": "発表ブログ投稿", "xpack.ingestManager.homeIntegration.tutorialModule.noticeText.integrationLink": "統合として利用可能", "xpack.ingestManager.homeIntegration.tutorialModule.noticeText.notePrefix": "注:", @@ -9126,7 +10030,7 @@ "xpack.ingestManager.integrations.settings.confirmInstallModal.installTitle": "{packageName}をインストール", "xpack.ingestManager.integrations.settings.confirmUninstallModal.cancelButtonLabel": "キャンセル", "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallButtonLabel": "{packageName}をアンインストール", - "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallCallout.description": "この統合によって作成されたKibanaおよびElasticsearchアセットは削除されます。エージェント構成とエージェントによって送信されたデータは影響を受けません。", + "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallCallout.description": "この統合によって作成されたKibanaおよびElasticsearchアセットは削除されます。エージェントポリシーとエージェントによって送信されたデータは影響を受けません。", "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallCallout.title": "{numOfAssets}個のアセットが削除されます", "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallDescription": "この操作は元に戻すことができません。続行していいですか?", "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallTitle": "{packageName}をアンインストール", @@ -9134,7 +10038,7 @@ "xpack.ingestManager.integrations.settings.packageInstallTitle": "{title}をインストール", "xpack.ingestManager.integrations.settings.packageSettingsTitle": "設定", "xpack.ingestManager.integrations.settings.packageUninstallDescription": "この統合によってインストールされたKibanaおよびElasticsearchアセットを削除します。", - "xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail": "{strongNote} {title}をアンインストールできません。この統合を使用しているアクティブなエージェントがあります。アンインストールするには、エージェント構成からすべての{title}統合を削除します。", + "xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail": "{strongNote} {title}をアンインストールできません。この統合を使用しているアクティブなエージェントがあります。アンインストールするには、エージェントポリシーからすべての{title}統合を削除します。", "xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteLabel": "注:", "xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallUninstallableNoteDetail": "{strongNote} {title}統合は既定でインストールされているため、削除できません。", "xpack.ingestManager.integrations.settings.packageUninstallTitle": "{title}をアンインストール", @@ -9153,10 +10057,15 @@ "xpack.ingestManager.metadataForm.keyLabel": "キー", "xpack.ingestManager.metadataForm.submitButtonText": "追加", "xpack.ingestManager.metadataForm.valueLabel": "値", + "xpack.ingestManager.namespaceValidation.invalidCharactersErrorMessage": "名前空間に無効な文字が含まれています", + "xpack.ingestManager.namespaceValidation.lowercaseErrorMessage": "名前空間は小文字で指定する必要があります", + "xpack.ingestManager.namespaceValidation.requiredErrorMessage": "名前空間は必須です", + "xpack.ingestManager.namespaceValidation.tooLongErrorMessage": "名前空間は100バイト以下でなければなりません", "xpack.ingestManager.newEnrollmentKey.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.newEnrollmentKey.flyoutTitle": "新しい登録トークンを作成", + "xpack.ingestManager.newEnrollmentKey.flyoutTitle": "登録トークンを作成", "xpack.ingestManager.newEnrollmentKey.keyCreatedToasts": "登録トークンが作成されました。", "xpack.ingestManager.newEnrollmentKey.nameLabel": "名前", + "xpack.ingestManager.newEnrollmentKey.policyLabel": "ポリシー", "xpack.ingestManager.newEnrollmentKey.submitButton": "登録トークンを作成", "xpack.ingestManager.noAccess.accessDeniedDescription": "Elastic Fleet にアクセスする権限がありません。Elastic Fleet を使用するには、このアプリケーションの読み取り権または全権を含むユーザーロールが必要です。", "xpack.ingestManager.noAccess.accessDeniedTitle": "アクセスが拒否されました", @@ -9166,39 +10075,85 @@ "xpack.ingestManager.overviewAgentTotalTitle": "合計エージェント数", "xpack.ingestManager.overviewDatastreamNamespacesTitle": "名前空間", "xpack.ingestManager.overviewDatastreamSizeTitle": "合計サイズ", - "xpack.ingestManager.overviewDatastreamTotalTitle": "データセット", + "xpack.ingestManager.overviewDatastreamTotalTitle": "データストリーム", "xpack.ingestManager.overviewIntegrationsInstalledTitle": "インストール済み", "xpack.ingestManager.overviewIntegrationsTotalTitle": "合計利用可能数", "xpack.ingestManager.overviewIntegrationsUpdatesAvailableTitle": "更新が可能です", - "xpack.ingestManager.overviewPageDataStreamsPanelAction": "データセットを表示", - "xpack.ingestManager.overviewPageDataStreamsPanelTitle": "データセット", - "xpack.ingestManager.overviewPageDataStreamsPanelTooltip": "エージェントが収集するデータはさまざまなデータセットに整理されます。", + "xpack.ingestManager.overviewPackagePolicyTitle": "使用済みの統合", + "xpack.ingestManager.overviewPageAgentsPanelTitle": "エージェント", + "xpack.ingestManager.overviewPageDataStreamsPanelAction": "データストリームを表示", + "xpack.ingestManager.overviewPageDataStreamsPanelTitle": "データストリーム", + "xpack.ingestManager.overviewPageDataStreamsPanelTooltip": "エージェントが収集するデータはさまざまなデータストリームに整理されます。", "xpack.ingestManager.overviewPageEnrollAgentButton": "エージェントの追加", "xpack.ingestManager.overviewPageFleetPanelAction": "エージェントを表示", - "xpack.ingestManager.overviewPageFleetPanelTooltip": "Fleetを使用して、中央の場所からエージェントを登録し、構成を管理します。", + "xpack.ingestManager.overviewPageFleetPanelTooltip": "Fleetを使用して、中央の場所からエージェントを登録し、ポリシーを管理します。", "xpack.ingestManager.overviewPageIntegrationsPanelAction": "統合を表示", "xpack.ingestManager.overviewPageIntegrationsPanelTitle": "統合", - "xpack.ingestManager.overviewPageIntegrationsPanelTooltip": "Elastic Stackの統合を参照し、インストールします。統合をエージェント構成に追加し、データの送信を開始します。", - "xpack.ingestManager.overviewPageSubtitle": "Elasticエージェントおよびエージェント構成の集中管理。", - "xpack.ingestManager.overviewPageTitle": "Ingest Manager", + "xpack.ingestManager.overviewPageIntegrationsPanelTooltip": "Elastic Stackの統合を参照し、インストールします。統合をエージェントポリシーに追加し、データの送信を開始します。", + "xpack.ingestManager.overviewPagePoliciesPanelAction": "ポリシーを表示", + "xpack.ingestManager.overviewPagePoliciesPanelTitle": "エージェントポリシー", + "xpack.ingestManager.overviewPagePoliciesPanelTooltip": "エージェントポリシーを使用すると、エージェントが収集するデータを管理できます。", + "xpack.ingestManager.overviewPageSubtitle": "Elasticエージェントとポリシーを中央の場所で管理します。", + "xpack.ingestManager.overviewPageTitle": "Fleet", + "xpack.ingestManager.overviewPolicyTotalTitle": "合計利用可能数", + "xpack.ingestManager.packagePolicyValidation.invalidArrayErrorMessage": "無効なフォーマット", + "xpack.ingestManager.packagePolicyValidation.invalidYamlFormatErrorMessage": "YAML形式が無効です", + "xpack.ingestManager.packagePolicyValidation.nameRequiredErrorMessage": "名前が必要です", + "xpack.ingestManager.packagePolicyValidation.requiredErrorMessage": "{fieldName}が必要です", "xpack.ingestManager.permissionDeniedErrorMessage": "Ingest Managerにアクセスする権限がありません。Ingest Managerには{roleName}権限が必要です。", "xpack.ingestManager.permissionDeniedErrorTitle": "パーミッションが拒否されました", "xpack.ingestManager.permissionsRequestErrorMessageDescription": "Ingest Managerアクセス権の確認中に問題が発生しました", "xpack.ingestManager.permissionsRequestErrorMessageTitle": "アクセス権を確認できません", + "xpack.ingestManager.policyDetails.addPackagePolicyButtonText": "統合の追加", + "xpack.ingestManager.policyDetails.ErrorGettingFullAgentPolicy": "エージェントポリシーの読み込みエラー", + "xpack.ingestManager.policyDetails.packagePoliciesTable.actionsColumnTitle": "アクション", + "xpack.ingestManager.policyDetails.packagePoliciesTable.deleteActionTitle": "統合の削除", + "xpack.ingestManager.policyDetails.packagePoliciesTable.descriptionColumnTitle": "説明", + "xpack.ingestManager.policyDetails.packagePoliciesTable.editActionTitle": "統合の編集", + "xpack.ingestManager.policyDetails.packagePoliciesTable.nameColumnTitle": "名前", + "xpack.ingestManager.policyDetails.packagePoliciesTable.namespaceColumnTitle": "名前空間", + "xpack.ingestManager.policyDetails.packagePoliciesTable.packageNameColumnTitle": "統合", + "xpack.ingestManager.policyDetails.policyDetailsTitle": "ポリシー「{id}」", + "xpack.ingestManager.policyDetails.policyNotFoundErrorTitle": "ポリシー「{id}」が見つかりません", + "xpack.ingestManager.policyDetails.subTabs.packagePoliciesTabText": "統合", + "xpack.ingestManager.policyDetails.subTabs.settingsTabText": "設定", + "xpack.ingestManager.policyDetails.summary.integrations": "統合", + "xpack.ingestManager.policyDetails.summary.lastUpdated": "最終更新日", + "xpack.ingestManager.policyDetails.summary.revision": "リビジョン", + "xpack.ingestManager.policyDetails.summary.usedBy": "使用者", + "xpack.ingestManager.policyDetails.unexceptedErrorTitle": "エージェントポリシーの読み込み中にエラーが発生しました", + "xpack.ingestManager.policyDetails.viewAgentListTitle": "すべてのエージェントポリシーを表示", + "xpack.ingestManager.policyDetails.yamlDownloadButtonLabel": "ダウンロードポリシー", + "xpack.ingestManager.policyDetails.yamlFlyoutCloseButtonLabel": "閉じる", + "xpack.ingestManager.policyDetails.yamlflyoutTitleWithName": "「{name}」エージェントポリシー", + "xpack.ingestManager.policyDetails.yamlflyoutTitleWithoutName": "エージェントポリシー", + "xpack.ingestManager.policyDetailsPackagePolicies.createFirstButtonText": "統合の追加", + "xpack.ingestManager.policyDetailsPackagePolicies.createFirstMessage": "このポリシーにはまだ統合がありません。", + "xpack.ingestManager.policyDetailsPackagePolicies.createFirstTitle": "最初の統合を追加", + "xpack.ingestManager.policyForm.deletePolicyActionText": "ポリシーを削除", + "xpack.ingestManager.policyForm.deletePolicyGroupDescription": "既存のデータは削除されません。", + "xpack.ingestManager.policyForm.deletePolicyGroupTitle": "ポリシーを削除", + "xpack.ingestManager.policyForm.generalSettingsGroupDescription": "エージェントポリシーの名前と説明を選択してください。", + "xpack.ingestManager.policyForm.generalSettingsGroupTitle": "一般設定", + "xpack.ingestManager.policyForm.unableToDeleteDefaultPolicyText": "デフォルトポリシーは削除できません", "xpack.ingestManager.securityRequiredErrorMessage": "Ingest Managerを使用するには、KibanaとElasticsearchでセキュリティを有効にする必要があります。", "xpack.ingestManager.securityRequiredErrorTitle": "セキュリティが有効ではありません", - "xpack.ingestManager.settings.autoUpgradeDisabledLabel": "エージェントバイナリバージョンを手動で管理します。ご利用にはゴールドサブスクリプションが必要です。", + "xpack.ingestManager.settings.additionalYamlConfig": "Elasticsearch出力構成", + "xpack.ingestManager.settings.autoUpgradeDisabledLabel": "エージェントバイナリバージョンを手動で管理します。サブスクリプションが必要です。", "xpack.ingestManager.settings.autoUpgradeEnabledLabel": "エージェントバイナリを自動的に更新し、最新マイナーバージョンを使用します。", "xpack.ingestManager.settings.autoUpgradeFieldLabel": "Elasticエージェントバイナリバージョン", "xpack.ingestManager.settings.cancelButtonLabel": "キャンセル", "xpack.ingestManager.settings.elasticHostError": "無効なURL", "xpack.ingestManager.settings.elasticsearchUrlLabel": "Elasticsearch URL", "xpack.ingestManager.settings.flyoutTitle": "Ingest Manager設定", - "xpack.ingestManager.settings.globalOutputDescription": "グローバル出力はすべてのエージェント構成に適用され、データの送信先を指定します。", + "xpack.ingestManager.settings.globalOutputDescription": "データを送信する場所を指定します。これらの設定はすべてのElasticエージェントポリシーに適用されます。", "xpack.ingestManager.settings.globalOutputTitle": "グローバル出力", "xpack.ingestManager.settings.integrationUpgradeDisabledFieldLabel": "統合バージョンを手動で管理します。", - "xpack.ingestManager.settings.integrationUpgradeEnabledFieldLabel": "自動的に統合を最新バージョンに更新し、最新のアセットを受信します。新しい機能を使用するには、エージェント構成を更新しなければならない場合があります。", + "xpack.ingestManager.settings.integrationUpgradeEnabledFieldLabel": "自動的に統合を最新バージョンに更新し、最新のアセットを取得します。新機能を使用するには、エージェントポリシーを更新しなければならない場合があります。", "xpack.ingestManager.settings.integrationUpgradeFieldLabel": "統合バージョン", + "xpack.ingestManager.settings.invalidYamlFormatErrorMessage": "無効なYAML形式:{reason}", + "xpack.ingestManager.settings.kibanaUrlDifferentPathOrProtocolError": "各URLのプロトコルとパスは同じでなければなりません", + "xpack.ingestManager.settings.kibanaUrlEmptyError": "1つ以上のURLが必要です。", "xpack.ingestManager.settings.kibanaUrlError": "無効なURL", "xpack.ingestManager.settings.kibanaUrlLabel": "Kibana URL", "xpack.ingestManager.settings.saveButtonLabel": "設定を保存", @@ -9207,20 +10162,46 @@ "xpack.ingestManager.setupPage.elasticsearchApiKeyFlagText": "{apiKeyLink}.{apiKeyFlag}を{true}に設定します。", "xpack.ingestManager.setupPage.elasticsearchSecurityFlagText": "{esSecurityLink}.{securityFlag}を{true}に設定します。", "xpack.ingestManager.setupPage.elasticsearchSecurityLink": "Elasticsearchセキュリティ", - "xpack.ingestManager.setupPage.enableText": "フリートを使用するには、Elasticユーザーを作成する必要があります。このユーザーは、APIキーを作成して、logs-*およびmetrics-*に書き込むことができます。", - "xpack.ingestManager.setupPage.enableTitle": "フリートを有効にする", + "xpack.ingestManager.setupPage.enableCentralManagement": "ユーザーを作成し、集中管理を有効にする", + "xpack.ingestManager.setupPage.enableText": "集中管理には、APIキーを作成し、log-*とmetrics-*に書き込むことができるElasticユーザーが必要です。", + "xpack.ingestManager.setupPage.enableTitle": "ElasticElasticエージェントの集中管理を有効にする", "xpack.ingestManager.setupPage.encryptionKeyFlagText": "{encryptionKeyLink}.{keyFlag}を32文字以上の英数字に設定します。", "xpack.ingestManager.setupPage.gettingStartedLink": "はじめに", "xpack.ingestManager.setupPage.gettingStartedText": "詳細については、{link}ガイドをお読みください。", "xpack.ingestManager.setupPage.kibanaEncryptionLink": "Kibana暗号化鍵", "xpack.ingestManager.setupPage.kibanaSecurityLink": "Kibanaセキュリティ", - "xpack.ingestManager.setupPage.missingRequirementsCalloutDescription": "Fleetを使用するには、次のElasticsearchとKibanaセキュリティ機能を有効にする必要があります。", + "xpack.ingestManager.setupPage.missingRequirementsCalloutDescription": "Elasticエージェントの集中管理を使用するには、次のElasticsearchとKibanaセキュリティ機能を有効にする必要があります。", "xpack.ingestManager.setupPage.missingRequirementsCalloutTitle": "不足しているセキュリティ要件", - "xpack.ingestManager.setupPage.missingRequirementsElasticsearchTitle": "Elasticsearch構成で、次の項目を有効にします。", - "xpack.ingestManager.setupPage.missingRequirementsKibanaTitle": "Kibana構成で、次の項目を有効にします。", + "xpack.ingestManager.setupPage.missingRequirementsElasticsearchTitle": "Elasticsearchポリシーでは、次のことができます。", + "xpack.ingestManager.setupPage.missingRequirementsKibanaTitle": "Kibanaポリシーでは、次のことができます。", "xpack.ingestManager.setupPage.tlsFlagText": "{kibanaSecurityLink}.{securityFlag}を{true}に設定します。開発目的では、危険な代替として{tlsFlag}を{true}に設定して、{tlsLink}を無効化できます。", "xpack.ingestManager.setupPage.tlsLink": "TLS", "xpack.ingestManager.unenrollAgents.cancelButtonLabel": "キャンセル", + "xpack.ingestManager.unenrollAgents.confirmMultipleButtonLabel": "{count}個のエージェントを登録解除", + "xpack.ingestManager.unenrollAgents.confirmSingleButtonLabel": "エージェントの登録解除", + "xpack.ingestManager.unenrollAgents.deleteMultipleDescription": "このアクションにより、複数のエージェントがFleetから削除され、新しいデータを取り込めなくなります。これらのエージェントによってすでに送信されたデータは一切影響を受けません。この操作は元に戻すことができません。", + "xpack.ingestManager.unenrollAgents.deleteSingleDescription": "このアクションにより、「{hostName}」で実行中の選択したエージェントがFleetから削除されます。エージェントによってすでに送信されたデータは一切削除されません。この操作は元に戻すことができません。", + "xpack.ingestManager.unenrollAgents.deleteSingleTitle": "エージェントの登録解除", + "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "{count, plural, one {エージェント} other {エージェント}}の登録解除エラー", + "xpack.ingestManager.unenrollAgents.forceDeleteMultipleTitle": "{count}個のエージェントを登録解除", + "xpack.ingestManager.unenrollAgents.forceUnenrollCheckboxLabel": "{count, plural, one {エージェント} other {エージェント}}がただちに削除されました。エージェントが最後のデータを送信するまで待機しない。", + "xpack.ingestManager.unenrollAgents.forceUnenrollLegendText": "{count, plural, one {エージェント} other {エージェント}}を強制的に登録解除", + "xpack.ingestManager.unenrollAgents.successForceMultiNotificationTitle": "エージェントが登録解除されました", + "xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle": "エージェントが登録解除されました", + "xpack.ingestManager.unenrollAgents.successMultiNotificationTitle": "エージェントを登録解除しています", + "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "エージェントを登録解除しています", + "xpack.ingestManager.upgradeAgents.cancelButtonLabel": "キャンセル", + "xpack.ingestManager.upgradeAgents.confirmMultipleButtonLabel": "{count}個のエージェントをアップグレード", + "xpack.ingestManager.upgradeAgents.confirmSingleButtonLabel": "エージェントをアップグレード", + "xpack.ingestManager.upgradeAgents.deleteMultipleTitle": "{count}個のエージェントをアップグレード", + "xpack.ingestManager.upgradeAgents.deleteSingleTitle": "エージェントをアップグレード", + "xpack.ingestManager.upgradeAgents.fatalErrorNotificationTitle": "{count, plural, one {エージェント} other {エージェント}}のアップグレードエラー", + "xpack.ingestManager.upgradeAgents.successMultiNotificationTitle": "エージェントをアップグレード中...", + "xpack.ingestManager.upgradeAgents.successSingleNotificationTitle": "エージェントをアップグレード中", + "xpack.ingestManager.upgradeAgents.upgradeMultipleDescription": "このアクションにより、複数のエージェントがバージョン{version}にアップグレードされます。この操作は元に戻すことができません。続行していいですか?", + "xpack.ingestManager.upgradeAgents.upgradeSingleDescription": "このアクションにより、「{hostName}」で実行中の選択したエージェントがバージョン{version}にアップグレードされます。この操作は元に戻すことができません。続行していいですか?", + "xpack.ingestPipelines.addProcesorFormOnFailureFlyout.cancelButtonLabel": "キャンセル", + "xpack.ingestPipelines.addProcessorFormOnFailureFlyout.addButtonLabel": "追加", "xpack.ingestPipelines.app.checkingPrivilegesDescription": "権限を確認中…", "xpack.ingestPipelines.app.checkingPrivilegesErrorMessage": "サーバーからユーザー特権を取得中にエラーが発生。", "xpack.ingestPipelines.app.deniedPrivilegeDescription": "Ingest Pipelinesを使用するには、{privilegesCount, plural, one {このクラスター特権} other {これらのクラスター特権}}が必要です:{missingPrivileges}。", @@ -9301,21 +10282,186 @@ "xpack.ingestPipelines.list.table.emptyPromptTitle": "パイプラインを作成して開始", "xpack.ingestPipelines.list.table.nameColumnTitle": "名前", "xpack.ingestPipelines.list.table.reloadButtonLabel": "再読み込み", + "xpack.ingestPipelines.pipelineEditor.addDocuments.addDocumentButtonLabel": "ドキュメントを追加", + "xpack.ingestPipelines.pipelineEditor.addDocuments.addDocumentErrorMessage": "ドキュメントの追加エラー", + "xpack.ingestPipelines.pipelineEditor.addDocuments.addDocumentSuccessMessage": "ドキュメントが追加されました", + "xpack.ingestPipelines.pipelineEditor.addDocuments.idFieldLabel": "ドキュメントID", + "xpack.ingestPipelines.pipelineEditor.addDocuments.idRequiredErrorMessage": "ドキュメントIDは必須です。", + "xpack.ingestPipelines.pipelineEditor.addDocuments.indexFieldLabel": "インデックス", + "xpack.ingestPipelines.pipelineEditor.addDocuments.indexRequiredErrorMessage": "インデックス名は必須です。", + "xpack.ingestPipelines.pipelineEditor.addDocumentsAccordion.addDocumentsButtonLabel": "インデックスからテストドキュメントを追加", + "xpack.ingestPipelines.pipelineEditor.addDocumentsAccordion.contentDescriptionText": "ドキュメントのインデックスとドキュメントIDを指定してください。", + "xpack.ingestPipelines.pipelineEditor.addDocumentsAccordion.discoverLinkDescriptionText": "既存のデータを検索するには、{discoverLink}を使用してください。", "xpack.ingestPipelines.pipelineEditor.addProcessorButtonLabel": "プロセッサーを追加", + "xpack.ingestPipelines.pipelineEditor.appendForm.fieldHelpText": "値を追加するフィールド。", + "xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldHelpText": "追加する値。", + "xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldLabel": "値", + "xpack.ingestPipelines.pipelineEditor.appendForm.valueRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.bytesForm.fieldNameHelpText": "変換するフィールド。フィールドに配列が含まれている場合、各配列値が変換されます。", + "xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError": "エラー距離値は必須です。", + "xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceFieldLabel": "エラー距離", + "xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceHelpText": "内接する形状の辺と外接円の差。出力された多角形の精度を決定します。{geo_shape}ではメートルで測定されますが、{shape}では単位を使用しません。", + "xpack.ingestPipelines.pipelineEditor.circleForm.fieldNameHelpText": "変換するフィールド。", + "xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeFieldHelpText": "出力された多角形を処理するときに使用するフィールドマッピングタイプ。", + "xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeFieldLabel": "形状タイプ", + "xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeGeoShape": "図形", + "xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeRequiredError": "形状タイプ値は必須です。", + "xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeShape": "形状", + "xpack.ingestPipelines.pipelineEditor.commonFields.fieldFieldLabel": "フィールド", + "xpack.ingestPipelines.pipelineEditor.commonFields.fieldRequiredError": "フィールド値が必要です。", + "xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldHelpText": "このプロセッサーを条件付きで実行します。", "xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldLabel": "条件(任意)", "xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureFieldLabel": "失敗を無視", + "xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureHelpText": "このプロセッサーのエラーを無視します。", + "xpack.ingestPipelines.pipelineEditor.commonFields.ignoreMissingFieldHelpText": "見つからない{field}のドキュメントを無視します。", + "xpack.ingestPipelines.pipelineEditor.commonFields.ignoreMissingFieldLabel": "不足している項目を無視", + "xpack.ingestPipelines.pipelineEditor.commonFields.propertiesFieldLabel": "プロパティ(任意)", + "xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldHelpText": "プロセッサーの識別子。デバッグとメトリックで有用です。", "xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldLabel": "タグ(任意)", + "xpack.ingestPipelines.pipelineEditor.commonFields.targetFieldHelpText": "出力フィールド。空の場合、所定の入力フィールドが更新されます。", + "xpack.ingestPipelines.pipelineEditor.commonFields.targetFieldLabel": "ターゲットフィールド(任意)", + "xpack.ingestPipelines.pipelineEditor.convertForm.autoOption": "自動", + "xpack.ingestPipelines.pipelineEditor.convertForm.booleanOption": "ブール", + "xpack.ingestPipelines.pipelineEditor.convertForm.doubleOption": "倍精度浮動小数点数", + "xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldHelpText": "空のフィールドを入力するために使用されます。値が入力されていない場合、空のフィールドはスキップされます。", + "xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldLabel": "空の値(任意)", + "xpack.ingestPipelines.pipelineEditor.convertForm.fieldNameHelpText": "変換するフィールド。", + "xpack.ingestPipelines.pipelineEditor.convertForm.floatOption": "浮動小数点数", + "xpack.ingestPipelines.pipelineEditor.convertForm.integerOption": "整数", + "xpack.ingestPipelines.pipelineEditor.convertForm.longOption": "ロング", + "xpack.ingestPipelines.pipelineEditor.convertForm.quoteFieldLabel": "引用符(任意)", + "xpack.ingestPipelines.pipelineEditor.convertForm.quoteHelpText": "CSVデータで使用されるEscape文字。デフォルトは{value}です。", + "xpack.ingestPipelines.pipelineEditor.convertForm.separatorFieldLabel": "区切り文字(任意)", + "xpack.ingestPipelines.pipelineEditor.convertForm.separatorHelpText": "CSVデータで使用される区切り文字。デフォルトは{value}です。", + "xpack.ingestPipelines.pipelineEditor.convertForm.separatorLengthError": "1文字でなければなりません。", + "xpack.ingestPipelines.pipelineEditor.convertForm.stringOption": "文字列", + "xpack.ingestPipelines.pipelineEditor.convertForm.typeFieldHelpText": "出力のフィールドデータ型。", + "xpack.ingestPipelines.pipelineEditor.convertForm.typeFieldLabel": "型", + "xpack.ingestPipelines.pipelineEditor.convertForm.typeRequiredError": "型値が必要です。", + "xpack.ingestPipelines.pipelineEditor.csvForm.fieldNameHelpText": "CSVデータを含むフィールド。", + "xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldRequiredError": "ターゲットフィールド値は必須です。", + "xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldsFieldLabel": "ターゲットフィールド", + "xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldsHelpText": "出力フィールド。抽出された値はこれらのフィールドにマッピングされます。", + "xpack.ingestPipelines.pipelineEditor.csvForm.trimFieldHelpText": "引用符で囲まれていないCSVデータデータの空白を削除します。", + "xpack.ingestPipelines.pipelineEditor.csvForm.trimFieldLabel": "トリム", "xpack.ingestPipelines.pipelineEditor.customForm.configurationRequiredError": "構成が必要です。", "xpack.ingestPipelines.pipelineEditor.customForm.invalidJsonError": "入力が無効です。", "xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldAriaLabel": "構成JSONエディター", "xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldLabel": "構成", + "xpack.ingestPipelines.pipelineEditor.dateForm.fieldNameHelpText": "変換するフィールド。", + "xpack.ingestPipelines.pipelineEditor.dateForm.formatsFieldHelpText": "想定されるデータ形式。指定された形式は連続で適用されます。Java時刻パターン、ISO8601、UNIX、UNIX_MS、TAI64Nを使用できます。", + "xpack.ingestPipelines.pipelineEditor.dateForm.formatsFieldLabel": "形式", + "xpack.ingestPipelines.pipelineEditor.dateForm.formatsRequiredError": "形式の値は必須です。", + "xpack.ingestPipelines.pipelineEditor.dateForm.localeFieldLabel": "ロケール(任意)", + "xpack.ingestPipelines.pipelineEditor.dateForm.localeHelpText": "日付のロケール。月名または曜日名を解析するときに有用です。デフォルトは{timezone}です。", + "xpack.ingestPipelines.pipelineEditor.dateForm.targetFieldHelpText": "出力フィールド。空の場合、所定の入力フィールドが更新されます。デフォルトは{defaultField}です。", + "xpack.ingestPipelines.pipelineEditor.dateForm.timezoneFieldLabel": "タイムゾーン(任意)", + "xpack.ingestPipelines.pipelineEditor.dateForm.timezoneHelpText": "日付のタイムゾーン。デフォルトは{timezone}です。", + "xpack.ingestPipelines.pipelineEditor.dateIndexForm.localeHelpText": "日付を解析するときに使用するロケール。月名または曜日名を解析するときに有用です。デフォルトは{locale}です。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateFormatsFieldLabel": "日付形式(任意)", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateFormatsHelpText": "想定されるデータ形式。指定された形式は連続で適用されます。Java時刻パターン、ISO8601、UNIX、UNIX_MS、TAI64Nを使用できます。デフォルトは{value}です。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.day": "日", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.hour": "時間", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.minute": "分", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.month": "月", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.second": "秒", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.week": "週", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.year": "年", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingFieldHelpText": "日付をインデックス名に書式設定するときに日付を端数処理するために使用される期間。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingFieldLabel": "日付の端数処理", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingRequiredError": "フィールド値が必要です。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.fieldNameHelpText": "日付またはタイムスタンプを含むフィールド。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNameFormatFieldHelpText": "解析された日付をインデックス名に出力するために使用される日付形式。デフォルトは{value}です。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNameFormatFieldLabel": "インデックス名形式(任意)", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldHelpText": "インデックス名の出力された日付の前に追加する接頭辞。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldLabel": "インデックス名接頭辞(任意)", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.localeFieldLabel": "ロケール(任意)", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.timezoneFieldLabel": "タイムゾーン(任意)", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.timezoneHelpText": "日付を解析し、インデックス名式を構築するために使用されるタイムゾーン。デフォルトは{timezone}です。", "xpack.ingestPipelines.pipelineEditor.deleteModal.deleteDescription": "このプロセッサーとエラーハンドラーを削除します。", + "xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorHelpText": "キー修飾子を指定する場合、結果を追加するときに、この文字でフィールドが区切られます。デフォルトは{value}です。", + "xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorparaotrFieldLabel": "区切り文字を末尾に追加(任意)", + "xpack.ingestPipelines.pipelineEditor.dissectForm.fieldNameHelpText": "分析するフィールド。", + "xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText": "指定したフィールドを分析するために使用されるパターン。パターンは、破棄する文字列の一部によって定義されます。{keyModifier}を使用して、分析動作を変更します。", + "xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText.dissectProcessorLink": "キー修飾子", + "xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldLabel": "パターン", + "xpack.ingestPipelines.pipelineEditor.dissectForm.patternRequiredError": "パターン値が必要です。", + "xpack.ingestPipelines.pipelineEditor.dotExpanderForm.fieldNameHelpText": "ドット表記を含むフィールド。", + "xpack.ingestPipelines.pipelineEditor.dotExpanderForm.fieldNameRequiresDotError": "フィールド値には、1つ以上のドット文字が必要です。", + "xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathFieldLabel": "パス", + "xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathHelpText": "出力フィールド。展開するフィールドが別のオブジェクトフィールドの一部である場合にのみ必要です。", + "xpack.ingestPipelines.pipelineEditor.dragAndDropList.removeItemLabel": "項目を削除", "xpack.ingestPipelines.pipelineEditor.dropZoneButton.moveHereToolTip": "ここに移動", "xpack.ingestPipelines.pipelineEditor.dropZoneButton.unavailableToolTip": "ここに移動できません", + "xpack.ingestPipelines.pipelineEditor.emptyPrompt.description": "インデックスの前に、プロセッサーを使用してデータを変換します。{learnMoreLink}", + "xpack.ingestPipelines.pipelineEditor.emptyPrompt.title": "最初のプロセッサーを追加", + "xpack.ingestPipelines.pipelineEditor.enrichForm.containsOption": "を含む", + "xpack.ingestPipelines.pipelineEditor.enrichForm.fieldNameHelpText": "受信ドキュメントをエンリッチドキュメントに照合するために使用されるフィールド。フィールド値はエンリッチポリシーで設定された一致フィールドと比較されます。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.intersectsOption": "と交わる", + "xpack.ingestPipelines.pipelineEditor.enrichForm.maxMatchesFieldHelpText": "ターゲットフィールドに含める、一致するエンリッチドキュメントの数。1~128を使用できます。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.maxMatchesFieldLabel": "最大一致数(任意)", + "xpack.ingestPipelines.pipelineEditor.enrichForm.maxMatchesMaxNumberError": "127以下でなければなりません。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.maxMatchesMinNumberError": "1より大きくなければなりません。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.overrideFieldHelpText": "有効にすると、プロセッサーは既存のフィールド値を上書きできます。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.overrideFieldLabel": "無効化", + "xpack.ingestPipelines.pipelineEditor.enrichForm.policyNameFieldLabel": "ポリシー名", + "xpack.ingestPipelines.pipelineEditor.enrichForm.policyNameHelpText": "{enrichPolicyLink}の名前", + "xpack.ingestPipelines.pipelineEditor.enrichForm.policyNameHelpText.enrichPolicyLink": "エンリッチポリシー", + "xpack.ingestPipelines.pipelineEditor.enrichForm.policyNameRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.shapeRelationFieldHelpText": "受信ドキュメントの図形をエンリッチドキュメントに照合するために使用される演算子。{geoMatchPolicyLink}でのみ使用されます。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.shapeRelationFieldHelpText.geoMatchPoliciesLink": "地理空間一致エンリッチポリシー", + "xpack.ingestPipelines.pipelineEditor.enrichForm.shapeRelationFieldLabel": "形状関係(任意)", + "xpack.ingestPipelines.pipelineEditor.enrichForm.targetFieldHelpText": "エンリッチデータを含めるために使用されるフィールド。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.targetFieldLabel": "ターゲットフィールド", + "xpack.ingestPipelines.pipelineEditor.enrichForm.targetFieldRequiredError": "ターゲットフィールド値は必須です。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.withinOption": "内", + "xpack.ingestPipelines.pipelineEditor.enrichFrom.disjointOption": "結合解除", + "xpack.ingestPipelines.pipelineEditor.failForm.fieldNameHelpText": "配列値を含むフィールド。", + "xpack.ingestPipelines.pipelineEditor.failForm.messageFieldLabel": "メッセージ", + "xpack.ingestPipelines.pipelineEditor.failForm.messageHelpText": "プロセッサーで返されるエラーメッセージ。", + "xpack.ingestPipelines.pipelineEditor.failForm.valueRequiredError": "メッセージは必須です。", + "xpack.ingestPipelines.pipelineEditor.foreachForm.optionsFieldAriaLabel": "構成JSONエディター", + "xpack.ingestPipelines.pipelineEditor.foreachForm.processorFieldLabel": "プロセッサー", + "xpack.ingestPipelines.pipelineEditor.foreachForm.processorHelpText": "各配列値で実行されるインジェストプロセッサー。", + "xpack.ingestPipelines.pipelineEditor.foreachForm.processorInvalidJsonError": "無効なJSON", + "xpack.ingestPipelines.pipelineEditor.foreachForm.processorRequiredError": "プロセッサーは必須です。", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.databaseFileHelpText": "{ingestGeoIP}構成ディレクトリのGeoIP2データベースファイル。デフォルトは{databaseFile}です。", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.databaseFileLabel": "データベースファイル(任意)", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.fieldNameHelpText": "地理的ルックアップ用のIPアドレスを含むフィールド。", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.firstOnlyFieldHelpText": "フィールドに配列が含まれる場合でも、最初の一致する地理データを使用します。", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.firstOnlyFieldLabel": "最初のみ", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.propertiesFieldHelpText": "ターゲットフィールドに追加されたプロパティ。有効なプロパティは、使用されるデータベースファイルによって異なります。", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.targetFieldHelpText": "地理データプロパティを含めるために使用されるフィールド。", + "xpack.ingestPipelines.pipelineEditor.grokForm.fieldNameHelpText": "一致を検索するフィールド。", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternDefinitionsAriaLabel": "パターン定義エディター", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternDefinitionsHelpText": "カスタムパターンを定義するパターン名およびパターンタプルのマップ。既存の名前と一致するパターンは、既存の定義よりも優先されます。", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternDefinitionsLabel": "パターン定義(任意)", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternsAddPatternLabel": "パターンを追加", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternsDefinitionsInvalidJSONError": "無効なJSON", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternsFieldLabel": "パターン", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternsHelpText": "名前付きの取り込みグループを照合して抽出するGrok式。最初の一致する式を使用します。", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternsValueRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.grokForm.traceMatchFieldHelpText": "一致する式のメタデータをドキュメントに追加します。", + "xpack.ingestPipelines.pipelineEditor.grokForm.traceMatchFieldLabel": "一致をトレース", + "xpack.ingestPipelines.pipelineEditor.gsubForm.fieldNameHelpText": "一致を検索するフィールド。", + "xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldHelpText": "フィールドのサブ文字列と照合するために使用される正規表現。", "xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldLabel": "パターン", - "xpack.ingestPipelines.pipelineEditor.gsubForm.patternRequiredError": "パターン値が必要です。", + "xpack.ingestPipelines.pipelineEditor.gsubForm.patternRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldHelpText": "一致の置換テキスト。", "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldLabel": "置換", - "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementRequiredError": "置換値が必要です。", + "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.htmlStripForm.fieldNameHelpText": "HTMLタグを削除するフィールド。", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.fieldMapHelpText": "ドキュメントフィールド名をモデルの既知のフィールド名にマッピングします。モデルのどのマッピングよりも優先されます。", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.fieldMapInvalidJSONError": "無効なJSON", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.fieldMapLabel": "フィールドマップ(任意)", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigField.classificationLinkLabel": "分類", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigField.regressionLinkLabel": "回帰", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigLabel": "推論構成 (任意)", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigurationHelpText": "推論タイプとオプションが含まれます。{regression}と{classification}の2種類あります。", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.modelIDFieldHelpText": "推論するモデルのID。", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.modelIDFieldLabel": "モデルID", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.patternRequiredError": "モデルID値は必須です。", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.targetFieldHelpText": "推論プロセッサー結果を含むフィールド。デフォルトは{targetField}です。", "xpack.ingestPipelines.pipelineEditor.item.cancelMoveButtonAriaLabel": "移動のキャンセル", "xpack.ingestPipelines.pipelineEditor.item.descriptionPlaceholder": "説明なし", "xpack.ingestPipelines.pipelineEditor.item.editButtonAriaLabel": "このプロセッサーを編集", @@ -9325,7 +10471,34 @@ "xpack.ingestPipelines.pipelineEditor.item.moreMenu.duplicateButtonLabel": "このプロセッサーを複製", "xpack.ingestPipelines.pipelineEditor.item.moveButtonLabel": "このプロセッサーを移動", "xpack.ingestPipelines.pipelineEditor.item.textInputAriaLabel": "この{type}プロセッサーの説明を入力", - "xpack.ingestPipelines.pipelineEditor.loadFromJson.buttonLabel": "JSONの読み込み", + "xpack.ingestPipelines.pipelineEditor.joinForm.fieldNameHelpText": "結合する配列値を含むフィールド。", + "xpack.ingestPipelines.pipelineEditor.joinForm.separatorFieldHelpText": "区切り文字。", + "xpack.ingestPipelines.pipelineEditor.joinForm.separatorFieldLabel": "区切り文字", + "xpack.ingestPipelines.pipelineEditor.joinForm.separatorRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.jsonForm.addToRootFieldHelpText": "JSONオブジェクトをドキュメントの最上位レベルに追加します。ターゲットフィールドと結合できません。", + "xpack.ingestPipelines.pipelineEditor.jsonForm.addToRootFieldLabel": "ルートに追加", + "xpack.ingestPipelines.pipelineEditor.jsonForm.fieldNameHelpText": "解析するフィールド。", + "xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysFieldLabel": "キーを除外", + "xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysHelpText": "出力から除外する、抽出されたキーのリスト。", + "xpack.ingestPipelines.pipelineEditor.kvForm.fieldNameHelpText": "キーと値のペアの文字列を含むフィールド。", + "xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitFieldLabel": "フィールド分割", + "xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitHelpText": "キーと値のペアを区切る正規表現パターン。一般的にはスペース文字です({space})。", + "xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysFieldLabel": "キーを含める", + "xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysHelpText": "出力に含める、抽出されたキーのリスト。デフォルトはすべてのキーです。", + "xpack.ingestPipelines.pipelineEditor.kvForm.prefixFieldLabel": "接頭辞", + "xpack.ingestPipelines.pipelineEditor.kvForm.prefixHelpText": "抽出されたキーに追加する接頭辞。", + "xpack.ingestPipelines.pipelineEditor.kvForm.stripBracketsFieldLabel": "括弧を削除", + "xpack.ingestPipelines.pipelineEditor.kvForm.stripBracketsHelpText": "抽出された値から括弧({paren}、{angle}、{square})と引用符({singleQuote}、{doubleQuote})を削除します。", + "xpack.ingestPipelines.pipelineEditor.kvForm.targetFieldHelpText": "抽出されたフィールドの出力フィールド。デフォルトはドキュメントルートです。", + "xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyFieldLabel": "キーを切り取る", + "xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyHelpText": "抽出されたキーから切り取る文字。", + "xpack.ingestPipelines.pipelineEditor.kvForm.trimValueFieldLabel": "値を切り取る", + "xpack.ingestPipelines.pipelineEditor.kvForm.trimValueHelpText": "抽出された値から切り取る文字。", + "xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitFieldLabel": "値を分割", + "xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitHelpText": "キーと値を分割するために使用される正規表現。一般的には代入演算子です({equal})。", + "xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.loadFromJson.buttonLabel": "プロセッサーをインポート", "xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.cancel": "キャンセル", "xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.confirm": "読み込みと上書き", "xpack.ingestPipelines.pipelineEditor.loadFromJson.editor": "パイプラインオブジェクト", @@ -9333,23 +10506,141 @@ "xpack.ingestPipelines.pipelineEditor.loadFromJson.error.title": "無効なパイプライン", "xpack.ingestPipelines.pipelineEditor.loadFromJson.modalTitle": "JSONの読み込み", "xpack.ingestPipelines.pipelineEditor.loadJsonModal.jsonEditorHelpText": "パイプラインオブジェクトを指定してください。これにより、既存のパイプラインプロセッサーとエラープロセッサーが無効化されます。", + "xpack.ingestPipelines.pipelineEditor.lowerCaseForm.fieldNameHelpText": "小文字にするフィールド。", "xpack.ingestPipelines.pipelineEditor.onFailureProcessorsDocumentationLink": "詳細情報", "xpack.ingestPipelines.pipelineEditor.onFailureProcessorsLabel": "エラーハンドラー", "xpack.ingestPipelines.pipelineEditor.onFailureTreeDescription": "このパイプラインの例外を処理するために使用されるプロセッサー。{learnMoreLink}", "xpack.ingestPipelines.pipelineEditor.onFailureTreeTitle": "障害プロセッサー", + "xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldHelpText": "実行するインジェストパイプラインの名前。", + "xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldLabel": "パイプライン名", + "xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameRequiredError": "値が必要です。", "xpack.ingestPipelines.pipelineEditor.processorsDocumentationLink": "詳細情報", - "xpack.ingestPipelines.pipelineEditor.processorsTreeDescription": "インデックスの前にドキュメントを前処理するために使用されるプロセッサー。{learnMoreLink}", + "xpack.ingestPipelines.pipelineEditor.processorsTreeDescription": "インデックスの前に、プロセッサーを使用してデータを変換します。{learnMoreLink}", "xpack.ingestPipelines.pipelineEditor.processorsTreeTitle": "プロセッサー", + "xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameField": "フィールド", + "xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameHelpText": "削除するフィールド。", + "xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameRequiredError": "値が必要です。", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.cancelButtonLabel": "キャンセル", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.confirmationButtonLabel": "プロセッサーの削除", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.titleText": "{type}プロセッサーの削除", + "xpack.ingestPipelines.pipelineEditor.renameForm.fieldNameHelpText": "名前を変更するフィールド。", + "xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldHelpText": "新しいフィールド名。このフィールドがすでに存在していてはなりません。", + "xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldLabel": "ターゲットフィールド", + "xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.idRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.langFieldHelpText": "スクリプト言語。デフォルトは{lang}です。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.langFieldLabel": "言語(任意)", + "xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldAriaLabel": "パラメーターJSONエディター", + "xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldHelpText": "変数としてスクリプトに渡される名前付きパラメーター。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldLabel": "パラメーター", + "xpack.ingestPipelines.pipelineEditor.scriptForm.processorInvalidJsonError": "無効なJSON", + "xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldAriaLabel": "ソーススクリプトJSONエディター", + "xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldHelpText": "実行するインラインスクリプト。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldLabel": "送信元", + "xpack.ingestPipelines.pipelineEditor.scriptForm.sourceRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldHelpText": "実行するストアドスクリプトのID。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldLabel": "ストアドスクリプトID", + "xpack.ingestPipelines.pipelineEditor.scriptForm.useScriptIdToggleLabel": "ストアドスクリプトを実行", + "xpack.ingestPipelines.pipelineEditor.setForm.fieldNameField": "挿入または更新するフィールド。", + "xpack.ingestPipelines.pipelineEditor.setForm.ignoreEmptyValueFieldHelpText": "{valueField}が{nullValue}であるか、空の文字列である場合は、フィールドを更新しません。", + "xpack.ingestPipelines.pipelineEditor.setForm.ignoreEmptyValueFieldLabel": "空の値を無視", + "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldHelpText": "有効にすると、既存のフィールド値を上書きします。無効にすると、{nullValue}フィールドのみを更新します。", "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel": "無効化", + "xpack.ingestPipelines.pipelineEditor.setForm.propertiesFieldHelpText": "追加するユーザー詳細情報。フォルトは{value}です。", + "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText": "フィールドの値。", "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "値", - "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "設定する値が必要です。", + "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.fieldNameField": "出力フィールド。", + "xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.propertiesFieldLabel": "プロパティ(任意)", "xpack.ingestPipelines.pipelineEditor.settingsForm.learnMoreLabelLink.processor": "{processorLabel}ドキュメント", + "xpack.ingestPipelines.pipelineEditor.sortForm.fieldNameHelpText": "並べ替える配列値を含むフィールド。", + "xpack.ingestPipelines.pipelineEditor.sortForm.orderField.ascendingOption": "昇順", + "xpack.ingestPipelines.pipelineEditor.sortForm.orderField.descendingOption": "降順", + "xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldHelpText": "並べ替え順。文字列と数値が混在した配列は辞書学的に並べ替えられます。", + "xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldLabel": "順序", + "xpack.ingestPipelines.pipelineEditor.splitForm.fieldNameHelpText": "分割するフィールド。", + "xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldHelpText": "分割されたフィールド値の末尾にある空白はすべて保持されます。", + "xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldLabel": "末尾の空白を保持", + "xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldHelpText": "フィールド値を区切る正規表現パターン。", + "xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldLabel": "区切り文字", + "xpack.ingestPipelines.pipelineEditor.splitForm.separatorRequiredError": "値が必要です。", + "xpack.ingestPipelines.pipelineEditor.testPipeline.buttonLabel": "ドキュメントを追加", + "xpack.ingestPipelines.pipelineEditor.testPipeline.documentLabel": "ドキュメント{documentNumber}", + "xpack.ingestPipelines.pipelineEditor.testPipeline.documentsdropdown.dropdownLabel": "ドキュメント:", + "xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.editDocumentsButtonLabel": "ドキュメントを編集", + "xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.popoverTitle": "ドキュメントをテスト", + "xpack.ingestPipelines.pipelineEditor.testPipeline.outputButtonLabel": "出力を表示", + "xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.cancelButtonLabel": "キャンセル", + "xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.description": "出力がリセットされます。", + "xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.resetButtonLabel": "ドキュメントを消去", + "xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.title": "ドキュメントを消去", + "xpack.ingestPipelines.pipelineEditor.testPipeline.selectedDocumentLabel": "ドキュメント{selectedDocument}", + "xpack.ingestPipelines.pipelineEditor.testPipeline.testPipelineActionsLabel": "パイプラインをテスト:", + "xpack.ingestPipelines.pipelineEditor.trimForm.fieldNameHelpText": "切り取るフィールド。文字列の配列の場合、各エレメントが切り取られます。", "xpack.ingestPipelines.pipelineEditor.typeField.fieldRequiredError": "タイプが必要です。", "xpack.ingestPipelines.pipelineEditor.typeField.typeFieldComboboxPlaceholder": "入力してエンターキーを押してください", "xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel": "プロセッサー", + "xpack.ingestPipelines.pipelineEditor.uppercaseForm.fieldNameHelpText": "大文字にするフィールド。文字列の配列の場合、各エレメントが大文字にされます。", + "xpack.ingestPipelines.pipelineEditor.urlDecodeForm.fieldNameHelpText": "デコードするフィールド。文字列の配列の場合、各エレメントがデコードされます。", + "xpack.ingestPipelines.pipelineEditor.userAgentForm.fieldNameHelpText": "ユーザーエージェント文字列を含むフィールド。", + "xpack.ingestPipelines.pipelineEditor.userAgentForm.propertiesFieldHelpText": "ターゲットフィールドに追加されたプロパティ。", + "xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldHelpText": "ユーザーエージェント文字列を解析するために使用される正規表現を含むファイル。", + "xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldLabel": "正規表現ファイル(任意)", + "xpack.ingestPipelines.pipelineEditor.userAgentForm.targetFieldHelpText": "出力フィールド。デフォルトは{defaultField}です。", + "xpack.ingestPipelines.pipelineEditorItem.droppedStatusAriaLabel": "ドロップ", + "xpack.ingestPipelines.pipelineEditorItem.errorIgnoredStatusAriaLabel": "エラーを無視", + "xpack.ingestPipelines.pipelineEditorItem.errorStatusAriaLabel": "エラー", + "xpack.ingestPipelines.pipelineEditorItem.inactiveStatusAriaLabel": "実行しない", + "xpack.ingestPipelines.pipelineEditorItem.skippedStatusAriaLabel": "スキップ", + "xpack.ingestPipelines.pipelineEditorItem.successStatusAriaLabel": "成功", + "xpack.ingestPipelines.pipelineEditorItem.unknownStatusAriaLabel": "不明", + "xpack.ingestPipelines.processorFormFlyout.cancelButtonLabel": "キャンセル", + "xpack.ingestPipelines.processorFormFlyout.updateButtonLabel": "更新", + "xpack.ingestPipelines.processorOutput.descriptionText": "テストドキュメントの変更をプレビューします。", + "xpack.ingestPipelines.processorOutput.documentLabel": "ドキュメント{number}", + "xpack.ingestPipelines.processorOutput.documentsDropdownLabel": "データをテスト:", + "xpack.ingestPipelines.processorOutput.droppedCalloutTitle": "ドキュメントは破棄されました。", + "xpack.ingestPipelines.processorOutput.ignoredErrorCodeBlockLabel": "無視されたエラーがあります", + "xpack.ingestPipelines.processorOutput.loadingMessage": "プロセッサー出力を読み込んでいます...", + "xpack.ingestPipelines.processorOutput.noOutputCalloutTitle": "このプロセッサーの出力はありません。", + "xpack.ingestPipelines.processorOutput.processorErrorCodeBlockLabel": "エラーが発生しました", + "xpack.ingestPipelines.processorOutput.processorInputCodeBlockLabel": "入力データ", + "xpack.ingestPipelines.processorOutput.processorOutputCodeBlockLabel": "出力データ", + "xpack.ingestPipelines.processorOutput.skippedCalloutTitle": "プロセッサーは実行されませんでした。", + "xpack.ingestPipelines.processors.description.append": "フィールド配列の末尾に値を追加します。フィールドに単一の値が含まれている場合、プロセッサーはまず値を配列に変換します。フィールドが存在しない場合、プロセッサーは追加された値を含む配列を作成します。", + "xpack.ingestPipelines.processors.description.bytes": "デジタルストレージの単位をバイトに変換します。たとえば、1KBは1024バイトになります。", + "xpack.ingestPipelines.processors.description.circle": "円の定義を近似多角形に変換します。", + "xpack.ingestPipelines.processors.description.convert": "フィールドを別のデータ型に変換します。たとえば、文字列をロングに変換できます。", + "xpack.ingestPipelines.processors.description.csv": "CSVデータからフィールド値を抽出します。", + "xpack.ingestPipelines.processors.description.date": "日付をドキュメントタイムスタンプに変換します。", + "xpack.ingestPipelines.processors.description.dateIndexName": "日付またはタイムスタンプを使用して、ドキュメントを正しい時間ベースのインデックスに追加します。インデックス名は、{value}などの日付演算パターンを使用する必要があります。", + "xpack.ingestPipelines.processors.description.dissect": "分析パターンを使用して、フィールドから一致を抽出します。", + "xpack.ingestPipelines.processors.description.dotExpander": "ドット表記を含むフィールドをオブジェクトフィールドに展開します。パイプラインの他のプロセッサーは、オブジェクトフィールドにアクセスできます。", + "xpack.ingestPipelines.processors.description.drop": "エラーを返さずにドキュメントを破棄します。指定した条件を満たすドキュメントにのみインデックスするために使用されます。", + "xpack.ingestPipelines.processors.description.enrich": "{enrichPolicyLink}に基づいてエンリッチデータを受信ドキュメントに追加します。", + "xpack.ingestPipelines.processors.description.fail": "エラー時にカスタムエラーメッセージを返します。一般的に、必要な条件を要求者に通知するために使用されます。", + "xpack.ingestPipelines.processors.description.foreach": "インジェストプロセッサーを配列の各値に適用します。", + "xpack.ingestPipelines.processors.description.geoip": "IPアドレスに基づいて地理データを追加します。Maxmindデータベースファイルの地理データを使用します。", + "xpack.ingestPipelines.processors.description.grok": "{grokLink}式を使用して、フィールドから一致を抽出します。", + "xpack.ingestPipelines.processors.description.gsub": "正規表現を使用して、フィールドサブ文字列を置換します。", + "xpack.ingestPipelines.processors.description.htmlStrip": "フィールドからHTMLタグを削除します。", + "xpack.ingestPipelines.processors.description.inference": "学習済みのデータフレーム分析モデルを使用して、受信データに対して推論します。", + "xpack.ingestPipelines.processors.description.join": "配列要素を文字列に結合します。各エレメント間に区切り文字を挿入します。", + "xpack.ingestPipelines.processors.description.json": "互換性がある文字列からJSONオブジェクトを作成します。", + "xpack.ingestPipelines.processors.description.kv": "キーと値のペアを含む文字列からフィールドを抽出します。", + "xpack.ingestPipelines.processors.description.lowercase": "文字列を小文字に変換します。", + "xpack.ingestPipelines.processors.description.pipeline": "別のインジェストノードパイプラインを実行します。", + "xpack.ingestPipelines.processors.description.remove": "1つ以上のフィールドを削除します。", + "xpack.ingestPipelines.processors.description.rename": "既存のフィールドの名前を変更します。", + "xpack.ingestPipelines.processors.description.script": "受信ドキュメントでスクリプトを実行します。", + "xpack.ingestPipelines.processors.description.set": "フィールドの値を設定します。", + "xpack.ingestPipelines.processors.description.setSecurityUser": "ユーザー名と電子メールアドレスなどの現在のユーザーの詳細情報を受信ドキュメントに追加します。インデックスリクエストには認証されたユーザーが必要です。", + "xpack.ingestPipelines.processors.description.sort": "フィールドの配列要素を並べ替えます。", + "xpack.ingestPipelines.processors.description.split": "フィールド値を配列に分割します。", + "xpack.ingestPipelines.processors.description.trim": "文字列から先頭と末尾の空白を削除します。", + "xpack.ingestPipelines.processors.description.uppercase": "文字列を大文字に変換します。", + "xpack.ingestPipelines.processors.description.urldecode": "URLエンコードされた文字列をデコードします。", + "xpack.ingestPipelines.processors.description.userAgent": "ブラウザーのユーザーエージェント文字列から値を抽出します。", "xpack.ingestPipelines.processors.label.append": "末尾に追加", "xpack.ingestPipelines.processors.label.bytes": "バイト", "xpack.ingestPipelines.processors.label.circle": "円", @@ -9370,7 +10661,7 @@ "xpack.ingestPipelines.processors.label.inference": "推定", "xpack.ingestPipelines.processors.label.join": "結合", "xpack.ingestPipelines.processors.label.json": "JSON", - "xpack.ingestPipelines.processors.label.kv": "KV", + "xpack.ingestPipelines.processors.label.kv": "キーと値(KV)", "xpack.ingestPipelines.processors.label.lowercase": "小文字", "xpack.ingestPipelines.processors.label.pipeline": "パイプライン", "xpack.ingestPipelines.processors.label.remove": "削除", @@ -9388,47 +10679,71 @@ "xpack.ingestPipelines.requestFlyout.descriptionText": "このElasticsearchリクエストは、このパイプラインを作成または更新します。", "xpack.ingestPipelines.requestFlyout.namedTitle": "「{name}」のリクエスト", "xpack.ingestPipelines.requestFlyout.unnamedTitle": "リクエスト", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.configurationTabTitle": "構成", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.configureOnFailureTitle": "エラープロセッサーの構成", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.configureTitle": "プロセッサーの構成", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.manageOnFailureTitle": "エラープロセッサーを管理", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.manageTitle": "プロセッサーを管理", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.outputTabTitle": "アウトプット", + "xpack.ingestPipelines.tabs.documentsTabTitle": "ドキュメント", "xpack.ingestPipelines.tabs.outputTabTitle": "アウトプット", + "xpack.ingestPipelines.testPipeline.errorNotificationText": "パイプラインの実行エラー", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel": "ドキュメント", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError": "ドキュメントJSONが無効です。", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.noDocumentsError": "ドキュメントが必要です。", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.oneDocumentRequiredError": "1つ以上のドキュメントが必要です。", "xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldAriaLabel": "ドキュメントJSONエディター", + "xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldClearAllButtonLabel": "すべて消去", "xpack.ingestPipelines.testPipelineFlyout.documentsTab.runButtonLabel": "パイプラインを実行", "xpack.ingestPipelines.testPipelineFlyout.documentsTab.runningButtonLabel": "実行中", - "xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink": "詳細", - "xpack.ingestPipelines.testPipelineFlyout.documentsTab.tabDescriptionText": "投入するパイプラインのドキュメントの配列を指定します。{learnMoreLink}", + "xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink": "詳細情報", + "xpack.ingestPipelines.testPipelineFlyout.documentsTab.tabDescriptionText": "投入するパイプラインのドキュメントを指定します。{learnMoreLink}", "xpack.ingestPipelines.testPipelineFlyout.executePipelineError": "パイプラインを実行できません", "xpack.ingestPipelines.testPipelineFlyout.outputTab.descriptionLinkLabel": "出力を更新", "xpack.ingestPipelines.testPipelineFlyout.outputTab.descriptionText": "出力データを表示するか、パイプライン経由で渡されるときに各プロセッサーがドキュメントにどのように影響するのかを確認します。", "xpack.ingestPipelines.testPipelineFlyout.outputTab.verboseSwitchLabel": "冗長出力を表示", "xpack.ingestPipelines.testPipelineFlyout.successNotificationText": "パイプラインが実行されました", "xpack.ingestPipelines.testPipelineFlyout.title": "パイプラインをテスト", + "xpack.lens.app.addToLibrary": "ライブラリに保存", + "xpack.lens.app.cancel": "キャンセル", + "xpack.lens.app.cancelButtonAriaLabel": "変更を保存せずに最後に使用していたアプリに戻る", "xpack.lens.app.docLoadingError": "保存されたドキュメントの保存中にエラーが発生", "xpack.lens.app.docSavingError": "ドキュメントの保存中にエラーが発生", "xpack.lens.app.indexPatternLoadingError": "インデックスパターンの読み込み中にエラーが発生", "xpack.lens.app.save": "保存", "xpack.lens.app.saveAndReturn": "保存して戻る", + "xpack.lens.app.saveAndReturnButtonAriaLabel": "現在のLensビジュアライゼーションを保存し、前回使用していたアプリに戻る", "xpack.lens.app.saveAs": "名前を付けて保存", + "xpack.lens.app.saveButtonAriaLabel": "現在のLensビジュアライゼーションを保存", "xpack.lens.app.saveModalType": "レンズビジュアライゼーション", "xpack.lens.app.unsavedWorkMessage": "作業内容を保存せずに、Lensから移動しますか?", "xpack.lens.app.unsavedWorkTitle": "保存されていない変更", + "xpack.lens.app.updatePanel": "{originatingAppName}でパネルを更新", "xpack.lens.app404": "404 Not Found", + "xpack.lens.breadcrumbsByValue": "ビジュアライゼーションを編集", "xpack.lens.breadcrumbsCreate": "作成", "xpack.lens.breadcrumbsTitle": "可視化", "xpack.lens.chartSwitch.dataLossDescription": "このチャートに切り替えると構成の一部が失われます", "xpack.lens.chartSwitch.dataLossLabel": "データ喪失", + "xpack.lens.chartSwitch.noResults": "{term}の結果が見つかりませんでした。", "xpack.lens.chartTitle.unsaved": "未保存", + "xpack.lens.configPanel.chartType": "チャートタイプ", "xpack.lens.configPanel.color.tooltip.auto": "カスタム色を指定しない場合、Lensは自動的に色を選択します。", "xpack.lens.configPanel.color.tooltip.custom": "[自動]モードに戻すには、カスタム色をオフにしてください。", "xpack.lens.configPanel.color.tooltip.disabled": "レイヤーに「内訳条件」が含まれている場合は、個別の系列をカスタム色にできません。", "xpack.lens.configPanel.selectVisualization": "ビジュアライゼーションを選択してください", - "xpack.lens.configure.editConfig": "構成の編集", - "xpack.lens.configure.emptyConfig": "ここにフィールドをドロップ", + "xpack.lens.configure.configurePanelTitle": "{groupLabel}構成", + "xpack.lens.configure.editConfig": "クリックして構成を編集するか、ドラッグして移動", + "xpack.lens.configure.emptyConfig": "フィールドを破棄、またはクリックして追加", + "xpack.lens.configure.invalidConfigTooltip": "無効な構成です。", + "xpack.lens.configure.invalidConfigTooltipClick": "詳細はクリックしてください。", + "xpack.lens.customBucketContainer.dragToReorder": "ドラッグして並べ替え", "xpack.lens.dataPanelWrapper.switchDatasource": "データソースに切り替える", + "xpack.lens.datatable.breakdown": "内訳の基準", "xpack.lens.datatable.conjunctionSign": " と ", "xpack.lens.datatable.expressionHelpLabel": "データベースレンダー", "xpack.lens.datatable.label": "データテーブル", + "xpack.lens.datatable.metrics": "メトリック", "xpack.lens.datatable.suggestionLabel": "表として", "xpack.lens.datatable.titleLabel": "タイトル", "xpack.lens.datatable.visualizationName": "データベース", @@ -9440,10 +10755,13 @@ "xpack.lens.datatypes.record": "レコード", "xpack.lens.datatypes.string": "文字列", "xpack.lens.deleteLayer": "レイヤーを削除", + "xpack.lens.dimensionContainer.close": "閉じる", + "xpack.lens.discover.visualizeFieldLegend": "Visualizeフィールド", "xpack.lens.editLayerSettings": "レイヤー設定を編集", "xpack.lens.editorFrame.dataFailure": "データの読み込み中にエラーが発生しました。", "xpack.lens.editorFrame.emptyWorkspace": "開始するにはここにフィールドをドロップしてください", - "xpack.lens.editorFrame.emptyWorkspaceHeading": "レンズはビジュアライゼーションを作成するための新しいツールです", + "xpack.lens.editorFrame.emptyWorkspaceHeading": "Lensはビジュアライゼーションを作成するための新しいツールです", + "xpack.lens.editorFrame.emptyWorkspaceSimple": "ここにフィールドをドロップ", "xpack.lens.editorFrame.expandRenderingErrorButton": "エラーの詳細を表示", "xpack.lens.editorFrame.expressionFailure": "表現を正常に実行できませんでした", "xpack.lens.editorFrame.goToForums": "リクエストとフィードバック", @@ -9477,8 +10795,10 @@ "xpack.lens.indexPattern.cardinality": "ユニークカウント", "xpack.lens.indexPattern.cardinalityOf": "{name} のユニークカウント", "xpack.lens.indexPattern.changeIndexPatternTitle": "インデックスパターンを変更", + "xpack.lens.indexPattern.chooseField": "フィールドを選択", + "xpack.lens.indexPattern.chooseFieldLabel": "この関数を使用するには、フィールドを選択してください。", "xpack.lens.indexPattern.columnFormatLabel": "値の形式", - "xpack.lens.indexPattern.columnLabel": "ラベル", + "xpack.lens.indexPattern.columnLabel": "表示名", "xpack.lens.indexPattern.count": "カウント", "xpack.lens.indexPattern.countOf": "ドキュメント数", "xpack.lens.indexPattern.dateHistogram": "日付ヒストグラム", @@ -9503,18 +10823,32 @@ "xpack.lens.indexPattern.fieldItemTooltip": "可視化するには、ドラッグアンドドロップします。", "xpack.lens.indexPattern.fieldPanelEmptyStringValue": "空の文字列", "xpack.lens.indexPattern.fieldPlaceholder": "フィールド", + "xpack.lens.indexPattern.fieldStatsButtonAriaLabel": "{fieldName}: {fieldType}。Enterを押すとフィールドプレビューが表示されます。", + "xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "このフィールドにはデータがありませんが、ドラッグアンドドロップで可視化できます。", "xpack.lens.indexPattern.fieldStatsButtonLabel": "フィールドプレビューを表示するには、クリックします。可視化するには、ドラッグアンドドロップします。", "xpack.lens.indexPattern.fieldStatsCountLabel": "カウント", "xpack.lens.indexPattern.fieldStatsDisplayToggle": "次のどちらかを切り替えます:", - "xpack.lens.indexPattern.fieldStatsNoData": "表示するデータがありません", + "xpack.lens.indexPattern.fieldStatsNoData": "このフィールドは空です。500件のサンプリングされたドキュメントに存在しません。このフィールドを構成に追加すると、空白のグラフが作成される場合があります。", "xpack.lens.indexPattern.fieldTimeDistributionLabel": "時間分布", "xpack.lens.indexPattern.fieldTopValuesLabel": "トップの値", + "xpack.lens.indexPattern.filters": "フィルター", + "xpack.lens.indexPattern.filters.addaFilter": "フィルターを追加", + "xpack.lens.indexPattern.filters.clickToEdit": "クリックして編集", + "xpack.lens.indexPattern.filters.isInvalid": "このクエリは無効です", + "xpack.lens.indexPattern.filters.label.placeholder": "すべてのレコード", + "xpack.lens.indexPattern.filters.queryPlaceholderKql": "{example}", + "xpack.lens.indexPattern.filters.queryPlaceholderLucene": "{example}", + "xpack.lens.indexPattern.filters.removeFilter": "フィルターを削除", + "xpack.lens.indexPattern.functionsLabel": "関数を選択", "xpack.lens.indexPattern.groupByDropdown": "グループ分けの条件", "xpack.lens.indexPattern.indexPatternLoadError": "インデックスパターンの読み込み中にエラーが発生", + "xpack.lens.indexPattern.intervals": "間隔", + "xpack.lens.indexPattern.invalidFieldLabel": "無効なフィールドです。インデックスパターンを確認するか、別のフィールドを選択してください。", "xpack.lens.indexPattern.invalidInterval": "無効な間隔値", "xpack.lens.indexPattern.invalidOperationLabel": "この関数を使用するには、別のフィールドを選択してください。", "xpack.lens.indexPattern.max": "最高", "xpack.lens.indexPattern.maxOf": "{name} お最高値", + "xpack.lens.indexPattern.metaFieldsLabel": "メタフィールド", "xpack.lens.indexPattern.min": "最低", "xpack.lens.indexPattern.minOf": "{name} お最低値", "xpack.lens.indexPattern.noPatternsDescription": "インデックスパターンを作成するか、別のデータソースに切り替えてください", @@ -9524,6 +10858,20 @@ "xpack.lens.indexPattern.otherDocsLabel": "その他", "xpack.lens.indexPattern.percentageOfLabel": "の {percentage}%", "xpack.lens.indexPattern.percentFormatLabel": "割合 (%)", + "xpack.lens.indexPattern.range.isInvalid": "この範囲は無効です", + "xpack.lens.indexPattern.ranges.addRange": "範囲を追加", + "xpack.lens.indexPattern.ranges.customIntervalsToggle": "カスタム範囲を作成", + "xpack.lens.indexPattern.ranges.customRanges": "範囲", + "xpack.lens.indexPattern.ranges.customRangesRemoval": "カスタム範囲を削除", + "xpack.lens.indexPattern.ranges.decreaseButtonLabel": "粒度を下げる", + "xpack.lens.indexPattern.ranges.deleteRange": "範囲を削除", + "xpack.lens.indexPattern.ranges.granularity": "間隔粒度", + "xpack.lens.indexPattern.ranges.granularityDescription": "フィールドを均等な間隔に分割します。", + "xpack.lens.indexPattern.ranges.increaseButtonLabel": "粒度を上げる", + "xpack.lens.indexPattern.ranges.lessThanOrEqualAppend": "≤", + "xpack.lens.indexPattern.ranges.lessThanOrEqualTooltip": "以下", + "xpack.lens.indexPattern.ranges.lessThanPrepend": "<", + "xpack.lens.indexPattern.ranges.lessThanTooltip": "より小さい", "xpack.lens.indexPattern.records": "記録", "xpack.lens.indexPattern.removeColumnLabel": "構成を削除", "xpack.lens.indexpattern.suggestions.nestingChangeLabel": "各 {outerOperation} の {innerOperation}", @@ -9540,6 +10888,7 @@ "xpack.lens.indexPattern.terms.size": "値の数", "xpack.lens.indexPattern.termsOf": "{name} のトップの値", "xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]", + "xpack.lens.indexPattern.useAsTopLevelAgg": "最初にこのフィールドでグループ化", "xpack.lens.indexPatterns.clearFiltersLabel": "名前とタイプフィルターを消去", "xpack.lens.indexPatterns.fieldFiltersLabel": "フィールドフィルター", "xpack.lens.indexPatterns.filterByNameAriaLabel": "検索フィールド", @@ -9553,6 +10902,7 @@ "xpack.lens.indexPatterns.noFilteredFieldsLabel": "選択したフィルターと一致するフィールドはありません。", "xpack.lens.indexPatternSuggestion.removeLayerLabel": "{indexPatternTitle}のみを表示", "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "レイヤー{layerNumber}のみを表示", + "xpack.lens.labelInput.label": "ラベル", "xpack.lens.lensSavedObjectLabel": "レンズビジュアライゼーション", "xpack.lens.metric.label": "メトリック", "xpack.lens.pageTitle": "レンズ", @@ -9570,14 +10920,28 @@ "xpack.lens.pieChart.categoriesInLegendLabel": "ラベルを非表示", "xpack.lens.pieChart.fitInsideOnlyLabel": "内部のみ", "xpack.lens.pieChart.hiddenNumbersLabel": "グラフから非表示", - "xpack.lens.pieChart.labelPositionLabel": "ラベル位置", - "xpack.lens.pieChart.nestedLegendLabel": "ネストされた凡例", - "xpack.lens.pieChart.numberLabels": "ラベル値", + "xpack.lens.pieChart.labelPositionLabel": "位置", + "xpack.lens.pieChart.legendVisibility.auto": "自動", + "xpack.lens.pieChart.legendVisibility.hide": "非表示", + "xpack.lens.pieChart.legendVisibility.show": "表示", + "xpack.lens.pieChart.nestedLegendLabel": "ネスト済み", + "xpack.lens.pieChart.numberLabels": "値", + "xpack.lens.pieChart.percentDecimalsLabel": "割合の最大小数点桁数", "xpack.lens.pieChart.showCategoriesLabel": "内部または外部", "xpack.lens.pieChart.showFormatterValuesLabel": "値を表示", "xpack.lens.pieChart.showPercentValuesLabel": "割合を表示", "xpack.lens.pieChart.showTreemapCategoriesLabel": "ラベルを表示", + "xpack.lens.pieChart.valuesLabel": "ラベル", "xpack.lens.resetLayer": "レイヤーをリセット", + "xpack.lens.searchTitle": "Lens:ビジュアライゼーションを作成", + "xpack.lens.shared.legendLabel": "凡例", + "xpack.lens.shared.legendPositionBottom": "一番下", + "xpack.lens.shared.legendPositionLabel": "位置", + "xpack.lens.shared.legendPositionLeft": "左", + "xpack.lens.shared.legendPositionRight": "右", + "xpack.lens.shared.legendPositionTop": "トップ", + "xpack.lens.shared.legendVisibilityLabel": "表示", + "xpack.lens.shared.nestedLegendLabel": "ネスト済み", "xpack.lens.sugegstion.refreshSuggestionLabel": "更新", "xpack.lens.suggestion.refreshSuggestionTooltip": "選択したビジュアライゼーションに基づいて、候補を更新します。", "xpack.lens.suggestions.currentVisLabel": "現在", @@ -9586,30 +10950,70 @@ "xpack.lens.visTypeAlias.promotion.description": "レンズは直感的に使える新しいビジュアライゼーションの作成方法です。お試しください。", "xpack.lens.visTypeAlias.title": "レンズビジュアライゼーション", "xpack.lens.visTypeAlias.type": "レンズ", + "xpack.lens.xyChart.addLayer": "レイヤーを追加", "xpack.lens.xyChart.addLayerButton": "レイヤーを追加", "xpack.lens.xyChart.addLayerTooltip": "複数のレイヤーを使用すると、グラフタイプを組み合わせたり、別のインデックスパターンを可視化したりすることができます。", + "xpack.lens.xyChart.axisNameLabel": "軸名", "xpack.lens.xyChart.axisSide.auto": "自動", + "xpack.lens.xyChart.axisSide.bottom": "一番下", "xpack.lens.xyChart.axisSide.label": "軸側", "xpack.lens.xyChart.axisSide.left": "左", "xpack.lens.xyChart.axisSide.right": "右", + "xpack.lens.xyChart.axisSide.top": "トップ", + "xpack.lens.xyChart.axisTitlesSettings.help": "xおよびy軸のタイトルを表示", + "xpack.lens.xyChart.bottomAxisDisabledHelpText": "この設定は、下の軸が有効であるときにのみ適用されます。", + "xpack.lens.xyChart.bottomAxisLabel": "下の軸", "xpack.lens.xyChart.chartTypeLabel": "チャートタイプ", "xpack.lens.xyChart.chartTypeLegend": "チャートタイプ", + "xpack.lens.xyChart.emptyXLabel": "(空)", "xpack.lens.xyChart.fittingDisabledHelpText": "この設定は折れ線グラフとエリアグラフでのみ適用されます。", "xpack.lens.xyChart.fittingFunction.help": "欠測値の処理方法を定義", + "xpack.lens.xyChart.Gridlines": "グリッド線", + "xpack.lens.xyChart.gridlinesSettings.help": "xおよびy軸のグリッド線を表示", "xpack.lens.xyChart.help": "X/Y チャート", "xpack.lens.xyChart.isVisible.help": "判例の表示・非表示を指定します。", + "xpack.lens.xyChart.leftAxisDisabledHelpText": "この設定は、左の軸が有効であるときにのみ適用されます。", + "xpack.lens.xyChart.leftAxisLabel": "左の軸", "xpack.lens.xyChart.legend.help": "チャートの凡例を構成します。", + "xpack.lens.xyChart.legendVisibility.auto": "自動", + "xpack.lens.xyChart.legendVisibility.hide": "非表示", + "xpack.lens.xyChart.legendVisibility.show": "表示", + "xpack.lens.xyChart.missingValuesLabel": "欠測値", "xpack.lens.xyChart.nestUnderRoot": "データセット全体", + "xpack.lens.xyChart.overwriteAxisTitle": "軸タイトルを上書き", "xpack.lens.xyChart.position.help": "凡例の配置を指定します。", "xpack.lens.xyChart.renderer.help": "X/Y チャートを再レンダリング", + "xpack.lens.xyChart.rightAxisDisabledHelpText": "この設定は、右の軸が有効であるときにのみ適用されます。", + "xpack.lens.xyChart.rightAxisLabel": "右の軸", "xpack.lens.xyChart.seriesColor.auto": "自動", "xpack.lens.xyChart.seriesColor.label": "系列色", + "xpack.lens.xyChart.ShowAxisTitleLabel": "表示", + "xpack.lens.xyChart.showSingleSeries.help": "エントリが1件の凡例を表示するかどうかを指定します", "xpack.lens.xyChart.splitSeries": "系列を分割", + "xpack.lens.xyChart.tickLabels": "目盛ラベル", + "xpack.lens.xyChart.tickLabelsSettings.help": "xおよびy軸の目盛ラベルを表示", "xpack.lens.xyChart.title.help": "軸のタイトル", + "xpack.lens.xyChart.topAxisDisabledHelpText": "この設定は、上の軸が有効であるときにのみ適用されます。", + "xpack.lens.xyChart.topAxisLabel": "上の軸", + "xpack.lens.xyChart.valuesLabel": "値", + "xpack.lens.xyChart.xAxisGridlines.help": "x軸のグリッド線を表示するかどうかを指定します。", "xpack.lens.xyChart.xAxisLabel": "X 軸", + "xpack.lens.xyChart.xAxisTickLabels.help": "x軸の目盛ラベルを表示するかどうかを指定します。", + "xpack.lens.xyChart.xAxisTitle.help": "x軸のタイトルを表示するかどうかを指定します。", + "xpack.lens.xyChart.xTitle.help": "x軸のタイトル", "xpack.lens.xyChart.yAxisLabel": "Y 軸", + "xpack.lens.xyChart.yLeftAxisgridlines.help": "左y軸のグリッド線を表示するかどうかを指定します。", + "xpack.lens.xyChart.yLeftAxisTickLabels.help": "左y軸の目盛ラベルを表示するかどうかを指定します。", + "xpack.lens.xyChart.yLeftAxisTitle.help": "左y軸のタイトルを表示するかどうかを指定します。", + "xpack.lens.xyChart.yLeftTitle.help": "左y軸のタイトル", + "xpack.lens.xyChart.yRightAxisgridlines.help": "右y軸のグリッド線を表示するかどうかを指定します。", + "xpack.lens.xyChart.yRightAxisTickLabels.help": "右y軸の目盛ラベルを表示するかどうかを指定します。", + "xpack.lens.xyChart.yRightAxisTitle.help": "右y軸のタイトルを表示するかどうかを指定します。", + "xpack.lens.xyChart.yRightTitle.help": "右y軸のタイトル", + "xpack.lens.xySuggestions.asPercentageTitle": "割合 (%)", "xpack.lens.xySuggestions.barChartTitle": "棒グラフ", "xpack.lens.xySuggestions.dateSuggestion": "{xTitle} の {yTitle}", + "xpack.lens.xySuggestions.emptyAxisTitle": "(空)", "xpack.lens.xySuggestions.flipTitle": "反転", "xpack.lens.xySuggestions.lineChartTitle": "折れ線グラフ", "xpack.lens.xySuggestions.nonDateSuggestion": "{xTitle} の {yTitle}", @@ -9617,13 +11021,21 @@ "xpack.lens.xySuggestions.unstackedChartTitle": "スタックが解除されました", "xpack.lens.xySuggestions.yAxixConjunctionSign": " と ", "xpack.lens.xyVisualization.areaLabel": "エリア", + "xpack.lens.xyVisualization.barHorizontalFullLabel": "横棒", + "xpack.lens.xyVisualization.barHorizontalLabel": "横棒", "xpack.lens.xyVisualization.barLabel": "バー", "xpack.lens.xyVisualization.lineLabel": "折れ線", - "xpack.lens.xyVisualization.mixedBarHorizontalLabel": "ミックスされた横棒", + "xpack.lens.xyVisualization.mixedBarHorizontalLabel": "混合横棒", "xpack.lens.xyVisualization.mixedLabel": "ミックスされた XY", "xpack.lens.xyVisualization.noDataLabel": "結果が見つかりませんでした", "xpack.lens.xyVisualization.stackedAreaLabel": "スタックされたエリア", + "xpack.lens.xyVisualization.stackedBarHorizontalFullLabel": "積み上げ横棒", + "xpack.lens.xyVisualization.stackedBarHorizontalLabel": "横積み上げ棒", "xpack.lens.xyVisualization.stackedBarLabel": "スタックされたバー", + "xpack.lens.xyVisualization.stackedPercentageAreaLabel": "割合エリア", + "xpack.lens.xyVisualization.stackedPercentageBarHorizontalFullLabel": "割合横棒", + "xpack.lens.xyVisualization.stackedPercentageBarHorizontalLabel": "横割合棒", + "xpack.lens.xyVisualization.stackedPercentageBarLabel": "割合棒", "xpack.lens.xyVisualization.xyLabel": "XY", "xpack.licenseMgmt.app.checkingPermissionsErrorMessage": "パーミッションの確認中にエラーが発生", "xpack.licenseMgmt.app.deniedPermissionDescription": "ライセンス管理を使用するには、{permissionType}権限が必要です", @@ -9794,11 +11206,14 @@ "xpack.logstash.upgradeFailureActions.goBackButtonLabel": "戻る", "xpack.logstash.upstreamPipelineArgumentMustContainAnIdPropertyErrorMessage": "upstreamPipeline 引数には id プロパティを含める必要があります", "xpack.logstash.workersTooltip": "パイプラインのフィルターとアウトプットステージを同時に実行するワーカーの数です。イベントが詰まってしまう場合や CPU が飽和状態ではない場合は、マシンの処理能力をより有効に活用するため、この数字を上げてみてください。\n\nデフォルト値:ホストの CPU コア数です", + "xpack.maps.actionSelect.label": "アクション", "xpack.maps.addLayerPanel.addLayer": "レイヤーを追加", "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "レイヤーを変更", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "キャンセル", "xpack.maps.aggs.defaultCountLabel": "カウント", "xpack.maps.appTitle": "マップ", + "xpack.maps.badge.readOnly.text": "読み取り専用", + "xpack.maps.badge.readOnly.tooltip": "マップを保存できません", "xpack.maps.blendedVectorLayer.clusteredLayerName": "クラスター化 {displayName}", "xpack.maps.breadCrumbs.unsavedChangesWarning": "マップには保存されていない変更があります。終了してよろしいですか?", "xpack.maps.choropleth.boundaries.elasticsearch": "Elasticsearchの点、線、多角形", @@ -9816,6 +11231,7 @@ "xpack.maps.common.esSpatialRelation.disjointLabel": "disjoint", "xpack.maps.common.esSpatialRelation.intersectsLabel": "intersects", "xpack.maps.common.esSpatialRelation.withinLabel": "within", + "xpack.maps.discover.visualizeFieldLabel": "Mapsで可視化", "xpack.maps.distanceFilterForm.filterLabelLabel": "ラベルでフィルタリング", "xpack.maps.drawTooltip.boundsInstructions": "クリックして四角形を開始します。マウスを移動して四角形サイズを調整します。もう一度クリックして終了します。", "xpack.maps.drawTooltip.distanceInstructions": "クリックして点を設定します。マウスを移動して距離を調整します。クリックして終了します。", @@ -9837,7 +11253,8 @@ "xpack.maps.esSearch.topHitsEntitiesCountMsg": "{entityCount} 件のエントリーを発見.", "xpack.maps.esSearch.topHitsResultsTrimmedMsg": "結果は最初の{entityCount}/~{totalEntities}エンティティに制限されます。", "xpack.maps.esSearch.topHitsSizeMsg": "エンティティごとに上位の{topHitsSize}ドキュメントを表示しています。", - "xpack.maps.feature.appDescription": "Elasticsearch と Elastic Maps Service の地理空間データを閲覧します", + "xpack.maps.feature.appDescription": "ElasticsearchとElastic Maps Serviceの地理空間データを閲覧します。", + "xpack.maps.featureCatalogue.mapsSubtitle": "地理的なデータをプロットします。", "xpack.maps.featureRegistry.mapsFeatureName": "マップ", "xpack.maps.fileUploadWizard.description": "ElasticsearchでGeoJSONデータにインデックスします", "xpack.maps.fileUploadWizard.importFileSetupLabel": "ファイルのインポート", @@ -9965,6 +11382,8 @@ "xpack.maps.mapListing.unableToDeleteToastTitle": "マップを削除できません", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "インデックスパターンを選択", "xpack.maps.mapSavedObjectLabel": "マップ", + "xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "自動的にマップをデータ境界に合わせる", + "xpack.maps.mapSettingsPanel.autoFitToDataBoundsLabel": "自動的にマップをデータ境界に合わせる", "xpack.maps.mapSettingsPanel.browserLocationLabel": "ブラウザーの位置情報", "xpack.maps.mapSettingsPanel.cancelLabel": "キャンセル", "xpack.maps.mapSettingsPanel.closeLabel": "閉じる", @@ -10006,6 +11425,7 @@ "xpack.maps.mvtSource.tooltipsTitle": "ツールチップフィールド", "xpack.maps.mvtSource.trashButtonAriaLabel": "フィールドの削除", "xpack.maps.mvtSource.trashButtonTitle": "フィールドの削除", + "xpack.maps.newMapTitle": "新しいマップ", "xpack.maps.noIndexPattern.doThisLinkTextDescription": "インデックスパターンを作成します", "xpack.maps.noIndexPattern.doThisPrefixDescription": "次のことが必要です ", "xpack.maps.noIndexPattern.doThisSuffixDescription": " 地理空間フィールドを含む", @@ -10082,6 +11502,7 @@ "xpack.maps.source.esGrid.metricsLabel": "メトリック", "xpack.maps.source.esGrid.noIndexPatternErrorMessage": "インデックスパターン {id} が見つかりません", "xpack.maps.source.esGrid.resolutionParamErrorMessage": "グリッド解像度パラメーターが認識されません: {resolution}", + "xpack.maps.source.esGrid.superFineDropDownOption": "高精細(ベータ)", "xpack.maps.source.esGridClustersDescription": "それぞれのグリッド付きセルのメトリックでグリッドにグループ分けされた地理空間データです。", "xpack.maps.source.esGridClustersTitle": "クラスターとグリッド", "xpack.maps.source.esGridHeatmapDescription": "密度を示すグリッドでグループ化された地理空間データ", @@ -10099,9 +11520,11 @@ "xpack.maps.source.esSearch.geoFieldTypeLabel": "地理空間フィールドタイプ", "xpack.maps.source.esSearch.indexPatternLabel": "インデックスパターン", "xpack.maps.source.esSearch.joinsDisabledReason": "クラスターでスケーリングするときに、結合はサポートされていません", + "xpack.maps.source.esSearch.joinsDisabledReasonMvt": "mvtベクトルタイルでスケーリングするときに、結合はサポートされていません", "xpack.maps.source.esSearch.limitScalingLabel": "結果を {maxResultWindow} に限定。", "xpack.maps.source.esSearch.loadErrorMessage": "インデックスパターン {id} が見つかりません", "xpack.maps.source.esSearch.loadTooltipPropertiesErrorMsg": "ドキュメントが見つかりません。_id: {docId}", + "xpack.maps.source.esSearch.mvtDescription": "大きいデータセットを高速表示するために、ベクトルタイルを使用します。", "xpack.maps.source.esSearch.selectLabel": "ジオフィールドを選択", "xpack.maps.source.esSearch.sortFieldLabel": "フィールド", "xpack.maps.source.esSearch.sortFieldSelectPlaceholder": "ソートフィールドを選択", @@ -10109,6 +11532,7 @@ "xpack.maps.source.esSearch.topHitsSizeLabel": "エンティティごとのドキュメント数", "xpack.maps.source.esSearch.topHitsSplitFieldLabel": "エンティティ", "xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", + "xpack.maps.source.esSearch.useMVTVectorTiles": "ベクトルタイルを使用", "xpack.maps.source.esSearch.useTopHitsLabel": "エンティティごとにトップヒットを表示。", "xpack.maps.source.esSearchDescription": "Elasticsearchの点、線、多角形", "xpack.maps.source.esSearchTitle": "ドキュメント", @@ -10234,22 +11658,39 @@ "xpack.maps.tooltip.layerFilterLabel": "レイヤー別に結果をフィルタリング", "xpack.maps.tooltip.loadingMsg": "読み込み中", "xpack.maps.tooltip.pageNumerText": "{pageNumber} / {total}", + "xpack.maps.tooltip.showAddFilterActionsViewLabel": "フィルターアクション", "xpack.maps.tooltip.showGeometryFilterViewLinkLabel": "ジオメトリでフィルタリング", "xpack.maps.tooltip.toolsControl.cancelDrawButtonLabel": "キャンセル", "xpack.maps.tooltip.unableToLoadContentTitle": "ツールヒントのコンテンツを読み込めません", + "xpack.maps.tooltip.viewActionsTitle": "フィルターアクションを表示", "xpack.maps.tooltipSelector.addLabelWithCount": "{count} を追加", "xpack.maps.tooltipSelector.addLabelWithoutCount": "追加", + "xpack.maps.tooltipSelector.emptyState.description": "ツールチップフィールドを追加し、フィールド値からフィルターを作成します。", "xpack.maps.tooltipSelector.grabButtonAriaLabel": "プロパティを並べ替える", "xpack.maps.tooltipSelector.grabButtonTitle": "プロパティを並べ替える", "xpack.maps.tooltipSelector.togglePopoverLabel": "追加", "xpack.maps.tooltipSelector.trashButtonAriaLabel": "プロパティを削除", "xpack.maps.tooltipSelector.trashButtonTitle": "プロパティを削除", + "xpack.maps.topNav.fullScreenButtonLabel": "全画面", + "xpack.maps.topNav.fullScreenDescription": "全画面", + "xpack.maps.topNav.openInspectorButtonLabel": "検査", + "xpack.maps.topNav.openInspectorDescription": "インスペクターを開きます", + "xpack.maps.topNav.openSettingsButtonLabel": "マップ設定", + "xpack.maps.topNav.openSettingsDescription": "マップ設定を開く", + "xpack.maps.topNav.saveAndReturnButtonLabel": "保存して戻る", + "xpack.maps.topNav.saveAsButtonLabel": "名前を付けて保存", + "xpack.maps.topNav.saveErrorMessage": "「{title}」の保存エラー", + "xpack.maps.topNav.saveMapButtonLabel": "保存", + "xpack.maps.topNav.saveMapDescription": "マップを保存", + "xpack.maps.topNav.saveMapDisabledButtonTooltip": "保存する前に、レイヤーの変更を保存するか、キャンセルしてください", + "xpack.maps.topNav.saveModalType": "マップ", + "xpack.maps.topNav.saveSuccessMessage": "「{title}」が保存されました", "xpack.maps.tutorials.ems.downloadStepText": "1.Elastic Maps Serviceにナビゲートします [着陸ページ]({emsLandingPageUrl})。\n2.左のサイドバーで、行政上の境界を設定します。\n3.[Download GeoJSON]ボタンをクリックします。", "xpack.maps.tutorials.ems.downloadStepTitle": "Elastic Maps Service境界のダウンロード", "xpack.maps.tutorials.ems.longDescription": "[Elastic Maps Service (EMS)](https://www.elastic.co/elastic-maps-service) は、行政区分のタイルレイヤーやベクターシェイプのホストになります。Elasticsearch における EMS 行政上の境界のインデックス作成により、境界のプロパティフィールドの検索ができます。", "xpack.maps.tutorials.ems.nameTitle": "ベクターシェイプ", "xpack.maps.tutorials.ems.shortDescription": "Elastic Maps Service の行政区画のベクターシェイプ", - "xpack.maps.tutorials.ems.uploadStepText": "1.[Elasticマップ]を開きます({newMapUrl})。\n2.[Add layer]をクリックしてから[Upload GeoJSON]を選択します。\n3.GeoJSON ファイルをアップロードして[Import file]をクリックします。", + "xpack.maps.tutorials.ems.uploadStepText": "1.[Maps]({newMapUrl})を開きます。\n2.[Add layer]をクリックしてから[Upload GeoJSON]を選択します。\n3.GeoJSON ファイルをアップロードして[Import file]をクリックします。", "xpack.maps.tutorials.ems.uploadStepTitle": "Elastic Maps Service境界のインデックス作成", "xpack.maps.validatedRange.rangeErrorMessage": "{min} と {max} の間でなければなりません", "xpack.maps.vector.dualSize.unitLabel": "px", @@ -10277,11 +11718,17 @@ "xpack.ml.accessDenied.description": "ML プラグインへのアクセスパーミッションがありません", "xpack.ml.accessDenied.label": "パーミッションがありません", "xpack.ml.accessDeniedLabel": "アクセスが拒否されました", - "xpack.ml.actions.applyInfluencersFiltersTitle": "値のフィラー", + "xpack.ml.accessDeniedTabLabel": "アクセス拒否", + "xpack.ml.actions.applyInfluencersFiltersTitle": "値でフィルター", "xpack.ml.actions.applyTimeRangeSelectionTitle": "時間範囲選択を適用", "xpack.ml.actions.editSwimlaneTitle": "スイムレーンの編集", "xpack.ml.actions.influencerFilterAliasLabel": "インフルエンサー{labelValue}", "xpack.ml.actions.openInAnomalyExplorerTitle": "異常エクスプローラーで開く", + "xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeDesc": "異常検知ジョブ結果を表示するときに使用する時間フィルター選択。", + "xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeName": "異常検知結果の時間フィルターデフォルト", + "xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeDesc": "シングルメトリックビューアーと異常エクスプローラーでデフォルト時間フィルターを使用します。有効ではない場合、ジョブの全時間範囲の結果が表示されます。", + "xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeName": "異常検知結果の時間フィルターデフォルトを有効にする", + "xpack.ml.analyticList.searchBar.invalidSearchErrorMessage": "無効な検索: {errorMessage}", "xpack.ml.annotationFlyout.applyToPartitionTextLabel": "注釈をこの系列に適用", "xpack.ml.annotationsTable.actionsColumnName": "アクション", "xpack.ml.annotationsTable.annotationColumnName": "注釈", @@ -10378,6 +11825,7 @@ "xpack.ml.anomalyDetection.jobManagementLabel": "ジョブ管理", "xpack.ml.anomalyDetection.singleMetricViewerLabel": "シングルメトリックビューアー", "xpack.ml.anomalyDetectionBreadcrumbLabel": "異常検知", + "xpack.ml.anomalyDetectionTabLabel": "異常検知", "xpack.ml.anomalyExplorerPageLabel": "異常エクスプローラー", "xpack.ml.anomalyResultsViewSelector.anomalyExplorerLabel": "異常エクスプローラーで結果を表示", "xpack.ml.anomalyResultsViewSelector.buttonGroupLegend": "異常結果ビューセレクター", @@ -10443,6 +11891,7 @@ "xpack.ml.calendarsList.deleteCalendars.deletingCalendarSuccessNotificationMessage": "{messageId} が選択されました", "xpack.ml.calendarsList.deleteCalendarsModal.cancelButtonLabel": "キャンセル", "xpack.ml.calendarsList.deleteCalendarsModal.deleteButtonLabel": "削除", + "xpack.ml.calendarsList.deleteCalendarsModal.deleteMultipleCalendarsTitle": "{calendarsCount, plural, one {{calendarsList}} other {#件のカレンダー}}を削除しますか?", "xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage": "カレンダーのリストの読み込み中にエラーが発生しました。", "xpack.ml.calendarsList.table.allJobsLabel": "すべてのジョブに適用", "xpack.ml.calendarsList.table.deleteButtonLabel": "削除", @@ -10505,7 +11954,9 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTestingHelpText": "データセットをテストするための正規化された混同行列", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "マルチクラス混同行列には、分析が実際のクラスで正しくデータポイントを分類した発生数と、別のクラスで誤分類した発生数が含まれます。", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTrainingHelpText": "データセットを学習するための正規化された混同行列", - "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", + "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobStatusLabel": "ジョブ状態", + "xpack.ml.dataframe.analytics.classificationExploration.evaluateSectionTitle": "モデル評価", + "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {#個のドキュメント} other {#個のドキュメント}}が評価されました", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "アクションを表示", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "すべての列を表示", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分類ジョブID {jobId}のデスティネーションインデックス", @@ -10547,6 +11998,7 @@ "xpack.ml.dataframe.analytics.create.calloutTitle": "分析フィールドがありません", "xpack.ml.dataframe.analytics.create.chooseSourceTitle": "ソースインデックスパターンを選択してください", "xpack.ml.dataframe.analytics.create.classificationHelpText": "分類はデータセットのデータポイントのラベルを予測します。", + "xpack.ml.dataframe.analytics.create.classificationTitle": "分類", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceFalseValue": "False", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceLabel": "演算機能影響", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceLabelHelpText": "機能影響演算が有効かどうかを指定します。デフォルトはtrueです。", @@ -10592,6 +12044,7 @@ "xpack.ml.dataframe.analytics.create.destinationIndexInputAriaLabel": "固有の宛先インデックス名を選択してください。", "xpack.ml.dataframe.analytics.create.destinationIndexInvalidError": "無効なデスティネーションインデックス名。", "xpack.ml.dataframe.analytics.create.destinationIndexLabel": "デスティネーションインデックス", + "xpack.ml.dataframe.analytics.create.DestIndexSameAsIdLabel": "ジョブIDと同じディスティネーションインデックス", "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "編集", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "Kibanaインデックスパターンの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "インデックスパターン{indexPatternName}はすでに作成されています。", @@ -10627,6 +12080,7 @@ "xpack.ml.dataframe.analytics.create.jobIdInvalidMaxLengthErrorMessage": "ジョブ ID は {maxLength, plural, one {# 文字} other {# 文字}} 以内でなければなりません。", "xpack.ml.dataframe.analytics.create.jobIdLabel": "ジョブID", "xpack.ml.dataframe.analytics.create.jobIdPlaceholder": "ジョブID", + "xpack.ml.dataframe.analytics.create.jsonEditorDisabledSwitchText": "構成には、フォームでサポートされていない高度なフィールドが含まれます。フォームに切り替えることができません。", "xpack.ml.dataframe.analytics.create.lambdaHelpText": "学習データセットの過剰適合を防止するための正則化パラメーター。非負の値でなければなりません。", "xpack.ml.dataframe.analytics.create.lambdaInputAriaLabel": "学習データセットの過剰適合を防止するための正則化パラメーター。", "xpack.ml.dataframe.analytics.create.lambdaLabel": "ラムダ", @@ -10643,7 +12097,7 @@ "xpack.ml.dataframe.analytics.create.modelMemoryLimitHelpText": "分析処理で許可されるメモリリソースのおおよその最大量。", "xpack.ml.dataframe.analytics.create.modelMemoryLimitLabel": "モデルメモリー制限", "xpack.ml.dataframe.analytics.create.modelMemoryUnitsInvalidError": "モデルメモリー制限のデータユニットが認識されません。{str}でなければなりません", - "xpack.ml.dataframe.analytics.create.modelMemoryUnitsMinError": "モデルメモリー制限を {mml} 未満にはできません", + "xpack.ml.dataframe.analytics.create.modelMemoryUnitsMinError": "モデルメモリー上限が推定値{mml}よりも低くなっています", "xpack.ml.dataframe.analytics.create.newAnalyticsTitle": "新しい分析ジョブ", "xpack.ml.dataframe.analytics.create.nNeighborsHelpText": "異常値検出の各方法が異常値スコアを計算するために使用する近傍の数。設定されていない場合、別のアンサンブルメンバーの異なる値が使用されます。正の整数でなければなりません", "xpack.ml.dataframe.analytics.create.nNeighborsInputAriaLabel": "異常値検出の各方法が異常値スコアを計算するために使用する近傍の数。", @@ -10656,6 +12110,7 @@ "xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesInputAriaLabel": "ドキュメントごとの機能重要度値の最大数。", "xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesLabel": "機能重要度値", "xpack.ml.dataframe.analytics.create.outlierDetectionHelpText": "異常値検出により、データセットにおける異常なデータポイントが特定されます。", + "xpack.ml.dataframe.analytics.create.outlierDetectionTitle": "外れ値検出", "xpack.ml.dataframe.analytics.create.outlierFractionHelpText": "異常値検出の前に異常であると想定されるデータセットの比率を設定します。", "xpack.ml.dataframe.analytics.create.outlierFractionInputAriaLabel": "異常値検出の前に異常であると想定されるデータセットの比率を設定します。", "xpack.ml.dataframe.analytics.create.outlierFractionLabel": "異常値割合", @@ -10664,8 +12119,11 @@ "xpack.ml.dataframe.analytics.create.randomizeSeedInputAriaLabel": "学習で使用されるドキュメントを選択するために使用される乱数生成器のシード", "xpack.ml.dataframe.analytics.create.randomizeSeedLabel": "シードのランダム化", "xpack.ml.dataframe.analytics.create.randomizeSeedText": "学習で使用されるドキュメントを選択するために使用される乱数生成器のシード。", + "xpack.ml.dataframe.analytics.create.regressionHelpText": "回帰はデータセットにおける数値を予測します。", + "xpack.ml.dataframe.analytics.create.regressionTitle": "回帰", "xpack.ml.dataframe.analytics.create.requiredFieldsError": "無効です。 {message}", "xpack.ml.dataframe.analytics.create.resultsFieldHelpText": "分析の結果を格納するフィールドの名前を定義します。デフォルトはmlです。", + "xpack.ml.dataframe.analytics.create.resultsFieldInputAriaLabel": "分析の結果を格納するフィールドの名前。", "xpack.ml.dataframe.analytics.create.resultsFieldLabel": "結果フィールド", "xpack.ml.dataframe.analytics.create.savedSearchLabel": "保存検索", "xpack.ml.dataFrame.analytics.create.searchSelection.notFoundLabel": "一致インデックスまたは保存した検索が見つかりません。", @@ -10687,7 +12145,11 @@ "xpack.ml.dataframe.analytics.create.switchToJsonEditorSwitch": "JSONエディターに切り替える", "xpack.ml.dataframe.analytics.create.trainingPercentHelpText": "学習で使用可能なドキュメントの割合を定義します。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "トレーニングパーセンテージ", + "xpack.ml.dataframe.analytics.create.unableToFetchExplainDataMessage": "分析フィールドデータの取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.unsupportedFieldsError": "無効です。 {message}", + "xpack.ml.dataframe.analytics.create.UseResultsFieldDefaultLabel": "結果フィールドデフォルト値「{defaultValue}」を使用", + "xpack.ml.dataframe.analytics.create.viewResultsCardDescription": "分析ジョブの結果を表示します。", + "xpack.ml.dataframe.analytics.create.viewResultsCardTitle": "結果を表示", "xpack.ml.dataframe.analytics.create.wizardCreateButton": "作成", "xpack.ml.dataframe.analytics.create.wizardStartCheckbox": "即時開始", "xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage": "依存変数のほかに、1つ以上のフィールドを分析に含める必要があります。", @@ -10698,6 +12160,8 @@ "xpack.ml.dataframe.analytics.creation.detailsStepTitle": "ジョブの詳細", "xpack.ml.dataframe.analytics.creationPageSourceIndexTitle": "ソースインデックスパターン:{indexTitle}", "xpack.ml.dataframe.analytics.creationPageTitle": "ジョブを作成", + "xpack.ml.dataframe.analytics.decisionPathFeatureBaselineTitle": "ベースライン", + "xpack.ml.dataframe.analytics.decisionPathFeatureOtherTitle": "その他", "xpack.ml.dataframe.analytics.errorCallout.evaluateErrorTitle": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.errorCallout.generalErrorTitle": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.errorCallout.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。ジョブが完了済みで、インデックスにドキュメントがあることを確認してください。", @@ -10705,13 +12169,43 @@ "xpack.ml.dataframe.analytics.errorCallout.noIndexCalloutBody": "インデックスのクエリが結果を返しませんでした。デスティネーションインデックスが存在し、ドキュメントがあることを確認してください。", "xpack.ml.dataframe.analytics.errorCallout.queryParsingErrorBody": "クエリ構文が無効であり、結果を返しませんでした。クエリ構文を確認し、再試行してください。", "xpack.ml.dataframe.analytics.errorCallout.queryParsingErrorTitle": "クエリをパースできません。", + "xpack.ml.dataframe.analytics.exploration.analysisDestinationIndexLabel": "デスティネーションインデックス", + "xpack.ml.dataframe.analytics.exploration.analysisSectionTitle": "分析", + "xpack.ml.dataframe.analytics.exploration.analysisSourceIndexLabel": "ソースインデックス", + "xpack.ml.dataframe.analytics.exploration.analysisTypeLabel": "型", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "機能影響スコア", + "xpack.ml.dataframe.analytics.exploration.explorationTableTitle": "結果", + "xpack.ml.dataframe.analytics.exploration.explorationTableTotalDocsLabel": "合計ドキュメント数", + "xpack.ml.dataframe.analytics.exploration.featureImportanceDocsLink": "特徴量の重要度ドキュメント", + "xpack.ml.dataframe.analytics.exploration.featureImportanceSummaryTitle": "合計特徴量の重要度", + "xpack.ml.dataframe.analytics.exploration.featureImportanceSummaryTooltipContent": "合計特徴量の重要度値は、すべての学習データでどの程度フィールドが予測に影響するのかを示します。", + "xpack.ml.dataframe.analytics.exploration.featureImportanceXAxisTitle": "特徴量の重要度平均大きさ", + "xpack.ml.dataframe.analytics.exploration.featureImportanceYSeriesName": "大きさ", + "xpack.ml.dataframe.analytics.exploration.noTotalFeatureImportanceCalloutMessage": "合計特徴量の重要度データは使用できません。データセットが均一であり、特徴量は予測に有意な影響を与えません。", + "xpack.ml.dataframe.analytics.exploration.querySyntaxError": "インデックスデータの読み込み中にエラーが発生しました。クエリ構文が有効であることを確認してください。", + "xpack.ml.dataframe.analytics.explorationQueryBar.buttonGroupLegend": "分析クエリバーフィルターボタン", + "xpack.ml.dataframe.analytics.explorationResults.baselineErrorMessageToast": "昨日重要度ベースラインの取得中にエラーが発生しました", + "xpack.ml.dataframe.analytics.explorationResults.classificationDecisionPathClassNameTitle": "クラス名", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathBaselineText": "ベースライン(学習データセットのすべてのデータポイントの予測の平均)", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathJSONTab": "JSON", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathLineTitle": "予測", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotHelpText": "SHAP決定プロットは{linkedFeatureImportanceValues}を使用して、モデルがどのように「{predictionFieldName}」の予測値に到達するのかを示します。", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotTab": "決定プロット", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathXAxisTitle": "「{predictionFieldName}」の予測", "xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText": "予測があるドキュメントを示す", "xpack.ml.dataframe.analytics.explorationResults.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", + "xpack.ml.dataframe.analytics.explorationResults.linkedFeatureImportanceValues": "特徴量の重要度値", + "xpack.ml.dataframe.analytics.explorationResults.missingBaselineCallout": "ベースライン値を計算できません。これによりシフトされた決定パスになる可能性があります。", + "xpack.ml.dataframe.analytics.explorationResults.regressionDecisionPathDataMissingCallout": "決定パスデータがありません。", + "xpack.ml.dataframe.analytics.explorationResults.testingSubsetLabel": "テスト", + "xpack.ml.dataframe.analytics.explorationResults.trainingSubsetLabel": "トレーニング", "xpack.ml.dataframe.analytics.indexPatternPromptLinkText": "インデックスパターンを作成します", "xpack.ml.dataframe.analytics.indexPatternPromptMessage": "{destIndex}のインデックス{destIndex}. {linkToIndexPatternManagement}にはインデックスパターンが存在しません。", "xpack.ml.dataframe.analytics.jobCaps.errorTitle": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.jobConfig.errorTitle": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", + "xpack.ml.dataframe.analytics.regressionExploration.evaluateNoTestingDocsError": "テストドキュメントが見つかりません", + "xpack.ml.dataframe.analytics.regressionExploration.evaluateNoTrainingDocsError": "トレーニングドキュメントが見つかりません", + "xpack.ml.dataframe.analytics.regressionExploration.evaluateSectionTitle": "モデル評価", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー", "xpack.ml.dataframe.analytics.regressionExploration.generalizationFilterText": ".学習データをフィルタリングしています。", @@ -10729,22 +12223,27 @@ "xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle": "トレーニングエラー", "xpack.ml.dataframe.analytics.regressionExploration.trainingFilterText": ".テストデータをフィルタリングしています。", "xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsMessagesLabel": "ジョブメッセージ", + "xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsStatsLabel": "ジョブ統計情報", + "xpack.ml.dataframe.analyticsList.cloneActionNameText": "クローンを作成", "xpack.ml.dataframe.analyticsList.cloneActionPermissionTooltip": "分析ジョブを複製する権限がありません。", "xpack.ml.dataframe.analyticsList.completeBatchAnalyticsToolTip": "{analyticsId}は完了済みの分析ジョブで、再度開始できません。", "xpack.ml.dataframe.analyticsList.createDataFrameAnalyticsButton": "ジョブを作成", "xpack.ml.dataframe.analyticsList.deleteActionDisabledToolTipContent": "削除するにはデータフレーム分析ジョブを停止してください。", + "xpack.ml.dataframe.analyticsList.deleteActionNameText": "削除", "xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage": "データフレーム分析ジョブ{analyticsId}の削除中にエラーが発生しました。", "xpack.ml.dataframe.analyticsList.deleteAnalyticsPrivilegeErrorMessage": "ユーザーはインデックス{indexName}を削除する権限がありません。{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage": "データフレーム分析ジョブ{analyticsId}の削除リクエストが受け付けられました。", + "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexErrorMessage": "ディスティネーションインデックス{destinationIndex}の削除中にエラーが発生しました", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternErrorMessage": "インデックスパターン{destinationIndex}の削除中にエラーが発生しました。{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternSuccessMessage": "インデックスパターン{destinationIndex}を削除する要求が確認されました。", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage": "ディスティネーションインデックス{destinationIndex}を削除する要求が確認されました。", "xpack.ml.dataframe.analyticsList.deleteDestinationIndexTitle": "ディスティネーションインデックス{indexName}を削除", "xpack.ml.dataframe.analyticsList.deleteModalCancelButton": "キャンセル", "xpack.ml.dataframe.analyticsList.deleteModalDeleteButton": "削除", - "xpack.ml.dataframe.analyticsList.deleteModalTitle": "{analyticsId}の削除", + "xpack.ml.dataframe.analyticsList.deleteModalTitle": "{analyticsId}を削除しますか?", "xpack.ml.dataframe.analyticsList.description": "説明", "xpack.ml.dataframe.analyticsList.destinationIndex": "デスティネーションインデックス", + "xpack.ml.dataframe.analyticsList.editActionNameText": "編集", "xpack.ml.dataframe.analyticsList.editActionPermissionTooltip": "分析ジョブを編集する権限がありません。", "xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartAriaLabel": "lazy startの許可を更新します。", "xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartFalseValue": "False", @@ -10764,11 +12263,12 @@ "xpack.ml.dataframe.analyticsList.editFlyoutSuccessMessage": "分析ジョブ{jobId}が更新されました。", "xpack.ml.dataframe.analyticsList.editFlyoutTitle": "{jobId}の編集", "xpack.ml.dataframe.analyticsList.editFlyoutUpdateButtonText": "更新", - "xpack.ml.dataFrame.analyticsList.emptyPromptButtonText": "最初のデータフレーム分析ジョブを作成", - "xpack.ml.dataFrame.analyticsList.emptyPromptTitle": "データフレーム分析ジョブが見つかりませんでした", + "xpack.ml.dataFrame.analyticsList.emptyPromptButtonText": "ジョブを作成", + "xpack.ml.dataFrame.analyticsList.emptyPromptTitle": "最初のデータフレーム分析ジョブを作成", "xpack.ml.dataFrame.analyticsList.errorPromptTitle": "データフレーム分析リストの取得中にエラーが発生しました。", "xpack.ml.dataframe.analyticsList.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage": "インデックスパターン{indexPattern}が存在するかどうかを確認するときにエラーが発生しました。{error}", "xpack.ml.dataframe.analyticsList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage": "ユーザーが{destinationIndex}を削除できるかどうかを確認するときにエラーが発生しました。{error}", + "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.analysisStats": "分析統計情報", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.phase": "フェーズ", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.progress": "進捗", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state": "ステータス", @@ -10777,6 +12277,12 @@ "xpack.ml.dataframe.analyticsList.experimentalBadgeLabel": "実験的", "xpack.ml.dataframe.analyticsList.experimentalBadgeTooltipContent": "データフレーム分析は実験段階の機能です。フィードバックをお待ちしています。", "xpack.ml.dataframe.analyticsList.fetchSourceIndexPatternForCloneErrorMessage": "インデックスパターン{indexPattern}が存在するかどうかを確認するときにエラーが発生しました。{error}", + "xpack.ml.dataframe.analyticsList.forceStopModalBody": "{analyticsId}は失敗状態です。ジョブを停止して、エラーを修正する必要があります。", + "xpack.ml.dataframe.analyticsList.forceStopModalCancelButton": "キャンセル", + "xpack.ml.dataframe.analyticsList.forceStopModalStartButton": "強制停止", + "xpack.ml.dataframe.analyticsList.forceStopModalTitle": "このジョブを強制的に停止しますか?", + "xpack.ml.dataframe.analyticsList.memoryStatus": "メモリー状態", + "xpack.ml.dataframe.analyticsList.noSourceIndexPatternForClone": "分析ジョブを複製できません。インデックス{indexPattern}のインデックスパターンが存在しません。", "xpack.ml.dataframe.analyticsList.progress": "進捗", "xpack.ml.dataframe.analyticsList.progressOfPhase": "フェーズ{currentPhase}の進捗: {progress}%", "xpack.ml.dataframe.analyticsList.refreshButtonLabel": "更新", @@ -10784,18 +12290,20 @@ "xpack.ml.dataframe.analyticsList.rowExpand": "{analyticsId}の詳細を表示", "xpack.ml.dataframe.analyticsList.showDetailsColumn.screenReaderDescription": "このカラムには各ジョブの詳細を示すクリック可能なコントロールが含まれます", "xpack.ml.dataframe.analyticsList.sourceIndex": "ソースインデックス", + "xpack.ml.dataframe.analyticsList.startActionNameText": "開始", "xpack.ml.dataframe.analyticsList.startAnalyticsErrorTitle": "ジョブの開始エラー", "xpack.ml.dataframe.analyticsList.startAnalyticsSuccessMessage": "データフレーム分析 {analyticsId} の開始リクエストが受け付けられました。", - "xpack.ml.dataframe.analyticsList.startModalBody": "データフレーム分析ジョブは、クラスターの検索とインデックスによる負荷を増やします。過剰な負荷が生じた場合は分析ジョブを停止してください。この分析ジョブを開始してよろしいですか?", + "xpack.ml.dataframe.analyticsList.startModalBody": "データフレーム分析ジョブは、クラスターの検索とインデックスによる負荷を増やします。負荷が過剰になった場合は、ジョブを停止してください。", "xpack.ml.dataframe.analyticsList.startModalCancelButton": "キャンセル", "xpack.ml.dataframe.analyticsList.startModalStartButton": "開始", - "xpack.ml.dataframe.analyticsList.startModalTitle": "{analyticsId}の開始", + "xpack.ml.dataframe.analyticsList.startModalTitle": "{analyticsId}を開始しますか?", "xpack.ml.dataframe.analyticsList.status": "ステータス", "xpack.ml.dataframe.analyticsList.statusFilter": "ステータス", + "xpack.ml.dataframe.analyticsList.stopActionNameText": "終了", "xpack.ml.dataframe.analyticsList.stopAnalyticsErrorMessage": "データフレーム分析{analyticsId}の停止中にエラーが発生しました。{error}", "xpack.ml.dataframe.analyticsList.stopAnalyticsSuccessMessage": "データフレーム分析 {analyticsId} の停止リクエストが受け付けられました。", "xpack.ml.dataframe.analyticsList.tableActionLabel": "アクション", - "xpack.ml.dataframe.analyticsList.title": "データフレーム分析ジョブ", + "xpack.ml.dataframe.analyticsList.title": "データフレーム分析", "xpack.ml.dataframe.analyticsList.type": "タイプ", "xpack.ml.dataframe.analyticsList.typeFilter": "タイプ", "xpack.ml.dataframe.analyticsList.viewActionJobFailedToolTipContent": "データフレーム分析ジョブに失敗しました。結果ページがありません。", @@ -10803,13 +12311,17 @@ "xpack.ml.dataframe.analyticsList.viewActionJobNotStartedToolTipContent": "データフレーム分析ジョブが開始しませんでした。結果ページがありません。", "xpack.ml.dataframe.analyticsList.viewActionName": "表示", "xpack.ml.dataframe.analyticsList.viewActionUnknownJobTypeToolTipContent": "このタイプのデータフレーム分析ジョブでは、結果ページがありません。", + "xpack.ml.dataframe.jobsTabLabel": "ジョブ", + "xpack.ml.dataframe.modelsTabLabel": "モデル", "xpack.ml.dataframe.stepCreateForm.createDataFrameAnalyticsSuccessMessage": "データフレーム分析 {jobId} の作成リクエストが受け付けられました。", "xpack.ml.dataframe.stepDetailsForm.destinationIndexInvalidErrorLink": "インデックス名の制限に関する詳細。", "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel": "探索", "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel": "ジョブ管理", "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel": "データフレーム分析", "xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel": "インデックス", + "xpack.ml.dataFrameAnalyticsBreadcrumbs.modelsListLabel": "モデル管理", "xpack.ml.dataFrameAnalyticsLabel": "データフレーム分析", + "xpack.ml.dataFrameAnalyticsTabLabel": "データフレーム分析", "xpack.ml.dataGrid.columnChart.ErrorMessageToast": "ヒストグラムデータの取得でエラーが発生しました。{error}", "xpack.ml.dataGrid.dataGridNoDataCalloutTitle": "インデックスプレビューを利用できません", "xpack.ml.dataGrid.histogramButtonText": "ヒストグラム", @@ -10863,6 +12375,7 @@ "xpack.ml.datavisualizer.startTrial.subscriptionsLinkText": "PlatinumまたはEnterprisesサブスクリプション", "xpack.ml.datavisualizerBreadcrumbLabel": "データビジュアライザー", "xpack.ml.dataVisualizerPageLabel": "データビジュアライザー", + "xpack.ml.dataVisualizerTabLabel": "データビジュアライザー", "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage": "メッセージを読み込めませんでした", "xpack.ml.editModelSnapshotFlyout.calloutText": "これはジョブ{jobId}で使用されている現在のスナップショットであるため削除できません。", "xpack.ml.editModelSnapshotFlyout.calloutTitle": "現在のスナップショット", @@ -10896,6 +12409,7 @@ "xpack.ml.explorer.charts.openInSingleMetricViewerButtonLabel": "シングルメトリックビューアーで開く", "xpack.ml.explorer.charts.tooManyBucketsDescription": "この選択は、表示するバケット数が多すぎます。ダッシュボードは短い時間範囲で表示するのが最適です。", "xpack.ml.explorer.charts.viewLabel": "表示", + "xpack.ml.explorer.clearSelectionLabel": "選択した項目をクリア", "xpack.ml.explorer.createNewJobLinkText": "ジョブを作成", "xpack.ml.explorer.dashboardsTable.addAndEditDashboardLabel": "ダッシュボードの追加と編集", "xpack.ml.explorer.dashboardsTable.addToDashboardLabel": "ダッシュボードに追加", @@ -10912,6 +12426,7 @@ "xpack.ml.explorer.intervalLabel": "間隔", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable": "クエリバーに無効な構文。インプットは有効な Kibana クエリ言語 (KQL) でなければなりません", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageQueryBar": "無効なクエリ", + "xpack.ml.explorer.invalidTimeRangeInUrlCallout": "無効なデフォルト時間フィルターのため、時間フィルターが全範囲に変更されました。{field}の詳細設定を確認してください。", "xpack.ml.explorer.jobIdLabel": "ジョブ ID", "xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(すべての影響因子のジョブスコア)", "xpack.ml.explorer.kueryBar.filterPlaceholder": "影響因子フィールドでフィルタリング… ({queryExample})", @@ -10934,6 +12449,7 @@ "xpack.ml.explorer.singleMetricChart.valueWithoutAnomalyScoreLabel": "値", "xpack.ml.explorer.sortedByMaxAnomalyScoreForTimeFormattedLabel": "({viewByLoadedForTimeFormatted} の最高異常スコアで分類)", "xpack.ml.explorer.sortedByMaxAnomalyScoreLabel": "(最高異常スコアで分類)", + "xpack.ml.explorer.stoppedPartitionsExistCallout": "stop_on_warnがオンであるため、想定される結果よりも少ない可能性があります。分類ステータスが警告に変更された{jobsWithStoppedPartitions, plural, one {ジョブ} other {ジョブ}} [{stoppedPartitions}]の一部のパーティションでは、分類と後続の異常検知の両方が停止しました。", "xpack.ml.explorer.swimlane.maxAnomalyScoreLabel": "最高異常スコア", "xpack.ml.explorer.swimlaneActions": "アクション", "xpack.ml.explorer.swimLanePagination": "異常スイムレーンページネーション", @@ -10994,6 +12510,7 @@ "xpack.ml.fieldTypeIcon.unknownTypeAriaLabel": "不明なタイプ", "xpack.ml.fileDatavisualizer.aboutPanel.analyzingDataTitle": "データを分析中", "xpack.ml.fileDatavisualizer.aboutPanel.selectOrDragAndDropFileDescription": "ファイルを選択するかドラッグ & ドロップしてください", + "xpack.ml.fileDatavisualizer.addCombinedFieldsLabel": "結合されたフィールドを追加", "xpack.ml.fileDatavisualizer.advancedImportSettings.createIndexPatternLabel": "インデックスパターンを作成", "xpack.ml.fileDatavisualizer.advancedImportSettings.indexNameAriaLabel": "インデックス名、必須フィールド", "xpack.ml.fileDatavisualizer.advancedImportSettings.indexNameLabel": "インデックス名", @@ -11015,6 +12532,11 @@ "xpack.ml.fileDatavisualizer.bottomBar.missingImportPrivilegesMessage": "データインポートを有効にするには、ingest_adminロールが必要です", "xpack.ml.fileDatavisualizer.bottomBar.readMode.cancelButtonLabel": "キャンセル", "xpack.ml.fileDatavisualizer.bottomBar.readMode.importButtonLabel": "インポート", + "xpack.ml.fileDatavisualizer.combinedFieldsForm.mappingsParseError": "マッピングのパース中にエラーが発生しました:{error}", + "xpack.ml.fileDatavisualizer.combinedFieldsForm.pipelineParseError": "パイプラインのパース中にエラーが発生しました:{error}", + "xpack.ml.fileDatavisualizer.combinedFieldsLabel": "結合されたフィールド", + "xpack.ml.fileDatavisualizer.combinedFieldsReadOnlyHelpTextLabel": "詳細タグで結合されたフィールドを編集", + "xpack.ml.fileDatavisualizer.combinedFieldsReadOnlyLabel": "結合されたフィールド", "xpack.ml.fileDatavisualizer.editFlyout.applyOverrideSettingsButtonLabel": "適用", "xpack.ml.fileDatavisualizer.editFlyout.closeOverrideSettingsButtonLabel": "閉じる", "xpack.ml.fileDatavisualizer.editFlyout.overrides.customDelimiterFormRowLabel": "カスタム区切り記号", @@ -11054,11 +12576,19 @@ "xpack.ml.fileDatavisualizer.fileContents.fileContentsTitle": "ファイルコンテンツ", "xpack.ml.fileDatavisualizer.fileContents.firstLinesDescription": "初めの {numberOfLines, plural, zero {# 行} 1 {# 行} other {# 行}}", "xpack.ml.fileDatavisualizer.fileDatavisualizerView.xmlNotCurrentlySupportedErrorMessage": "XML は現在サポートされていません", - "xpack.ml.fileDatavisualizer.fileErrorCallouts.fileCouldNotBeReadTitle": "ファイルを読み込めませんでした", + "xpack.ml.fileDatavisualizer.fileErrorCallouts.applyOverridesDescription": "ファイル形式やタイムスタンプ形式などこのデータに関する何らかの情報がある場合は、初期オーバーライドを追加すると、残りの構造を推論するのに役立つことがあります。", + "xpack.ml.fileDatavisualizer.fileErrorCallouts.fileCouldNotBeReadTitle": "ファイル構造を決定できません", "xpack.ml.fileDatavisualizer.fileErrorCallouts.fileSizeExceedsAllowedSizeByDiffFormatErrorMessage": "アップロードするよう選択されたファイルのサイズが {diffFormatted} に許可された最大サイズの {maxFileSizeFormatted} を超えています", "xpack.ml.fileDatavisualizer.fileErrorCallouts.fileSizeExceedsAllowedSizeErrorMessage": "アップロードするよう選択されたファイルのサイズは {fileSizeFormatted} で、許可された最大サイズの {maxFileSizeFormatted} を超えています。", "xpack.ml.fileDatavisualizer.fileErrorCallouts.fileSizeTooLargeTitle": "ファイルサイズが大きすぎます。", + "xpack.ml.fileDatavisualizer.fileErrorCallouts.overrideButton": "上書き設定を適用", "xpack.ml.fileDatavisualizer.fileErrorCallouts.revertingToPreviousSettingsDescription": "以前の設定に戻しています。", + "xpack.ml.fileDatavisualizer.geoPointCombinedFieldLabel": "地理ポイントフィールドを追加", + "xpack.ml.fileDatavisualizer.geoPointForm.geoPointFieldAriaLabel": "地理ポイントフィールド、必須フィールド", + "xpack.ml.fileDatavisualizer.geoPointForm.geoPointFieldLabel": "地理ポイントフィールド", + "xpack.ml.fileDatavisualizer.geoPointForm.latFieldLabel": "緯度フィールド", + "xpack.ml.fileDatavisualizer.geoPointForm.lonFieldLabel": "経度フィールド", + "xpack.ml.fileDatavisualizer.geoPointForm.submitButtonLabel": "追加", "xpack.ml.fileDatavisualizer.importErrors.checkingPermissionErrorMessage": "パーミッションエラーをインポートします", "xpack.ml.fileDatavisualizer.importErrors.creatingIndexErrorMessage": "インデックスの作成中にエラーが発生しました", "xpack.ml.fileDatavisualizer.importErrors.creatingIndexPatternErrorMessage": "インデックスパターンの作成中にエラーが発生しました", @@ -11113,6 +12643,8 @@ "xpack.ml.fileDatavisualizer.importView.parsePipelineError": "投入パイプラインのパース中にエラーが発生しました:", "xpack.ml.fileDatavisualizer.importView.parseSettingsError": "設定のパース中にエラーが発生しました:", "xpack.ml.fileDatavisualizer.importView.resetButtonLabel": "リセット", + "xpack.ml.fileDatavisualizer.nameCollisionMsg": "「{name}」はすでに存在します。一意の名前を入力してください。", + "xpack.ml.fileDatavisualizer.removeCombinedFieldsLabel": "結合されたフィールドを削除", "xpack.ml.fileDatavisualizer.resultsLinks.createNewMLJobTitle": "新規 ML ジョブの作成", "xpack.ml.fileDatavisualizer.resultsLinks.fileBeatConfig": "Filebeat 構成を作成", "xpack.ml.fileDatavisualizer.resultsLinks.fileBeatConfigBottomText": "{password} が {user} ユーザーのパスワードである場合、{esUrl} は Elasticsearch の URL です。", @@ -11140,6 +12672,8 @@ "xpack.ml.fileDatavisualizer.welcomeContent.uploadedFilesAllowedSizeDescription": "最大{maxFileSize}のファイルをアップロードできます。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileDescription": "ファイルデータビジュアライザーは、ログファイルのフィールドとメトリックの理解に役立ちます。ファイルをアップロードして、データを分析し、 Elasticsearch インデックスにインポートするか選択できます。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileTitle": "ログファイルのデータを可視化 {experimentalBadge}", + "xpack.ml.fileDataVisualizerDescription": "CSV、NDJSON、またはログファイルをインポートします。", + "xpack.ml.fileDataVisualizerTitle": "ファイルをアップロード", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "実際値が通常値と同じ", "xpack.ml.formatters.metricChangeDescription.moreThan100xHigherDescription": "100x よりも高い", "xpack.ml.formatters.metricChangeDescription.moreThan100xLowerDescription": "100x よりも低い", @@ -11233,8 +12767,8 @@ "xpack.ml.jobsList.deleteJobModal.cancelButtonLabel": "キャンセル", "xpack.ml.jobsList.deleteJobModal.closeButtonLabel": "閉じる", "xpack.ml.jobsList.deleteJobModal.deleteButtonLabel": "削除", - "xpack.ml.jobsList.deleteJobModal.deleteJobsTitle": "{jobsCount, plural, one {{jobId}} other {# 件のジョブ}}を削除", - "xpack.ml.jobsList.deleteJobModal.deleteMultipleJobsDescription": "{jobsCount, plural, one {ジョブ} other {複数ジョブ}}の削除には時間がかかる場合があります。{jobsCount, plural, one {} other {}}バックグラウンドで削除され、ジョブリストからすぐに消えない場合があります", + "xpack.ml.jobsList.deleteJobModal.deleteJobsTitle": "{jobsCount, plural, one {{jobId}} other {# 件のジョブ}}を削除しますか?", + "xpack.ml.jobsList.deleteJobModal.deleteMultipleJobsDescription": "{jobsCount, plural, one {ジョブ} other {複数ジョブ}}の削除には時間がかかる場合があります。{jobsCount, plural, one {} other {}}バックグラウンドで削除され、ジョブリストからすぐに消えない場合があります。", "xpack.ml.jobsList.deleteJobModal.deletingJobsStatusLabel": "ジョブを削除中", "xpack.ml.jobsList.descriptionLabel": "説明", "xpack.ml.jobsList.editJobFlyout.changesNotSavedNotificationMessage": "{jobId} への変更を保存できませんでした", @@ -11335,6 +12869,7 @@ "xpack.ml.jobsList.managementActions.deleteJobLabel": "ジョブを削除", "xpack.ml.jobsList.managementActions.editJobDescription": "ジョブを編集します", "xpack.ml.jobsList.managementActions.editJobLabel": "ジョブを編集", + "xpack.ml.jobsList.managementActions.noSourceIndexPatternForClone": "異常検知ジョブ{jobId}を複製できません。インデックス{indexPatternTitle}のインデックスパターンが存在しません。", "xpack.ml.jobsList.managementActions.startDatafeedDescription": "データフィードを開始します", "xpack.ml.jobsList.managementActions.startDatafeedLabel": "データフィードを開始", "xpack.ml.jobsList.managementActions.stopDatafeedDescription": "データフィードを停止します", @@ -11396,6 +12931,7 @@ "xpack.ml.jobsList.title": "異常検知ジョブ", "xpack.ml.machineLearningBreadcrumbLabel": "機械学習", "xpack.ml.machineLearningDescription": "時系列データから通常の動作を自動的に学習し、異常を検知します。", + "xpack.ml.machineLearningSubtitle": "モデリング、予測、検出を行います。", "xpack.ml.machineLearningTitle": "機械学習", "xpack.ml.management.jobsList.accessDeniedTitle": "アクセスが拒否されました", "xpack.ml.management.jobsList.analyticsDocsLabel": "分析ジョブドキュメント", @@ -11445,6 +12981,8 @@ "xpack.ml.models.jobValidation.messages.cardinalityPartitionFieldMessage": "{fieldName} の基数が 1000 を超えているため、メモリーの使用量が大きくなる可能性があります。", "xpack.ml.models.jobValidation.messages.categorizationFiltersInvalidMessage": "カテゴリー分けフィルターの構成が無効です。フィルターが有効な正規表現で {categorizationFieldName} が設定されていることを確認してください。", "xpack.ml.models.jobValidation.messages.categorizationFiltersValidMessage": "カテゴリー分けフィルターチェックが合格しました。", + "xpack.ml.models.jobValidation.messages.categorizerMissingPerPartitionFieldMessage": "パーティション単位の分類が有効であるときに、「mlcategory」を参照する検出器のパーティションフィールドを設定する必要があります。", + "xpack.ml.models.jobValidation.messages.categorizerVaryingPerPartitionFieldNamesMessage": "パーティション単位の分類が有効であるときには、キーワード「mlcategory」の検出器に別のpartition_field_nameを設定できません。[{fields}]が見つかりました。", "xpack.ml.models.jobValidation.messages.detectorsDuplicatesMessage": "重複する検知器が検出されました。{functionParam}、{fieldNameParam}、{byFieldNameParam}、{overFieldNameParam}、{partitionFieldNameParam} の構成の組み合わせが同じ検知器は、同じジョブで使用できません。", "xpack.ml.models.jobValidation.messages.detectorsEmptyMessage": "重複する検知器は検出されませんでした。検知器を少なくとも 1 つ指定する必要があります。", "xpack.ml.models.jobValidation.messages.detectorsFunctionEmptyMessage": "検知器の関数が 1 つも入力されていません。", @@ -11518,6 +13056,7 @@ "xpack.ml.navMenu.anomalyDetectionTabLinkText": "異常検知", "xpack.ml.navMenu.dataFrameAnalyticsTabLinkText": "分析", "xpack.ml.navMenu.dataVisualizerTabLinkText": "データビジュアライザー", + "xpack.ml.navMenu.mlAppNameText": "機械学習", "xpack.ml.navMenu.overviewTabLinkText": "概要", "xpack.ml.navMenu.settingsTabLinkText": "設定", "xpack.ml.newJob.page.createJob": "ジョブを作成", @@ -11600,6 +13139,11 @@ "xpack.ml.newJob.wizard.datafeedStep.query.title": "Elasticsearch クエリ", "xpack.ml.newJob.wizard.datafeedStep.queryDelay.description": "現在の時刻と最新のインプットデータ時刻の間の秒単位での遅延です。", "xpack.ml.newJob.wizard.datafeedStep.queryDelay.title": "クエリの遅延", + "xpack.ml.newJob.wizard.datafeedStep.resetQueryButton": "データフィードクエリをデフォルトにリセット", + "xpack.ml.newJob.wizard.datafeedStep.resetQueryConfirm.cancel": "キャンセル", + "xpack.ml.newJob.wizard.datafeedStep.resetQueryConfirm.confirm": "確認", + "xpack.ml.newJob.wizard.datafeedStep.resetQueryConfirm.description": "データフィードクエリをデフォルトに設定します。", + "xpack.ml.newJob.wizard.datafeedStep.resetQueryConfirm.title": "データフィードクエリをリセット", "xpack.ml.newJob.wizard.datafeedStep.scrollSize.description": "検索ごとにリクエストするドキュメントの最高数です。", "xpack.ml.newJob.wizard.datafeedStep.scrollSize.title": "スクロールサイズ", "xpack.ml.newJob.wizard.datafeedStep.timeField.description": "インデックスパターンのデフォルトの時間フィールドは自動的に選択されますが、上書きできます。", @@ -11607,6 +13151,9 @@ "xpack.ml.newJob.wizard.editCategorizationAnalyzerFlyoutButton": "カテゴリー分けアナライザーを編集", "xpack.ml.newJob.wizard.editJsonButton": "JSON を編集", "xpack.ml.newJob.wizard.estimateModelMemoryError": "モデルメモリ上限を計算できませんでした", + "xpack.ml.newJob.wizard.extraStep.categorizationJob.categorizationPerPartitionFieldLabel": "パーティションフィールド", + "xpack.ml.newJob.wizard.extraStep.categorizationJob.perPartitionCategorizationLabel": "パーティション単位の分類を有効にする", + "xpack.ml.newJob.wizard.extraStep.categorizationJob.stopOnWarnLabel": "警告時に停止する", "xpack.ml.newJob.wizard.jobCreatorTitle.advanced": "高度な設定", "xpack.ml.newJob.wizard.jobCreatorTitle.categorization": "カテゴリー分け", "xpack.ml.newJob.wizard.jobCreatorTitle.multiMetric": "マルチメトリック", @@ -11673,9 +13220,15 @@ "xpack.ml.newJob.wizard.jobType.useWizardTitle": "ウィザードを使用", "xpack.ml.newJob.wizard.jsonFlyout.closeButton": "閉じる", "xpack.ml.newJob.wizard.jsonFlyout.datafeed.title": "データフィード構成 JSON", + "xpack.ml.newJob.wizard.jsonFlyout.indicesChange.calloutText": "ここではデータフィードで使用されているインデックスを変更できません。別のインデックスパターンまたは保存された検索を選択する場合は、ジョブ作成をやり直し、別のインデックスパターンを選択してください。", + "xpack.ml.newJob.wizard.jsonFlyout.indicesChange.calloutTitle": "インデックスが変更されました", "xpack.ml.newJob.wizard.jsonFlyout.job.title": "ジョブ構成 JSON", "xpack.ml.newJob.wizard.jsonFlyout.saveButton": "保存", "xpack.ml.newJob.wizard.nextStepButton": "次へ", + "xpack.ml.newJob.wizard.perPartitionCategorization.enable.description": "パーティション単位の分類が有効な場合は、パーティションフィールドの各値のカテゴリが独立して決定されます。", + "xpack.ml.newJob.wizard.perPartitionCategorization.enable.title": "パーティション単位の分類を有効にする", + "xpack.ml.newJob.wizard.perPartitionCategorizationSwitchLabel": "パーティション単位の分類を有効にする", + "xpack.ml.newJob.wizard.perPartitionCategorizationtopOnWarnSwitchLabel": "警告時に停止する", "xpack.ml.newJob.wizard.pickFieldsStep.addDetectorButton": "ディテクターを追加", "xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorList.deleteButton": "削除", "xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorList.editButton": "編集", @@ -11714,6 +13267,7 @@ "xpack.ml.newJob.wizard.pickFieldsStep.categorizationFieldCalloutTitle.valid": "選択したカテゴリーフィールドは有効です", "xpack.ml.newJob.wizard.pickFieldsStep.categorizationFieldExamples.title": "例", "xpack.ml.newJob.wizard.pickFieldsStep.categorizationFieldOptional.description": "オプション。非構造化ログデータの場合に使用。テキストデータタイプの使用をお勧めします。", + "xpack.ml.newJob.wizard.pickFieldsStep.categorizationStoppedPartitionsTitle": "停止したパーティション", "xpack.ml.newJob.wizard.pickFieldsStep.categorizationTotalCategories": "合計カテゴリー数: {totalCategories}", "xpack.ml.newJob.wizard.pickFieldsStep.detectorTitle.placeholder": "{field} で分割された {title}", "xpack.ml.newJob.wizard.pickFieldsStep.influencers.description": "どのカテゴリーフィールドが結果に影響を与えるか選択します。異常の原因は誰または何だと思いますか?1-3 個の影響因子をお勧めします。", @@ -11732,6 +13286,9 @@ "xpack.ml.newJob.wizard.pickFieldsStep.splitCards.dataSplitBy": "{field} で分割されたデータ", "xpack.ml.newJob.wizard.pickFieldsStep.splitField.description": "分析のパーティションに使用するフィールドを選択します。このフィールドのそれぞれの値は、個々に独立してモデリングされます。", "xpack.ml.newJob.wizard.pickFieldsStep.splitField.title": "フィールドの分割", + "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsErrorCallout": "停止したパーティションのリストの取得中にエラーが発生しました。", + "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsExistCallout": "パーティション単位の分類とstop_on_warn設定が有効です。ジョブ「{jobId}」の一部のパーティションは分類に適さず、さらなる分類または異常検知分析から除外されました。", + "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsPreviewColumnName": "停止したパーティション名", "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.description": "オプション。インプットデータが事前にまとめられている場合に使用、例: \\{docCountParam\\}。", "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.title": "サマリーカウントフィールド", "xpack.ml.newJob.wizard.previewJsonButton": "JSON をプレビュー", @@ -11802,6 +13359,8 @@ "xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeError": "ジョブの開始エラー", "xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeSuccess": "ジョブ {jobId} が開始しました", "xpack.ml.newJob.wizard.summaryStep.resetJobButton": "ジョブをリセット", + "xpack.ml.newJob.wizard.summaryStep.startDatafeedCheckbox": "即時開始", + "xpack.ml.newJob.wizard.summaryStep.startDatafeedCheckboxHelpText": "選択されていない場合、後でジョブからジョブを開始できます。", "xpack.ml.newJob.wizard.summaryStep.timeRange.end.title": "終了", "xpack.ml.newJob.wizard.summaryStep.timeRange.start.title": "開始", "xpack.ml.newJob.wizard.summaryStep.trueLabel": "True", @@ -11810,6 +13369,8 @@ "xpack.ml.newJob.wizard.timeRangeStep.timeRangePicker.endDateLabel": "終了日", "xpack.ml.newJob.wizard.timeRangeStep.timeRangePicker.startDateLabel": "開始日", "xpack.ml.newJob.wizard.validateJob.bucketSpanMustBeSetErrorMessage": "バケットスパンを設定する必要があります", + "xpack.ml.newJob.wizard.validateJob.categorizerMissingPerPartitionFieldMessage": "パーティション単位の分類が有効であるときに、「mlcategory」を参照する検出器のパーティションフィールドを設定する必要があります。", + "xpack.ml.newJob.wizard.validateJob.categorizerVaryingPerPartitionFieldNamesMessage": "パーティション単位の分類が有効であるときには、キーワード「mlcategory」の検出器に別のpartition_field_nameを設定できません。", "xpack.ml.newJob.wizard.validateJob.duplicatedDetectorsErrorMessage": "重複する検知器が検出されました。", "xpack.ml.newJob.wizard.validateJob.frequencyInvalidTimeIntervalFormatErrorMessage": "{value}は有効な期間の形式ではありません。例:{thirtySeconds}、{tenMinutes}、{oneHour}、{sevenDays}。また、0よりも大きい数字である必要があります。", "xpack.ml.newJob.wizard.validateJob.groupNameAlreadyExists": "グループ ID が既に存在します。グループ ID は既存のジョブやグループと同じにできません。", @@ -11822,6 +13383,8 @@ "xpack.ml.newJob.wizard.validateJob.modelMemoryLimitUnitsInvalidErrorMessage": "モデルメモリー制限のデータユニットが認識されません。{str} でなければなりません", "xpack.ml.newJob.wizard.validateJob.queryCannotBeEmpty": "データフィードクエリは未入力のままにできません。", "xpack.ml.newJob.wizard.validateJob.queryIsInvalidEsQuery": "データフィードクエリは有効な Elasticsearch クエリでなければなりません。", + "xpack.ml.overview.analytics.resultActions.openJobText": "ジョブ結果を表示", + "xpack.ml.overview.analytics.viewActionName": "表示", "xpack.ml.overview.analyticsList.createFirstJobMessage": "最初のデータフレーム分析ジョブを作成", "xpack.ml.overview.analyticsList.createJobButtonText": "ジョブを作成", "xpack.ml.overview.analyticsList.emptyPromptText": "データフレーム分析では、データに対して異常値検出、回帰、分類分析を実行し、結果に注釈を付けることができます。ジョブは注釈付きデータと共に、ソースデータのコピーを新規インデックスに保存します。", @@ -11851,10 +13414,12 @@ "xpack.ml.overview.anomalyDetection.tableMaxScoreErrorTooltip": "最高異常スコアの読み込み中に問題が発生しました", "xpack.ml.overview.anomalyDetection.tableMaxScoreTooltip": "グループ内の 24 時間以内のすべてのジョブの最高スコアです", "xpack.ml.overview.anomalyDetection.tableNumJobs": "グループのジョブ", + "xpack.ml.overview.anomalyDetection.viewActionName": "表示", "xpack.ml.overview.feedbackSectionLink": "オンラインでのフィードバック", "xpack.ml.overview.feedbackSectionText": "ご利用に際し、ご意見やご提案がありましたら、{feedbackLink}までお送りください。", "xpack.ml.overview.feedbackSectionTitle": "フィードバック", "xpack.ml.overview.gettingStartedSectionDocs": "ドキュメンテーション", + "xpack.ml.overview.gettingStartedSectionText": "機械学習へようこそ。はじめに{docs}をご覧になるか、新しいジョブを作成してください。{transforms}を使用して、分析ジョブの機能インデックスを作成することをお勧めします。", "xpack.ml.overview.gettingStartedSectionTitle": "はじめて使う", "xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearchの変換", "xpack.ml.overview.overviewLabel": "概要", @@ -11867,6 +13432,7 @@ "xpack.ml.overviewJobsList.statsBar.failedJobsLabel": "失敗したジョブ", "xpack.ml.overviewJobsList.statsBar.openJobsLabel": "ジョブを開く", "xpack.ml.overviewJobsList.statsBar.totalJobsLabel": "合計ジョブ数", + "xpack.ml.overviewTabLabel": "概要", "xpack.ml.plugin.title": "機械学習", "xpack.ml.privilege.licenseHasExpiredTooltip": "ご使用のライセンスは期限切れです。", "xpack.ml.privilege.noPermission.createCalendarsTooltip": "カレンダーを作成するパーミッションがありません。", @@ -11902,7 +13468,7 @@ "xpack.ml.ruleEditor.deleteRuleModal.cancelButtonLabel": "キャンセル", "xpack.ml.ruleEditor.deleteRuleModal.deleteButtonLabel": "削除", "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleLinkText": "ルールを削除", - "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleTitle": "ルールの削除", + "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleTitle": "ルールを削除しますか?", "xpack.ml.ruleEditor.detectorDescriptionList.detectorTitle": "検知器", "xpack.ml.ruleEditor.detectorDescriptionList.jobIdTitle": "ジョブ ID", "xpack.ml.ruleEditor.detectorDescriptionList.selectedAnomalyDescription": "実際値 {actual}、通常値 {typical}", @@ -11970,7 +13536,7 @@ "xpack.ml.settings.anomalyDetection.createCalendarLink": "作成", "xpack.ml.settings.anomalyDetection.createFilterListsLink": "作成", "xpack.ml.settings.anomalyDetection.filterListsSummaryCount": "{filterListsCountBadge} {filterListsCount, plural, one {個のフィルターリスト} other {個のフィルターリスト}}があります", - "xpack.ml.settings.anomalyDetection.filterListsText": " フィルターリストには、イベントを機械学習分析に含める、または除外するのに使用する値が含まれています。", + "xpack.ml.settings.anomalyDetection.filterListsText": "フィルターリストには、イベントを機械学習分析に含める、または除外するのに使用する値が含まれています。", "xpack.ml.settings.anomalyDetection.filterListsTitle": "フィルターリスト", "xpack.ml.settings.anomalyDetection.loadingCalendarsCountErrorMessage": "カレンダー数の取得中にエラーが発生しました", "xpack.ml.settings.anomalyDetection.loadingFilterListCountErrorMessage": "フィルターリスト数の取得中にエラーが発生しました", @@ -11994,7 +13560,7 @@ "xpack.ml.settings.filterLists.deleteFilterListModal.cancelButtonLabel": "キャンセル", "xpack.ml.settings.filterLists.deleteFilterListModal.confirmButtonLabel": "削除", "xpack.ml.settings.filterLists.deleteFilterListModal.deleteButtonLabel": "削除", - "xpack.ml.settings.filterLists.deleteFilterListModal.modalTitle": "{selectedFilterListsLength, plural, one {{selectedFilterId}} other {# フィルターリスト}}の削除", + "xpack.ml.settings.filterLists.deleteFilterListModal.modalTitle": "{selectedFilterListsLength, plural, one {{selectedFilterId}} other {# フィルターリスト}}を削除しますか?", "xpack.ml.settings.filterLists.deleteFilterLists.deletingErrorMessage": "フィルターリスト {filterListId} の削除中にエラーが発生しました。{respMessage}", "xpack.ml.settings.filterLists.deleteFilterLists.deletingNotificationMessage": "{filterListsToDeleteLength, plural, one {{filterListToDeleteId}} other {# フィルターリスト}}を削除しています", "xpack.ml.settings.filterLists.deleteFilterLists.filtersSuccessfullyDeletedNotificationMessage": "{filterListsToDeleteLength, plural, one {{filterListToDeleteId}} other {# フィルターリスト}} が削除されました", @@ -12032,6 +13598,7 @@ "xpack.ml.settings.filterLists.toolbar.deleteItemButtonLabel": "アイテムを削除", "xpack.ml.settings.title": "設定", "xpack.ml.settingsBreadcrumbLabel": "設定", + "xpack.ml.settingsTabLabel": "設定", "xpack.ml.singleMetricViewerPageLabel": "シングルメトリックビューアー", "xpack.ml.stepDefineForm.invalidQuery": "無効なクエリ", "xpack.ml.stepDefineForm.queryPlaceholderKql": "例: {example}", @@ -12106,6 +13673,7 @@ "xpack.ml.timeSeriesExplorer.forecastsList.viewColumnName": "表示", "xpack.ml.timeSeriesExplorer.forecastsList.viewForecastAriaLabel": "{createdDate} に作成された予測を表示", "xpack.ml.timeSeriesExplorer.intervalLabel": "間隔", + "xpack.ml.timeSeriesExplorer.invalidTimeRangeInUrlCallout": "無効なデフォルト時間フィルターのため、このジョブの時間フィルターが全範囲に変更されました。{field}の詳細設定を確認してください。", "xpack.ml.timeSeriesExplorer.loadingLabel": "読み込み中", "xpack.ml.timeSeriesExplorer.noResultsFoundLabel": "結果が見つかりませんでした", "xpack.ml.timeSeriesExplorer.noSingleMetricJobsFoundLabel": "シングルメトリックジョブが見つかりませんでした", @@ -12149,8 +13717,41 @@ "xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomLabel": "ズーム:", "xpack.ml.timeSeriesExplorer.tryWideningTheTimeSelectionDescription": "時間範囲を広げるか、さらに過去に遡ってみてください。", "xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage": "このダッシュボードでは 1 度に 1 つのジョブしか表示できません", + "xpack.ml.toastNotificationService.errorTitle": "エラーが発生しました", "xpack.ml.tooltips.newJobDedicatedIndexTooltip": "このジョブの結果が別のインデックスに格納されます。", "xpack.ml.tooltips.newJobRecognizerJobPrefixTooltip": "それぞれのジョブIDの頭に付ける接頭辞です。", + "xpack.ml.trainedModels.modelsList.actionsHeader": "アクション", + "xpack.ml.trainedModels.modelsList.collapseRow": "縮小", + "xpack.ml.trainedModels.modelsList.createdAtHeader": "作成日時:", + "xpack.ml.trainedModels.modelsList.deleteModal.cancelButtonLabel": "キャンセル", + "xpack.ml.trainedModels.modelsList.deleteModal.deleteButtonLabel": "削除", + "xpack.ml.trainedModels.modelsList.deleteModal.modelsWithPipelinesWarningMessage": "{modelsWithPipelinesCount, plural, one{モデル} other {モデル}} {modelsWithPipelines} {modelsWithPipelinesCount, plural, one{には} other {には}}パイプラインが関連付けられています。", + "xpack.ml.trainedModels.modelsList.deleteModelActionLabel": "モデルを削除", + "xpack.ml.trainedModels.modelsList.deleteModelsButtonLabel": "削除", + "xpack.ml.trainedModels.modelsList.disableSelectableMessage": "モデルにはパイプラインが関連付けられています", + "xpack.ml.trainedModels.modelsList.expandedRow.analyticsConfigTitle": "分析構成", + "xpack.ml.trainedModels.modelsList.expandedRow.byPipelineTitle": "パイプライン別", + "xpack.ml.trainedModels.modelsList.expandedRow.byProcessorTitle": "プロセッサー別", + "xpack.ml.trainedModels.modelsList.expandedRow.configTabLabel": "構成", + "xpack.ml.trainedModels.modelsList.expandedRow.detailsTabLabel": "詳細", + "xpack.ml.trainedModels.modelsList.expandedRow.detailsTitle": "詳細", + "xpack.ml.trainedModels.modelsList.expandedRow.editPipelineLabel": "編集", + "xpack.ml.trainedModels.modelsList.expandedRow.inferenceConfigTitle": "推論構成", + "xpack.ml.trainedModels.modelsList.expandedRow.inferenceStatsTitle": "推論統計情報", + "xpack.ml.trainedModels.modelsList.expandedRow.ingestStatsTitle": "統計情報を取り込む", + "xpack.ml.trainedModels.modelsList.expandedRow.pipelinesTabLabel": "パイプライン", + "xpack.ml.trainedModels.modelsList.expandedRow.processorsTitle": "プロセッサー", + "xpack.ml.trainedModels.modelsList.expandedRow.statsTabLabel": "統計", + "xpack.ml.trainedModels.modelsList.expandRow": "拡張", + "xpack.ml.trainedModels.modelsList.fetchFailedErrorMessage": "モデルの取り込みが失敗しました", + "xpack.ml.trainedModels.modelsList.fetchModelStatsErrorMessage": "モデル統計情報の取り込みが失敗しました", + "xpack.ml.trainedModels.modelsList.modelIdHeader": "ID", + "xpack.ml.trainedModels.modelsList.selectableMessage": "モデルを選択", + "xpack.ml.trainedModels.modelsList.successfullyDeletedMessage": "{modelsCount, plural, one {モデル{modelsToDeleteIds}} other {#個のモデル}} {modelsCount, plural, one {が} other {が}}正常に削除されました", + "xpack.ml.trainedModels.modelsList.totalAmountLabel": "学習済みモデルの合計数", + "xpack.ml.trainedModels.modelsList.typeHeader": "型", + "xpack.ml.trainedModels.modelsList.unableToDeleteModelsErrorMessage": "モデルを削除できません", + "xpack.ml.trainedModels.modelsList.viewTrainingDataActionLabel": "学習データを表示", "xpack.ml.upgrade.upgradeWarning.upgradeInProgressWarningDescription": "機械学習に関連したインデックスは現在アップグレード中です。", "xpack.ml.upgrade.upgradeWarning.upgradeInProgressWarningDescriptionExtra": "現在いくつかのアクションが利用できません。", "xpack.ml.upgrade.upgradeWarning.upgradeInProgressWarningTitle": "インデックスの移行が進行中です", @@ -12173,6 +13774,12 @@ "xpack.monitoring.ajaxErrorHandler.requestFailedNotification.retryButtonLabel": "再試行", "xpack.monitoring.ajaxErrorHandler.requestFailedNotificationTitle": "監視リクエスト失敗", "xpack.monitoring.alerts.actionGroups.default": "デフォルト", + "xpack.monitoring.alerts.actionVariables.action": "このアラートに対する推奨されるアクション。", + "xpack.monitoring.alerts.actionVariables.actionPlain": "このアラートに推奨されるアクション(Markdownなし)。", + "xpack.monitoring.alerts.actionVariables.clusterName": "ノードが属しているクラスター。", + "xpack.monitoring.alerts.actionVariables.internalFullMessage": "詳細な内部メッセージはElasticで生成されました。", + "xpack.monitoring.alerts.actionVariables.internalShortMessage": "内部メッセージ(省略あり)はElasticで生成されました。", + "xpack.monitoring.alerts.actionVariables.state": "現在のアラートの状態。", "xpack.monitoring.alerts.badge.panelTitle": "アラート", "xpack.monitoring.alerts.callout.dangerLabel": "危険アラート", "xpack.monitoring.alerts.callout.warningLabel": "警告アラート", @@ -12203,8 +13810,23 @@ "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.hotThreads": "#start_linkCheck hot threads#end_link", "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.runningTasks": "#start_linkCheck long running tasks#end_link", "xpack.monitoring.alerts.cpuUsage.ui.resolvedMessage": "ノード{nodeName}でのCPU使用状況は現在しきい値を下回っています。現在、#resolved時点で、{cpuUsage}%と報告されています。", - "xpack.monitoring.alerts.validation.duration": "有効な期間が必要です。", - "xpack.monitoring.alerts.validation.threshold": "有効な数字が必要です。", + "xpack.monitoring.alerts.diskUsage.actionVariables.count": "高ディスク使用率を報告しているノード数。", + "xpack.monitoring.alerts.diskUsage.actionVariables.nodes": "高ディスク使用率を報告しているノードのリスト。", + "xpack.monitoring.alerts.diskUsage.firing.internalFullMessage": "ディスク使用状況アラートは、クラスター{clusterName}の{count}個のノードで実行されています。{action}", + "xpack.monitoring.alerts.diskUsage.firing.internalShortMessage": "ディスク使用状況アラートは、クラスター{clusterName}の{count}個のノードで実行されています。{shortActionText}", + "xpack.monitoring.alerts.diskUsage.fullAction": "ノードの表示", + "xpack.monitoring.alerts.diskUsage.label": "ディスク使用量", + "xpack.monitoring.alerts.diskUsage.paramDetails.duration.label": "平均を確認", + "xpack.monitoring.alerts.diskUsage.paramDetails.threshold.label": "ディスク容量が超過したときに通知", + "xpack.monitoring.alerts.diskUsage.resolved.internalMessage": "ディスク使用状況アラートは、クラスター{clusterName}の{count}個のノードで解決されました。", + "xpack.monitoring.alerts.diskUsage.shortAction": "影響を受けるノード全体のディスク使用状況レベルを検証します。", + "xpack.monitoring.alerts.diskUsage.ui.firingMessage": "ノード#start_link{nodeName}#end_linkは、#absoluteでディスク使用率{diskUsage}%を報告しています", + "xpack.monitoring.alerts.diskUsage.ui.nextSteps.addMoreNodes": "#start_linkその他のデータノードを追加#end_link", + "xpack.monitoring.alerts.diskUsage.ui.nextSteps.identifyIndices": "#start_link大きいインデックスを特定#end_link", + "xpack.monitoring.alerts.diskUsage.ui.nextSteps.ilmPolicies": "#start_linkILMポリシーを導入#end_link", + "xpack.monitoring.alerts.diskUsage.ui.nextSteps.resizeYourDeployment": "#start_linkデプロイのサイズを変更(ECE)#end_link", + "xpack.monitoring.alerts.diskUsage.ui.nextSteps.tuneDisk": "#start_linkディスク使用状況の最適化#end_link", + "xpack.monitoring.alerts.diskUsage.ui.resolvedMessage": "ノード{nodeName}でのディスク使用状況は現在しきい値を下回っています。現在、#resolved時点で、{diskUsage}%と報告されています。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.clusterHealth": "このクラスターを実行しているElasticsearchのバージョン。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage": "{clusterName}に対してElasticsearchバージョン不一致アラートが実行されています。Elasticsearchは{versions}を実行しています。{action}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "{clusterName}に対してElasticsearchバージョン不一致アラートが実行されています。{shortActionText}", @@ -12251,7 +13873,44 @@ "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンのLogstash({versions})が実行されています。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.resolvedMessage": "このクラスターではすべてのLogstashのバージョンが同じです。", + "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "高メモリー使用率を報告しているノード数。", + "xpack.monitoring.alerts.memoryUsage.actionVariables.nodes": "高メモリー使用率を報告しているノードのリスト。", + "xpack.monitoring.alerts.memoryUsage.firing.internalFullMessage": "メモリー使用状況アラートは、クラスター{clusterName}の{count}個のノードで実行されています。{action}", + "xpack.monitoring.alerts.memoryUsage.firing.internalShortMessage": "メモリー使用状況アラートは、クラスター{clusterName}の{count}個のノードで実行されています。{shortActionText}", + "xpack.monitoring.alerts.memoryUsage.fullAction": "ノードの表示", + "xpack.monitoring.alerts.memoryUsage.label": "メモリー使用状況(JVM)", + "xpack.monitoring.alerts.memoryUsage.paramDetails.duration.label": "平均を確認", + "xpack.monitoring.alerts.memoryUsage.paramDetails.threshold.label": "メモリー使用状況が超過したときに通知", + "xpack.monitoring.alerts.memoryUsage.resolved.internalMessage": "メモリー使用状況アラートは、クラスター{clusterName}の{count}個のノードで解決されました。", + "xpack.monitoring.alerts.memoryUsage.shortAction": "影響を受けるノード全体のメモリー使用状況レベルを検証します。", + "xpack.monitoring.alerts.memoryUsage.ui.firingMessage": "ノード#start_link{nodeName}#end_linkは、#absoluteでJVMメモリー使用率{memoryUsage}%を報告しています", + "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.addMoreNodes": "#start_linkその他のデータノードを追加#end_link", + "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.identifyIndicesShards": "#start_link大きいインデックス/シャードを特定#end_link", + "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.managingHeap": "#start_linkESヒープの管理#end_link", + "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.resizeYourDeployment": "#start_linkデプロイのサイズを変更(ECE)#end_link", + "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.tuneThreadPools": "#start_linkスレッドプールの微調整#end_link", + "xpack.monitoring.alerts.memoryUsage.ui.resolvedMessage": "ノード{nodeName}でのJVMメモリー使用状況は現在しきい値を下回っています。現在、#resolved時点で、{memoryUsage}%と報告されています。", "xpack.monitoring.alerts.migrate.manageAction.requiredFieldError": "{field} は必須フィールドです。", + "xpack.monitoring.alerts.missingData.actionVariables.count": "監視データが見つからないスタック製品数。", + "xpack.monitoring.alerts.missingData.actionVariables.stackProducts": "監視データが見つからないスタック製品。", + "xpack.monitoring.alerts.missingData.firing": "実行中", + "xpack.monitoring.alerts.missingData.firing.internalFullMessage": "クラスター{clusterName}では、{count}個のスタック製品の監視データが検出されませんでした。{action}", + "xpack.monitoring.alerts.missingData.firing.internalShortMessage": "クラスター{clusterName}では、{count}個のスタック製品の監視データが検出されませんでした。{shortActionText}", + "xpack.monitoring.alerts.missingData.fullAction": "これらのスタック製品に関連する監視データを表示します。", + "xpack.monitoring.alerts.missingData.label": "見つからない監視データ", + "xpack.monitoring.alerts.missingData.paramDetails.duration.label": "監視データが見つからない場合に通知", + "xpack.monitoring.alerts.missingData.paramDetails.limit.label": "遡って監視データを検索", + "xpack.monitoring.alerts.missingData.resolved": "解決済み", + "xpack.monitoring.alerts.missingData.resolved.internalFullMessage": "クラスター{clusterName}では、{count}個のスタック製品の監視データが確認されています。", + "xpack.monitoring.alerts.missingData.resolved.internalShortMessage": "クラスター{clusterName}では、{count}個のスタック製品の監視データが確認されています。", + "xpack.monitoring.alerts.missingData.shortAction": "これらのスタック製品が起動して実行中であることを検証してから、監視設定を確認してください。", + "xpack.monitoring.alerts.missingData.ui.firingMessage": "#absolute以降、過去{gapDuration}には、{stackProduct} {type}: {stackProductName}から監視データが検出されていません。", + "xpack.monitoring.alerts.missingData.ui.nextSteps.verifySettings": "{type}で監視設定を検証", + "xpack.monitoring.alerts.missingData.ui.nextSteps.viewAll": "#start_linkすべての{stackProduct} {type}を表示#end_link", + "xpack.monitoring.alerts.missingData.ui.notQuiteResolvedMessage": "まだ{stackProduct} {type}:{stackProductName}の監視データが確認されていません。試行を停止します。これを変更するには、さらに過去のデータを検索するようにアラートを構成します。", + "xpack.monitoring.alerts.missingData.ui.resolvedMessage": "#resolved時点では、{stackProduct} {type}: {stackProductName}の監視データが確認されています。", + "xpack.monitoring.alerts.missingData.validation.duration": "有効な期間が必要です。", + "xpack.monitoring.alerts.missingData.validation.limit": "有効な上限が必要です。", "xpack.monitoring.alerts.nodesChanged.actionVariables.added": "ノードのリストがクラスターに追加されました。", "xpack.monitoring.alerts.nodesChanged.actionVariables.removed": "ノードのリストがクラスターから削除されました。", "xpack.monitoring.alerts.nodesChanged.actionVariables.restarted": "ノードのリストがクラスターで再起動しました。", @@ -12274,13 +13933,25 @@ "xpack.monitoring.alerts.panel.muteAlert.errorTitle": "アラートをミュートできません", "xpack.monitoring.alerts.panel.muteTitle": "ミュート", "xpack.monitoring.alerts.panel.ummuteAlert.errorTitle": "アラートをミュート解除できません", + "xpack.monitoring.alerts.state.firing": "実行中", + "xpack.monitoring.alerts.state.resolved": "解決済み", "xpack.monitoring.alerts.status.alertsTooltip": "アラート", "xpack.monitoring.alerts.status.clearText": "クリア", "xpack.monitoring.alerts.status.clearToolip": "アラートは実行されていません", "xpack.monitoring.alerts.status.highSeverityTooltip": "すぐに対処が必要な致命的な問題があります!", "xpack.monitoring.alerts.status.lowSeverityTooltip": "低重要度の問題があります", "xpack.monitoring.alerts.status.mediumSeverityTooltip": "スタックに影響を及ぼす可能性がある問題があります。", + "xpack.monitoring.alerts.typeLabel.instance": "インスタンス", + "xpack.monitoring.alerts.typeLabel.instances": "インスタンス", + "xpack.monitoring.alerts.typeLabel.node": "ノード", + "xpack.monitoring.alerts.typeLabel.nodes": "ノード", + "xpack.monitoring.alerts.typeLabel.server": "サーバー", + "xpack.monitoring.alerts.typeLabel.servers": "サーバー", + "xpack.monitoring.alerts.validation.duration": "有効な期間が必要です。", + "xpack.monitoring.alerts.validation.threshold": "有効な数字が必要です。", "xpack.monitoring.apm.healthStatusLabel": "ヘルス: {status}", + "xpack.monitoring.apm.instance.heading": "APMサーバーインスタンス", + "xpack.monitoring.apm.instance.pageTitle": "APMサーバーインスタンス:{instanceName}", "xpack.monitoring.apm.instance.routeTitle": "{apm} - インスタンス", "xpack.monitoring.apm.instance.status.lastEventDescription": "{timeOfLastEvent} 前", "xpack.monitoring.apm.instance.status.lastEventLabel": "最後のイベント", @@ -12292,11 +13963,13 @@ "xpack.monitoring.apm.instances.allocatedMemoryTitle": "割当メモリー", "xpack.monitoring.apm.instances.bytesSentRateTitle": "送信バイトレート", "xpack.monitoring.apm.instances.filterInstancesPlaceholder": "フィルターインスタンス…", + "xpack.monitoring.apm.instances.heading": "APMインスタンス", "xpack.monitoring.apm.instances.lastEventTitle": "最後のイベント", "xpack.monitoring.apm.instances.lastEventValue": "{timeOfLastEvent} 前", "xpack.monitoring.apm.instances.nameTitle": "名前", "xpack.monitoring.apm.instances.outputEnabledTitle": "アウトプットが有効です", "xpack.monitoring.apm.instances.outputErrorsTitle": "アウトプットエラー", + "xpack.monitoring.apm.instances.pageTitle": "APMサーバーインスタンス", "xpack.monitoring.apm.instances.routeTitle": "{apm} - インスタンス", "xpack.monitoring.apm.instances.status.lastEventDescription": "{timeOfLastEvent} 前", "xpack.monitoring.apm.instances.status.lastEventLabel": "最後のイベント", @@ -12306,10 +13979,11 @@ "xpack.monitoring.apm.instances.totalEventsRateTitle": "合計イベントレート", "xpack.monitoring.apm.instances.versionFilter": "バージョン", "xpack.monitoring.apm.instances.versionTitle": "バージョン", + "xpack.monitoring.apm.overview.heading": "APMサーバー概要", + "xpack.monitoring.apm.overview.pageTitle": "APMサーバー概要", + "xpack.monitoring.apm.overview.routeTitle": "APMサーバー", "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": "構成の再読み込み", @@ -12321,10 +13995,12 @@ "xpack.monitoring.beats.instance.hostLabel": "ホスト", "xpack.monitoring.beats.instance.nameLabel": "名前", "xpack.monitoring.beats.instance.outputLabel": "アウトプット", + "xpack.monitoring.beats.instance.pageTitle": "Beatインスタンス:{beatName}", "xpack.monitoring.beats.instance.routeTitle": "ビート - {instanceName} - 概要", "xpack.monitoring.beats.instance.typeLabel": "タイプ", "xpack.monitoring.beats.instance.uptimeLabel": "起動時間", "xpack.monitoring.beats.instance.versionLabel": "バージョン", + "xpack.monitoring.beats.instances.alertsColumnTitle": "アラート", "xpack.monitoring.beats.instances.allocatedMemoryTitle": "割当メモリー", "xpack.monitoring.beats.instances.bytesSentRateTitle": "送信バイトレート", "xpack.monitoring.beats.instances.nameTitle": "名前", @@ -12336,17 +14012,20 @@ "xpack.monitoring.beats.instances.versionFilter": "バージョン", "xpack.monitoring.beats.instances.versionTitle": "バージョン", "xpack.monitoring.beats.listing.heading": "Beatsリスト", - "xpack.monitoring.beats.overview.activeBeatsInLastDayTitle": "最終日のアクティブなビート", + "xpack.monitoring.beats.listing.pageTitle": "Beatsリスト", + "xpack.monitoring.beats.overview.activeBeatsInLastDayTitle": "最終日のアクティブなBeats", "xpack.monitoring.beats.overview.bytesSentLabel": "送信バイト数", + "xpack.monitoring.beats.overview.heading": "Beatsの概要", "xpack.monitoring.beats.overview.latestActive.last1DayLabel": "過去 1 日", "xpack.monitoring.beats.overview.latestActive.last1HourLabel": "過去 1 時間", "xpack.monitoring.beats.overview.latestActive.last1MinuteLabel": "過去 1 か月", "xpack.monitoring.beats.overview.latestActive.last20MinutesLabel": "過去 20 分間", "xpack.monitoring.beats.overview.latestActive.last5MinutesLabel": "過去 5 分間", "xpack.monitoring.beats.overview.noActivityDescription": "こんにちは!ここには最新のビートアクティビティが表示されますが、1 日以内にアクティビティがないようです。", + "xpack.monitoring.beats.overview.pageTitle": "Beatsの概要", "xpack.monitoring.beats.overview.routeTitle": "ビート - 概要", - "xpack.monitoring.beats.overview.top5BeatTypesInLastDayTitle": "最終日のトップ 5 のビートタイプ", - "xpack.monitoring.beats.overview.top5VersionsInLastDayTitle": "最終日のトップ 5 のバージョン", + "xpack.monitoring.beats.overview.top5BeatTypesInLastDayTitle": "最終日のトップ 5のBeat タイプ", + "xpack.monitoring.beats.overview.top5VersionsInLastDayTitle": "最終日のトップ5のバージョン", "xpack.monitoring.beats.overview.totalBeatsLabel": "合計ビート数", "xpack.monitoring.beats.overview.totalEventsLabel": "合計イベント数", "xpack.monitoring.beats.routeTitle": "ビート", @@ -12354,13 +14033,13 @@ "xpack.monitoring.beatsNavigation.instancesLinkText": "インスタンス", "xpack.monitoring.beatsNavigation.overviewLinkText": "概要", "xpack.monitoring.breadcrumbs.apm.instancesLabel": "インスタンス", - "xpack.monitoring.breadcrumbs.apmLabel": "APM", + "xpack.monitoring.breadcrumbs.apmLabel": "APMサーバー", "xpack.monitoring.breadcrumbs.beats.instancesLabel": "インスタンス", "xpack.monitoring.breadcrumbs.beatsLabel": "ビート", "xpack.monitoring.breadcrumbs.clustersLabel": "クラスター", "xpack.monitoring.breadcrumbs.es.ccrLabel": "CCR", "xpack.monitoring.breadcrumbs.es.indicesLabel": "インデックス", - "xpack.monitoring.breadcrumbs.es.jobsLabel": "ジョブ", + "xpack.monitoring.breadcrumbs.es.jobsLabel": "機械学習ジョブ", "xpack.monitoring.breadcrumbs.es.nodesLabel": "ノード", "xpack.monitoring.breadcrumbs.kibana.instancesLabel": "インスタンス", "xpack.monitoring.breadcrumbs.logstash.nodesLabel": "ノード", @@ -12372,6 +14051,9 @@ "xpack.monitoring.chart.screenReaderUnaccessibleTitle": "このチャートはスクリーンリーダーではアクセスできません", "xpack.monitoring.chart.seriesScreenReaderListDescription": "間隔: {bucketSize}", "xpack.monitoring.chart.timeSeries.zoomOut": "ズームアウト", + "xpack.monitoring.cluster.health.healthy": "正常", + "xpack.monitoring.cluster.health.primaryShards": "見つからないプライマリシャード", + "xpack.monitoring.cluster.health.replicaShards": "見つからないレプリカシャード", "xpack.monitoring.cluster.listing.dataColumnTitle": "データ", "xpack.monitoring.cluster.listing.incompatibleLicense.getLicenseLinkLabel": "全機能を利用できるライセンスを取得", "xpack.monitoring.cluster.listing.incompatibleLicense.infoMessage": "複数クラスターの監視が必要ですか?{getLicenseInfoLink} して、複数クラスターの監視をご利用ください。", @@ -12388,21 +14070,22 @@ "xpack.monitoring.cluster.listing.logstashColumnTitle": "Logstash", "xpack.monitoring.cluster.listing.nameColumnTitle": "名前", "xpack.monitoring.cluster.listing.nodesColumnTitle": "ノード", + "xpack.monitoring.cluster.listing.pageTitle": "クラスターリスト", "xpack.monitoring.cluster.listing.standaloneClusterCallOutDismiss": "閉じる", "xpack.monitoring.cluster.listing.standaloneClusterCallOutLink": "これらのインスタンスを表示。", "xpack.monitoring.cluster.listing.standaloneClusterCallOutText": "または、下の表のスタンドアローンクラスターをクリックしてください", "xpack.monitoring.cluster.listing.standaloneClusterCallOutTitle": "Elasticsearch クラスターに接続されていないインスタンスがあるようです。", "xpack.monitoring.cluster.listing.statusColumnTitle": "ステータス", "xpack.monitoring.cluster.listing.unknownHealthMessage": "不明", - "xpack.monitoring.cluster.overview.apmPanel.apmTitle": "APM", - "xpack.monitoring.cluster.overview.apmPanel.instancesTotalLinkAriaLabel": "APM インスタンス: {apmsTotal}", + "xpack.monitoring.cluster.overview.apmPanel.apmTitle": "APMサーバー", + "xpack.monitoring.cluster.overview.apmPanel.instancesTotalLinkAriaLabel": "APMサーバーインスタンス: {apmsTotal}", "xpack.monitoring.cluster.overview.apmPanel.lastEventDescription": "{timeOfLastEvent} 前", "xpack.monitoring.cluster.overview.apmPanel.lastEventLabel": "最後のイベント", "xpack.monitoring.cluster.overview.apmPanel.memoryUsageLabel": "メモリー使用状況", - "xpack.monitoring.cluster.overview.apmPanel.overviewLinkAriaLabel": "APM の概要", - "xpack.monitoring.cluster.overview.apmPanel.overviewLinkLabel": "概要", + "xpack.monitoring.cluster.overview.apmPanel.overviewLinkAriaLabel": "APMサーバー概要", + "xpack.monitoring.cluster.overview.apmPanel.overviewLinkLabel": "APMサーバー概要", "xpack.monitoring.cluster.overview.apmPanel.processedEventsLabel": "処理済みのイベント", - "xpack.monitoring.cluster.overview.apmPanel.serversTotalLinkLabel": "APM Server: {apmsTotal}", + "xpack.monitoring.cluster.overview.apmPanel.serversTotalLinkLabel": "APMサーバー:{apmsTotal}", "xpack.monitoring.cluster.overview.beatsPanel.beatsTitle": "ビート", "xpack.monitoring.cluster.overview.beatsPanel.beatsTotalLinkLabel": "ビート: {beatsTotal}", "xpack.monitoring.cluster.overview.beatsPanel.bytesSentLabel": "送信バイト数", @@ -12421,7 +14104,7 @@ "xpack.monitoring.cluster.overview.esPanel.indicesCountLinkAriaLabel": "Elasticsearch インデックス: {indicesCount}", "xpack.monitoring.cluster.overview.esPanel.indicesCountLinkLabel": "インデックス: {indicesCount}", "xpack.monitoring.cluster.overview.esPanel.infoLogsTooltipText": "情報ログの数です", - "xpack.monitoring.cluster.overview.esPanel.jobsLabel": "ジョブ", + "xpack.monitoring.cluster.overview.esPanel.jobsLabel": "機械学習ジョブ", "xpack.monitoring.cluster.overview.esPanel.jvmHeapLabel": "{javaVirtualMachine} ヒープ", "xpack.monitoring.cluster.overview.esPanel.licenseLabel": "ライセンス", "xpack.monitoring.cluster.overview.esPanel.logsLinkAriaLabel": "Elasticsearch ログ", @@ -12462,6 +14145,7 @@ "xpack.monitoring.cluster.overview.logstashPanel.uptimeLabel": "起動時間", "xpack.monitoring.cluster.overview.logstashPanel.withMemoryQueuesLabel": "メモリーキューあり", "xpack.monitoring.cluster.overview.logstashPanel.withPersistentQueuesLabel": "永続キューあり", + "xpack.monitoring.cluster.overview.pageTitle": "クラスターの概要", "xpack.monitoring.cluster.overviewTitle": "概要", "xpack.monitoring.clusterAlerts.checkLicense.licenseIsBasicDescription": "Watcher が無効になっているか、[{clusterSource}] クラスターの現在のライセンスがベーシックの場合、クラスターアラートは表示されません。", "xpack.monitoring.clusterAlerts.checkLicense.licenseNotActiveDescription": "[{clusterSource}] クラスターの現在のライセンス [{type}] がアクティブでないため、クラスターアラートは表示されません。", @@ -12476,15 +14160,16 @@ "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": "インデックス", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.lastFetchTimeColumnTitle": "最終取得時刻", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.opsSyncedColumnTitle": "同期されたオペレーション", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.syncLagOpsColumnTitle": "同期の遅延 (オペレーション数)", + "xpack.monitoring.elasticsearch.ccr.pageTitle": "Elasticsearch - CCR", "xpack.monitoring.elasticsearch.ccr.routeTitle": "Elasticsearch - CCR", "xpack.monitoring.elasticsearch.ccr.shard.instanceTitle": "インデックス{followerIndex} シャード: {shardId}", + "xpack.monitoring.elasticsearch.ccr.shard.pageTitle": "Elasticsearch Ccrシャード - インデックス:{followerIndex} シャード:{shardId}", "xpack.monitoring.elasticsearch.ccr.shard.routeTitle": "Elasticsearch - CCR - シャード", "xpack.monitoring.elasticsearch.ccr.shardsTable.errorColumnTitle": "エラー", "xpack.monitoring.elasticsearch.ccr.shardsTable.lastFetchTimeColumnTitle": "最終取得時刻", @@ -12524,11 +14209,13 @@ "xpack.monitoring.elasticsearch.indices.monitoringTablePlaceholder": "インデックスのフィルタリング…", "xpack.monitoring.elasticsearch.indices.nameTitle": "名前", "xpack.monitoring.elasticsearch.indices.noIndicesMatchYourSelectionDescription": "選択項目に一致するインデックスがありません。時間範囲を変更してみてください。", + "xpack.monitoring.elasticsearch.indices.overview.pageTitle": "インデックス:{indexName}", "xpack.monitoring.elasticsearch.indices.overview.routeTitle": "Elasticsearch - インデックス - {indexName} - 概要", + "xpack.monitoring.elasticsearch.indices.pageTitle": "デフォルトのインデックス", "xpack.monitoring.elasticsearch.indices.routeTitle": "Elasticsearch - インデックス", "xpack.monitoring.elasticsearch.indices.searchRateTitle": "検索レート", "xpack.monitoring.elasticsearch.indices.statusTitle": "ステータス", - "xpack.monitoring.elasticsearch.indices.systemIndicesLabel": "システムインデックス", + "xpack.monitoring.elasticsearch.indices.systemIndicesLabel": "システムインデックスのフィルター", "xpack.monitoring.elasticsearch.indices.unassignedShardsTitle": "未割り当てシャード", "xpack.monitoring.elasticsearch.mlJobListing.filterJobsPlaceholder": "ジョブをフィルタリング…", "xpack.monitoring.elasticsearch.mlJobListing.forecastsTitle": "予測", @@ -12540,8 +14227,17 @@ "xpack.monitoring.elasticsearch.mlJobListing.processedRecordsTitle": "処理済みレコード", "xpack.monitoring.elasticsearch.mlJobListing.stateTitle": "ステータス", "xpack.monitoring.elasticsearch.mlJobListing.statusIconLabel": "ジョブ状態: {status}", + "xpack.monitoring.elasticsearch.mlJobs.pageTitle": "Elasticsearch - 機械学習ジョブ", "xpack.monitoring.elasticsearch.mlJobs.routeTitle": "Elasticsearch - 機械学習ジョブ", "xpack.monitoring.elasticsearch.node.advanced.routeTitle": "Elasticsearch - ノード - {nodeSummaryName} - 高度な設定", + "xpack.monitoring.elasticsearch.node.cells.tooltip.iconLabel": "このメトリックの詳細", + "xpack.monitoring.elasticsearch.node.cells.tooltip.max": "最高値", + "xpack.monitoring.elasticsearch.node.cells.tooltip.min": "最低値", + "xpack.monitoring.elasticsearch.node.cells.tooltip.preface": "現在の期間に適用されます", + "xpack.monitoring.elasticsearch.node.cells.tooltip.trending": "傾向", + "xpack.monitoring.elasticsearch.node.cells.trendingDownText": "ダウン", + "xpack.monitoring.elasticsearch.node.cells.trendingUpText": "アップ", + "xpack.monitoring.elasticsearch.node.overview.pageTitle": "Elasticsearchノード:{node}", "xpack.monitoring.elasticsearch.node.overview.routeTitle": "Elasticsearch - ノード - {nodeName} - 概要", "xpack.monitoring.elasticsearch.node.statusIconLabel": "ステータス: {status}", "xpack.monitoring.elasticsearch.nodeDetailStatus.alerts": "アラート", @@ -12567,12 +14263,14 @@ "xpack.monitoring.elasticsearch.nodes.metricbeatMigration.disableInternalCollectionTitle": "Metricbeat による Elasticsearch ノードの監視が開始されました", "xpack.monitoring.elasticsearch.nodes.monitoringTablePlaceholder": "ノードをフィルタリング…", "xpack.monitoring.elasticsearch.nodes.nameColumnTitle": "名前", + "xpack.monitoring.elasticsearch.nodes.pageTitle": "Elasticsearchノード", "xpack.monitoring.elasticsearch.nodes.routeTitle": "Elasticsearch - ノード", "xpack.monitoring.elasticsearch.nodes.shardsColumnTitle": "シャード", "xpack.monitoring.elasticsearch.nodes.statusColumn.offlineLabel": "オフライン", "xpack.monitoring.elasticsearch.nodes.statusColumn.onlineLabel": "オンライン", "xpack.monitoring.elasticsearch.nodes.statusColumnTitle": "ステータス", "xpack.monitoring.elasticsearch.nodes.unknownNodeTypeLabel": "不明", + "xpack.monitoring.elasticsearch.overview.pageTitle": "Elasticsearchの概要", "xpack.monitoring.elasticsearch.shardActivity.completedRecoveriesLabel": "完了済みの復元", "xpack.monitoring.elasticsearch.shardActivity.noActiveShardRecoveriesMessage.completedRecoveriesLinkText": "完了済みの復元", "xpack.monitoring.elasticsearch.shardActivity.noActiveShardRecoveriesMessage.completedRecoveriesLinkTextProblem": "このクラスターにはアクティブなシャードの復元がありません。", @@ -12603,6 +14301,7 @@ "xpack.monitoring.elasticsearch.shardAllocation.shardLegendTitle": "シャードの凡例", "xpack.monitoring.elasticsearch.shardAllocation.tableBody.noShardsAllocatedDescription": "シャードが割り当てられていません。", "xpack.monitoring.elasticsearch.shardAllocation.tableBodyDisplayName": "TableBody", + "xpack.monitoring.elasticsearch.shardAllocation.tableHead.filterSystemIndices": "システムインデックスのフィルター", "xpack.monitoring.elasticsearch.shardAllocation.tableHead.indicesLabel": "インデックス", "xpack.monitoring.elasticsearch.shardAllocation.unassignedDisplayName": "割り当てなし", "xpack.monitoring.elasticsearch.shardAllocation.unassignedPrimaryLabel": "未割り当てプライマリ", @@ -12630,7 +14329,7 @@ "xpack.monitoring.esNavigation.indicesLinkText": "インデックス", "xpack.monitoring.esNavigation.instance.advancedLinkText": "高度な設定", "xpack.monitoring.esNavigation.instance.overviewLinkText": "概要", - "xpack.monitoring.esNavigation.jobsLinkText": "ジョブ", + "xpack.monitoring.esNavigation.jobsLinkText": "機械学習ジョブ", "xpack.monitoring.esNavigation.nodesLinkText": "ノード", "xpack.monitoring.esNavigation.overviewLinkText": "概要", "xpack.monitoring.euiSSPTable.setupNewButtonLabel": "新規 {identifier} の監視を設定", @@ -12641,16 +14340,19 @@ "xpack.monitoring.expiredLicenseStatusDescription": "ご使用のライセンスは{expiryDate}に期限切れになりました", "xpack.monitoring.expiredLicenseStatusTitle": "ご使用の{typeTitleCase}ライセンスは期限切れです", "xpack.monitoring.feature.reserved.description": "ユーザーアクセスを許可するには、monitoring_user ロールも割り当てる必要があります。", + "xpack.monitoring.featureCatalogueDescription": "ご使用のデプロイのリアルタイムのヘルスとパフォーマンスをトラッキングします。", + "xpack.monitoring.featureCatalogueTitle": "スタックを監視", "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.tlsAndEncryptionError": "スタック監視アラートでは、KibanaとElasticsearchの間のトランスポートレイヤーセキュリティと、kibana.ymlファイルの暗号化鍵が必要です。", "xpack.monitoring.healthCheck.tlsAndEncryptionErrorTitle": "追加の設定が必要です", - "xpack.monitoring.janLabel": "1月", - "xpack.monitoring.julLabel": "7月", - "xpack.monitoring.junLabel": "6月", + "xpack.monitoring.internalAndMetricbeatMonitoringToast.description": "スタック監視で、Metricbeatと「レガシー収集」の両方を使用している可能性があります。\n 8.0.0では、Metricbeatを使用して、監視データを収集する必要があります。\n セットアップモードの手順に従い、残りの監視をMetricbeatに移行してください。", + "xpack.monitoring.internalAndMetricbeatMonitoringToast.title": "一部のレガシー監視が検出されました", + "xpack.monitoring.internalMonitoringToast.description": "スタック監視で、「レガシー収集」の両方を使用している可能性があります。\n この監視方法は、次のメジャーリリース(8.0.0)ではサポートされていません。\n セットアップモードの手順に従い、Metricbeatで監視を開始してください。", + "xpack.monitoring.internalMonitoringToast.enterSetupMode": "設定モードにする", + "xpack.monitoring.internalMonitoringToast.learnMoreAction": "詳細", + "xpack.monitoring.internalMonitoringToast.title": "内部監視が検出されました", "xpack.monitoring.kibana.clusterStatus.connectionsLabel": "接続", "xpack.monitoring.kibana.clusterStatus.instancesLabel": "インスタンス", "xpack.monitoring.kibana.clusterStatus.maxResponseTimeLabel": "最高応答時間", @@ -12660,8 +14362,11 @@ "xpack.monitoring.kibana.detailStatus.transportAddressLabel": "トランスポートアドレス", "xpack.monitoring.kibana.detailStatus.uptimeLabel": "起動時間", "xpack.monitoring.kibana.detailStatus.versionLabel": "バージョン", + "xpack.monitoring.kibana.instance.pageTitle": "Kibanaインスタンス:{instance}", "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeDescription": "次のインスタンスは監視されていません。\n 下の「Metricbeat で監視」をクリックして、監視を開始してください。", "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeTitle": "Kibana インスタンスが検出されました", + "xpack.monitoring.kibana.instances.pageTitle": "Kibanaインスタンス", + "xpack.monitoring.kibana.instances.routeTitle": "Kibana - インスタンス", "xpack.monitoring.kibana.listing.alertsColumnTitle": "アラート", "xpack.monitoring.kibana.listing.filterInstancesPlaceholder": "フィルターインスタンス…", "xpack.monitoring.kibana.listing.loadAverageColumnTitle": "平均負荷", @@ -12670,6 +14375,7 @@ "xpack.monitoring.kibana.listing.requestsColumnTitle": "リクエスト", "xpack.monitoring.kibana.listing.responseTimeColumnTitle": "応答時間", "xpack.monitoring.kibana.listing.statusColumnTitle": "ステータス", + "xpack.monitoring.kibana.overview.pageTitle": "Kibanaの概要", "xpack.monitoring.kibana.shardActivity.bytesTitle": "バイト", "xpack.monitoring.kibana.shardActivity.filesTitle": "ファイル", "xpack.monitoring.kibana.shardActivity.indexTitle": "インデックス", @@ -12683,6 +14389,7 @@ "xpack.monitoring.license.heading": "ライセンス", "xpack.monitoring.license.howToUpdateLicenseDescription": "このクラスターのライセンスを更新するには、Elasticsearch {apiText}でライセンスファイルを提供してください:", "xpack.monitoring.license.licenseRouteTitle": "ライセンス", + "xpack.monitoring.loading.pageTitle": "読み込み中", "xpack.monitoring.logs.listing.calloutLinkText": "ログ", "xpack.monitoring.logs.listing.calloutTitle": "他のログを表示する場合", "xpack.monitoring.logs.listing.clusterPageDescription": "このクラスターの最も新しいログを最高合計 {limit} 件まで表示しています。", @@ -12737,8 +14444,11 @@ "xpack.monitoring.logstash.detailStatus.versionLabel": "バージョン", "xpack.monitoring.logstash.filterNodesPlaceholder": "ノードをフィルタリング…", "xpack.monitoring.logstash.filterPipelinesPlaceholder": "パイプラインのフィルタリング…", + "xpack.monitoring.logstash.node.advanced.pageTitle": "Logstashノード:{nodeName}", "xpack.monitoring.logstash.node.advanced.routeTitle": "Logstash - {nodeName} - 高度な設定", + "xpack.monitoring.logstash.node.pageTitle": "Logstashノード:{nodeName}", "xpack.monitoring.logstash.node.pipelines.notAvailableDescription": "パイプラインの監視は Logstash バージョン 6.0.0 以降でのみ利用できます。このノードはバージョン {logstashVersion} を実行しています。", + "xpack.monitoring.logstash.node.pipelines.pageTitle": "Logstashノードパイプライン:{nodeName}", "xpack.monitoring.logstash.node.pipelines.routeTitle": "Logstash - {nodeName} - パイプライン", "xpack.monitoring.logstash.node.routeTitle": "Logstash - {nodeName}", "xpack.monitoring.logstash.nodes.alertsColumnTitle": "アラート", @@ -12750,7 +14460,10 @@ "xpack.monitoring.logstash.nodes.jvmHeapUsedTitle": "{javaVirtualMachine} ヒープを使用中", "xpack.monitoring.logstash.nodes.loadAverageTitle": "平均負荷", "xpack.monitoring.logstash.nodes.nameTitle": "名前", + "xpack.monitoring.logstash.nodes.pageTitle": "Logstashノード", + "xpack.monitoring.logstash.nodes.routeTitle": "Logstash - ノード", "xpack.monitoring.logstash.nodes.versionTitle": "バージョン", + "xpack.monitoring.logstash.overview.pageTitle": "Logstashの概要", "xpack.monitoring.logstash.pipeline.detailDrawer.conditionalStatementDescription": "これはパイプラインのコンディションのステートメントです。", "xpack.monitoring.logstash.pipeline.detailDrawer.eventsEmittedLabel": "送信イベント", "xpack.monitoring.logstash.pipeline.detailDrawer.eventsEmittedRateLabel": "イベント送信レート", @@ -12761,6 +14474,7 @@ "xpack.monitoring.logstash.pipeline.detailDrawer.specifyVertexIdDescription": "この {vertexType} には指定された ID がありません。ID を指定することで、パイプラインの変更時にその差をトラッキングできます。このプラグインの ID を次のように指定できます:", "xpack.monitoring.logstash.pipeline.detailDrawer.structureDescription": "これは Logstash でインプットと残りのパイプラインの間のイベントのバッファリングに使用される内部構造です。", "xpack.monitoring.logstash.pipeline.detailDrawer.vertexIdDescription": "この {vertexType} の ID は {vertexId} です。", + "xpack.monitoring.logstash.pipeline.pageTitle": "Logstashパイプライン:{pipeline}", "xpack.monitoring.logstash.pipeline.queue.noMetricsDescription": "キューメトリックが利用できません", "xpack.monitoring.logstash.pipeline.relativeFirstSeenAgoLabel": "{relativeFirstSeen} 前", "xpack.monitoring.logstash.pipeline.relativeLastSeenAgoLabel": "{relativeLastSeen} 前まで", @@ -12769,6 +14483,8 @@ "xpack.monitoring.logstash.pipelines.eventsEmittedRateTitle": "イベント送信レート", "xpack.monitoring.logstash.pipelines.idTitle": "ID", "xpack.monitoring.logstash.pipelines.numberOfNodesTitle": "ノード数", + "xpack.monitoring.logstash.pipelines.pageTitle": "Logstashパイプライン", + "xpack.monitoring.logstash.pipelines.routeTitle": "Logstashパイプライン", "xpack.monitoring.logstash.pipelineStatement.viewDetailsAriaLabel": "詳細を表示", "xpack.monitoring.logstash.pipelineViewer.filtersTitle": "フィルター", "xpack.monitoring.logstash.pipelineViewer.inputsTitle": "インプット", @@ -12780,8 +14496,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 +15119,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 +15157,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 +15200,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": "アラートが見つかりません。", @@ -13514,6 +15218,15 @@ "xpack.observability.emptySection.apps.uptime.description": "サイトとサービスの可用性をアクティブに監視するアラートを受信し、問題をより迅速に解決して、ユーザーエクスペリエンスを最適化します。", "xpack.observability.emptySection.apps.uptime.link": "Heartbeatのインストール", "xpack.observability.emptySection.apps.uptime.title": "アップタイム", + "xpack.observability.emptySection.apps.ux.description": "パフォーマンスは分散ですWebアプリケーションにアクセスするすべてのユーザーの経験を計測し、全員にとっての経験を改善する方法を理解します。", + "xpack.observability.emptySection.apps.ux.link": "Rumエージェントをインストール", + "xpack.observability.emptySection.apps.ux.title": "ユーザーエクスペリエンス", + "xpack.observability.featureCatalogueDescription": "専用UIで、ログ、メトリック、アプリケーショントレース、システム可用性を連結します。", + "xpack.observability.featureCatalogueDescription1": "インフラストラクチャメトリックを監視します。", + "xpack.observability.featureCatalogueDescription2": "アプリケーションリクエストをトレースします。", + "xpack.observability.featureCatalogueDescription3": "SLAを計測し、問題に対応します。", + "xpack.observability.featureCatalogueSubtitle": "一元化と監視", + "xpack.observability.featureCatalogueTitle": "オブザーバビリティ", "xpack.observability.home.addData": "データの追加", "xpack.observability.home.breadcrumb": "概要", "xpack.observability.home.feedback": "フィードバックを送信する", @@ -13557,6 +15270,8 @@ "xpack.observability.overview.uptime.monitors": "監視", "xpack.observability.overview.uptime.title": "アップタイム", "xpack.observability.overview.uptime.up": "アップ", + "xpack.observability.overview.ux.appLink": "アプリで表示", + "xpack.observability.overview.ux.title": "ユーザーエクスペリエンス", "xpack.observability.resources.documentation": "ドキュメント", "xpack.observability.resources.forum": "ディスカッションフォーラム", "xpack.observability.resources.title": "リソース", @@ -13570,6 +15285,29 @@ "xpack.observability.section.apps.uptime.description": "サイトとサービスの可用性をアクティブに監視するアラートを受信し、問題をより迅速に解決して、ユーザーエクスペリエンスを最適化します。", "xpack.observability.section.apps.uptime.title": "アップタイム", "xpack.observability.section.errorPanel": "データの取得時にエラーが発生しました。再試行してください", + "xpack.observability.ux.coreVitals.average": "平均", + "xpack.observability.ux.coreVitals.averageMessage": " {bad}未満", + "xpack.observability.ux.coreVitals.cls": "累積レイアウト変更", + "xpack.observability.ux.coreVitals.cls.help": "累積レイアウト変更(CLS):視覚的な安定性を計測します。優れたユーザーエクスペリエンスを実現するには、ページのCLSを0.1未満に保ってください。", + "xpack.observability.ux.coreVitals.fid.help": "初回入力遅延(FID)は双方向性を計測します。優れたユーザーエクスペリエンスを実現するには、ページのFIDを100ミリ秒未満に保ってください。", + "xpack.observability.ux.coreVitals.fip": "初回入力遅延", + "xpack.observability.ux.coreVitals.good": "優れている", + "xpack.observability.ux.coreVitals.is": "is", + "xpack.observability.ux.coreVitals.lcp": "最大コンテンツの描画", + "xpack.observability.ux.coreVitals.lcp.help": "最大コンテンツの描画(LCP)は読み込みパフォーマンスを計測します。優れたユーザーエクスペリエンスを実現するには、ページの読み込みが開始した後、2.5秒以内にLCPが実行されるようにしてください。", + "xpack.observability.ux.coreVitals.legends.good": "優れている", + "xpack.observability.ux.coreVitals.legends.needsImprovement": "要改善", + "xpack.observability.ux.coreVitals.legends.poor": "悪い", + "xpack.observability.ux.coreVitals.less": "少ない", + "xpack.observability.ux.coreVitals.more": "多い", + "xpack.observability.ux.coreVitals.paletteLegend.rankPercentage": "{labelsInd} ({ranksInd}%)", + "xpack.observability.ux.coreVitals.poor": "悪い", + "xpack.observability.ux.coreVitals.takes": "取得", + "xpack.observability.ux.coreWebVitals": "コアWebバイタル", + "xpack.observability.ux.coreWebVitals.service": "サービス", + "xpack.observability.ux.dashboard.webCoreVitals.help": "詳細", + "xpack.observability.ux.dashboard.webVitals.palette.tooltip": "{title}は{value}{averageMessage}より{moreOrLess}{isOrTakes}ため、{percentage} %のユーザーが{exp}を経験しています。", + "xpack.observability.ux.service.help": "最大トラフィックのRUMサービスが選択されています", "xpack.painlessLab.apiReferenceButtonLabel": "API リファレンス", "xpack.painlessLab.context.defaultLabel": "スクリプト結果は文字列に変換されます", "xpack.painlessLab.context.filterLabel": "フィルターのスクリプトクエリのコンテキストを使用する", @@ -13759,9 +15497,9 @@ "xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "その名前のリモートクラスターはありません。", "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "ES からレスポンスが返らず、クラスターを編集できません。", "xpack.reporting.breadcrumb": "レポート", - "xpack.reporting.browsers.chromium.errorDetected": "レポート生成時にエラーを検出しました: {err}", - "xpack.reporting.browsers.chromium.pageErrorDetected": "レポート生成時にページでエラーを検出しました: {err}", - "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "無許可の着信 URL を受信しました: 「{interceptedUrl}」、終了します", + "xpack.reporting.browsers.chromium.errorDetected": "レポートでエラーが発生しました:{err}", + "xpack.reporting.browsers.chromium.pageErrorDetected": "ページでレポートエラーが発生しました: {err}", + "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "許可されていない送信URLを受信しました: 「{interceptedUrl}」。要求が失敗しています。ブラウザーを終了しています。", "xpack.reporting.chromiumDriver.failedToCompleteRequest": "リクエストを完了できませんでした: {error}", "xpack.reporting.chromiumDriver.failedToCompleteRequestUsingHeaders": "ヘッダーを使用してリクエストを完了できませんでした: {error}", "xpack.reporting.dashboard.csvDownloadStartedMessage": "間もなく CSV がダウンロードされます。", @@ -13769,6 +15507,13 @@ "xpack.reporting.dashboard.downloadCsvPanelTitle": "CSV をダウンロード", "xpack.reporting.dashboard.failedCsvDownloadMessage": "現在 CSV を生成できません。", "xpack.reporting.dashboard.failedCsvDownloadTitle": "CSV のダウンロードに失敗", + "xpack.reporting.diagnostic.browserCrashed": "ブラウザーは起動中に異常終了しました", + "xpack.reporting.diagnostic.browserErrored": "ブラウザープロセスは起動中にエラーが発生しました", + "xpack.reporting.diagnostic.browserMissingDependency": "システム依存関係が不足しているため、ブラウザーを正常に起動できませんでした。{url}を参照してください", + "xpack.reporting.diagnostic.browserMissingFonts": "ブラウザーはデフォルトフォントを検索できませんでした。この問題を修正するには、{url}を参照してください。", + "xpack.reporting.diagnostic.configSizeMismatch": "xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH}({kibanaMaxContentBytes})はElasticSearchの{ES_MAX_SIZE_BYTES_PATH}({elasticSearchMaxContentBytes})を超えています。ElasticSearchで一致する{ES_MAX_SIZE_BYTES_PATH}を設定してください。あるいは、Kibanaでxpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH}を低くしてください。", + "xpack.reporting.diagnostic.noUsableSandbox": "Chromiumサンドボックスを使用できません。これは「xpack.reporting.capture.browser.chromium.disableSandbox」で無効にすることができます。この作業はご自身の責任で行ってください。{url}を参照してください", + "xpack.reporting.diagnostic.screenshotFailureMessage": "Kibanaインストールのスクリーンショットを作成できませんでした。", "xpack.reporting.errorButton.showReportErrorAriaLabel": "レポートエラーを表示", "xpack.reporting.errorButton.unableToFetchReportContentTitle": "レポートのコンテンツを取得できません", "xpack.reporting.errorButton.unableToGenerateReportTitle": "レポートを生成できません", @@ -13788,7 +15533,24 @@ "xpack.reporting.jobStatuses.pendingText": "保留中", "xpack.reporting.jobStatuses.processingText": "処理中", "xpack.reporting.jobStatuses.warningText": "完了しましたが警告があります", - "xpack.reporting.listing.reports.subtitle": "Kibana アプリケーションで生成されたレポートがここに表示されます", + "xpack.reporting.listing.diagnosticApiCallFailure": "診断の実行中に問題が発生しました:{error}", + "xpack.reporting.listing.diagnosticBrowserButton": "ブラウザーを確認", + "xpack.reporting.listing.diagnosticBrowserMessage": "レポートはヘッドレスブラウザーを使用して、PDFとPNGを生成します。ブラウザーを正常に起動できることを確認してください。", + "xpack.reporting.listing.diagnosticBrowserTitle": "ブラウザーを確認", + "xpack.reporting.listing.diagnosticButton": "レポート診断を実行", + "xpack.reporting.listing.diagnosticConfigButton": "構成を検証", + "xpack.reporting.listing.diagnosticConfigMessage": "Kibana構成がレポート用に正しく設定されていることを確認してください。", + "xpack.reporting.listing.diagnosticConfigTitle": "Kibana構成を検証", + "xpack.reporting.listing.diagnosticDescription": "診断を実行し、一般的なレポートの問題を自動的にトラブルシューティングします。", + "xpack.reporting.listing.diagnosticFailureDescription": "次に、一部の問題の詳細を示します。", + "xpack.reporting.listing.diagnosticFailureTitle": "正常に動作していない項目があります。", + "xpack.reporting.listing.diagnosticScreenshotButton": "スクリーンショットを作成", + "xpack.reporting.listing.diagnosticScreenshotMessage": "ヘッドレスブラウザーがページのスクリーンショットを作成できることを確認してください。", + "xpack.reporting.listing.diagnosticScreenshotTitle": "画面キャプチャを確認", + "xpack.reporting.listing.diagnosticSuccessMessage": "すべて問題なく、レポートは機能します。", + "xpack.reporting.listing.diagnosticSuccessTitle": "準備できました。", + "xpack.reporting.listing.diagnosticTitle": "レポート診断", + "xpack.reporting.listing.reports.subtitle": "Kibanaアプリケーションで生成されたレポートを取得します。", "xpack.reporting.listing.reportstitle": "レポート", "xpack.reporting.listing.table.csvContainsFormulas": "CSVには、スプレッドシートアプリケーションで式と解釈される可能性のある文字が含まれています。", "xpack.reporting.listing.table.deleteCancelButton": "キャンセル", @@ -13826,6 +15588,8 @@ "xpack.reporting.panelContent.successfullyQueuedReportNotificationTitle": "{objectType} のレポートキュー", "xpack.reporting.panelContent.whatCanBeExportedWarningDescription": "初めに変更内容を保存してください", "xpack.reporting.panelContent.whatCanBeExportedWarningTitle": "保存された {objectType} のみエクスポートできます", + "xpack.reporting.pdfFooterImageDescription": "PDFのフッターに使用するカスタム画像です", + "xpack.reporting.pdfFooterImageLabel": "PDFフッター画像", "xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportMessage": "レポートには、スプレッドシートアプリケーションで式と解釈される可能性のある文字が含まれています。", "xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportTitle": "レポートには式{reportObjectType} '{reportObjectTitle}'が含まれている場合があります", "xpack.reporting.publicNotifier.downloadReportButtonLabel": "レポートをダウンロード", @@ -13843,6 +15607,7 @@ "xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle": "{reportObjectType}「{reportObjectTitle}」のレポートが作成されました", "xpack.reporting.registerFeature.reportingDescription": "ディスカバリ、可視化、ダッシュボードから生成されたレポートを管理します。", "xpack.reporting.registerFeature.reportingTitle": "レポート", + "xpack.reporting.screencapture.browserWasClosed": "ブラウザーは予期せず終了しました。詳細については、サーバーログを確認してください。", "xpack.reporting.screencapture.couldntFinishRendering": "{count} 件のビジュアライゼーションのレンダリングが完了するのを待つ間にエラーが発生しました。「{configKey}」を増やす必要があるかもしれません。 {error}", "xpack.reporting.screencapture.couldntLoadKibana": "Kibana URL を開こうとするときにエラーが発生しました。「{configKey}」を増やす必要があるかもしれません。 {error}", "xpack.reporting.screencapture.injectCss": "Kibana CSS をレポート用に更新しようとしたときにエラーが発生しました。{error}", @@ -14150,6 +15915,11 @@ "xpack.security.apiKeys.breadcrumb": "API キー", "xpack.security.authentication.login.validateLogin.requiredPasswordErrorMessage": "パスワードが必要です", "xpack.security.authentication.login.validateLogin.requiredUsernameErrorMessage": "ユーザー名が必要です", + "xpack.security.checkup.dismissButtonText": "閉じる", + "xpack.security.checkup.dontShowAgain": "今後表示しない", + "xpack.security.checkup.enableButtonText": "セキュリティを有効にする", + "xpack.security.checkup.insecureClusterMessage": "当社の無料のセキュリティ機能を使用すると、不正アクセスから保護することができます。", + "xpack.security.checkup.insecureClusterTitle": "インストールを保護してください", "xpack.security.common.extendedRoleDeprecationNotice": "{roleName} ロールは非推奨です。{reason}", "xpack.security.components.sessionIdleTimeoutWarning.message": "操作がないため間もなくログアウト{timeout}します。再開するには [OK] をクリックしてください。", "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "OK", @@ -14231,7 +16001,7 @@ "xpack.security.management.deprecatedBadge": "非推奨", "xpack.security.management.disabledBadge": "無効", "xpack.security.management.editRole.cancelButtonLabel": "キャンセル", - "xpack.security.management.editRole.changeAllPrivilegesLink": "(すべて変更)", + "xpack.security.management.editRole.changeAllPrivilegesLink": "一斉アクション", "xpack.security.management.editRole.collapsiblePanel.hideLinkText": "非表示", "xpack.security.management.editRole.collapsiblePanel.showLinkText": "表示", "xpack.security.management.editRole.createRoleText": "ロールを作成", @@ -14252,7 +16022,12 @@ "xpack.security.management.editRole.elasticSearchPrivileges.learnMoreLinkText": "詳細", "xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription": "このロールがクラスターに対して実行できる操作を管理します。 ", "xpack.security.management.editRole.elasticSearchPrivileges.runAsPrivilegesTitle": "権限として実行", + "xpack.security.management.editRole.errorDeletingRoleError": "ロールの削除エラー", + "xpack.security.management.editRole.errorSavingRoleError": "ロールの保存エラー", "xpack.security.management.editRole.featureTable.customizeSubFeaturePrivilegesSwitchLabel": "サブ機能権限をカスタマイズする", + "xpack.security.management.editRole.featureTable.featureAccordionSwitchLabel": "{grantedCount} / {featureCount} {featureCount, plural, one {機能} other {機能}}が付与されました", + "xpack.security.management.editRole.featureTable.featureVisibilityTitle": "機能権限をカスタマイズ", + "xpack.security.management.editRole.featureTable.managementCategoryHelpText": "スタック管理へのアクセスは、ElasticsearchとKibanaの両方の権限によって決まり、明示的に無効にすることはできません。", "xpack.security.management.editRole.featureTable.privilegeCustomizationTooltip": "機能でサブ機能の権限がカスタマイズされています。この行を展開すると詳細が表示されます。", "xpack.security.management.editRole.indexPrivilegeForm.deleteSpacePrivilegeAriaLabel": "インデックスの権限を削除", "xpack.security.management.editRole.indexPrivilegeForm.grantedDocumentsQueryFormRowLabel": "提供されたドキュメントのクエリ", @@ -14290,7 +16065,7 @@ "xpack.security.management.editRole.simplePrivilegeForm.specifyPrivilegeForRoleDescription": "このロールの Kibana の権限を指定します。", "xpack.security.management.editRole.simplePrivilegeForm.unsupportedSpacePrivilegesWarning": "このロールはスペースへの権限が定義されていますが、Kibana でスペースが有効ではありません。このロールを保存するとこれらの権限が削除されます。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "{kibanaAdmin}ロールによりアカウントにすべての権限が提供されていることを確認し、再試行してください。", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "* グローバル (すべてのスペース)", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "*すべてのスペース", "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "利用可能なすべてのスペースを表示する権限がありません。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "権限が不十分です", "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin", @@ -14300,15 +16075,17 @@ "xpack.security.management.editRole.spacePrivilegeForm.featurePrivilegeSummaryDescription": "機能によってはスペースごとに非表示になっているか、グローバルスペース権限による影響を受けているものもあります。", "xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeNotice": "これらの権限はすべての現在および未来のスペースに適用されます。", "xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeWarning": "グローバル権限の作成は他のスペース権限に影響を与える可能性があります。", - "xpack.security.management.editRole.spacePrivilegeForm.modalTitle": "スペース権限", - "xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormLabel": "権限", + "xpack.security.management.editRole.spacePrivilegeForm.modalTitle": "Kibanaの権限", + "xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormHelpText": "このスペース全体の現在と将来のすべての機能に対して、付与する権限レベルを割り当てます。", + "xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormLabel": "すべての機能の権限", + "xpack.security.management.editRole.spacePrivilegeForm.spaceSelectorFormHelpText": "権限を割り当てる1つ以上のKibanaスペースを選択します。", "xpack.security.management.editRole.spacePrivilegeForm.spaceSelectorFormLabel": "スペース", "xpack.security.management.editRole.spacePrivilegeForm.summaryOfFeaturePrivileges": "機能権限のサマリー", "xpack.security.management.editRole.spacePrivilegeForm.supersededWarning": "宣言された権限は、構成済みグローバル権限よりも許容度が低くなります。権限サマリーを表示すると有効な権限がわかります。", "xpack.security.management.editRole.spacePrivilegeForm.supersededWarningTitle": "グローバル権限に置き換え", - "xpack.security.management.editRole.spacePrivilegeMatrix.globalSpaceName": "グローバル", + "xpack.security.management.editRole.spacePrivilegeMatrix.globalSpaceName": "すべてのスペース", "xpack.security.management.editRole.spacePrivilegeMatrix.showNMoreSpacesLink": "他 {count} 件", - "xpack.security.management.editRole.spacePrivilegeSection.addSpacePrivilegeButton": "スペース権限を追加", + "xpack.security.management.editRole.spacePrivilegeSection.addSpacePrivilegeButton": "Kibanaの権限を追加", "xpack.security.management.editRole.spacePrivilegeSection.noAccessToKibanaTitle": "このロールは Kibana へのアクセスを許可しません", "xpack.security.management.editRole.spacePrivilegeTable.deletePrivilegesLabel": "次のスペースの権限を削除: {spaceNames}", "xpack.security.management.editRole.spacePrivilegeTable.editPrivilegesLabel": "次のスペースの権限を編集: {spaceNames}", @@ -14426,7 +16203,7 @@ "xpack.security.management.editRoles.indexPrivilegeForm.grantedFieldsFormRowLabel": "許可されたフィールド", "xpack.security.management.editRoles.indexPrivilegeForm.grantFieldPrivilegesLabel": "特定のフィールドへのアクセスを許可", "xpack.security.management.editRolespacePrivilegeForm.createGlobalPrivilegeButton": "グローバル権限を作成", - "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "スペース権限を作成", + "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "Kibanaの権限を追加", "xpack.security.management.editRolespacePrivilegeForm.updateGlobalPrivilegeButton": "グローバル特権を更新", "xpack.security.management.editRolespacePrivilegeForm.updatePrivilegeButton": "スペース権限を更新", "xpack.security.management.enabledBadge": "有効", @@ -14553,8 +16330,12 @@ "xpack.security.overwrittenSession.continueAsUserText": "{username} として続行", "xpack.security.overwrittenSession.title": "以前別のユーザー名でログインしました。", "xpack.security.overwrittenSessionAppTitle": "上書きされたセッション", - "xpack.security.registerFeature.securitySettingsDescription": "ユーザーとロールでデータを保護し、誰が何にアクセスできるのか簡単に管理できます。", - "xpack.security.registerFeature.securitySettingsTitle": "セキュリティ設定", + "xpack.security.registerFeature.securitySettingsDescription": "アクセスできるユーザーと、ユーザーが実行できるタスクを制御します。", + "xpack.security.registerFeature.securitySettingsTitle": "アクセス権の管理", + "xpack.security.resetSession.description": "前のページに戻るか、別のユーザーでログインします。", + "xpack.security.resetSession.goBackButtonLabel": "戻る", + "xpack.security.resetSession.logOutButtonLabel": "別のユーザーでログイン", + "xpack.security.resetSession.title": "要求されたページにアクセスするための権限がありません。", "xpack.security.role_mappings.validation.invalidName": "名前が必要です。", "xpack.security.role_mappings.validation.invalidRoleRule": "1つ以上のルールが必要です。", "xpack.security.role_mappings.validation.invalidRoles": "1つ以上のロールが必要です。", @@ -14567,6 +16348,7 @@ "xpack.security.users.createBreadcrumb": "作成", "xpack.securitySolution.add_filter_to_global_search_bar.filterForValueHoverAction": "値でフィルター", "xpack.securitySolution.add_filter_to_global_search_bar.filterOutValueHoverAction": "値を除外", + "xpack.securitySolution.administration.list.beta": "ベータ", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "このルールで生成されたすべてのアラートのリスクスコアを選択します。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "デフォルトリスクスコア", "xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel": "ソースイベント値を使用して、デフォルトリスクスコアを上書きします。", @@ -14591,8 +16373,10 @@ "xpack.securitySolution.alertsView.errorFetchingAlertsData": "アラートデータをクエリできませんでした", "xpack.securitySolution.alertsView.moduleLabel": "モジュール", "xpack.securitySolution.alertsView.showing": "表示中", - "xpack.securitySolution.alertsView.totalCountOfAlerts": "外部アラートが検索条件に一致します", + "xpack.securitySolution.alertsView.totalCountOfAlerts": "外部アラート", "xpack.securitySolution.alertsView.unit": "外部{totalCount, plural, =1 {アラート} other {アラート}}", + "xpack.securitySolution.allHost.errorSearchDescription": "すべてのホスト検索でエラーが発生しました", + "xpack.securitySolution.allHost.failSearchDescription": "すべてのホストで検索を実行できませんでした", "xpack.securitySolution.andOrBadge.and": "AND", "xpack.securitySolution.andOrBadge.or": "OR", "xpack.securitySolution.anomaliesTable.table.anomaliesDescription": "異常", @@ -14721,6 +16505,8 @@ "xpack.securitySolution.auditd.violatedSeLinuxPolicyDescription": "selinuxポリシーに違反しました", "xpack.securitySolution.auditd.wasAuthorizedToUseDescription": "が以下の使用を承認されました。", "xpack.securitySolution.auditd.withResultDescription": "結果付き", + "xpack.securitySolution.authentications.errorSearchDescription": "認証検索でエラーが発生しました", + "xpack.securitySolution.authentications.failSearchDescription": "認証で検索を実行できませんでした", "xpack.securitySolution.authenticationsTable.authentications": "認証", "xpack.securitySolution.authenticationsTable.failures": "失敗", "xpack.securitySolution.authenticationsTable.lastFailedDestination": "前回失敗したデスティネーション", @@ -14736,7 +16522,13 @@ "xpack.securitySolution.authenticationsTable.user": "ユーザー", "xpack.securitySolution.authz.mlUnavailable": "機械学習プラグインが使用できません。プラグインを有効にしてください。", "xpack.securitySolution.authz.userIsNotMlAdminMessage": "現在のユーザーは機械学習管理者ではありません。", + "xpack.securitySolution.autocomplete.fieldRequiredError": "値を空にすることはできません", + "xpack.securitySolution.autocomplete.invalidDateError": "有効な日付ではありません", + "xpack.securitySolution.autocomplete.invalidNumberError": "有効な数値ではありません", "xpack.securitySolution.autocomplete.loadingDescription": "読み込み中…", + "xpack.securitySolution.autocomplete.selectField": "最初にフィールドを選択してください...", + "xpack.securitySolution.beatFields.errorSearchDescription": "Beatフィールドの取得でエラーが発生しました", + "xpack.securitySolution.beatFields.failSearchDescription": "Beatフィールドで検索を実行できませんでした", "xpack.securitySolution.case.allCases.actions": "アクション", "xpack.securitySolution.case.allCases.comments": "コメント", "xpack.securitySolution.case.allCases.noTagsAvailable": "利用可能なタグがありません", @@ -14814,6 +16606,7 @@ "xpack.securitySolution.case.caseView.emailBody": "ケースリファレンス: {caseUrl}", "xpack.securitySolution.case.caseView.emailSubject": "セキュリティケース - {caseTitle}", "xpack.securitySolution.case.caseView.errorsPushServiceCallOutTitle": "ケースを外部システムにプッシュするには、以下が必要です。", + "xpack.securitySolution.case.caseView.fieldChanged": "変更されたコネクターフィールド", "xpack.securitySolution.case.caseView.fieldRequiredError": "必須フィールド", "xpack.securitySolution.case.caseView.goToDocumentationButton": "ドキュメンテーションを表示", "xpack.securitySolution.case.caseView.moveToCommentAria": "参照されたコメントをハイライト", @@ -14822,8 +16615,6 @@ "xpack.securitySolution.case.caseView.noTags": "現在、このケースにタグは割り当てられていません。", "xpack.securitySolution.case.caseView.openedOn": "開始日", "xpack.securitySolution.case.caseView.optional": "オプション", - "xpack.securitySolution.case.caseView.pageBadgeLabel": "ベータ", - "xpack.securitySolution.case.caseView.pageBadgeTooltip": "ケースワークフローはまだベータです。Kibana repoで問題や不具合を報告して製品の改善にご協力ください。", "xpack.securitySolution.case.caseView.particpantsLabel": "参加者", "xpack.securitySolution.case.caseView.pushNamedIncident": "{ thirdParty }インシデントとしてプッシュ", "xpack.securitySolution.case.caseView.pushThirdPartyIncident": "外部インシデントとしてプッシュ", @@ -14849,6 +16640,7 @@ "xpack.securitySolution.case.caseView.unknown": "不明", "xpack.securitySolution.case.caseView.updateNamedIncident": "{ thirdParty }インシデントを更新", "xpack.securitySolution.case.caseView.updateThirdPartyIncident": "外部インシデントを更新", + "xpack.securitySolution.case.common.noConnector": "コネクターを選択していません", "xpack.securitySolution.case.configure.errorPushingToService": "サービスへのプッシュエラー", "xpack.securitySolution.case.configure.successSaveToast": "保存された外部接続設定", "xpack.securitySolution.case.configureCases.addNewConnector": "新しいコネクターを追加", @@ -14887,9 +16679,20 @@ "xpack.securitySolution.case.createCase.fieldTagsHelpText": "このケースの1つ以上のカスタム識別タグを入力します。新しいタグを開始するには、各タグの後でEnterを押します。", "xpack.securitySolution.case.createCase.titleFieldRequiredError": "タイトルが必要です。", "xpack.securitySolution.case.dismissErrorsPushServiceCallOutTitle": "閉じる", + "xpack.securitySolution.case.editConnector.editConnectorLinkAria": "クリックしてコネクターを編集", "xpack.securitySolution.case.pageTitle": "ケース", "xpack.securitySolution.case.readOnlySavedObjectDescription": "ケースを表示する権限のみが付与されています。ケースを開いて更新する必要がある場合は、Kibana管理者に連絡してください。", "xpack.securitySolution.case.readOnlySavedObjectTitle": "新しいケースを開いたり、既存のケースを更新したりすることはできません", + "xpack.securitySolution.case.settings.jira.issueTypesSelectFieldLabel": "問題タイプ", + "xpack.securitySolution.case.settings.jira.parentIssueSearchLabel": "親問題", + "xpack.securitySolution.case.settings.jira.prioritySelectFieldLabel": "優先度", + "xpack.securitySolution.case.settings.resilient.incidentTypesLabel": "インシデントタイプ", + "xpack.securitySolution.case.settings.resilient.incidentTypesPlaceholder": "タイプを選択", + "xpack.securitySolution.case.settings.resilient.severityLabel": "深刻度", + "xpack.securitySolution.case.settings.resilient.unableToGetIncidentTypesMessage": "インシデントタイプを取得できません", + "xpack.securitySolution.case.settings.resilient.unableToGetSeverityMessage": "深刻度を取得できません", + "xpack.securitySolution.caseSettingsRegistry.get.missingCaseSettingErrorMessage": "オブジェクトタイプ「{id}」は登録されていません。", + "xpack.securitySolution.caseSettingsRegistry.register.duplicateCaseSettingErrorMessage": "オブジェクトタイプ「{id}」は既に登録されています。", "xpack.securitySolution.certificate.fingerprint.clientCertLabel": "クライアント証明書", "xpack.securitySolution.certificate.fingerprint.serverCertLabel": "サーバー証明書", "xpack.securitySolution.chart.allOthersGroupingLabel": "その他すべて", @@ -14902,6 +16705,8 @@ "xpack.securitySolution.clipboard.copy": "コピー", "xpack.securitySolution.clipboard.copy.to.the.clipboard": "クリップボードにコピー", "xpack.securitySolution.clipboard.to.the.clipboard": "クリップボードに", + "xpack.securitySolution.components.create.stepOneTitle": "ケースフィールド", + "xpack.securitySolution.components.create.stepTwoTitle": "外部インシデント管理システムフィールド", "xpack.securitySolution.components.embeddables.embeddedMap.clientLayerLabel": "クライアントポイント", "xpack.securitySolution.components.embeddables.embeddedMap.destinationLayerLabel": "デスティネーションポイント", "xpack.securitySolution.components.embeddables.embeddedMap.embeddableHeaderHelp": "マップ構成ヘルプ", @@ -14959,7 +16764,7 @@ "xpack.securitySolution.components.mlPopup.hooks.errors.siemJobFetchFailureTitle": "セキュリティジョブ取得エラー", "xpack.securitySolution.components.mlPopup.jobsTable.createCustomJobButtonLabel": "カスタムジョブを作成", "xpack.securitySolution.components.mlPopup.jobsTable.jobNameColumn": "ジョブ名", - "xpack.securitySolution.components.mlPopup.jobsTable.noItemsDescription": "SIEM機械学習ジョブが見つかりませんでした", + "xpack.securitySolution.components.mlPopup.jobsTable.noItemsDescription": "セキュリティ機械学習ジョブが見つかりません", "xpack.securitySolution.components.mlPopup.jobsTable.runJobColumn": "ジョブを実行", "xpack.securitySolution.components.mlPopup.jobsTable.tagsColumn": "グループ", "xpack.securitySolution.components.mlPopup.licenseButtonLabel": "ライセンスの管理", @@ -14971,6 +16776,19 @@ "xpack.securitySolution.components.mlPopup.upgradeButtonLabel": "サブスクリプションオプション", "xpack.securitySolution.components.mlPopup.upgradeDescription": "SIEMの異常検出機能にアクセスするには、ライセンスをプラチナに更新するか、30日間の無料トライアルを開始するか、AWS、GCP、またはAzureで{cloudLink}にサインアップしてください。その後、機械学習ジョブを実行して異常を表示できます。", "xpack.securitySolution.components.mlPopup.upgradeTitle": "E lastic Platinumへのアップグレード", + "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel": "親問題を選択", + "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder": "親問題を選択", + "xpack.securitySolution.components.settings.jira.searchIssuesLoading": "読み込み中…", + "xpack.securitySolution.components.settings.jira.unableToGetFieldsMessage": "フィールドを取得できません", + "xpack.securitySolution.components.settings.jira.unableToGetIssueMessage": "ID {id}の問題を取得できません", + "xpack.securitySolution.components.settings.jira.unableToGetIssuesMessage": "問題を取得できません", + "xpack.securitySolution.components.settings.jira.unableToGetIssueTypesMessage": "問題タイプを取得できません", + "xpack.securitySolution.components.settings.serviceNow.impactSelectFieldLabel": "インパクト", + "xpack.securitySolution.components.settings.serviceNow.severitySelectFieldLabel": "深刻度", + "xpack.securitySolution.components.settings.servicenow.severitySelectHighOptionLabel": "高", + "xpack.securitySolution.components.settings.servicenow.severitySelectLowOptionLabel": "低", + "xpack.securitySolution.components.settings.servicenow.severitySelectMediumOptionLabel": "中", + "xpack.securitySolution.components.settings.serviceNow.urgencySelectFieldLabel": "緊急", "xpack.securitySolution.components.stepDefineRule.ruleTypeField.subscriptionsLink": "プラチナサブスクリプション", "xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "異常データをクエリできませんでした", "xpack.securitySolution.containers.anomalies.stackByJobId": "ジョブ", @@ -15037,6 +16855,7 @@ "xpack.securitySolution.detectionEngine.alerts.actions.addException": "ルール例外の追加", "xpack.securitySolution.detectionEngine.alerts.actions.closeAlertTitle": "アラートを閉じる", "xpack.securitySolution.detectionEngine.alerts.actions.inProgressAlertTitle": "実行中に設定", + "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineAriaLabel": "アラートをタイムラインに送信", "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineTitle": "タイムラインで調査", "xpack.securitySolution.detectionEngine.alerts.actions.openAlertTitle": "アラートを開く", "xpack.securitySolution.detectionEngine.alerts.closedAlertFailedToastMessage": "アラートをクローズできませんでした。", @@ -15066,7 +16885,8 @@ "xpack.securitySolution.detectionEngine.alerts.openAlertsTitle": "開く", "xpack.securitySolution.detectionEngine.alerts.openedAlertFailedToastMessage": "アラートを開けませんでした", "xpack.securitySolution.detectionEngine.alerts.openedAlertSuccessToastMessage": "{totalAlerts} {totalAlerts, plural, =1 {件のアラート} other {件のアラート}}を正常に開きました。", - "xpack.securitySolution.detectionEngine.alerts.totalCountOfAlertsTitle": "アラートが検索条件に一致します", + "xpack.securitySolution.detectionEngine.alerts.totalCountOfAlertsTitle": "アラート", + "xpack.securitySolution.detectionEngine.alerts.updateAlertStatusFailedSingleAlert": "アラートを更新できませんでした。アラートはすでに修正されています。", "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showBuildingBlockTitle": "基本アラートを含める", "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersTitle": "追加のフィルター", "xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.closeSelectedTitle": "選択した項目を閉じる", @@ -15095,6 +16915,7 @@ "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle": "有効化せずにルールを作成", "xpack.securitySolution.detectionEngine.createRule.backToRulesDescription": "検出ルールに戻る", "xpack.securitySolution.detectionEngine.createRule.editRuleButton": "編集", + "xpack.securitySolution.detectionEngine.createRule.eqlRuleTypeDescription": "イベント相関関係", "xpack.securitySolution.detectionEngine.createRule.filtersLabel": "フィルター", "xpack.securitySolution.detectionEngine.createRule.mlRuleTypeDescription": "機械学習", "xpack.securitySolution.detectionEngine.createRule.pageTitle": "新規ルールを作成", @@ -15142,10 +16963,16 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customMitreAttackTechniquesFieldRequiredError": "Tacticには1つ以上のTechniqueが必要です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError": "KQLが無効です", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError": "カスタムクエリが必要です。", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "すべての一致には、フィールドと脅威インデックスフィールドの両方が必要です。", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError": "1つ以上の脅威一致が必要です。", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.EqlQueryBarLabel": "EQLクエリ", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel": "異常スコアしきい値", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel": "機械学習ジョブ", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel": "カスタムクエリ", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel": "ルールタイプ", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatIndexPatternsLabel": "脅威インデックスパターン", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatMappingLabel": "脅威マッピング", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatQueryBarLabel": "脅威インデックスクエリ", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "しきい値", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.importTimelineModalTitle": "保存されたタイムラインからクエリをインポート", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.importTimelineQueryButton": "保存されたタイムラインからクエリをインポート", @@ -15159,18 +16986,26 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError": "インデックスパターンが最低1つ必要です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.referencesUrlInvalidError": "URLの形式が無効です", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.resetDefaultIndicesButton": "デフォルトインデックスパターンにリセット", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.eqlTypeDescription": "イベントクエリ言語(EQL)を使用して、イベントを照合したり、シーケンスを生成したり、データをスタックしたりします。", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.eqlTypeTitle": "イベント相関関係", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDescription": "異常なアクティビティを検出するための ML ジョブを選択します。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription": "ML にアクセスするには {subscriptionsLink} が必要です", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeTitle": "機械学習", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.queryTypeDescription": "KQL または Lucene を使用して、インデックス全体にわたる問題を検出します。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.queryTypeTitle": "カスタムクエリ", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchDescription": "値リストをアップロードし、確認済みの悪い属性のリストでルールを書き込みます。", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchTitle": "脅威一致", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeDescription": "クエリ結果を集約し、いつ一致数がしきい値を超えるのかを検出します。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeTitle": "しきい値", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchField.threatMatchFieldPlaceholderText": "すべての結果", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription": "脅威インデックスを選択", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError": "インデックスパターンが最低1つ必要です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText": "すべての結果", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText": "ルールが true であると評価された場合に自動アクションを実行するタイミングを選択します。", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel": "アクション頻度", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.invalidMustacheTemplateErrorMessage": "{key}は有効なmustacheテンプレートではありません", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.noConnectorSelectedErrorMessage": "コネクターを選択していません", + "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.noReadActionsPrivileges": "ルールアクションを作成できません「Actions」プラグインの「読み取り」アクセス権がありません。", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithActivatingTitle": "ルールの作成と有効化", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithoutActivatingTitle": "有効化せずにルールを作成", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText": "ルックバック期間に時間を追加してアラートの見落としを防ぎます。", @@ -15181,6 +17016,8 @@ "xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.invalidTimeMessageDescription": "時間が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.minutesOptionDescription": "分", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.secondsOptionDescription": "秒", + "xpack.securitySolution.detectionEngine.createRule.threatMatchRuleTypeDescription": "脅威一致", + "xpack.securitySolution.detectionEngine.createRule.threatQueryLabel": "脅威クエリ", "xpack.securitySolution.detectionEngine.createRule.thresholdRuleTypeDescription": "しきい値", "xpack.securitySolution.detectionEngine.details.stepAboutRule.aboutText": "概要", "xpack.securitySolution.detectionEngine.details.stepAboutRule.detailsLabel": "詳細", @@ -15198,9 +17035,12 @@ "xpack.securitySolution.detectionEngine.emptyActionBeats": "セットアップの手順を表示", "xpack.securitySolution.detectionEngine.emptyActionSecondary": "ドキュメントに移動", "xpack.securitySolution.detectionEngine.emptyTitle": "セキュリティアプリケーションの検出エンジンに関連したインデックスがないようです", + "xpack.securitySolution.detectionEngine.eqlOverViewLink.text": "イベントクエリ言語(EQL)の概要", + "xpack.securitySolution.detectionEngine.eqlQueryBar.label": "EQLクエリを入力", + "xpack.securitySolution.detectionEngine.eqlValidation.requestError": "EQLクエリの確認中にエラーが発生しました", + "xpack.securitySolution.detectionEngine.eqlValidation.showErrorsLabel": "EQL確認エラーを表示", + "xpack.securitySolution.detectionEngine.eqlValidation.title": "EQL確認エラー", "xpack.securitySolution.detectionEngine.goToDocumentationButton": "ドキュメンテーションを表示", - "xpack.securitySolution.detectionEngine.headerPage.pageBadgeLabel": "ベータ", - "xpack.securitySolution.detectionEngine.headerPage.pageBadgeTooltip": "アラートはまだベータ段階です。Kibana repoで問題や不具合を報告して製品の改善にご協力ください。", "xpack.securitySolution.detectionEngine.lastSignalTitle": "前回のアラート", "xpack.securitySolution.detectionEngine.mitreAttack.addTitle": "MITRE ATT&CK\\u2122脅威を追加", "xpack.securitySolution.detectionEngine.mitreAttack.tacticPlaceHolderDescription": "Tacticを追加...", @@ -15498,6 +17338,20 @@ "xpack.securitySolution.detectionEngine.noWriteAlertsCallOutTitle": "アラート状態を変更することはできません", "xpack.securitySolution.detectionEngine.pageTitle": "検出エンジン", "xpack.securitySolution.detectionEngine.panelSubtitleShowing": "表示中", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "カウント", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer": "注:このプレビューは、ルール例外とタイムスタンプオーバーライドの効果を除外します。", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql": "注:このプレビューは、ルール例外とタイムスタンプオーバーライドの効果を除外します。結果は100件に制限されます。", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphHitsTitle": "ヒット数", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "プレビュー取得エラー", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "クエリプレビュー", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "ノイズ警告:このルールではノイズが多く生じる可能性があります。クエリを絞り込むことを検討してください。これは1時間ごとに1アラートという線形進行に基づいています。", + "xpack.securitySolution.detectionEngine.queryPreview.queryNoHits": "ヒットが見つかりませんでした。", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphCapHitWarning": "クエリの上限サイズ{cap}に達しました。このクエリは表示されている{cap}を超えるヒットを生成できませんでした。", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphThresholdWithFieldTitle": "{buckets} {buckets, plural, =1 {固有のヒット} other {固有のヒット}}", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTimestampWarning": "イベントで「@timestamp」フィールドが見つかりません", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTitle": "{hits} {hits, plural, =1 {ヒット} other {ヒット}}", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "クエリ結果をプレビューするデータのタイムフレームを選択します", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "クイッククエリプレビュー", "xpack.securitySolution.detectionEngine.readOnlyCallOutMsg": "現在、検出エンジンルールを作成/編集するための必要な権限がありません。サポートについては、管理者にお問い合わせください。", "xpack.securitySolution.detectionEngine.readOnlyCallOutTitle": "ルールアクセス権が必要です", "xpack.securitySolution.detectionEngine.rule.editRule.errorMsgDescription": "{countError, plural, one {このタブ} other {これらのタブ}}に無効な入力があります: {tabHasError}", @@ -15523,21 +17377,21 @@ "xpack.securitySolution.detectionEngine.rules.aboutRuleTitle": "ルールについて", "xpack.securitySolution.detectionEngine.rules.addNewRuleTitle": "新規ルールを作成", "xpack.securitySolution.detectionEngine.rules.addPageTitle": "作成", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteeRuleDescription": "ルールの削除...", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "ルールの複製...", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleErrorDescription": "ルールの複製エラー...", + "xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteeRuleDescription": "ルールの削除", + "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "ルールの複製", + "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleErrorDescription": "ルールの複製エラー", "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateTitle": "複製", "xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription": "ルール設定の編集", "xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "ルールのエクスポート", "xpack.securitySolution.detectionEngine.rules.allRules.activeRuleDescription": "アクティブ", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedErrorTitle": "{totalRules, plural, =1 {ルール} other {ルール}}の有効化エラー…", + "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedErrorTitle": "{totalRules, plural, =1 {ルール} other {ルール}}の有効化エラー", "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedTitle": "選択した項目の有効化", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedErrorTitle": "{totalRules, plural, =1 {ルール} other {ルール}}の無効化エラー…", + "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedErrorTitle": "{totalRules, plural, =1 {ルール} other {ルール}}の無効化エラー", "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedTitle": "選択した項目の無効化", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedErrorTitle": "{totalRules, plural, =1 {ルール} other {ルール}}の削除エラー…", + "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedErrorTitle": "{totalRules, plural, =1 {ルール} other {ルール}}の削除エラー", "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "選択には削除できないイミュータブルルールがあります", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle": "選択項目を削除...", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle": "選択した項目の複製…", + "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle": "選択した項目を削除", + "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle": "選択した項目の複製", "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.exportSelectedTitle": "選択した項目のエクスポート", "xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "一斉アクション", "xpack.securitySolution.detectionEngine.rules.allRules.columns.activateTitle": "有効化", @@ -15546,11 +17400,14 @@ "xpack.securitySolution.detectionEngine.rules.allRules.columns.lastLookBackDate": "前回の確認日", "xpack.securitySolution.detectionEngine.rules.allRules.columns.lastResponseTitle": "前回の応答", "xpack.securitySolution.detectionEngine.rules.allRules.columns.lastRunTitle": "前回の実行", + "xpack.securitySolution.detectionEngine.rules.allRules.columns.lastUpdateTitle": "最終更新", "xpack.securitySolution.detectionEngine.rules.allRules.columns.queryTimes": "クエリ時間(ミリ秒)", "xpack.securitySolution.detectionEngine.rules.allRules.columns.riskScoreTitle": "リスクスコア", "xpack.securitySolution.detectionEngine.rules.allRules.columns.ruleTitle": "ルール", "xpack.securitySolution.detectionEngine.rules.allRules.columns.severityTitle": "深刻度", + "xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsPopoverTitle": "すべて表示", "xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsTitle": "タグ", + "xpack.securitySolution.detectionEngine.rules.allRules.columns.versionTitle": "バージョン", "xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle": "rules_export", "xpack.securitySolution.detectionEngine.rules.allRules.filters.customRulesTitle": "カスタムルール", "xpack.securitySolution.detectionEngine.rules.allRules.filters.elasticRulesTitle": "Elasticルール", @@ -15577,14 +17434,13 @@ "xpack.securitySolution.detectionEngine.rules.defineRuleTitle": "ルールの定義", "xpack.securitySolution.detectionEngine.rules.deleteDescription": "削除", "xpack.securitySolution.detectionEngine.rules.editPageTitle": "編集", - "xpack.securitySolution.detectionEngine.rules.importRuleTitle": "ルールのインポート...", + "xpack.securitySolution.detectionEngine.rules.importRuleTitle": "ルールのインポート", "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton": "Elastic事前構築済みルールおよびタイムラインテンプレートを読み込む", "xpack.securitySolution.detectionEngine.rules.optionalFieldDescription": "オプション", "xpack.securitySolution.detectionEngine.rules.pageTitle": "検出ルール", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton": "独自のルールの作成", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elasticセキュリティには、バックグラウンドで実行され、条件が合うとアラートを作成する事前構築済み検出ルールがあります。デフォルトでは、Elastic Endpoint Securityルールを除くすべての事前構築済みルールが無効になっています。有効にする追加のルールを選択することができます。", + "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elasticセキュリティには、バックグラウンドで実行され、条件が合うとアラートを作成する事前構築済み検出ルールがあります。デフォルトでは、Endpoint Securityルールを除くすべての事前構築済みルールが無効になっています。有効にする追加のルールを選択することができます。", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle": "Elastic事前構築済み検出ルールを読み込む", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "事前構築済み検出ルールおよびタイムラインテンプレートを読み込む", "xpack.securitySolution.detectionEngine.rules.releaseNotesHelp": "リリースノート", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton": "{missingRules} Elastic事前構築済み{missingRules, plural, =1 {ルール} other {ルール}}と{missingTimelines} Elastic事前構築済み{missingTimelines, plural, =1 {タイムライン} other {タイムライン}}をインストール ", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton": "{missingRules} Elasticの事前構築済みの{missingRules, plural, =1 {個のルール} other {個のルール}}をインストール ", @@ -15635,10 +17491,91 @@ "xpack.securitySolution.editDataProvider.selectAnOperatorPlaceholder": "演算子を選択", "xpack.securitySolution.editDataProvider.valueLabel": "値", "xpack.securitySolution.editDataProvider.valuePlaceholder": "値", - "xpack.securitySolution.emptyMessage": "Elastic セキュリティは無料かつオープンのElastic SIEMに、Elastic Endpoint Securityを搭載。脅威の防御と検知、脅威への対応を支援します。開始するには、セキュリティソリューション関連データをElastic Stackに追加する必要があります。詳細については、以下をご覧ください ", + "xpack.securitySolution.emptyMessage": "Elastic Securityは無料かつオープンのElastic SIEMに、Endpoint Securityを搭載。脅威の防御と検知、脅威への対応を支援します。開始するには、セキュリティソリューション関連データをElastic Stackに追加する必要があります。詳細については、以下をご覧ください ", "xpack.securitySolution.emptyString.emptyStringDescription": "空の文字列", + "xpack.securitySolution.endpoint.details.endpointVersion": "エンドポイントバージョン", + "xpack.securitySolution.endpoint.details.errorBody": "フライアウトを終了して、利用可能なホストを選択してください。", + "xpack.securitySolution.endpoint.details.errorTitle": "ホストが見つかりませんでした", + "xpack.securitySolution.endpoint.details.hostname": "ホスト名", + "xpack.securitySolution.endpoint.details.ipAddress": "IPアドレス", + "xpack.securitySolution.endpoint.details.lastSeen": "前回の認識", + "xpack.securitySolution.endpoint.details.linkToIngestTitle": "ポリシーの再割り当て", + "xpack.securitySolution.endpoint.details.noPolicyResponse": "ポリシー応答がありません", + "xpack.securitySolution.endpoint.details.os": "OS", + "xpack.securitySolution.endpoint.details.policy": "統合ポリシー", + "xpack.securitySolution.endpoint.details.policyResponse.configure_dns_events": "DNSイベントの構成", + "xpack.securitySolution.endpoint.details.policyResponse.configure_elasticsearch_connection": "Elastic Search接続の構成", + "xpack.securitySolution.endpoint.details.policyResponse.configure_file_events": "ファイルイベントの構成", + "xpack.securitySolution.endpoint.details.policyResponse.configure_imageload_events": "画像読み込みイベントの構成", + "xpack.securitySolution.endpoint.details.policyResponse.configure_kernel": "カーネルの構成", + "xpack.securitySolution.endpoint.details.policyResponse.configure_logging": "ロギングの構成", + "xpack.securitySolution.endpoint.details.policyResponse.configure_malware": "マルウェアの構成", + "xpack.securitySolution.endpoint.details.policyResponse.configure_network_events": "ネットワークイベントの構成", + "xpack.securitySolution.endpoint.details.policyResponse.configure_process_events": "プロセスイベントの構成", + "xpack.securitySolution.endpoint.details.policyResponse.configure_registry_events": "レジストリイベントの構成", + "xpack.securitySolution.endpoint.details.policyResponse.configure_security_events": "セキュリティイベントの構成", + "xpack.securitySolution.endpoint.details.policyResponse.connect_kernel": "カーネルを接続", + "xpack.securitySolution.endpoint.details.policyResponse.detect_async_image_load_events": "非同期画像読み込みイベントを検出", + "xpack.securitySolution.endpoint.details.policyResponse.detect_file_open_events": "ファイルオープンイベントを検出", + "xpack.securitySolution.endpoint.details.policyResponse.detect_file_write_events": "ファイル書き込みイベントを検出", + "xpack.securitySolution.endpoint.details.policyResponse.detect_network_events": "ネットワークイベントを検出", + "xpack.securitySolution.endpoint.details.policyResponse.detect_process_events": "プロセスイベントを検出", + "xpack.securitySolution.endpoint.details.policyResponse.detect_registry_events": "レジストリイベントを検出", + "xpack.securitySolution.endpoint.details.policyResponse.detect_sync_image_load_events": "同期画像読み込みイベントを検出", + "xpack.securitySolution.endpoint.details.policyResponse.download_global_artifacts": "グローバルアーチファクトをダウンロード", + "xpack.securitySolution.endpoint.details.policyResponse.download_user_artifacts": "ユーザーアーチファクトをダウンロード", + "xpack.securitySolution.endpoint.details.policyResponse.events": "イベント", + "xpack.securitySolution.endpoint.details.policyResponse.failed": "失敗", + "xpack.securitySolution.endpoint.details.policyResponse.load_config": "構成の読み込み", + "xpack.securitySolution.endpoint.details.policyResponse.load_malware_model": "マルウェアモデルの読み込み", + "xpack.securitySolution.endpoint.details.policyResponse.logging": "ログ", + "xpack.securitySolution.endpoint.details.policyResponse.malware": "マルウェア", + "xpack.securitySolution.endpoint.details.policyResponse.read_elasticsearch_config": "Elasticsearch構成を読み取る", + "xpack.securitySolution.endpoint.details.policyResponse.read_events_config": "イベント構成を読み取る", + "xpack.securitySolution.endpoint.details.policyResponse.read_kernel_config": "カーネル構成を読み取る", + "xpack.securitySolution.endpoint.details.policyResponse.read_logging_config": "ロギング構成を読み取る", + "xpack.securitySolution.endpoint.details.policyResponse.read_malware_config": "マルウェア構成を読み取る", + "xpack.securitySolution.endpoint.details.policyResponse.streaming": "ストリーム中", + "xpack.securitySolution.endpoint.details.policyResponse.success": "成功", + "xpack.securitySolution.endpoint.details.policyResponse.warning": "警告", + "xpack.securitySolution.endpoint.details.policyResponse.workflow": "ワークフロー", + "xpack.securitySolution.endpoint.details.policyStatus": "ポリシー応答", + "xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失敗} other {不明}}", + "xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "推奨のデフォルト値で統合が保存されます。後からこれを変更するには、エージェントポリシー内でEndpoint Security統合を編集します。", + "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.actionSecurityPolicy": "セキュリティポリシーを編集", + "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.actionTrustedApps": "信頼できるアプリケーションを表示", + "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.menuButton": "アクション", + "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.message": "詳細構成オプションを表示するには、メニューからアクションを選択します。", + "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.trustedAppsMessageReturnBackLabel": "統合の編集に戻る", "xpack.securitySolution.endpoint.ingestToastMessage": "Ingest Managerが設定中に失敗しました。", "xpack.securitySolution.endpoint.ingestToastTitle": "アプリを初期化できませんでした", + "xpack.securitySolution.endpoint.list.actionmenu": "開く", + "xpack.securitySolution.endpoint.list.actions": "アクション", + "xpack.securitySolution.endpoint.list.actions.agentDetails": "エージェント詳細を表示", + "xpack.securitySolution.endpoint.list.actions.agentPolicy": "エージェントポリシーを表示", + "xpack.securitySolution.endpoint.list.actions.hostDetails": "ホスト詳細を表示", + "xpack.securitySolution.endpoint.list.endpointsEnrolling": "エンドポイントを登録しています。進行状況を追跡するには、{agentsLink}してください。", + "xpack.securitySolution.endpoint.list.endpointsEnrolling.viewAgentsLink": "エージェントを表示", + "xpack.securitySolution.endpoint.list.endpointVersion": "バージョン", + "xpack.securitySolution.endpoint.list.hostname": "ホスト名", + "xpack.securitySolution.endpoint.list.hostStatus": "エージェントステータス", + "xpack.securitySolution.endpoint.list.hostStatusValue": "{hostStatus, select, online {オンライン} error {エラー} unenrolling {登録解除中} other {オフライン}}", + "xpack.securitySolution.endpoint.list.ip": "IPアドレス", + "xpack.securitySolution.endpoint.list.lastActive": "前回のアーカイブ", + "xpack.securitySolution.endpoint.list.loadingPolicies": "統合を読み込んでいます", + "xpack.securitySolution.endpoint.list.noEndpointsInstructions": "Endpoint Security統合を追加しました。次の手順を使用して、エージェントを登録してください。", + "xpack.securitySolution.endpoint.list.noEndpointsPrompt": "次のステップ:Endpoint Securityでエージェントを登録してください", + "xpack.securitySolution.endpoint.list.noPolicies": "統合はありません。", + "xpack.securitySolution.endpoint.list.os": "オペレーティングシステム", + "xpack.securitySolution.endpoint.list.pageSubTitle": "Endpoint Securityを実行しているホスト", + "xpack.securitySolution.endpoint.list.pageTitle": "エンドポイント", + "xpack.securitySolution.endpoint.list.policy": "統合ポリシー", + "xpack.securitySolution.endpoint.list.policyStatus": "ポリシーステータス", + "xpack.securitySolution.endpoint.list.stepOne": "既存の統合から選択してください。これは後から変更できます。", + "xpack.securitySolution.endpoint.list.stepOneTitle": "使用する統合を選択", + "xpack.securitySolution.endpoint.list.stepTwo": "開始するために必要なコマンドが提供されます。", + "xpack.securitySolution.endpoint.list.stepTwoTitle": "Ingest Manager経由でEndpoint Securityによって有効にされたエージェントを登録", + "xpack.securitySolution.endpoint.list.totalCount": "{totalItemCount, plural, one {# ホスト} other {# ホスト}}", "xpack.securitySolution.endpoint.policy.details.backToListTitle": "エンドポイントホストに戻る", "xpack.securitySolution.endpoint.policy.details.cancel": "キャンセル", "xpack.securitySolution.endpoint.policy.details.detect": "検知", @@ -15658,7 +17595,7 @@ "xpack.securitySolution.endpoint.policy.details.updateConfirm.confirmButtonTitle": "変更を保存してデプロイ", "xpack.securitySolution.endpoint.policy.details.updateConfirm.message": "この操作は元に戻すことができません。続行していいですか?", "xpack.securitySolution.endpoint.policy.details.updateConfirm.title": "変更を保存してデプロイ", - "xpack.securitySolution.endpoint.policy.details.updateConfirm.warningMessage": "これらの変更を保存すると、このエージェント構成に割り当てられたすべての有効なエンドポイントに更新が適用されます。", + "xpack.securitySolution.endpoint.policy.details.updateConfirm.warningMessage": "これらの変更を保存すると、このエージェントポリシー成に割り当てられたすべての有効なエンドポイントに更新が適用されます。", "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失敗しました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "統合{name}が更新されました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", @@ -15668,7 +17605,7 @@ "xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle": "エラー", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle": "オフライン", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.onlineTitle": "オンライン", - "xpack.securitySolution.endpoint.policyDetails.agentsSummary.totalTitle": "ホスト", + "xpack.securitySolution.endpoint.policyDetails.agentsSummary.totalTitle": "エンドポイント", "xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents": "イベント", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file": "ファイル", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network": "ネットワーク", @@ -15687,6 +17624,7 @@ "xpack.securitySolution.endpoint.policyDetailType": "タイプ", "xpack.securitySolution.endpoint.policyList.actionButtonText": "Endpoint Securityを追加", "xpack.securitySolution.endpoint.policyList.actionMenu": "開く", + "xpack.securitySolution.endpoint.policyList.agentPolicyAction": "エージェントポリシーを表示", "xpack.securitySolution.endpoint.policyList.createdAt": "作成日時", "xpack.securitySolution.endpoint.policyList.createdBy": "作成者", "xpack.securitySolution.endpoint.policyList.createNewButton": "新しいポリシーを作成", @@ -15704,10 +17642,10 @@ "xpack.securitySolution.endpoint.policyList.emptyCreateNewButton": "エージェントの登録", "xpack.securitySolution.endpoint.policyList.nameField": "ポリシー名", "xpack.securitySolution.endpoint.policyList.onboardingDocsLink": "セキュリティアプリドキュメントを表示", - "xpack.securitySolution.endpoint.policyList.onboardingSectionOne": "Elastic Endpoint Securityでは、脅威防御、検出、深いセキュリティデータの可視化を実現し、ホストを保護します。", - "xpack.securitySolution.endpoint.policyList.onboardingSectionThree": "開始するには、Elastic Endpoint Security統合をエージェントに追加します。詳細については、 ", - "xpack.securitySolution.endpoint.policyList.onboardingSectionTwo": "このページでは、Elastic Endpoint Securityを実行している環境でホストを表示して管理できます。", - "xpack.securitySolution.endpoint.policyList.onboardingTitle": "Elastic Endpoint Securityの基本", + "xpack.securitySolution.endpoint.policyList.onboardingSectionOne": "Endpoint Securityでは、脅威防御、検出、深いセキュリティデータの可視化を実現し、ホストを保護します。", + "xpack.securitySolution.endpoint.policyList.onboardingSectionThree": "開始するには、Endpoint Security統合をエージェントに追加します。詳細については、 ", + "xpack.securitySolution.endpoint.policyList.onboardingSectionTwo": "このページでは、Endpoint Securityを実行している環境でホストを表示して管理できます。", + "xpack.securitySolution.endpoint.policyList.onboardingTitle": "Endpoint Securityの基本", "xpack.securitySolution.endpoint.policyList.policyDeleteAction": "ポリシーを削除", "xpack.securitySolution.endpoint.policyList.revision": "rev. {revNumber}", "xpack.securitySolution.endpoint.policyList.updatedAt": "最終更新", @@ -15715,17 +17653,23 @@ "xpack.securitySolution.endpoint.policyList.versionField": "v{version}", "xpack.securitySolution.endpoint.policyList.versionFieldLabel": "バージョン", "xpack.securitySolution.endpoint.policyList.viewTitleTotalCount": "{totalItemCount, plural, one {# ポリシー} other {# ポリシー}}", + "xpack.securitySolution.endpoint.policyResponse.backLinkTitle": "エンドポイント詳細", + "xpack.securitySolution.endpoint.policyResponse.title": "ポリシー応答", "xpack.securitySolution.endpoint.resolver.eitherLineageLimitExceeded": "以下のビジュアライゼーションとイベントリストの一部のプロセスイベントを表示できませんでした。データの上限に達しました。", "xpack.securitySolution.endpoint.resolver.elapsedTime": "{duration} {durationType}", "xpack.securitySolution.endpoint.resolver.loadingError": "データの読み込み中にエラーが発生しました。", "xpack.securitySolution.endpoint.resolver.panel.error.error": "エラー", "xpack.securitySolution.endpoint.resolver.panel.error.events": "イベント", - "xpack.securitySolution.endpoint.resolver.panel.error.goBack": "このリンクをクリックすると、すべてのプロセスのリストに戻ります。", + "xpack.securitySolution.endpoint.resolver.panel.error.goBack": "すべてのプロセスを表示", + "xpack.securitySolution.endpoint.resolver.panel.nodeEventsByType.errorPrimary": "イベントを読み込めません。", + "xpack.securitySolution.endpoint.resolver.panel.nodeEventsByType.errorSecondary": "イベントの取得中にエラーが発生しました。", + "xpack.securitySolution.endpoint.resolver.panel.nodeEventsByType.loadMore": "その他のデータ尾読み込む", "xpack.securitySolution.endpoint.resolver.panel.processDescList.events": "イベント", + "xpack.securitySolution.endpoint.resolver.panel.processDescList.numberOfEvents": "{relatedEventTotal}件のイベント", "xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events": "イベント", "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events": "イベント", "xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb": "{totalCount}件のイベント", - "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "イベントを待機しています...", + "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "イベントを読み込み中...", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime": "@ {date}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType": "{category} {eventType}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.countByCategory": "{count} {category}", @@ -15739,6 +17683,7 @@ "xpack.securitySolution.endpoint.resolver.panel.table.row.processNameTitle": "プロセス名", "xpack.securitySolution.endpoint.resolver.panel.table.row.timestampTitle": "タイムスタンプ", "xpack.securitySolution.endpoint.resolver.panel.table.row.valueMissingDescription": "値が見つかりません", + "xpack.securitySolution.endpoint.resolver.processDescription": "{isEventBeingAnalyzed, select, true {分析されたイベント· {descriptionText}} false {{descriptionText}}}", "xpack.securitySolution.endpoint.resolver.relatedEventLimitExceeded": "{numberOfEventsMissing} {category}件のイベントを表示できませんでした。データの上限に達しました。", "xpack.securitySolution.endpoint.resolver.relatedEventLimitTitle": "このリストには、{numberOfEntries}件のプロセスイベントが含まれています。", "xpack.securitySolution.endpoint.resolver.relatedEvents": "イベント", @@ -15751,9 +17696,11 @@ "xpack.securitySolution.endpoint.resolver.terminatedTrigger": "トリガーを中断しました", "xpack.securitySolution.endpointManagement.noPermissionsSubText": "Ingest Managerが無効である可能性があります。この機能を使用するには、Ingest Managerを有効にする必要があります。Ingest Managerを有効にする権限がない場合は、Kibana管理者に連絡してください。", "xpack.securitySolution.endpointManagemnet.noPermissionsText": "Elastic Security Administrationを使用するために必要なKibana権限がありません。", + "xpack.securitySolution.endpointsTab": "エンドポイント", "xpack.securitySolution.enpdoint.resolver.panelutils.betaBadgeLabel": "BETA", "xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate": "無効な日付", - "xpack.securitySolution.event.module.linkToElasticEndpointSecurityDescription": "Elastic Endpoint Securityで開く", + "xpack.securitySolution.enpdoint.resolver.panelutils.noTimestampRetrieved": "タイムスタンプが取得されていません", + "xpack.securitySolution.event.module.linkToElasticEndpointSecurityDescription": "Endpoint Securityで開く", "xpack.securitySolution.eventDetails.blank": " ", "xpack.securitySolution.eventDetails.copyToClipboard": "クリップボードにコピー", "xpack.securitySolution.eventDetails.copyToClipboardTooltip": "クリップボードにコピー", @@ -15807,15 +17754,16 @@ "xpack.securitySolution.eventsViewer.showingLabel": "表示中", "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, =1 {イベント} other {イベント}}", "xpack.securitySolution.exceptions.addException.addEndpointException": "エンドポイント例外の追加", - "xpack.securitySolution.exceptions.addException.addException": "例外の追加", - "xpack.securitySolution.exceptions.addException.bulkCloseLabel": "他のルールで生成されたアラートを含む、この例外と一致するすべてのアラートを終了", - "xpack.securitySolution.exceptions.addException.bulkCloseLabel.disabled": "この例外の属性と一致するすべてのアラートを閉じる(リストと非ECSフィールドはサポートされません)", + "xpack.securitySolution.exceptions.addException.addException": "ルール例外の追加", + "xpack.securitySolution.exceptions.addException.bulkCloseLabel": "この例外一致し、このルールによって生成された、すべてのアラートを閉じる", + "xpack.securitySolution.exceptions.addException.bulkCloseLabel.disabled": "この例外と一致し、このルールによって生成された、すべてのアラートを閉じる(リストと非ECSフィールドはサポートされません)", "xpack.securitySolution.exceptions.addException.cancel": "キャンセル", "xpack.securitySolution.exceptions.addException.endpointQuarantineText": "すべてのエンドポイントホストで、例外と一致する隔離されたファイルは、自動的に元の場所に復元されます。この例外はエンドポイント例外を使用するすべてのルールに適用されます。", "xpack.securitySolution.exceptions.addException.error": "例外を追加できませんでした", "xpack.securitySolution.exceptions.addException.fetchError": "例外リストの取得エラー", "xpack.securitySolution.exceptions.addException.fetchError.title": "エラー", "xpack.securitySolution.exceptions.addException.infoLabel": "ルールの条件が満たされるときにアラートが生成されます。例外:", + "xpack.securitySolution.exceptions.addException.sequenceWarning": "このルールのクエリにはEQLシーケンス文があります。作成された例外は、シーケンスのすべてのイベントに適用されます。", "xpack.securitySolution.exceptions.addException.success": "正常に例外を追加しました", "xpack.securitySolution.exceptions.andDescription": "AND", "xpack.securitySolution.exceptions.builder.addNestedDescription": "ネストされた条件を追加", @@ -15828,29 +17776,37 @@ "xpack.securitySolution.exceptions.builder.fieldDescription": "フィールド", "xpack.securitySolution.exceptions.builder.operatorDescription": "演算子", "xpack.securitySolution.exceptions.builder.valueDescription": "値", + "xpack.securitySolution.exceptions.cancelLabel": "キャンセル", + "xpack.securitySolution.exceptions.clearExceptionsLabel": "例外リストを削除", "xpack.securitySolution.exceptions.commentEventLabel": "コメントを追加しました", "xpack.securitySolution.exceptions.commentLabel": "コメント", "xpack.securitySolution.exceptions.createdByLabel": "作成者", "xpack.securitySolution.exceptions.dateCreatedLabel": "日付が作成されました", "xpack.securitySolution.exceptions.descriptionLabel": "説明", "xpack.securitySolution.exceptions.detectionListLabel": "検出リスト", + "xpack.securitySolution.exceptions.dissasociateExceptionListError": "例外リストを削除できませんでした", + "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外リスト({id})が正常に削除されました", "xpack.securitySolution.exceptions.doesNotExistOperatorLabel": "存在しない", "xpack.securitySolution.exceptions.editButtonLabel": "編集", - "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "他のルールで生成されたアラートを含む、この例外と一致するすべてのアラートを終了", - "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "この例外の属性と一致するすべてのアラートを閉じる(リストと非ECSフィールドはサポートされません)", + "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "この例外一致し、このルールによって生成された、すべてのアラートを閉じる", + "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "この例外と一致し、このルールによって生成された、すべてのアラートを閉じる(リストと非ECSフィールドはサポートされません)", "xpack.securitySolution.exceptions.editException.cancel": "キャンセル", "xpack.securitySolution.exceptions.editException.editEndpointExceptionTitle": "エンドポイント例外の編集", "xpack.securitySolution.exceptions.editException.editExceptionSaveButton": "保存", - "xpack.securitySolution.exceptions.editException.editExceptionTitle": "例外の編集", + "xpack.securitySolution.exceptions.editException.editExceptionTitle": "ルール例外を編集", "xpack.securitySolution.exceptions.editException.endpointQuarantineText": "すべてのエンドポイントホストで、例外と一致する隔離されたファイルは、自動的に元の場所に復元されます。この例外はエンドポイント例外を使用するすべてのルールに適用されます。", "xpack.securitySolution.exceptions.editException.error": "例外を更新できませんでした", "xpack.securitySolution.exceptions.editException.infoLabel": "ルールの条件が満たされるときにアラートが生成されます。例外:", + "xpack.securitySolution.exceptions.editException.sequenceWarning": "このルールのクエリにはEQLシーケンス文があります。修正された例外は、シーケンスのすべてのイベントに適用されます。", "xpack.securitySolution.exceptions.editException.success": "正常に例外を更新しました", "xpack.securitySolution.exceptions.editException.versionConflictDescription": "最初に編集することを選択したときからこの例外が更新されている可能性があります。[キャンセル]をクリックし、もう一度例外を編集してください。", "xpack.securitySolution.exceptions.editException.versionConflictTitle": "申し訳ございません、エラーが発生しました", "xpack.securitySolution.exceptions.endpointListLabel": "エンドポイントリスト", + "xpack.securitySolution.exceptions.errorLabel": "エラー", "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "ページごとの項目数: {items}", "xpack.securitySolution.exceptions.existsOperatorLabel": "存在する", + "xpack.securitySolution.exceptions.fetch404Error": "関連付けられた例外リスト({listId})は存在しません。その他の例外を検出ルールに追加するには、見つからない例外リストを削除してください。", + "xpack.securitySolution.exceptions.fetchError": "例外リストの取得エラー", "xpack.securitySolution.exceptions.fieldDescription": "フィールド", "xpack.securitySolution.exceptions.hideCommentsLabel": "({comments}) {comments, plural, =1 {件のコメント} other {件のコメント}}を非表示", "xpack.securitySolution.exceptions.isInListOperatorLabel": "リストにある", @@ -15859,6 +17815,7 @@ "xpack.securitySolution.exceptions.isNotOperatorLabel": "is not", "xpack.securitySolution.exceptions.isOneOfOperatorLabel": "is one of", "xpack.securitySolution.exceptions.isOperatorLabel": "is", + "xpack.securitySolution.exceptions.modalErrorAccordionText": "ルール参照情報を表示:", "xpack.securitySolution.exceptions.operatingSystemLabel": "OS", "xpack.securitySolution.exceptions.operatorDescription": "演算子", "xpack.securitySolution.exceptions.orDescription": "OR", @@ -15885,6 +17842,11 @@ "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "検索結果が見つかりません。", "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "検索フィールド(例:host.name)", "xpack.securitySolution.exitFullScreenButton": "全画面を終了", + "xpack.securitySolution.featureCatalogue.subtitle": "SIEM & Endpoint Security", + "xpack.securitySolution.featureCatalogueDescription": "インフラストラクチャ全体の統合保護のため、脅威を防止、収集、検出し、それに対応します。", + "xpack.securitySolution.featureCatalogueDescription1": "自律的に脅威を防止します。", + "xpack.securitySolution.featureCatalogueDescription2": "検出して対応します。", + "xpack.securitySolution.featureCatalogueDescription3": "インシデントを調査します。", "xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle": "セキュリティ", "xpack.securitySolution.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, =1 {カテゴリ} other {カテゴリ}}", "xpack.securitySolution.fieldBrowser.categoriesTitle": "カテゴリー", @@ -15902,6 +17864,8 @@ "xpack.securitySolution.fieldBrowser.toggleColumnTooltip": "列を切り替えます", "xpack.securitySolution.fieldBrowser.viewCategoryTooltip": "すべての {categoryId} フィールドを表示します", "xpack.securitySolution.fieldRenderers.moreLabel": "もっと", + "xpack.securitySolution.firstLastSeenHost.errorSearchDescription": "最初の前回確認されたホスト検索でエラーが発生しました", + "xpack.securitySolution.firstLastSeenHost.failSearchDescription": "最初の前回確認されたホストで検索を実行できませんでした", "xpack.securitySolution.flyout.button.text": "Timeline", "xpack.securitySolution.flyout.button.timeline": "タイムライン", "xpack.securitySolution.footer.autoRefreshActiveDescription": "自動更新アクション", @@ -15913,7 +17877,7 @@ "xpack.securitySolution.footer.loadingTimelineData": "タイムラインデータを読み込み中", "xpack.securitySolution.footer.of": "/", "xpack.securitySolution.footer.rows": "行", - "xpack.securitySolution.footer.totalCountOfEvents": "イベントが検索条件に一致します", + "xpack.securitySolution.footer.totalCountOfEvents": "イベント", "xpack.securitySolution.footer.updated": "更新しました", "xpack.securitySolution.formatted.duration.aFewMillisecondsTooltip": "数ミリ秒", "xpack.securitySolution.formatted.duration.aFewNanosecondsTooltip": "数ナノ秒", @@ -15952,6 +17916,8 @@ "xpack.securitySolution.host.details.overview.platformTitle": "プラットフォーム", "xpack.securitySolution.host.details.overview.regionTitle": "地域", "xpack.securitySolution.host.details.versionLabel": "バージョン", + "xpack.securitySolution.hostOverview.errorSearchDescription": "ホスト概要検索でエラーが発生しました", + "xpack.securitySolution.hostOverview.failSearchDescription": "ホスト概要で検索を実行できませんでした", "xpack.securitySolution.hosts.kqlPlaceholder": "例:host.name: \"foo\"", "xpack.securitySolution.hosts.navigation.alertsTitle": "外部アラート", "xpack.securitySolution.hosts.navigation.allHostsTitle": "すべてのホスト", @@ -15963,6 +17929,12 @@ "xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingAuthenticationsData": "認証データをクエリできませんでした", "xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingEventsData": "イベントデータをクエリできませんでした", "xpack.securitySolution.hosts.pageTitle": "ホスト", + "xpack.securitySolution.hostsKpiAuthentications.errorSearchDescription": "ホストKPI認証検索でエラーが発生しました", + "xpack.securitySolution.hostsKpiAuthentications.failSearchDescription": "ホストKPI認証で検索を実行できませんでした", + "xpack.securitySolution.hostsKpiHosts.errorSearchDescription": "ホストKPIホスト検索でエラーが発生しました", + "xpack.securitySolution.hostsKpiHosts.failSearchDescription": "ホストKPIホストで検索を実行できませんでした", + "xpack.securitySolution.hostsKpiUniqueIps.errorSearchDescription": "ホストKPI一意のIP検索でエラーが発生しました", + "xpack.securitySolution.hostsKpiUniqueIps.failSearchDescription": "ホストKPI一意のIPで検索を実行できませんでした", "xpack.securitySolution.hostsTable.firstLastSeenToolTip": "選択された日付範囲との相関付けです", "xpack.securitySolution.hostsTable.hostsTitle": "すべてのホスト", "xpack.securitySolution.hostsTable.lastSeenTitle": "前回の認識", @@ -15971,6 +17943,14 @@ "xpack.securitySolution.hostsTable.rows": "{numRows} {numRows, plural, =0 {行} =1 {行} other {行}}", "xpack.securitySolution.hostsTable.unit": "{totalCount, plural, =1 {ホスト} other {ホスト}}", "xpack.securitySolution.hostsTable.versionTitle": "バージョン", + "xpack.securitySolution.indexPatterns.allDefault": "すべてのデフォルト", + "xpack.securitySolution.indexPatterns.dataSourcesLabel": "データソース", + "xpack.securitySolution.indexPatterns.disabled": "このページでは無効なインデックスパターンが推奨されますが、最初にKibanaインデックスパターン設定で構成する必要があります。", + "xpack.securitySolution.indexPatterns.help": "データソース選択", + "xpack.securitySolution.indexPatterns.pickIndexPatternsCombo": "インデックスパターンを選択", + "xpack.securitySolution.indexPatterns.resetButton": "リセット", + "xpack.securitySolution.indexPatterns.save": "保存", + "xpack.securitySolution.indexPatterns.selectionLabel": "このページでデータソースを選択", "xpack.securitySolution.insert.timeline.insertTimelineButton": "タイムラインリンクの挿入", "xpack.securitySolution.inspect.modal.closeTitle": "閉じる", "xpack.securitySolution.inspect.modal.indexPatternDescription": "Elasticsearch インデックスに接続したインデックスパターンです。これらのインデックスは Kibana > 高度な設定で構成できます。", @@ -16003,6 +17983,8 @@ "xpack.securitySolution.kpiNetwork.uniquePrivateIps.sourceChartLabel": "Src.", "xpack.securitySolution.kpiNetwork.uniquePrivateIps.sourceUnitLabel": "ソース", "xpack.securitySolution.kpiNetwork.uniquePrivateIps.title": "固有のプライベート IP", + "xpack.securitySolution.lastEventTime.errorSearchDescription": "前回のイベント時刻検索でエラーが発生しました。", + "xpack.securitySolution.lastEventTime.failSearchDescription": "前回のイベント時刻で検索を実行できませんでした", "xpack.securitySolution.licensing.unsupportedMachineLearningMessage": "ご使用のライセンスは機械翻訳をサポートしていません。ライセンスをアップグレードしてください。", "xpack.securitySolution.lists.cancelValueListsUploadTitle": "アップロードのキャンセル", "xpack.securitySolution.lists.closeValueListsModalTitle": "閉じる", @@ -16026,6 +18008,7 @@ "xpack.securitySolution.lists.valueListsTable.exportActionName": "エクスポート", "xpack.securitySolution.lists.valueListsTable.fileNameColumn": "ファイル名", "xpack.securitySolution.lists.valueListsTable.title": "値リスト", + "xpack.securitySolution.lists.valueListsTable.typeColumn": "型", "xpack.securitySolution.lists.valueListsTable.uploadDateColumn": "アップロード日", "xpack.securitySolution.lists.valueListsUploadButton": "リストのアップロード", "xpack.securitySolution.lists.valueListsUploadError": "値リストのアップロードエラーが発生しました。", @@ -16044,7 +18027,15 @@ "xpack.securitySolution.markdown.toolTip.timelineId": "タイムラインID:{ timelineId }", "xpack.securitySolution.markdownEditor.markdown": "マークダウン", "xpack.securitySolution.markdownEditor.markdownInputHelp": "Markdown 構文ヘルプ", + "xpack.securitySolution.markdownEditor.plugins.timeline.insertTimelineButtonLabel": "タイムラインリンクの挿入", + "xpack.securitySolution.markdownEditor.plugins.timeline.noParenthesesErrorMsg": "想定される左括弧", + "xpack.securitySolution.markdownEditor.plugins.timeline.noTimelineIdFoundErrorMsg": "タイムラインIDが見つかりません", + "xpack.securitySolution.markdownEditor.plugins.timeline.noTimelineNameFoundErrorMsg": "タイムライン名が見つかりません", + "xpack.securitySolution.markdownEditor.plugins.timeline.toolTip.timelineId": "タイムラインID:{ timelineId }", + "xpack.securitySolution.markdownEditor.plugins.timeline.toolTip.timelineUrlIsNotValidErrorMsg": "タイムラインURLが無効です => {timelineUrl}", "xpack.securitySolution.markdownEditor.preview": "プレビュー", + "xpack.securitySolution.matrixHistogram.errorSearchDescription": "行列ヒストグラム検索でエラーが発生しました", + "xpack.securitySolution.matrixHistogram.failSearchDescription": "行列ヒストグラムで検索を実行できませんでした", "xpack.securitySolution.ml.score.anomalousEntityTitle": "異常エンティティ", "xpack.securitySolution.ml.score.anomalyJobTitle": "ジョブ名", "xpack.securitySolution.ml.score.detectedTitle": "検出", @@ -16110,6 +18101,10 @@ "xpack.securitySolution.network.navigation.httpTitle": "HTTP", "xpack.securitySolution.network.navigation.tlsTitle": "TLS", "xpack.securitySolution.network.pageTitle": "ネットワーク", + "xpack.securitySolution.networkDetails.errorSearchDescription": "ネットワーク詳細検索でエラーが発生しました", + "xpack.securitySolution.networkDetails.failSearchDescription": "ネットワーク詳細で検索を実行できませんでした", + "xpack.securitySolution.networkDns.errorSearchDescription": "ネットワークDNS検索でエラーが発生しました", + "xpack.securitySolution.networkDns.failSearchDescription": "ネットワークDNSで検索を実行できませんでした", "xpack.securitySolution.networkDnsTable.column.bytesInTitle": "受信 DNS バイト", "xpack.securitySolution.networkDnsTable.column.bytesOutTitle": "送信 DNS バイト", "xpack.securitySolution.networkDnsTable.column.registeredDomain": "登録ドメイン", @@ -16120,6 +18115,8 @@ "xpack.securitySolution.networkDnsTable.select.includePtrRecords": "PTR 記録を含める", "xpack.securitySolution.networkDnsTable.title": "トップ DNS ドメイン", "xpack.securitySolution.networkDnsTable.unit": "{totalCount, plural, =1 {ドメイン} other {ドメイン}}", + "xpack.securitySolution.networkHttp.errorSearchDescription": "ネットワークHTTP検索でエラーが発生しました", + "xpack.securitySolution.networkHttp.failSearchDescription": "ネットワークHTTPで検索を実行できませんでした", "xpack.securitySolution.networkHttpTable.column.domainTitle": "ドメイン", "xpack.securitySolution.networkHttpTable.column.lastHostTitle": "最後のホスト", "xpack.securitySolution.networkHttpTable.column.lastSourceIpTitle": "最後のソースIP", @@ -16130,6 +18127,20 @@ "xpack.securitySolution.networkHttpTable.rows": "{numRows} {numRows, plural, =0 {行} =1 {行} other {行}}", "xpack.securitySolution.networkHttpTable.title": "HTTPリクエスト", "xpack.securitySolution.networkHttpTable.unit": "{totalCount, plural, =1 {リクエスト} other {リクエスト}}", + "xpack.securitySolution.networkKpiDns.errorSearchDescription": "ネットワークKPI DNS検索でエラーが発生しました", + "xpack.securitySolution.networkKpiDns.failSearchDescription": "ネットワークKPI DNSで検索を実行できませんでした", + "xpack.securitySolution.networkKpiNetworkEvents.errorSearchDescription": "ネットワークKPIネットワークイベント検索でエラーが発生しました", + "xpack.securitySolution.networkKpiNetworkEvents.failSearchDescription": "ネットワークKPIネットワークイベントで検索を実行できませんでした", + "xpack.securitySolution.networkKpiTlsHandshakes.errorSearchDescription": "ネットワークKPI TLSハンドシェイク検索でエラーが発生しました", + "xpack.securitySolution.networkKpiTlsHandshakes.failSearchDescription": "ネットワークKPI TLSハンドシェイクで検索を実行できませんでした", + "xpack.securitySolution.networkKpiUniqueFlows.errorSearchDescription": "ネットワークKPI一意のフロー検索でエラーが発生しました", + "xpack.securitySolution.networkKpiUniqueFlows.failSearchDescription": "ネットワークKPI一意のフローで検索を実行できませんでした", + "xpack.securitySolution.networkKpiUniquePrivateIps.errorSearchDescription": "ネットワークKPI一意のプライベートIP検索でエラーが発生しました", + "xpack.securitySolution.networkKpiUniquePrivateIps.failSearchDescription": "ネットワークKPI一意のプライベートIPで検索を実行できませんでした", + "xpack.securitySolution.networkTls.errorSearchDescription": "ネットワークTLS検索でエラーが発生しました", + "xpack.securitySolution.networkTls.failSearchDescription": "ネットワークTLSで検索を実行できませんでした", + "xpack.securitySolution.networkTopCountries.errorSearchDescription": "ネットワーク上位の国検索でエラーが発生しました", + "xpack.securitySolution.networkTopCountries.failSearchDescription": "ネットワーク上位の国で検索を実行できませんでした", "xpack.securitySolution.networkTopCountriesTable.column.bytesInTitle": "受信バイト", "xpack.securitySolution.networkTopCountriesTable.column.bytesOutTitle": "送信バイト", "xpack.securitySolution.networkTopCountriesTable.column.countryTitle": "国", @@ -16140,6 +18151,8 @@ "xpack.securitySolution.networkTopCountriesTable.heading.sourceCountries": "ソースの国", "xpack.securitySolution.networkTopCountriesTable.heading.unit": "{totalCount, plural, =1 {国} other {国}}", "xpack.securitySolution.networkTopCountriesTable.rows": "{numRows} {numRows, plural, =0 {行} =1 {行} other {行}}", + "xpack.securitySolution.networkTopNFlow.errorSearchDescription": "ネットワーク上位nフロー検索でエラーが発生しました", + "xpack.securitySolution.networkTopNFlow.failSearchDescription": "ネットワーク上位nフローで検索を実行できませんでした", "xpack.securitySolution.networkTopNFlowTable.column.asTitle": "自動システム", "xpack.securitySolution.networkTopNFlowTable.column.bytesInTitle": "受信バイト", "xpack.securitySolution.networkTopNFlowTable.column.bytesOutTitle": "送信バイト", @@ -16152,8 +18165,11 @@ "xpack.securitySolution.networkTopNFlowTable.rows": "{numRows} {numRows, plural, =0 {行} =1 {行} other {行}}", "xpack.securitySolution.networkTopNFlowTable.sourceIps": "ソース IP", "xpack.securitySolution.networkTopNFlowTable.unit": "{totalCount, plural, =1 {IP} other {IP}}", + "xpack.securitySolution.networkUsers.errorSearchDescription": "ネットワークユーザー検索でエラーが発生しました", + "xpack.securitySolution.networkUsers.failSearchDescription": "ネットワークユーザーで検索を実行できませんでした", "xpack.securitySolution.newsFeed.advancedSettingsLinkTitle": "SIEM高度な設定", - "xpack.securitySolution.newsFeed.noNewsMessage": "現在のニュースフィードURLは最新のニュースを返しませんでした。URLを更新するか、セキュリティニュースを無効にすることができます", + "xpack.securitySolution.newsFeed.noNewsMessage": "現在のニュースフィードURLは最新のニュースを返しませんでした。", + "xpack.securitySolution.newsFeed.noNewsMessageForAdmin": "現在のニュースフィードURLは最新のニュースを返しませんでした。URLを更新するか、セキュリティニュースを無効にすることができます", "xpack.securitySolution.notes.addANotePlaceholder": "メモを追加", "xpack.securitySolution.notes.addedANoteLabel": "メモを追加しました", "xpack.securitySolution.notes.addNoteButtonLabel": "メモを追加", @@ -16204,6 +18220,7 @@ "xpack.securitySolution.open.timeline.singleTemplateLabel": "テンプレート", "xpack.securitySolution.open.timeline.singleTimelineLabel": "タイムライン", "xpack.securitySolution.open.timeline.successfullyExportedTimelinesTitle": "{totalTimelines, plural, =0 {all timelines} =1 {{totalTimelines} タイムライン} other {{totalTimelines} タイムライン}}のエクスポートが正常に完了しました", + "xpack.securitySolution.open.timeline.successfullyExportedTimelineTemplatesTitle": "{totalTimelineTemplates, plural, =0 {すべてのタイムライン} =1 {{totalTimelineTemplates} タイムラインテンプレート} other {{totalTimelineTemplates} タイムラインテンプレート}}が正常にエクスポートされました", "xpack.securitySolution.open.timeline.timelineNameTableHeader": "タイムライン名", "xpack.securitySolution.open.timeline.timelineTemplateNameTableHeader": "テンプレート名", "xpack.securitySolution.open.timeline.untitledTimelineLabel": "無題のタイムライン", @@ -16226,8 +18243,8 @@ "xpack.securitySolution.overview.endpointNotice.dismiss": "メッセージを消去", "xpack.securitySolution.overview.endpointNotice.introducing": "導入: ", "xpack.securitySolution.overview.endpointNotice.message": "脅威防御、検出、深いセキュリティデータの可視化を実現し、ホストを保護します。", - "xpack.securitySolution.overview.endpointNotice.title": "Elastic Endpoint Security(ベータ)", - "xpack.securitySolution.overview.endpointNotice.tryButton": "Elastic Endpoint Security(ベータ)を試す", + "xpack.securitySolution.overview.endpointNotice.title": "Endpoint Security(ベータ)", + "xpack.securitySolution.overview.endpointNotice.tryButton": "Endpoint Security(ベータ)を試す", "xpack.securitySolution.overview.eventsTitle": "イベント数", "xpack.securitySolution.overview.feedbackText": "Elastic SIEM に関するご意見やご提案は、お気軽に {feedback}", "xpack.securitySolution.overview.feedbackText.feedbackLinkText": "フィードバックをオンラインで送信", @@ -16240,7 +18257,7 @@ "xpack.securitySolution.overview.fileBeatZeekTitle": "Zeek", "xpack.securitySolution.overview.hostsAction": "ホストを表示", "xpack.securitySolution.overview.hostStatGroupAuditbeat": "Auditbeat", - "xpack.securitySolution.overview.hostStatGroupElasticEndpointSecurity": "Elastic Endpoint Security", + "xpack.securitySolution.overview.hostStatGroupElasticEndpointSecurity": "Endpoint Security", "xpack.securitySolution.overview.hostStatGroupFilebeat": "Filebeat", "xpack.securitySolution.overview.hostStatGroupWinlogbeat": "Winlogbeat", "xpack.securitySolution.overview.hostsTitle": "ホストイベント", @@ -16273,14 +18290,18 @@ "xpack.securitySolution.overview.viewEventsButtonLabel": "イベントを表示", "xpack.securitySolution.overview.winlogbeatMWSysmonOperational": "Microsoft-Windows-Sysmon/Operational", "xpack.securitySolution.overview.winlogbeatSecurityTitle": "セキュリティ", + "xpack.securitySolution.overviewHost.errorSearchDescription": "ホスト概要検索でエラーが発生しました", + "xpack.securitySolution.overviewHost.failSearchDescription": "ホスト概要で検索を実行できませんでした", "xpack.securitySolution.pages.common.emptyActionBeats": "Beatsでデータを追加", "xpack.securitySolution.pages.common.emptyActionBeatsDescription": "Lightweight Beatsは数百または数千台のコンピューターとシステムからデータを送信できます", "xpack.securitySolution.pages.common.emptyActionElasticAgent": "Elasticエージェントでデータを追加", "xpack.securitySolution.pages.common.emptyActionElasticAgentDescription": "Elasticエージェントでは、シンプルかつ統合された方法で、監視をホストに追加することができます。", - "xpack.securitySolution.pages.common.emptyActionEndpoint": "Elastic Endpoint Securityを追加", + "xpack.securitySolution.pages.common.emptyActionEndpoint": "Endpoint Securityを追加", "xpack.securitySolution.pages.common.emptyActionEndpointDescription": "脅威防御、検出、深いセキュリティデータの可視化を実現し、ホストを保護します。", "xpack.securitySolution.pages.common.emptyActionSecondary": "入門ガイドを表示します。", "xpack.securitySolution.pages.common.emptyTitle": "Elastic Securityへようこそ。始めましょう。", + "xpack.securitySolution.pages.common.updateAlertStatusFailed": "{ conflicts } {conflicts, plural, =1 {アラート} other {アラート}}を更新できませんでした。", + "xpack.securitySolution.pages.common.updateAlertStatusFailedDetailed": "{ updated } {updated, plural, =1 {アラート} other {アラート}}が正常に更新されましたが、{ conflicts }は更新できませんでした。\n { conflicts, plural, =1 {} other {}}すでに修正されています。", "xpack.securitySolution.pages.fourohfour.noContentFoundDescription": "コンテンツがありません", "xpack.securitySolution.paginatedTable.rowsButtonLabel": "ページごとの行数", "xpack.securitySolution.paginatedTable.showingSubtitle": "表示中", @@ -16307,8 +18328,22 @@ "xpack.securitySolution.recentTimelines.pinnedEventsTooltip": "ピン付けされたイベント", "xpack.securitySolution.recentTimelines.untitledTimelineLabel": "無題のタイムライン", "xpack.securitySolution.recentTimelines.viewAllTimelinesLink": "すべてのタイムラインを表示", + "xpack.securitySolution.resolver.eventDescription.dnsQuestionNameLabel": "{ dnsQuestionName }", + "xpack.securitySolution.resolver.eventDescription.entityIDLabel": "{ entityID }", + "xpack.securitySolution.resolver.eventDescription.fileEventLabel": "{ filePath }", + "xpack.securitySolution.resolver.eventDescription.legacyEventLabel": "{ processName }", + "xpack.securitySolution.resolver.eventDescription.networkEventLabel": "{ networkDirection } { forwardedIP }", + "xpack.securitySolution.resolver.eventDescription.registryKeyLabel": "{ registryKey }", + "xpack.securitySolution.resolver.eventDescription.registryPathLabel": "{ registryPath }", + "xpack.securitySolution.resolver.node_icon": "{running, select, true {実行中のプロセス} false {終了したプロセス}}", + "xpack.securitySolution.resolver.panel.copyToClipboard": "クリップボードにコピー", + "xpack.securitySolution.resolver.panel.eventDetail.requestError": "イベント詳細を取得できませんでした", + "xpack.securitySolution.resolver.panel.nodeList.title": "すべてのプロセスイベント", + "xpack.securitySolution.resolver.panel.table.row.analyzedEvent": "分析されたイベント", "xpack.securitySolution.security.title": "セキュリティ", "xpack.securitySolution.source.destination.packetsLabel": "パケット", + "xpack.securitySolution.stepDefineRule.previewQueryAriaLabel": "クエリプレビュータイムフレーム選択", + "xpack.securitySolution.stepDefineRule.previewQueryLabel": "結果を表示", "xpack.securitySolution.system.acceptedAConnectionViaDescription": "次の手段で接続を受け付けました。", "xpack.securitySolution.system.acceptedDescription": "以下を経由してユーザーを受け入れました。", "xpack.securitySolution.system.attemptedLoginDescription": "以下を経由してログインを試行しました:", @@ -16344,6 +18379,12 @@ "xpack.securitySolution.system.withExitCodeDescription": "終了コードで", "xpack.securitySolution.system.withResultDescription": "結果付き", "xpack.securitySolution.tables.rowItemHelper.moreDescription": "行は表示されていません", + "xpack.securitySolution.threatMatch.andDescription": "AND", + "xpack.securitySolution.threatMatch.fieldDescription": "フィールド", + "xpack.securitySolution.threatMatch.fieldPlaceholderDescription": "検索", + "xpack.securitySolution.threatMatch.matchesLabel": "一致", + "xpack.securitySolution.threatMatch.orDescription": "OR", + "xpack.securitySolution.threatMatch.threatFieldDescription": "脅威インデックスフィールド", "xpack.securitySolution.timeline.autosave.warning.description": "別のユーザーがこのタイムラインに変更を加えました。このタイムラインを更新してこれらの変更を反映させるまで、ユーザーによる変更は自動的に保存されません。", "xpack.securitySolution.timeline.autosave.warning.refresh.title": "タイムラインを更新", "xpack.securitySolution.timeline.autosave.warning.title": "更新されるまで自動保存は無効です", @@ -16405,7 +18446,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": "新しいケースに添付", @@ -16440,14 +18480,22 @@ "xpack.securitySolution.timeline.rangePicker.oneWeek": "1 週間", "xpack.securitySolution.timeline.rangePicker.oneYear": "1 年", "xpack.securitySolution.timeline.searchBoxPlaceholder": "例:{timeline}名、または説明", - "xpack.securitySolution.timeline.searchOrFilter.eventTypeAllEvent": "すべて", + "xpack.securitySolution.timeline.searchOrFilter.customeIndexNames": "カスタム", + "xpack.securitySolution.timeline.searchOrFilter.eventTypeAllEvent": "すべてのデータソース", "xpack.securitySolution.timeline.searchOrFilter.eventTypeDetectionAlertsEvent": "検出アラート", - "xpack.securitySolution.timeline.searchOrFilter.eventTypeRawEvent": "未加工イベント", + "xpack.securitySolution.timeline.searchOrFilter.eventTypeRawEvent": "イベント", "xpack.securitySolution.timeline.searchOrFilter.filterDescription": "上のデータプロバイダーからのイベントは、隣接の KQL でフィルターされます", "xpack.securitySolution.timeline.searchOrFilter.filterKqlPlaceholder": "イベントをフィルター", "xpack.securitySolution.timeline.searchOrFilter.filterKqlSelectedText": "フィルター", "xpack.securitySolution.timeline.searchOrFilter.filterKqlTooltip": "上のデータプロバイダーからのイベントは、この KQL でフィルターされます", "xpack.securitySolution.timeline.searchOrFilter.filterOrSearchWithKql": "KQLでフィルターまたは検索", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.configure": "上記の選択のそれぞれに関連付けられたデータソースを表示", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.help": "データソース選択", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.hideAdvancedSettings": "詳細設定を表示しない", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.pickIndexPatternsCombo": "インデックスパターンを選択", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.resetSettings": "リセット", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.save": "保存", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.showAdvancedSettings": "詳細設定を表示", "xpack.securitySolution.timeline.searchOrFilter.searchDescription": "上のデータプロバイダーからのイベントは、隣接のKQLからの結果と組み合わされます。", "xpack.securitySolution.timeline.searchOrFilter.searchKqlPlaceholder": "イベントを検索", "xpack.securitySolution.timeline.searchOrFilter.searchKqlSelectedText": "検索", @@ -16455,6 +18503,8 @@ "xpack.securitySolution.timeline.source": "送信元", "xpack.securitySolution.timeline.tcp": "TCP", "xpack.securitySolution.timeline.typeTooltip": "タイプ", + "xpack.securitySolution.timelineEvents.errorSearchDescription": "タイムラインイベント検索でエラーが発生しました", + "xpack.securitySolution.timelineEvents.failSearchDescription": "タイムラインイベントで検索を実行できませんでした", "xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "すべてのタイムラインデータをクエリできませんでした", "xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "タイムラインのインポート", "xpack.securitySolution.timelines.allTimelines.panelTitle": "すべてのタイムライン", @@ -16473,10 +18523,64 @@ "xpack.securitySolution.timelines.pageTitle": "タイムライン", "xpack.securitySolution.timelines.updateTimelineErrorText": "問題が発生しました", "xpack.securitySolution.timelines.updateTimelineErrorTitle": "タイムラインエラー", - "xpack.securitySolution.topN.alertEventsSelectLabel": "アラートイベント", + "xpack.securitySolution.topN.alertEventsSelectLabel": "検出アラート", "xpack.securitySolution.topN.allEventsSelectLabel": "すべてのイベント", "xpack.securitySolution.topN.closeButtonLabel": "閉じる", "xpack.securitySolution.topN.rawEventsSelectLabel": "未加工イベント", + "xpack.securitySolution.trustedapps.aboutInfo": "パフォーマンスを改善したり、ホストで実行されている他のアプリケーションとの競合を解消したりするには、信頼できるアプリケーションを追加します。信頼できるアプリケーションは、Endpoint Securityを実行しているホストに適用されます。", + "xpack.securitySolution.trustedapps.card.operator.includes": "is", + "xpack.securitySolution.trustedapps.card.removeButtonLabel": "削除", + "xpack.securitySolution.trustedapps.create.conditionFieldValueRequiredMsg": "[{row}] フィールドエントリには値が必要です", + "xpack.securitySolution.trustedapps.create.conditionRequiredMsg": "1つ以上のフィールド定義が必要です", + "xpack.securitySolution.trustedapps.create.description": "説明", + "xpack.securitySolution.trustedapps.create.name": "信頼できるアプリケーションに名前を付ける", + "xpack.securitySolution.trustedapps.create.nameRequiredMsg": "名前が必要です", + "xpack.securitySolution.trustedapps.create.os": "オペレーティングシステムを選択", + "xpack.securitySolution.trustedapps.create.osRequiredMsg": "オペレーティングシステムは必須です", + "xpack.securitySolution.trustedapps.createTrustedAppFlyout.cancelButton": "キャンセル", + "xpack.securitySolution.trustedapps.createTrustedAppFlyout.saveButton": "信頼できるアプリケーションを追加", + "xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle": "「{name}」は信頼できるアプリケーションリストに追加されました。", + "xpack.securitySolution.trustedapps.createTrustedAppFlyout.title": "信頼できるアプリケーションを追加", + "xpack.securitySolution.trustedapps.deletionDialog.cancelButton": "キャンセル", + "xpack.securitySolution.trustedapps.deletionDialog.confirmButton": "信頼できるアプリケーションを削除", + "xpack.securitySolution.trustedapps.deletionDialog.mainMessage": "信頼できるアプリケーション「{name}」を削除しています。", + "xpack.securitySolution.trustedapps.deletionDialog.subMessage": "この操作は元に戻すことができません。続行していいですか?", + "xpack.securitySolution.trustedapps.deletionDialog.title": "信頼できるアプリケーションを削除", + "xpack.securitySolution.trustedapps.deletionError.text": "信頼できるアプリケーションリストから「{name}」を削除できません。理由:{message}", + "xpack.securitySolution.trustedapps.deletionError.title": "削除失敗", + "xpack.securitySolution.trustedapps.deletionSuccess.text": "「{name}」は信頼できるアプリケーションリストから削除されました。", + "xpack.securitySolution.trustedapps.deletionSuccess.title": "正常に削除されました", + "xpack.securitySolution.trustedapps.list.actions.delete": "削除", + "xpack.securitySolution.trustedapps.list.actions.delete.description": "このエントリを削除", + "xpack.securitySolution.trustedapps.list.addButton": "信頼できるアプリケーションを追加", + "xpack.securitySolution.trustedapps.list.backButton": "戻る", + "xpack.securitySolution.trustedapps.list.columns.actions": "アクション", + "xpack.securitySolution.trustedapps.list.pageTitle": "信頼できるアプリケーション", + "xpack.securitySolution.trustedapps.list.totalCount": "{totalItemCount, plural, one {#個の信頼できるアプリケーション} other {#個の信頼できるアプリケーション}}", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field": "フィールド", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash": "ハッシュ", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path": "パス", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator": "演算子", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator.is": "is", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "エントリを削除", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.value": "値", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません", + "xpack.securitySolution.trustedapps.noResults": "項目が見つかりません", + "xpack.securitySolution.trustedapps.os.linux": "Linux", + "xpack.securitySolution.trustedapps.os.macos": "Mac OS", + "xpack.securitySolution.trustedapps.os.windows": "Windows", + "xpack.securitySolution.trustedapps.trustedapp.createdAt": "作成日", + "xpack.securitySolution.trustedapps.trustedapp.createdBy": "作成者", + "xpack.securitySolution.trustedapps.trustedapp.description": "説明", + "xpack.securitySolution.trustedapps.trustedapp.entry.field": "フィールド", + "xpack.securitySolution.trustedapps.trustedapp.entry.operator": "演算子", + "xpack.securitySolution.trustedapps.trustedapp.entry.value": "値", + "xpack.securitySolution.trustedapps.trustedapp.name": "名前", + "xpack.securitySolution.trustedapps.trustedapp.os": "OS", + "xpack.securitySolution.trustedapps.view.toggle.grid": "グリッドビュー", + "xpack.securitySolution.trustedapps.view.toggle.list": "リストビュー", + "xpack.securitySolution.trustedAppsTab": "信頼できるアプリケーション", "xpack.securitySolution.uiSettings.defaultAnomalyScoreDescription": "

機械学習ジョブの異常がこの値を超えると、セキュリティアプリに表示されます。

有効な値:0 ~ 100。

", "xpack.securitySolution.uiSettings.defaultAnomalyScoreLabel": "デフォルトの異常しきい値", "xpack.securitySolution.uiSettings.defaultIndexDescription": "

セキュリティアプリがイベントを収集するElasticsearchインデックスのコンマ区切りのリストです。

", @@ -16491,6 +18595,8 @@ "xpack.securitySolution.uiSettings.ipReputationLinksDescription": "IP 詳細ページに表示される評判 URL のリストを作成するための URL テンプレートの配列。", "xpack.securitySolution.uiSettings.newsFeedUrl": "ニュースフィードURL", "xpack.securitySolution.uiSettings.newsFeedUrlDescription": "

ニュースフィードコンテンツはこのURLから取得されます

", + "xpack.securitySolution.uncommonProcesses.errorSearchDescription": "一般的ではないプロセス検索でエラーが発生しました", + "xpack.securitySolution.uncommonProcesses.failSearchDescription": "一般的ではないプロセスで検索を実行できませんでした", "xpack.securitySolution.uncommonProcessTable.hostsTitle": "すべてのホスト", "xpack.securitySolution.uncommonProcessTable.lastCommandTitle": "前回のコマンド", "xpack.securitySolution.uncommonProcessTable.lastUserTitle": "前回のユーザー", @@ -16593,6 +18699,8 @@ "xpack.snapshotRestore.executeRetention.confirmModal.executeRetentionTitle": "今すぐスナップショットの保存を実行しますか?", "xpack.snapshotRestore.executeRetention.errorMessage": "保存の実行中にエラーが発生しました", "xpack.snapshotRestore.executeRetention.successMessage": "保存を実行中です", + "xpack.snapshotRestore.featureCatalogueDescription": "スナップショットをバックアップリポジトリに保存し、インデックスとクラスター状態を回復するために復元します。", + "xpack.snapshotRestore.featureCatalogueTitle": "バックアップと復元", "xpack.snapshotRestore.home.breadcrumbTitle": "スナップショットリポジドリ", "xpack.snapshotRestore.home.policiesTabTitle": "ポリシー", "xpack.snapshotRestore.home.repositoriesTabTitle": "レポジトリ", @@ -16690,6 +18798,8 @@ "xpack.snapshotRestore.policyForm.stepLogistics.repositoryDescriptionTitle": "レポジトリ", "xpack.snapshotRestore.policyForm.stepLogistics.scheduleDescription": "スナップショットを撮影する頻度です。", "xpack.snapshotRestore.policyForm.stepLogistics.scheduleDescriptionTitle": "スケジュール", + "xpack.snapshotRestore.policyForm.stepLogistics.selectRepository.policyRepositoryNotFoundDescription": "リポジトリ{repo}は存在しません。既存のリポジトリを選択してください。", + "xpack.snapshotRestore.policyForm.stepLogistics.selectRepository.policyRepositoryNotFoundTitle": "リポジトリが見つかりません", "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescription": "スナップショットの名前です。それぞれの名前に自動的に追加される固有の識別子です。", "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescriptionTitle": "スナップショット名", "xpack.snapshotRestore.policyForm.stepLogisticsTitle": "ロジスティクス", @@ -17019,6 +19129,7 @@ "xpack.snapshotRestore.repositoryForm.typeHDFS.uriDescription": "HDFS の URL アドレスです。", "xpack.snapshotRestore.repositoryForm.typeHDFS.uriLabel": "URI (必須)", "xpack.snapshotRestore.repositoryForm.typeHDFS.uriTitle": "URI", + "xpack.snapshotRestore.repositoryForm.typeReadonly.urlAllowedDescription": "このURLは{settingKey}設定で登録する必要があります。", "xpack.snapshotRestore.repositoryForm.typeReadonly.urlDescription": "スナップショットの場所です。", "xpack.snapshotRestore.repositoryForm.typeReadonly.urlFilePathDescription": "このファイルの場所は {settingKey} 設定で登録する必要があります。", "xpack.snapshotRestore.repositoryForm.typeReadonly.urlLabel": "パス (必須)", @@ -17309,32 +19420,63 @@ "xpack.spaces.management.confirmDeleteModal.deletingSpaceWarningMessage": "スペースを削除すると、スペースと {allContents} が永久に削除されます。この操作は元に戻すことができません。", "xpack.spaces.management.confirmDeleteModal.redirectAfterDeletingCurrentSpaceWarningMessage": "現在のスペース {name} を削除しようとしています。続行すると、別のスペースを選択する画面に移動します。", "xpack.spaces.management.confirmDeleteModal.spaceNamesDoNoMatchErrorMessage": "スペース名が一致していません。", - "xpack.spaces.management.copyToSpace.actionDescription": "この保存されたオブジェクトを1つまたは複数のスペースにコピーします。", + "xpack.spaces.management.copyToSpace.actionDescription": "1つ以上のスペースでこの保存されたオブジェクトのコピーを作成します", "xpack.spaces.management.copyToSpace.actionTitle": "スペースにコピー", + "xpack.spaces.management.copyToSpace.cancelButton": "キャンセル", + "xpack.spaces.management.copyToSpace.copyDetail.overwriteSwitch": "上書きしますか?", + "xpack.spaces.management.copyToSpace.copyDetail.selectControlLabel": "オブジェクトID", "xpack.spaces.management.copyToSpace.copyErrorTitle": "保存されたオブジェクトのコピー中にエラーが発生", - "xpack.spaces.management.copyToSpace.copyResultsLabel": "コピー結果", - "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "保存されたオブジェクトは上書きされます。「スキップ」をクリックしてこの操作をキャンセルします。", - "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "保存されたオブジェクトがコピーされました。", - "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "この保存されたオブジェクトのコピー中にエラーが発生しました。", - "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "{space}スペースに1つまたは複数の矛盾が検出されました。解決するにはこのセクションを拡張してください。", + "xpack.spaces.management.copyToSpace.copyModeControl.copyOptionsTitle": "オプションをコピー", + "xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.disabledText": "以前にオブジェクトがスペースにコピーまたはインポートされたかどうかを確認します。", + "xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.disabledTitle": "既存のオブジェクトを確認", + "xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.enabledText": "このオプションを使用すると、同じ場所でオブジェクトの1つ以上のコピーを作成します。", + "xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.enabledTitle": "ランダムIDで新しいオブジェクトを作成", + "xpack.spaces.management.copyToSpace.copyModeControl.includeRelated.text": "このオブジェクトと関連するオブジェクトをコピーします。ダッシュボードでは、関連するビジュアライゼーション、インデックスパターン、および保存された検索もコピーされます。", + "xpack.spaces.management.copyToSpace.copyModeControl.includeRelated.title": "関連オブジェクトを含める", + "xpack.spaces.management.copyToSpace.copyModeControl.overwrite.disabledLabel": "競合時にアクションを要求", + "xpack.spaces.management.copyToSpace.copyModeControl.overwrite.enabledLabel": "自動的に競合を上書き", + "xpack.spaces.management.copyToSpace.copyModeControl.relationshipOptionsTitle": "関係", + "xpack.spaces.management.copyToSpace.copyResultsLabel": "結果", + "xpack.spaces.management.copyToSpace.copyStatus.ambiguousConflictMessage": "これは既存の複数のオブジェクトと競合します。[上書き]を有効にすると、置換します。", + "xpack.spaces.management.copyToSpace.copyStatus.conflictMessage": "これは既存のオブジェクトと競合します。[上書き]を有効にすると、置換します。", + "xpack.spaces.management.copyToSpace.copyStatus.missingReferencesAutomaticOverwriteMessage": "オブジェクトは上書きされますが、1つ以上の参照が見つかりません。", + "xpack.spaces.management.copyToSpace.copyStatus.missingReferencesMessage": "オブジェクトはコピーされますが、1つ以上の参照が見つかりません。", + "xpack.spaces.management.copyToSpace.copyStatus.missingReferencesOverwriteMessage": "オブジェクトは上書きされますが、1つ以上の参照が見つかりません。[上書き]を無効にすると、スキップします。", + "xpack.spaces.management.copyToSpace.copyStatus.pendingAutomaticOverwriteMessage": "オブジェクトは上書きされます。", + "xpack.spaces.management.copyToSpace.copyStatus.pendingMessage": "オブジェクトはコピーされます。", + "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "オブジェクトは上書きされます。[上書き]を無効にすると、スキップします。", + "xpack.spaces.management.copyToSpace.copyStatus.successAutomaticOverwriteMessage": "オブジェクトは上書きされました。", + "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "オブジェクトはコピーされました。", + "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "このオブジェクトのコピー中にエラーが発生しました。", + "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "{space}スペースで競合が検出されました。解決するにはこのセクションを拡張してください。", "xpack.spaces.management.copyToSpace.copyStatusSummary.failedMessage": "{space}スペースへのコピーに失敗しました。詳細はこのセクションを展開してください。", - "xpack.spaces.management.copyToSpace.copyStatusSummary.successMessage": "{space}スペースにコピーされました。", + "xpack.spaces.management.copyToSpace.copyStatusSummary.missingReferencesMessage": "{space}スペースで見つからない参照が検出されました。詳細はこのセクションを展開してください。", + "xpack.spaces.management.copyToSpace.copyStatusSummary.successMessage": "正常に{space}スペースにコピーされました。", "xpack.spaces.management.copyToSpace.copyToSpacesButton": "{spaceCount} {spaceCount, plural, one {スペース} other {スペース}}にコピー", + "xpack.spaces.management.copyToSpace.createNewCopiesLabel": "ランダムIDで新しいオブジェクトを作成", "xpack.spaces.management.copyToSpace.disabledCopyToSpacesButton": "コピー", + "xpack.spaces.management.copyToSpace.dontCreateNewCopiesLabel": "既存のオブジェクトを確認", "xpack.spaces.management.copyToSpace.finishCopyToSpacesButton": "終了", "xpack.spaces.management.copyToSpace.finishedButtonLabel": "コピーが完了しました。", - "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "{overwriteCount}件のオブジェクトを上書き", - "xpack.spaces.management.copyToSpace.includeRelatedLabel": "関連性のある保存されたオブジェクトを含みます", + "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "{overwriteCount}個のオブジェクトをコピー", + "xpack.spaces.management.copyToSpace.includeRelatedLabel": "関連する保存されたオブジェクトを含める", "xpack.spaces.management.copyToSpace.inProgressButtonLabel": "コピーが進行中です。お待ちください。", "xpack.spaces.management.copyToSpace.noSpacesBody": "コピーできるスペースがありません。", "xpack.spaces.management.copyToSpace.noSpacesTitle": "スペースがありません", + "xpack.spaces.management.copyToSpace.overwriteAllConflictsText": "すべて上書き", + "xpack.spaces.management.copyToSpace.overwriteLabel": "自動的に競合を上書き", + "xpack.spaces.management.copyToSpace.resolveAllConflictsLink": "(すべて解決)", "xpack.spaces.management.copyToSpace.resolveCopyErrorTitle": "保存されたオブジェクトの矛盾の解決中にエラーが発生", - "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "上書き成功", + "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "コピー成功", + "xpack.spaces.management.copyToSpace.selectSpacesControl.disabledTooltip": "オブジェクトはすでにこのスペースに存在します。", + "xpack.spaces.management.copyToSpace.selectSpacesLabel": "スペースを選択", + "xpack.spaces.management.copyToSpace.skipAllConflictsText": "すべてスキップ", "xpack.spaces.management.copyToSpace.spacesLoadErrorTitle": "利用可能なスペースを読み込み中にエラーが発生", "xpack.spaces.management.copyToSpaceFlyoutFooter.errorCount": "エラー", "xpack.spaces.management.copyToSpaceFlyoutFooter.pendingCount": "保留中", + "xpack.spaces.management.copyToSpaceFlyoutFooter.skippedCount": "スキップ", "xpack.spaces.management.copyToSpaceFlyoutFooter.successCount": "コピー完了", - "xpack.spaces.management.copyToSpaceFlyoutHeader": "保存されたオブジェクトのスペースへのコピー", + "xpack.spaces.management.copyToSpaceFlyoutHeader": "スペースにコピー", "xpack.spaces.management.createSpaceBreadcrumb": "作成", "xpack.spaces.management.customizeSpaceAvatar.colorFormRowLabel": "色", "xpack.spaces.management.customizeSpaceAvatar.imageUrl": "カスタム画像", @@ -17345,22 +19487,30 @@ "xpack.spaces.management.deleteSpacesButton.deleteSpaceButtonLabel": "スペースを削除", "xpack.spaces.management.deleteSpacesButton.deleteSpaceErrorTitle": "スペースの削除中にエラーが発生: {errorMessage}", "xpack.spaces.management.deleteSpacesButton.spaceSuccessfullyDeletedNotificationMessage": "{spaceName} スペースが削除されました。", + "xpack.spaces.management.deselectAllFeaturesLink": "すべて選択解除", + "xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "カテゴリ切り替え", "xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": "(表示されているすべての機能)", - "xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "機能の表示をカスタマイズ", - "xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "このスペースでどの機能が表示されるかを管理します。", + "xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "機能", + "xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "このスペースの機能の表示を設定", + "xpack.spaces.management.enabledSpaceFeatures.goToRolesLink": "機能へのアクセスを保護する場合は、{manageSecurityRoles}してください。", "xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": "(表示されている機能がありません)", "xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "この機能は UI で非表示になっていますが、無効ではありません。", - "xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "ロール", + "xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "セキュリティロールを管理", "xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": "({featureCount} 件中 {enabledCount} 件の機能を表示中)", + "xpack.spaces.management.featureAccordionSwitchLabel": "{featureCount}件中{enabledCount}件の機能を表示中", + "xpack.spaces.management.featureVisibilityTitle": "機能の表示", "xpack.spaces.management.hideAllFeaturesText": "すべて非表示", + "xpack.spaces.management.managementCategoryHelpText": "スタック管理へのアクセスは割り当てられた権限によって決まり、スペースで非表示にすることはできません。", "xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "アバター", "xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "素晴らしいスペース", "xpack.spaces.management.manageSpacePage.cancelSpaceButton": "キャンセル", "xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip": "クリックしてこのスペースのアバターをカスタマイズします", "xpack.spaces.management.manageSpacePage.createSpaceButton": "スペースを作成", "xpack.spaces.management.manageSpacePage.createSpaceTitle": "スペースの作成", + "xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "スペースに名前を付けてアバターをカスタマイズします", "xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierEditable": "URL 識別子に注意してください。スペースの作成後に変更することはできません。", "xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierNotEditable": "URL 識別子は変更できません。", + "xpack.spaces.management.manageSpacePage.customizeSpaceTitle": "スペースのカスタマイズ", "xpack.spaces.management.manageSpacePage.customizeVisibleFeatures": "表示される機能のカスタマイズ", "xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "スペースの読み込み中にエラーが発生: {message}", "xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "スペースの保存中にエラーが発生: {message}", @@ -17375,6 +19525,43 @@ "xpack.spaces.management.secureSpaceMessage.howToAssignRoleToSpaceDescription": "スペースへのロールの割り当てをご希望ですか?{rolesLink} にアクセスしてください。", "xpack.spaces.management.secureSpaceMessage.rolesLinkText": "ロール", "xpack.spaces.management.secureSpaceMessage.rolesLinkTextAriaLabel": "ロール管理ページ", + "xpack.spaces.management.selectAllFeaturesLink": "すべて選択", + "xpack.spaces.management.shareToSpace.actionDescription": "この保存されたオブジェクトを1つ以上のスペースと共有します。", + "xpack.spaces.management.shareToSpace.actionTitle": "スペースと共有", + "xpack.spaces.management.shareToSpace.allSpacesLabel": "*すべてのスペース", + "xpack.spaces.management.shareToSpace.cancelButton": "キャンセル", + "xpack.spaces.management.shareToSpace.columnDescription": "このオブジェクトが現在共有されている他のスペース", + "xpack.spaces.management.shareToSpace.columnTitle": "共有されているスペース", + "xpack.spaces.management.shareToSpace.columnUnauthorizedLabel": "これらのスペースを表示するアクセス権がありません。", + "xpack.spaces.management.shareToSpace.noAvailableSpaces.canCreateNewSpace.linkText": "新しいスペースを作成", + "xpack.spaces.management.shareToSpace.noAvailableSpaces.canCreateNewSpace.text": "オブジェクトを共有するには、{createANewSpaceLink}できます。", + "xpack.spaces.management.shareToSpace.shareAddSuccessTextPlural": "「{object}」は{spaceTargets}個のスペースに追加されました。", + "xpack.spaces.management.shareToSpace.shareAddSuccessTextSingular": "「{object}」は1つのスペースに追加されました。", + "xpack.spaces.management.shareToSpace.shareEditSuccessTitle": "オブジェクトが更新されました", + "xpack.spaces.management.shareToSpace.shareErrorTitle": "保存されたオブジェクトの更新エラー", + "xpack.spaces.management.shareToSpace.shareModeControl.hiddenCountLabel": "+{hiddenCount}個が非表示", + "xpack.spaces.management.shareToSpace.shareModeControl.selectedCountLabel": "{selectedCount}個が選択済み", + "xpack.spaces.management.shareToSpace.shareModeControl.selectSpacesLabel": "スペースを選択", + "xpack.spaces.management.shareToSpace.shareModeControl.shareOptionsTitle": "共有オプション", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToAllSpaces.cannotCheckTooltip": "このオプションを使用するには、追加権限が必要です。", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToAllSpaces.cannotUncheckTooltip": "このオプションを変更するには、追加権限が必要です。", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToAllSpaces.text": "現在と将来のすべてのスペースでオブジェクトを使用可能にします。", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToAllSpaces.title": "すべてのスペース", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToExplicitSpaces.text": "選択したスペースでのみオブジェクトを使用可能にします。", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToExplicitSpaces.title": "スペースを選択", + "xpack.spaces.management.shareToSpace.shareNewSuccessTitle": "オブジェクトは共有されています", + "xpack.spaces.management.shareToSpace.shareRemoveSuccessTextPlural": "「{object}」は{spaceTargets}個のスペースから削除されました。", + "xpack.spaces.management.shareToSpace.shareRemoveSuccessTextSingular": "「{object}」は1つのスペースから削除されました。", + "xpack.spaces.management.shareToSpace.shareToSpacesButton": "保存して閉じる", + "xpack.spaces.management.shareToSpace.shareWarningBody": "1つのスペースでのみ編集するには、{makeACopyLink}してください。", + "xpack.spaces.management.shareToSpace.shareWarningLink": "コピーを作成", + "xpack.spaces.management.shareToSpace.shareWarningTitle": "共有オブジェクトの編集は、すべてのスペースで変更を適用します。", + "xpack.spaces.management.shareToSpace.showLessSpacesLink": "縮小表示", + "xpack.spaces.management.shareToSpace.showMoreSpacesLink": "他{count}件", + "xpack.spaces.management.shareToSpace.spacesLoadErrorTitle": "利用可能なスペースを読み込み中にエラーが発生", + "xpack.spaces.management.shareToSpace.unknownSpacesLabel.additionalPrivilegesLink": "追加権限", + "xpack.spaces.management.shareToSpace.unknownSpacesLabel.text": "非表示のスペースを表示するには、{additionalPrivilegesLink}が必要です。", + "xpack.spaces.management.shareToSpaceFlyoutHeader": "スペースと共有", "xpack.spaces.management.showAllFeaturesText": "すべて表示", "xpack.spaces.management.spaceIdentifier.customizeSpaceLinkText": "[カスタマイズ]", "xpack.spaces.management.spaceIdentifier.customizeSpaceNameLinkLabel": "URL 識別子をカスタマイズ", @@ -17403,6 +19590,7 @@ "xpack.spaces.management.spacesGridPage.spacesTitle": "スペース", "xpack.spaces.management.spacesGridPage.spaceSuccessfullyDeletedNotificationMessage": "「{spaceName}」 スペースが削除されました。", "xpack.spaces.management.toggleAllFeaturesLink": "(すべて変更)", + "xpack.spaces.management.unauthorizedPrompt.permissionDeniedDescription": "スペースを管理するアクセス権がありません。", "xpack.spaces.management.unauthorizedPrompt.permissionDeniedTitle": "パーミッションが拒否されました", "xpack.spaces.management.validateSpace.describeMaxLengthErrorMessage": "説明は 2000 文字以内でなければなりません。", "xpack.spaces.management.validateSpace.nameMaxLengthErrorMessage": "名前はは 1024 文字以内でなければなりません。", @@ -17415,10 +19603,54 @@ "xpack.spaces.navControl.spacesMenu.noSpacesFoundTitle": " スペースが見つかりません ", "xpack.spaces.spaceSelector.appTitle": "スペースを選択", "xpack.spaces.spaceSelector.changeSpaceAnytimeAvailabilityText": "スペースはいつでも変更できます", + "xpack.spaces.spaceSelector.contactSysAdminDescription": "システム管理者にお問い合わせください。", + "xpack.spaces.spaceSelector.errorLoadingSpacesDescription": "スペースの読み込みエラー({message})", "xpack.spaces.spaceSelector.findSpacePlaceholder": "スペースを検索", "xpack.spaces.spaceSelector.noSpacesMatchSearchCriteriaDescription": "検索条件に一致するスペースがありません", "xpack.spaces.spaceSelector.selectSpacesTitle": "スペースの選択", "xpack.spaces.spacesTitle": "スペース", + "xpack.stackAlerts.featureRegistry.actionsFeatureName": "スタックアラート", + "xpack.stackAlerts.geoThreshold.actionGroupThresholdMetTitle": "追跡しきい値が満たされました", + "xpack.stackAlerts.geoThreshold.actionVariableContextCrossingDocumentIdLabel": "クロスエンティティドキュメントのID", + "xpack.stackAlerts.geoThreshold.actionVariableContextCrossingLineLabel": "クロスイベントを決定するために使用された2つの場所を接続するGeoJSON行", + "xpack.stackAlerts.geoThreshold.actionVariableContextCurrentBoundaryIdLabel": "エンティティを含む現在の境界ID(該当する場合)", + "xpack.stackAlerts.geoThreshold.actionVariableContextEntityIdLabel": "アラートをトリガーしたドキュメントのエンティティID", + "xpack.stackAlerts.geoThreshold.actionVariableContextFromBoundaryIdLabel": "エンティティを含む以前の境界ID(該当する場合)", + "xpack.stackAlerts.geoThreshold.actionVariableContextFromBoundaryNameLabel": "エンティティがそこからクロスし、以前に検出された境界(該当する場合)", + "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityDateTimeLabel": "前回エンティティが前の境界で記録された日時", + "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityDocumentIdLabel": "クロスエンティティドキュメントのID", + "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityLocationLabel": "エンティティの以前に取り込まれた場所", + "xpack.stackAlerts.geoThreshold.actionVariableContextTimeOfDetectionLabel": "この変更が記録された、アラート間隔終了日時", + "xpack.stackAlerts.geoThreshold.actionVariableContextToBoundaryNameLabel": "エンティティがその中にクロスし、現在検出されている境界(該当する場合)", + "xpack.stackAlerts.geoThreshold.actionVariableContextToEntityDateTimeLabel": "現在の境界でエンティティが検出された日時", + "xpack.stackAlerts.geoThreshold.actionVariableContextToEntityLocationLabel": "エンティティの直近に取り込まれた場所", + "xpack.stackAlerts.geoThreshold.alertTypeTitle": "地理追跡しきい値", + "xpack.stackAlerts.indexThreshold.actionGroupThresholdMetTitle": "しきい値一致", + "xpack.stackAlerts.indexThreshold.actionVariableContextDateLabel": "アラートがしきい値を超えた日付。", + "xpack.stackAlerts.indexThreshold.actionVariableContextGroupLabel": "しきい値を超えたグループ。", + "xpack.stackAlerts.indexThreshold.actionVariableContextMessageLabel": "アラートの事前構成メッセージ。", + "xpack.stackAlerts.indexThreshold.actionVariableContextThresholdComparatorLabel": "しきい値に達したかどうかを判定するために使用する比較関数。", + "xpack.stackAlerts.indexThreshold.actionVariableContextThresholdLabel": "しきい値として使用する値の配列。「between」と「notBetween」には2つの値が必要です。その他は1つの値が必要です。", + "xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "アラートの事前構成タイトル。", + "xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "しきい値を超えた値。", + "xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage": "[aggType]が「{aggType}」のときには[aggField]に値が必要です", + "xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "アラート{name}グループ{group}値{value}が{date}に{window}にわたってしきい値{function}を超えました", + "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "アラート{name}グループ{group}がしきい値を超えました", + "xpack.stackAlerts.indexThreshold.alertTypeTitle": "インデックスしきい値", + "xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage": "[dateStart]が[dateEnd]よりも大です", + "xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage": "{fieldName}の無効な{formatName}形式:「{fieldValue}」", + "xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage": "[interval]: [dateStart]が[dateEnd]と等しくない場合に指定する必要があります", + "xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage": "無効な aggType:「{aggType}」", + "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "無効なthresholdComparatorが指定されました: {comparator}", + "xpack.stackAlerts.indexThreshold.invalidDateErrorMessage": "無効な日付{date}", + "xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage": "無効な期間:「{duration}」", + "xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage": "無効なgroupBy:「{groupBy}」", + "xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage": "[termSize]: {maxGroups}以下でなければなりません。", + "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]: 「{thresholdComparator}」比較子の場合には2つの要素が必要です", + "xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage": "無効なtimeWindowUnit:「{timeWindowUnit}」", + "xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage": "間隔{intervals}の計算値が{maxIntervals}よりも大です", + "xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage": "[termField]: [groupBy]がトップのときにはtermFieldが必要です", + "xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage": "[termSize]: [groupBy]がトップのときにはtermSizeが必要です", "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "ディスティネーションインデックスの削除", "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "ディスティネーションインデックスパターンの削除", "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "ディスティネーションインデックス{destinationIndex}の削除", @@ -17448,8 +19680,10 @@ "xpack.transform.capability.noPermission.deleteTransformTooltip": "データフレーム変換を削除するパーミッションがありません。", "xpack.transform.capability.noPermission.startOrStopTransformTooltip": "データフレーム変換を開始・停止するパーミッションがありません。", "xpack.transform.capability.pleaseContactAdministratorTooltip": "{message} 管理者にお問い合わせください。", - "xpack.transform.clone.errorPromptText": "Kibana インデックスパターン ID を取得できませんでした。", + "xpack.transform.clone.errorPromptText": "ソースインデックスパターンが存在するかどうかを確認するときにエラーが発生しました", "xpack.transform.clone.errorPromptTitle": "変換構成の取得中にエラーが発生しました。", + "xpack.transform.clone.fetchErrorPromptText": "KibanaインデックスパターンIDを取得できませんでした。", + "xpack.transform.clone.noIndexPatternErrorPromptText": "変換を複製できません{indexPattern}のインデックスパターンが存在しません。", "xpack.transform.cloneTransform.breadcrumbTitle": "クローン変換", "xpack.transform.createTransform.breadcrumbTitle": "変換の作成", "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "ディスティネーションインデックス{destinationIndex}の削除中にエラーが発生しました", @@ -17520,6 +19754,7 @@ "xpack.transform.stepCreateForm.startTransformButton": "開始", "xpack.transform.stepCreateForm.startTransformDescription": "変換を開始します。変換は、クラスターの検索とインデックスによる負荷を増やします。過剰な負荷が生じた場合は変換を停止してください。変換の開始後、変換の閲覧を続けるオプションが提供されます。", "xpack.transform.stepCreateForm.startTransformErrorMessage": "変換 {transformId} の開始中にエラーが発生しました。", + "xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage": "変換開始要求の呼び出し中にエラーが発生しました。", "xpack.transform.stepCreateForm.startTransformSuccessMessage": "変換 {transformId} の開始リクエストが受け付けられました。", "xpack.transform.stepCreateForm.transformListCardDescription": "データフレームジョブの管理ページに戻ります。", "xpack.transform.stepCreateForm.transformListCardTitle": "データフレームジョブ", @@ -17563,6 +19798,7 @@ "xpack.transform.stepDefineSummary.queryCodeBlockLabel": "クエリ", "xpack.transform.stepDefineSummary.queryLabel": "クエリ", "xpack.transform.stepDefineSummary.savedSearchLabel": "保存検索", + "xpack.transform.stepDetailsForm.advancedSettingsAccordionButtonContent": "高度な設定", "xpack.transform.stepDetailsForm.continuousModeAriaLabel": "遅延を選択してください。", "xpack.transform.stepDetailsForm.continuousModeDateFieldHelpText": "新しいドキュメントを特定するために使用できる日付フィールドを選択してください。", "xpack.transform.stepDetailsForm.continuousModeDateFieldLabel": "日付フィールド", @@ -17575,11 +19811,23 @@ "xpack.transform.stepDetailsForm.destinationIndexInvalidError": "無効なデスティネーションインデックス名。", "xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink": "インデックス名の制限に関する詳細。", "xpack.transform.stepDetailsForm.destinationIndexLabel": "デスティネーションインデックス", + "xpack.transform.stepDetailsForm.editFlyoutFormFrequencyPlaceholderText": "デフォルト:{defaultValue}", + "xpack.transform.stepDetailsForm.editFlyoutFormMaxPageSearchSizePlaceholderText": "デフォルト:{defaultValue}", "xpack.transform.stepDetailsForm.errorGettingIndexNames": "既存のインデックス名の取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "既存の変換 ID の取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.errorGettingTransformPreview": "変換プレビューの取得中にエラーが発生しました。", + "xpack.transform.stepDetailsForm.frequencyAriaLabel": "頻度を選択してください。", + "xpack.transform.stepDetailsForm.frequencyError": "無効な頻度形式", + "xpack.transform.stepDetailsForm.frequencyHelpText": "変換が連続実行されているときにソースインデックスで変更を確認する間の間隔。また、変換が検索またはインデックス中に一時障害が発生した場合に、再試行する間隔も決定します。最小値は1秒で、最大値は1時間です。", + "xpack.transform.stepDetailsForm.frequencyLabel": "頻度", + "xpack.transform.stepDetailsForm.indexPatternTimeFieldHelpText": "グローバル時間フィルターで使用するためのプライマリ時間フィールドを選択してください。", + "xpack.transform.stepDetailsForm.indexPatternTimeFieldLabel": "時間フィールド", "xpack.transform.stepDetailsForm.indexPatternTitleError": "このタイトルのインデックスパターンが既に存在します。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeAriaLabel": "最大ページ検索サイズを選択してください。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeError": "max_page_search_sizeは10~10000の範囲の数値でなければなりません。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeHelpText": "各チェックポイントの複合集計で使用する、初期ページサイズを定義します。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeLabel": "最大ページ検索サイズ", "xpack.transform.stepDetailsForm.noTimeFieldOptionLabel": "時間フィルターを使用しない", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "オプションの変換の説明を選択してください。", "xpack.transform.stepDetailsForm.transformDescriptionLabel": "変換の説明", @@ -17588,9 +19836,13 @@ "xpack.transform.stepDetailsForm.transformIdInputAriaLabel": "固有のジョブ ID を選択してください。", "xpack.transform.stepDetailsForm.transformIdInvalidError": "小文字のアルファベットと数字 (a-z と 0-9)、ハイフンまたはアンダーラインのみ使用でき、最初と最後を英数字にする必要があります。", "xpack.transform.stepDetailsForm.transformIdLabel": "ジョブ ID", + "xpack.transform.stepDetailsSummary.advancedSettingsAccordionButtonContent": "高度な設定", "xpack.transform.stepDetailsSummary.continuousModeDateFieldLabel": "連続モード日付フィールド", "xpack.transform.stepDetailsSummary.createIndexPatternMessage": "このジョブの Kibana インデックスパターンが作成されます。", "xpack.transform.stepDetailsSummary.destinationIndexLabel": "デスティネーションインデックス", + "xpack.transform.stepDetailsSummary.frequencyLabel": "頻度", + "xpack.transform.stepDetailsSummary.indexPatternTimeFieldLabel": "Kibanaインデックスパターン時間フィールド", + "xpack.transform.stepDetailsSummary.maxPageSearchSizeLabel": "最大ページ検索サイズ", "xpack.transform.stepDetailsSummary.transformDescriptionLabel": "変換の説明", "xpack.transform.stepDetailsSummary.transformIdLabel": "ジョブ ID", "xpack.transform.tableActionLabel": "アクション", @@ -17603,25 +19855,40 @@ "xpack.transform.transformList.bulkDeleteModalTitle": "{count} 件の{count, plural, one {変換} other {変換}}を削除", "xpack.transform.transformList.bulkDeleteTransformSuccessMessage": "{count} {count, plural, one {個の変換} other {個の変換}}を正常に削除しました。", "xpack.transform.transformList.bulkStartModalTitle": "{count} 件の{count, plural, one {変換} other {変換}}を開始", + "xpack.transform.transformList.cloneActionNameText": "クローンを作成", "xpack.transform.transformList.completeBatchTransformBulkActionToolTip": "1 つまたは複数の変換が完了済みの一斉変換で、再度開始できません。", "xpack.transform.transformList.completeBatchTransformToolTip": "{transformId} は完了済みの一斉変換で、再度開始できません。", "xpack.transform.transformList.createTransformButton": "変換の作成", "xpack.transform.transformList.deleteActionDisabledToolTipContent": "削除するにはデータフレームジョブを停止してください。", + "xpack.transform.transformList.deleteActionNameText": "削除", "xpack.transform.transformList.deleteBulkActionDisabledToolTipContent": "削除するには、選択された変換のうちの 1 つまたは複数を停止する必要があります。", "xpack.transform.transformList.deleteModalCancelButton": "キャンセル", "xpack.transform.transformList.deleteModalDeleteButton": "削除", - "xpack.transform.transformList.deleteModalTitle": "{transformId} 削除", + "xpack.transform.transformList.deleteModalTitle": "{transformId}を削除しますか?", "xpack.transform.transformList.deleteTransformErrorMessage": "変換 {transformId} の削除中にエラーが発生しました", "xpack.transform.transformList.deleteTransformGenericErrorMessage": "変換を削除するための API エンドポイントの呼び出し中にエラーが発生しました。", "xpack.transform.transformList.deleteTransformSuccessMessage": "変換 {transformId} の削除リクエストが受け付けられました。", + "xpack.transform.transformList.editActionNameText": "編集", "xpack.transform.transformList.editFlyoutCalloutDocs": "ドキュメントを表示", "xpack.transform.transformList.editFlyoutCalloutText": "このフォームでは、変換を更新できます。更新できるプロパティのリストは、変換を作成するときに定義できるリストのサブセットです。", "xpack.transform.transformList.editFlyoutCancelButtonText": "キャンセル", + "xpack.transform.transformList.editFlyoutFormAdvancedSettingsButtonContent": "高度な設定", "xpack.transform.transformList.editFlyoutFormDescriptionLabel": "説明", - "xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "スロットリングを有効にするには、インプットドキュメントの毎秒あたりのドキュメントの上限を設定します。", + "xpack.transform.transformList.editFlyoutFormDestinationButtonContent": "ディスティネーション構成", + "xpack.transform.transformList.editFlyoutFormDestinationIndexLabel": "デスティネーションインデックス", + "xpack.transform.transformList.editFlyoutFormDestinationPipelineLabel": "パイプライン", + "xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "スロットリングを有効にするには、毎秒入力するドキュメントの上限を設定します。", + "xpack.transform.transformList.editFlyoutFormDocsPerSecondLabel": "毎秒あたりのドキュメント", + "xpack.transform.transformList.editFlyoutFormFrequencyHelpText": "変換が連続実行されているときにソースインデックスで変更を確認する間の間隔。また、変換が検索またはインデックス中に一時障害が発生した場合に、再試行する間隔も決定します。最小値は1秒で、最大値は1時間です。", "xpack.transform.transformList.editFlyoutFormFrequencyLabel": "頻度", "xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage": "頻度値が無効です。", - "xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "値はゼロより大きい数値でなければなりません。", + "xpack.transform.transformList.editFlyoutFormFrequencyPlaceholderText": "デフォルト:{defaultValue}", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelptext": "各チェックポイントの複合集計で使用する、初期ページサイズを定義します。", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeLabel": "最大ページ検索サイズ", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizePlaceholderText": "デフォルト:{defaultValue}", + "xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "値は1以上の整数でなければなりません。", + "xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "値は10~10000の範囲の整数でなければなりません。", + "xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必須フィールド。", "xpack.transform.transformList.editFlyoutFormStringNotValidErrorMessage": "値は文字列型でなければなりません。", "xpack.transform.transformList.editFlyoutTitle": "{transformId}を編集", "xpack.transform.transformList.editFlyoutUpdateButtonText": "更新", @@ -17631,17 +19898,22 @@ "xpack.transform.transformList.refreshButtonLabel": "更新", "xpack.transform.transformList.rowCollapse": "{transformId} の詳細を非表示", "xpack.transform.transformList.rowExpand": "{transformId} の詳細を表示", + "xpack.transform.transformList.searchBar.invalidSearchErrorMessage": "無効な検索: {errorMessage}", "xpack.transform.transformList.showDetailsColumn.screenReaderDescription": "このカラムには変換ごとの詳細を示すクリック可能なコントロールが含まれます", + "xpack.transform.transformList.startActionNameText": "開始", "xpack.transform.transformList.startedTransformBulkToolTip": "1 つまたは複数の変換が既に開始済みです。", "xpack.transform.transformList.startedTransformToolTip": "{transformId} は既に開始済みです。", + "xpack.transform.transformList.startModalBody": "変換は、クラスターの検索とインデックスによる負荷を増やします。過剰な負荷が生じた場合は変換を停止してください。", "xpack.transform.transformList.startModalCancelButton": "キャンセル", "xpack.transform.transformList.startModalStartButton": "開始", - "xpack.transform.transformList.startModalTitle": "{transformId} を開始", + "xpack.transform.transformList.startModalTitle": "{transformId}を開始しますか?", "xpack.transform.transformList.startTransformErrorMessage": "変換 {transformId} の開始中にエラーが発生しました", "xpack.transform.transformList.startTransformSuccessMessage": "変換 {transformId} の開始リクエストが受け付けられました。", + "xpack.transform.transformList.stopActionNameText": "終了", "xpack.transform.transformList.stoppedTransformBulkToolTip": "1 つまたは複数の変換が既に開始済みです。", "xpack.transform.transformList.stoppedTransformToolTip": "{transformId} は既に停止済みです。", "xpack.transform.transformList.stopTransformErrorMessage": "データフレーム変換 {transformId} の停止中にエラーが発生しました", + "xpack.transform.transformList.stopTransformResponseSchemaErrorMessage": "変換停止要求の呼び出し中にエラーが発生しました。", "xpack.transform.transformList.stopTransformSuccessMessage": "データフレーム変換 {transformId} の停止リクエストが受け付けられました。", "xpack.transform.transformList.transformDescription": "変換を使用して、集約されたインデックスまたはエンティティ中心のインデックスに、既存のElasticsearchインデックスをインデックスします。", "xpack.transform.transformList.transformDetails.messagesPane.errorMessage": "メッセージを読み込めませんでした", @@ -17671,6 +19943,7 @@ "xpack.triggersActionsUI.actionVariables.tagsLabel": "アラートのタグ。", "xpack.triggersActionsUI.alerts.breadcrumbTitle": "アラート", "xpack.triggersActionsUI.appName": "アラートとアクション", + "xpack.triggersActionsUI.case.configureCases.mappingFieldSummary": "まとめ", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "このコネクターは Kibana の構成で無効になっています。", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "このコネクターには {minimumLicenseRequired} ライセンスが必要です。", "xpack.triggersActionsUI.common.constants.comparators.groupByTypes.allDocumentsLabel": "すべてのドキュメント", @@ -17709,6 +19982,9 @@ "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.configureAccountsHelpLabel": "電子メールアカウントを構成しています。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "サーバーからメールを送信します。", "xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "送信元は有効なメールアドレスではありません。", + "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText": "パスワードが必要です。", + "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthUserNameText": "ユーザー名が必要です。", + "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson": "ドキュメントが必要です。有効なJSONオブジェクトにしてください。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "To、Cc、または Bcc のエントリーがありません。 1 つ以上のエントリーが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText": "送信元が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText": "ホストが必要です。", @@ -17748,6 +20024,37 @@ "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshLabel": "更新インデックス", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshTooltip": "影響を受けるシャードを更新し、この処理を検索できるようにします。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText": "データを Elasticsearch にインデックスしてください。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle": "Jira", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel": "APIトークンまたはパスワード", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel": "URL", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel": "追加のコメント(任意)", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.descriptionTextAreaFieldLabel": "説明(オプション)", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel": "電子メールアドレスまたはユーザー名", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.impactSelectFieldLabel": "ラベル (任意)", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField": "URLが無効です。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments": "コメント", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription": "説明", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.parentIssueSearchLabel": "親問題", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey": "プロジェクトキー", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField": "APIトークンまたはパスワードが必要です", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField": "URLが必要です。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "説明が必要です。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "電子メールアドレスまたはユーザー名が必要です", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "プロジェクトキーが必要です", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredTitleTextField": "タイトルが必要です。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldHelp": "JIRAは、このアクションを、Kibanaの保存されたオブジェクトのIDに関連付けます。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldLabel": "オブジェクトID(任意)", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "親問題を選択", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder": "親問題を選択", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesLoading": "読み込み中…", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText": "Jiraでデータを更新するか、新しい問題にプッシュ", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.severitySelectFieldLabel": "優先度", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel": "まとめ", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetFieldsMessage": "フィールドを取得できません", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueMessage": "ID {id}の問題を取得できません", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssuesMessage": "問題を取得できません", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage": "問題タイプを取得できません", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.urgencySelectFieldLabel": "問題タイプ", "xpack.triggersActionsUI.components.builtinActionTypes.mappingFieldNotMapped": "マップされません", "xpack.triggersActionsUI.components.builtinActionTypes.noConnector": "コネクターを選択していません", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "PagerDuty に送信", @@ -17755,8 +20062,10 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "クラス (任意)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.componentTextFieldLabel": "コンポーネント(任意)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.dedupKeyTextFieldLabel": "DedupKey (任意)", + "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.dedupKeyTextRequiredFieldLabel": "DedupKey", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.invalidTimestamp": "タイムスタンプは、{nowShortFormat}や{nowLongFormat}などの有効な日付でなければなりません。", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText": "ルーティングキーが必要です。", + "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredDedupKeyText": "インシデントを解決または確認するときには、DedupKeyが必要です。", + "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText": "統合キー/ルーティングキーが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText": "概要が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventActionSelectFieldLabel": "イベントアクション", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectAcknowledgeOptionLabel": "承認", @@ -17774,6 +20083,29 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.sourceTextFieldLabel": "ソース (任意)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "まとめ", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "タイムスタンプ (任意)", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle": "Resilient", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId": "APIキーID", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret": "APIキーシークレット", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel": "URL", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel": "追加のコメント(任意)", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.descriptionTextAreaFieldLabel": "説明(オプション)", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField": "URLが無効です。", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments": "コメント", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription": "説明", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription": "名前", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId": "組織ID", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField": "APIキーIDが必要です", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField": "APIキーシークレットが必要です", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField": "URLが必要です。", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField": "組織IDが必要です", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.savedObjectIdFieldHelp": "IBM Resilientは、このアクションを、Kibanaの保存されたオブジェクトのIDに関連付けます。", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.savedObjectIdFieldLabel": "オブジェクトID(任意)", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText": "Resilientでデータを更新するか、または新しいインシデントにプッシュします。", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.severity": "深刻度", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.titleFieldLabel": "名前", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage": "インシデントタイプを取得できません", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetSeverityMessage": "深刻度を取得できません", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.urgencySelectFieldLabel": "インシデントタイプ", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle": "サーバーログに送信", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logLevelFieldLabel": "レベル", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "メッセージ", @@ -17795,11 +20127,14 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredEmailTextField": "電子メールが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "パスワードが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "ユーザー名が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText": "ServiceNowでデータを更新するか、または新しいインシデントにプッシュします。", + "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.savedObjectIdFieldHelp": "ServiceNowは、このアクションを、Kibanaの保存されたオブジェクトのIDに関連付けます。", + "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.savedObjectIdFieldLabel": "オブジェクトID(任意)", + "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText": "ServiceNowでインシデントを作成します。", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.severitySelectFieldLabel": "深刻度", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectHighOptionLabel": "高", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectLawOptionLabel": "低", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectMediumOptionLabel": "中", + "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.title": "インシデント", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.titleFieldLabel": "短い説明", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.urgencySelectFieldLabel": "緊急", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel": "ユーザー名", @@ -17853,6 +20188,42 @@ "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "{numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}}を削除 ", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText": "{numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}を回復できません。", + "xpack.triggersActionsUI.geoThreshold.boundaryNameSelect": "境界名を選択", + "xpack.triggersActionsUI.geoThreshold.boundaryNameSelectLabel": "人間が読み取れる境界名(任意)", + "xpack.triggersActionsUI.geoThreshold.delayOffset": "遅延評価オフセット", + "xpack.triggersActionsUI.geoThreshold.delayOffsetTooltip": "遅延サイクルでアラートを評価し、データレイテンシに合わせて調整します", + "xpack.triggersActionsUI.geoThreshold.entityByLabel": "グループ基準", + "xpack.triggersActionsUI.geoThreshold.entityIndexLabel": "インデックス", + "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryGeoFieldText": "境界地理フィールドは必須です。", + "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryIndexTitleText": "境界インデックスパターンタイトルは必須です。", + "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryTypeText": "境界タイプは必須です。", + "xpack.triggersActionsUI.geoThreshold.error.requiredDateFieldText": "日付フィールドが必要です。", + "xpack.triggersActionsUI.geoThreshold.error.requiredEntityText": "エンティティは必須です。", + "xpack.triggersActionsUI.geoThreshold.error.requiredGeoFieldText": "地理フィールドは必須です。", + "xpack.triggersActionsUI.geoThreshold.error.requiredIndexTitleText": "インデックスパターンが必要です。", + "xpack.triggersActionsUI.geoThreshold.error.requiredTrackingEventText": "追跡イベントは必須です。", + "xpack.triggersActionsUI.geoThreshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", + "xpack.triggersActionsUI.geoThreshold.geofieldLabel": "地理空間フィールド", + "xpack.triggersActionsUI.geoThreshold.indexLabel": "インデックス", + "xpack.triggersActionsUI.geoThreshold.indexPatternSelectLabel": "インデックスパターン", + "xpack.triggersActionsUI.geoThreshold.indexPatternSelectPlaceholder": "インデックスパターンを選択", + "xpack.triggersActionsUI.geoThreshold.name.trackingThreshold": "追跡しきい値", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisLinkTextDescription": "インデックスパターンを作成します", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisPrefixDescription": "次のことが必要です ", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisSuffixDescription": " 地理空間フィールドを含む", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.getStartedLinkText": "サンプルデータセットで始めましょう。", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.hintDescription": "地理空間データセットがありませんか? ", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.messageTitle": "地理空間フィールドを含むインデックスパターンが見つかりませんでした", + "xpack.triggersActionsUI.geoThreshold.selectBoundaryIndex": "境界を選択:", + "xpack.triggersActionsUI.geoThreshold.selectEntity": "エンティティを選択", + "xpack.triggersActionsUI.geoThreshold.selectGeoLabel": "ジオフィールドを選択", + "xpack.triggersActionsUI.geoThreshold.selectIndex": "条件を定義してください", + "xpack.triggersActionsUI.geoThreshold.selectLabel": "ジオフィールドを選択", + "xpack.triggersActionsUI.geoThreshold.selectOffset": "オフセットを選択(任意)", + "xpack.triggersActionsUI.geoThreshold.selectTimeLabel": "時刻フィールドを選択", + "xpack.triggersActionsUI.geoThreshold.timeFieldLabel": "時間フィールド", + "xpack.triggersActionsUI.geoThreshold.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", + "xpack.triggersActionsUI.geoThreshold.whenEntityLabel": "エンティティ", "xpack.triggersActionsUI.home.alertsTabTitle": "アラート", "xpack.triggersActionsUI.home.appTitle": "アラートとアクション", "xpack.triggersActionsUI.home.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", @@ -17863,6 +20234,7 @@ "xpack.triggersActionsUI.sections.actionAdd.indexAction.indexTextFieldLabel": "タグ (任意)", "xpack.triggersActionsUI.sections.actionConnectorAdd.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.sections.actionConnectorAdd.manageLicensePlanBannerLinkTitle": "ライセンスの管理", + "xpack.triggersActionsUI.sections.actionConnectorAdd.saveAndTestButtonLabel": "保存してテスト", "xpack.triggersActionsUI.sections.actionConnectorAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerLinkTitle": "サブスクリプションオプション", "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerMessage": "すべてのサードパーティコネクターにすぐにアクセスするには、ライセンスをアップグレードするか、30日間無料の試用版を開始してください。", @@ -17870,6 +20242,7 @@ "xpack.triggersActionsUI.sections.actionConnectorForm.actionNameLabel": "コネクター名", "xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningHelpLinkText": "詳細情報", "xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionTypeConfigurationWarningTitleText": "アクションタイプが登録されていません。", + "xpack.triggersActionsUI.sections.actionConnectorForm.connectorSettingsLabel": "コネクター設定", "xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "名前が必要です。", "xpack.triggersActionsUI.sections.actionForm.getMoreActionsTitle": "さらにアクションを表示", "xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage": "(構成済み)", @@ -17879,11 +20252,15 @@ "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDescription": "このコネクターを削除", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDisabledDescription": "コネクターを削除できません", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionName": "削除", + "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorDescription": "このコネクターを実行", + "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorDisabledDescription": "コネクターを実行できません", + "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorName": "実行", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actionTypeTitle": "タイプ", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.nameTitle": "名前", "xpack.triggersActionsUI.sections.actionsConnectorsList.filters.actionTypeIdName": "タイプ", "xpack.triggersActionsUI.sections.actionsConnectorsList.multipleTitle": "コネクター", - "xpack.triggersActionsUI.sections.actionsConnectorsList.noPermissionToCreateTitle": "コネクターを作成するパーミッションがありません。", + "xpack.triggersActionsUI.sections.actionsConnectorsList.noPermissionToCreateDescription": "システム管理者にお問い合わせください。", + "xpack.triggersActionsUI.sections.actionsConnectorsList.noPermissionToCreateTitle": "コネクターを作成する権限がありません", "xpack.triggersActionsUI.sections.actionsConnectorsList.singleTitle": "コネクター", "xpack.triggersActionsUI.sections.actionsConnectorsList.unableToLoadActionsMessage": "コネクターを読み込めません", "xpack.triggersActionsUI.sections.actionsConnectorsList.unableToLoadActionTypesMessage": "アクションタイプを読み込めません", @@ -17917,7 +20294,9 @@ "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "条件を定義してください", "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "アラートビジュアライゼーションを読み込めません", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "アラートの作成", + "xpack.triggersActionsUI.sections.alertAdd.geoThreshold.closePopoverLabel": "閉じる", "xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中...", + "xpack.triggersActionsUI.sections.alertAdd.operationName": "作成", "xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription": "プレビューを生成するための式を完成します。", "xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "アラートを作成できません。", @@ -17941,11 +20320,13 @@ "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.start": "開始", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.status": "ステータス", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active": "アクティブ", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "非アクティブ", + "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "OK", "xpack.triggersActionsUI.sections.alertDetails.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.disableTitle": "無効にする", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle": "ミュート", + "xpack.triggersActionsUI.sections.alertDetails.dismissButtonTitle": "閉じる", "xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel": "編集", + "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertInstanceSummaryMessage": "アラートインスタンス概要を読み込めません:{message}", "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage": "アラートを読み込めません: {message}", "xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel": "アプリで表示", "xpack.triggersActionsUI.sections.alertEdit.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", @@ -17970,7 +20351,9 @@ "xpack.triggersActionsUI.sections.alertForm.changeAlertTypeAriaLabel": "削除", "xpack.triggersActionsUI.sections.alertForm.checkFieldLabel": "確認間隔", "xpack.triggersActionsUI.sections.alertForm.checkWithTooltip": "条件を評価する頻度を定義します。", - "xpack.triggersActionsUI.sections.alertForm.emptyConnectorsLabel": "{actionTypeName} 接続がありません。", + "xpack.triggersActionsUI.sections.alertForm.emptyConnectorsLabel": "{actionTypeName}コネクターがありません", + "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedAlertTypes": "アラートを{operation}するには、適切な権限が付与されている必要があります。", + "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedAlertTypesTitle": "アラートタイプを{operation}する権限がありません。", "xpack.triggersActionsUI.sections.alertForm.error.requiredAlertTypeIdText": "アラートトリガーが必要です。", "xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText": "確認間隔が必要です。", "xpack.triggersActionsUI.sections.alertForm.error.requiredNameText": "名前が必要です。", @@ -17989,14 +20372,27 @@ "xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage": "アクションタイプを読み込めません", "xpack.triggersActionsUI.sections.alertForm.unableToLoadAlertTypesMessage": "アラートタイプを読み込めません", "xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle": "コネクターを読み込めません。", + "xpack.triggersActionsUI.sections.alertForm.unauthorizedToCreateForEmptyConnectors": "許可されたユーザーのみがコネクターを構成できます。管理者にお問い合わせください。", "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "アクションタイプ", "xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "アラートの作成", + "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonDecrypting": "アラートの復号中にエラーが発生しました。", + "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonReading": "アラートの読み取り中にエラーが発生しました。", + "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "アラートの実行中にエラーが発生しました。", + "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "不明な理由でエラーが発生しました。", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "アクション", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsText": "アクション", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "タイプ", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "次の間隔で実行", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名前", + "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle": "ステータス", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "タグ", + "xpack.triggersActionsUI.sections.alertsList.alertStatusActive": "アクティブ", + "xpack.triggersActionsUI.sections.alertsList.alertStatusError": "エラー", + "xpack.triggersActionsUI.sections.alertsList.alertStatusFilterLabel": "ステータス", + "xpack.triggersActionsUI.sections.alertsList.alertStatusOk": "OK", + "xpack.triggersActionsUI.sections.alertsList.alertStatusPending": "保留中", + "xpack.triggersActionsUI.sections.alertsList.alertStatusUnknown": "不明", + "xpack.triggersActionsUI.sections.alertsList.attentionBannerTitle": "{totalStausesError} {totalStausesError, plural, one {{singleTitle}} other {# {multipleTitle}}}でエラーが見つかりました。", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.buttonTitle": "アラートを管理", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.deleteAllTitle": "削除", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.disableAllTitle": "無効にする", @@ -18014,16 +20410,28 @@ "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteHelpText": "ミュートにすると、アラートは確認されますが、アクションは実行されません。", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "ミュート", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "アクション", + "xpack.triggersActionsUI.sections.alertsList.dismissBunnerButtonLabel": "閉じる", "xpack.triggersActionsUI.sections.alertsList.multipleTitle": "アラート", + "xpack.triggersActionsUI.sections.alertsList.noPermissionToCreateDescription": "システム管理者にお問い合わせください。", + "xpack.triggersActionsUI.sections.alertsList.noPermissionToCreateTitle": "アラートを作成する権限がありません", "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "検索", "xpack.triggersActionsUI.sections.alertsList.singleTitle": "アラート", + "xpack.triggersActionsUI.sections.alertsList.totalItemsCountDescription": "{pageSize}/{totalItemCount}件のアラートを表示しています。", + "xpack.triggersActionsUI.sections.alertsList.totalStausesActiveDescription": "有効:{totalStausesActive}", + "xpack.triggersActionsUI.sections.alertsList.totalStausesErrorDescription": "エラー:{totalStausesError}", + "xpack.triggersActionsUI.sections.alertsList.totalStausesOkDescription": "Ok:{totalStausesOk}", + "xpack.triggersActionsUI.sections.alertsList.totalStausesPendingDescription": "保留:{totalStausesPending}", + "xpack.triggersActionsUI.sections.alertsList.totalStausesUnknownDescription": "不明:{totalStausesUnknown}", "xpack.triggersActionsUI.sections.alertsList.typeFilterLabel": "タイプ", "xpack.triggersActionsUI.sections.alertsList.unableToLoadActionTypesMessage": "アクションタイプを読み込めません", "xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertsMessage": "アラートを読み込めません", + "xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertsStatusesInfoMessage": "アラートステータス情報を読み込めません", "xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertTypesMessage": "アラートタイプを読み込めません", - "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "{titleBcc}", + "xpack.triggersActionsUI.sections.alertsList.viewBunnerButtonLabel": "表示", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "Cc を追加", + "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "認証", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel": "送信元", + "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hasAuthSwitchLabel": "このサーバーの認証が必要です", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel": "ホスト", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.messageTextAreaFieldLabel": "メッセージ", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel": "パスワード", @@ -18040,20 +20448,37 @@ "xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "このコネクターは読み取り専用です。", "xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "コネクターを編集", "xpack.triggersActionsUI.sections.editConnectorForm.preconfiguredHelpLabel": "あらかじめ構成されたコネクターの詳細をご覧ください。", + "xpack.triggersActionsUI.sections.editConnectorForm.saveAndCloseButtonLabel": "保存して閉じる", "xpack.triggersActionsUI.sections.editConnectorForm.saveButtonLabel": "保存", + "xpack.triggersActionsUI.sections.editConnectorForm.tabText": "構成", "xpack.triggersActionsUI.sections.editConnectorForm.updateErrorNotificationText": "コネクターを更新できません。", "xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText": "「{connectorName}」を更新しました", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.betaBadgeTooltipContent": "{pluginName}はベータ段階で、変更される可能性があります。デザインとコードはオフィシャルGA機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャルGA機能のSLAが適用されません。", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.flyoutTitle": "{connectorName}", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.tooltipContent": "このコネクターはあらかじめ構成されているため、編集できません。", + "xpack.triggersActionsUI.sections.testConnectorForm.awaitingExecutionDescription": "アクションを実行すると、結果がここに表示されます。", + "xpack.triggersActionsUI.sections.testConnectorForm.executeTestButton": "実行", + "xpack.triggersActionsUI.sections.testConnectorForm.executeTestDisabled": "コネクターをテストする前に、変更を保存してください。", + "xpack.triggersActionsUI.sections.testConnectorForm.executionFailureAdditionalDetails": "詳細:", + "xpack.triggersActionsUI.sections.testConnectorForm.executionFailureDescription": "次のエラーが見つかりました。", + "xpack.triggersActionsUI.sections.testConnectorForm.executionFailureTitle": "アクションを実行できませんでした", + "xpack.triggersActionsUI.sections.testConnectorForm.executionFailureUnknownReason": "不明な理由", + "xpack.triggersActionsUI.sections.testConnectorForm.executionSuccessfulDescription": "結果が想定どおりであることを確認してください。", + "xpack.triggersActionsUI.sections.testConnectorForm.executionSuccessfulTitle": "アクションが成功しました", + "xpack.triggersActionsUI.sections.testConnectorForm.tabText": "テスト", "xpack.triggersActionsUI.timeUnits.dayLabel": "{timeValue, plural, one {日} other {日}}", "xpack.triggersActionsUI.timeUnits.hourLabel": "{timeValue, plural, one {時間} other {時間}}", "xpack.triggersActionsUI.timeUnits.minuteLabel": "{timeValue, plural, one {分} other {分}}", "xpack.triggersActionsUI.timeUnits.secondLabel": "{timeValue, plural, one {秒} other {秒}}", "xpack.triggersActionsUI.typeRegistry.get.missingActionTypeErrorMessage": "オブジェクトタイプ「{id}」は登録されていません。", "xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage": "オブジェクトタイプ「{id}」は既に登録されています。", + "xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel": "ベータ", + "xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip": "このアクションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャルGA機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャルGA機能のSLAが適用されません。バグを報告したり、その他のフィードバックを提供したりして、当社を支援してください。", "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "変更", "xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "不十分なライセンスレベル", + "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpText": "概要", + "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip": "ドリルダウンがコンテキストメニューに表示されるタイミングを決定します。", + "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel": "オプションを表示:", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "パネルに追加", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "キャンセル", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "時間範囲", @@ -18091,6 +20516,18 @@ "xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.deleteDrilldownsButtonLabel": "削除({count})", "xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.editDrilldownButtonLabel": "編集", "xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.selectThisDrilldownCheckboxLabel": "このドリルダウンを選択", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle": "変数を追加", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel": "新しいタブで開く", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText": "プレビュー\\{\\{event.*\\}\\}では、変数にダミー値が代入されます。", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel": "URLプレビュー:", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText": "プレビュー", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel": "URLテンプレートを入力:", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText": "例:{exampleUrl}", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText": "構文ヘルプ", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "変数をフィルター", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "ヘルプ", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "無効な形式:{message}", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "無効なフォーマット。例:{exampleUrl}", "xpack.upgradeAssistant.appTitle": "{version} アップグレードアシスタント", "xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.calloutDetail": "{snapshotRestoreDocsButton} でデータをバックアップします。", "xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.snapshotRestoreDocsButtonLabel": "API のスナップショットと復元", @@ -18222,15 +20659,26 @@ "xpack.uptime.alerts.durationAnomaly.clientName": "アップタイム期間異常", "xpack.uptime.alerts.durationAnomaly.defaultActionMessage": "{anomalyStartTimestamp}に、{monitor}、url {monitorUrl}で異常({severity}レベル)応答時間が検出されました。異常重要度スコアは{severityScore}です。\n位置情報{observerLocation}から高い応答時間{slowestAnomalyResponse}が検出されました。想定された応答時間は{expectedResponseTime}です。", "xpack.uptime.alerts.monitorStatus": "稼働状況の監視ステータス", + "xpack.uptime.alerts.monitorStatus.actionVariables.availabilityMessage": "{availabilityRatio}%のしきい値を下回ります。想定される可用性は{expectedAvailability}%です", "xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description": "アラートによって「ダウン」と検知された一部またはすべてのモニターを示す、生成された概要。", "xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description": "現在ダウンしているモニターを要約する生成されたメッセージ。", + "xpack.uptime.alerts.monitorStatus.actionVariables.down": "ダウン", + "xpack.uptime.alerts.monitorStatus.actionVariables.downAndAvailabilityMessage": "{statusMessage}と{availabilityMessage}", "xpack.uptime.alerts.monitorStatus.actionVariables.state.currentTriggerStarted": "アラートがトリガーされた場合、現在のトリガー状態が開始するときを示すタイムスタンプ", "xpack.uptime.alerts.monitorStatus.actionVariables.state.firstCheckedAt": "このアラートが最初に確認されるときを示すタイムスタンプ", "xpack.uptime.alerts.monitorStatus.actionVariables.state.firstTriggeredAt": "このアラートが最初にトリガーされたときを示すタイムスタンプ", "xpack.uptime.alerts.monitorStatus.actionVariables.state.isTriggered": "アラートが現在トリガーしているかどうかを示すフラグ", "xpack.uptime.alerts.monitorStatus.actionVariables.state.lastCheckedAt": "アラートの直近の確認時刻を示すタイムスタンプ", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.lastErrorMessage": "最新のエラーメッセージを監視", "xpack.uptime.alerts.monitorStatus.actionVariables.state.lastResolvedAt": "このアラートの直近の解決を示すタイムスタンプ", "xpack.uptime.alerts.monitorStatus.actionVariables.state.lastTriggeredAt": "アラートの直近のトリガー時刻を示すタイムスタンプ", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.monitor": "名前、IDS、優先名の人間にとってわかりやすい表示(My Monitorなど)", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.monitorId": "モニターのID。", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.monitorType": "モニターのタイプ(HTTP/TCPなど)。", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.monitorUrl": "モニターのURL。", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.observerHostname": "Heartbeatチェックが実行されるオブザーバーのホスト名。", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.observerLocation": "Heartbeatチェックが実行されるオブザーバーの位置情報。", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.statusMessage": "ステータスメッセージ。例:停止、または可用性チェックの場合は可用性しきい値未満、あるいは両方", "xpack.uptime.alerts.monitorStatus.addFilter": "フィルターを追加します", "xpack.uptime.alerts.monitorStatus.addFilter.location": "場所", "xpack.uptime.alerts.monitorStatus.addFilter.port": "ポート", @@ -18247,6 +20695,7 @@ "xpack.uptime.alerts.monitorStatus.availability.unit.headline": "時間範囲単位を選択します", "xpack.uptime.alerts.monitorStatus.availability.unit.selectable": "この選択を使用して、このアラートの可用性範囲単位を設定", "xpack.uptime.alerts.monitorStatus.clientName": "稼働状況の監視ステータス", + "xpack.uptime.alerts.monitorStatus.defaultActionMessage": "URL {monitorUrl}のモニター{monitorName}は{observerLocation}から{statusMessage}です。最新のエラーメッセージは{latestErrorMessage}です", "xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel": "監視状態アラートのフィルター基準を許可するインプット", "xpack.uptime.alerts.monitorStatus.filters.anyLocation": "任意の場所", "xpack.uptime.alerts.monitorStatus.filters.anyPort": "任意のポート", @@ -18282,6 +20731,7 @@ "xpack.uptime.alerts.monitorStatus.timerangeValueField.expression": "within", "xpack.uptime.alerts.monitorStatus.timerangeValueField.value": "最終{value}", "xpack.uptime.alerts.monitorStatus.title.label": "稼働状況の監視ステータス", + "xpack.uptime.alerts.settings.createConnector": "コネクターを作成", "xpack.uptime.alerts.timerangeUnitSelectable.daysOption.ariaLabel": "「日」の時間範囲選択項目", "xpack.uptime.alerts.timerangeUnitSelectable.hoursOption.ariaLabel": "「時間」の時間範囲選択項目", "xpack.uptime.alerts.timerangeUnitSelectable.minutesOption.ariaLabel": "「分」の時間範囲選択項目", @@ -18314,7 +20764,7 @@ "xpack.uptime.alerts.toggleAlertFlyoutButtonText": "アラート", "xpack.uptime.alertsPopover.toggleButton.ariaLabel": "アラートコンテキストメニューを開く", "xpack.uptime.apmIntegrationAction.description": "このモニターの検索 APM", - "xpack.uptime.apmIntegrationAction.text": "ドメインでAPMを確認", + "xpack.uptime.apmIntegrationAction.text": "APMデータを表示", "xpack.uptime.availabilityLabelText": "{value} %", "xpack.uptime.badge.readOnly.text": "読み込み専用", "xpack.uptime.badge.readOnly.tooltip": "を保存できませんでした", @@ -18385,6 +20835,7 @@ "xpack.uptime.ml.enableAnomalyDetectionPanel.createNewJobButtonLabel": "新規ジョブを作成", "xpack.uptime.ml.enableAnomalyDetectionPanel.disableAnomalyAlert": "異常アラートを無効にする", "xpack.uptime.ml.enableAnomalyDetectionPanel.disableAnomalyDetectionTitle": "異常検知を無効にする", + "xpack.uptime.ml.enableAnomalyDetectionPanel.enable_or_manage_job": "異常検知ジョブを有効にできます。ジョブがすでに存在する場合はジョブまたはアラートを管理できます。", "xpack.uptime.ml.enableAnomalyDetectionPanel.enableAnomalyAlert": "異常アラートを有効にする", "xpack.uptime.ml.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle": "異常検知を有効にする", "xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreatedNotificationText": "これで応答時間グラフについての分析が実行されます。応答時間グラフに結果が追加されるまで、しばらく時間がかかる可能性があります。", @@ -18401,7 +20852,7 @@ "xpack.uptime.ml.enableAnomalyDetectionPanel.manageMLJobDescription.noteText": "注:ジョブが結果の計算を開始するまでに少し時間がかかる場合があります。", "xpack.uptime.ml.enableAnomalyDetectionPanel.startTrial": "無料の 14 日トライアルを開始", "xpack.uptime.ml.enableAnomalyDetectionPanel.startTrialDesc": "期間異常検知機能を利用するには、Elastic Platinum ライセンスが必要です。", - "xpack.uptime.monitorCharts.durationChart.leftAxis.title": "期間ms", + "xpack.uptime.monitorCharts.durationChart.leftAxis.title": "期間(ミリ秒)", "xpack.uptime.monitorCharts.monitorDuration.titleLabel": "監視期間", "xpack.uptime.monitorCharts.monitorDuration.titleLabelWithAnomaly": "監視期間 (異常: {noOfAnomalies})", "xpack.uptime.monitorDetails.ml.confirmAlertDeleteMessage": "異常のアラートを削除しますか?", @@ -18409,10 +20860,15 @@ "xpack.uptime.monitorDetails.ml.deleteJobWarning": "ジョブの削除に時間がかかる可能性があります。削除はバックグラウンドで実行され、データの表示がすぐに消えないことがあります。", "xpack.uptime.monitorDetails.ml.deleteMessage": "ジョブを削除中...", "xpack.uptime.monitorList.anomalyColumn.label": "レスポンス異常スコア", + "xpack.uptime.monitorList.defineConnector.description": "アラートを有効にするには、デフォルトのアラートアクションコネクターを定義してください。", + "xpack.uptime.monitorList.disableDownAlert": "ステータスアラートを無効にする", "xpack.uptime.monitorList.downLineSeries.downLabel": "ダウン", "xpack.uptime.monitorList.drawer.locations.statusDown": "{locations}でダウン", "xpack.uptime.monitorList.drawer.locations.statusUp": "{locations}でアップ", "xpack.uptime.monitorList.drawer.missingLocation": "一部のHeartbeatインスタンスには位置情報が定義されていません。Heartbeat構成への{link}。", + "xpack.uptime.monitorList.enabledAlerts.noAlert": "このモニターではアラートが有効ではありません。", + "xpack.uptime.monitorList.enabledAlerts.title": "有効なアラート:", + "xpack.uptime.monitorList.enableDownAlert": "ステータスアラートを有効にする", "xpack.uptime.monitorList.expandDrawerButton.ariaLabel": "ID {id}のモニターの行を展開", "xpack.uptime.monitorList.geoName.helpLinkAnnotation": "場所を追加", "xpack.uptime.monitorList.infraIntegrationAction.container.message": "コンテナーメトリックを表示", @@ -18442,10 +20898,15 @@ "xpack.uptime.monitorList.noDownHistory": "このモニターは選択された時間範囲で一度も{emphasizedText}していません。", "xpack.uptime.monitorList.noItemForSelectedFiltersMessage": "選択されたフィルター条件でモニターが見つかりませんでした", "xpack.uptime.monitorList.noItemMessage": "アップタイムモニターが見つかりません", - "xpack.uptime.monitorList.observabilityIntegrationsColumn.apmIntegrationLink.tooltip": "ここをクリックしてAPM でドメイン「{domain}」を確認します。", + "xpack.uptime.monitorList.observabilityIntegrationsColumn.apmIntegrationLink.tooltip": "ここをクリックすると、APMのドメイン「{domain}」、または明示的に定義された「サービス名」を確認します。", "xpack.uptime.monitorList.observabilityIntegrationsColumn.popoverIconButton.ariaLabel": "URL {monitorUrl}で監査のための移行ポップオーバーを開く", "xpack.uptime.monitorList.pageSizePopoverButtonText": "ページあたりの行数: {size}", "xpack.uptime.monitorList.pageSizeSelect.numRowsItemMessage": "{numRows} 行", + "xpack.uptime.monitorList.redirects.description": "Pingの実行中にHeartbeatは{number}リダイレクトに従いました。", + "xpack.uptime.monitorList.redirects.openWindow": "リンクは新しいウィンドウで開きます。", + "xpack.uptime.monitorList.redirects.title": "リダイレクト", + "xpack.uptime.monitorList.redirects.title.number": "{number}", + "xpack.uptime.monitorList.statusAlert.label": "ステータスアラート", "xpack.uptime.monitorList.statusColumn.downLabel": "ダウン", "xpack.uptime.monitorList.statusColumn.locStatusMessage": "場所{noLoc}か所", "xpack.uptime.monitorList.statusColumn.locStatusMessage.multiple": "場所{noLoc}か所", @@ -18473,11 +20934,22 @@ "xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel": "監視 URL リンク", "xpack.uptime.monitorStatusBar.sslCertificate.title": "TLS証明書", "xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "最終確認からの経過時間", + "xpack.uptime.monitorStatusBar.type.ariaLabel": "モニタータイプ", + "xpack.uptime.monitorStatusBar.type.label": "型", "xpack.uptime.navigateToAlertingButton.content": "アラートを管理", "xpack.uptime.navigateToAlertingUi": "Uptime を離れてアラート管理ページに移動します", "xpack.uptime.notFountPage.homeLinkText": "ホームへ戻る", "xpack.uptime.openAlertContextPanel.ariaLabel": "アラートコンテキストパネルを開くと、アラートタイプを選択できます", "xpack.uptime.openAlertContextPanel.label": "アラートの作成", + "xpack.uptime.overview.alerts.disabled.failed": "アラートを無効にできません。", + "xpack.uptime.overview.alerts.disabled.success": "アラートが正常に無効にされました。", + "xpack.uptime.overview.alerts.enabled.failed": "アラートを有効にできません。", + "xpack.uptime.overview.alerts.enabled.success": "アラートが正常に有効にされました。 ", + "xpack.uptime.overview.alerts.enabled.success.description": "モニターが停止しているときには、メッセージが{actionConnectors}に送信されます。", + "xpack.uptime.overview.pageHeader.syntheticsCallout.announcementLink": "お知らせを読む", + "xpack.uptime.overview.pageHeader.syntheticsCallout.content": "アップタイムは、スクリプト化された複数ステップの可用性チェックのサポートをプレビューしています。つまり、単に単一のページのアップ/ダウンのチェックだけではなく、Webページの要素を操作したり、全体的な可用性を確認したりできます(購入やシステムへのサインインなど)。詳細については以下をクリックしてください。これらの機能を先駆けて使用したい場合は、プレビュー合成エージェントをダウンロードし、アップタイムでチェックを表示できます。", + "xpack.uptime.overview.pageHeader.syntheticsCallout.dismissButtonText": "閉じる", + "xpack.uptime.overview.pageHeader.syntheticsCallout.title": "Elastic Synthetics", "xpack.uptime.overviewPage.headerText": "概要", "xpack.uptime.overviewPageLink.disabled.ariaLabel": "無効になったページ付けボタンです。モニターリストがこれ以上ナビゲーションできないことを示しています。", "xpack.uptime.overviewPageLink.next.ariaLabel": "次の結果ページ", @@ -18519,6 +20991,7 @@ "xpack.uptime.settings.invalid.error": "値は0よりも大きい値でなければなりません。", "xpack.uptime.settings.invalid.nanError": "値は整数でなければなりません。", "xpack.uptime.settings.returnToOverviewLinkLabel": "概要に戻る", + "xpack.uptime.settings.saveSuccess": "設定が保存されました。", "xpack.uptime.settingsBreadcrumbText": "設定", "xpack.uptime.snapshot.donutChart.ariaLabel": "現在のステータスを表す円グラフ、{total}個中{down}個のモニターがダウンしています。", "xpack.uptime.snapshot.donutChart.legend.downRowLabel": "ダウン", @@ -18538,10 +21011,15 @@ "xpack.uptime.sourceConfiguration.ageLimit.units.days": "日", "xpack.uptime.sourceConfiguration.ageLimitThresholdInput.ariaLabel": "TLS証明書が有効である最大日数を制御するインプット。この期間を過ぎると、Kibanaで警告が表示されます。", "xpack.uptime.sourceConfiguration.ageThresholdDefaultValue": "デフォルト値は{defaultValue}です", + "xpack.uptime.sourceConfiguration.alertConnectors": "アラートコネクター", + "xpack.uptime.sourceConfiguration.alertDefaultForm.selectConnector": "1つ以上のコネクターを選択してください", + "xpack.uptime.sourceConfiguration.alertDefaults": "アラートデフォルト", "xpack.uptime.sourceConfiguration.applySettingsButtonLabel": "変更を適用", "xpack.uptime.sourceConfiguration.certificateExpirationThresholdInput.ariaLabel": "TLS証明書の満了日までの最小日数を制御するインプット。この期間を過ぎると、Kibanaで警告が表示されます。", "xpack.uptime.sourceConfiguration.certificateThresholdDescription": "証明書エラーを表示し、アラートを通知するしきい値を変更します。注:すべての構成されたアラートに影響します。", "xpack.uptime.sourceConfiguration.certificationSectionTitle": "証明書の有効期限", + "xpack.uptime.sourceConfiguration.defaultConnectors": "デフォルトコネクター", + "xpack.uptime.sourceConfiguration.defaultConnectors.description": "アラートを送信するために使用されるデフォルトコネクター。", "xpack.uptime.sourceConfiguration.discardSettingsButtonLabel": "キャンセル", "xpack.uptime.sourceConfiguration.errorStateLabel": "有効期限しきい値", "xpack.uptime.sourceConfiguration.expirationThreshold": "有効期限/使用期間しきい値", @@ -18552,12 +21030,38 @@ "xpack.uptime.sourceConfiguration.heartbeatIndicesTitle": "アップタイムインデックス", "xpack.uptime.sourceConfiguration.indicesSectionTitle": "インデックス", "xpack.uptime.sourceConfiguration.warningStateLabel": "使用期間上限", + "xpack.uptime.synthetics.consoleStepList.message": "実行できませんでした。記録されたコンソール出力は次のとおりです。", + "xpack.uptime.synthetics.consoleStepList.title": "ステップが実行されませんでした", + "xpack.uptime.synthetics.emptyJourney.message.checkGroupField": "チェックグループは{codeBlock}です。", + "xpack.uptime.synthetics.emptyJourney.message.footer": "表示する詳細情報はありません。", + "xpack.uptime.synthetics.emptyJourney.message.heading": "ステップが含まれていませんでした。", + "xpack.uptime.synthetics.emptyJourney.title": "ステップがありません。", + "xpack.uptime.synthetics.executedJourney.heading": "概要情報", + "xpack.uptime.synthetics.executedStep.errorHeading": "エラー", + "xpack.uptime.synthetics.executedStep.scriptHeading": "スクリプトのステップ", + "xpack.uptime.synthetics.executedStep.stackTrace": "スタックトレース", + "xpack.uptime.synthetics.executedStep.stepName": "{stepNumber}. {stepName}", + "xpack.uptime.synthetics.experimentalCallout.title": "実験的機能", + "xpack.uptime.synthetics.journey.allFailedMessage": "{total}ステップ - すべて失敗またはスキップされました", + "xpack.uptime.synthetics.journey.allSucceededMessage": "{total}ステップ - すべて成功しました", + "xpack.uptime.synthetics.journey.partialSuccessMessage": "{total}ステップ - {succeeded}成功しました", + "xpack.uptime.synthetics.screenshot.noImageMessage": "画像がありません", + "xpack.uptime.synthetics.screenshotDisplay.altText": "名前「{stepName}」のステップのスクリーンショット", + "xpack.uptime.synthetics.screenshotDisplay.altTextWithoutName": "スクリーンショット", + "xpack.uptime.synthetics.screenshotDisplay.fullScreenshotAltText": "名前「{stepName}」のステップの詳細スクリーンショット", + "xpack.uptime.synthetics.screenshotDisplay.fullScreenshotAltTextWithoutName": "詳細スクリーンショット", + "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltText": "名前「{stepName}」のステップのサムネイルスクリーンショット", + "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltTextWithoutName": "サムネイルスクリーンショット", + "xpack.uptime.synthetics.statusBadge.failedMessage": "失敗", + "xpack.uptime.synthetics.statusBadge.skippedMessage": "スキップ", + "xpack.uptime.synthetics.statusBadge.succeededMessage": "成功", "xpack.uptime.title": "アップタイム", "xpack.uptime.toggleAlertButton.content": "ステータスアラートを監視", "xpack.uptime.toggleAlertFlyout.ariaLabel": "アラートの追加ポップアップを開く", "xpack.uptime.toggleTlsAlertButton.ariaLabel": "TLSアラートの追加ポップアップを開く", "xpack.uptime.toggleTlsAlertButton.content": "TLSアラート", "xpack.uptime.uptimeFeatureCatalogueTitle": "起動時間", + "xpack.urlDrilldown.DisplayName": "URLに移動", "xpack.watcher.app.licenseErrorLinkText": "ライセンスを管理します。", "xpack.watcher.app.licenseErrorTitle": "ライセンスエラー", "xpack.watcher.appName": "Watcher", @@ -18700,6 +21204,7 @@ "xpack.watcher.sections.watchEdit.errorTitle": "ウォッチの読み込み中にエラーが発生しました", "xpack.watcher.sections.watchEdit.json.cancelButtonLabel": "キャンセル", "xpack.watcher.sections.watchEdit.json.createButtonLabel": "ウォッチを作成", + "xpack.watcher.sections.watchEdit.json.createSuccessNotificationText": "「{watchDisplayName}」が作成されました", "xpack.watcher.sections.watchEdit.json.editTabLabel": "編集", "xpack.watcher.sections.watchEdit.json.error.invalidActionType": "アクション\"{action}\"に不明なアクションタイプが指定されています。", "xpack.watcher.sections.watchEdit.json.error.invalidIdText": "ID には文字、数字、アンダースコア、ハイフン、ピリオド、数字のみを使用できます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 97caed949d4e..5f1c72929b2c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -91,6 +91,7 @@ "advancedSettings.categoryNames.timelionLabel": "Timelion", "advancedSettings.categoryNames.visualizationsLabel": "可视化", "advancedSettings.categorySearchLabel": "类别", + "advancedSettings.featureCatalogueTitle": "定制您的 Kibana 体验 — 更改日期格式、打开深色模式,等等。", "advancedSettings.field.changeImageLinkAriaLabel": "更改 {ariaName}", "advancedSettings.field.changeImageLinkText": "更改图片", "advancedSettings.field.codeEditorSyntaxErrorMessage": "JSON 语法无效", @@ -123,6 +124,8 @@ "advancedSettings.pageTitle": "设置", "advancedSettings.searchBar.unableToParseQueryErrorMessage": "无法解析查询", "advancedSettings.searchBarAriaLabel": "搜索高级设置", + "advancedSettings.voiceAnnouncement.ariaLabel": "“高级设置”的结果信息", + "advancedSettings.voiceAnnouncement.noSearchResultScreenReaderMessage": "{sectionLenght, plural, one {# 个部分} other {# 个部分}}中有 {optionLenght, plural, one {# 个选项} other {# 个选项}}", "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "您搜索了“{query}”。{sectionLenght, plural, one {# 个部分} other {# 个部分}}中有 {optionLenght, plural, one {# 个选项} other {# 个选项}}", "apmOss.tutorial.apmAgents.statusCheck.btnLabel": "检查代理状态", "apmOss.tutorial.apmAgents.statusCheck.errorMessage": "尚未从代理收到任何数据", @@ -207,6 +210,7 @@ "apmOss.tutorial.nodeClient.configure.commands.setRequiredServiceNameComment": "覆盖来自 package.json 的服务名", "apmOss.tutorial.nodeClient.configure.commands.useIfApmRequiresTokenComment": "APM Server 需要令牌时使用", "apmOss.tutorial.nodeClient.configure.textPost": "请参阅[文档]({documentationLink})以了解高级用法,包括如何用于 [Babel/ES 模块]({babelEsModulesLink})。", + "apmOss.tutorial.nodeClient.configure.textPre": "代理是在应用程序进程内运行的库。APM 服务是基于 `serviceName` 以编程方式创建的。此代理不仅支持各种框架,而且还可与您的定制堆栈结合使用。", "apmOss.tutorial.nodeClient.configure.title": "配置代理", "apmOss.tutorial.nodeClient.install.textPre": "将 Node.js 的 APM 代理安装为您的应用程序的依赖项。", "apmOss.tutorial.nodeClient.install.title": "安装 APM 代理", @@ -255,8 +259,8 @@ "console.autocomplete.addMethodMetaText": "方法", "console.consoleDisplayName": "控制台", "console.consoleMenu.copyAsCurlMessage": "请求已复制为 cURL", - "console.devToolsDescription": "跳过 cURL 并使用此 JSON 接口直接处理您的数据。", - "console.devToolsTitle": "控制台", + "console.devToolsDescription": "跳过 cURL 并使用 JSON 接口在控制台中处理您的数据。", + "console.devToolsTitle": "与 Elasticsearch API 进行交互", "console.exampleOutputTextarea": "开发工具控制台编辑器示例", "console.helpPage.keyboardCommands.autoIndentDescription": "自动缩进当前请求", "console.helpPage.keyboardCommands.closeAutoCompleteMenuDescription": "关闭自动完成菜单", @@ -451,8 +455,70 @@ "core.fatalErrors.somethingWentWrongTitle": "出问题了", "core.fatalErrors.tryRefreshingPageDescription": "请尝试刷新页面。如果无效,请返回上一页或清除您的会话数据。", "core.notifications.errorToast.closeModal": "关闭", + "core.notifications.globalToast.ariaLabel": "通知消息列表", "core.notifications.unableUpdateUISettingNotificationMessageTitle": "无法更新 UI 设置", + "core.status.greenTitle": "绿色", + "core.status.redTitle": "红色", + "core.status.yellowTitle": "黄色", + "core.statusPage.loadStatus.serverIsDownErrorMessage": "无法请求服务器状态。也许您的服务器已关闭?", + "core.statusPage.loadStatus.serverStatusCodeErrorMessage": "无法请求服务器状态,状态代码为 {responseStatus}", + "core.statusPage.metricsTiles.columns.heapTotalHeader": "堆总数", + "core.statusPage.metricsTiles.columns.heapUsedHeader": "已使用堆数", + "core.statusPage.metricsTiles.columns.loadHeader": "加载", + "core.statusPage.metricsTiles.columns.requestsPerSecHeader": "每秒请求数", + "core.statusPage.metricsTiles.columns.resTimeAvgHeader": "响应时间平均值", + "core.statusPage.metricsTiles.columns.resTimeMaxHeader": "响应时间最大值", + "core.statusPage.serverStatus.statusTitle": "Kibana 状态为 {kibanaStatus}", + "core.statusPage.statusApp.loadingErrorText": "加载状态时发生错误", + "core.statusPage.statusApp.statusActions.buildText": "BUILD {buildNum}", + "core.statusPage.statusApp.statusActions.commitText": "COMMIT {buildSha}", + "core.statusPage.statusApp.statusTitle": "插件状态", + "core.statusPage.statusTable.columns.idHeader": "ID", + "core.statusPage.statusTable.columns.statusHeader": "状态", "core.toasts.errorToast.seeFullError": "请参阅完整的错误信息", + "core.ui_settings.params.darkModeText": "对 Kibana UI 启用深色模式。需要刷新页面,才能应用设置。", + "core.ui_settings.params.darkModeTitle": "深色模式", + "core.ui_settings.params.dateFormat.dayOfWeekText": "一周应该从哪一天开始?", + "core.ui_settings.params.dateFormat.dayOfWeekTitle": "周内日", + "core.ui_settings.params.dateFormat.optionsLinkText": "格式", + "core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "ISO8601 时间间隔", + "core.ui_settings.params.dateFormat.scaledText": "定义在以下场合中采用的格式的值:基于时间的数据按顺序呈现,且经格式化的时间戳应适应度量之间的时间间隔。键是{intervalsLink}。", + "core.ui_settings.params.dateFormat.scaledTitle": "标度日期格式", + "core.ui_settings.params.dateFormat.timezone.invalidValidationMessage": "时区无效:{timezone}", + "core.ui_settings.params.dateFormat.timezoneText": "应使用哪个时区。{defaultOption} 将使用您的浏览器检测到的时区。", + "core.ui_settings.params.dateFormat.timezoneTitle": "用于设置日期格式的时区", + "core.ui_settings.params.dateFormatText": "显示格式正确的日期时,请使用此{formatLink}", + "core.ui_settings.params.dateFormatTitle": "日期格式", + "core.ui_settings.params.dateNanosFormatText": "用于 Elasticsearch 的 {dateNanosLink} 数据类型", + "core.ui_settings.params.dateNanosFormatTitle": "纳秒格式的日期", + "core.ui_settings.params.dateNanosLinkTitle": "date_nanos", + "core.ui_settings.params.dayOfWeekText.invalidValidationMessage": "周内日无效:{dayOfWeek}", + "core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "必须是相对 URL。", + "core.ui_settings.params.defaultRoute.defaultRouteText": "此设置用于指定打开 Kibana 时的默认路由。您可以使用此设置修改打开 Kibana 时的登陆页面。路由必须是相对 URL。", + "core.ui_settings.params.defaultRoute.defaultRouteTitle": "默认路由", + "core.ui_settings.params.disableAnimationsText": "在 Kibana UI 中关闭所有不必要的动画。刷新页面可应用所做的更改。", + "core.ui_settings.params.disableAnimationsTitle": "禁用动画", + "core.ui_settings.params.maxCellHeightText": "表单元格应占用的最大高度。设置为 0 可禁用截断", + "core.ui_settings.params.maxCellHeightTitle": "最大表单元格高度", + "core.ui_settings.params.notifications.banner.markdownLinkText": "Markdown 受支持", + "core.ui_settings.params.notifications.bannerLifetimeText": "在屏幕上显示横幅通知的时间(毫秒)。设置为 {infinityValue} 将禁用倒计时。", + "core.ui_settings.params.notifications.bannerLifetimeTitle": "横幅通知生存时间", + "core.ui_settings.params.notifications.bannerText": "用于向所有用户发送临时通知的定制横幅。{markdownLink}。", + "core.ui_settings.params.notifications.bannerTitle": "定制横幅通知", + "core.ui_settings.params.notifications.errorLifetimeText": "在屏幕上显示错误通知的时间(毫秒)。设置为 {infinityValue} 将禁用此项。", + "core.ui_settings.params.notifications.errorLifetimeTitle": "错误通知生存时间", + "core.ui_settings.params.notifications.infoLifetimeText": "在屏幕上显示信息通知的时间(毫秒)。设置为 {infinityValue} 将禁用此项。", + "core.ui_settings.params.notifications.infoLifetimeTitle": "信息通知生存时间", + "core.ui_settings.params.notifications.warningLifetimeText": "在屏幕上显示警告通知的时间(毫秒)。设置为 {infinityValue} 将禁用此项。", + "core.ui_settings.params.notifications.warningLifetimeTitle": "警告通知生存时间", + "core.ui_settings.params.pageNavigationDesc": "更改导航样式", + "core.ui_settings.params.pageNavigationLegacy": "旧版", + "core.ui_settings.params.pageNavigationModern": "现代", + "core.ui_settings.params.pageNavigationName": "侧边导航样式", + "core.ui_settings.params.storeUrlText": "有时,URL 可能会变得过长,使某些浏览器无法进行处理。为此,我们将正测试在会话存储中存储 URL 的组成部分是否会有所帮助。请向我们反馈您的体验!", + "core.ui_settings.params.storeUrlTitle": "将 URL 存储在会话存储中", + "core.ui_settings.params.themeVersionText": "在用于当前版和下一版 Kibana 的主题之间切换。需要刷新页面,才能应用设置。", + "core.ui_settings.params.themeVersionTitle": "主题版本", "core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "前往主页", "core.ui.chrome.headerGlobalNav.helpMenuAskElasticTitle": "问询 Elastic", "core.ui.chrome.headerGlobalNav.helpMenuButtonAriaLabel": "帮助菜单", @@ -479,6 +545,7 @@ "core.ui.kibanaNavList.label": "Kibana", "core.ui.legacyBrowserMessage": "此 Elastic 安装启用了当前浏览器未满足的严格安全要求。", "core.ui.legacyBrowserTitle": "请升级您的浏览器", + "core.ui.loadingIndicatorAriaLabel": "正在加载内容", "core.ui.managementNavList.label": "管理", "core.ui.observabilityNavList.label": "可观测性", "core.ui.overlays.banner.attentionTitle": "注意", @@ -498,11 +565,8 @@ "core.ui.securityNavList.label": "安全", "core.ui.welcomeErrorMessage": "Elastic 未正确加载。检查服务器输出以了解详情。", "core.ui.welcomeMessage": "正在加载 Elastic", - "core.status.greenTitle": "绿", - "core.status.redTitle": "红", - "core.status.yellowTitle": "黄", "dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化", - "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全屏", + "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "最大化面板", "dashboard.addExistingVisualizationLinkText": "将现有", "dashboard.addNewVisualizationText": "或新对象添加到此仪表板", "dashboard.addPanel.noMatchingObjectsMessage": "未找到任何匹配对象。", @@ -532,6 +596,7 @@ "dashboard.emptyDashboardTitle": "此仪表板是空的。", "dashboard.factory.displayName": "仪表板", "dashboard.featureCatalogue.dashboardDescription": "显示和共享可视化和已保存搜索的集合。", + "dashboard.featureCatalogue.dashboardSubtitle": "在仪表板中分析数据。", "dashboard.featureCatalogue.dashboardTitle": "仪表板", "dashboard.fillDashboardTitle": "此仪表板是空的。让我们来填充它!", "dashboard.helpMenu.appName": "仪表板", @@ -550,13 +615,17 @@ "dashboard.listing.table.entityName": "仪表板", "dashboard.listing.table.entityNamePlural": "仪表板", "dashboard.listing.table.titleColumnName": "标题", + "dashboard.panel.AddToLibrary": "添加到库", "dashboard.panel.clonedToast": "克隆的仪表板", "dashboard.panel.clonePanel": "克隆仪表板", "dashboard.panel.invalidData": "url 中的数据无效", + "dashboard.panel.LibraryNotification": "库", + "dashboard.panel.libraryNotification.toolTip": "此面板链接到库项目。编辑该面板可能会影响其他仪表板。", "dashboard.panel.removePanel.replacePanel": "替换面板", "dashboard.panel.title.clonedTag": "副本", "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段", "dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}", + "dashboard.panel.unlinkFromLibrary": "取消与库项目的链接", "dashboard.placeholder.factory.displayName": "占位符", "dashboard.savedDashboard.newDashboardTitle": "新建仪表板", "dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "时间未随此仪表板保存,因此无法同步。", @@ -673,6 +742,8 @@ "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "时间筛选的默认刷新时间间隔。需要使用毫秒单位指定“值”。", "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "时间筛选刷新时间间隔", "data.advancedSettings.timepicker.thisWeek": "本周", + "data.advancedSettings.timepicker.timeDefaultsText": "要在 Kibana 启动时使用的时间筛选选项(如果未使用时间筛选选项)", + "data.advancedSettings.timepicker.timeDefaultsTitle": "时间筛选默认值", "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 和 {lt} {to}", "data.common.kql.errors.endOfInputText": "输入结束", @@ -797,13 +868,16 @@ "data.functions.esaggs.inspector.dataRequest.title": "数据", "data.functions.indexPatternLoad.help": "加载索引模式", "data.functions.indexPatternLoad.id.help": "要加载的索引模式 id", - "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。", + "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "要在 Kibana 中可视化和浏览数据,必须创建索引模式,以从 Elasticsearch 中检索数据。", "data.indexPatterns.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", + "data.indexPatterns.fetchFieldSaveErrorTitle": "在提取索引模式 {title}(ID:{id})的字段后保存出错", "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", "data.noDataPopover.content": "此时间范围不包含任何数据。增大或调整时间范围,以查看更多的字段并创建图表。", "data.noDataPopover.dismissAction": "不再显示", "data.noDataPopover.subtitle": "提示", "data.noDataPopover.title": "空数据集", + "data.painlessError.buttonTxt": "编辑脚本", + "data.painlessError.painlessScriptedFieldErrorMessage": "执行 Painless 脚本时出错:“{script}”。", "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "无效的日历时间间隔:{interval},值必须为 1", "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "时间间隔格式无效:{interval}", "data.query.queryBar.comboboxAriaLabel": "搜索并筛选 {pageType} 页面", @@ -897,6 +971,7 @@ "data.search.aggs.buckets.histogram.interval.help": "要用于此聚合的时间间隔", "data.search.aggs.buckets.histogram.intervalBase.help": "要用于此聚合的 IntervalBase", "data.search.aggs.buckets.histogram.json.help": "聚合发送至 Elasticsearch 时要包括的高级 json", + "data.search.aggs.buckets.histogram.maxBars.help": "计算得到大约这么多条形所需的时间间隔", "data.search.aggs.buckets.histogram.minDocCount.help": "指定是否要将 min_doc_count 用于此聚合", "data.search.aggs.buckets.histogram.schema.help": "要用于此聚合的方案", "data.search.aggs.buckets.histogramTitle": "Histogram", @@ -927,6 +1002,12 @@ "data.search.aggs.buckets.range.ranges.help": "要用于此聚合的序列化范围。", "data.search.aggs.buckets.range.schema.help": "要用于此聚合的方案", "data.search.aggs.buckets.rangeTitle": "范围", + "data.search.aggs.buckets.shardDelay.customLabel.help": "表示此聚合的定制标签", + "data.search.aggs.buckets.shardDelay.delay.help": "要处理的分片之间的延迟(毫秒)。", + "data.search.aggs.buckets.shardDelay.enabled.help": "指定是否应启用此聚合", + "data.search.aggs.buckets.shardDelay.id.help": "此聚合的 ID", + "data.search.aggs.buckets.shardDelay.json.help": "在该聚合发送到 Elasticsearch 时要包括的高级 json", + "data.search.aggs.buckets.shardDelay.schema.help": "用于此聚合的方案", "data.search.aggs.buckets.significantTerms.customLabel.help": "表示此聚合的定制标签", "data.search.aggs.buckets.significantTerms.enabled.help": "指定是否启用此聚合", "data.search.aggs.buckets.significantTerms.exclude.help": "指定要从结果中排除的存储桶值", @@ -974,6 +1055,7 @@ "data.search.aggs.function.buckets.histogram.help": "为 Histogram 聚合生成序列化聚合配置", "data.search.aggs.function.buckets.ipRange.help": "为 IP 范围聚合生成序列化聚合配置", "data.search.aggs.function.buckets.range.help": "为范围聚合生成序列化聚合配置", + "data.search.aggs.function.buckets.shardDelay.help": "为分片延迟聚合生成序列化聚合配置", "data.search.aggs.function.buckets.significantTerms.help": "为重要词聚合生成序列化聚合配置", "data.search.aggs.function.buckets.terms.help": "为词聚合生成序列化聚合配置", "data.search.aggs.function.metrics.avg.help": "为平均值聚合生成序列化聚合配置", @@ -1201,9 +1283,25 @@ "data.search.aggs.metrics.uniqueCountTitle": "唯一计数", "data.search.aggs.otherBucket.labelForMissingValuesLabel": "缺失值的标签", "data.search.aggs.otherBucket.labelForOtherBucketLabel": "其他存储桶的标签", + "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "已保存的字段“{fieldParameter}”无效,无法用于“{aggType}”聚合。请选择新字段。", "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} 是必需字段", "data.search.aggs.percentageOfLabel": "{label} 的百分比", "data.search.aggs.string.customLabel": "定制标签", + "data.search.dataRequest.title": "数据", + "data.search.es_search.dataRequest.description": "此请求查询 Elasticsearch,以获取可视化的数据。", + "data.search.es_search.hitsDescription": "由查询返回的文档数目。", + "data.search.es_search.hitsLabel": "命中数", + "data.search.es_search.hitsTotalDescription": "与查询匹配的文档数目。", + "data.search.es_search.hitsTotalLabel": "命中数(总数)", + "data.search.es_search.indexPatternDescription": "连接到 Elasticsearch 索引的索引模式。", + "data.search.es_search.indexPatternLabel": "索引模式", + "data.search.es_search.queryTimeDescription": "处理查询所花费的时间。不包括发送请求或在浏览器中解析它的时间。", + "data.search.es_search.queryTimeLabel": "查询时间", + "data.search.es_search.queryTimeValue": "{queryTime}ms", + "data.search.esdsl.help": "运行 Elasticsearch 请求", + "data.search.esdsl.index.help": "要查询的 ElasticSearch 索引", + "data.search.esdsl.q.help": "查询 DSL", + "data.search.esdsl.size.help": "ElasticSearch searchAPI 大小参数", "data.search.searchBar.savedQueryDescriptionLabelText": "描述", "data.search.searchBar.savedQueryDescriptionText": "保存想要再次使用的查询文本和筛选。", "data.search.searchBar.savedQueryForm.titleConflictText": "标题与现有已保存查询有冲突", @@ -1262,7 +1360,12 @@ "data.search.searchSource.requestTimeDescription": "请求从浏览器到 Elasticsearch 以及返回的时间。不包括请求在队列中等候的时间。", "data.search.searchSource.requestTimeLabel": "请求时间", "data.search.searchSource.requestTimeValue": "{requestTime}ms", + "data.search.timeoutContactAdmin": "您的查询已超时。请联系您的系统管理员以延长运行时间。", + "data.search.timeoutIncreaseSetting": "您的查询已超时。使用搜索超时高级设置延长运行时间。", + "data.search.timeoutIncreaseSettingActionText": "编辑设置", "data.search.unableToGetSavedQueryToastTitle": "无法加载已保存查询 {savedQueryId}", + "data.search.upgradeLicense": "您的查询已超时。使用我们免费的基础级许可,您的查询永不会超时。", + "data.search.upgradeLicenseActionText": "立即升级", "devTools.badge.readOnly.text": "只读", "devTools.badge.readOnly.tooltip": "无法保存", "devTools.devToolsTitle": "开发工具", @@ -1278,6 +1381,8 @@ "discover.advancedSettings.context.tieBreakerFieldsTitle": "平分决胜字段", "discover.advancedSettings.defaultColumnsText": "“发现”选项卡中默认显示的列", "discover.advancedSettings.defaultColumnsTitle": "默认列", + "discover.advancedSettings.discover.modifyColumnsOnSwitchText": "移除新索引模式中未提供的列。", + "discover.advancedSettings.discover.modifyColumnsOnSwitchTitle": "更改索引模式时修改列", "discover.advancedSettings.docTableHideTimeColumnText": "在 Discover 中和仪表板上的所有已保存搜索中隐藏“时间”列。", "discover.advancedSettings.docTableHideTimeColumnTitle": "隐藏“时间”列", "discover.advancedSettings.fieldsPopularLimitText": "要显示的排名前 N 最常见字段", @@ -1314,6 +1419,7 @@ "discover.context.unableToLoadDocumentDescription": "无法加载文档", "discover.discoverBreadcrumbTitle": "Discover", "discover.discoverDescription": "通过查询和筛选原始文档来以交互方式浏览您的数据。", + "discover.discoverSubtitle": "搜索和查找洞见。", "discover.discoverTitle": "Discover", "discover.doc.couldNotFindDocumentsDescription": "无文档匹配该 ID。", "discover.doc.failedToExecuteQueryDescription": "无法执行搜索", @@ -1368,12 +1474,16 @@ "discover.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "discover.embeddable.search.displayName": "搜索", "discover.fieldChooser.detailViews.emptyStringText": "空字符串", + "discover.fieldChooser.detailViews.existsText": "存在于", "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}:“{value}”", "discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留 {field}:“{value}”", "discover.fieldChooser.detailViews.recordsText": "个记录", "discover.fieldChooser.detailViews.visualizeLinkText": "可视化", "discover.fieldChooser.discoverField.addButtonAriaLabel": "将 {field} 添加到表中", + "discover.fieldChooser.discoverField.addFieldTooltip": "将字段添加为列", + "discover.fieldChooser.discoverField.fieldTopValuesLabel": "排名前 5 值", "discover.fieldChooser.discoverField.removeButtonAriaLabel": "从表中移除 {field}", + "discover.fieldChooser.discoverField.removeFieldTooltip": "从表中移除字段", "discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription": "脚本字段执行时间会很长。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "分析不适用于地理字段。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "分析不适用于对象字段。", @@ -1412,7 +1522,7 @@ "discover.histogramOfFoundDocumentsAriaLabel": "已找到文档的直方图", "discover.hitsPluralTitle": "{hits, plural, one {次命中} other {次命中}}", "discover.howToChangeTheTimeTooltip": "要更改时间,请使用上面的全局时间筛选", - "discover.howToSeeOtherMatchingDocumentsDescription": "以下是匹配您的搜索的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。 ", + "discover.howToSeeOtherMatchingDocumentsDescription": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。", "discover.inspectorRequestDataTitle": "数据", "discover.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "discover.localMenu.inspectTitle": "检查", @@ -1482,6 +1592,7 @@ "embeddableApi.panel.labelError": "错误", "embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabel": "面板选项", "embeddableApi.panel.optionsMenu.panelOptionsButtonEnhancedAriaLabel": "{title} 的面板选项", + "embeddableApi.panel.placeholderTitle": "[无标题]", "embeddableApi.panel.removePanel.displayName": "从仪表板删除", "embeddableApi.samples.contactCard.displayName": "联系卡片", "embeddableApi.samples.filterableContainer.displayName": "可筛选仪表板", @@ -1562,12 +1673,17 @@ "expressions.functions.kibana_context.savedSearchId.help": "指定要用于查询和筛选的已保存搜索 ID", "expressions.functions.kibana_context.timeRange.help": "指定 Kibana 时间范围筛选", "expressions.functions.kibana.help": "获取 kibana 全局上下文", - "expressions.functions.var.help": "更新 kibana 全局上下文", - "expressions.functions.var.name.help": "指定变量的名称", - "expressions.functions.varset.help": "更新 kibana 全局上下文", - "expressions.functions.varset.name.help": "指定变量的名称", - "expressions.functions.varset.val.help": "为变量指定值。如果未提供,将使用输入上下文", + "expressions.functions.theme.args.defaultHelpText": "主题信息不可用时的默认值。", + "expressions.functions.theme.args.variableHelpText": "要读取的主题变量的名称。", + "expressions.functions.themeHelpText": "读取主题设置。", + "expressions.functions.var.help": "更新 Kibana 全局上下文。", + "expressions.functions.var.name.help": "指定变量的名称。", + "expressions.functions.varset.help": "更新 Kibana 全局上下文。", + "expressions.functions.varset.name.help": "指定变量的名称。", + "expressions.functions.varset.val.help": "指定变量的值。如果未指定,则使用输入上下文。", "expressions.types.number.fromStringConversionErrorMessage": "无法将“{string}”字符串的类型转换为数字", + "home.addData.sampleDataButtonLabel": "试用我们的样例数据", + "home.addData.sectionTitle": "采集您的数据", "home.breadcrumbs.addDataTitle": "添加数据", "home.breadcrumbs.homeTitle": "主页", "home.dataManagementDisableCollection": " 要停止收集, ", @@ -1583,10 +1699,12 @@ "home.directory.tabs.otherTitle": "其他", "home.exploreButtonLabel": "自己浏览", "home.exploreYourDataDescription": "完成所有步骤后,您便可以随时浏览自己的数据。", - "home.letsStartDescription": "我们注意到在您的集群中没有任何数据。您可以试用我们的样例数据和仪表板,也可以直接使用自己的数据。", - "home.letsStartTitle": "开始使用", + "home.header.title": "主页", + "home.letsStartDescription": "从任何源将数据添加到您的集群,然后对其进行实时分析和可视化。使用我们的解决方案可随处添加搜索,观察您的生态系统,并防范安全威胁。", + "home.letsStartTitle": "首先添加您的数据", "home.loadTutorials.requestFailedErrorMessage": "请求失败,状态代码:{status}", "home.loadTutorials.unableToLoadErrorMessage": "无法加载教程", + "home.manageData.sectionTitle": "管理您的数据", "home.pageTitle": "主页", "home.recentlyAccessed.recentlyViewedTitle": "最近查看", "home.sampleData.ecommerceSpec.averageSalesPriceTitle": "[电子商务] 平均销售价格", @@ -1599,6 +1717,7 @@ "home.sampleData.ecommerceSpec.revenueDashboardTitle": "[电子商务] 收入仪表板", "home.sampleData.ecommerceSpec.salesByCategoryTitle": "[电子商务] 按类别划分的销售额", "home.sampleData.ecommerceSpec.salesByGenderTitle": "[电子商务] 按性别划分的销售额", + "home.sampleData.ecommerceSpec.salesCountMapTitle": "[电子商务] 销售计数地图", "home.sampleData.ecommerceSpec.soldProductsPerDayTitle": "[电子商务] 每天已售产品", "home.sampleData.ecommerceSpec.topSellingProductsTitle": "[电子商务] 热卖产品", "home.sampleData.ecommerceSpec.totalRevenueTitle": "[电子商务] 总收入", @@ -1611,6 +1730,7 @@ "home.sampleData.flightsSpec.delayBucketsTitle": "[航班] 延误存储桶", "home.sampleData.flightsSpec.delaysAndCancellationsTitle": "[航班] 延误与取消", "home.sampleData.flightsSpec.delayTypeTitle": "[航班] 延误类型", + "home.sampleData.flightsSpec.departuresCountMapTitle": "[航班] 离港计数地图", "home.sampleData.flightsSpec.destinationWeatherTitle": "[航班] 到达地天气", "home.sampleData.flightsSpec.flightCancellationsTitle": "[航班] 航班取消", "home.sampleData.flightsSpec.flightCountAndAverageTicketPriceTitle": "[航班] 航班计数和平均票价", @@ -1635,6 +1755,7 @@ "home.sampleData.logsSpec.sourceAndDestinationSankeyChartTitle": "[日志] 始发地和到达地 Sankey 图", "home.sampleData.logsSpec.uniqueVisitorsTitle": "[日志] 独立访客与平均字节数", "home.sampleData.logsSpec.visitorOSTitle": "[日志] 按 OS 划分的访客", + "home.sampleData.logsSpec.visitorsMapTitle": "[日志] 访客地图", "home.sampleData.logsSpec.webTrafficDescription": "分析 Elastic 网站的模拟 Web 流量日志数据", "home.sampleData.logsSpec.webTrafficTitle": "[日志] 网络流量", "home.sampleData.logsSpecDescription": "用于监测 Web 日志的样例数据、可视化和仪表板。", @@ -1658,7 +1779,8 @@ "home.sampleDataSetCard.removingButtonLabel": "正在移除", "home.sampleDataSetCard.viewDataButtonAriaLabel": "查看 {datasetName}", "home.sampleDataSetCard.viewDataButtonLabel": "查看数据", - "home.tryButtonLabel": "试用我们的样例数据", + "home.solutionsSection.sectionTitle": "选取您的解决方案", + "home.tryButtonLabel": "添加数据", "home.tutorial.addDataToKibanaTitle": "添加数据", "home.tutorial.card.sampleDataDescription": "开始使用这些“一键式”数据集浏览 Kibana。", "home.tutorial.card.sampleDataTitle": "样例数据", @@ -1690,6 +1812,8 @@ "home.tutorial.tabs.securitySolutionTitle": "安全", "home.tutorial.unexpectedStatusCheckStateErrorDescription": "意外的状态检查状态 {statusCheckState}", "home.tutorial.unhandledInstructionTypeErrorDescription": "未处理的指令类型 {visibleInstructions}", + "home.tutorialDirectory.featureCatalogueDescription": "从热门应用和服务中采集数据。", + "home.tutorialDirectory.featureCatalogueTitle": "添加数据", "home.tutorials.activemqLogs.artifacts.dashboards.linkLabel": "ActiveMQ 应用程序事件", "home.tutorials.activemqLogs.longDescription": "使用 Filebeat 收集 ActiveMQ 日志。[了解详情]({learnMoreLink})。", "home.tutorials.activemqLogs.nameTitle": "ActiveMQ 日志", @@ -1769,15 +1893,15 @@ "home.tutorials.common.auditbeatInstructions.config.windowsTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.auditbeatInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.auditbeatInstructions.install.debTextPost": "寻找 32 位软件包?请参阅[下载页面]({linkUrl})。", - "home.tutorials.common.auditbeatInstructions.install.debTextPre": "首次使用 Auditbeat?请参阅[入门指南]({linkUrl})。", + "home.tutorials.common.auditbeatInstructions.install.debTextPre": "首次使用 Auditbeat?查看[快速入门]({linkUrl})。", "home.tutorials.common.auditbeatInstructions.install.debTitle": "下载并安装 Auditbeat", - "home.tutorials.common.auditbeatInstructions.install.osxTextPre": "首次使用 Auditbeat?请参阅[入门指南]({linkUrl})。", + "home.tutorials.common.auditbeatInstructions.install.osxTextPre": "首次使用 Auditbeat?查看[快速入门]({linkUrl})。", "home.tutorials.common.auditbeatInstructions.install.osxTitle": "下载并安装 Auditbeat", "home.tutorials.common.auditbeatInstructions.install.rpmTextPost": "寻找 32 位软件包?请参阅[下载页面]({linkUrl})。", - "home.tutorials.common.auditbeatInstructions.install.rpmTextPre": "首次使用 Auditbeat?请参阅[入门指南]({linkUrl})。", + "home.tutorials.common.auditbeatInstructions.install.rpmTextPre": "首次使用 Auditbeat?查看[快速入门]({linkUrl})。", "home.tutorials.common.auditbeatInstructions.install.rpmTitle": "下载并安装 Auditbeat", "home.tutorials.common.auditbeatInstructions.install.windowsTextPost": "在 {auditbeatPath} 文件中修改 {propertyName} 下的设置以指向您的 Elasticsearch 安装。", - "home.tutorials.common.auditbeatInstructions.install.windowsTextPre": "首次使用 Auditbeat?请参阅[入门指南]({guideLinkUrl})。\n 1.从[下载]({auditbeatLinkUrl})页面下载 Auditbeat Windows zip 文件。\n 2.将 zip 文件的内容解压缩到 {folderPath}。\n 3.将 `{directoryName}` 目录重命名为 `Auditbeat`。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果您正在运行 Windows XP,您可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符处,运行以下命令以将 Auditbeat 安装为 Windows 服务。", + "home.tutorials.common.auditbeatInstructions.install.windowsTextPre": "首次使用 Auditbeat?查看[快速入门]({guideLinkUrl})。\n 1.从[下载]({auditbeatLinkUrl})页面下载 Auditbeat Windows zip 文件。\n 2.将该 zip 文件的内容解压缩到 {folderPath}。\n 3.将 `{directoryName}` 目录重命名为“Auditbeat”。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果运行的是 Windows XP,则可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符处,运行以下命令以将 Auditbeat 安装为 Windows 服务。", "home.tutorials.common.auditbeatInstructions.install.windowsTitle": "下载并安装 Auditbeat", "home.tutorials.common.auditbeatInstructions.start.debTextPre": "`setup` 命令加载 Kibana 仪表板。如果仪表板已设置,请省略此命令。", "home.tutorials.common.auditbeatInstructions.start.debTitle": "启动 Auditbeat", @@ -1827,15 +1951,15 @@ "home.tutorials.common.filebeatInstructions.config.windowsTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.filebeatInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.filebeatInstructions.install.debTextPost": "寻找 32 位软件包?请参阅[下载页面]({linkUrl})。", - "home.tutorials.common.filebeatInstructions.install.debTextPre": "首次使用 Filebeat?请参阅[入门指南]({linkUrl})。", + "home.tutorials.common.filebeatInstructions.install.debTextPre": "首次使用 Filebeat?查看[快速入门]({linkUrl})。", "home.tutorials.common.filebeatInstructions.install.debTitle": "下载并安装 Filebeat", - "home.tutorials.common.filebeatInstructions.install.osxTextPre": "首次使用 Filebeat?请参阅[入门指南]({linkUrl})。", + "home.tutorials.common.filebeatInstructions.install.osxTextPre": "首次使用 Filebeat?查看[快速入门]({linkUrl})。", "home.tutorials.common.filebeatInstructions.install.osxTitle": "下载并安装 Filebeat", "home.tutorials.common.filebeatInstructions.install.rpmTextPost": "寻找 32 位软件包?请参阅[下载页面]({linkUrl})。", - "home.tutorials.common.filebeatInstructions.install.rpmTextPre": "首次使用 Filebeat?请参阅[入门指南]({linkUrl})。", + "home.tutorials.common.filebeatInstructions.install.rpmTextPre": "首次使用 Filebeat?查看[快速入门]({linkUrl})。", "home.tutorials.common.filebeatInstructions.install.rpmTitle": "下载并安装 Filebeat", "home.tutorials.common.filebeatInstructions.install.windowsTextPost": "在 {filebeatPath} 文件中修改 {propertyName} 下的设置以指向您的 Elasticsearch 安装。", - "home.tutorials.common.filebeatInstructions.install.windowsTextPre": "首次使用 Filebeat?请参阅[入门指南]({guideLinkUrl})。\n 1.从[下载]({filebeatLinkUrl})页面下载 Filebeat Windows zip 文件。\n 2.将 zip 文件的内容解压缩到 {folderPath}。\n 3.将 `{directoryName}` 目录重命名为 `Filebeat`。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果您正在运行 Windows XP,您可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符处,运行以下命令以将 Filebeat 安装为 Windows 服务。", + "home.tutorials.common.filebeatInstructions.install.windowsTextPre": "首次使用 Filebeat?查看[快速入门]({guideLinkUrl})。\n 1.从[下载]({filebeatLinkUrl})页面下载 Filebeat Windows zip 文件。\n 2.将该 zip 文件的内容解压缩到 {folderPath}。\n 3.将 `{directoryName}` 目录重命名为“Filebeat”。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果运行的是 Windows XP,则可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符处,运行以下命令以将 Filebeat 安装为 Windows 服务。", "home.tutorials.common.filebeatInstructions.install.windowsTitle": "下载并安装 Filebeat", "home.tutorials.common.filebeatInstructions.start.debTextPre": "`setup` 命令加载 Kibana 仪表板。如果仪表板已设置,请省略此命令。", "home.tutorials.common.filebeatInstructions.start.debTitle": "启动 Filebeat", @@ -1874,11 +1998,11 @@ "home.tutorials.common.functionbeatInstructions.deploy.osxTitle": "将 Functionbeat 部署到 AWS Lambda", "home.tutorials.common.functionbeatInstructions.deploy.windowsTextPre": "这会将 Functionbeat 安装为 Lambda 函数。`setup` 命令检查 Elasticsearch 配置并加载 Kibana 索引模式。通常可省略此命令。", "home.tutorials.common.functionbeatInstructions.deploy.windowsTitle": "将 Functionbeat 部署到 AWS Lambda", - "home.tutorials.common.functionbeatInstructions.install.linuxTextPre": "首次使用 Functionbeat?请参阅[入门指南]({link})。", + "home.tutorials.common.functionbeatInstructions.install.linuxTextPre": "首次使用 Functionbeat?查看[快速入门]({link})。", "home.tutorials.common.functionbeatInstructions.install.linuxTitle": "下载并安装 Functionbeat", - "home.tutorials.common.functionbeatInstructions.install.osxTextPre": "首次使用 Functionbeat?请参阅[入门指南]({link})。", + "home.tutorials.common.functionbeatInstructions.install.osxTextPre": "首次使用 Functionbeat?查看[快速入门]({link})。", "home.tutorials.common.functionbeatInstructions.install.osxTitle": "下载并安装 Functionbeat", - "home.tutorials.common.functionbeatInstructions.install.windowsTextPre": "首次使用 Functionbeat?请参阅[入门指南]({functionbeatLink})。\n 1.从[下载]({elasticLink})页面下载 Functionbeat Windows zip 文件。\n 2.将 zip 文件的内容解压缩到 {folderPath}。\n 3.将 {directoryName} 目录重命名为 `Functionbeat`。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果您正在运行 Windows XP,您可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符下,前往 Functionbeat 目录:", + "home.tutorials.common.functionbeatInstructions.install.windowsTextPre": "首次使用 Functionbeat?查看[快速入门]({functionbeatLink})。\n 1.从[下载]({elasticLink})页面下载 Functionbeat Windows zip 文件。\n 2.将该 zip 文件的内容解压缩到 {folderPath}。\n 3.将 {directoryName} 目录重命名为“Functionbeat”。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果运行的是 Windows XP,则可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符处,前往 Functionbeat 目录:", "home.tutorials.common.functionbeatInstructions.install.windowsTitle": "下载并安装 Functionbeat", "home.tutorials.common.functionbeatStatusCheck.buttonLabel": "检查数据", "home.tutorials.common.functionbeatStatusCheck.errorText": "尚未从 Functionbeat 收到任何数据", @@ -1921,13 +2045,13 @@ "home.tutorials.common.heartbeatInstructions.config.windowsTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.heartbeatInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.heartbeatInstructions.install.debTextPost": "寻找 32 位软件包?请参阅[下载页面]({link})。", - "home.tutorials.common.heartbeatInstructions.install.debTextPre": "首次使用 Heartbeat?请参阅[入门指南]({link})。", + "home.tutorials.common.heartbeatInstructions.install.debTextPre": "首次使用 Heartbeat?查看[快速入门]({link})。", "home.tutorials.common.heartbeatInstructions.install.debTitle": "下载并安装 Heartbeat", - "home.tutorials.common.heartbeatInstructions.install.osxTextPre": "首次使用 Heartbeat?请参阅[入门指南]({link})。", + "home.tutorials.common.heartbeatInstructions.install.osxTextPre": "首次使用 Heartbeat?查看[快速入门]({link})。", "home.tutorials.common.heartbeatInstructions.install.osxTitle": "下载并安装 Heartbeat", - "home.tutorials.common.heartbeatInstructions.install.rpmTextPre": "首次使用 Heartbeat?请参阅[入门指南]({link})。", + "home.tutorials.common.heartbeatInstructions.install.rpmTextPre": "首次使用 Heartbeat?查看[快速入门]({link})。", "home.tutorials.common.heartbeatInstructions.install.rpmTitle": "下载并安装 Heartbeat", - "home.tutorials.common.heartbeatInstructions.install.windowsTextPre": "首次使用 Heartbeat?请参阅[入门指南]({heartbeatLink})。\n 1.从[下载]({elasticLink})页面下载 Heartbeat Windows zip 文件。\n 2.将 zip 文件的内容解压缩到 {folderPath}。\n 3.将 {directoryName} 目录重命名为 `Heartbeat`。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果您正在运行 Windows XP,您可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符处,运行以下命令以将 Heartbeat 安装为 Windows 服务。", + "home.tutorials.common.heartbeatInstructions.install.windowsTextPre": "首次使用 Heartbeat?查看[快速入门]({heartbeatLink})。\n 1.从[下载]({elasticLink})页面下载 Heartbeat Windows zip 文件。\n 2.将该 zip 文件的内容解压缩到 {folderPath}。\n 3.将 {directoryName} 目录重命名为 `Heartbeat`。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果运行的是 Windows XP,则可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符处,运行以下命令以将 Heartbeat 安装为 Windows 服务。", "home.tutorials.common.heartbeatInstructions.install.windowsTitle": "下载并安装 Heartbeat", "home.tutorials.common.heartbeatInstructions.start.debTextPre": "`setup` 命令加载 Kibana 索引模式。", "home.tutorials.common.heartbeatInstructions.start.debTitle": "启动 Heartbeat", @@ -1984,14 +2108,14 @@ "home.tutorials.common.metricbeatInstructions.config.windowsTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.metricbeatInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.metricbeatInstructions.install.debTextPost": "寻找 32 位软件包?请参阅[下载页面]({link})。", - "home.tutorials.common.metricbeatInstructions.install.debTextPre": "首次使用 Metricbeat?请参阅[入门指南]({link})。", + "home.tutorials.common.metricbeatInstructions.install.debTextPre": "首次使用 Metricbeat?查看[快速入门]({link})。", "home.tutorials.common.metricbeatInstructions.install.debTitle": "下载并安装 Metricbeat", - "home.tutorials.common.metricbeatInstructions.install.osxTextPre": "首次使用 Metricbeat?请参阅[入门指南]({link})。", + "home.tutorials.common.metricbeatInstructions.install.osxTextPre": "首次使用 Metricbeat?查看[快速入门]({link})。", "home.tutorials.common.metricbeatInstructions.install.osxTitle": "下载并安装 Metricbeat", - "home.tutorials.common.metricbeatInstructions.install.rpmTextPre": "首次使用 Metricbeat?请参阅[入门指南]({link})。", + "home.tutorials.common.metricbeatInstructions.install.rpmTextPre": "首次使用 Metricbeat?查看[快速入门]({link})。", "home.tutorials.common.metricbeatInstructions.install.rpmTitle": "下载并安装 Metricbeat", "home.tutorials.common.metricbeatInstructions.install.windowsTextPost": "在 {path} 文件中修改 `output.elasticsearch` 下的设置以指向您的 Elasticsearch 安装。", - "home.tutorials.common.metricbeatInstructions.install.windowsTextPre": "首次使用 Metricbeat?请参阅[入门指南]({metricbeatLink})。\n 1.从[下载]({elasticLink})页面下载 Metricbeat Windows zip 文件。\n 2.将 zip 文件的内容解压缩到 {folderPath}。\n 3.将 {directoryName} 目录重命名为 `Metricbeat`。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果您正在运行 Windows XP,您可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符处,运行以下命令以将 Metricbeat 安装为 Windows 服务。", + "home.tutorials.common.metricbeatInstructions.install.windowsTextPre": "首次使用 Metricbeat?查看[快速入门]({metricbeatLink})。\n 1.从[下载]({elasticLink})页面下载 Metricbeat Windows zip 文件。\n 2.将该 zip 文件的内容解压缩到 {folderPath}。\n 3.将 {directoryName} 目录重命名为 `Metricbeat`。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果运行的是 Windows XP,则可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符处,运行以下命令以将 Metricbeat 安装为 Windows 服务。", "home.tutorials.common.metricbeatInstructions.install.windowsTitle": "下载并安装 Metricbeat", "home.tutorials.common.metricbeatInstructions.start.debTextPre": "`setup` 命令加载 Kibana 仪表板。如果仪表板已设置,请省略此命令。", "home.tutorials.common.metricbeatInstructions.start.debTitle": "启动 Metricbeat", @@ -2019,7 +2143,7 @@ "home.tutorials.common.winlogbeatInstructions.config.windowsTextPre": "修改 {path} 以设置连接信息:", "home.tutorials.common.winlogbeatInstructions.config.windowsTitle": "编辑配置", "home.tutorials.common.winlogbeatInstructions.install.windowsTextPost": "在 {path} 文件中修改 `output.elasticsearch` 下的设置以指向您的 Elasticsearch 安装。", - "home.tutorials.common.winlogbeatInstructions.install.windowsTextPre": "首次使用 Winlogbeat?请参阅[入门指南]({winlogbeatLink})。\n 1.从[下载]({elasticLink})页面下载 Winlogbeat Windows zip 文件。\n 2.将 zip 文件的内容解压缩到 {folderPath}。\n 3.将 {directoryName} 目录重命名为 `Winlogbeat`。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果您正在运行 Windows XP,您可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符处,运行以下命令以将 Winlogbeat 安装为 Windows 服务。", + "home.tutorials.common.winlogbeatInstructions.install.windowsTextPre": "首次使用 Winlogbeat?查看[快速入门]({winlogbeatLink})。\n 1.从[下载]({elasticLink})页面下载 Winlogbeat Windows zip 文件。\n 2.将该 zip 文件的内容解压缩到 {folderPath}。\n 3.将 {directoryName} 目录重命名为 `Winlogbeat`。\n 4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果运行的是 Windows XP,则可能需要下载并安装 PowerShell。\n 5.从 PowerShell 提示符处,运行以下命令以将 Winlogbeat 安装为 Windows 服务。", "home.tutorials.common.winlogbeatInstructions.install.windowsTitle": "下载并安装 Winlogbeat", "home.tutorials.common.winlogbeatInstructions.start.windowsTextPre": "`setup` 命令加载 Kibana 仪表板。如果仪表板已设置,请省略此命令。", "home.tutorials.common.winlogbeatInstructions.start.windowsTitle": "启动 Winlogbeat", @@ -2285,7 +2409,21 @@ "indexPatternManagement.createIndexPattern.betaLabel": "公测版", "indexPatternManagement.createIndexPattern.description": "索引模式可以匹配单个源,例如 {single} 或 {multiple} 个数据源、{star}。", "indexPatternManagement.createIndexPattern.documentation": "阅读文档", + "indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription": "此功能需要基础级许可证。", + "indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel": "基础级", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "检查新数据", + "indexPatternManagement.createIndexPattern.emptyState.createAnyway": "部分索引可能已隐藏。仍然尝试{link}。", + "indexPatternManagement.createIndexPattern.emptyState.createAnywayLink": "创建索引模式", + "indexPatternManagement.createIndexPattern.emptyState.haveData": "假设您已有数据?", + "indexPatternManagement.createIndexPattern.emptyState.integrationCardDescription": "从各种源添加数据。", + "indexPatternManagement.createIndexPattern.emptyState.integrationCardTitle": "添加集成", + "indexPatternManagement.createIndexPattern.emptyState.learnMore": "希望了解详情?", + "indexPatternManagement.createIndexPattern.emptyState.noDataTitle": "准备试用 Kibana?首先,您需要数据。", + "indexPatternManagement.createIndexPattern.emptyState.readDocs": "阅读文档", + "indexPatternManagement.createIndexPattern.emptyState.sampleDataCardDescription": "加载数据集和 Kibana 仪表板。", + "indexPatternManagement.createIndexPattern.emptyState.sampleDataCardTitle": "添加样例数据", + "indexPatternManagement.createIndexPattern.emptyState.uploadCardDescription": "导入 CSV、NDJSON 或日志文件。", + "indexPatternManagement.createIndexPattern.emptyState.uploadCardTitle": "上传文件", "indexPatternManagement.createIndexPattern.includeSystemIndicesToggleSwitchLabel": "包括系统和隐藏索引", "indexPatternManagement.createIndexPattern.loadClustersFailMsg": "无法加载远程集群", "indexPatternManagement.createIndexPattern.loadIndicesFailMsg": "无法加载索引", @@ -2317,15 +2455,16 @@ "indexPatternManagement.createIndexPattern.stepTime.fieldLabel": "时间字段", "indexPatternManagement.createIndexPattern.stepTime.noTimeFieldOptionLabel": "我不想使用时间筛选", "indexPatternManagement.createIndexPattern.stepTime.noTimeFieldsLabel": "匹配此索引模式的索引不包含任何时间字段。", - "indexPatternManagement.createIndexPattern.stepTime.options.hideButton": "隐藏高级选项", + "indexPatternManagement.createIndexPattern.stepTime.options.hideButton": "隐藏高级设置", "indexPatternManagement.createIndexPattern.stepTime.options.patternHeader": "定制索引模式 ID", "indexPatternManagement.createIndexPattern.stepTime.options.patternLabel": "Kibana 将为每个索引模式提供唯一的标识符。如果不想使用此唯一 ID,请输入定制 ID。", "indexPatternManagement.createIndexPattern.stepTime.options.patternPlaceholder": "custom-index-pattern-id", - "indexPatternManagement.createIndexPattern.stepTime.options.showButton": "显示高级选项", + "indexPatternManagement.createIndexPattern.stepTime.options.showButton": "显示高级设置", "indexPatternManagement.createIndexPattern.stepTime.patterAlreadyExists": "自定义索引模式 ID 已存在。", "indexPatternManagement.createIndexPattern.stepTime.refreshButton": "刷新", "indexPatternManagement.createIndexPattern.stepTime.timeDescription": "选择用于全局时间筛选的主要时间字段。", "indexPatternManagement.createIndexPattern.stepTimeHeader": "第 2 步(共 2 步):配置设置", + "indexPatternManagement.createIndexPattern.stepTimeLabel": "为您的 {indexPattern} {indexPatternName} 指定设置。", "indexPatternManagement.createIndexPatternHeader": "创建 {indexPatternName}", "indexPatternManagement.dataStreamLabel": "数据流", "indexPatternManagement.date.documentationLabel": "文档", @@ -2344,6 +2483,7 @@ "indexPatternManagement.duration.decimalPlacesLabel": "小数位数", "indexPatternManagement.duration.inputFormatLabel": "输入格式", "indexPatternManagement.duration.outputFormatLabel": "输出格式", + "indexPatternManagement.duration.showSuffixLabel": "显示后缀", "indexPatternManagement.durationErrorMessage": "小数位数必须介于 0 和 20 之间", "indexPatternManagement.editHeader": "编辑 {fieldName}", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", @@ -2431,10 +2571,15 @@ "indexPatternManagement.editIndexPattern.tabs.fieldsHeader": "字段", "indexPatternManagement.editIndexPattern.tabs.scriptedHeader": "脚本字段", "indexPatternManagement.editIndexPattern.tabs.sourceHeader": "源筛选", - "indexPatternManagement.editIndexPattern.timeFilterHeader": "时间筛选字段名称:“{timeFieldName}”", + "indexPatternManagement.editIndexPattern.timeFilterHeader": "时间字段:“{timeFieldName}”", "indexPatternManagement.editIndexPattern.timeFilterLabel.mappingAPILink": "映射 API", "indexPatternManagement.editIndexPattern.timeFilterLabel.timeFilterDetail": "此页根据 Elasticsearch 的记录列出“{indexPatternTitle}”索引中的每个字段以及字段的关联核心类型。要更改字段类型,请使用 Elasticsearch", "indexPatternManagement.editIndexPatternLiveRegionAriaLabel": "索引模式", + "indexPatternManagement.emptyIndexPatternPrompt.documentation": "阅读文档", + "indexPatternManagement.emptyIndexPatternPrompt.indexPatternExplanation": "Kibana 需要索引模式,以识别您要浏览的索引。索引模式可以指向特定索引(例如昨天的日志数据),或包含日志数据的所有索引。", + "indexPatternManagement.emptyIndexPatternPrompt.learnMore": "希望了解详情?", + "indexPatternManagement.emptyIndexPatternPrompt.nowCreate": "现在,创建索引模式。", + "indexPatternManagement.emptyIndexPatternPrompt.youHaveData": "您在 Elasticsearch 中有数据。", "indexPatternManagement.fieldTypeConflict": "字段类型冲突", "indexPatternManagement.formatHeader": "格式", "indexPatternManagement.formatLabel": "设置格式允许您控制特定值的显示方式。其还会导致值完全更改,并阻止 Discover 中的突出显示起作用。", @@ -2451,6 +2596,7 @@ "indexPatternManagement.indexPatterns.createFieldBreadcrumb": "创建字段", "indexPatternManagement.indexPatterns.listBreadcrumb": "索引模式", "indexPatternManagement.indexPatternTable.createBtn": "创建索引模式", + "indexPatternManagement.indexPatternTable.indexPatternExplanation": "创建和管理帮助您从 Elasticsearch 中检索数据的索引模式。", "indexPatternManagement.indexPatternTable.title": "索引模式", "indexPatternManagement.labelTemplate.example.idLabel": "用户 #{value}", "indexPatternManagement.labelTemplate.example.output.idLabel": "用户", @@ -2646,6 +2792,8 @@ "kibana_utils.history.savedObjectIsMissingNotificationMessage": "已保存对象缺失", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "无法完全还原 URL,请确保使用共享功能。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,另外,似乎没有任何可安全删除的项目。\n\n通常,这可以通过移到全新的选项卡来解决,但这种情况可能是由更大的问题造成。如果您定期看到这个消息,请在 {gitHubIssuesUrl} 报告问题。", + "kibana_utils.stateManagement.url.restoreUrlErrorTitle": "从 URL 还原状态时出错", + "kibana_utils.stateManagement.url.saveStateInUrlErrorTitle": "在 URL 中保存状态时出错", "kibana-react.dualRangeControl.maxInputAriaLabel": "范围最大值", "kibana-react.dualRangeControl.minInputAriaLabel": "范围最小值", "kibana-react.dualRangeControl.mustSetBothErrorMessage": "下限值和上限值都须设置", @@ -2654,6 +2802,14 @@ "kibana-react.exitFullScreenButton.exitFullScreenModeButtonAriaLabel": "退出全屏模式", "kibana-react.exitFullScreenButton.exitFullScreenModeButtonText": "退出全屏", "kibana-react.exitFullScreenButton.fullScreenModeDescription": "在全屏模式下,按 ESC 键可退出。", + "kibana-react.kbnOverviewPageHeader.addDataButtonLabel": "添加数据", + "kibana-react.kbnOverviewPageHeader.devToolsButtonLabel": "开发工具", + "kibana-react.kbnOverviewPageHeader.stackManagementButtonLabel": "管理", + "kibana-react.mountPointPortal.errorMessage": "呈现门户内容时出错", + "kibana-react.pageFooter.appDirectoryButtonLabel": "查看应用目录", + "kibana-react.pageFooter.changeDefaultRouteSuccessToast": "登陆页面已更新", + "kibana-react.pageFooter.changeHomeRouteLink": "登录时显示不同页面", + "kibana-react.pageFooter.makeDefaultRouteLink": "将此设为我的登陆页面", "kibana-react.splitPanel.adjustPanelSizeAriaLabel": "按左/右箭头键调整面板大小", "kibana-react.tableListView.listing.createNewItemButtonLabel": "创建 {entityName}", "kibana-react.tableListView.listing.deleteButtonMessage": "删除 {itemCount} 个 {entityName}", @@ -2673,6 +2829,24 @@ "kibana-react.tableListView.listing.table.editActionDescription": "编辑", "kibana-react.tableListView.listing.table.editActionName": "编辑", "kibana-react.tableListView.listing.unableToDeleteDangerMessage": "无法删除{entityName}", + "kibanaOverview.addData.sampleDataButtonLabel": "试用我们的样例数据", + "kibanaOverview.addData.sectionTitle": "采集您的数据", + "kibanaOverview.apps.title": "浏览这些应用", + "kibanaOverview.gettingStarted.addDataButtonLabel": "添加您的数据", + "kibanaOverview.gettingStarted.description": "Kibana 使您能够以自己的方式可视化数据。 首先提个问题,看看答案将您带往何处。", + "kibanaOverview.gettingStarted.title": "Kibana 入门", + "kibanaOverview.header.title": "Kibana", + "kibanaOverview.kibana.appDescription1": "在仪表板中分析数据。", + "kibanaOverview.kibana.appDescription2": "搜索和查找洞见。", + "kibanaOverview.kibana.appDescription3": "设计完美的报告。", + "kibanaOverview.kibana.appDescription4": "绘制地理数据。", + "kibanaOverview.kibana.appDescription5": "建模、预测和检测。", + "kibanaOverview.kibana.appDescription6": "显示模式和关系。", + "kibanaOverview.kibana.solution.subtitle": "可视化和分析", + "kibanaOverview.kibana.solution.title": "Kibana", + "kibanaOverview.manageData.sectionTitle": "管理您的数据", + "kibanaOverview.more.title": "Elastic 让您事半功倍", + "kibanaOverview.news.title": "最近的新闻", "management.breadcrumb": "Stack Management", "management.landing.header": "欢迎使用 Stack Management {version}", "management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", @@ -2694,9 +2868,19 @@ "management.stackManagement.managementDescription": "您用于管理 Elastic Stack 的中心控制台。", "management.stackManagement.managementLabel": "Stack Management", "management.stackManagement.title": "Stack Management", + "maps_legacy.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "单元格维度的解释", + "maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionText": "在磁贴地图上显示的最大 geoHash 精度:7 代表较高,10 代表非常高,12 代表最大值。{cellDimensionsLink}", + "maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionTitle": "最大磁贴地图精度", + "maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText": "属性", + "maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsText": "坐标地图中 WMS 地图服务器支持的默认{propertiesLink}", + "maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsTitle": "默认 WMS 属性", "maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子对象应实现此方法以响应数据更新", + "maps_legacy.defaultDistributionMessage": "要获取 Maps,请升级到 {defaultDistribution} 版的 Elasticsearch 和 Kibana。", "maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel": "适应数据边界", "maps_legacy.kibanaMap.zoomWarning": "已达到缩放级别数目上限。要一直放大,请升级到 Elasticsearch 和 Kibana 的{defaultDistribution}。您可以通过 {ems} 免费使用其他缩放级别。或者,您可以配置自己的地图服务器。请前往 { wms } 或 { configSettings} 以获取详细信息。", + "maps_legacy.legacyMapDeprecationMessage": "使用 Maps,可以添加多个图层和索引,绘制单个文档,使用数据值表示特征,添加热图、网格和集群,等等。{getMapsMessage}", + "maps_legacy.legacyMapDeprecationTitle": "在 8.0 中,{label} 将迁移到 Maps。", + "maps_legacy.openInMapsButtonLabel": "在 Maps 中查看", "maps_legacy.wmsOptions.attributionStringTip": "右下角的归因字符串。", "maps_legacy.wmsOptions.baseLayerSettingsTitle": "基础图层设置", "maps_legacy.wmsOptions.imageFormatToUseTip": "通常为 image/png 或 image/jpeg。如果服务器将返回透明图层,则请使用 png。", @@ -2721,7 +2905,11 @@ "newsfeed.flyoutList.closeButtonLabel": "鍏抽棴", "newsfeed.flyoutList.versionTextLabel": "{version}", "newsfeed.flyoutList.whatsNewTitle": "Elastic 新闻", + "newsfeed.headerButton.readAriaLabel": "新闻源菜单 - 所有项目已读", + "newsfeed.headerButton.unreadAriaLabel": "新闻源菜单 - 存在未读项目", "newsfeed.loadingPrompt.gettingNewsText": "正在获取最近的新闻...", + "regionMap.advancedSettings.visualization.showRegionMapWarningsText": "词无法联接到地图上的形状时,区域地图是否显示警告。", + "regionMap.advancedSettings.visualization.showRegionMapWarningsTitle": "显示区域地图警告", "regionMap.choroplethLayer.downloadingVectorData404ErrorMessage": "尝试提取 {name} 时,服务器响应状态为“404”。请确保目标文件位于该位置。", "regionMap.choroplethLayer.downloadingVectorDataErrorMessage": "无法下载 {name} 文件。请确保服务器的 CORS 配置允许来自此主机上的 Kibana 应用程序的请求。", "regionMap.choroplethLayer.downloadingVectorDataErrorMessageTitle": "下载矢量数据时出错", @@ -2765,6 +2953,8 @@ "savedObjects.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认", "savedObjects.saveModal.cancelButtonLabel": "取消", "savedObjects.saveModal.descriptionLabel": "描述", + "savedObjects.saveModal.duplicateTitleDescription": "保存“{title}”时会创建重复的标题。", + "savedObjects.saveModal.duplicateTitleLabel": "此 {objectType} 已存在", "savedObjects.saveModal.saveAsNewLabel": "另存为新的 {objectType}", "savedObjects.saveModal.saveButtonLabel": "保存", "savedObjects.saveModal.saveTitle": "保存 {objectType}", @@ -2781,6 +2971,14 @@ "savedObjectsManagement.deleteSavedObjectsConfirmModalDescription": "此操作将删除以下已保存对象:", "savedObjectsManagement.field.offLabel": "关闭", "savedObjectsManagement.field.onLabel": "开启", + "savedObjectsManagement.importSummary.createdCountHeader": "{createdCount} 个新", + "savedObjectsManagement.importSummary.createdOutcomeLabel": "已创建", + "savedObjectsManagement.importSummary.errorCountHeader": "{errorCount} 个错误", + "savedObjectsManagement.importSummary.errorOutcomeLabel": "{errorMessage}", + "savedObjectsManagement.importSummary.headerLabelPlural": "{importCount} 个对象已导入", + "savedObjectsManagement.importSummary.headerLabelSingular": "1 个对象已导入", + "savedObjectsManagement.importSummary.overwrittenCountHeader": "{overwrittenCount} 个已覆盖", + "savedObjectsManagement.importSummary.overwrittenOutcomeLabel": "已覆盖", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "覆盖", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "确定要覆盖“{title}”?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", @@ -2837,11 +3035,26 @@ "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription": "受影响对象样例", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName": "受影响对象样例", "savedObjectsManagement.objectsTable.flyout.resolveImportErrorsFileErrorMessage": "无法处理该文件。", + "savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel": "选择要导入的文件", "savedObjectsManagement.objectsTable.header.exportButtonLabel": "导出 {filteredCount, plural, one{# 个对象} other {# 个对象}}", "savedObjectsManagement.objectsTable.header.importButtonLabel": "导入", "savedObjectsManagement.objectsTable.header.refreshButtonLabel": "刷新", "savedObjectsManagement.objectsTable.header.savedObjectsTitle": "已保存对象", - "savedObjectsManagement.objectsTable.howToDeleteSavedObjectsDescription": "从这里您可以删除已保存对象,如已保存搜索。还可以编辑已保存对象的原始数据。通常,对象只能通过其关联的应用程序进行修改;或许您应该遵循这一原则,而非使用此屏幕进行修改。", + "savedObjectsManagement.objectsTable.howToDeleteSavedObjectsDescription": "管理和共享已保存对象。要编辑对象的底层数据,请前往其关联应用程序。", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.disabledText": "检查以前是否已复制或导入对象。", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.disabledTitle": "检查现有对象", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.enabledText": "使用此选项可创建对象的一个或多个副本。", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.enabledTitle": "使用随机 ID 创建新对象", + "savedObjectsManagement.objectsTable.importModeControl.importOptionsTitle": "导入选项", + "savedObjectsManagement.objectsTable.importModeControl.overwrite.disabledLabel": "冲突时请求操作", + "savedObjectsManagement.objectsTable.importModeControl.overwrite.enabledLabel": "自动覆盖冲突", + "savedObjectsManagement.objectsTable.importSummary.unsupportedTypeError": "对象类型不受支持", + "savedObjectsManagement.objectsTable.overwriteModal.body.ambiguousConflict": "“{title}”与多个现有对象冲突。覆盖一个?", + "savedObjectsManagement.objectsTable.overwriteModal.body.conflict": "“{title}”与现有对象冲突。将其覆盖?", + "savedObjectsManagement.objectsTable.overwriteModal.cancelButtonText": "跳过", + "savedObjectsManagement.objectsTable.overwriteModal.overwriteButtonText": "覆盖", + "savedObjectsManagement.objectsTable.overwriteModal.selectControlLabel": "对象 ID", + "savedObjectsManagement.objectsTable.overwriteModal.title": "覆盖 {type}?", "savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionDescription": "检查此已保存对象", "savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionName": "检查", "savedObjectsManagement.objectsTable.relationships.columnActionsName": "操作", @@ -2873,6 +3086,7 @@ "savedObjectsManagement.objectsTable.table.exportButtonLabel": "导出", "savedObjectsManagement.objectsTable.table.exportPopoverButtonLabel": "导出", "savedObjectsManagement.objectsTable.table.typeFilterName": "类型", + "savedObjectsManagement.objectsTable.unableFindSavedObjectNotificationMessage": "找不到已保存对象", "savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage": "找不到已保存对象", "savedObjectsManagement.parsingFieldErrorMessage": "为索引模式“{indexName}”解析“{fieldName}”时发生错误:{errorMessage}", "savedObjectsManagement.view.cancelButtonAriaLabel": "取消", @@ -2890,7 +3104,11 @@ "savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "与此对象关联的已保存搜索已不存在。", "savedObjectsManagement.view.viewItemButtonLabel": "查看“{title}”", "savedObjectsManagement.view.viewItemTitle": "查看“{title}”", - "usageCollection.stats.notReadyMessage": "统计尚未就绪。请稍后重试", + "security.checkup.dismissButtonText": "关闭", + "security.checkup.dontShowAgain": "不再显示", + "security.checkup.insecureClusterMessage": "我们的免费安全功能可防止未经授权的访问。", + "security.checkup.insecureClusterTitle": "请确保您的安装安全", + "security.checkup.learnMoreButtonText": "了解详情", "share.advancedSettings.csv.quoteValuesText": "在 CSV 导出中是否应使用引号引起值?", "share.advancedSettings.csv.quoteValuesTitle": "使用引号引起 CSV 值", "share.advancedSettings.csv.separatorText": "使用此字符串分隔导出的值", @@ -2925,6 +3143,7 @@ "telemetry.callout.errorLoadingClusterStatisticsTitle": "加载集群统计信息时出错", "telemetry.callout.errorUnprivilegedUserDescription": "您无权查看未加密的集群统计信息。", "telemetry.callout.errorUnprivilegedUserTitle": "显示集群统计信息时出错", + "telemetry.clusterData": "集群数据", "telemetry.optInErrorToastText": "尝试设置使用统计信息首选项时发生错误。", "telemetry.optInErrorToastTitle": "错误", "telemetry.optInNoticeSeenErrorTitle": "错误", @@ -2934,6 +3153,8 @@ "telemetry.provideUsageStatisticsAriaName": "提供使用情况统计", "telemetry.provideUsageStatisticsTitle": "提供使用情况统计", "telemetry.readOurUsageDataPrivacyStatementLinkText": "隐私声明", + "telemetry.securityData": "Endpoint Security 数据", + "telemetry.seeExamplesOfWhatWeCollect": "查看我们收集的{clusterData}和 {endpointSecurityData}的示例。", "telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack?数据使用情况收集当前已禁用。启用数据使用情况收集可帮助我们管理并改善产品和服务。有关详情,请参阅我们的{privacyStatementLink}。", "telemetry.telemetryConfigAndLinkDescription": "启用使用情况数据收集可帮助我们管理并改善产品和服务。有关更多详情,请参阅我们的{privacyStatementLink}。", "telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。", @@ -2979,6 +3200,8 @@ "timelion.cells.actions.reorderAriaLabel": "拖动以重新排序", "timelion.cells.actions.reorderTooltip": "拖动以重新排序", "timelion.chart.seriesList.noSchemaWarning": "没有此类面板类型:{renderType}", + "timelion.deprecation.here": "请将其迁移至仪表板。", + "timelion.deprecation.message": "Timelion 应用自 7.0 版起已过时,将在 8.0 中移除。要继续使用 Timelion 工作表,{timeLionDeprecationLink}。", "timelion.emptyExpressionErrorMessage": "Timelion 错误:未提供表达式", "timelion.expressionInputAriaLabel": "Timelion 表达式", "timelion.expressionInputPlaceholder": "请尝试使用 {esQuery} 查询", @@ -3317,8 +3540,16 @@ "timelion.vis.invalidIntervalErrorMessage": "时间间隔格式无效。", "timelion.vis.selectIntervalHelpText": "选择选项或创建定制值。示例:30s、20m、24h、2d、1w、1M", "timelion.vis.selectIntervalPlaceholder": "选择时间间隔", + "uiActions.actionPanel.more": "更多", "uiActions.actionPanel.title": "选项", "uiActions.errors.incompatibleAction": "操作不兼容", + "uiActions.triggers.applyFilterDescription": "应用 kibana 筛选时。可能是单个值或范围筛选。", + "uiActions.triggers.applyFilterTitle": "应用筛选", + "uiActions.triggers.selectRangeDescription": "可视化上的一组值", + "uiActions.triggers.selectRangeTitle": "范围选择", + "uiActions.triggers.valueClickDescription": "可视化上的数据点单击", + "uiActions.triggers.valueClickTitle": "单击", + "usageCollection.stats.notReadyMessage": "统计信息尚未准备就绪。请稍后重试。", "visDefaultEditor.advancedToggle.advancedLinkLabel": "高级", "visDefaultEditor.agg.disableAggButtonTooltip": "禁用聚合", "visDefaultEditor.agg.enableAggButtonTooltip": "启用聚合", @@ -3383,9 +3614,13 @@ "visDefaultEditor.controls.ipRangesAriaLabel": "IP 范围", "visDefaultEditor.controls.jsonInputLabel": "JSON 输入", "visDefaultEditor.controls.jsonInputTooltip": "此处以 JSON 格式添加的任何属性将与此部分的 elasticsearch 聚合定义合并。例如,词聚合上的“shard_size”。", + "visDefaultEditor.controls.maxBars.autoPlaceholder": "自动", + "visDefaultEditor.controls.maxBars.maxBarsHelpText": "将根据可用数据自动选择时间间隔。最大条形数不能大于“高级设置”的 {histogramMaxBars}", + "visDefaultEditor.controls.maxBars.maxBarsLabel": "最大条形数", "visDefaultEditor.controls.metricLabel": "指标", "visDefaultEditor.controls.metrics.bucketTitle": "存储桶", "visDefaultEditor.controls.metrics.metricTitle": "指标", + "visDefaultEditor.controls.numberInterval.autoInteralIsUsed": "已使用自动时间间隔", "visDefaultEditor.controls.numberInterval.minimumIntervalLabel": "最小时间间隔", "visDefaultEditor.controls.numberInterval.minimumIntervalTooltip": "提供的值创建的存储桶数目大于“高级设置”的 {histogramMaxBars} 指定的数目时,将自动缩放时间间隔", "visDefaultEditor.controls.numberInterval.selectIntervalPlaceholder": "输入时间间隔", @@ -3434,6 +3669,7 @@ "visDefaultEditor.controls.timeInterval.scaledHelpText": "当前缩放至 {bucketDescription}", "visDefaultEditor.controls.timeInterval.selectIntervalPlaceholder": "选择时间间隔", "visDefaultEditor.controls.timeInterval.selectOptionHelpText": "选择选项或创建定制值。示例:30s、20m、24h、2d、1w、1M", + "visDefaultEditor.controls.useAutoInterval": "使用自动时间间隔", "visDefaultEditor.editorConfig.dateHistogram.customInterval.helpText": "必须是配置时间间隔的倍数:{interval}", "visDefaultEditor.editorConfig.histogram.interval.helpText": "必须是配置时间间隔的倍数:{interval}", "visDefaultEditor.metrics.wrongLastBucketTypeErrorMessage": "使用“{type}”指标聚合时,上一存储桶聚合必须是“Date Histogram”或“Histogram”。", @@ -4084,9 +4320,21 @@ "visTypeVega.esQueryParser.shiftMustValueTypeErrorMessage": "{shiftParam} 必须为数值", "visTypeVega.esQueryParser.timefilterValueErrorMessage": "{timefilter} 属性必须设置为 {trueValue}、{minValue} 或 {maxValue}", "visTypeVega.esQueryParser.unknownUnitValueErrorMessage": "{unitParamName} 值未知。必须为以下值之一:[{unitParamValues}]", + "visTypeVega.esQueryParser.unnamedRequest": "未命名的请求 #{index}", "visTypeVega.esQueryParser.urlBodyValueTypeErrorMessage": "{configName} 必须为对象", "visTypeVega.esQueryParser.urlContextAndUrlTimefieldMustNotBeUsedErrorMessage": "设置了 {queryParam} 时,不得使用 {urlContext} 和 {timefield}", "visTypeVega.function.help": "Vega 可视化", + "visTypeVega.inspector.dataSetsLabel": "数据集", + "visTypeVega.inspector.dataViewer.dataSetAriaLabel": "数据集", + "visTypeVega.inspector.dataViewer.gridAriaLabel": "{name} 数据网格", + "visTypeVega.inspector.signalValuesLabel": "信号值", + "visTypeVega.inspector.signalViewer.gridAriaLabel": "信号值数据网格", + "visTypeVega.inspector.specLabel": "规范", + "visTypeVega.inspector.specViewer.copyToClipboardLabel": "复制到剪贴板", + "visTypeVega.inspector.vegaAdapter.signal": "信号", + "visTypeVega.inspector.vegaAdapter.value": "值", + "visTypeVega.inspector.vegaDebugLabel": "Vega 调试", + "visTypeVega.mapView.experimentalMapLayerInfo": "地图图层处于试验状态,不受正式发行版功能的支持 SLA 的约束。如欲提供反馈,请在 {githubLink} 中创建问题。", "visTypeVega.mapView.mapStyleNotFoundWarningMessage": "找不到 {mapStyleParam}", "visTypeVega.mapView.minZoomAndMaxZoomHaveBeenSwappedWarningMessage": "已交换 {minZoomPropertyName} 和 {maxZoomPropertyName}", "visTypeVega.mapView.resettingPropertyToMaxValueWarningMessage": "将 {name} 重置为 {max}", @@ -4094,6 +4342,7 @@ "visTypeVega.type.vegaDescription": "使用 Vega 和 Vega-Lite 创建定制可视化", "visTypeVega.urlParser.dataUrlRequiresUrlParameterInFormErrorMessage": "{dataUrlParam} 需要以“{formLink}”形式的 {urlParam} 参数", "visTypeVega.urlParser.urlShouldHaveQuerySubObjectWarningMessage": "使用 {urlObject} 应具有 {subObjectName} 子对象", + "visTypeVega.vegaParser.autoSizeDoesNotAllowFalse": "{autoSizeParam} 已启用,只能通过将 {autoSizeParam} 设置为 {noneParam} 来禁用", "visTypeVega.vegaParser.baseView.externalUrlsAreNotEnabledErrorMessage": "未启用外部 URL。将 {enableExternalUrls} 添加到 {kibanaConfigFileName}", "visTypeVega.vegaParser.baseView.functionIsNotDefinedForGraphErrorMessage": "没有为此图表定义 {funcName}", "visTypeVega.vegaParser.baseView.timeValuesTypeErrorMessage": "设置时间筛选时出错:时间值必须为相对日期或绝对日期。{start}、{end}", @@ -4101,6 +4350,7 @@ "visTypeVega.vegaParser.dataExceedsSomeParamsUseTimesLimitErrorMessage": "数据不得包含 {urlParam}、{valuesParam} 和 {sourceParam} 中的多个值", "visTypeVega.vegaParser.hostConfigIsDeprecatedWarningMessage": "{deprecatedConfigName} 已弃用。请改用 {newConfigName}。", "visTypeVega.vegaParser.hostConfigValueTypeErrorMessage": "如果存在,{configName} 必须为对象", + "visTypeVega.vegaParser.inputSpecDoesNotSpecifySchemaErrorMessage": "您的规范要求 {schemaParam} 字段包含\nVega(请参见 {vegaSchemaUrl})或\nVega-Lite(请参见 {vegaLiteSchemaUrl})的有效 URL。\n该 URL 仅限标识符。Kibana 和您的浏览器将不访问此 URL。", "visTypeVega.vegaParser.invalidVegaSpecErrorMessage": "Vega 规范无效", "visTypeVega.vegaParser.kibanaConfigValueTypeErrorMessage": "如果存在,{configName} 必须为对象", "visTypeVega.vegaParser.mapStyleValueTypeWarningMessage": "{mapStyleConfigName} 可能为 {mapStyleConfigFirstAllowedValue} 或 {mapStyleConfigSecondAllowedValue}", @@ -4114,6 +4364,7 @@ "visTypeVega.vegaParser.unrecognizedControlsLocationValueErrorMessage": "无法识别的 {controlsLocationParam} 值。应为 [{locToDirMap}] 之一", "visTypeVega.vegaParser.unrecognizedDirValueErrorMessage": "{dirParam} 值无法识别。应为 [{expectedValues}] 之一", "visTypeVega.vegaParser.VLCompilerShouldHaveGeneratedSingleProtectionObjectErrorMessage": "内部错误:Vega-Lite 编译器应已生成单个投影对象", + "visTypeVega.vegaParser.widthAndHeightParamsAreIgnored": "{widthParam} 和 {heightParam} 参数已忽略,因为 {autoSizeParam} 已启用。将 {autoSizeParam} 设置为 {noneParam} 可禁用", "visTypeVega.visualization.indexNotFoundErrorMessage": "找不到索引 {index}", "visTypeVega.visualization.renderErrorTitle": "Vega 错误", "visTypeVega.visualization.unableToFindDefaultIndexErrorMessage": "找不到默认索引", @@ -4292,6 +4543,7 @@ "visTypeVislib.thresholdLine.style.dashedText": "虚线", "visTypeVislib.thresholdLine.style.dotdashedText": "点虚线", "visTypeVislib.thresholdLine.style.fullText": "实线", + "visTypeVislib.vislib.errors.noResultsFoundTitle": "找不到结果", "visTypeVislib.vislib.heatmap.maxBucketsText": "定义了过多的序列 ({nr})。配置的最大值为 {max}。", "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "筛留值 {legendDataLabel}", "visTypeVislib.vislib.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", @@ -4308,6 +4560,7 @@ "visualizations.disabledLabVisualizationMessage": "请在高级设置中打开实验室模式,以查看实验室可视化。", "visualizations.disabledLabVisualizationTitle": "{title} 为实验室可视化。", "visualizations.displayName": "可视化", + "visualizations.embeddable.placeholderTitle": "占位符标题", "visualizations.function.range.from.help": "范围起始", "visualizations.function.range.help": "生成范围对象", "visualizations.function.range.to.help": "范围结束", @@ -4334,14 +4587,23 @@ "visualizations.newVisWizard.searchSelection.savedObjectType.search": "已保存搜索", "visualizations.newVisWizard.selectVisType": "选择可视化类型", "visualizations.newVisWizard.title": "新建可视化", + "visualizations.noResultsFoundTitle": "找不到结果", "visualizations.savedObjectName": "可视化", + "visualizations.savingVisualizationFailed.errorMsg": "保存可视化失败", "visualizations.visualizationTypeInvalidMessage": "无效的可视化类型“{visType}”", "visualize.badge.readOnly.text": "只读", "visualize.badge.readOnly.tooltip": "无法保存可视化", + "visualize.byValue_pageHeading": "已嵌入到 {originatingApp} 应用中的 {chartType} 类型可视化", + "visualize.confirmModal.confirmTextDescription": "离开 Visualize 编辑器而不保存更改?", + "visualize.confirmModal.title": "未保存的更改", "visualize.createVisualization.failedToLoadErrorMessage": "无法加载可视化", "visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId", "visualize.createVisualization.noVisTypeErrorMessage": "必须提供有效的可视化类型", + "visualize.dashboard.prefix.breadcrumb": "仪表板", + "visualize.discover.visualizeFieldLabel": "可视化字段", "visualize.editor.createBreadcrumb": "创建", + "visualize.editor.defaultEditBreadcrumbText": "编辑", + "visualize.experimentalVisInfoText": "此可视化为试验性功能,不受正式发行版功能支持 SLA 的约束。如欲提供反馈,请在 {githubLink} 中创建问题。", "visualize.helpMenu.appName": "Visualize", "visualize.linkedToSearch.unlinkSuccessNotificationText": "已取消与已保存搜索“{searchTitle}”的链接", "visualize.listing.betaTitle": "公测版", @@ -4362,6 +4624,9 @@ "visualize.noMatchRoute.bannerText": "Visualize 应用程序无法识别此路由:{route}。", "visualize.noMatchRoute.bannerTitleText": "未找到页面", "visualize.pageHeading": "{chartName} {chartType} 可视化", + "visualize.topNavMenu.cancelAndReturnButtonTooltip": "完成前放弃所做的更改", + "visualize.topNavMenu.cancelButtonAriaLabel": "返回到上一个应用而不保存更改", + "visualize.topNavMenu.cancelButtonLabel": "取消", "visualize.topNavMenu.openInspectorButtonAriaLabel": "打开检查器查看可视化", "visualize.topNavMenu.openInspectorButtonLabel": "检查", "visualize.topNavMenu.openInspectorDisabledButtonTooltip": "此可视化不支持任何检查器。", @@ -4392,11 +4657,15 @@ "xpack.actions.builtin.case.connectorApiNullError": "需要指定连接器 [apiUrl]", "xpack.actions.builtin.case.jiraTitle": "Jira", "xpack.actions.builtin.case.resilientTitle": "IBM Resilient", + "xpack.actions.builtin.configuration.apiAllowedHostsError": "配置连接器操作时出错:{message}", "xpack.actions.builtin.email.errorSendingErrorMessage": "发送电子邮件时出错", "xpack.actions.builtin.emailTitle": "电子邮件", "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "索引文档时出错", "xpack.actions.builtin.esIndexTitle": "索引", + "xpack.actions.builtin.jira.configuration.apiAllowedHostsError": "配置连接器操作时出错:{message}", + "xpack.actions.builtin.jira.configuration.emptyMapping": "[incidentConfiguration.mapping]:应为非空,但却为空", "xpack.actions.builtin.pagerduty.invalidTimestampErrorMessage": "解析时间戳“{timestamp}”时出错", + "xpack.actions.builtin.pagerduty.missingDedupkeyErrorMessage": "当 eventAction 是“{eventAction}”时需要 DedupKey", "xpack.actions.builtin.pagerduty.pagerdutyConfigurationError": "配置 pagerduty 操作时出错:{message}", "xpack.actions.builtin.pagerduty.postingErrorMessage": "发布 pagerduty 事件时出错", "xpack.actions.builtin.pagerduty.postingRetryErrorMessage": "发布 pagerduty 事件时出错:http 状态 {status},请稍后重试", @@ -4424,39 +4693,18 @@ "xpack.actions.builtin.webhook.webhookConfigurationErrorNoHostname": "配置 Webhook 操作时出错:无法解析 url:{err}", "xpack.actions.builtin.webhookTitle": "Webhook", "xpack.actions.disabledActionTypeError": "操作类型“{actionType}”在 Kibana 配置 xpack.actions.enabledActionTypes 中未启用", + "xpack.actions.featureRegistry.actionsFeatureName": "操作和连接器", "xpack.actions.serverSideErrors.expirerdLicenseErrorMessage": "操作类型 {actionTypeId} 已禁用,因为您的{licenseType}许可证已过期。", "xpack.actions.serverSideErrors.invalidLicenseErrorMessage": "操作类型 {actionTypeId} 已禁用,因为您的{licenseType}许可证不支持。请升级您的许可证。", "xpack.actions.serverSideErrors.predefinedActionDeleteDisabled": "不允许删除预配置的操作 {id}。", "xpack.actions.serverSideErrors.predefinedActionUpdateDisabled": "不允许更新预配置的操作 {id}。", "xpack.actions.serverSideErrors.unavailableLicenseErrorMessage": "操作类型 {actionTypeId} 已禁用,因为许可证信息当前不可用。", "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "操作不可用 - 许可信息当前不可用。", - "xpack.stackAlerts.indexThreshold.actionGroupThresholdMetTitle": "阈值已达到", - "xpack.stackAlerts.indexThreshold.actionVariableContextDateLabel": "告警超过阈值的日期。", - "xpack.stackAlerts.indexThreshold.actionVariableContextGroupLabel": "超过阈值的组。", - "xpack.stackAlerts.indexThreshold.actionVariableContextMessageLabel": "告警的预构造消息。", - "xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "告警的预构造标题。", - "xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "超过阈值的值。", - "xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage": "[aggField]:当 [aggType] 为“{aggType}”时必须有值", - "xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "告警 {name} 组 {group} 值 {value} 在 {window} 于 {date}超过阈值 {function}", - "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "告警 {name} 组 {group} 超过阈值", - "xpack.stackAlerts.indexThreshold.alertTypeTitle": "索引阈值", - "xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage": "[dateStart]:大于 [dateEnd]", - "xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage": "{fieldName} 的 {formatName} 格式无效:“{fieldValue}”", - "xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage": "[interval]:如果 [dateStart] 不等于 [dateEnd],则必须指定", - "xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage": "无效的 aggType:“{aggType}”", - "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "指定了无效的 thresholdComparator:{comparator}", - "xpack.stackAlerts.indexThreshold.invalidDateErrorMessage": "无效的日期 {date}", - "xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage": "无效的持续时间:“{duration}”", - "xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage": "无效的 groupBy:“{groupBy}”", - "xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage": "[termSize]:必须小于或等于 {maxGroups}", - "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]:对于“{thresholdComparator}”比较运算符,必须包含两个元素", - "xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage": "无效的 timeWindowUnit:“{timeWindowUnit}”", - "xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage": "时间间隔 {intervals} 的计算数目大于最大值 {maxIntervals}", - "xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage": "[termField]:[groupBy] 为 top 时,必需 termField", - "xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage": "[termSize]:[groupBy] 为 top 时,必需 termSize", + "xpack.actions.urlAllowedHostsConfigurationError": "目标 {field} 的“{value}”未添加到 Kibana 配置 xpack.actions.allowedHosts", "xpack.alerts.alertNavigationRegistry.get.missingNavigationError": "在“{consumer}”内针对告警类型“{alertType}”的导航未注册。", "xpack.alerts.alertNavigationRegistry.register.duplicateDefaultError": "“{consumer}”内的默认导航已注册。", "xpack.alerts.alertNavigationRegistry.register.duplicateNavigationError": "在“{consumer}”内针对告警类型“{alertType}”的导航已注册。", + "xpack.alerts.alertsClient.invalidDate": "参数 {field} 的日期无效:“{dateValue}”", "xpack.alerts.alertsClient.validateActions.invalidGroups": "无效操作组:{groups}", "xpack.alerts.alertTypeRegistry.get.missingAlertTypeError": "未注册告警类型“{id}”。", "xpack.alerts.alertTypeRegistry.register.duplicateAlertTypeError": "已注册告警类型“{id}”。", @@ -4464,6 +4712,7 @@ "xpack.alerts.appName": "告警", "xpack.alerts.loadAlertType.missingAlertTypeError": "未注册告警类型“{id}”。", "xpack.alerts.serverSideErrors.unavailableLicenseInformationErrorMessage": "告警不可用 - 许可信息当前不可用。", + "xpack.apm.a.thresholdMet": "已达到阈值", "xpack.apm.addDataButtonLabel": "添加数据", "xpack.apm.agentConfig.allOptionLabel": "全部", "xpack.apm.agentConfig.apiRequestSize.description": "通过块编码(HTTP 流)发送到 APM Server 摄入 API 的请求正文最大压缩总大小。\n注意,有可能出现小幅的过冲。\n\n允许使用的字节单位包括 `b`、`kb` 和 `mb`。`1kb` 等于 `1024b`。", @@ -4573,7 +4822,29 @@ "xpack.apm.agentMetrics.java.threadCount": "平均计数", "xpack.apm.agentMetrics.java.threadCountChartTitle": "线程计数", "xpack.apm.agentMetrics.java.threadCountMax": "最大计数", - "xpack.apm.alertTypes.transactionDuration": "事务持续时间", + "xpack.apm.alerting.fields.all_option": "全部", + "xpack.apm.alerting.fields.environment": "环境", + "xpack.apm.alerting.fields.service": "服务", + "xpack.apm.alerting.fields.type": "类型", + "xpack.apm.alerts.action_variables.environment": "创建告警的事务类型", + "xpack.apm.alerts.action_variables.intervalSize": "符合告警条件的时段的长度和单位", + "xpack.apm.alerts.action_variables.serviceName": "创建告警的服务", + "xpack.apm.alerts.action_variables.threshold": "只要触发值大于此值,就会导致告警触发", + "xpack.apm.alerts.action_variables.transactionType": "创建告警的事务类型", + "xpack.apm.alerts.action_variables.triggerValue": "超过阈值并触发告警的值", + "xpack.apm.alerts.anomalySeverity.criticalLabel": "紧急", + "xpack.apm.alerts.anomalySeverity.majorLabel": "重大", + "xpack.apm.alerts.anomalySeverity.minor": "轻微", + "xpack.apm.alerts.anomalySeverity.scoreDetailsDescription": "{value} 及以上分数", + "xpack.apm.alerts.anomalySeverity.warningLabel": "警告", + "xpack.apm.alertTypes.errorCount": "错误计数阈值", + "xpack.apm.alertTypes.errorCount.defaultActionMessage": "由于以下条件 \\{\\{alertName\\}\\} 告警触发:\n\n- 服务名称:\\{\\{context.serviceName\\}\\}\n- 环境:\\{\\{context.environment\\}\\}\n- 阈值:\\{\\{context.threshold\\}\\} 个错误\n- 已触发的值:在过去 \\{\\{context.interval\\}\\}有 \\{\\{context.triggerValue\\}\\} 个错误", + "xpack.apm.alertTypes.transactionDuration": "事务持续时间阈值", + "xpack.apm.alertTypes.transactionDuration.defaultActionMessage": "由于以下条件 \\{\\{alertName\\}\\} 告警触发:\n\n- 服务名称:\\{\\{context.serviceName\\}\\}\n- 类型:\\{\\{context.transactionType\\}\\}\n- 环境:\\{\\{context.environment\\}\\}\n- 阈值:\\{\\{context.threshold\\}\\}ms\n- 已触发的值:在过去 \\{\\{context.interval\\}\\}为 \\{\\{context.triggerValue\\}\\}", + "xpack.apm.alertTypes.transactionDurationAnomaly": "事务持续时间异常", + "xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage": "由于以下条件 \\{\\{alertName\\}\\} 告警触发:\n\n- 服务名称:\\{\\{context.serviceName\\}\\}\n- 类型:\\{\\{context.transactionType\\}\\}\n- 环境:\\{\\{context.environment\\}\\}\n- 严重性阈值:\\{\\{context.threshold\\}\\}\n- 严重性值:\\{\\{context.thresholdValue\\}\\}\n", + "xpack.apm.alertTypes.transactionErrorRate": "事务错误率阈值", + "xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage": "由于以下条件 \\{\\{alertName\\}\\} 告警触发:\n\n- 服务名称:\\{\\{context.serviceName\\}\\}\n- 类型:\\{\\{context.transactionType\\}\\}\n- 环境:\\{\\{context.environment\\}\\}\n- 阈值:\\{\\{context.threshold\\}\\}%\n- 已触发的值:在过去 \\{\\{context.interval\\}\\}有 \\{\\{context.triggerValue\\}\\}% 的错误", "xpack.apm.anomaly_detection.error.invalid_license": "要使用异常检测,必须订阅 Elastic 白金级许可证。有了该许可证,您便可借助 Machine Learning 监测服务。", "xpack.apm.anomaly_detection.error.missing_read_privileges": "必须对 Machine Learning 和 APM 具有“读”权限,才能查看“异常检测”作业", "xpack.apm.anomaly_detection.error.missing_write_privileges": "必须对 Machine Learning 和 APM 具有“写”权限,才能创建“异常检测”作业", @@ -4587,6 +4858,7 @@ "xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText": "尚未针对环境“{currentEnvironment}”启用异常检测。单击可继续设置。", "xpack.apm.anomalyDetectionSetup.notEnabledText": "异常检测尚未启用。单击以继续设置。", "xpack.apm.apmDescription": "自动从您的应用程序内收集深入全面的性能指标和错误。", + "xpack.apm.apply.label": "应用", "xpack.apm.applyFilter": "应用 {title} 筛选", "xpack.apm.applyOptions": "应用选项", "xpack.apm.breadcrumb.errorsTitle": "错误", @@ -4610,6 +4882,11 @@ "xpack.apm.chart.memorySeries.systemAverageLabel": "平均值", "xpack.apm.chart.memorySeries.systemMaxLabel": "最大值", "xpack.apm.clearFilters": "清除筛选", + "xpack.apm.csm.breakdownFilter.browser": "浏览器", + "xpack.apm.csm.breakdownFilter.device": "设备", + "xpack.apm.csm.breakdownFilter.location": "位置", + "xpack.apm.csm.breakDownFilter.noBreakdown": "无细目", + "xpack.apm.csm.breakdownFilter.os": "OS", "xpack.apm.customLink.buttom.create": "创建定制链接", "xpack.apm.customLink.buttom.create.title": "创建", "xpack.apm.customLink.buttom.manage": "管理定制链接", @@ -4618,6 +4895,8 @@ "xpack.apm.emptyMessage.noDataFoundLabel": "未找到任何数据", "xpack.apm.error.prompt.body": "有关详情,请查看您的浏览器开发者控制台。", "xpack.apm.error.prompt.title": "抱歉,发生错误 :(", + "xpack.apm.errorCountAlert.name": "错误计数阈值", + "xpack.apm.errorCountAlertTrigger.errors": " 错误", "xpack.apm.errorGroupDetails.avgLabel": "平均", "xpack.apm.errorGroupDetails.culpritLabel": "原因", "xpack.apm.errorGroupDetails.errorGroupTitle": "错误组 {errorGroupId}", @@ -4642,11 +4921,12 @@ "xpack.apm.errorsTable.occurrencesColumnLabel": "发生次数", "xpack.apm.errorsTable.typeColumnLabel": "类型", "xpack.apm.errorsTable.unhandledLabel": "未处理", - "xpack.apm.featureRegistry.apmFeatureName": "APM", + "xpack.apm.featureRegistry.apmFeatureName": "APM 和用户体验", "xpack.apm.feedbackMenu.appName": "APM", "xpack.apm.fetcher.error.status": "错误", "xpack.apm.fetcher.error.title": "提取资源时出错", "xpack.apm.fetcher.error.url": "URL", + "xpack.apm.filter.environment.allLabel": "全部", "xpack.apm.filter.environment.label": "环境", "xpack.apm.filter.environment.notDefinedLabel": "未定义", "xpack.apm.filter.environment.selectEnvironmentLabel": "选择环境", @@ -4661,6 +4941,13 @@ "xpack.apm.header.badge.readOnly.tooltip": "无法保存", "xpack.apm.helpMenu.upgradeAssistantLink": "升级助手", "xpack.apm.histogram.plot.noDataLabel": "此时间范围内没有数据。", + "xpack.apm.home.alertsMenu.alerts": "告警", + "xpack.apm.home.alertsMenu.createAnomalyAlert": "创建异常告警", + "xpack.apm.home.alertsMenu.createThresholdAlert": "创建阈值告警", + "xpack.apm.home.alertsMenu.errorCount": "错误计数", + "xpack.apm.home.alertsMenu.transactionDuration": "事务持续时间", + "xpack.apm.home.alertsMenu.transactionErrorRate": "事务错误率", + "xpack.apm.home.alertsMenu.viewActiveAlerts": "查看活动告警", "xpack.apm.home.serviceMapTabLabel": "服务地图", "xpack.apm.home.servicesTabLabel": "服务", "xpack.apm.home.tracesTabLabel": "追溯", @@ -4716,11 +5003,13 @@ "xpack.apm.metrics.plot.noDataLabel": "此时间范围内没有数据。", "xpack.apm.metrics.transactionChart.machineLearningLabel": "Machine Learning", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "环绕平均持续时间的流显示预期边界。对 ≥ 75 的异常分数显示标注。", + "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "使用搜索栏筛选时,Machine Learning 结果处于隐藏状态", "xpack.apm.metrics.transactionChart.pageLoadTimesLabel": "页面加载时间", "xpack.apm.metrics.transactionChart.requestsPerMinuteLabel": "每分钟请求数", "xpack.apm.metrics.transactionChart.routeChangeTimesLabel": "路由更改时间", "xpack.apm.metrics.transactionChart.transactionDurationLabel": "事务持续时间", "xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel": "每分钟事务数", + "xpack.apm.metrics.transactionChart.viewJob": "查看作业:", "xpack.apm.notAvailableLabel": "不适用", "xpack.apm.percentOfParent": "({value} 的 {parentType, select, transaction {事务} trace {trace} })", "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "没有可用数据", @@ -4729,24 +5018,53 @@ "xpack.apm.propertiesTable.tabs.logStacktraceLabel": "日志堆栈跟踪", "xpack.apm.propertiesTable.tabs.metadataLabel": "元数据", "xpack.apm.propertiesTable.tabs.timelineLabel": "时间线", + "xpack.apm.rum.coreVitals.fcp": "首次内容绘制", + "xpack.apm.rum.coreVitals.tbt": "阻止总时间", "xpack.apm.rum.dashboard.backend": "后端", "xpack.apm.rum.dashboard.frontend": "前端", + "xpack.apm.rum.dashboard.impactfulMetrics.highTrafficPages": "高流量页面", + "xpack.apm.rum.dashboard.impactfulMetrics.jsErrors": "JavaScript 错误", "xpack.apm.rum.dashboard.overall.label": "总体", "xpack.apm.rum.dashboard.pageLoadDistribution.label": "页面加载分布", + "xpack.apm.rum.dashboard.pageLoadDuration.label": "页面加载持续时间", "xpack.apm.rum.dashboard.pageLoadTime.label": "页面加载时间(秒)", "xpack.apm.rum.dashboard.pageLoadTimes.label": "页面加载时间", "xpack.apm.rum.dashboard.pagesLoaded.label": "已加载页面", "xpack.apm.rum.dashboard.pageViews": "页面查看次数", "xpack.apm.rum.dashboard.resetZoom.label": "重置缩放比例", "xpack.apm.rum.filterGroup.breakdown": "细目", + "xpack.apm.rum.filterGroup.coreWebVitals": "网站体验核心指标", "xpack.apm.rum.filterGroup.seconds": "秒", "xpack.apm.rum.filterGroup.selectBreakdown": "选择细分", + "xpack.apm.rum.filters.searchByUrl": "按 URL 搜索", + "xpack.apm.rum.filters.searchResults": "{total} 项搜索结果", + "xpack.apm.rum.filters.select": "选择", + "xpack.apm.rum.filters.topPages": "顶级页面", + "xpack.apm.rum.filters.url": "URL", + "xpack.apm.rum.filters.url.loadingResults": "正在加载结果", + "xpack.apm.rum.filters.url.noResults": "没有可用结果", + "xpack.apm.rum.jsErrors.errorMessage": "错误消息", + "xpack.apm.rum.jsErrors.errorRate": "错误率", + "xpack.apm.rum.jsErrors.errorRateValue": "{errorRate}%", + "xpack.apm.rum.jsErrors.impactedPageLoads": "受影响的页面加载", + "xpack.apm.rum.jsErrors.totalErrors": "错误总数", + "xpack.apm.rum.userExperienceMetrics": "用户体验指标", + "xpack.apm.rum.uxMetrics.longestLongTasks": "长期任务最长持续时间", + "xpack.apm.rum.uxMetrics.noOfLongTasks": "长期任务数目", + "xpack.apm.rum.uxMetrics.sumLongTasks": "长期任务总持续时间", "xpack.apm.rum.visitorBreakdown": "访问者细分", + "xpack.apm.rum.visitorBreakdown.browser": "浏览器", + "xpack.apm.rum.visitorBreakdown.operatingSystem": "操作系统", + "xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration": "页面加载平均持续时间", + "xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion": "按区域列出的页面加载持续时间(平均值)", "xpack.apm.searchInput.filter": "筛选...", "xpack.apm.selectPlaceholder": "选择选项:", "xpack.apm.serviceDetails.alertsMenu.alerts": "告警", + "xpack.apm.serviceDetails.alertsMenu.createAnomalyAlert": "创建异常告警", "xpack.apm.serviceDetails.alertsMenu.createThresholdAlert": "创建阈值告警", + "xpack.apm.serviceDetails.alertsMenu.errorCount": "错误计数", "xpack.apm.serviceDetails.alertsMenu.transactionDuration": "事务持续时间", + "xpack.apm.serviceDetails.alertsMenu.transactionErrorRate": "事务错误率", "xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "查看活动的告警", "xpack.apm.serviceDetails.errorsTabLabel": "错误", "xpack.apm.serviceDetails.metrics.cpuUsageChartTitle": "CPU 使用", @@ -4755,6 +5073,10 @@ "xpack.apm.serviceDetails.metricsTabLabel": "指标", "xpack.apm.serviceDetails.nodesTabLabel": "JVM", "xpack.apm.serviceDetails.transactionsTabLabel": "事务", + "xpack.apm.serviceHealthStatus.critical": "紧急", + "xpack.apm.serviceHealthStatus.healthy": "运行正常", + "xpack.apm.serviceHealthStatus.unknown": "未知", + "xpack.apm.serviceHealthStatus.warning": "警告", "xpack.apm.serviceMap.anomalyDetectionPopoverDisabled": "通过在 APM 设置中启用异常检测来显示服务运行状况指标。", "xpack.apm.serviceMap.anomalyDetectionPopoverLink": "查看异常", "xpack.apm.serviceMap.anomalyDetectionPopoverNoData": "在选定时间范围内找不到异常分数。请在 Anomaly Explorer 中查看详情。", @@ -4773,7 +5095,10 @@ "xpack.apm.serviceMap.errorRatePopoverStat": "事务错误率(平均值)", "xpack.apm.serviceMap.focusMapButtonText": "聚焦地图", "xpack.apm.serviceMap.invalidLicenseMessage": "要访问服务地图,必须订阅 Elastic 白金级许可证。使用该许可证,您将能够可视化整个应用程序堆栈以及 APM 数据。", + "xpack.apm.serviceMap.noServicesPromptDescription": "我们在当前选择的时间范围和环境内找不到任何要映射的服务。请尝试其他范围或检查选定环境。如果您未使用任何服务,请使用我们的设置说明以开始。", + "xpack.apm.serviceMap.noServicesPromptTitle": "没有可用服务", "xpack.apm.serviceMap.popoverMetrics.noDataText": "选定的环境没有数据。请尝试切换到其他环境。", + "xpack.apm.serviceMap.resourceCountLabel": "{count} 项资源", "xpack.apm.serviceMap.serviceDetailsButtonText": "服务详情", "xpack.apm.serviceMap.subtypePopoverStat": "子类型", "xpack.apm.serviceMap.typePopoverStat": "类型", @@ -4787,6 +5112,10 @@ "xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningText": "无法识别这些指标属于哪些 JVM。这可能因为运行的 APM Server 版本低于 7.5。如果升级到 APM Server 7.5 或更高版本,应可解决此问题。有关升级的详细信息,请参阅{link}。或者,也可以使用 Kibana 查询栏按主机名、容器 ID 或其他字段筛选。", "xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningTitle": "找不到 JVM", "xpack.apm.serviceNodeNameMissing": "(空)", + "xpack.apm.serviceOverview.mlNudgeMessage.content": "我们集成了 ML 异常检测,让您可以查看服务的运行状况", + "xpack.apm.serviceOverview.mlNudgeMessage.dismissButton": "关闭消息", + "xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton": "了解详情", + "xpack.apm.serviceOverview.mlNudgeMessage.title": "启用异常检测可查看服务的运行状况", "xpack.apm.serviceOverview.toastText": "您正在运行 Elastic Stack 7.0+,我们检测到来自以前 6.x 版本的不兼容数据。如果想在 APM 中查看,您应迁移这些数据。在以下位置查看更多: ", "xpack.apm.serviceOverview.toastTitle": "在选定时间范围中检测到旧数据", "xpack.apm.serviceOverview.upgradeAssistantLink": "升级助手", @@ -4795,9 +5124,11 @@ "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均响应时间", "xpack.apm.servicesTable.environmentColumnLabel": "环境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 个环境} other {# 个环境}}", + "xpack.apm.servicesTable.healthColumnLabel": "运行状况", "xpack.apm.servicesTable.nameColumnLabel": "名称", "xpack.apm.servicesTable.noServicesLabel": "似乎您没有安装任何 APM 服务。让我们添加一些!", "xpack.apm.servicesTable.notFoundLabel": "未找到任何服务", + "xpack.apm.servicesTable.transactionErrorRate": "错误率 %", "xpack.apm.servicesTable.transactionsPerMinuteColumnLabel": "每分钟事务数", "xpack.apm.servicesTable.transactionsPerMinuteUnitLabel": "tpm", "xpack.apm.servicesTable.UpgradeAssistantLink": "通过访问 Kibana 升级助手来了解详情", @@ -4972,10 +5303,14 @@ "xpack.apm.transactionDurationAlert.aggregationType.95th": "第 95 个百分位", "xpack.apm.transactionDurationAlert.aggregationType.99th": "第 99 个百分位", "xpack.apm.transactionDurationAlert.aggregationType.avg": "平均值", - "xpack.apm.transactionDurationAlert.name": "事务持续时间", + "xpack.apm.transactionDurationAlert.name": "事务持续时间阈值", "xpack.apm.transactionDurationAlertTrigger.ms": "ms", "xpack.apm.transactionDurationAlertTrigger.when": "当", + "xpack.apm.transactionDurationAnomalyAlert.name": "事务持续时间异常", + "xpack.apm.transactionDurationAnomalyAlertTrigger.anomalySeverity": "有异常,严重性为", "xpack.apm.transactionDurationLabel": "持续时间", + "xpack.apm.transactionErrorRateAlert.name": "事务错误率阈值", + "xpack.apm.transactionErrorRateAlertTrigger.isAbove": "高于", "xpack.apm.transactions.chart.95thPercentileLabel": "第 95 个百分位", "xpack.apm.transactions.chart.99thPercentileLabel": "第 99 个百分位", "xpack.apm.transactions.chart.anomalyBoundariesLabel": "异常边界", @@ -4993,8 +5328,20 @@ "xpack.apm.tutorial.elasticCloud.textPre": "要启用 APM Server,请前往 [Elastic Cloud 控制台](https://cloud.elastic.co/deployments?q={cloudId}) 并在部署设置中启用 APM。启用后,请刷新此页面。", "xpack.apm.tutorial.elasticCloudInstructions.title": "APM 代理", "xpack.apm.tutorial.specProvider.artifacts.application.label": "启动 APM", + "xpack.apm.uifilter.badge.removeFilter": "移除筛选", "xpack.apm.unitLabel": "选择单位", "xpack.apm.unsavedChanges": "{unsavedChangesCount, plural, =0{0 个未保存更改} one {1 个未保存更改} other {# 个未保存更改}} ", + "xpack.apm.ux.jsErrors.percent": "{pageLoadPercent}%", + "xpack.apm.ux.localFilters.titles.webApplication": "Web 应用程序", + "xpack.apm.ux.percentile.50thMedian": "第 50 个(中值)", + "xpack.apm.ux.percentile.75th": "第 75 个", + "xpack.apm.ux.percentile.90th": "第 90 个", + "xpack.apm.ux.percentile.95th": "第 95 个", + "xpack.apm.ux.percentile.99th": "第 99 个", + "xpack.apm.ux.percentile.label": "百分位数", + "xpack.apm.ux.title": "用户体验", + "xpack.apm.ux.url.hitEnter.include": "单击 {icon} 可包括与 {searchValue} 匹配的所有 URL", + "xpack.apm.ux.visitorBreakdown.noData": "无数据。", "xpack.apm.version": "版本", "xpack.apm.waterfall.exceedsMax": "此跟踪中的项目数超过显示的项目数", "xpack.beatsManagement.beat.actionSectionTypeLabel": "类型:{beatType}。", @@ -5189,12 +5536,16 @@ "xpack.canvas.argFormArgSimpleForm.requiredTooltip": "此参数为必需,应指定值。", "xpack.canvas.argFormPendingArgValue.loadingMessage": "正在加载", "xpack.canvas.argFormSimpleFailure.failureTooltip": "此参数的接口无法解析该值,因此将使用回退输入", + "xpack.canvas.asset.confirmModalButtonLabel": "移除", + "xpack.canvas.asset.confirmModalDetail": "确定要移除此资产?", + "xpack.canvas.asset.confirmModalTitle": "移除资产", "xpack.canvas.asset.copyAssetTooltip": "将 ID 复制到剪贴板", "xpack.canvas.asset.createImageTooltip": "创建图像元素", "xpack.canvas.asset.deleteAssetTooltip": "删除", "xpack.canvas.asset.downloadAssetTooltip": "下载", "xpack.canvas.asset.thumbnailAltText": "资产缩略图", "xpack.canvas.assetManager.manageButtonLabel": "管理资产", + "xpack.canvas.assetModal.copyAssetMessage": "已将“{id}”复制到剪贴板", "xpack.canvas.assetModal.emptyAssetsDescription": "导入您的资产以开始", "xpack.canvas.assetModal.filePickerPromptText": "选择或拖放图像", "xpack.canvas.assetModal.loadingText": "正在上传图像", @@ -5218,6 +5569,7 @@ "xpack.canvas.customElementModal.remainingCharactersDescription": "剩余 {numberOfRemainingCharacter} 个字符", "xpack.canvas.customElementModal.saveButtonLabel": "保存", "xpack.canvas.datasourceDatasourceComponent.changeButtonLabel": "更改元素数据源", + "xpack.canvas.datasourceDatasourceComponent.expressionArgDescription": "数据源包含由表达式控制的参数。使用表达式编辑器可修改数据源。", "xpack.canvas.datasourceDatasourceComponent.previewButtonLabel": "预览数据", "xpack.canvas.datasourceDatasourceComponent.saveButtonLabel": "保存", "xpack.canvas.datasourceDatasourcePreview.emptyFirstLineDescription": "找不到与您的搜索条件匹配的任何文档。", @@ -5360,6 +5712,7 @@ "xpack.canvas.expressionTypes.argTypes.seriesStyle.styleLabel": "样式", "xpack.canvas.expressionTypes.argTypes.seriesStyleLabel": "设置选定已命名序列的样式", "xpack.canvas.expressionTypes.argTypes.seriesStyleTitle": "序列样式", + "xpack.canvas.featureCatalogue.canvasSubtitle": "设计完美的报告。", "xpack.canvas.functionForm.contextError": "错误:{errorMessage}", "xpack.canvas.functionForm.functionUnknown.unknownArgumentTypeError": "表达式类型“{expressionType}”未知", "xpack.canvas.functions.all.args.conditionHelpText": "要检查的条件。", @@ -5369,25 +5722,25 @@ "xpack.canvas.functions.alterColumn.args.typeHelpText": "将列转换成的类型。留空将不更改类型。", "xpack.canvas.functions.alterColumn.cannotConvertTypeErrorMessage": "无法连接到“{type}”", "xpack.canvas.functions.alterColumn.columnNotFoundErrorMessage": "找不到列:“{column}”", - "xpack.canvas.functions.alterColumnHelpText": "在核心类型(包括 {list})和 {end} 之间转换并重命名列。另见 {mapColumnFn} 和 {staticColumnFn}。", + "xpack.canvas.functions.alterColumnHelpText": "在核心类型(包括 {list} 和 {end})之间转换,并重命名列。另请参见 {mapColumnFn} 和 {staticColumnFn}。", "xpack.canvas.functions.any.args.conditionHelpText": "要检查的条件。", "xpack.canvas.functions.anyHelpText": "至少满足一个条件时,则返回 {BOOLEAN_TRUE}。另见 {all_fn}。", - "xpack.canvas.functions.as.args.nameHelpText": "要给予列的名称。", + "xpack.canvas.functions.as.args.nameHelpText": "要为列提供的名称。", "xpack.canvas.functions.asHelpText": "使用单个值创建 {DATATABLE}。另见 {getCellFn}。", "xpack.canvas.functions.asset.args.id": "要检索的资产的 ID。", "xpack.canvas.functions.asset.invalidAssetId": "无法通过以下 ID 获取资产:“{assetId}”", "xpack.canvas.functions.assetHelpText": "检索要作为参数值来提供的 Canvas Workpad 资产对象。通常为图像。", - "xpack.canvas.functions.axisConfig.args.maxHelpText": "轴上显示的最大值。必须为数字或自 Epoch 起毫秒数表示的日期或 {ISO8601} 字符串。", - "xpack.canvas.functions.axisConfig.args.minHelpText": "轴上显示的最小值。必须为数字或自 Epoch 起毫秒数表示的日期或 {ISO8601} 字符串。", + "xpack.canvas.functions.axisConfig.args.maxHelpText": "轴上显示的最大值。必须为数字、自 Epoch 起以毫秒数表示的日期或 {ISO8601} 字符串。", + "xpack.canvas.functions.axisConfig.args.minHelpText": "轴上显示的最小值。必须为数字、自 Epoch 起以毫秒数表示的日期或 {ISO8601} 字符串。", "xpack.canvas.functions.axisConfig.args.positionHelpText": "轴标签的位置。例如 {list} 或 {end}。", "xpack.canvas.functions.axisConfig.args.showHelpText": "显示轴标签?", - "xpack.canvas.functions.axisConfig.args.tickSizeHelpText": "刻度间的增量大小。仅用于`数值`轴", + "xpack.canvas.functions.axisConfig.args.tickSizeHelpText": "各刻度间的增量大小。仅适用于 `number` 轴。", "xpack.canvas.functions.axisConfig.invalidMaxPositionErrorMessage": "日期字符串无效:“{max}”。“max”必须是数值、以毫秒为单位的日期或 ISO8601 日期字符串", "xpack.canvas.functions.axisConfig.invalidMinDateStringErrorMessage": "日期字符串无效:“{min}”。“min”必须是数值、以毫秒为单位的日期或 ISO8601 日期字符串", "xpack.canvas.functions.axisConfig.invalidPositionErrorMessage": "无效的位置:“{position}”", "xpack.canvas.functions.axisConfigHelpText": "配置可视化的轴。仅用于 {plotFn}。", - "xpack.canvas.functions.case.args.ifHelpText": "此值表示条件是否满足,通常使用子表达式。{IF_ARG} 和 {WHEN_ARG} 参数都提供时,前者将覆盖后者。", - "xpack.canvas.functions.case.args.thenHelpText": "条件得到满足时要返回的值。", + "xpack.canvas.functions.case.args.ifHelpText": "此值指示是否符合条件。当 {IF_ARG} 和 {WHEN_ARG} 参数都提供时,前者将覆盖后者。", + "xpack.canvas.functions.case.args.thenHelpText": "条件得到满足时返回的值。", "xpack.canvas.functions.case.args.whenHelpText": "与 {CONTEXT} 比较的值,通过比较来确定它们是否相等。同时指定 {WHEN_ARG} 和 {IF_ARG} 时,将忽略前者。", "xpack.canvas.functions.caseHelpText": "构建要传递给 {switchFn} 函数的 {case},包括条件/结果。", "xpack.canvas.functions.clearHelpText": "清除 {CONTEXT},然后返回 {TYPE_NULL}。", @@ -5397,7 +5750,7 @@ "xpack.canvas.functions.compare.args.opHelpText": "要用于比较的运算符:{eq}(等于)、{gt}(大于)、{gte}(大于或等于)、{lt}(小于)、{lte}(小于或等于)、{ne} 或 {neq}(不等于)。", "xpack.canvas.functions.compare.args.toHelpText": "与 {CONTEXT} 比较的值。", "xpack.canvas.functions.compare.invalidCompareOperatorErrorMessage": "无效的比较运算符:“{op}”。使用 {ops}", - "xpack.canvas.functions.compareHelpText": "将 {CONTEXT} 与指定值比较,以确定 {BOOLEAN_TRUE} 或 {BOOLEAN_FALSE}。通常与 `{ifFn}` 或 `{caseFn}` 一起使用。这仅适用于基元类型,如 {examples}。另见 `{eqFn}`、`{gtFn}`、`{gteFn}`、`{ltFn}`、`{lteFn}`、`{neqFn}`", + "xpack.canvas.functions.compareHelpText": "将 {CONTEXT} 与指定值进行比较,可确定 {BOOLEAN_TRUE} 或 {BOOLEAN_FALSE}。通常与 `{ifFn}` 或 `{caseFn}` 结合使用。这仅适用于基元类型,如 {examples}。另请参见 {eqFn}、{gtFn}、{gteFn}、{ltFn}、{lteFn}、{neqFn}", "xpack.canvas.functions.containerStyle.args.backgroundColorHelpText": "有效的 {CSS} 背景色。", "xpack.canvas.functions.containerStyle.args.backgroundImageHelpText": "有效的 {CSS} 背景图。", "xpack.canvas.functions.containerStyle.args.backgroundRepeatHelpText": "有效的 {CSS} 背景重复。", @@ -5406,7 +5759,7 @@ "xpack.canvas.functions.containerStyle.args.borderRadiusHelpText": "设置圆角时要使用的像素数。", "xpack.canvas.functions.containerStyle.args.opacityHelpText": "0 和 1 之间的数值,表示元素的透明度。", "xpack.canvas.functions.containerStyle.args.overflowHelpText": "有效的 {CSS} 溢出。", - "xpack.canvas.functions.containerStyle.args.paddingHelpText": "内容与边框的距离(像素)。", + "xpack.canvas.functions.containerStyle.args.paddingHelpText": "内容到边框的距离(像素)。", "xpack.canvas.functions.containerStyle.invalidBackgroundImageErrorMessage": "无效的背景图。请提供资产或 URL。", "xpack.canvas.functions.containerStyleHelpText": "创建用于为元素容器提供样式的对象,包括背景、边框和透明度。", "xpack.canvas.functions.contextHelpText": "返回传入该函数的任何内容。需要将 {CONTEXT} 用作充当子表达式的参数时,这会非常有用。", @@ -5415,7 +5768,7 @@ "xpack.canvas.functions.csv.args.newlineHelpText": "行分隔字符。", "xpack.canvas.functions.csv.invalidInputCSVErrorMessage": "解析输入 CSV 时出错。", "xpack.canvas.functions.csvHelpText": "从 {CSV} 输入创建 {DATATABLE}。", - "xpack.canvas.functions.date.args.formatHelpText": "用于解析指定日期字符串的 {MOMENTJS}。请参见 {url}。", + "xpack.canvas.functions.date.args.formatHelpText": "用于解析指定日期字符串的 {MOMENTJS} 格式。有关更多信息,请参见 {url}。", "xpack.canvas.functions.date.args.valueHelpText": "解析成自 Epoch 起毫秒数的可选日期字符串。日期字符串可以是有效的 {JS} {date} 输入,也可以是要使用 {formatArg} 参数解析的字符串。必须为 {ISO8601} 字符串,或必须提供该格式。", "xpack.canvas.functions.date.invalidDateInputErrorMessage": "无效的日期输入:{date}", "xpack.canvas.functions.dateHelpText": "将当前时间或从指定字符串解析的时间返回为自 Epoch 起毫秒数。", @@ -5426,8 +5779,8 @@ "xpack.canvas.functions.doHelpText": "执行多个子表达式,然后返回原始 {CONTEXT}。用于运行产生操作或副作用时不会更改原始 {CONTEXT} 的函数。", "xpack.canvas.functions.dropdownControl.args.filterColumnHelpText": "要筛选的列或字段。", "xpack.canvas.functions.dropdownControl.args.filterGroupHelpText": "筛选的组名称。", - "xpack.canvas.functions.dropdownControl.args.valueColumnHelpText": "从其中提取下拉列表唯一值的列或字段。", - "xpack.canvas.functions.dropdownControlHelpText": "配置下拉列表筛选控制元素。", + "xpack.canvas.functions.dropdownControl.args.valueColumnHelpText": "从其中提取下拉控件的唯一值的列或字段。", + "xpack.canvas.functions.dropdownControlHelpText": "配置下拉筛选控件元素。", "xpack.canvas.functions.eq.args.valueHelpText": "与 {CONTEXT} 比较的值。", "xpack.canvas.functions.eqHelpText": "返回 {CONTEXT} 是否等于参数。", "xpack.canvas.functions.escount.args.indexHelpText": "索引或索引模式。例如:{example}。", @@ -5456,49 +5809,52 @@ "xpack.canvas.functions.formatdate.args.formatHelpText": "{MOMENTJS} 格式。例如:{example}。请参见 {url}。", "xpack.canvas.functions.formatdateHelpText": "使用 {MOMENTJS} 格式化 {ISO8601} 日期字符串或自 Epoch 起以毫秒表示的日期。请参见 {url}。", "xpack.canvas.functions.formatnumber.args.formatHelpText": "{NUMERALJS} 格式字符串。例如 {example1} 或 {example2}。", - "xpack.canvas.functions.formatnumberHelpText": "使用 {NUMERALJS} 将数字格式化为带格式数字字符串。", + "xpack.canvas.functions.formatnumberHelpText": "使用 {NUMERALJS} 将数字格式化为带格式的数字字符串。", "xpack.canvas.functions.getCell.args.columnHelpText": "从其中提取值的列的名称。如果未提供,将从第一列检索值。", "xpack.canvas.functions.getCell.args.rowHelpText": "行编号,从 0 开始。", "xpack.canvas.functions.getCell.columnNotFoundErrorMessage": "找不到列:“{column}”", "xpack.canvas.functions.getCell.rowNotFoundErrorMessage": "找不到行:“{row}”", - "xpack.canvas.functions.getCellHelpText": "从 {DATATABLE} 提取单个单元格。", + "xpack.canvas.functions.getCellHelpText": "从 {DATATABLE} 中提取单个单元格。", "xpack.canvas.functions.gt.args.valueHelpText": "与 {CONTEXT} 比较的值。", "xpack.canvas.functions.gte.args.valueHelpText": "与 {CONTEXT} 比较的值。", "xpack.canvas.functions.gteHelpText": "返回 {CONTEXT} 是否大于或等于参数。", "xpack.canvas.functions.gtHelpText": "返回 {CONTEXT} 是否大于参数。", "xpack.canvas.functions.head.args.countHelpText": "要从 {DATATABLE} 的起始位置检索的行数目。", - "xpack.canvas.functions.headHelpText": "从 {DATATABLE} 检索前 {n} 行。另见 {tailFn}", + "xpack.canvas.functions.headHelpText": "从 {DATATABLE} 中检索前 {n} 行。另请参见 {tailFn}。", "xpack.canvas.functions.if.args.conditionHelpText": "表示条件是否满足的 {BOOLEAN_TRUE} 或 {BOOLEAN_FALSE},通常由子表达式返回。未指定时,将返回原始 {CONTEXT}。", "xpack.canvas.functions.if.args.elseHelpText": "条件为 {BOOLEAN_FALSE} 时的返回值。未指定且条件未满足时,将返回原始 {CONTEXT}。", "xpack.canvas.functions.if.args.thenHelpText": "条件为 {BOOLEAN_TRUE} 时的返回值。未指定且条件满足时,将返回原始 {CONTEXT}。", - "xpack.canvas.functions.ifHelpText": "执行条件逻辑", + "xpack.canvas.functions.ifHelpText": "执行条件逻辑。", "xpack.canvas.functions.image.args.dataurlHelpText": "图像的 {https} {URL} 或 {BASE64} 数据 {URL}。", "xpack.canvas.functions.image.args.modeHelpText": "{contain} 将显示整个图像,图像缩放至适合大小。{cover} 将使用该图像填充容器,根据需要在两边或底部裁剪图像。{stretch} 将图像的高和宽调整为容器的 100%。", "xpack.canvas.functions.image.invalidImageModeErrorMessage": "“mode”必须为“{contain}”、“{cover}”或“{stretch}”", "xpack.canvas.functions.imageHelpText": "显示图像。将图像资产作为 {BASE64} 数据 {URL} 提供或传入子表达式。", - "xpack.canvas.functions.joinRows.args.columnHelpText": "要用于联接值的列", - "xpack.canvas.functions.joinRows.args.distinctHelpText": "移除重复值?", - "xpack.canvas.functions.joinRows.args.quoteHelpText": "引起值的引号字符", - "xpack.canvas.functions.joinRows.args.separatorHelpText": "用于分隔行值的分隔符", + "xpack.canvas.functions.joinRows.args.columnHelpText": "从其中提取值的列或字段。", + "xpack.canvas.functions.joinRows.args.distinctHelpText": "仅提取唯一值?", + "xpack.canvas.functions.joinRows.args.quoteHelpText": "要将每个提取的值引起来的引号字符。", + "xpack.canvas.functions.joinRows.args.separatorHelpText": "要插在每个提取的值之间的分隔符。", "xpack.canvas.functions.joinRows.columnNotFoundErrorMessage": "找不到列:“{column}”", - "xpack.canvas.functions.joinRowsHelpText": "将数据库中的行的值联接成字符串", + "xpack.canvas.functions.joinRowsHelpText": "将 `datatable` 中各行的值连接成单个字符串。", + "xpack.canvas.functions.locationHelpText": "使用浏览器的 {geolocationAPI} 查找您的当前位置。性能可能有所不同,但相当准确。请参见 {url}。如果计划生成 PDF,请不要使用 {locationFn},因为此函数需要用户输入。", "xpack.canvas.functions.lt.args.valueHelpText": "与 {CONTEXT} 比较的值。", "xpack.canvas.functions.lte.args.valueHelpText": "与 {CONTEXT} 比较的值。", "xpack.canvas.functions.lteHelpText": "返回 {CONTEXT} 是否小于或等于参数。", "xpack.canvas.functions.ltHelpText": "返回 {CONTEXT} 是否小于参数。", "xpack.canvas.functions.mapCenter.args.latHelpText": "地图中心的纬度", - "xpack.canvas.functions.mapCenterHelpText": "返回具有地图中心坐标和缩放级别的对象", + "xpack.canvas.functions.mapCenterHelpText": "返回包含地图中心坐标和缩放级别的对象。", "xpack.canvas.functions.mapColumn.args.expressionHelpText": "作为单行 {DATATABLE} 传递到每一行的 {CANVAS} 表达式。", "xpack.canvas.functions.mapColumn.args.nameHelpText": "结果列的名称。", + "xpack.canvas.functions.mapColumnHelpText": "添加计算为其他列的结果的列。只有提供参数时,才会执行更改。另请参见 {alterColumnFn} 和 {staticColumnFn}。", "xpack.canvas.functions.markdown.args.contentHelpText": "包含 {MARKDOWN} 的文本字符串。要进行串联,请传递 {stringFn} 函数多次。", "xpack.canvas.functions.markdown.args.fontHelpText": "内容的 {CSS} 字体属性。例如:{fontFamily} 或 {fontWeight}。", - "xpack.canvas.functions.markdown.args.openLinkHelpText": "表示是否在新选项卡中打开链接的 true/false 值。默认值为 false。设置为 true 将在新选项卡中打开所有链接。", + "xpack.canvas.functions.markdown.args.openLinkHelpText": "用于在新标签页中打开链接的 true 或 false 值。默认值为 `false`。设置为 `true` 时将在新标签页中打开所有链接。", "xpack.canvas.functions.markdownHelpText": "添加呈现 {MARKDOWN} 文本的元素。提示:将 {markdownFn} 函数用于单个数字、指标和文本段落。", "xpack.canvas.functions.math.args.expressionHelpText": "已计算的 {TINYMATH} 表达式。请参阅 {TINYMATH_URL}。", "xpack.canvas.functions.math.emptyDatatableErrorMessage": "空数据表", "xpack.canvas.functions.math.emptyExpressionErrorMessage": "空表达式", "xpack.canvas.functions.math.executionFailedErrorMessage": "无法执行数学表达式。检查您的列名称", "xpack.canvas.functions.math.tooManyResultsErrorMessage": "表达式必须返回单个数字。尝试将您的表达式包装在 {mean} 或 {sum} 中", + "xpack.canvas.functions.mathHelpText": "使用 {TYPE_NUMBER} 或 {DATATABLE} 作为 {CONTEXT} 来解释 {TINYMATH} 数学表达式。{DATATABLE} 列按列名使用。如果 {CONTEXT} 是数字,则作为 {value} 使用。", "xpack.canvas.functions.metric.args.labelFontHelpText": "标签的 {CSS} 字体属性。例如 {FONT_FAMILY} 或 {FONT_WEIGHT}。", "xpack.canvas.functions.metric.args.labelHelpText": "描述指标的文本。", "xpack.canvas.functions.metric.args.metricFontHelpText": "指标的 {CSS} 字体属性。例如 {FONT_FAMILY} 或 {FONT_WEIGHT}。", @@ -5514,21 +5870,25 @@ "xpack.canvas.functions.pie.args.holeHelpText": "在饼图中绘制介于 `0` and `100`(饼图半径的百分比)之间的孔洞。", "xpack.canvas.functions.pie.args.labelRadiusHelpText": "要用作标签圆形半径的容器面积百分比。", "xpack.canvas.functions.pie.args.labelsHelpText": "显示饼图标签?", + "xpack.canvas.functions.pie.args.legendHelpText": "图例位置。例如 {legend} 或 {BOOLEAN_FALSE}。如果是 {BOOLEAN_FALSE},则图例处于隐藏状态。", + "xpack.canvas.functions.pie.args.paletteHelpText": "用于描述要在此饼图中使用的颜色的 {palette} 对象。", "xpack.canvas.functions.pie.args.radiusHelpText": "饼图的半径,表示为可用空间的百分比(介于 `0` 和 `1` 之间)。要自动设置半径,请使用 {auto}。", "xpack.canvas.functions.pie.args.seriesStyleHelpText": "特定序列的样式", "xpack.canvas.functions.pie.args.tiltHelpText": "倾斜百分比,其中 `1` 为完全垂直,`0` 为完全水平。", "xpack.canvas.functions.pieHelpText": "配置饼图元素。", "xpack.canvas.functions.plot.args.defaultStyleHelpText": "要用于每个序列的默认样式。", "xpack.canvas.functions.plot.args.fontHelpText": "标签的 {CSS} 字体属性。例如 {FONT_FAMILY} 或 {FONT_WEIGHT}。", + "xpack.canvas.functions.plot.args.legendHelpText": "图例位置。例如 {legend} 或 {BOOLEAN_FALSE}。如果是 {BOOLEAN_FALSE},则图例处于隐藏状态。", + "xpack.canvas.functions.plot.args.paletteHelpText": "用于描述要在此图表中使用的颜色的 {palette} 对象。", "xpack.canvas.functions.plot.args.seriesStyleHelpText": "特定序列的样式", "xpack.canvas.functions.plot.args.xaxisHelpText": "轴配置。为 {BOOLEAN_FALSE} 时,轴隐藏。", "xpack.canvas.functions.plot.args.yaxisHelpText": "轴配置。为 {BOOLEAN_FALSE} 时,轴隐藏。", - "xpack.canvas.functions.plotHelpText": "配置图表元素", + "xpack.canvas.functions.plotHelpText": "配置图表元素。", "xpack.canvas.functions.ply.args.byHelpText": "用于细分 {DATATABLE} 的列。", "xpack.canvas.functions.ply.args.expressionHelpText": "要将每个结果 {DATATABLE} 传入的表达式。提示:表达式必须返回 {DATATABLE}。使用 `{asFn}` 将文本转成 {DATATABLE}。多个表达式必须返回相同数量的行。如果需要返回不同的行数,请导向另一个 {plyFn} 实例。如果多个表达式返回同名的列,最后一列将胜出。", "xpack.canvas.functions.ply.columnNotFoundErrorMessage": "找不到列:“{by}”", "xpack.canvas.functions.ply.rowCountMismatchErrorMessage": "所有表达式必须返回相同数目的行", - "xpack.canvas.functions.plyHelpText": "通过指定列的唯一值细分 {DATATABLE},并将生成的表传入表达式,然后合并每个表达式的输出", + "xpack.canvas.functions.plyHelpText": "按指定列的唯一值细分 {DATATABLE},并将生成的表传入表达式,然后合并每个表达式的输出。", "xpack.canvas.functions.pointseries.args.colorHelpText": "要用于确定标记颜色的表达式。", "xpack.canvas.functions.pointseries.args.sizeHelpText": "标记的大小。仅适用于支持的元素。", "xpack.canvas.functions.pointseries.args.textHelpText": "要在标记上显示的文本。仅适用于支持的元素。", @@ -5539,7 +5899,7 @@ "xpack.canvas.functions.progress.args.barColorHelpText": "背景条形的颜色。", "xpack.canvas.functions.progress.args.barWeightHelpText": "背景条形的粗细。", "xpack.canvas.functions.progress.args.fontHelpText": "标签的 {CSS} 字体属性。例如 {FONT_FAMILY} 或 {FONT_WEIGHT}。", - "xpack.canvas.functions.progress.args.labelHelpText": "要显示或隐藏标签,请使用 {BOOLEAN_TRUE} 或 {BOOLEAN_FALSE}。或者,提供要显示为标签的字符串。", + "xpack.canvas.functions.progress.args.labelHelpText": "要显示或隐藏标签,请使用 {BOOLEAN_TRUE} 或 {BOOLEAN_FALSE}。或者,提供字符串以显示为标签。", "xpack.canvas.functions.progress.args.maxHelpText": "进度元素的最大值。", "xpack.canvas.functions.progress.args.shapeHelpText": "选择 {list} 或 {end}。", "xpack.canvas.functions.progress.args.valueColorHelpText": "进度条的颜色。", @@ -5550,8 +5910,8 @@ "xpack.canvas.functions.render.args.asHelpText": "要呈现的元素类型。您可能需要专门的函数,例如 {plotFn} 或 {shapeFn}。", "xpack.canvas.functions.render.args.containerStyleHelpText": "容器的样式,包括背景、边框和透明度。", "xpack.canvas.functions.render.args.cssHelpText": "要限定于元素的任何定制 {CSS} 块。", - "xpack.canvas.functions.renderHelpText": "将 {CONTEXT} 呈现为特定元素并设置元素级别选项,例如背景和边框样式。", - "xpack.canvas.functions.repeatImage.args.emptyImageHelpText": "使用图像在 {CONTEXT} 和元素的 {maxArg} 参数之间填充差异。以 {BASE64} 数据 {URL} 方式提供图像资产,或传入子表达式。", + "xpack.canvas.functions.renderHelpText": "将 {CONTEXT} 呈现为特定元素,并设置元素级别选项,例如背景和边框样式。", + "xpack.canvas.functions.repeatImage.args.emptyImageHelpText": "使用此图像填充元素的 {CONTEXT} 和 {maxArg} 参数之间的差距。以 {BASE64} 数据 {URL} 的形式提供图像资产或传入子表达式。", "xpack.canvas.functions.repeatImage.args.imageHelpText": "要重复的图像。将图像资产作为 {BASE64} 数据 {URL} 提供或传入子表达式。", "xpack.canvas.functions.repeatImage.args.maxHelpText": "图像可以重复的最大次数。", "xpack.canvas.functions.repeatImage.args.sizeHelpText": "图像的最大高度或宽度,以像素为单位。图像的高大于宽时,此函数将限制高度。", @@ -5559,19 +5919,19 @@ "xpack.canvas.functions.replace.args.flagsHelpText": "指定标志。请参见 {url}。", "xpack.canvas.functions.replace.args.patternHelpText": "{JS} 正则表达式的文本或模式。例如:{example}。您可以在此处使用捕获组。", "xpack.canvas.functions.replace.args.replacementHelpText": "字符串匹配部分的替代。捕获组可以通过其索引进行访问。例如:{example}。", - "xpack.canvas.functions.replaceImageHelpText": "使用正则表达式替换字符串各部分。", + "xpack.canvas.functions.replaceImageHelpText": "使用正则表达式替换字符串的各部分。", "xpack.canvas.functions.revealImage.args.emptyImageHelpText": "要显示的可选背景图像。将图像资产作为 {BASE64} 数据 {URL} 提供或传入子表达式。", "xpack.canvas.functions.revealImage.args.imageHelpText": "要显示的图像。将图像资产作为 {BASE64} 数据 {URL} 提供或传入子表达式。", "xpack.canvas.functions.revealImage.args.originHelpText": "要开始图像填充的位置。例如 {list} 或 {end}。", "xpack.canvas.functions.revealImage.invalidPercentErrorMessage": "无效的值:“{percent}”。百分比必须介于 0 和 1 之间", "xpack.canvas.functions.revealImageHelpText": "配置图像显示元素。", - "xpack.canvas.functions.rounddate.args.formatHelpText": "用于存储桶存储的 {MOMENTJS} 格式。例如,{example} 将每个日期舍入到月份。请参见 {url}。", + "xpack.canvas.functions.rounddate.args.formatHelpText": "用于存储桶存储的 {MOMENTJS} 格式。例如,{example} 四舍五入到月份。请参见 {url}。", "xpack.canvas.functions.rounddateHelpText": "使用 {MOMENTJS} 格式字符串舍入自 Epoch 起毫秒数,并返回自 Epoch 起毫秒数。", - "xpack.canvas.functions.rowCountHelpText": "返回行数。与 {plyFn} 搭配,可获取唯一行值的计数或唯一行值的组合。", - "xpack.canvas.functions.savedLens.args.idHelpText": "已保存 Lens 对象的 ID", + "xpack.canvas.functions.rowCountHelpText": "返回行数。与 {plyFn} 搭配使用,可获取唯一列值的计数或唯一列值的组合。", + "xpack.canvas.functions.savedLens.args.idHelpText": "已保存 Lens 可视化对象的 ID", "xpack.canvas.functions.savedLens.args.timerangeHelpText": "应包括的数据的时间范围", - "xpack.canvas.functions.savedLens.args.titleHelpText": "lens 可嵌入对象的标题", - "xpack.canvas.functions.savedLensHelpText": "为已保存 Lens 对象返回可嵌入对象", + "xpack.canvas.functions.savedLens.args.titleHelpText": "Lens 可视化对象的标题", + "xpack.canvas.functions.savedLensHelpText": "返回已保存 Lens 可视化对象的可嵌入对象。", "xpack.canvas.functions.savedMap.args.centerHelpText": "地图应具有的中心和缩放级别", "xpack.canvas.functions.savedMap.args.hideLayer": "应隐藏的地图图层的 ID", "xpack.canvas.functions.savedMap.args.idHelpText": "已保存地图对象的 ID", @@ -5579,20 +5939,21 @@ "xpack.canvas.functions.savedMap.args.timerangeHelpText": "应包括的数据的时间范围", "xpack.canvas.functions.savedMap.args.titleHelpText": "地图的标题", "xpack.canvas.functions.savedMap.args.zoomHelpText": "地图的缩放级别", - "xpack.canvas.functions.savedMapHelpText": "为已保存地图对象返回可嵌入对象", + "xpack.canvas.functions.savedMapHelpText": "返回已保存地图对象的可嵌入对象。", "xpack.canvas.functions.savedSearchHelpText": "为已保存搜索对象返回可嵌入对象", "xpack.canvas.functions.savedVisualization.args.colorsHelpText": "定义用于特定序列的颜色", - "xpack.canvas.functions.savedVisualization.args.hideLegendHelpText": "图例是否应隐藏", + "xpack.canvas.functions.savedVisualization.args.hideLegendHelpText": "指定用于隐藏图例的选项", "xpack.canvas.functions.savedVisualization.args.idHelpText": "已保存可视化对象的 ID", "xpack.canvas.functions.savedVisualization.args.timerangeHelpText": "应包括的数据的时间范围", - "xpack.canvas.functions.savedVisualizationHelpText": "为已保存可视化对象返回可嵌入对象", + "xpack.canvas.functions.savedVisualization.args.titleHelpText": "可视化对象的标题", + "xpack.canvas.functions.savedVisualizationHelpText": "返回已保存可视化对象的可嵌入对象。", "xpack.canvas.functions.seriesStyle.args.barsHelpText": "条形的宽度。", "xpack.canvas.functions.seriesStyle.args.colorHelpText": "线条颜色。", "xpack.canvas.functions.seriesStyle.args.fillHelpText": "应该填入点吗?", "xpack.canvas.functions.seriesStyle.args.horizontalBarsHelpText": "将图表中的条形方向设置为横向。", "xpack.canvas.functions.seriesStyle.args.labelHelpText": "要加上样式的序列的名称。", "xpack.canvas.functions.seriesStyle.args.linesHelpText": "线条的宽度。", - "xpack.canvas.functions.seriesStyle.args.pointsHelpText": "折线上的点大小", + "xpack.canvas.functions.seriesStyle.args.pointsHelpText": "折线图上的点大小。", "xpack.canvas.functions.seriesStyle.args.stackHelpText": "指定是否应堆叠序列。编号为堆叠 ID。具有相同堆叠 ID 的序列将堆叠在一起。", "xpack.canvas.functions.seriesStyleHelpText": "创建用于在图表上描述序列属性的对象。在图表绘制函数(如 {plotFn} 或 {pieFn})中使用 {seriesStyleFn}。", "xpack.canvas.functions.shape.args.borderHelpText": "形状轮廓边框的 {SVG} 颜色。", @@ -5601,21 +5962,22 @@ "xpack.canvas.functions.shape.args.maintainAspectHelpText": "维持形状的原始纵横比?", "xpack.canvas.functions.shape.args.shapeHelpText": "选取形状。", "xpack.canvas.functions.shapeHelpText": "创建形状。", - "xpack.canvas.functions.sort.args.byHelpText": "排序要依据的列。未指定时,将按第一列排序 `{DATATABLE}`。", - "xpack.canvas.functions.sort.args.reverseHelpText": "反转排序顺序。未指定时,将升序排序 `{DATATABLE}`。", + "xpack.canvas.functions.sort.args.byHelpText": "排序依据的列。如果未指定,则 {DATATABLE} 按第一列排序。", + "xpack.canvas.functions.sort.args.reverseHelpText": "反转排序顺序。如果未指定,则 {DATATABLE} 按升序排序。", + "xpack.canvas.functions.sortHelpText": "按指定列对 {DATATABLE} 进行排序。", "xpack.canvas.functions.staticColumn.args.nameHelpText": "新列的名称。", - "xpack.canvas.functions.staticColumn.args.valueHelpText": "在每一行新列中要插入的值。提示:使用子表达式将其他列汇总为静态值。", - "xpack.canvas.functions.staticColumnHelpText": "在每一行添加具有相同静态值的列。另见 {alterColumnFn} 和 {mapColumnFn}。", + "xpack.canvas.functions.staticColumn.args.valueHelpText": "要在新列的每一行中插入的值。提示:使用子表达式可将其他列汇总为静态值。", + "xpack.canvas.functions.staticColumnHelpText": "添加每一行都具有相同静态值的列。另请参见 {alterColumnFn} 和 {mapColumnFn}。", "xpack.canvas.functions.string.args.valueHelpText": "要连结成一个字符串的值。根据需要加入空格。", "xpack.canvas.functions.stringHelpText": "将所有参数串联成单个字符串。", - "xpack.canvas.functions.switch.args.caseHelpText": "要检查的条件", + "xpack.canvas.functions.switch.args.caseHelpText": "要检查的条件。", "xpack.canvas.functions.switch.args.defaultHelpText": "未满足任何条件时返回的值。未指定且没有条件满足时,将返回原始 {CONTEXT}。", - "xpack.canvas.functions.switchHelpText": "执行具有多个条件的条件逻辑。另见 {caseFn},其用于构建要传递到 {switchFn} 函数的 {case}。", + "xpack.canvas.functions.switchHelpText": "执行具有多个条件的条件逻辑。另请参见 {caseFn},该函数用于构建要传递到 {switchFn} 函数的 {case}。", "xpack.canvas.functions.table.args.fontHelpText": "表内容的 {CSS} 字体属性。例如 {FONT_FAMILY} 或 {FONT_WEIGHT}。", "xpack.canvas.functions.table.args.paginateHelpText": "显示分页控件?为 {BOOLEAN_FALSE} 时,仅显示第一页。", "xpack.canvas.functions.table.args.perPageHelpText": "要在每页上显示的行数目。", - "xpack.canvas.functions.table.args.showHeaderHelpText": "显示/隐藏具有每列标题的标题行。", - "xpack.canvas.functions.tableHelpText": "配置表元素", + "xpack.canvas.functions.table.args.showHeaderHelpText": "显示或隐藏包含每列标题的标题行。", + "xpack.canvas.functions.tableHelpText": "配置表元素。", "xpack.canvas.functions.tail.args.countHelpText": "要从 {DATATABLE} 的结尾位置检索的行数目。", "xpack.canvas.functions.tailHelpText": "从 {DATATABLE} 结尾检索后 N 行。另见 {headFn}。", "xpack.canvas.functions.timefilter.args.columnHelpText": "要筛选的列或字段。", @@ -5632,16 +5994,16 @@ "xpack.canvas.functions.timelion.args.query": "Timelion 查询", "xpack.canvas.functions.timelion.args.timezone": "时间范围的时区。请参阅 {MOMENTJS_TIMEZONE_URL}。", "xpack.canvas.functions.timelion.args.to": "表示时间范围结束的 {ELASTICSEARCH} {DATEMATH} 字符串。", - "xpack.canvas.functions.timelionHelpText": "使用 Timelion 从多个源中提取一个或多个时间序列。", + "xpack.canvas.functions.timelionHelpText": "使用 Timelion 可从多个源中提取一个或多个时间序列。", "xpack.canvas.functions.timerange.args.fromHelpText": "时间范围起始", "xpack.canvas.functions.timerange.args.toHelpText": "时间范围结束", - "xpack.canvas.functions.timerangeHelpText": "表示时间跨度的对象", + "xpack.canvas.functions.timerangeHelpText": "表示时间跨度的对象。", "xpack.canvas.functions.to.args.type": "表达式语言中的已知数据类型。", "xpack.canvas.functions.to.missingType": "必须指定转换类型", - "xpack.canvas.functions.toHelpText": "将 {CONTEXT} 的类型显式转换为指定类型。", + "xpack.canvas.functions.toHelpText": "将 {CONTEXT} 的类型从一种类型显式转换为指定类型。", "xpack.canvas.functions.urlparam.args.defaultHelpText": "未指定 {URL} 参数时返回的值。", "xpack.canvas.functions.urlparam.args.paramHelpText": "要检索的 {URL} 哈希参数。", - "xpack.canvas.functions.urlparamHelpText": "检索要在表达式中使用的 {URL} 参数。{urlparamFn} 函数始终返回 {TYPE_STRING}。例如,从 {URL} 的参数 {myVar} 检索值 {value} ({example})。", + "xpack.canvas.functions.urlparamHelpText": "检索要在表达式中使用的 {URL} 参数。{urlparamFn} 函数始终返回 {TYPE_STRING}。例如,可从 {URL} {example} 中检索参数 {myVar} 的值 {value}。", "xpack.canvas.groupSettings.multipleElementsActionsDescription": "取消选择这些元素以编辑各自的设置,按 ({gKey}) 以对它们进行分组,或将此选择另存为新元素,以在整个 Workpad 中重复使用。", "xpack.canvas.groupSettings.multipleElementsDescription": "当前选择了多个元素。", "xpack.canvas.groupSettings.saveGroupDescription": "将此组另存为新元素,以在整个 Workpad 重复使用。", @@ -5716,7 +6078,11 @@ "xpack.canvas.pageConfig.transitionLabel": "切换", "xpack.canvas.pageConfig.transitionPreviewLabel": "预览", "xpack.canvas.pageConfig.transitions.noneDropDownOptionLabel": "无", + "xpack.canvas.pageManager.addPageTooltip": "将新页面添加到此 Workpad", + "xpack.canvas.pageManager.confirmRemoveDescription": "确定要移除此页面?", + "xpack.canvas.pageManager.confirmRemoveTitle": "移除页面", "xpack.canvas.pageManager.pageNumberAriaLabel": "加载页码 {pageNumber}", + "xpack.canvas.pageManager.removeButtonLabel": "移除", "xpack.canvas.pagePreviewPageControls.clonePageAriaLabel": "克隆页面", "xpack.canvas.pagePreviewPageControls.clonePageTooltip": "克隆", "xpack.canvas.pagePreviewPageControls.deletePageAriaLabel": "删除页面", @@ -5825,6 +6191,7 @@ "xpack.canvas.textStylePicker.styleUnderlineOption": "下划线", "xpack.canvas.timePicker.applyButtonLabel": "应用", "xpack.canvas.toolbar.editorButtonLabel": "表达式编辑器", + "xpack.canvas.toolbar.errorMessage": "工具栏错误:{message}", "xpack.canvas.toolbar.nextPageAriaLabel": "下一页", "xpack.canvas.toolbar.pageButtonLabel": "页 {pageNum}{rest}", "xpack.canvas.toolbar.previousPageAriaLabel": "上一页", @@ -6584,6 +6951,8 @@ "xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDeprecation": "此设置已过时,将在 Kibana 8.0 中移除。", "xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDescription": "属于“仅查看仪表板”模式的角色", "xpack.dashboardMode.uiSettings.dashboardsOnlyRolesTitle": "仅限仪表板的角色", + "xpack.data.advancedSettings.searchTimeout": "搜索超时", + "xpack.data.advancedSettings.searchTimeoutDesc": "更改搜索会话的最大超时值,或设置为 0 以禁用超时,让查询运行至结束。", "xpack.data.kueryAutocomplete.andOperatorDescription": "需要{bothArguments}为 true", "xpack.data.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "两个参数都", "xpack.data.kueryAutocomplete.equalOperatorDescription": "{equals}某一值", @@ -6616,11 +6985,26 @@ "xpack.enterpriseSearch.appSearch.enginesOverview.table.column.fieldCount": "字段计数", "xpack.enterpriseSearch.appSearch.enginesOverview.table.column.name": "名称", "xpack.enterpriseSearch.appSearch.enginesOverview.title": "引擎概览", + "xpack.enterpriseSearch.appSearch.nav.credentials": "凭据", + "xpack.enterpriseSearch.appSearch.nav.engines": "引擎", + "xpack.enterpriseSearch.appSearch.nav.roleMappings": "角色映射", + "xpack.enterpriseSearch.appSearch.nav.settings": "帐户设置", + "xpack.enterpriseSearch.appSearch.productCardDescription": "Elastic App Search 提供用户友好的工具,用于设计强大的搜索功能,并将其部署到您的网站或 Web/移动应用程序。", "xpack.enterpriseSearch.appSearch.productCta": "启动 App Search", + "xpack.enterpriseSearch.appSearch.productDescription": "利用仪表板、分析和 API 执行高级应用程序搜索简单易行。", "xpack.enterpriseSearch.appSearch.productName": "App Search", "xpack.enterpriseSearch.appSearch.setupGuide.description": "Elastic App Search 提供的工具用于设计强大的搜索并将其部署到您的网站和移动应用程序。", "xpack.enterpriseSearch.appSearch.setupGuide.notConfigured": "App Search 在您的 Kibana 实例中尚未得到配置。", "xpack.enterpriseSearch.appSearch.setupGuide.videoAlt": "App Search 入门 - 在此视频中,我们将指导您如何开始使用 App Search", + "xpack.enterpriseSearch.appSearch.tokens.admin.description": "私有管理员密钥用于与凭据 API 进行交互。", + "xpack.enterpriseSearch.appSearch.tokens.admin.name": "私有管理员密钥", + "xpack.enterpriseSearch.appSearch.tokens.private.description": "私有 API 密钥用于对一个或多个引擎执行读和/写访问。", + "xpack.enterpriseSearch.appSearch.tokens.private.name": "私有 API 密钥", + "xpack.enterpriseSearch.appSearch.tokens.search.description": "公有搜索密钥仅用于搜索终端。", + "xpack.enterpriseSearch.appSearch.tokens.search.name": "公有搜索密钥", + "xpack.enterpriseSearch.enterpriseSearch.setupGuide.description": "随时随地进行全面搜索。为工作繁忙的团队轻松实现强大的现代搜索体验。将预先调整的搜索功能快速添加到您的网站、应用或工作区。全面搜索就是这么简单。", + "xpack.enterpriseSearch.enterpriseSearch.setupGuide.notConfigured": "企业搜索尚未在您的 Kibana 实例中配置。", + "xpack.enterpriseSearch.enterpriseSearch.setupGuide.videoAlt": "企业搜索入门", "xpack.enterpriseSearch.errorConnectingState.description1": "我们无法与以下主机 URL 的企业搜索建立连接:{enterpriseSearchUrl}", "xpack.enterpriseSearch.errorConnectingState.description2": "确保在 {configFile} 中已正确配置主机 URL。", "xpack.enterpriseSearch.errorConnectingState.description3": "确认企业搜索服务器响应。", @@ -6630,6 +7014,26 @@ "xpack.enterpriseSearch.errorConnectingState.troubleshootAuth": "检查您的用户身份验证:", "xpack.enterpriseSearch.errorConnectingState.troubleshootAuthNative": "必须使用 Elasticsearch 本机身份验证或 SSO/SAML 执行身份验证。", "xpack.enterpriseSearch.errorConnectingState.troubleshootAuthSAML": "如果使用的是 SSO/SAML,则还必须在“企业搜索”中设置 SAML 领域。", + "xpack.enterpriseSearch.FeatureCatalogue.description": "使用一组优化的 API 和工具打造搜索体验。", + "xpack.enterpriseSearch.featureCatalogue.subtitle": "全面搜索", + "xpack.enterpriseSearch.featureCatalogueDescription1": "打造强大的搜索体验。", + "xpack.enterpriseSearch.featureCatalogueDescription2": "将您的用户连接到相关数据。", + "xpack.enterpriseSearch.featureCatalogueDescription3": "统一您的团队内容。", + "xpack.enterpriseSearch.nav.hierarchy": "次级", + "xpack.enterpriseSearch.nav.menu": "菜单", + "xpack.enterpriseSearch.nav.toggleMenu": "切换次级导航", + "xpack.enterpriseSearch.navTitle": "概览", + "xpack.enterpriseSearch.notFound.action1": "返回到您的仪表板", + "xpack.enterpriseSearch.notFound.action2": "联系支持人员", + "xpack.enterpriseSearch.notFound.description": "找不到您要查找的页面。", + "xpack.enterpriseSearch.notFound.title": "404 错误", + "xpack.enterpriseSearch.overview.heading": "欢迎使用 Elastic 企业搜索", + "xpack.enterpriseSearch.overview.productCard.heading": "Elastic {productName}", + "xpack.enterpriseSearch.overview.productCard.launchButton": "推出 {productName}", + "xpack.enterpriseSearch.overview.productCard.setupButton": "设置 {productName}", + "xpack.enterpriseSearch.overview.subheading": "选择产品开始使用", + "xpack.enterpriseSearch.productName": "企业搜索", + "xpack.enterpriseSearch.readOnlyMode.warning": "企业搜索处于只读模式。您将无法执行更改,例如创建、编辑或删除。", "xpack.enterpriseSearch.setupGuide.step1.instruction1": "在 {configFile} 文件中,将 {configSetting} 设置为 {productName} 实例的 URL。例如:", "xpack.enterpriseSearch.setupGuide.step1.title": "将 {productName} 主机 URL 添加到 Kibana 配置", "xpack.enterpriseSearch.setupGuide.step2.instruction1": "重新启动 Kibana 以应用上一步骤中的配置更改。", @@ -6645,6 +7049,82 @@ "xpack.enterpriseSearch.troubleshooting.standardAuth.title": "不支持使用标准身份验证的 {productName}", "xpack.enterpriseSearch.workplaceSearch.activityFeedEmptyDefault.title": "您的组织最近无活动", "xpack.enterpriseSearch.workplaceSearch.activityFeedNamedDefault.title": "{name} 最近无活动", + "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.cancel.action": "取消", + "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.heading": "添加组", + "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.submit.action": "添加组", + "xpack.enterpriseSearch.workplaceSearch.groups.addGroupForm.action": "创建组", + "xpack.enterpriseSearch.workplaceSearch.groups.clearFilters.action": "清除筛选", + "xpack.enterpriseSearch.workplaceSearch.groups.contentSourceCountHeading": "{numSources} 个共享内容源", + "xpack.enterpriseSearch.workplaceSearch.groups.description": "将共享内容源和用户分配到组,以便为各种内部团队打造相关搜索体验。", + "xpack.enterpriseSearch.workplaceSearch.groups.filterGroups.placeholder": "按名称筛选组......", + "xpack.enterpriseSearch.workplaceSearch.groups.filterSources.buttonText": "源", + "xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.buttonText": "用户", + "xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.placeholder": "筛选用户......", + "xpack.enterpriseSearch.workplaceSearch.groups.groupDeleted": "组“{groupName}”已成功删除。", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerCancel": "取消", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerHeaderTitle": "管理 {label}", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSelectAllToggle": "全部{action}", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSourceEmpty.body": "可能您尚未添加任何共享内容源。", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSourceEmpty.title": "哎哟!", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerUpdate": "更新", + "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerUpdateAddSourceButton": "添加共享源", + "xpack.enterpriseSearch.workplaceSearch.groups.groupNotFound": "找不到 ID 为“{groupId}”的组。", + "xpack.enterpriseSearch.workplaceSearch.groups.groupPrioritizationUpdated": "已成功更新共享源的优先级排序", + "xpack.enterpriseSearch.workplaceSearch.groups.groupRenamed": "已将此组成功重命名为“{groupName}”。", + "xpack.enterpriseSearch.workplaceSearch.groups.groupSourcesUpdated": "已成功更新共享内容源。", + "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader": "组", + "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.sourcesTableHeader": "内容源", + "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader": "用户", + "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.emailTableHeader": "电子邮件", + "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.usernameTableHeader": "用户名", + "xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText": "上次更新于 {updatedAt}。", + "xpack.enterpriseSearch.workplaceSearch.groups.groupUsersUpdated": "已成功更新此组的用户", + "xpack.enterpriseSearch.workplaceSearch.groups.heading": "管理组", + "xpack.enterpriseSearch.workplaceSearch.groups.inviteUsers.action": "邀请用户", + "xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action": "管理组", + "xpack.enterpriseSearch.workplaceSearch.groups.newGroupSavedSuccess": "已成功创建 {groupName}", + "xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "无共享内容源", + "xpack.enterpriseSearch.workplaceSearch.groups.noUsersFound": "找不到用户", + "xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "无用户", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.cancelRemoveButtonText": "取消", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "删除 {name}", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "您的组将从 Workplace Search 中删除。确定要移除 {name}?", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "确认", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription": "未与此组共享任何内容源。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptyUsersDescription": "此组中没有用户。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesDescription": "可按“{name}”组中的所有用户搜索。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupUsersDescription": "成员将可以对该组的源进行搜索。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.manageSourcesButtonText": "管理共享内容源", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.manageUsersButtonText": "管理用户", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.nameSectionDescription": "定制此组的名称。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.nameSectionTitle": "组名", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.removeButtonText": "移除组", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.removeSectionDescription": "此操作无法撤消。", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.removeSectionTitle": "移除此组", + "xpack.enterpriseSearch.workplaceSearch.groups.overview.saveNameButtonText": "保存名称", + "xpack.enterpriseSearch.workplaceSearch.groups.searchResults.notFoound": "找不到结果。", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.headerActionText": "保存", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.headerDescription": "校准组内容源的相对文档重要性。", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.headerTitle": "共享内容源的优先级排序", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.priorityTableHeader": "相关性优先级", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.sourceTableHeader": "源", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.zeroStateBody": "与 {groupName} 共享两个或多个源,以定制源优先级排序。", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.zeroStateButtonText": "添加共享内容源", + "xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.zeroStateTitle": "未与此组共享任何源", + "xpack.enterpriseSearch.workplaceSearch.groups.sourcesModalLabel": "共享内容源", + "xpack.enterpriseSearch.workplaceSearch.groups.sourcesModalTitle": "选择要与 {groupName} 共享的内容源", + "xpack.enterpriseSearch.workplaceSearch.groups.userListCount": "正在显示 {numUsers} 个用户中的 {maxVisibleUsers} 个。", + "xpack.enterpriseSearch.workplaceSearch.groups.usersModalLabel": "用户", + "xpack.enterpriseSearch.workplaceSearch.headerActions.searchApplication": "前往搜索应用程序", + "xpack.enterpriseSearch.workplaceSearch.nav.groups": "组", + "xpack.enterpriseSearch.workplaceSearch.nav.groups.groupOverview": "概览", + "xpack.enterpriseSearch.workplaceSearch.nav.groups.sourcePrioritization": "源的优先级排序", + "xpack.enterpriseSearch.workplaceSearch.nav.overview": "概览", + "xpack.enterpriseSearch.workplaceSearch.nav.personalDashboard": "查看我的个人仪表板", + "xpack.enterpriseSearch.workplaceSearch.nav.roleMappings": "角色映射", + "xpack.enterpriseSearch.workplaceSearch.nav.security": "安全", + "xpack.enterpriseSearch.workplaceSearch.nav.settings": "设置", + "xpack.enterpriseSearch.workplaceSearch.nav.sources": "源", "xpack.enterpriseSearch.workplaceSearch.organizationStats.activeUsers": "活动用户", "xpack.enterpriseSearch.workplaceSearch.organizationStats.invitations": "邀请", "xpack.enterpriseSearch.workplaceSearch.organizationStats.privateSources": "专用源", @@ -6661,7 +7141,9 @@ "xpack.enterpriseSearch.workplaceSearch.overviewOnboardingUsersCard.description": "邀请同事加入此组织以便一同搜索。", "xpack.enterpriseSearch.workplaceSearch.overviewOnboardingUsersCard.title": "用户和邀请", "xpack.enterpriseSearch.workplaceSearch.overviewUsersCard.title": "很好,您已邀请同事一同搜索。", + "xpack.enterpriseSearch.workplaceSearch.productCardDescription": "通过即时连接到常见生产力和协作工具,将团队的所有内容统一放在一个位置。", "xpack.enterpriseSearch.workplaceSearch.productCta": "启动 Workplace Search", + "xpack.enterpriseSearch.workplaceSearch.productDescription": "搜索整个虚拟工作区中存在的所有文档、文件和源。", "xpack.enterpriseSearch.workplaceSearch.productName": "Workplace Search", "xpack.enterpriseSearch.workplaceSearch.recentActivity.title": "最近活动", "xpack.enterpriseSearch.workplaceSearch.recentActivitySourceLink.linkLabel": "查看源", @@ -6671,6 +7153,7 @@ "xpack.enterpriseSearch.workplaceSearch.sourcesOnboardingCard.buttonLabel": "添加 {label} 源", "xpack.enterpriseSearch.workplaceSearch.sourcesOnboardingCard.description": "您已添加 {sourcesCount, number} 个共享{sourcesCount, plural, one {源} other {源}}。祝您搜索愉快。", "xpack.enterpriseSearch.workplaceSearch.usersOnboardingCard.buttonLabel": "邀请 {label} 用户", + "xpack.eventLog.savedObjectProviderRegistry.getProvidersClient.noDefaultProvider": "事件日志需要默认提供程序。", "xpack.features.advancedSettingsFeatureName": "高级设置", "xpack.features.dashboardFeatureName": "仪表板", "xpack.features.devToolsFeatureName": "开发工具", @@ -6739,6 +7222,15 @@ "xpack.fileUpload.noIndexSuppliedErrorMessage": "未提供任何索引。", "xpack.fileUpload.patternReader.featuresOmitted": "不具有几何形状的一些特征已省略", "xpack.globalSearch.find.invalidLicenseError": "GlobalSearch API 已禁用,因为许可状态无效:{errorMessage}", + "xpack.globalSearchBar.searchBar.mobileSearchButtonAriaLabel": "全站点搜索", + "xpack.globalSearchBar.searchBar.noResults": "尝试搜索应用程序、仪表板和可视化等。", + "xpack.globalSearchBar.searchBar.noResultsHeading": "找不到结果", + "xpack.globalSearchBar.searchBar.noResultsImageAlt": "黑洞的图示", + "xpack.globalSearchBar.searchBar.placeholder": "搜索 Elastic", + "xpack.globalSearchBar.searchBar.shortcutDescription.macCommandDescription": "Command + /", + "xpack.globalSearchBar.searchBar.shortcutDescription.shortcutDetail": "{shortcutDescription}{commandDescription}", + "xpack.globalSearchBar.searchBar.shortcutDescription.shortcutInstructionDescription": "快捷方式", + "xpack.globalSearchBar.searchBar.shortcutDescription.windowsCommandDescription": "Control + /", "xpack.graph.badge.readOnly.text": "只读", "xpack.graph.badge.readOnly.tooltip": "无法保存 Graph 工作空间", "xpack.graph.bar.exploreLabel": "Graph", @@ -6746,6 +7238,8 @@ "xpack.graph.bar.pickSourceLabel": "选择数据源", "xpack.graph.bar.pickSourceTooltip": "选择数据源以开始绘制关系图。", "xpack.graph.bar.searchFieldPlaceholder": "搜索数据并将其添加到图表", + "xpack.graph.blocklist.noEntriesDescription": "您没有任何已阻止词。选择顶点并单击右侧控制面板中的{stopSign}可阻止它们。与已阻止词匹配的文档不再可供浏览,并且与它们的关系已隐藏。", + "xpack.graph.blocklist.removeButtonAriaLabel": "删除", "xpack.graph.clearWorkspace.confirmButtonLabel": "更改数据源", "xpack.graph.clearWorkspace.confirmText": "如果更改数据源,您当前的字段和顶点将会重置。", "xpack.graph.clearWorkspace.modalTitle": "未保存更改", @@ -6858,6 +7352,7 @@ "xpack.graph.outlinkEncoders.textPlainTitle": "纯文本", "xpack.graph.pageTitle": "图表", "xpack.graph.pluginDescription": "显示并分析 Elasticsearch 数据中的相关关系。", + "xpack.graph.pluginSubtitle": "显示模式和关系。", "xpack.graph.sampleData.label": "图表", "xpack.graph.savedWorkspace.workspaceNameTitle": "新建 Graph 工作空间", "xpack.graph.saveWorkspace.savingErrorMessage": "无法保存工作空间:{message}", @@ -6882,6 +7377,9 @@ "xpack.graph.settings.advancedSettings.timeoutInputLabel": "超时 (ms)", "xpack.graph.settings.advancedSettings.timeoutUnit": "ms", "xpack.graph.settings.advancedSettingsTitle": "高级设置", + "xpack.graph.settings.blocklist.blocklistHelpText": "不允许在图表中使用这些词。", + "xpack.graph.settings.blocklist.clearButtonLabel": "全部删除", + "xpack.graph.settings.blocklistTitle": "阻止列表", "xpack.graph.settings.closeLabel": "关闭", "xpack.graph.settings.drillDowns.cancelButtonLabel": "取消", "xpack.graph.settings.drillDowns.defaultUrlTemplateTitle": "原始文档", @@ -6972,9 +7470,11 @@ "xpack.grokDebugger.registryProviderTitle": "Grok Debugger", "xpack.grokDebugger.sampleDataLabel": "样例数据", "xpack.grokDebugger.serverInactiveLicenseError": "Grok Debugger 工具需要活动的许可证。", + "xpack.grokDebugger.simulate.errorTitle": "模拟错误", "xpack.grokDebugger.simulateButtonLabel": "模拟", "xpack.grokDebugger.structuredDataLabel": "结构化数据", "xpack.grokDebugger.trialLicenseTitle": "试用", + "xpack.grokDebugger.unknownErrorTitle": "出问题了", "xpack.idxMgmt.aliasesTab.noAliasesTitle": "未定义任何别名。", "xpack.idxMgmt.appTitle": "索引管理", "xpack.idxMgmt.badgeAriaLabel": "{label}。选择以基于此选项进行筛选。", @@ -7085,6 +7585,8 @@ "xpack.idxMgmt.componentTemplatesSelector.filters.mappingsLabel": "映射", "xpack.idxMgmt.componentTemplatesSelector.loadingComponentsDescription": "正在加载组件模板……", "xpack.idxMgmt.componentTemplatesSelector.loadingComponentsErrorMessage": "加载组件时出错", + "xpack.idxMgmt.componentTemplatesSelector.noComponentSelectedLabel-1": "将组件模板构建块添加到此模板。", + "xpack.idxMgmt.componentTemplatesSelector.noComponentSelectedLabel-2": "组件模板按指定顺序应用。", "xpack.idxMgmt.componentTemplatesSelector.removeItemIconLabel": "移除", "xpack.idxMgmt.componentTemplatesSelector.searchBox.placeholder": "搜索组件模板", "xpack.idxMgmt.componentTemplatesSelector.searchResult.emptyPrompt.clearSearchButtonLabel": "清除搜索", @@ -7101,10 +7603,22 @@ "xpack.idxMgmt.dataStreamDetailPanel.deleteButtonLabel": "删除数据流", "xpack.idxMgmt.dataStreamDetailPanel.generationTitle": "世代", "xpack.idxMgmt.dataStreamDetailPanel.generationToolTip": "为数据流创建的后备索引的累积计数", + "xpack.idxMgmt.dataStreamDetailPanel.healthTitle": "运行状况", + "xpack.idxMgmt.dataStreamDetailPanel.healthToolTip": "数据流的当前后备索引的运行状况", + "xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyContentNoneMessage": "无", + "xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyTitle": "索引生命周期策略", + "xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyToolTip": "用于管理数据流数据的索引生命周期策略", + "xpack.idxMgmt.dataStreamDetailPanel.indexTemplateTitle": "索引模板", + "xpack.idxMgmt.dataStreamDetailPanel.indexTemplateToolTip": "用于配置数据流及其后备索引的索引模板", "xpack.idxMgmt.dataStreamDetailPanel.indicesTitle": "索引", "xpack.idxMgmt.dataStreamDetailPanel.indicesToolTip": "数据流当前的后备索引", "xpack.idxMgmt.dataStreamDetailPanel.loadingDataStreamDescription": "正在加载数据流", "xpack.idxMgmt.dataStreamDetailPanel.loadingDataStreamErrorMessage": "加载数据流时出错", + "xpack.idxMgmt.dataStreamDetailPanel.maxTimeStampNoneMessage": "永不", + "xpack.idxMgmt.dataStreamDetailPanel.maxTimeStampTitle": "上次更新时间", + "xpack.idxMgmt.dataStreamDetailPanel.maxTimeStampToolTip": "要添加到数据流的最新文档", + "xpack.idxMgmt.dataStreamDetailPanel.storageSizeTitle": "存储大小", + "xpack.idxMgmt.dataStreamDetailPanel.storageSizeToolTip": "数据流的后备索引中所有分片的总大小", "xpack.idxMgmt.dataStreamDetailPanel.timestampFieldTitle": "时间戳字段", "xpack.idxMgmt.dataStreamDetailPanel.timestampFieldToolTip": "时间戳字段由数据流中的所有文档共享", "xpack.idxMgmt.dataStreamList.dataStreamsDescription": "数据流在多个索引上存储时序数据。{learnMoreLink}", @@ -7121,9 +7635,15 @@ "xpack.idxMgmt.dataStreamList.table.actionDeleteDecription": "删除此数据流", "xpack.idxMgmt.dataStreamList.table.actionDeleteText": "删除", "xpack.idxMgmt.dataStreamList.table.deleteDataStreamsButtonLabel": "删除{count, plural, one {数据流} other {数据流} }", + "xpack.idxMgmt.dataStreamList.table.healthColumnTitle": "运行状况", "xpack.idxMgmt.dataStreamList.table.indicesColumnTitle": "索引", + "xpack.idxMgmt.dataStreamList.table.maxTimeStampColumnNoneMessage": "永不", + "xpack.idxMgmt.dataStreamList.table.maxTimeStampColumnTitle": "上次更新时间", "xpack.idxMgmt.dataStreamList.table.nameColumnTitle": "名称", "xpack.idxMgmt.dataStreamList.table.noDataStreamsMessage": "找不到任何数据流", + "xpack.idxMgmt.dataStreamList.table.storageSizeColumnTitle": "存储大小", + "xpack.idxMgmt.dataStreamListControls.includeStatsSwitchLabel": "包含统计信息", + "xpack.idxMgmt.dataStreamListControls.includeStatsSwitchToolTip": "包含统计信息可能会延长重新加载时间", "xpack.idxMgmt.dataStreamListDescription.learnMoreLinkText": "了解详情。", "xpack.idxMgmt.deleteDataStreamsConfirmationModal.cancelButtonLabel": "取消", "xpack.idxMgmt.deleteDataStreamsConfirmationModal.confirmButtonLabel": "删除{dataStreamsCount, plural, one {数据流} other {数据流} }", @@ -7169,7 +7689,7 @@ "xpack.idxMgmt.formWizard.stepAliases.fieldAliasesAriaLabel": "别名代码编辑器", "xpack.idxMgmt.formWizard.stepAliases.fieldAliasesLabel": "别名", "xpack.idxMgmt.formWizard.stepAliases.stepTitle": "别名(可选)", - "xpack.idxMgmt.formWizard.stepComponents.componentsDescription": "组件模板允许您保存索引设置、映射和别名并在索引模板中继承它们。", + "xpack.idxMgmt.formWizard.stepComponents.componentsDescription": "组件模板可用于保存索引设置、映射和别名,并在索引模板中继承它们。", "xpack.idxMgmt.formWizard.stepComponents.docsButtonLabel": "组件模板文档", "xpack.idxMgmt.formWizard.stepComponents.stepTitle": "组件模板(可选)", "xpack.idxMgmt.formWizard.stepMappings.docsButtonLabel": "映射文档", @@ -7349,6 +7869,8 @@ "xpack.idxMgmt.mappingsEditor.configuration.throwErrorsForUnmappedFieldsLabel": "文档包含未映射字段时引发异常", "xpack.idxMgmt.mappingsEditor.confirmationModal.deleteAliasesDescription": "还将删除以下别名。", "xpack.idxMgmt.mappingsEditor.confirmationModal.deleteFieldsDescription": "这还将删除以下字段。", + "xpack.idxMgmt.mappingsEditor.constantKeyword.valueFieldDescription": "此字段的值,适用于索引中的所有文档。如果未指定,则默认为在索引的第一个文档中指定的值。", + "xpack.idxMgmt.mappingsEditor.constantKeyword.valueFieldTitle": "设置值", "xpack.idxMgmt.mappingsEditor.copyToDocLinkText": "“复制到”文档", "xpack.idxMgmt.mappingsEditor.copyToFieldDescription": "将多个字段的值复制到组字段中。然后可以将此组字段作为单个字段进行查询。", "xpack.idxMgmt.mappingsEditor.copyToFieldTitle": "复制到组字段", @@ -7366,6 +7888,8 @@ "xpack.idxMgmt.mappingsEditor.dataType.byteLongDescription": "字节字段接受最小值 {minValue} 且最大值 {maxValue} 的带符号 8 位整数。", "xpack.idxMgmt.mappingsEditor.dataType.completionSuggesterDescription": "完成建议器", "xpack.idxMgmt.mappingsEditor.dataType.completionSuggesterLongDescription": "完成建议器字段支持自动完成,但需要会占用内存且构建缓慢的特殊数据结构。", + "xpack.idxMgmt.mappingsEditor.dataType.constantKeywordDescription": "常量关键字", + "xpack.idxMgmt.mappingsEditor.dataType.constantKeywordLongDescription": "常量关键字字段是一种特殊类型的关键字字段,这些字段包含对于索引中的所有文档都相同的关键字。支持与 {keyword} 字段相同的查询和聚合。", "xpack.idxMgmt.mappingsEditor.dataType.dateDescription": "日期", "xpack.idxMgmt.mappingsEditor.dataType.dateLongDescription": "日期字段接受格式日期的字符串(“2015/01/01 12:10:30”)、表示自 Epoch 起毫秒数的长整数以及表示自 Epoch 起秒数的整数。允许多种日期格式。有时区的日期将转换为 UTC。", "xpack.idxMgmt.mappingsEditor.dataType.dateNanosDescription": "日期纳秒", @@ -7390,6 +7914,8 @@ "xpack.idxMgmt.mappingsEditor.dataType.geoShapeDescription": "地理形状", "xpack.idxMgmt.mappingsEditor.dataType.halfFloatDescription": "半浮点", "xpack.idxMgmt.mappingsEditor.dataType.halfFloatLongDescription": "半浮点字段接受半精度 16 位浮点数,限制为有限值 (IEEE 754)。", + "xpack.idxMgmt.mappingsEditor.dataType.histogramDescription": "直方图", + "xpack.idxMgmt.mappingsEditor.dataType.histogramLongDescription": "直方图字段存储表示直方图的预聚合数值数据,旨在用于聚合。", "xpack.idxMgmt.mappingsEditor.dataType.integerDescription": "整数", "xpack.idxMgmt.mappingsEditor.dataType.integerLongDescription": "整数字段接受最小值 {minValue} 且最大值 {maxValue} 的带符号 32 位整数。", "xpack.idxMgmt.mappingsEditor.dataType.integerRangeDescription": "整数范围", @@ -7421,6 +7947,8 @@ "xpack.idxMgmt.mappingsEditor.dataType.percolatorDescription": "Percolator", "xpack.idxMgmt.mappingsEditor.dataType.percolatorLongDescription": "Percolator 数据类型启用 {percolator}。", "xpack.idxMgmt.mappingsEditor.dataType.percolatorLongDescription.learnMoreLink": "percolator 查询", + "xpack.idxMgmt.mappingsEditor.dataType.pointDescription": "点", + "xpack.idxMgmt.mappingsEditor.dataType.pointLongDescription": "点字段支持搜索落在二维平面坐标系中的 {code} 对。", "xpack.idxMgmt.mappingsEditor.dataType.rangeDescription": "范围", "xpack.idxMgmt.mappingsEditor.dataType.rangeSubtypeDescription": "范围类型", "xpack.idxMgmt.mappingsEditor.dataType.rankFeatureDescription": "排名功能", @@ -7442,6 +7970,11 @@ "xpack.idxMgmt.mappingsEditor.dataType.textLongDescription.keywordTypeLink": "关键字数据类型", "xpack.idxMgmt.mappingsEditor.dataType.tokenCountDescription": "词元计数", "xpack.idxMgmt.mappingsEditor.dataType.tokenCountLongDescription": "词元计数字段接受字符串值。 将分析这些字符串并索引字符串中的词元数目。", + "xpack.idxMgmt.mappingsEditor.dataType.versionDescription": "版本", + "xpack.idxMgmt.mappingsEditor.dataType.versionLongDescription": "版本字段有助于处理软件版本值。此字段未针对大量通配符、正则表达式或模糊搜索进行优化。对于这些查询类型,请使用{keywordType}。", + "xpack.idxMgmt.mappingsEditor.dataType.versionLongDescription.keywordTypeLink": "关键字数据类型", + "xpack.idxMgmt.mappingsEditor.dataType.wildcardDescription": "通配符", + "xpack.idxMgmt.mappingsEditor.dataType.wildcardLongDescription": "通配符字段存储针对通配符类 grep 查询优化的值。", "xpack.idxMgmt.mappingsEditor.date.localeFieldTitle": "设置区域设置", "xpack.idxMgmt.mappingsEditor.dateType.localeFieldDescription": "解析日期时要使用的区域设置。因为月名称或缩写在各个语言中可能不相同,所以这会非常有用。默认为 {root} 区域设置。", "xpack.idxMgmt.mappingsEditor.dateType.nullValueFieldDescription": "将显式 null 值替换为日期,以便可以对其进行索引和搜索。", @@ -7605,7 +8138,9 @@ "xpack.idxMgmt.mappingsEditor.geoShapeType.orientationFieldTitle": "设置方向", "xpack.idxMgmt.mappingsEditor.hideErrorsButtonLabel": "隐藏错误", "xpack.idxMgmt.mappingsEditor.ignoreAboveDocLinkText": "“忽略上述”文档", + "xpack.idxMgmt.mappingsEditor.ignoreAboveFieldDescription": "将不索引超过此值的字符串。这有助于防止超出 Lucene 的词字符长度限值,即 8,191 个 UTF-8 字符。", "xpack.idxMgmt.mappingsEditor.ignoreAboveFieldLabel": "字符长度限制", + "xpack.idxMgmt.mappingsEditor.ignoreAboveFieldTitle": "设置长度限值", "xpack.idxMgmt.mappingsEditor.ignoredMalformedFieldDescription": "默认情况下,不索引包含字段错误数据类型的文档。如果启用,将索引这些文档,但将筛除数据类型错误的字段。注意:如果索引过多这样的文档,基于该字段的查询将无意义。", "xpack.idxMgmt.mappingsEditor.ignoredZValueFieldDescription": "将接受三维点,但仅索引维度和经度值;将忽略第三维。", "xpack.idxMgmt.mappingsEditor.ignoreMalformedDocLinkText": "“忽略格式错误”文档", @@ -7656,6 +8191,10 @@ "xpack.idxMgmt.mappingsEditor.metaFieldDocumentionLink": "了解详情。", "xpack.idxMgmt.mappingsEditor.metaFieldEditorAriaLabel": "_meta 字段数据编辑器", "xpack.idxMgmt.mappingsEditor.metaFieldTitle": "_meta 字段", + "xpack.idxMgmt.mappingsEditor.metaParameterAriaLabel": "元数据字段数据编辑器", + "xpack.idxMgmt.mappingsEditor.metaParameterDescription": "与字段有关的任意信息。指定为 JSON 键值对。", + "xpack.idxMgmt.mappingsEditor.metaParameterDocLinkText": "元数据文档", + "xpack.idxMgmt.mappingsEditor.metaParameterTitle": "设置元数据", "xpack.idxMgmt.mappingsEditor.minSegmentSizeFieldLabel": "最小分段大小", "xpack.idxMgmt.mappingsEditor.multiFieldBadgeLabel": "{dataType} 多字段", "xpack.idxMgmt.mappingsEditor.multiFieldIntroductionText": "此字段是多字段。可以使用多字段以不同方式索引相同的字段。", @@ -7678,11 +8217,18 @@ "xpack.idxMgmt.mappingsEditor.parameters.localeHelpText": "使用 {hyphen} 或 {underscore} 分隔语言、国家/地区和变体。最多允许使用 2 个分隔符。例如:{locale}。", "xpack.idxMgmt.mappingsEditor.parameters.localeLabel": "区域设置", "xpack.idxMgmt.mappingsEditor.parameters.maxInputLengthLabel": "最大输入长度", + "xpack.idxMgmt.mappingsEditor.parameters.metaFieldEditorArraysNotAllowedError": "不允许使用数组。", + "xpack.idxMgmt.mappingsEditor.parameters.metaFieldEditorJsonError": "JSON 无效。", + "xpack.idxMgmt.mappingsEditor.parameters.metaFieldEditorOnlyStringValuesAllowedError": "值必须是字符串。", + "xpack.idxMgmt.mappingsEditor.parameters.metaHelpText": "使用 JSON 格式:{code}", + "xpack.idxMgmt.mappingsEditor.parameters.metaLabel": "元数据", "xpack.idxMgmt.mappingsEditor.parameters.normalizerHelpText": "索引设置中定义的标准化器的名称。", "xpack.idxMgmt.mappingsEditor.parameters.nullValueIpHelpText": "接受 IP 地址。", "xpack.idxMgmt.mappingsEditor.parameters.orientationLabel": "方向", "xpack.idxMgmt.mappingsEditor.parameters.pathHelpText": "根到目标字段的绝对路径。", "xpack.idxMgmt.mappingsEditor.parameters.pathLabel": "字段路径", + "xpack.idxMgmt.mappingsEditor.parameters.pointNullValueHelpText": "点可以表示为对象、字符串、数组或 {docsLink} POINT。", + "xpack.idxMgmt.mappingsEditor.parameters.pointWellKnownTextDocumentationLink": "Well-Known Text", "xpack.idxMgmt.mappingsEditor.parameters.positionIncrementGapLabel": "位置增量间隔", "xpack.idxMgmt.mappingsEditor.parameters.scalingFactorFieldDescription": "值在索引时将乘以此因数并舍入到最近的长整型值。高因数值可改善精确性,但也会增加空间要求。", "xpack.idxMgmt.mappingsEditor.parameters.scalingFactorFieldTitle": "缩放因数", @@ -7711,13 +8257,19 @@ "xpack.idxMgmt.mappingsEditor.parameters.validations.smallerZeroErrorMessage": "值必须大于或等于 0。", "xpack.idxMgmt.mappingsEditor.parameters.validations.spacesNotAllowedErrorMessage": "不允许使用空格。", "xpack.idxMgmt.mappingsEditor.parameters.validations.typeIsRequiredErrorMessage": "指定字段类型。", + "xpack.idxMgmt.mappingsEditor.parameters.valueLabel": "值", "xpack.idxMgmt.mappingsEditor.parameters.wellKnownTextDocumentationLink": "Well-Known Text", + "xpack.idxMgmt.mappingsEditor.point.ignoreMalformedFieldDescription": "默认情况下,不索引包含格式错误的点的文档。如果启用,将索引这些文档,但会筛除包含格式错误的点的字段。注意:如果索引过多这样的文档,基于该字段的查询将无意义。", + "xpack.idxMgmt.mappingsEditor.point.ignoreZValueFieldDescription": "将接受三维点,但仅索引 x 和 y 值;将忽略第三维。", + "xpack.idxMgmt.mappingsEditor.point.nullValueFieldDescription": "将显式 null 值替换为点值,以便可以对其进行索引和搜索。", "xpack.idxMgmt.mappingsEditor.positionIncrementGapDocLinkText": "“位置递增间隔”文档", "xpack.idxMgmt.mappingsEditor.positionIncrementGapFieldDescription": "应在字符串数组的所有元素之间插入的虚假字词位置数目。", "xpack.idxMgmt.mappingsEditor.positionIncrementGapFieldTitle": "设置位置递增间隔", "xpack.idxMgmt.mappingsEditor.positionsErrorMessage": "需要将索引选项(在“可搜索”切换下)设置为“位置”或“偏移”,以便可以更改位置递增间隔。", "xpack.idxMgmt.mappingsEditor.positionsErrorTitle": "“位置”未启用。", "xpack.idxMgmt.mappingsEditor.predefinedButtonLabel": "使用内置分析器", + "xpack.idxMgmt.mappingsEditor.rankFeature.positiveScoreImpactFieldDescription": "与分数负相关的排名功能应禁用此字段。", + "xpack.idxMgmt.mappingsEditor.rankFeature.positiveScoreImpactFieldTitle": "正分数影响", "xpack.idxMgmt.mappingsEditor.relationshipsTitle": "关系", "xpack.idxMgmt.mappingsEditor.removeFieldButtonLabel": "移除", "xpack.idxMgmt.mappingsEditor.routingDescription": "文档可以路由到索引中的特定分片。使用定制路由时,只要索引文档,都需要提供路由值,否则可能将会在多个分片上索引文档。{docsLink}", @@ -7776,6 +8328,15 @@ "xpack.idxMgmt.refreshIndicesAction.successfullyRefreshedIndicesMessage": "已成功刷新:[{indexNames}]", "xpack.idxMgmt.reloadIndicesAction.indicesPageRefreshFailureMessage": "无法刷新当前页面的索引。", "xpack.idxMgmt.settingsTab.noIndexSettingsTitle": "未定义任何设置。", + "xpack.idxMgmt.simulateTemplate.closeButtonLabel": "关闭", + "xpack.idxMgmt.simulateTemplate.descriptionText": "这是最终模板,将根据所选的组件模板和添加的任何覆盖应用于匹配的索引。", + "xpack.idxMgmt.simulateTemplate.filters.aliases": "别名", + "xpack.idxMgmt.simulateTemplate.filters.indexSettings": "索引设置", + "xpack.idxMgmt.simulateTemplate.filters.label": "包括:", + "xpack.idxMgmt.simulateTemplate.filters.mappings": "映射", + "xpack.idxMgmt.simulateTemplate.noFilterSelected": "至少选择一个选项进行预览。", + "xpack.idxMgmt.simulateTemplate.title": "预览索引模板", + "xpack.idxMgmt.simulateTemplate.updateButtonLabel": "更新", "xpack.idxMgmt.summary.headers.aliases": "别名", "xpack.idxMgmt.summary.headers.deletedDocumentsHeader": "文档已删除", "xpack.idxMgmt.summary.headers.documentsHeader": "文档计数", @@ -7806,6 +8367,8 @@ "xpack.idxMgmt.templateDetails.manageButtonLabel": "管理", "xpack.idxMgmt.templateDetails.manageContextMenuPanelTitle": "模板选项", "xpack.idxMgmt.templateDetails.mappingsTabTitle": "映射", + "xpack.idxMgmt.templateDetails.previewTab.descriptionText": "这是将应用于匹配索引的最终模板。", + "xpack.idxMgmt.templateDetails.previewTabTitle": "预览", "xpack.idxMgmt.templateDetails.settingsTabTitle": "设置", "xpack.idxMgmt.templateDetails.summaryTab.componentsDescriptionListTitle": "组件模板", "xpack.idxMgmt.templateDetails.summaryTab.dataStreamDescriptionListTitle": "数据流", @@ -7826,6 +8389,7 @@ "xpack.idxMgmt.templateEdit.systemTemplateWarningDescription": "系统模板对内部操作至关重要。", "xpack.idxMgmt.templateEdit.systemTemplateWarningTitle": "编辑系统模板会使 Kibana 无法运行", "xpack.idxMgmt.templateForm.createButtonLabel": "创建模板", + "xpack.idxMgmt.templateForm.previewIndexTemplateButtonLabel": "预览索引模板", "xpack.idxMgmt.templateForm.saveButtonLabel": "保存模板", "xpack.idxMgmt.templateForm.saveTemplateError": "无法创建模板", "xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel": "添加元数据", @@ -7857,6 +8421,8 @@ "xpack.idxMgmt.templateForm.stepLogistics.stepTitle": "运筹", "xpack.idxMgmt.templateForm.stepLogistics.versionDescription": "在外部管理系统中标识该模板的编号。", "xpack.idxMgmt.templateForm.stepLogistics.versionTitle": "版本", + "xpack.idxMgmt.templateForm.stepReview.previewTab.descriptionText": "这是将应用于匹配索引的最终模板。组件模板按指定顺序应用。显式映射、设置和别名覆盖组件模板。", + "xpack.idxMgmt.templateForm.stepReview.previewTabTitle": "预览", "xpack.idxMgmt.templateForm.stepReview.requestTab.descriptionText": "此请求将创建以下索引模板。", "xpack.idxMgmt.templateForm.stepReview.requestTabTitle": "请求", "xpack.idxMgmt.templateForm.stepReview.stepTitle": "查看 “{templateName}” 的详情", @@ -7929,8 +8495,14 @@ "xpack.indexLifecycleMgmt.activePhaseMessage": "有效", "xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel": "添加生命周期策略", "xpack.indexLifecycleMgmt.appTitle": "索引生命周期策略", + "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody": "至少将一个节点分配到冷层、温层或热层,以使用基于角色的分配。如果没有可用节点,则策略无法完成分配。", + "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "没有分配到冷层的节点", + "xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。将处于冷阶段的数据存储在成本较低的硬件上。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "冻结索引", + "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription": "设置副本数目。默认情况下与上一阶段相同。", "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "副本分片数目", + "xpack.indexLifecycleMgmt.coldPhase.replicasTitle": "副本", + "xpack.indexLifecycleMgmt.common.dataTier.title": "数据分配", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "取消", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "删除", "xpack.indexLifecycleMgmt.confirmDelete.errorMessage": "删除策略 {policyName} 时出错", @@ -7938,10 +8510,26 @@ "xpack.indexLifecycleMgmt.confirmDelete.title": "删除策略“{name}”", "xpack.indexLifecycleMgmt.confirmDelete.undoneWarning": "无法恢复删除的策略。", "xpack.indexLifecycleMgmt.editPolicy.cancelButton": "取消", + "xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription": "在 elasticsearch.yml 中定义定制节点属性,以使用基于属性的分配。将改用冷节点。", + "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "激活冷阶段", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText": "您查询自己索引的频率较低,因此您可以在效率较低的硬件上分配分片。因为您的查询较为缓慢,所以您可以减少副本分片数目。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel": "冷阶段", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "冻结的索引在集群上有很少的开销,已被阻止进行写操作。您可以搜索冻结的索引,但查询应会较慢。", + "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "使索引只读,并最大限度减小其内存占用。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "冻结", + "xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel": "设置副本", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.allocationFieldLabel": "数据层选项", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "根据节点属性移动数据。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "定制", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "将数据移到冷层中的节点。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input": "使用冷节点(建议)", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText": "不要移动冷阶段的数据。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input": "关闭", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText": "根据节点属性移动数据。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input": "定制", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText": "将数据移到温层中的节点。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input": "使用温节点(建议)", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText": "不要移动温阶段的数据。", + "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.input": "关闭", "xpack.indexLifecycleMgmt.editPolicy.createdMessage": "创建于", "xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage": "创建索引生命周期策略", "xpack.indexLifecycleMgmt.editPolicy.creationDaysOptionLabel": "天(自索引创建)", @@ -7951,10 +8539,17 @@ "xpack.indexLifecycleMgmt.editPolicy.creationMinutesOptionLabel": "分钟(自索引创建)", "xpack.indexLifecycleMgmt.editPolicy.creationNanoSecondsOptionLabel": "纳秒(自索引创建)", "xpack.indexLifecycleMgmt.editPolicy.creationSecondsOptionLabel": "秒(自索引创建)", + "xpack.indexLifecycleMgmt.editPolicy.dataTierColdLabel": "冷", + "xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel": "热", + "xpack.indexLifecycleMgmt.editPolicy.dataTierWarmLabel": "温", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.activateWarmPhaseSwitchLabel": "激活删除阶段", + "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyLink": "创建新策略", + "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyMessage": "输入现有快照策略的名称,或使用此名称{link}。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyTitle": "未找到策略名称", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescriptionText": "您不再需要自己的索引。 您可以定义安全删除它的时间。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseLabel": "删除阶段", + "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedLink": "创建快照生命周期策略", + "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedMessage": "{link}以自动创建和删除集群快照。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedTitle": "找不到快照策略", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesLoadedMessage": "刷新此字段并输入现有快照策略的名称。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesLoadedTitle": "无法加载现有策略", @@ -7966,6 +8561,9 @@ "xpack.indexLifecycleMgmt.editPolicy.editingExistingPolicyExplanationMessage": "所做的任何更改将影响附加到此策略的索引。或者,您可以在新策略中保存这些更改。", "xpack.indexLifecycleMgmt.editPolicy.editingExistingPolicyMessage": "您正在编辑现有策略", "xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage": "编辑索引生命周期策略 {originalPolicyName}", + "xpack.indexLifecycleMgmt.editPolicy.forceMerge.bestCompressionText": "对已存储字段使用较高压缩率,但会降低性能。", + "xpack.indexLifecycleMgmt.editPolicy.forceMerge.enableExplanationText": "通过合并较小文件并清除已删除文件,减少分片中的分段数目。", + "xpack.indexLifecycleMgmt.editPolicy.forceMerge.enableText": "强制合并", "xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage": "请修复此页面上的错误。", "xpack.indexLifecycleMgmt.editPolicy.hidePolicyJsonButto": "隐藏请求", "xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescriptionMessage": "此阶段为必需。您正频繁地查询并写到您的索引。 为了获取更快的更新,在索引变得过大或过旧时,您可以滚动更新索引。", @@ -7977,13 +8575,21 @@ "xpack.indexLifecycleMgmt.editPolicy.learnAboutIndexTemplatesLink": "了解索引模板", "xpack.indexLifecycleMgmt.editPolicy.learnAboutShardAllocationLink": "了解分片分配", "xpack.indexLifecycleMgmt.editPolicy.learnAboutTimingText": "了解计时", + "xpack.indexLifecycleMgmt.editPolicy.lifecyclePoliciesLoadingFailedTitle": "无法加载现有生命周期策略", + "xpack.indexLifecycleMgmt.editPolicy.lifecyclePoliciesReloadButton": "重试", "xpack.indexLifecycleMgmt.editPolicy.lifecyclePolicyDescriptionText": "使用索引策略自动化索引生命周期的四个阶段,从频繁地写入到索引到删除索引。", "xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError": "最大存在时间必填。", "xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError": "最大文档数必填。", "xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大索引大小必填。", "xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名称", - "xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "选择节点属性来控制分片分配", - "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml 中未配置任何节点属性", + "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.customOption.description": "使用节点属性控制分片分配。{learnMoreLink}。", + "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption": "不要修改分配配置", + "xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "选择节点属性", + "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle": "无法加载节点属性", + "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "未配置定制节点属性", + "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton": "重试", + "xpack.indexLifecycleMgmt.editPolicy.nodeDetailsLoadingFailedTitle": "无法加载节点属性详情", + "xpack.indexLifecycleMgmt.editPolicy.nodeDetailsReloadButton": "重试", "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字必填。", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "冷阶段计时", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "冷阶段计时单位", @@ -7992,6 +8598,7 @@ "xpack.indexLifecycleMgmt.editPolicy.phaseErrorMessage": "修复错误", "xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel": "温阶段计时", "xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel": "温阶段计时单位", + "xpack.indexLifecycleMgmt.editPolicy.policiesLoading": "正在加载策略……", "xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError": "该策略名称已被使用。", "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError": "策略名称不能包含逗号。", "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError": "策略名称不能包含空格。", @@ -8016,13 +8623,20 @@ "xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage": "{verb}生命周期策略“{lifecycleName}”", "xpack.indexLifecycleMgmt.editPolicy.updatedMessage": "已更新", "xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage": "策略名称不能以下划线开头,且不能包含问号或空格。", - "xpack.indexLifecycleMgmt.editPolicy.viewNodeDetailsButton": "查看附加到此配置的节点列表", + "xpack.indexLifecycleMgmt.editPolicy.viewNodeDetailsButton": "查看具有选定属性的节点", + "xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription": "在 elasticsearch.yml 中定义定制节点属性,以使用基于属性的分配。将改用温节点。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.activateWarmPhaseSwitchLabel": "激活温阶段", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.indexPriorityExplanationText": "设置在节点重新启动后恢复索引的优先级。较高优先级的索引会在较低优先级的索引之前恢复。", + "xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel": "设置副本", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.shrinkIndexExplanationText": "将索引缩小成具有较少主分片的新索引。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.shrinkText": "缩小", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescriptionMessage": "您仍在查询自己的索引,但其为只读。您可以将分片分配给效率较低的硬件。为了获取更快的搜索,您可以减少分片数目并强制合并段。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseLabel": "温阶段", + "xpack.indexLifecycleMgmt.featureCatalogueDescription": "定义生命周期策略,以随着索引老化自动执行操作。", + "xpack.indexLifecycleMgmt.featureCatalogueTitle": "管理索引生命周期", + "xpack.indexLifecycleMgmt.forcemerge.bestCompressionLabel": "压缩已存储字段", + "xpack.indexLifecycleMgmt.forcemerge.enableLabel": "强制合并数据", + "xpack.indexLifecycleMgmt.forceMerge.numberOfSegmentsLabel": "分段数目", "xpack.indexLifecycleMgmt.hotPhase.bytesLabel": "字节", "xpack.indexLifecycleMgmt.hotPhase.daysLabel": "天", "xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel": "启用滚动更新", @@ -8069,6 +8683,7 @@ "xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.modalTitle": "将生命周期策略添加到“{indexName}”", "xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.noPoliciesWarningTitle": "未定义任何索引生命周期策略", "xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.noPolicySelectedErrorMessage": "必须选择策略。", + "xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyToTemplateConfirmModal.errorLoadingTemplatesButton": "重试", "xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyToTemplateConfirmModal.indexHasNoAliasesWarningMessage": "策略 “{existingPolicyName}” 已附加到此索引模板。添加此策略将覆盖该配置。", "xpack.indexLifecycleMgmt.indexManagementTable.removeLifecyclePolicyConfirmModal.cancelButtonText": "取消", "xpack.indexLifecycleMgmt.indexManagementTable.removeLifecyclePolicyConfirmModal.modalTitle": "从{count, plural, one {索引} other {索引}}中移除生命周期策略", @@ -8080,6 +8695,7 @@ "xpack.indexLifecycleMgmt.indexMgmtBanner.filterLabel": "显示错误", "xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel": "冷", "xpack.indexLifecycleMgmt.indexMgmtFilter.deleteLabel": "删除", + "xpack.indexLifecycleMgmt.indexMgmtFilter.frozenLabel": "已冻结", "xpack.indexLifecycleMgmt.indexMgmtFilter.hotLabel": "热", "xpack.indexLifecycleMgmt.indexMgmtFilter.lifecyclePhaseLabel": "生命周期阶段", "xpack.indexLifecycleMgmt.indexMgmtFilter.lifecycleStatusLabel": "生命周期状态", @@ -8104,10 +8720,12 @@ "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.chooseTemplateLabel": "索引模板", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.chooseTemplateMessage": "选择索引模板", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.confirmButton": "添加策略", + "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.errorLoadingTemplatesTitle": "无法加载索引模板", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.errorMessage": "向索引模板 “{templateName}” 添加策略 “{policyName}” 时出错", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.explanationText": "这会将生命周期策略应用到匹配索引模板的所有索引。", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.noTemplateSelectedErrorMessage": "必须选择索引模板。", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.rolloverAliasLabel": "滚动更新索引的别名", + "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.showLegacyTemplates": "显示旧版索引模板", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.successMessage": "已将策略 “{policyName}” 添加到索引模板 “{templateName}”。", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.templateHasPolicyWarningTitle": "模板已有策略", "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title": "将策略 “{name}” 添加到索引模板", @@ -8122,6 +8740,9 @@ "xpack.indexLifecycleMgmt.policyTable.headers.modifiedDateHeader": "上次修改日期", "xpack.indexLifecycleMgmt.policyTable.headers.nameHeader": "名称", "xpack.indexLifecycleMgmt.policyTable.headers.versionHeader": "版本", + "xpack.indexLifecycleMgmt.policyTable.policiesLoading": "正在加载策略……", + "xpack.indexLifecycleMgmt.policyTable.policiesLoadingFailedTitle": "无法加载现有生命周期策略", + "xpack.indexLifecycleMgmt.policyTable.policiesReloadButton": "重试", "xpack.indexLifecycleMgmt.policyTable.policyActionsMenu.panelTitle": "策略选项", "xpack.indexLifecycleMgmt.policyTable.sectionDescription": "管理变旧的索引。 附加策略以自动化何时以及如何在索引整个生命周期中变迁索引。", "xpack.indexLifecycleMgmt.policyTable.sectionHeading": "索引生命周期策略", @@ -8131,9 +8752,19 @@ "xpack.indexLifecycleMgmt.removeIndexLifecycleActionButtonLabel": "删除生命周期策略", "xpack.indexLifecycleMgmt.retryIndexLifecycleAction.retriedLifecycleMessage": "已为以下索引调用重试生命周期步骤:{indexNames}", "xpack.indexLifecycleMgmt.retryIndexLifecycleActionButtonLabel": "重试生命周期步骤", + "xpack.indexLifecycleMgmt.templateNotFoundMessage": "找不到模板 {name}。", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody": "至少将一个节点分配到温层或冷层,以使用基于角色的分配。如果没有可用节点,则策略无法完成分配。", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle": "没有分配到温层的节点", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold": "此策略会改为将冷阶段的数据移到{tier}层节点。", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold.title": "没有分配到冷层的节点", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "此策略会改为将温阶段的数据移到{tier}层节点。", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "没有分配到温层的节点", + "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。", "xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel": "滚动更新时移到温阶段", "xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel": "主分片数目", + "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription": "设置副本数目。默认情况下与上一阶段相同。", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "副本分片数目", + "xpack.indexLifecycleMgmt.warmPhase.replicasTitle": "副本", "xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel": "缩小索引", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", @@ -8143,6 +8774,13 @@ "xpack.infra.alerting.logs.createAlertButton": "创建告警", "xpack.infra.alerting.logs.manageAlerts": "管理告警", "xpack.infra.alerting.manageAlerts": "管理告警", + "xpack.infra.alerts.charts.errorMessage": "哇哦,出问题了", + "xpack.infra.alerts.charts.loadingMessage": "正在加载", + "xpack.infra.alerts.charts.noDataMessage": "没有可用图表数据", + "xpack.infra.alerts.timeLabels.days": "天", + "xpack.infra.alerts.timeLabels.hours": "小时", + "xpack.infra.alerts.timeLabels.minutes": "分钟", + "xpack.infra.alerts.timeLabels.seconds": "秒", "xpack.infra.analysisSetup.actionStepTitle": "创建 ML 作业", "xpack.infra.analysisSetup.configurationStepTitle": "配置", "xpack.infra.analysisSetup.createMlJobButton": "创建 ML 作业", @@ -8219,6 +8857,7 @@ "xpack.infra.header.infrastructureNavigationTitle": "指标", "xpack.infra.header.infrastructureTitle": "指标", "xpack.infra.header.logsTitle": "日志", + "xpack.infra.hideHistory": "隐藏历史记录", "xpack.infra.homePage.documentTitle": "指标", "xpack.infra.homePage.inventoryTabTitle": "库存", "xpack.infra.homePage.metricsExplorerTabTitle": "指标浏览器", @@ -8247,6 +8886,12 @@ "xpack.infra.inventoryModels.findToolbar.error": "您尝试查找的工具栏不存在。", "xpack.infra.inventoryModels.host.singularDisplayName": "主机", "xpack.infra.inventoryModels.pod.singularDisplayName": "Kubernetes Pod", + "xpack.infra.inventoryTimeline.checkNewDataButtonLabel": "检查新数据", + "xpack.infra.inventoryTimeline.errorTitle": "无法显示历史记录数据。", + "xpack.infra.inventoryTimeline.header": "平均值 {metricLabel}", + "xpack.infra.inventoryTimeline.legend.anomalyLabel": "检测到异常", + "xpack.infra.inventoryTimeline.noHistoryDataTitle": "没有要显示的历史记录数据。", + "xpack.infra.inventoryTimeline.retryButtonLabel": "重试", "xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage": "{metricId} 的模型需要云 ID,但没有为 {nodeId} 提供。", "xpack.infra.kibanaMetrics.invalidInfraMetricErrorMessage": "{id} 不是有效的 InfraMetric", "xpack.infra.kibanaMetrics.nodeDoesNotExistErrorMessage": "{nodeId} 不存在。", @@ -8287,12 +8932,20 @@ "xpack.infra.logs.alertFlyout.error.criterionComparatorRequired": "比较运算符必填。", "xpack.infra.logs.alertFlyout.error.criterionFieldRequired": "“字段”必填。", "xpack.infra.logs.alertFlyout.error.criterionValueRequired": "“值”必填。", + "xpack.infra.logs.alertFlyout.error.thresholdRequired": "“数值阈值”必填。", "xpack.infra.logs.alertFlyout.error.timeSizeRequired": "“时间大小”必填。", "xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix": "具有", "xpack.infra.logs.alertFlyout.removeCondition": "删除条件", "xpack.infra.logs.alertFlyout.sourceStatusError": "抱歉,加载字段信息时有问题", "xpack.infra.logs.alertFlyout.sourceStatusErrorTryAgain": "重试", "xpack.infra.logs.alertFlyout.successiveCriterionFieldPrefix": "且", + "xpack.infra.logs.alertFlyout.thresholdPopoverTitle": "阈值", + "xpack.infra.logs.alertFlyout.thresholdPrefix": "是", + "xpack.infra.logs.alertFlyout.thresholdTypeCount": "符合以下条件的日志条目", + "xpack.infra.logs.alertFlyout.thresholdTypeCountSuffix": "的计数,", + "xpack.infra.logs.alertFlyout.thresholdTypePrefix": "即查询 A", + "xpack.infra.logs.alertFlyout.thresholdTypeRatio": "与查询 B 的", + "xpack.infra.logs.alertFlyout.thresholdTypeRatioSuffix": "比率为", "xpack.infra.logs.alerting.comparator.eq": "是", "xpack.infra.logs.alerting.comparator.eqNumber": "等于", "xpack.infra.logs.alerting.comparator.gt": "大于", @@ -8306,10 +8959,21 @@ "xpack.infra.logs.alerting.comparator.notMatch": "不匹配", "xpack.infra.logs.alerting.comparator.notMatchPhrase": "不匹配短语", "xpack.infra.logs.alerting.threshold.conditionsActionVariableDescription": "日志条目需要满足的条件", - "xpack.infra.logs.alerting.threshold.defaultActionMessage": "\\{\\{#context.group\\}\\}\\{\\{context.group\\}\\} - \\{\\{/context.group\\}\\}\\{\\{context.matchingDocuments\\}\\} 个日志条目满足以下条件:\\{\\{context.conditions\\}\\}", + "xpack.infra.logs.alerting.threshold.defaultActionMessage": "\\{\\{^context.isRatio\\}\\}\\{\\{#context.group\\}\\}\\{\\{context.group\\}\\} - \\{\\{/context.group\\}\\}\\{\\{context.matchingDocuments\\}\\} 个日志条目已符合以下条件:\\{\\{context.conditions\\}\\}\\{\\{/context.isRatio\\}\\}\\{\\{#context.isRatio\\}\\}\\{\\{#context.group\\}\\}\\{\\{context.group\\}\\} - \\{\\{/context.group\\}\\} 与 \\{\\{context.numeratorConditions\\}\\} 匹配的日志条目计数和与 \\{\\{context.denominatorConditions\\}\\} 匹配的日志条目计数的比率为 \\{\\{context.ratio\\}\\}\\{\\{/context.isRatio\\}\\}", + "xpack.infra.logs.alerting.threshold.denominatorConditionsActionVariableDescription": "比率的分母需要满足的条件", "xpack.infra.logs.alerting.threshold.documentCountActionVariableDescription": "匹配所提供条件的日志条目数", + "xpack.infra.logs.alerting.threshold.everythingSeriesName": "日志条目", "xpack.infra.logs.alerting.threshold.fired": "已触发", "xpack.infra.logs.alerting.threshold.groupByActionVariableDescription": "负责触发告警的组的名称", + "xpack.infra.logs.alerting.threshold.isRatioActionVariableDescription": "表示此告警是否配置了比率", + "xpack.infra.logs.alerting.threshold.numeratorConditionsActionVariableDescription": "比率的分子需要满足的条件", + "xpack.infra.logs.alerting.threshold.ratioActionVariableDescription": "两组条件的比率值", + "xpack.infra.logs.alerting.threshold.ratioCriteriaQueryAText": "查询 A", + "xpack.infra.logs.alerting.threshold.ratioCriteriaQueryBText": "查询 B", + "xpack.infra.logs.alerting.threshold.timestampActionVariableDescription": "触发告警时的 UTC 时间戳", + "xpack.infra.logs.alerts.dataTimeRangeLabel": "过去 {lookback} {timeLabel}的数据", + "xpack.infra.logs.alerts.dataTimeRangeLabelWithGrouping": "过去 {lookback} {timeLabel}的数据,按 {groupByLabel} 进行分组(显示{displayedGroups}/{totalGroups} 个组)", + "xpack.infra.logs.analsysisSetup.indexQualityWarningTooltipMessage": "分析这些索引中的日志消息时,我们检测到一些问题,这可能表明结果质量降低。考虑将这些索引或有问题的数据集排除在分析之外。", "xpack.infra.logs.analysis.analyzeInMlButtonLabel": "在 ML 中分析", "xpack.infra.logs.analysis.anomaliesExpandedRowActualRateDescription": "实际", "xpack.infra.logs.analysis.anomaliesExpandedRowActualRateTitle": "{actualCount, plural, one {消息} other {消息}}", @@ -8342,6 +9006,7 @@ "xpack.infra.logs.analysis.logEntryCategoriesModuleDescription": "使用 Machine Learning 自动归类日志消息。", "xpack.infra.logs.analysis.logEntryCategoriesModuleName": "归类", "xpack.infra.logs.analysis.logEntryExamplesViewAnomalyInMlLabel": "在 Machine Learning 中查看异常", + "xpack.infra.logs.analysis.logEntryExamplesViewDetailsLabel": "查看详情", "xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel": "在流中查看", "xpack.infra.logs.analysis.logEntryRateModuleDescription": "使用 Machine Learning 自动检测异常日志条目速率。", "xpack.infra.logs.analysis.logEntryRateModuleName": "日志速率", @@ -8362,6 +9027,7 @@ "xpack.infra.logs.analysis.setupStatusTryAgainButton": "重试", "xpack.infra.logs.analysis.setupStatusUnknownTitle": "我们无法确定您的 ML 作业的状态。", "xpack.infra.logs.analysis.userManagementButtonLabel": "管理用户", + "xpack.infra.logs.analysis.viewInMlButtonLabel": "在 Machine Learning 中查看", "xpack.infra.logs.analysisPage.loadingMessage": "正在检查分析作业的状态......", "xpack.infra.logs.analysisPage.unavailable.mlAppLink": "Machine Learning 应用", "xpack.infra.logs.categoryExample.viewInContextText": "在上下文中查看", @@ -8408,8 +9074,9 @@ "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "在 ML 中分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "在 ML 应用中分析此类别。", "xpack.infra.logs.logEntryCategories.categoryColumnTitle": "类别", - "xpack.infra.logs.logEntryCategories.categoryQualityWarningCalloutMessage": "分析日志消息时,我们检测到一些可能会导致归类结果质量降低的问题。", + "xpack.infra.logs.logEntryCategories.categoryQualityWarningCalloutMessage": "分析日志消息时,我们检测到一些问题,这可能表明归类结果质量降低。考虑将相应的数据集排除在分析之外。", "xpack.infra.logs.logEntryCategories.categoryQUalityWarningCalloutTitle": "质量警告", + "xpack.infra.logs.logEntryCategories.categoryQualityWarningDetailsAccordionButtonLabel": "详情", "xpack.infra.logs.logEntryCategories.countColumnTitle": "消息计数", "xpack.infra.logs.logEntryCategories.datasetColumnTitle": "数据集", "xpack.infra.logs.logEntryCategories.jobStatusLoadingMessage": "正在检查归类作业的状态......", @@ -8423,7 +9090,7 @@ "xpack.infra.logs.logEntryCategories.setupDescription": "要启用日志类别分析,请设置 Machine Learning 作业。", "xpack.infra.logs.logEntryCategories.setupTitle": "设置日志类别分析", "xpack.infra.logs.logEntryCategories.showAnalysisSetupButtonLabel": "ML 设置", - "xpack.infra.logs.logEntryCategories.singleCategoryWarningReasonDescription": "分析只能从日志消息中提取单个类别。", + "xpack.infra.logs.logEntryCategories.singleCategoryWarningReasonDescription": "分析无法从日志消息中提取多个类别。", "xpack.infra.logs.logEntryCategories.topCategoriesSectionLoadingAriaLabel": "正在加载消息类别", "xpack.infra.logs.logEntryCategories.topCategoriesSectionTitle": "日志消息类别", "xpack.infra.logs.logEntryCategories.trendColumnTitle": "趋势", @@ -8601,8 +9268,10 @@ "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "在", "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "存在 {boldedResultsNumber}无数据结果。", "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber": "{noData, plural, one {# 个} other {# 个}}", - "xpack.infra.metrics.alertFlyout.alertPreviewResult": "此告警将发生 {firedTimes}", - "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "在过去 {lookback}。", + "xpack.infra.metrics.alertFlyout.alertPreviewResult": "有 {firedTimes}", + "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "在过去 {lookback} 满足此告警的条件。", + "xpack.infra.metrics.alertFlyout.alertPreviewTotalNotifications": "因此,此告警将根据“{alertThrottle}”的选定“通知频率”设置发送{notifications}。", + "xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber": "{notifs, plural, one {# 个通知} other {# 个通知}}", "xpack.infra.metrics.alertFlyout.conditions": "条件", "xpack.infra.metrics.alertFlyout.createAlertPerHelpText": "为每个唯一值创建告警。例如:“host.id”或“cloud.region”。", "xpack.infra.metrics.alertFlyout.createAlertPerText": "创建告警时间间隔(可选)", @@ -8621,7 +9290,7 @@ "xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "当", "xpack.infra.metrics.alertFlyout.filterHelpText": "使用 KQL 表达式限制告警触发器的范围。", "xpack.infra.metrics.alertFlyout.filterLabel": "筛选(可选)", - "xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {# 次} other {# 次}}", + "xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {# 个实例} other {# 个实例}}", "xpack.infra.metrics.alertFlyout.hourLabel": "小时", "xpack.infra.metrics.alertFlyout.lastDayLabel": "昨天", "xpack.infra.metrics.alertFlyout.lastHourLabel": "上一小时", @@ -8640,6 +9309,7 @@ "xpack.infra.metrics.alertFlyout.weekLabel": "周", "xpack.infra.metrics.alerting.alertStateActionVariableDescription": "告警的当前状态", "xpack.infra.metrics.alerting.groupActionVariableDescription": "报告数据的组名称", + "xpack.infra.metrics.alerting.inventory.noDataFormattedValue": "[无数据]", "xpack.infra.metrics.alerting.inventory.threshold.defaultActionMessage": "\\{\\{alertName\\}\\} - \\{\\{context.group\\}\\} 处于 \\{\\{context.alertState\\}\\} 状态\n\n原因:\n\\{\\{context.reason\\}\\}\n", "xpack.infra.metrics.alerting.inventory.threshold.fired": "已触发", "xpack.infra.metrics.alerting.metricActionVariableDescription": "指定条件中的指标名称。用法:(ctx.metric.condition0, ctx.metric.condition1, 诸如此类)。", @@ -8659,6 +9329,7 @@ "xpack.infra.metrics.alerting.threshold.gtComparator": "大于", "xpack.infra.metrics.alerting.threshold.ltComparator": "小于", "xpack.infra.metrics.alerting.threshold.noDataAlertReason": "{metric} 在过去 {interval}中未报告数据", + "xpack.infra.metrics.alerting.threshold.noDataFormattedValue": "[无数据]", "xpack.infra.metrics.alerting.threshold.noDataState": "无数据", "xpack.infra.metrics.alerting.threshold.okState": "正常 [已恢复]", "xpack.infra.metrics.alerting.threshold.outsideRangeComparator": "不介于", @@ -8713,6 +9384,7 @@ "xpack.infra.metricsExplorer.emptyChart.body": "无法呈现图表。", "xpack.infra.metricsExplorer.emptyChart.title": "图表数据缺失", "xpack.infra.metricsExplorer.errorMessage": "似乎请求失败,并出现“{message}”", + "xpack.infra.metricsExplorer.everything": "所有内容", "xpack.infra.metricsExplorer.filterByLabel": "添加筛选", "xpack.infra.metricsExplorer.footerPaginationMessage": "显示 {length} 个图表,共 {total} 个,按“{groupBy}”分组", "xpack.infra.metricsExplorer.groupByAriaLabel": "图表绘制依据", @@ -8729,6 +9401,46 @@ "xpack.infra.metricsExplorer.openInTSVB": "在 Visualize 中打开", "xpack.infra.metricsExplorer.viewNodeDetail": "查看 {name} 的指标", "xpack.infra.metricsHeaderAddDataButtonLabel": "添加数据", + "xpack.infra.ml.anomalyDetectionButton": "异常检测", + "xpack.infra.ml.anomalyFlyout.anomaliesTabLabel": "查看异常", + "xpack.infra.ml.anomalyFlyout.create.createButton": "启用", + "xpack.infra.ml.anomalyFlyout.create.description": "异常检测由 Machine Learning 提供支持。Machine Learning 作业适用于以下资源类型。启用这些作业以开始检测基础架构指标中的异常。", + "xpack.infra.ml.anomalyFlyout.create.hostDescription": "检测主机上的内存使用情况和网络流量异常。", + "xpack.infra.ml.anomalyFlyout.create.hostSuccessTitle": "主机", + "xpack.infra.ml.anomalyFlyout.create.hostTitle": "主机", + "xpack.infra.ml.anomalyFlyout.create.k8sDescription": "检测 Kubernetes Pod 上的内存使用情况和网络流量异常。", + "xpack.infra.ml.anomalyFlyout.create.k8sSuccessTitle": "Kubernetes", + "xpack.infra.ml.anomalyFlyout.create.k8sTitle": "Kubernetes Pod", + "xpack.infra.ml.anomalyFlyout.create.recreateButton": "重新创建作业", + "xpack.infra.ml.anomalyFlyout.enabledCallout": "已为 {target} 启用异常检测", + "xpack.infra.ml.anomalyFlyout.flyoutHeader": "Machine Learning 异常检测", + "xpack.infra.ml.anomalyFlyout.jobStatusLoadingMessage": "正在检查指标作业的状态......", + "xpack.infra.ml.anomalyFlyout.manageJobs": "管理作业", + "xpack.infra.ml.aomalyFlyout.jobSetup.flyoutHeader": "为 {nodeType} 启用 Machine Learning", + "xpack.infra.ml.metricsHostModuleDescription": "使用 Machine Learning 自动检测异常日志条目速率。", + "xpack.infra.ml.metricsModuleName": "指标异常检测", + "xpack.infra.ml.splash.learnMoreLink": "阅读文档", + "xpack.infra.ml.splash.learnMoreTitle": "希望了解详情?", + "xpack.infra.ml.splash.loadingMessage": "正在检查许可证......", + "xpack.infra.ml.splash.splashImageAlt": "占位符图像", + "xpack.infra.ml.splash.startTrialCta": "开始试用", + "xpack.infra.ml.splash.startTrialDescription": "我们的免费试用版包含 Machine Learning 功能,可用于检测日志中的异常。", + "xpack.infra.ml.splash.startTrialTitle": "要访问异常检测,请启动免费试用版", + "xpack.infra.ml.splash.updateSubscriptionCta": "升级订阅", + "xpack.infra.ml.splash.updateSubscriptionDescription": "必须具有白金级订阅,才能使用 Machine Learning 功能。", + "xpack.infra.ml.splash.updateSubscriptionTitle": "要访问异常检测,请升级到白金级订阅", + "xpack.infra.ml.steps.setupProcess.cancelButton": "取消", + "xpack.infra.ml.steps.setupProcess.description": "作业一旦创建,设置就无法更改。您可以随时重新创建作业,但是,以前检测到的异常将会移除。", + "xpack.infra.ml.steps.setupProcess.enableButton": "启用作业", + "xpack.infra.ml.steps.setupProcess.failureText": "创建必需的 ML 作业时出现问题。", + "xpack.infra.ml.steps.setupProcess.loadingText": "正在创建 ML 作业......", + "xpack.infra.ml.steps.setupProcess.partition.description": "通过分区,可为具有相似行为的数据组构建独立模型。例如,可按机器类型或云可用区分区。", + "xpack.infra.ml.steps.setupProcess.partition.label": "分区字段", + "xpack.infra.ml.steps.setupProcess.partition.title": "您想如何对数据进行分区?", + "xpack.infra.ml.steps.setupProcess.tryAgainButton": "重试", + "xpack.infra.ml.steps.setupProcess.when.description": "默认情况下,Machine Learning 作业会分析过去 4 周的数据,并继续无限期地运行。", + "xpack.infra.ml.steps.setupProcess.when.timePicker.label": "开始日期", + "xpack.infra.ml.steps.setupProcess.when.title": "您的模型何时开始?", "xpack.infra.node.ariaLabel": "{nodeName},单击打开菜单", "xpack.infra.nodeContextMenu.createAlertLink": "创建告警", "xpack.infra.nodeContextMenu.description": "查看 {label} {value} 的详情", @@ -8777,6 +9489,7 @@ "xpack.infra.savedView.searchPlaceholder": "搜索已保存视图", "xpack.infra.savedView.unknownView": "未选择视图", "xpack.infra.savedView.updateView": "更新视图", + "xpack.infra.showHistory": "显示历史记录", "xpack.infra.snapshot.missingSnapshotMetricError": "{nodeType} 的 {metric} 聚合不可用。", "xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "添加列", "xpack.infra.sourceConfiguration.applySettingsButtonLabel": "应用", @@ -8882,27 +9595,27 @@ "xpack.infra.waffle.metriclabel": "指标", "xpack.infra.waffle.metricOptions.countText": "计数", "xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用", - "xpack.infra.waffle.metricOptions.diskIOReadBytes": "磁盘读取", - "xpack.infra.waffle.metricOptions.diskIOWriteBytes": "磁盘写入", + "xpack.infra.waffle.metricOptions.diskIOReadBytes": "磁盘读取数", + "xpack.infra.waffle.metricOptions.diskIOWriteBytes": "磁盘写入数", "xpack.infra.waffle.metricOptions.hostLogRateText": "日志速率", "xpack.infra.waffle.metricOptions.inboundTrafficText": "入站流量", "xpack.infra.waffle.metricOptions.loadText": "负载", - "xpack.infra.waffle.metricOptions.memoryUsageText": "内存利用率", + "xpack.infra.waffle.metricOptions.memoryUsageText": "内存使用量", "xpack.infra.waffle.metricOptions.outboundTrafficText": "出站流量", - "xpack.infra.waffle.metricOptions.rdsActiveTransactions": "活动事务", - "xpack.infra.waffle.metricOptions.rdsConnections": "连接", + "xpack.infra.waffle.metricOptions.rdsActiveTransactions": "活动事务数", + "xpack.infra.waffle.metricOptions.rdsConnections": "连接数", "xpack.infra.waffle.metricOptions.rdsLatency": "延迟", - "xpack.infra.waffle.metricOptions.rdsQueriesExecuted": "已执行查询", + "xpack.infra.waffle.metricOptions.rdsQueriesExecuted": "已执行的查询数", "xpack.infra.waffle.metricOptions.s3BucketSize": "存储桶大小", - "xpack.infra.waffle.metricOptions.s3DownloadBytes": "下载(字节)", + "xpack.infra.waffle.metricOptions.s3DownloadBytes": "下载量(字节)", "xpack.infra.waffle.metricOptions.s3NumberOfObjects": "对象数目", "xpack.infra.waffle.metricOptions.s3TotalRequests": "请求总数", - "xpack.infra.waffle.metricOptions.s3UploadBytes": "上传(字节)", - "xpack.infra.waffle.metricOptions.sqsMessagesDelayed": "已推迟消息", - "xpack.infra.waffle.metricOptions.sqsMessagesEmpty": "已返回消息为空", - "xpack.infra.waffle.metricOptions.sqsMessagesSent": "已添加消息", - "xpack.infra.waffle.metricOptions.sqsMessagesVisible": "可用消息", - "xpack.infra.waffle.metricOptions.sqsOldestMessage": "最旧消息", + "xpack.infra.waffle.metricOptions.s3UploadBytes": "上传量(字节)", + "xpack.infra.waffle.metricOptions.sqsMessagesDelayed": "延迟的消息数", + "xpack.infra.waffle.metricOptions.sqsMessagesEmpty": "返回空的消息数", + "xpack.infra.waffle.metricOptions.sqsMessagesSent": "已添加的消息数", + "xpack.infra.waffle.metricOptions.sqsMessagesVisible": "可用消息数", + "xpack.infra.waffle.metricOptions.sqsOldestMessage": "最早的消息数", "xpack.infra.waffle.noDataDescription": "尝试调整您的时间或筛选。", "xpack.infra.waffle.noDataTitle": "没有可显示的数据。", "xpack.infra.waffle.region": "全部", @@ -8926,38 +9639,59 @@ "xpack.infra.waffle.unableToSelectMetricErrorTitle": "无法选择指标选项或指标值。", "xpack.infra.waffleTime.autoRefreshButtonLabel": "自动刷新", "xpack.infra.waffleTime.stopRefreshingButtonLabel": "停止刷新", + "xpack.ingestManager.agentBulkActions.agentsSelected": "已选择 {count, plural, one {# 个代理} other {# 个代理}}", + "xpack.ingestManager.agentBulkActions.clearSelection": "清除所选内容", + "xpack.ingestManager.agentBulkActions.reassignPolicy": "分配到新策略", + "xpack.ingestManager.agentBulkActions.selectAll": "选择所有页面上的所有内容", + "xpack.ingestManager.agentBulkActions.totalAgents": "正在显示 {count, plural, one {# 个代理} other {# 个代理}}", + "xpack.ingestManager.agentBulkActions.totalAgentsWithLimit": "正在显示 {count} 个代理(共 {total} 个)", + "xpack.ingestManager.agentBulkActions.unenrollAgents": "取消注册代理", + "xpack.ingestManager.agentBulkActions.upgradeAgents": "升级代理", "xpack.ingestManager.agentDetails.actionsButton": "操作", "xpack.ingestManager.agentDetails.agentDetailsTitle": "代理“{id}”", "xpack.ingestManager.agentDetails.agentNotFoundErrorDescription": "找不到代理 ID {agentId}", "xpack.ingestManager.agentDetails.agentNotFoundErrorTitle": "未找到代理", + "xpack.ingestManager.agentDetails.agentPolicyLabel": "代理策略", + "xpack.ingestManager.agentDetails.agentVersionLabel": "代理版本", "xpack.ingestManager.agentDetails.hostIdLabel": "代理 ID", "xpack.ingestManager.agentDetails.hostNameLabel": "主机名", "xpack.ingestManager.agentDetails.localMetadataSectionSubtitle": "本地元数据", "xpack.ingestManager.agentDetails.metadataSectionTitle": "元数据", "xpack.ingestManager.agentDetails.platformLabel": "平台", + "xpack.ingestManager.agentDetails.policyLabel": "策略", + "xpack.ingestManager.agentDetails.releaseLabel": "代理发行版", "xpack.ingestManager.agentDetails.statusLabel": "状态", "xpack.ingestManager.agentDetails.subTabs.activityLogTab": "活动日志", "xpack.ingestManager.agentDetails.subTabs.detailsTab": "代理详情", "xpack.ingestManager.agentDetails.unexceptedErrorTitle": "加载代理时出错", + "xpack.ingestManager.agentDetails.upgradeAvailableTooltip": "升级可用", "xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "用户提供的元数据", "xpack.ingestManager.agentDetails.versionLabel": "代理版本", "xpack.ingestManager.agentDetails.viewAgentListTitle": "查看所有代理", + "xpack.ingestManager.agentEnrollment.agentDescription": "将 Elastic 代理添加到您的主机,以收集数据并将其发送到 Elastic Stack。", + "xpack.ingestManager.agentEnrollment.agentsNotInitializedText": "注册代理前,请{link}。", "xpack.ingestManager.agentEnrollment.cancelButtonLabel": "取消", "xpack.ingestManager.agentEnrollment.continueButtonLabel": "继续", + "xpack.ingestManager.agentEnrollment.copyPolicyButton": "复制到剪贴板", "xpack.ingestManager.agentEnrollment.copyRunInstructionsButton": "复制到剪贴板", - "xpack.ingestManager.agentEnrollment.downloadDescription": "在主机计算机上下载 Elastic 代理。可以从 Elastic 代理下载页面访问代理二进制文件及其验证签名。", - "xpack.ingestManager.agentEnrollment.downloadLink": "前往 elastic.co/downloads", - "xpack.ingestManager.agentEnrollment.enrollFleetTabLabel": "注册到 Fleet", - "xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "独立模式", + "xpack.ingestManager.agentEnrollment.downloadDescription": "可从 Elastic 代理下载页面下载代理二进制文件及其验证签名。", + "xpack.ingestManager.agentEnrollment.downloadLink": "前往下载页面", + "xpack.ingestManager.agentEnrollment.downloadPolicyButton": "下载策略", + "xpack.ingestManager.agentEnrollment.enrollFleetTabLabel": "在 Fleet 中注册", + "xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "独立运行", "xpack.ingestManager.agentEnrollment.flyoutTitle": "添加代理", - "xpack.ingestManager.agentEnrollment.managedDescription": "无论是需要一个代理还是需要数以千计的代理,Fleet 允许您轻松地集中管理并部署代理的更新。按照下面的说明下载 Elastic 代理并将代理注册到 Fleet。", - "xpack.ingestManager.agentEnrollment.standaloneDescription": "如果希望对以独立模式运行的代理进行配置更改,则需要手动更新。按照下面的说明下载并设置独立模式的 Elastic 代理。", + "xpack.ingestManager.agentEnrollment.goToDataStreamsLink": "数据流", + "xpack.ingestManager.agentEnrollment.managedDescription": "在 Fleet 中注册 Elastic 代理,以便自动部署更新并集中管理该代理。", + "xpack.ingestManager.agentEnrollment.setUpAgentsLink": "为 Elastic 代理设置集中管理", + "xpack.ingestManager.agentEnrollment.standaloneDescription": "独立运行 Elastic 代理,以在安装代理的主机上手动配置和更新代理。", + "xpack.ingestManager.agentEnrollment.stepCheckForDataDescription": "该代理应该开始发送数据。前往 {link} 以查看您的数据。", "xpack.ingestManager.agentEnrollment.stepCheckForDataTitle": "检查数据", - "xpack.ingestManager.agentEnrollment.stepConfigureAgentDescription": "在安装 Elastic 代理的系统上复制此配置并将其放入名为 {fileName} 的文件中。切勿忘记修改配置文件中 {outputSection} 部分的{ESUsernameVariable} 和 {ESPasswordVariable},以便其使用您的实际 Elasticsearch 凭据。", + "xpack.ingestManager.agentEnrollment.stepChooseAgentPolicyTitle": "选择代理策略", + "xpack.ingestManager.agentEnrollment.stepConfigureAgentDescription": "在安装 Elastic 代理的主机上将此策略复制到 {fileName}。在 {fileName} 的 {outputSection} 部分中修改 {ESUsernameVariable} 和 {ESPasswordVariable},以使用您的 Elasticsearch 凭据。", "xpack.ingestManager.agentEnrollment.stepConfigureAgentTitle": "配置代理", - "xpack.ingestManager.agentEnrollment.stepDownloadAgentTitle": "下载 Elastic 代理", + "xpack.ingestManager.agentEnrollment.stepDownloadAgentTitle": "将 Elastic 代理下载到您的主机", "xpack.ingestManager.agentEnrollment.stepEnrollAndRunAgentTitle": "注册并启动 Elastic 代理", - "xpack.ingestManager.agentEnrollment.stepRunAgentDescription": "从代理的目录中,运行以下命令以启动代理。", + "xpack.ingestManager.agentEnrollment.stepRunAgentDescription": "从代理目录运行此命令,以安装、注册并启动 Elastic 代理。您可以重复使用此命令在多个主机上设置代理。需要管理员权限。", "xpack.ingestManager.agentEnrollment.stepRunAgentTitle": "启动代理", "xpack.ingestManager.agentEventsList.collapseDetailsAriaLabel": "隐藏详情", "xpack.ingestManager.agentEventsList.expandDetailsAriaLabel": "显示详情", @@ -8974,11 +9708,13 @@ "xpack.ingestManager.agentEventSubtype.degradedLabel": "已降级", "xpack.ingestManager.agentEventSubtype.failedLabel": "失败", "xpack.ingestManager.agentEventSubtype.inProgressLabel": "进行中", + "xpack.ingestManager.agentEventSubtype.policyLabel": "策略", "xpack.ingestManager.agentEventSubtype.runningLabel": "正在运行", "xpack.ingestManager.agentEventSubtype.startingLabel": "正在启动", "xpack.ingestManager.agentEventSubtype.stoppedLabel": "已停止", "xpack.ingestManager.agentEventSubtype.stoppingLabel": "正在停止", "xpack.ingestManager.agentEventSubtype.unknownLabel": "未知", + "xpack.ingestManager.agentEventSubtype.updatingLabel": "正在更新", "xpack.ingestManager.agentEventType.actionLabel": "操作", "xpack.ingestManager.agentEventType.actionResultLabel": "操作结果", "xpack.ingestManager.agentEventType.errorLabel": "错误", @@ -8992,9 +9728,11 @@ "xpack.ingestManager.agentHealth.offlineStatusText": "脱机", "xpack.ingestManager.agentHealth.onlineStatusText": "联机", "xpack.ingestManager.agentHealth.unenrollingStatusText": "正在取消注册", + "xpack.ingestManager.agentHealth.updatingStatusText": "正在更新", "xpack.ingestManager.agentHealth.warningStatusText": "错误", "xpack.ingestManager.agentList.actionsColumnTitle": "操作", "xpack.ingestManager.agentList.addButton": "添加代理", + "xpack.ingestManager.agentList.agentUpgradeLabel": "升级可用", "xpack.ingestManager.agentList.clearFiltersLinkText": "清除筛选", "xpack.ingestManager.agentList.enrollButton": "添加代理", "xpack.ingestManager.agentList.forceUnenrollOneButton": "强制取消注册", @@ -9004,87 +9742,258 @@ "xpack.ingestManager.agentList.noAgentsPrompt": "未注册任何代理", "xpack.ingestManager.agentList.noFilteredAgentsPrompt": "未找到任何代理。{clearFiltersLink}", "xpack.ingestManager.agentList.outOfDateLabel": "过时", - "xpack.ingestManager.agentList.reassignActionText": "分配新代理配置", + "xpack.ingestManager.agentList.policyColumnTitle": "代理策略", + "xpack.ingestManager.agentList.policyFilterText": "代理策略", + "xpack.ingestManager.agentList.reassignActionText": "分配到新策略", "xpack.ingestManager.agentList.revisionNumber": "修订 {revNumber}", - "xpack.ingestManager.agentList.showInactiveSwitchLabel": "显示非活动", + "xpack.ingestManager.agentList.showInactiveSwitchLabel": "非活动", + "xpack.ingestManager.agentList.showUpgradeableFilterLabel": "升级可用", "xpack.ingestManager.agentList.statusColumnTitle": "状态", "xpack.ingestManager.agentList.statusErrorFilterText": "错误", "xpack.ingestManager.agentList.statusFilterText": "状态", "xpack.ingestManager.agentList.statusOfflineFilterText": "脱机", "xpack.ingestManager.agentList.statusOnlineFilterText": "联机", - "xpack.ingestManager.agentList.unenrollOneButton": "取消注册", + "xpack.ingestManager.agentList.statusUpdatingFilterText": "正在更新", + "xpack.ingestManager.agentList.unenrollOneButton": "取消注册代理", + "xpack.ingestManager.agentList.upgradeOneButton": "升级代理", "xpack.ingestManager.agentList.versionTitle": "版本", "xpack.ingestManager.agentList.viewActionText": "查看代理", "xpack.ingestManager.agentListStatus.errorLabel": "错误", "xpack.ingestManager.agentListStatus.offlineLabel": "脱机", "xpack.ingestManager.agentListStatus.onlineLabel": "联机", "xpack.ingestManager.agentListStatus.totalLabel": "代理", + "xpack.ingestManager.agentPolicy.confirmModalCalloutDescription": "Fleet 检测到您的部分代理已在使用选定代理策略 {policyName}。由于此操作,Fleet 会将更新部署到使用此策略的所有代理。", + "xpack.ingestManager.agentPolicy.confirmModalCalloutTitle": "此操作将更新 {agentCount, plural, one {# 个代理} other {# 个代理}}", + "xpack.ingestManager.agentPolicy.confirmModalCancelButtonLabel": "取消", + "xpack.ingestManager.agentPolicy.confirmModalConfirmButtonLabel": "保存并部署更改", + "xpack.ingestManager.agentPolicy.confirmModalDescription": "此操作无法撤消。是否确定要继续?", + "xpack.ingestManager.agentPolicy.confirmModalTitle": "保存并部署更改", + "xpack.ingestManager.agentPolicy.linkedAgentCountText": "{count, plural, one {# 个代理} other {# 个代理}}", + "xpack.ingestManager.agentPolicyActionMenu.buttonText": "操作", + "xpack.ingestManager.agentPolicyActionMenu.copyPolicyActionText": "复制策略", + "xpack.ingestManager.agentPolicyActionMenu.enrollAgentActionText": "添加代理", + "xpack.ingestManager.agentPolicyActionMenu.viewPolicyText": "查看策略", + "xpack.ingestManager.agentPolicyForm.advancedOptionsToggleLabel": "高级选项", + "xpack.ingestManager.agentPolicyForm.descriptionFieldLabel": "描述", + "xpack.ingestManager.agentPolicyForm.descriptionFieldPlaceholder": "此策略将如何使用?", + "xpack.ingestManager.agentPolicyForm.monitoringDescription": "收集有关代理的数据,用于调试和跟踪性能。", + "xpack.ingestManager.agentPolicyForm.monitoringLabel": "代理监测", + "xpack.ingestManager.agentPolicyForm.monitoringLogsFieldLabel": "收集代理日志", + "xpack.ingestManager.agentPolicyForm.monitoringLogsTooltipText": "从使用此策略的 Elastic 代理收集日志。", + "xpack.ingestManager.agentPolicyForm.monitoringMetricsFieldLabel": "收集代理指标", + "xpack.ingestManager.agentPolicyForm.monitoringMetricsTooltipText": "从使用此策略的 Elastic 代理收集指标。", + "xpack.ingestManager.agentPolicyForm.nameFieldLabel": "名称", + "xpack.ingestManager.agentPolicyForm.nameFieldPlaceholder": "选择名称", + "xpack.ingestManager.agentPolicyForm.nameRequiredErrorMessage": "“代理策略名称”必填。", + "xpack.ingestManager.agentPolicyForm.namespaceFieldDescription": "将默认命名空间应用于使用此策略的集成。集成可以指定自己的命名空间。", + "xpack.ingestManager.agentPolicyForm.namespaceFieldLabel": "默认命名空间", + "xpack.ingestManager.agentPolicyForm.systemMonitoringFieldLabel": "系统监测", + "xpack.ingestManager.agentPolicyForm.systemMonitoringText": "收集系统指标", + "xpack.ingestManager.agentPolicyForm.systemMonitoringTooltipText": "启用此选项可使用收集系统指标和信息的集成启动您的策略。", + "xpack.ingestManager.agentPolicyList.actionsColumnTitle": "操作", + "xpack.ingestManager.agentPolicyList.addButton": "创建代理策略", + "xpack.ingestManager.agentPolicyList.agentsColumnTitle": "代理", + "xpack.ingestManager.agentPolicyList.clearFiltersLinkText": "清除筛选", + "xpack.ingestManager.agentPolicyList.descriptionColumnTitle": "描述", + "xpack.ingestManager.agentPolicyList.loadingAgentPoliciesMessage": "正在加载代理策略…...", + "xpack.ingestManager.agentPolicyList.nameColumnTitle": "名称", + "xpack.ingestManager.agentPolicyList.noAgentPoliciesPrompt": "无代理策略", + "xpack.ingestManager.agentPolicyList.noFilteredAgentPoliciesPrompt": "找不到任何代理策略。{clearFiltersLink}", + "xpack.ingestManager.agentPolicyList.packagePoliciesCountColumnTitle": "集成", + "xpack.ingestManager.agentPolicyList.pageSubtitle": "使用代理策略管理代理及其收集的数据。", + "xpack.ingestManager.agentPolicyList.pageTitle": "代理策略", + "xpack.ingestManager.agentPolicyList.reloadAgentPoliciesButtonText": "重新加载", + "xpack.ingestManager.agentPolicyList.revisionNumber": "修订版 {revNumber}", + "xpack.ingestManager.agentPolicyList.updatedOnColumnTitle": "上次更新时间", + "xpack.ingestManager.agentReassignPolicy.cancelButtonLabel": "取消", + "xpack.ingestManager.agentReassignPolicy.continueButtonLabel": "分配策略", + "xpack.ingestManager.agentReassignPolicy.flyoutDescription": "选择要将选定{count, plural, one {代理} other {代理}}分配到的新代理策略。", + "xpack.ingestManager.agentReassignPolicy.flyoutTitle": "分配新代理策略", + "xpack.ingestManager.agentReassignPolicy.policyDescription": "选定代理策略将收集 {count, plural, one {{countValue} 个集成} other {{countValue} 个集成}}的数据:", + "xpack.ingestManager.agentReassignPolicy.selectPolicyLabel": "代理策略", + "xpack.ingestManager.agentReassignPolicy.successSingleNotificationTitle": "代理策略已重新分配", + "xpack.ingestManager.agents.pageSubtitle": "管理策略更新并将其部署到一组任意大小的代理。", + "xpack.ingestManager.agents.pageTitle": "代理", "xpack.ingestManager.alphaMessageDescription": "不推荐在生产环境中使用采集管理器。", "xpack.ingestManager.alphaMessageLinkText": "查看更多详情。", "xpack.ingestManager.alphaMessageTitle": "公测版", "xpack.ingestManager.alphaMessaging.docsLink": "文档", - "xpack.ingestManager.alphaMessaging.feedbackText": "建议您阅读我们的“{docsLink}”或在我们的{forumLink}提问题或发送反馈。", + "xpack.ingestManager.alphaMessaging.feedbackText": "阅读我们的{docsLink}或前往我们的{forumLink},以了解问题或提供反馈。", "xpack.ingestManager.alphaMessaging.flyoutTitle": "关于本版本", "xpack.ingestManager.alphaMessaging.forumLink": "讨论论坛", "xpack.ingestManager.alphaMessaging.introText": "采集管理器仍处于开发状态,不适用于生产环境。此公测版用于用户测试采集管理器和新 Elastic 代理并提供相关反馈。此插件不受支持 SLA 的约束。", "xpack.ingestManager.alphaMessging.closeFlyoutLabel": "关闭", - "xpack.ingestManager.appNavigation.dataStreamsLinkText": "数据集", + "xpack.ingestManager.appNavigation.agentsLinkText": "代理", + "xpack.ingestManager.appNavigation.dataStreamsLinkText": "数据流", "xpack.ingestManager.appNavigation.epmLinkText": "集成", "xpack.ingestManager.appNavigation.overviewLinkText": "概览", + "xpack.ingestManager.appNavigation.policiesLinkText": "策略", "xpack.ingestManager.appNavigation.sendFeedbackButton": "发送反馈", "xpack.ingestManager.appNavigation.settingsButton": "设置", - "xpack.ingestManager.appTitle": "Ingest Manager", + "xpack.ingestManager.appTitle": "Fleet", "xpack.ingestManager.betaBadge.labelText": "公测版", "xpack.ingestManager.betaBadge.tooltipText": "不推荐在生产环境中使用此插件。请在我们讨论论坛中报告错误。", + "xpack.ingestManager.breadcrumbs.addPackagePolicyPageTitle": "添加集成", + "xpack.ingestManager.breadcrumbs.agentsPageTitle": "代理", "xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle": "全部", - "xpack.ingestManager.breadcrumbs.appTitle": "采集管理器", - "xpack.ingestManager.breadcrumbs.datastreamsPageTitle": "数据集", + "xpack.ingestManager.breadcrumbs.appTitle": "Fleet", + "xpack.ingestManager.breadcrumbs.datastreamsPageTitle": "数据流", + "xpack.ingestManager.breadcrumbs.editPackagePolicyPageTitle": "编辑集成", + "xpack.ingestManager.breadcrumbs.enrollmentTokensPageTitle": "注册令牌", "xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle": "已安装", "xpack.ingestManager.breadcrumbs.integrationsPageTitle": "集成", "xpack.ingestManager.breadcrumbs.overviewPageTitle": "概览", + "xpack.ingestManager.breadcrumbs.policiesPageTitle": "策略", + "xpack.ingestManager.copyAgentPolicy.confirmModal.cancelButtonLabel": "取消", + "xpack.ingestManager.copyAgentPolicy.confirmModal.confirmButtonLabel": "复制策略", + "xpack.ingestManager.copyAgentPolicy.confirmModal.copyPolicyPrompt": "为您的新代理策略选择名称和描述。", + "xpack.ingestManager.copyAgentPolicy.confirmModal.copyPolicyTitle": "复制代理策略“{name}”", + "xpack.ingestManager.copyAgentPolicy.confirmModal.defaultNewPolicyName": "{name}(副本)", + "xpack.ingestManager.copyAgentPolicy.confirmModal.newDescriptionLabel": "描述", + "xpack.ingestManager.copyAgentPolicy.confirmModal.newNameLabel": "新策略名称", + "xpack.ingestManager.copyAgentPolicy.failureNotificationTitle": "复制代理策略“{id}”时出错", + "xpack.ingestManager.copyAgentPolicy.fatalErrorNotificationTitle": "复制代理策略时出错", + "xpack.ingestManager.copyAgentPolicy.successNotificationTitle": "代理策略已复制", + "xpack.ingestManager.createAgentPolicy.cancelButtonLabel": "取消", + "xpack.ingestManager.createAgentPolicy.errorNotificationTitle": "无法创建代理策略", + "xpack.ingestManager.createAgentPolicy.flyoutTitle": "创建代理策略", + "xpack.ingestManager.createAgentPolicy.flyoutTitleDescription": "代理策略用于管理一组代理的设置。您可以将集成添加到代理策略,以指定代理收集的数据。编辑代理策略时,可以使用 Fleet 将更新部署到一组指定代理。", + "xpack.ingestManager.createAgentPolicy.submitButtonLabel": "创建代理策略", + "xpack.ingestManager.createAgentPolicy.successNotificationTitle": "代理策略“{name}”已创建", + "xpack.ingestManager.createPackagePolicy.addedNotificationMessage": "Fleet 会将更新部署到所有使用策略“{agentPolicyName}”的代理。", + "xpack.ingestManager.createPackagePolicy.addedNotificationTitle": "“{packagePolicyName}”集成已添加。", + "xpack.ingestManager.createPackagePolicy.agentPolicyNameLabel": "代理策略", + "xpack.ingestManager.createPackagePolicy.cancelButton": "取消", + "xpack.ingestManager.createPackagePolicy.cancelLinkText": "取消", + "xpack.ingestManager.createPackagePolicy.errorOnSaveText": "您的集成策略有错误。请在保存前修复这些错误。", + "xpack.ingestManager.createPackagePolicy.pageDescriptionfromPackage": "按照以下说明将此集成添加到代理策略。", + "xpack.ingestManager.createPackagePolicy.pageDescriptionfromPolicy": "为选定代理策略配置集成。", + "xpack.ingestManager.createPackagePolicy.pageTitle": "添加集成", + "xpack.ingestManager.createPackagePolicy.pageTitleWithPackageName": "添加 {packageName} 集成", + "xpack.ingestManager.createPackagePolicy.saveButton": "保存集成", + "xpack.ingestManager.createPackagePolicy.stepConfigure.advancedOptionsToggleLinkText": "高级选项", + "xpack.ingestManager.createPackagePolicy.stepConfigure.errorCountText": "{count, plural, one {# 个错误} other {# 个错误}}", + "xpack.ingestManager.createPackagePolicy.stepConfigure.hideStreamsAriaLabel": "隐藏 {type} 输入", + "xpack.ingestManager.createPackagePolicy.stepConfigure.inputSettingsDescription": "以下设置适用于下面的所有输入。", + "xpack.ingestManager.createPackagePolicy.stepConfigure.inputSettingsTitle": "设置", + "xpack.ingestManager.createPackagePolicy.stepConfigure.inputVarFieldOptionalLabel": "可选", + "xpack.ingestManager.createPackagePolicy.stepConfigure.integrationSettingsSectionDescription": "选择有助于确定如何使用此集成的名称和描述。", + "xpack.ingestManager.createPackagePolicy.stepConfigure.integrationSettingsSectionTitle": "集成设置", + "xpack.ingestManager.createPackagePolicy.stepConfigure.noPolicyOptionsMessage": "没有可配置的内容", + "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyDescriptionInputLabel": "描述", + "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyNameInputLabel": "集成名称", + "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyNamespaceInputLabel": "命名空间", + "xpack.ingestManager.createPackagePolicy.stepConfigure.showStreamsAriaLabel": "显示 {type} 输入", + "xpack.ingestManager.createPackagePolicy.stepConfigure.toggleAdvancedOptionsButtonText": "高级选项", + "xpack.ingestManager.createPackagePolicy.stepConfigurePackagePolicyTitle": "配置集成", + "xpack.ingestManager.createPackagePolicy.stepSelectAgentPolicyTitle": "选择代理策略", + "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingPackagesTitle": "加载集成时出错", + "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingPolicyTitle": "加载代理策略信息时出错", + "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定集成时出错", + "xpack.ingestManager.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder": "搜索集成", + "xpack.ingestManager.createPackagePolicy.stepSelectPackageTitle": "选择集成", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.addButton": "创建代理策略", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsCountText": "{count, plural, one {# 个代理} other {# 个代理}}已注册", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsDescriptionText": "{count, plural, one {# 个代理} other {# 个代理}}已注册到选定代理策略中。", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.agentPolicyLabel": "代理策略", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.agentPolicyPlaceholderText": "选择要将此集成添加到的代理策略", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingAgentPoliciesTitle": "加载代理策略时出错", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingPackageTitle": "加载软件包信息时出错", + "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingSelectedAgentPolicyTitle": "加载选定代理策略时出错", "xpack.ingestManager.dataStreamList.actionsColumnTitle": "操作", "xpack.ingestManager.dataStreamList.datasetColumnTitle": "数据集", "xpack.ingestManager.dataStreamList.integrationColumnTitle": "集成", "xpack.ingestManager.dataStreamList.lastActivityColumnTitle": "上次活动", - "xpack.ingestManager.dataStreamList.loadingDataStreamsMessage": "正在加载数据集……", + "xpack.ingestManager.dataStreamList.loadingDataStreamsMessage": "正在加载数据流……", "xpack.ingestManager.dataStreamList.namespaceColumnTitle": "命名空间", - "xpack.ingestManager.dataStreamList.noDataStreamsPrompt": "无数据集", - "xpack.ingestManager.dataStreamList.noFilteredDataStreamsMessage": "找不到匹配的数据集", + "xpack.ingestManager.dataStreamList.noDataStreamsPrompt": "无数据流", + "xpack.ingestManager.dataStreamList.noFilteredDataStreamsMessage": "找不到匹配的数据流", "xpack.ingestManager.dataStreamList.pageSubtitle": "管理您的代理创建的数据。", - "xpack.ingestManager.dataStreamList.pageTitle": "数据集", + "xpack.ingestManager.dataStreamList.pageTitle": "数据流", "xpack.ingestManager.dataStreamList.reloadDataStreamsButtonText": "重新加载", - "xpack.ingestManager.dataStreamList.searchPlaceholderTitle": "筛选数据集", + "xpack.ingestManager.dataStreamList.searchPlaceholderTitle": "筛选数据流", "xpack.ingestManager.dataStreamList.sizeColumnTitle": "大小", "xpack.ingestManager.dataStreamList.typeColumnTitle": "类型", "xpack.ingestManager.dataStreamList.viewDashboardActionText": "查看仪表板", "xpack.ingestManager.dataStreamList.viewDashboardsActionText": "查看仪表板", "xpack.ingestManager.dataStreamList.viewDashboardsPanelTitle": "查看仪表板", "xpack.ingestManager.defaultSearchPlaceholderText": "搜索", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# 个代理} other {# 个代理}}已分配到此代理策略。在删除此策略前取消分配这些代理。", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.affectedAgentsTitle": "在用的策略", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.cancelButtonLabel": "取消", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.confirmButtonLabel": "删除策略", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.deletePolicyTitle": "删除此代理策略?", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.irreversibleMessage": "此操作无法撤消。", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.loadingAgentsCountMessage": "正在检查受影响的代理数量……", + "xpack.ingestManager.deleteAgentPolicy.confirmModal.loadingButtonLabel": "正在加载……", + "xpack.ingestManager.deleteAgentPolicy.failureSingleNotificationTitle": "删除代理策略“{id}”时出错", + "xpack.ingestManager.deleteAgentPolicy.fatalErrorNotificationTitle": "删除代理策略时出错", + "xpack.ingestManager.deleteAgentPolicy.successSingleNotificationTitle": "已删除代理策略“{id}”", + "xpack.ingestManager.deletePackagePolicy.confirmModal.affectedAgentsMessage": "Fleet 检测到您的部分代理已在使用 {agentPolicyName}。", + "xpack.ingestManager.deletePackagePolicy.confirmModal.affectedAgentsTitle": "此操作将影响 {agentsCount} 个{agentsCount, plural, one {代理} other {代理}}。", + "xpack.ingestManager.deletePackagePolicy.confirmModal.cancelButtonLabel": "取消", + "xpack.ingestManager.deletePackagePolicy.confirmModal.confirmButtonLabel": "删除{agentPoliciesCount, plural, one {集成} other {集成}}", + "xpack.ingestManager.deletePackagePolicy.confirmModal.deleteMultipleTitle": "删除 {count, plural, one {集成} other {# 个集成}}?", + "xpack.ingestManager.deletePackagePolicy.confirmModal.generalMessage": "此操作无法撤消。是否确定要继续?", + "xpack.ingestManager.deletePackagePolicy.confirmModal.loadingAgentsCountMessage": "正在检查受影响的代理……", + "xpack.ingestManager.deletePackagePolicy.confirmModal.loadingButtonLabel": "正在加载……", + "xpack.ingestManager.deletePackagePolicy.failureMultipleNotificationTitle": "删除 {count} 个集成时出错", + "xpack.ingestManager.deletePackagePolicy.failureSingleNotificationTitle": "删除集成“{id}”时出错", + "xpack.ingestManager.deletePackagePolicy.fatalErrorNotificationTitle": "删除集成时出错", + "xpack.ingestManager.deletePackagePolicy.successMultipleNotificationTitle": "已删除 {count} 个集成", + "xpack.ingestManager.deletePackagePolicy.successSingleNotificationTitle": "已删除集成“{id}”", "xpack.ingestManager.disabledSecurityDescription": "必须在 Kibana 和 Elasticsearch 启用安全性,才能使用 Elastic Fleet。", "xpack.ingestManager.disabledSecurityTitle": "安全性未启用", + "xpack.ingestManager.editAgentPolicy.cancelButtonText": "取消", + "xpack.ingestManager.editAgentPolicy.errorNotificationTitle": "无法更新代理策略", + "xpack.ingestManager.editAgentPolicy.saveButtonText": "保存更改", + "xpack.ingestManager.editAgentPolicy.savingButtonText": "正在保存……", + "xpack.ingestManager.editAgentPolicy.successNotificationTitle": "已成功更新“{name}”设置", + "xpack.ingestManager.editAgentPolicy.unsavedChangesText": "您有未保存的更改", + "xpack.ingestManager.editPackagePolicy.cancelButton": "取消", + "xpack.ingestManager.editPackagePolicy.errorLoadingDataMessage": "加载此集成信息时出错", + "xpack.ingestManager.editPackagePolicy.errorLoadingDataTitle": "加载数据时出错", + "xpack.ingestManager.editPackagePolicy.failedConflictNotificationMessage": "数据已过时。刷新页面以获取最新策略。", + "xpack.ingestManager.editPackagePolicy.failedNotificationTitle": "更新“{packagePolicyName}”时出错", + "xpack.ingestManager.editPackagePolicy.pageDescription": "修改集成设置并将更改部署到选定代理策略。", + "xpack.ingestManager.editPackagePolicy.pageTitle": "编辑集成", + "xpack.ingestManager.editPackagePolicy.pageTitleWithPackageName": "编辑 {packageName} 集成", + "xpack.ingestManager.editPackagePolicy.saveButton": "保存集成", + "xpack.ingestManager.editPackagePolicy.updatedNotificationMessage": "Fleet 会将更新部署到所有使用策略“{agentPolicyName}”的代理", + "xpack.ingestManager.editPackagePolicy.updatedNotificationTitle": "已成功更新“{packagePolicyName}”", "xpack.ingestManager.enrollemntAPIKeyList.emptyMessage": "未找到任何注册令牌。", "xpack.ingestManager.enrollemntAPIKeyList.loadingTokensMessage": "正在加载注册令牌......", - "xpack.ingestManager.enrollmentInstructions.descriptionText": "从代理的目录,运行相应命令以注册并启动 Elastic 代理。您可以重复使用这些命令在多台机器上设置代理。请务必以具有系统“管理员”权限的用户身份执行注册步骤。", + "xpack.ingestManager.enrollmentInstructions.descriptionText": "从代理目录运行相应命令,以安装、注册并启动 Elastic 代理。您可以重复使用这些命令在多个主机上设置代理。需要管理员权限。", + "xpack.ingestManager.enrollmentInstructions.linuxMacOSTitle": "Linux、macOS", + "xpack.ingestManager.enrollmentInstructions.moreInstructionsLink": "Elastic 代理文档", + "xpack.ingestManager.enrollmentInstructions.moreInstructionsText": "有关更多说明和选项,请参见 {link}。", "xpack.ingestManager.enrollmentInstructions.windowsTitle": "Windows", + "xpack.ingestManager.enrollmentStepAgentPolicy.enrollmentTokenSelectLabel": "注册令牌", + "xpack.ingestManager.enrollmentStepAgentPolicy.policySelectAriaLabel": "代理策略", + "xpack.ingestManager.enrollmentStepAgentPolicy.policySelectLabel": "代理策略", + "xpack.ingestManager.enrollmentStepAgentPolicy.showAuthenticationSettingsButton": "身份验证设置", "xpack.ingestManager.enrollmentTokenDeleteModal.cancelButton": "取消", - "xpack.ingestManager.enrollmentTokenDeleteModal.deleteButton": "删除", - "xpack.ingestManager.enrollmentTokenDeleteModal.description": "确定要删除 {keyName}。", - "xpack.ingestManager.enrollmentTokenDeleteModal.title": "删除注册令牌", + "xpack.ingestManager.enrollmentTokenDeleteModal.deleteButton": "撤销注册令牌", + "xpack.ingestManager.enrollmentTokenDeleteModal.description": "确定要撤销 {keyName}?使用此令牌的代理将无法再访问策略或发送数据。 ", + "xpack.ingestManager.enrollmentTokenDeleteModal.title": "撤销注册令牌", "xpack.ingestManager.enrollmentTokensList.actionsTitle": "操作", "xpack.ingestManager.enrollmentTokensList.activeTitle": "活动", "xpack.ingestManager.enrollmentTokensList.createdAtTitle": "创建日期", "xpack.ingestManager.enrollmentTokensList.hideTokenButtonLabel": "隐藏令牌", "xpack.ingestManager.enrollmentTokensList.nameTitle": "名称", - "xpack.ingestManager.enrollmentTokensList.newKeyButton": "新建注册令牌", - "xpack.ingestManager.enrollmentTokensList.pageDescription": "这是可用于注册代理的注册令牌列表。", + "xpack.ingestManager.enrollmentTokensList.newKeyButton": "创建注册令牌", + "xpack.ingestManager.enrollmentTokensList.pageDescription": "创建和撤销注册令牌。注册令牌允许一个或多个代理注册于 Fleet 中并发送数据。", + "xpack.ingestManager.enrollmentTokensList.policyTitle": "代理策略", "xpack.ingestManager.enrollmentTokensList.revokeTokenButtonLabel": "撤销令牌", "xpack.ingestManager.enrollmentTokensList.secretTitle": "密钥", "xpack.ingestManager.enrollmentTokensList.showTokenButtonLabel": "显示令牌", + "xpack.ingestManager.epm.addPackagePolicyButtonText": "添加 {packageName}", "xpack.ingestManager.epm.assetGroupTitle": "{assetType} 资产", "xpack.ingestManager.epm.browseAllButtonText": "浏览所有集成", "xpack.ingestManager.epm.illustrationAltText": "集成的图示", "xpack.ingestManager.epm.loadingIntegrationErrorTitle": "加载集成详情时出错", "xpack.ingestManager.epm.packageDetailsNav.overviewLinkText": "概览", + "xpack.ingestManager.epm.packageDetailsNav.packagePoliciesLinkText": "使用情况", "xpack.ingestManager.epm.packageDetailsNav.settingsLinkText": "设置", "xpack.ingestManager.epm.pageSubtitle": "浏览集成以了解热门应用和服务。", "xpack.ingestManager.epm.pageTitle": "集成", @@ -9104,13 +10013,15 @@ "xpack.ingestManager.epmList.noPackagesFoundPlaceholder": "未找到任何软件包", "xpack.ingestManager.epmList.searchPackagesPlaceholder": "搜索集成", "xpack.ingestManager.epmList.updatesAvailableFilterLinkText": "有可用更新", + "xpack.ingestManager.featureCatalogueDescription": "添加并管理您所有的 Elastic 代理和集成。", + "xpack.ingestManager.featureCatalogueTitle": "添加 Elastic 代理", "xpack.ingestManager.genericActionsMenuText": "打开", "xpack.ingestManager.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "关闭消息", - "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeText": "通过 Elastic 代理,您能够以简单统一的方式将日志、指标和其他类型数据的监测添加到主机。不再需要安装多个 Beats 和其他代理,这样在整个基础设施中部署配置会更轻松更快速。有关更多信息,请阅读我们的{blogPostLink}。", + "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeText": "通过 Elastic 代理,可以简单统一的方式将日志、指标和其他类型数据的监测添加到主机。不再需要安装多个 Beats 和其他代理,这样将策略部署到整个基础架构更容易也更快速。有关更多信息,请阅读我们的{blogPostLink}。", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "公告博客", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix}Elastic 代理和采集管理器公测版", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeTitle.newPrefix": "新通告:", - "xpack.ingestManager.homeIntegration.tutorialModule.noticeText": "{notePrefix}此模块的较新版本在采集管理器公测版中{availableAsIntegrationLink}。要详细了解代理配置和新 Elastic 代理,请阅读我们的{blogPostLink}。", + "xpack.ingestManager.homeIntegration.tutorialModule.noticeText": "{notePrefix}此模块的较新版本在采集管理器公测版中{availableAsIntegrationLink}。要详细了解代理策略和新的 Elastic 代理,请阅读我们的{blogPostLink}。", "xpack.ingestManager.homeIntegration.tutorialModule.noticeText.blogPostLink": "公告博客", "xpack.ingestManager.homeIntegration.tutorialModule.noticeText.integrationLink": "作为集成提供", "xpack.ingestManager.homeIntegration.tutorialModule.noticeText.notePrefix": "注意:", @@ -9132,7 +10043,7 @@ "xpack.ingestManager.integrations.settings.confirmInstallModal.installTitle": "安装 {packageName}", "xpack.ingestManager.integrations.settings.confirmUninstallModal.cancelButtonLabel": "取消", "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallButtonLabel": "卸载 {packageName}", - "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallCallout.description": "将会移除此集成创建的 Kibana 和 Elasticsearch 资产。将不会影响代理配置和您的代理发送的任何数据。", + "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallCallout.description": "将会移除由此集成创建的 Kibana 和 Elasticsearch 资产。将不会影响代理策略以及您的代理发送的任何数据。", "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallCallout.title": "此操作将移除 {numOfAssets} 个资产", "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallDescription": "此操作无法撤消。是否确定要继续?", "xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallTitle": "卸载 {packageName}", @@ -9140,7 +10051,7 @@ "xpack.ingestManager.integrations.settings.packageInstallTitle": "安装 {title}", "xpack.ingestManager.integrations.settings.packageSettingsTitle": "设置", "xpack.ingestManager.integrations.settings.packageUninstallDescription": "移除此集成安装的 Kibana 和 Elasticsearch 资产。", - "xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail": "{strongNote}{title} 无法卸载,因为有使用此集成的活动代理。要卸载,请从您的代理配置中移除所有 {title} 集成。", + "xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail": "{strongNote}{title} 无法卸载,因为存在使用此集成的活动代理。要卸载,请从您的代理策略中移除所有 {title} 集成。", "xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteLabel": "注意:", "xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallUninstallableNoteDetail": "{strongNote} {title} 集成默认安装,无法移除。", "xpack.ingestManager.integrations.settings.packageUninstallTitle": "卸载 {title}", @@ -9159,10 +10070,15 @@ "xpack.ingestManager.metadataForm.keyLabel": "键", "xpack.ingestManager.metadataForm.submitButtonText": "添加", "xpack.ingestManager.metadataForm.valueLabel": "值", + "xpack.ingestManager.namespaceValidation.invalidCharactersErrorMessage": "命名空间包含无效字符", + "xpack.ingestManager.namespaceValidation.lowercaseErrorMessage": "命名空间必须小写", + "xpack.ingestManager.namespaceValidation.requiredErrorMessage": "“命名空间”必填", + "xpack.ingestManager.namespaceValidation.tooLongErrorMessage": "命名空间不能超过 100 个字节", "xpack.ingestManager.newEnrollmentKey.cancelButtonLabel": "取消", - "xpack.ingestManager.newEnrollmentKey.flyoutTitle": "创建新的注册令牌", + "xpack.ingestManager.newEnrollmentKey.flyoutTitle": "创建注册令牌", "xpack.ingestManager.newEnrollmentKey.keyCreatedToasts": "注册令牌已创建。", "xpack.ingestManager.newEnrollmentKey.nameLabel": "名称", + "xpack.ingestManager.newEnrollmentKey.policyLabel": "策略", "xpack.ingestManager.newEnrollmentKey.submitButton": "创建注册令牌", "xpack.ingestManager.noAccess.accessDeniedDescription": "您无权访问 Elastic Fleet。要使用 Elastic Fleet,您需要包含此应用程序读取权限或所有权限的用户角色。", "xpack.ingestManager.noAccess.accessDeniedTitle": "访问被拒绝", @@ -9172,39 +10088,85 @@ "xpack.ingestManager.overviewAgentTotalTitle": "代理总数", "xpack.ingestManager.overviewDatastreamNamespacesTitle": "命名空间", "xpack.ingestManager.overviewDatastreamSizeTitle": "总大小", - "xpack.ingestManager.overviewDatastreamTotalTitle": "数据集", + "xpack.ingestManager.overviewDatastreamTotalTitle": "数据流", "xpack.ingestManager.overviewIntegrationsInstalledTitle": "安装时间", "xpack.ingestManager.overviewIntegrationsTotalTitle": "可用总计", "xpack.ingestManager.overviewIntegrationsUpdatesAvailableTitle": "可用更新", - "xpack.ingestManager.overviewPageDataStreamsPanelAction": "查看数据集", - "xpack.ingestManager.overviewPageDataStreamsPanelTitle": "数据集", - "xpack.ingestManager.overviewPageDataStreamsPanelTooltip": "您的代理收集的数据组织到各种数据集中。", + "xpack.ingestManager.overviewPackagePolicyTitle": "已使用的集成", + "xpack.ingestManager.overviewPageAgentsPanelTitle": "代理", + "xpack.ingestManager.overviewPageDataStreamsPanelAction": "查看数据流", + "xpack.ingestManager.overviewPageDataStreamsPanelTitle": "数据流", + "xpack.ingestManager.overviewPageDataStreamsPanelTooltip": "您的代理收集的数据组织到各种数据流中。", "xpack.ingestManager.overviewPageEnrollAgentButton": "添加代理", "xpack.ingestManager.overviewPageFleetPanelAction": "查看代理", - "xpack.ingestManager.overviewPageFleetPanelTooltip": "使用 Fleet 注册代理并从集中位置管理其配置。", + "xpack.ingestManager.overviewPageFleetPanelTooltip": "使用 Fleet 注册代理并从中央位置管理其策略。", "xpack.ingestManager.overviewPageIntegrationsPanelAction": "查看集成", "xpack.ingestManager.overviewPageIntegrationsPanelTitle": "集成", - "xpack.ingestManager.overviewPageIntegrationsPanelTooltip": "浏览并安装 Elastic Stack 的集成。将集成添加到您的代理配置以开始发送数据。", - "xpack.ingestManager.overviewPageSubtitle": "Elastic 代理和代理配置的集中管理。", - "xpack.ingestManager.overviewPageTitle": "Ingest Manager", + "xpack.ingestManager.overviewPageIntegrationsPanelTooltip": "浏览并安装适用于 Elastic Stack 的集成。将集成添加到您的代理策略,以开始发送数据。", + "xpack.ingestManager.overviewPagePoliciesPanelAction": "查看策略", + "xpack.ingestManager.overviewPagePoliciesPanelTitle": "代理策略", + "xpack.ingestManager.overviewPagePoliciesPanelTooltip": "使用代理策略控制您的代理收集的数据。", + "xpack.ingestManager.overviewPageSubtitle": "在集中位置管理 Elastic 代理及其策略。", + "xpack.ingestManager.overviewPageTitle": "Fleet", + "xpack.ingestManager.overviewPolicyTotalTitle": "可用总计", + "xpack.ingestManager.packagePolicyValidation.invalidArrayErrorMessage": "格式无效", + "xpack.ingestManager.packagePolicyValidation.invalidYamlFormatErrorMessage": "YAML 格式无效", + "xpack.ingestManager.packagePolicyValidation.nameRequiredErrorMessage": "“名称”必填", + "xpack.ingestManager.packagePolicyValidation.requiredErrorMessage": "“{fieldName}”必填", "xpack.ingestManager.permissionDeniedErrorMessage": "您无权访问 Ingest Manager。Ingest Manager 需要{roleName}权限。", "xpack.ingestManager.permissionDeniedErrorTitle": "权限被拒绝", "xpack.ingestManager.permissionsRequestErrorMessageDescription": "检查 Ingest Manager 权限时出现问题", "xpack.ingestManager.permissionsRequestErrorMessageTitle": "无法检查权限", + "xpack.ingestManager.policyDetails.addPackagePolicyButtonText": "添加集成", + "xpack.ingestManager.policyDetails.ErrorGettingFullAgentPolicy": "加载代理策略时出错", + "xpack.ingestManager.policyDetails.packagePoliciesTable.actionsColumnTitle": "操作", + "xpack.ingestManager.policyDetails.packagePoliciesTable.deleteActionTitle": "删除集成", + "xpack.ingestManager.policyDetails.packagePoliciesTable.descriptionColumnTitle": "描述", + "xpack.ingestManager.policyDetails.packagePoliciesTable.editActionTitle": "编辑集成", + "xpack.ingestManager.policyDetails.packagePoliciesTable.nameColumnTitle": "名称", + "xpack.ingestManager.policyDetails.packagePoliciesTable.namespaceColumnTitle": "命名空间", + "xpack.ingestManager.policyDetails.packagePoliciesTable.packageNameColumnTitle": "集成", + "xpack.ingestManager.policyDetails.policyDetailsTitle": "策略“{id}”", + "xpack.ingestManager.policyDetails.policyNotFoundErrorTitle": "找不到策略“{id}”", + "xpack.ingestManager.policyDetails.subTabs.packagePoliciesTabText": "集成", + "xpack.ingestManager.policyDetails.subTabs.settingsTabText": "设置", + "xpack.ingestManager.policyDetails.summary.integrations": "集成", + "xpack.ingestManager.policyDetails.summary.lastUpdated": "上次更新时间", + "xpack.ingestManager.policyDetails.summary.revision": "修订", + "xpack.ingestManager.policyDetails.summary.usedBy": "使用者", + "xpack.ingestManager.policyDetails.unexceptedErrorTitle": "加载代理策略时发生错误", + "xpack.ingestManager.policyDetails.viewAgentListTitle": "查看所有代理策略", + "xpack.ingestManager.policyDetails.yamlDownloadButtonLabel": "下载策略", + "xpack.ingestManager.policyDetails.yamlFlyoutCloseButtonLabel": "关闭", + "xpack.ingestManager.policyDetails.yamlflyoutTitleWithName": "代理策略“{name}”", + "xpack.ingestManager.policyDetails.yamlflyoutTitleWithoutName": "代理策略", + "xpack.ingestManager.policyDetailsPackagePolicies.createFirstButtonText": "添加集成", + "xpack.ingestManager.policyDetailsPackagePolicies.createFirstMessage": "此策略尚无任何集成。", + "xpack.ingestManager.policyDetailsPackagePolicies.createFirstTitle": "添加您的首个集成", + "xpack.ingestManager.policyForm.deletePolicyActionText": "删除策略", + "xpack.ingestManager.policyForm.deletePolicyGroupDescription": "现有数据将不会删除。", + "xpack.ingestManager.policyForm.deletePolicyGroupTitle": "删除策略", + "xpack.ingestManager.policyForm.generalSettingsGroupDescription": "为您的代理策略选择名称和描述。", + "xpack.ingestManager.policyForm.generalSettingsGroupTitle": "常规设置", + "xpack.ingestManager.policyForm.unableToDeleteDefaultPolicyText": "默认策略无法删除", "xpack.ingestManager.securityRequiredErrorMessage": "必须在 Kibana 和 Elasticsearch 启用安全性,才能使用 Ingest Manager。", "xpack.ingestManager.securityRequiredErrorTitle": "安全性未启用", - "xpack.ingestManager.settings.autoUpgradeDisabledLabel": "手动管理代理二进制文件版本。需要黄金级许可证。", + "xpack.ingestManager.settings.additionalYamlConfig": "Elasticsearch 输出配置", + "xpack.ingestManager.settings.autoUpgradeDisabledLabel": "手动管理代理二进制文件版本。需要黄金级订阅。", "xpack.ingestManager.settings.autoUpgradeEnabledLabel": "自动更新代理二进制文件以使用最新的次要版本。", "xpack.ingestManager.settings.autoUpgradeFieldLabel": "Elastic 代理二进制文件版本", "xpack.ingestManager.settings.cancelButtonLabel": "取消", "xpack.ingestManager.settings.elasticHostError": "URL 无效", "xpack.ingestManager.settings.elasticsearchUrlLabel": "Elasticsearch URL", "xpack.ingestManager.settings.flyoutTitle": "Ingest Manager 设置", - "xpack.ingestManager.settings.globalOutputDescription": "全局输出将应用于所有代理配置,并指定将数据发送到哪里。", + "xpack.ingestManager.settings.globalOutputDescription": "指定将数据发送到何处。这些设置将应用于所有的 Elastic 代理策略。", "xpack.ingestManager.settings.globalOutputTitle": "全局输出", "xpack.ingestManager.settings.integrationUpgradeDisabledFieldLabel": "自行手动管理集成版本。", - "xpack.ingestManager.settings.integrationUpgradeEnabledFieldLabel": "自动将集成更新到最新版本以接收最新的资产。要使用新功能,可能需要更新代理配置。", + "xpack.ingestManager.settings.integrationUpgradeEnabledFieldLabel": "将集成自动更新到最新版本以获取最新资产。您可能需要更新代理策略以使用新功能。", "xpack.ingestManager.settings.integrationUpgradeFieldLabel": "集成版本", + "xpack.ingestManager.settings.invalidYamlFormatErrorMessage": "YAML 无效:{reason}", + "xpack.ingestManager.settings.kibanaUrlDifferentPathOrProtocolError": "对于每个 URL,协议和路径必须相同", + "xpack.ingestManager.settings.kibanaUrlEmptyError": "至少需要一个 URL", "xpack.ingestManager.settings.kibanaUrlError": "URL 无效", "xpack.ingestManager.settings.kibanaUrlLabel": "Kibana URL", "xpack.ingestManager.settings.saveButtonLabel": "保存设置", @@ -9213,20 +10175,46 @@ "xpack.ingestManager.setupPage.elasticsearchApiKeyFlagText": "{apiKeyLink}。将 {apiKeyFlag} 设置为 {true}。", "xpack.ingestManager.setupPage.elasticsearchSecurityFlagText": "{esSecurityLink}。将 {securityFlag} 设置为 {true}。", "xpack.ingestManager.setupPage.elasticsearchSecurityLink": "Elasticsearch 安全", - "xpack.ingestManager.setupPage.enableText": "要使用 Fleet,必须创建 Elastic 用户。此用户可以创建 API 密钥并写入到 logs-* and metrics-*。", - "xpack.ingestManager.setupPage.enableTitle": "启用 Fleet", + "xpack.ingestManager.setupPage.enableCentralManagement": "创建用户并启用集中管理", + "xpack.ingestManager.setupPage.enableText": "集中管理需要可以创建 API 密钥并写入到 logs-* 和 metrics-* 的 Elastic 用户。", + "xpack.ingestManager.setupPage.enableTitle": "对 Elastic 代理启用集中管理", "xpack.ingestManager.setupPage.encryptionKeyFlagText": "{encryptionKeyLink}。将 {keyFlag} 设置为至少 32 个字符的字母数字值。", "xpack.ingestManager.setupPage.gettingStartedLink": "入门", "xpack.ingestManager.setupPage.gettingStartedText": "有关更多信息,请阅读我们的{link}指南。", "xpack.ingestManager.setupPage.kibanaEncryptionLink": "Kibana 加密密钥", "xpack.ingestManager.setupPage.kibanaSecurityLink": "Kibana 安全性", - "xpack.ingestManager.setupPage.missingRequirementsCalloutDescription": "要使用 Fleet,必须启用以下 Elasticsearch 和 Kibana 安全性功能。", + "xpack.ingestManager.setupPage.missingRequirementsCalloutDescription": "要对 Elastic 代理使用集中管理,请启用下面的 Elasticsearch 和 Kibana 安全功能。", "xpack.ingestManager.setupPage.missingRequirementsCalloutTitle": "缺失安全性要求", - "xpack.ingestManager.setupPage.missingRequirementsElasticsearchTitle": "在您的 Elasticsearch 配置中,启用:", - "xpack.ingestManager.setupPage.missingRequirementsKibanaTitle": "在您的 Kibana 配置中,启用:", + "xpack.ingestManager.setupPage.missingRequirementsElasticsearchTitle": "在 Elasticsearch 策略中,启用:", + "xpack.ingestManager.setupPage.missingRequirementsKibanaTitle": "在 Kibana 策略中,启用:", "xpack.ingestManager.setupPage.tlsFlagText": "{kibanaSecurityLink}。将 {securityFlag} 设置为 {true}。出于开发目的,作为非安全的备用方案可以通过将 {tlsFlag} 设置为 {true} 来禁用 {tlsLink}。", "xpack.ingestManager.setupPage.tlsLink": "TLS", "xpack.ingestManager.unenrollAgents.cancelButtonLabel": "取消", + "xpack.ingestManager.unenrollAgents.confirmMultipleButtonLabel": "取消注册 {count} 个代理", + "xpack.ingestManager.unenrollAgents.confirmSingleButtonLabel": "取消注册代理", + "xpack.ingestManager.unenrollAgents.deleteMultipleDescription": "此操作将从 Fleet 中移除多个代理,并防止采集新数据。将不会影响任何已由这些代理发送的数据。此操作无法撤消。", + "xpack.ingestManager.unenrollAgents.deleteSingleDescription": "此操作将从 Fleet 中移除“{hostName}”上运行的选定代理。由该代理发送的任何数据将不会被删除。此操作无法撤消。", + "xpack.ingestManager.unenrollAgents.deleteSingleTitle": "取消注册代理", + "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "取消注册{count, plural, one {代理} other {代理}}时出错", + "xpack.ingestManager.unenrollAgents.forceDeleteMultipleTitle": "取消注册 {count} 个代理", + "xpack.ingestManager.unenrollAgents.forceUnenrollCheckboxLabel": "立即移除{count, plural, one {代理} other {代理}}。不用等待代理发送任何最终数据。", + "xpack.ingestManager.unenrollAgents.forceUnenrollLegendText": "强制取消注册{count, plural, one {代理} other {代理}}", + "xpack.ingestManager.unenrollAgents.successForceMultiNotificationTitle": "代理已取消注册", + "xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle": "代理已取消注册", + "xpack.ingestManager.unenrollAgents.successMultiNotificationTitle": "正在取消注册代理", + "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "正在取消注册代理", + "xpack.ingestManager.upgradeAgents.cancelButtonLabel": "取消", + "xpack.ingestManager.upgradeAgents.confirmMultipleButtonLabel": "升级 {count} 个代理", + "xpack.ingestManager.upgradeAgents.confirmSingleButtonLabel": "升级代理", + "xpack.ingestManager.upgradeAgents.deleteMultipleTitle": "升级 {count} 个代理", + "xpack.ingestManager.upgradeAgents.deleteSingleTitle": "升级代理", + "xpack.ingestManager.upgradeAgents.fatalErrorNotificationTitle": "升级{count, plural, one {代理} other {代理}}时出错", + "xpack.ingestManager.upgradeAgents.successMultiNotificationTitle": "正在升级代理", + "xpack.ingestManager.upgradeAgents.successSingleNotificationTitle": "正在升级代理", + "xpack.ingestManager.upgradeAgents.upgradeMultipleDescription": "此操作会将多个代理升级到版本 {version}。此操作无法撤消。是否确定要继续?", + "xpack.ingestManager.upgradeAgents.upgradeSingleDescription": "此操作会将“{hostName}”上运行的选定代理升级到版本 {version}。此操作无法撤消。是否确定要继续?", + "xpack.ingestPipelines.addProcesorFormOnFailureFlyout.cancelButtonLabel": "取消", + "xpack.ingestPipelines.addProcessorFormOnFailureFlyout.addButtonLabel": "添加", "xpack.ingestPipelines.app.checkingPrivilegesDescription": "正在检查权限……", "xpack.ingestPipelines.app.checkingPrivilegesErrorMessage": "从服务器获取用户权限时出错。", "xpack.ingestPipelines.app.deniedPrivilegeDescription": "要使用“采集管道”,必须具有{privilegesCount, plural, one {以下集群权限} other {以下集群权限}}:{missingPrivileges}。", @@ -9307,21 +10295,186 @@ "xpack.ingestPipelines.list.table.emptyPromptTitle": "首先创建管道", "xpack.ingestPipelines.list.table.nameColumnTitle": "名称", "xpack.ingestPipelines.list.table.reloadButtonLabel": "重新加载", + "xpack.ingestPipelines.pipelineEditor.addDocuments.addDocumentButtonLabel": "添加文档", + "xpack.ingestPipelines.pipelineEditor.addDocuments.addDocumentErrorMessage": "添加文档时出错", + "xpack.ingestPipelines.pipelineEditor.addDocuments.addDocumentSuccessMessage": "文档已添加", + "xpack.ingestPipelines.pipelineEditor.addDocuments.idFieldLabel": "文档 ID", + "xpack.ingestPipelines.pipelineEditor.addDocuments.idRequiredErrorMessage": "需要文档 ID。", + "xpack.ingestPipelines.pipelineEditor.addDocuments.indexFieldLabel": "索引", + "xpack.ingestPipelines.pipelineEditor.addDocuments.indexRequiredErrorMessage": "需要索引名称。", + "xpack.ingestPipelines.pipelineEditor.addDocumentsAccordion.addDocumentsButtonLabel": "从索引添加测试文档", + "xpack.ingestPipelines.pipelineEditor.addDocumentsAccordion.contentDescriptionText": "提供该文档的索引和文档 ID。", + "xpack.ingestPipelines.pipelineEditor.addDocumentsAccordion.discoverLinkDescriptionText": "要浏览现有数据,请使用{discoverLink}。", "xpack.ingestPipelines.pipelineEditor.addProcessorButtonLabel": "添加处理器", + "xpack.ingestPipelines.pipelineEditor.appendForm.fieldHelpText": "要将值追加到的字段。", + "xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldHelpText": "要追加的值。", + "xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldLabel": "值", + "xpack.ingestPipelines.pipelineEditor.appendForm.valueRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.bytesForm.fieldNameHelpText": "要转换的字段。如果字段包含数组,则将转换每个数组值。", + "xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError": "需要误差距离值。", + "xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceFieldLabel": "误差距离", + "xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceHelpText": "内接形状的边到包围圆之间的差距。确定输出多边形的准确性。对于 {geo_shape},以米为度量单位,但是对于 {shape},则不使用任何单位。", + "xpack.ingestPipelines.pipelineEditor.circleForm.fieldNameHelpText": "要转换的字段。", + "xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeFieldHelpText": "在处理输出多边形时要使用的字段映射类型。", + "xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeFieldLabel": "形状类型", + "xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeGeoShape": "几何形状", + "xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeRequiredError": "需要形状类型值。", + "xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeShape": "形状", + "xpack.ingestPipelines.pipelineEditor.commonFields.fieldFieldLabel": "字段", + "xpack.ingestPipelines.pipelineEditor.commonFields.fieldRequiredError": "需要字段值。", + "xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldHelpText": "有条件地运行此处理器。", "xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldLabel": "条件(可选)", "xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureFieldLabel": "忽略失败", + "xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureHelpText": "忽略此处理器的故障。", + "xpack.ingestPipelines.pipelineEditor.commonFields.ignoreMissingFieldHelpText": "忽略缺少 {field} 的文档。", + "xpack.ingestPipelines.pipelineEditor.commonFields.ignoreMissingFieldLabel": "忽略缺失", + "xpack.ingestPipelines.pipelineEditor.commonFields.propertiesFieldLabel": "属性(可选)", + "xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldHelpText": "用于处理器的标识符。用于调试和指标。", "xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldLabel": "标记(可选)", + "xpack.ingestPipelines.pipelineEditor.commonFields.targetFieldHelpText": "输出字段。如果为空,则输入字段将适当更新。", + "xpack.ingestPipelines.pipelineEditor.commonFields.targetFieldLabel": "目标字段(可选)", + "xpack.ingestPipelines.pipelineEditor.convertForm.autoOption": "自动", + "xpack.ingestPipelines.pipelineEditor.convertForm.booleanOption": "布尔型", + "xpack.ingestPipelines.pipelineEditor.convertForm.doubleOption": "双精度", + "xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldHelpText": "用于填充空字段。如果未提供值,则跳过空字段。", + "xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldLabel": "空值(可选)", + "xpack.ingestPipelines.pipelineEditor.convertForm.fieldNameHelpText": "要转换的字段。", + "xpack.ingestPipelines.pipelineEditor.convertForm.floatOption": "浮点型", + "xpack.ingestPipelines.pipelineEditor.convertForm.integerOption": "整型", + "xpack.ingestPipelines.pipelineEditor.convertForm.longOption": "长整型", + "xpack.ingestPipelines.pipelineEditor.convertForm.quoteFieldLabel": "引号(可选)", + "xpack.ingestPipelines.pipelineEditor.convertForm.quoteHelpText": "在 CSV 数据中使用的转义字符。默认为 {value}。", + "xpack.ingestPipelines.pipelineEditor.convertForm.separatorFieldLabel": "分隔符(可选)", + "xpack.ingestPipelines.pipelineEditor.convertForm.separatorHelpText": "在 CSV 数据中使用的分隔符。默认为 {value}。", + "xpack.ingestPipelines.pipelineEditor.convertForm.separatorLengthError": "必须是单个字符。", + "xpack.ingestPipelines.pipelineEditor.convertForm.stringOption": "字符串", + "xpack.ingestPipelines.pipelineEditor.convertForm.typeFieldHelpText": "输出的字段数据类型。", + "xpack.ingestPipelines.pipelineEditor.convertForm.typeFieldLabel": "类型", + "xpack.ingestPipelines.pipelineEditor.convertForm.typeRequiredError": "需要类型值。", + "xpack.ingestPipelines.pipelineEditor.csvForm.fieldNameHelpText": "包含 CSV 数据的字段。", + "xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldRequiredError": "需要目标字段值。", + "xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldsFieldLabel": "目标字段", + "xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldsHelpText": "输出字段。已提取的值映射到这些字段。", + "xpack.ingestPipelines.pipelineEditor.csvForm.trimFieldHelpText": "移除不带引号的 CSV 数据中的空格。", + "xpack.ingestPipelines.pipelineEditor.csvForm.trimFieldLabel": "剪裁", "xpack.ingestPipelines.pipelineEditor.customForm.configurationRequiredError": "配置必填。", "xpack.ingestPipelines.pipelineEditor.customForm.invalidJsonError": "输入无效。", "xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldAriaLabel": "配置 JSON 编辑器", "xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldLabel": "配置", + "xpack.ingestPipelines.pipelineEditor.dateForm.fieldNameHelpText": "要转换的字段。", + "xpack.ingestPipelines.pipelineEditor.dateForm.formatsFieldHelpText": "预期的日期格式。提供的格式按顺序应用。接受 Java 时间模式、ISO8601、UNIX、UNIX_MS 或 TAI64N 格式。", + "xpack.ingestPipelines.pipelineEditor.dateForm.formatsFieldLabel": "格式", + "xpack.ingestPipelines.pipelineEditor.dateForm.formatsRequiredError": "需要格式的值。", + "xpack.ingestPipelines.pipelineEditor.dateForm.localeFieldLabel": "区域设置(可选)", + "xpack.ingestPipelines.pipelineEditor.dateForm.localeHelpText": "日期的区域设置。用于解析月或日名称。默认为 {timezone}。", + "xpack.ingestPipelines.pipelineEditor.dateForm.targetFieldHelpText": "输出字段。如果为空,则输入字段将适当更新。默认为 {defaultField}。", + "xpack.ingestPipelines.pipelineEditor.dateForm.timezoneFieldLabel": "时区(可选)", + "xpack.ingestPipelines.pipelineEditor.dateForm.timezoneHelpText": "日期的时区。默认为 {timezone}。", + "xpack.ingestPipelines.pipelineEditor.dateIndexForm.localeHelpText": "要在解析日期时使用的区域设置。用于解析月或日名称。默认为 {locale}。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateFormatsFieldLabel": "日期格式(可选)", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateFormatsHelpText": "预期的日期格式。提供的格式按顺序应用。接受 Java 时间模式、ISO8601、UNIX、UNIX_MS 或 TAI64N 格式。默认为 {value}。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.day": "天", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.hour": "小时", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.minute": "分钟", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.month": "月", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.second": "秒", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.week": "周", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRounding.year": "年", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingFieldHelpText": "将日期格式化为索引名称时用于四舍五入日期的时段。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingFieldLabel": "日期四舍五入", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingRequiredError": "需要字段值。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.fieldNameHelpText": "包含日期或时间戳的字段。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNameFormatFieldHelpText": "用于将已解析日期输出到索引名称的日期格式。默认为 {value}。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNameFormatFieldLabel": "索引名称格式(可选)", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldHelpText": "要在索引名称中的输出日期前添加的前缀。", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldLabel": "索引名称前缀(可选)", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.localeFieldLabel": "区域设置(可选)", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.timezoneFieldLabel": "时区(可选)", + "xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.timezoneHelpText": "解析日期和构造索引名称表达式时使用的时区。默认为 {timezone}。", "xpack.ingestPipelines.pipelineEditor.deleteModal.deleteDescription": "删除此处理器和其失败时处理程序。", + "xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorHelpText": "如果您指定了键修饰符,则在追加结果时,此字符用于分隔字段。默认为 {value}。", + "xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorparaotrFieldLabel": "追加分隔符(可选)", + "xpack.ingestPipelines.pipelineEditor.dissectForm.fieldNameHelpText": "要分解的字段。", + "xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText": "用于分解指定字段的模式。该模式由要丢弃的字符串部分定义。使用 {keyModifier} 可更改分解行为。", + "xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText.dissectProcessorLink": "键修饰符", + "xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldLabel": "模式", + "xpack.ingestPipelines.pipelineEditor.dissectForm.patternRequiredError": "需要模式值。", + "xpack.ingestPipelines.pipelineEditor.dotExpanderForm.fieldNameHelpText": "包含点表示法的字段。", + "xpack.ingestPipelines.pipelineEditor.dotExpanderForm.fieldNameRequiresDotError": "字段值至少需要一个点字符。", + "xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathFieldLabel": "路径", + "xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathHelpText": "输出字段。仅当要扩展的字段属于其他对象字段时才需要。", + "xpack.ingestPipelines.pipelineEditor.dragAndDropList.removeItemLabel": "移除项目", "xpack.ingestPipelines.pipelineEditor.dropZoneButton.moveHereToolTip": "移到此处", "xpack.ingestPipelines.pipelineEditor.dropZoneButton.unavailableToolTip": "无法移到此处", + "xpack.ingestPipelines.pipelineEditor.emptyPrompt.description": "使用处理器可在索引前转换数据。{learnMoreLink}", + "xpack.ingestPipelines.pipelineEditor.emptyPrompt.title": "添加您的首个处理器", + "xpack.ingestPipelines.pipelineEditor.enrichForm.containsOption": "Contains", + "xpack.ingestPipelines.pipelineEditor.enrichForm.fieldNameHelpText": "用于将传入文档匹配到扩充文档的字段。字段值会与扩充策略中设置的匹配字段进行比较。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.intersectsOption": "Intersects", + "xpack.ingestPipelines.pipelineEditor.enrichForm.maxMatchesFieldHelpText": "要包含在目标字段中的匹配扩充文档数目。接受 1 到 128。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.maxMatchesFieldLabel": "最大匹配数(可选)", + "xpack.ingestPipelines.pipelineEditor.enrichForm.maxMatchesMaxNumberError": "此数字必须小于 128。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.maxMatchesMinNumberError": "此数字必须大于 0。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.overrideFieldHelpText": "如果启用,则处理器可以覆盖预先存在的字段值。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.overrideFieldLabel": "覆盖", + "xpack.ingestPipelines.pipelineEditor.enrichForm.policyNameFieldLabel": "策略名称", + "xpack.ingestPipelines.pipelineEditor.enrichForm.policyNameHelpText": "{enrichPolicyLink}的名称。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.policyNameHelpText.enrichPolicyLink": "扩充策略", + "xpack.ingestPipelines.pipelineEditor.enrichForm.policyNameRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.shapeRelationFieldHelpText": "用于将传入文档的几何形状匹配到扩充文档的运算符。仅用于{geoMatchPolicyLink}。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.shapeRelationFieldHelpText.geoMatchPoliciesLink": "Geo-match 扩充策略", + "xpack.ingestPipelines.pipelineEditor.enrichForm.shapeRelationFieldLabel": "形状关系(可选)", + "xpack.ingestPipelines.pipelineEditor.enrichForm.targetFieldHelpText": "用于包含扩充数据的字段。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.targetFieldLabel": "目标字段", + "xpack.ingestPipelines.pipelineEditor.enrichForm.targetFieldRequiredError": "需要目标字段值。", + "xpack.ingestPipelines.pipelineEditor.enrichForm.withinOption": "Within", + "xpack.ingestPipelines.pipelineEditor.enrichFrom.disjointOption": "Disjoint", + "xpack.ingestPipelines.pipelineEditor.failForm.fieldNameHelpText": "包含数组值的字段。", + "xpack.ingestPipelines.pipelineEditor.failForm.messageFieldLabel": "消息", + "xpack.ingestPipelines.pipelineEditor.failForm.messageHelpText": "由处理器返回的错误消息。", + "xpack.ingestPipelines.pipelineEditor.failForm.valueRequiredError": "需要消息。", + "xpack.ingestPipelines.pipelineEditor.foreachForm.optionsFieldAriaLabel": "配置 JSON 编辑器", + "xpack.ingestPipelines.pipelineEditor.foreachForm.processorFieldLabel": "处理器", + "xpack.ingestPipelines.pipelineEditor.foreachForm.processorHelpText": "要对每个数组值运行的采集处理器。", + "xpack.ingestPipelines.pipelineEditor.foreachForm.processorInvalidJsonError": "JSON 无效", + "xpack.ingestPipelines.pipelineEditor.foreachForm.processorRequiredError": "需要处理器。", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.databaseFileHelpText": "{ingestGeoIP} 配置目录中的 GeoIP2 数据库文件。默认为 {databaseFile}。", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.databaseFileLabel": "数据库文件(可选)", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.fieldNameHelpText": "包含用于地理查找的 IP 地址的字段。", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.firstOnlyFieldHelpText": "使用首个匹配的地理数据,即使字段包含数组。", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.firstOnlyFieldLabel": "仅限首个", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.propertiesFieldHelpText": "添加到目标字段的属性。有效属性取决于使用的数据库文件。", + "xpack.ingestPipelines.pipelineEditor.geoIPForm.targetFieldHelpText": "用于包含地理数据属性的字段。", + "xpack.ingestPipelines.pipelineEditor.grokForm.fieldNameHelpText": "用于搜索匹配项的字段。", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternDefinitionsAriaLabel": "模式定义编辑器", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternDefinitionsHelpText": "定义定制模式的模式名称和模式元组的映射。与现有名称匹配的模式将覆盖预先存在的定义。", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternDefinitionsLabel": "模式定义(可选)", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternsAddPatternLabel": "添加模式", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternsDefinitionsInvalidJSONError": "JSON 无效", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternsFieldLabel": "模式", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternsHelpText": "用于匹配和提取已命名捕获组的 Grok 表达式。使用首个匹配的表达式。", + "xpack.ingestPipelines.pipelineEditor.grokForm.patternsValueRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.grokForm.traceMatchFieldHelpText": "将有关匹配表达式的元数据添加到文档。", + "xpack.ingestPipelines.pipelineEditor.grokForm.traceMatchFieldLabel": "跟踪匹配项", + "xpack.ingestPipelines.pipelineEditor.gsubForm.fieldNameHelpText": "用于搜索匹配项的字段。", + "xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldHelpText": "用于匹配字段中的子字符串的正则表达式。", "xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldLabel": "模式", - "xpack.ingestPipelines.pipelineEditor.gsubForm.patternRequiredError": "模式值必填。", + "xpack.ingestPipelines.pipelineEditor.gsubForm.patternRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldHelpText": "匹配项的替换文本。", "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldLabel": "替换", - "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementRequiredError": "替换值必填。", + "xpack.ingestPipelines.pipelineEditor.gsubForm.replacementRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.htmlStripForm.fieldNameHelpText": "从其中移除 HTML 标记的字段。", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.fieldMapHelpText": "将文档字段名称映射到模型的已知字段名称。优先于模型中的任何映射。", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.fieldMapInvalidJSONError": "JSON 无效", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.fieldMapLabel": "字段映射(可选)", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigField.classificationLinkLabel": "分类", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigField.regressionLinkLabel": "回归", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigLabel": "推理配置(可选)", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigurationHelpText": "包含推理类型及其选项。有两种类型:{regression} 和 {classification}。", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.modelIDFieldHelpText": "推理所根据的模型的 ID。", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.modelIDFieldLabel": "模型 ID", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.patternRequiredError": "需要模型 ID 值。", + "xpack.ingestPipelines.pipelineEditor.inferenceForm.targetFieldHelpText": "用于包含推理处理器结果的字段。默认为 {targetField}。", "xpack.ingestPipelines.pipelineEditor.item.cancelMoveButtonAriaLabel": "取消移动", "xpack.ingestPipelines.pipelineEditor.item.descriptionPlaceholder": "无描述", "xpack.ingestPipelines.pipelineEditor.item.editButtonAriaLabel": "编辑此处理器", @@ -9331,7 +10484,34 @@ "xpack.ingestPipelines.pipelineEditor.item.moreMenu.duplicateButtonLabel": "复制此处理器", "xpack.ingestPipelines.pipelineEditor.item.moveButtonLabel": "移动此处理器", "xpack.ingestPipelines.pipelineEditor.item.textInputAriaLabel": "为此 {type} 处理器提供描述", - "xpack.ingestPipelines.pipelineEditor.loadFromJson.buttonLabel": "加载 JSON", + "xpack.ingestPipelines.pipelineEditor.joinForm.fieldNameHelpText": "包含要联接的数组值的字段。", + "xpack.ingestPipelines.pipelineEditor.joinForm.separatorFieldHelpText": "分隔符。", + "xpack.ingestPipelines.pipelineEditor.joinForm.separatorFieldLabel": "分隔符", + "xpack.ingestPipelines.pipelineEditor.joinForm.separatorRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.jsonForm.addToRootFieldHelpText": "将 JSON 对象添加到文档的顶层。不能与目标字段进行组合。", + "xpack.ingestPipelines.pipelineEditor.jsonForm.addToRootFieldLabel": "添加到根目录", + "xpack.ingestPipelines.pipelineEditor.jsonForm.fieldNameHelpText": "要解析的字段。", + "xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysFieldLabel": "排除键", + "xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysHelpText": "要从输出中排除的已提取键的列表。", + "xpack.ingestPipelines.pipelineEditor.kvForm.fieldNameHelpText": "包含键值对字符串的字段。", + "xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitFieldLabel": "字段拆分", + "xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitHelpText": "用于分隔键值对的正则表达式模式。通常是空格字符 ({space})。", + "xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysFieldLabel": "包括键", + "xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysHelpText": "要包括在输出中的已提取键的列表。默认为所有键。", + "xpack.ingestPipelines.pipelineEditor.kvForm.prefixFieldLabel": "前缀", + "xpack.ingestPipelines.pipelineEditor.kvForm.prefixHelpText": "要添加到已提取键的前缀。", + "xpack.ingestPipelines.pipelineEditor.kvForm.stripBracketsFieldLabel": "剥离括号", + "xpack.ingestPipelines.pipelineEditor.kvForm.stripBracketsHelpText": "从已提取值中移除括号({paren}、{angle}、{square})和引号({singleQuote}、{doubleQuote})。", + "xpack.ingestPipelines.pipelineEditor.kvForm.targetFieldHelpText": "已提取字段的输出字段。默认为文档根目录。", + "xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyFieldLabel": "剪裁键", + "xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyHelpText": "要从已提取键中剪裁的字符。", + "xpack.ingestPipelines.pipelineEditor.kvForm.trimValueFieldLabel": "剪裁值", + "xpack.ingestPipelines.pipelineEditor.kvForm.trimValueHelpText": "要从已提取值中剪裁的字符。", + "xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitFieldLabel": "值拆分", + "xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitHelpText": "用于拆分键和值的正则表达式模式。通常是赋值运算符 ({equal})。", + "xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.loadFromJson.buttonLabel": "导入处理器", "xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.cancel": "取消", "xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.confirm": "加载并覆盖", "xpack.ingestPipelines.pipelineEditor.loadFromJson.editor": "管道对象", @@ -9339,23 +10519,141 @@ "xpack.ingestPipelines.pipelineEditor.loadFromJson.error.title": "管道无效", "xpack.ingestPipelines.pipelineEditor.loadFromJson.modalTitle": "加载 JSON", "xpack.ingestPipelines.pipelineEditor.loadJsonModal.jsonEditorHelpText": "提供管道对象。这将覆盖现有管道处理器和失败时处理器。", + "xpack.ingestPipelines.pipelineEditor.lowerCaseForm.fieldNameHelpText": "要小写的字段。", "xpack.ingestPipelines.pipelineEditor.onFailureProcessorsDocumentationLink": "了解详情。", "xpack.ingestPipelines.pipelineEditor.onFailureProcessorsLabel": "失败处理程序", "xpack.ingestPipelines.pipelineEditor.onFailureTreeDescription": "用于处理此管道中的异常的处理器。{learnMoreLink}", "xpack.ingestPipelines.pipelineEditor.onFailureTreeTitle": "失败处理器", + "xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldHelpText": "要运行的采集管道的名称。", + "xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldLabel": "管道名称", + "xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameRequiredError": "需要值。", "xpack.ingestPipelines.pipelineEditor.processorsDocumentationLink": "了解详情。", - "xpack.ingestPipelines.pipelineEditor.processorsTreeDescription": "用于在索引之前预处理文档的处理器。{learnMoreLink}", + "xpack.ingestPipelines.pipelineEditor.processorsTreeDescription": "使用处理器可在索引前转换数据。{learnMoreLink}", "xpack.ingestPipelines.pipelineEditor.processorsTreeTitle": "处理器", + "xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameField": "字段", + "xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameHelpText": "要移除的字段。", + "xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameRequiredError": "需要值。", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.cancelButtonLabel": "取消", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.confirmationButtonLabel": "删除处理器", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.titleText": "删除 {type} 处理器", + "xpack.ingestPipelines.pipelineEditor.renameForm.fieldNameHelpText": "要重命名的字段。", + "xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldHelpText": "新字段名称。此字段不能已存在。", + "xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldLabel": "目标字段", + "xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.idRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.langFieldHelpText": "脚本语言。默认为 {lang}。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.langFieldLabel": "语言(可选)", + "xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldAriaLabel": "参数 JSON 编辑器", + "xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldHelpText": "作为变量传递到脚本的命名参数。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldLabel": "参数", + "xpack.ingestPipelines.pipelineEditor.scriptForm.processorInvalidJsonError": "JSON 无效", + "xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldAriaLabel": "源脚本 JSON 编辑器", + "xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldHelpText": "要运行的内联脚本。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldLabel": "源", + "xpack.ingestPipelines.pipelineEditor.scriptForm.sourceRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldHelpText": "要运行的存储脚本的 ID。", + "xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldLabel": "存储脚本 ID", + "xpack.ingestPipelines.pipelineEditor.scriptForm.useScriptIdToggleLabel": "运行存储脚本", + "xpack.ingestPipelines.pipelineEditor.setForm.fieldNameField": "要插入或更新的字段。", + "xpack.ingestPipelines.pipelineEditor.setForm.ignoreEmptyValueFieldHelpText": "如果 {valueField} 是 {nullValue} 或空字符串,请不要更新该字段。", + "xpack.ingestPipelines.pipelineEditor.setForm.ignoreEmptyValueFieldLabel": "忽略空值", + "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldHelpText": "如果启用,则覆盖现有字段值。如果禁用,则仅更新 {nullValue} 字段。", "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel": "覆盖", + "xpack.ingestPipelines.pipelineEditor.setForm.propertiesFieldHelpText": "要添加的用户详情。默认为 {value}", + "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText": "字段的值。", "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "值", - "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "需要设置值。", + "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.fieldNameField": "输出字段。", + "xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.propertiesFieldLabel": "属性(可选)", "xpack.ingestPipelines.pipelineEditor.settingsForm.learnMoreLabelLink.processor": "{processorLabel}文档", + "xpack.ingestPipelines.pipelineEditor.sortForm.fieldNameHelpText": "包含要排序的数组值的字段。", + "xpack.ingestPipelines.pipelineEditor.sortForm.orderField.ascendingOption": "升序", + "xpack.ingestPipelines.pipelineEditor.sortForm.orderField.descendingOption": "降序", + "xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldHelpText": "排序顺序。包含字符串和数字组合的数组按字典顺序排序。", + "xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldLabel": "顺序", + "xpack.ingestPipelines.pipelineEditor.splitForm.fieldNameHelpText": "要拆分的字段。", + "xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldHelpText": "保留拆分字段值中的任何尾随空格。", + "xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldLabel": "保留尾随", + "xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldHelpText": "用于分隔字段值的正则表达式模式。", + "xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldLabel": "分隔符", + "xpack.ingestPipelines.pipelineEditor.splitForm.separatorRequiredError": "需要值。", + "xpack.ingestPipelines.pipelineEditor.testPipeline.buttonLabel": "添加文档", + "xpack.ingestPipelines.pipelineEditor.testPipeline.documentLabel": "文档 {documentNumber}", + "xpack.ingestPipelines.pipelineEditor.testPipeline.documentsdropdown.dropdownLabel": "文档:", + "xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.editDocumentsButtonLabel": "编辑文档", + "xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.popoverTitle": "测试文档", + "xpack.ingestPipelines.pipelineEditor.testPipeline.outputButtonLabel": "查看输出", + "xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.cancelButtonLabel": "取消", + "xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.description": "这将重置输出。", + "xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.resetButtonLabel": "清除文档", + "xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.title": "清除文档", + "xpack.ingestPipelines.pipelineEditor.testPipeline.selectedDocumentLabel": "文档 {selectedDocument}", + "xpack.ingestPipelines.pipelineEditor.testPipeline.testPipelineActionsLabel": "测试管道:", + "xpack.ingestPipelines.pipelineEditor.trimForm.fieldNameHelpText": "要剪裁的字段。对于字符串数组,每个元素都要剪裁。", "xpack.ingestPipelines.pipelineEditor.typeField.fieldRequiredError": "类型必填。", "xpack.ingestPipelines.pipelineEditor.typeField.typeFieldComboboxPlaceholder": "键入后按“ENTER”键", "xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel": "处理器", + "xpack.ingestPipelines.pipelineEditor.uppercaseForm.fieldNameHelpText": "要小写的字段。对于字符串数组,每个元素都要大写。", + "xpack.ingestPipelines.pipelineEditor.urlDecodeForm.fieldNameHelpText": "要解码的字段。对于字符串数组,每个元素都要解码。", + "xpack.ingestPipelines.pipelineEditor.userAgentForm.fieldNameHelpText": "包含用户代理字符串的字段。", + "xpack.ingestPipelines.pipelineEditor.userAgentForm.propertiesFieldHelpText": "添加到目标字段的属性。", + "xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldHelpText": "包含用于解析用户代理字符串的正则表达式的文件。", + "xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldLabel": "正则表达式文件(可选)", + "xpack.ingestPipelines.pipelineEditor.userAgentForm.targetFieldHelpText": "输出字段。默认为 {defaultField}。", + "xpack.ingestPipelines.pipelineEditorItem.droppedStatusAriaLabel": "已丢弃", + "xpack.ingestPipelines.pipelineEditorItem.errorIgnoredStatusAriaLabel": "错误已忽略", + "xpack.ingestPipelines.pipelineEditorItem.errorStatusAriaLabel": "错误", + "xpack.ingestPipelines.pipelineEditorItem.inactiveStatusAriaLabel": "未运行", + "xpack.ingestPipelines.pipelineEditorItem.skippedStatusAriaLabel": "已跳过", + "xpack.ingestPipelines.pipelineEditorItem.successStatusAriaLabel": "成功", + "xpack.ingestPipelines.pipelineEditorItem.unknownStatusAriaLabel": "未知", + "xpack.ingestPipelines.processorFormFlyout.cancelButtonLabel": "取消", + "xpack.ingestPipelines.processorFormFlyout.updateButtonLabel": "更新", + "xpack.ingestPipelines.processorOutput.descriptionText": "预览对测试文档所做的更改。", + "xpack.ingestPipelines.processorOutput.documentLabel": "文档 {number}", + "xpack.ingestPipelines.processorOutput.documentsDropdownLabel": "测试数据:", + "xpack.ingestPipelines.processorOutput.droppedCalloutTitle": "该文档已丢弃。", + "xpack.ingestPipelines.processorOutput.ignoredErrorCodeBlockLabel": "存在已忽略的错误", + "xpack.ingestPipelines.processorOutput.loadingMessage": "正在加载处理器输出…...", + "xpack.ingestPipelines.processorOutput.noOutputCalloutTitle": "输出不适用于此处理器。", + "xpack.ingestPipelines.processorOutput.processorErrorCodeBlockLabel": "有错误", + "xpack.ingestPipelines.processorOutput.processorInputCodeBlockLabel": "数据输入", + "xpack.ingestPipelines.processorOutput.processorOutputCodeBlockLabel": "数据输出", + "xpack.ingestPipelines.processorOutput.skippedCalloutTitle": "该处理器尚未运行。", + "xpack.ingestPipelines.processors.description.append": "将值追加到字段的数组。如果该字段包含单个值,则处理器首先将其转换为数组。如果该字段不存在,则处理器将创建包含已追加值的数组。", + "xpack.ingestPipelines.processors.description.bytes": "将数字存储单位转换为字节。例如,1KB 变成 1024 字节。", + "xpack.ingestPipelines.processors.description.circle": "将圆定义转换为近似多边形。", + "xpack.ingestPipelines.processors.description.convert": "将字段转换为其他数据类型。例如,可将字符串转换为长整型。", + "xpack.ingestPipelines.processors.description.csv": "从 CSV 数据中提取字段值。", + "xpack.ingestPipelines.processors.description.date": "将日期转换为文档时间戳。", + "xpack.ingestPipelines.processors.description.dateIndexName": "使用日期或时间戳可将文档添加到基于正确时间的索引。索引名称必须使用日期数学模式,例如 {value}。", + "xpack.ingestPipelines.processors.description.dissect": "使用分解模式从字段中提取匹配项。", + "xpack.ingestPipelines.processors.description.dotExpander": "将包含点表示法的字段扩展到对象字段中。此后,管道中的其他处理器便可访问该对象字段。", + "xpack.ingestPipelines.processors.description.drop": "丢弃文档而不返回错误。用于仅索引符合指定条件的文档。", + "xpack.ingestPipelines.processors.description.enrich": "根据{enrichPolicyLink}将扩充数据添加到传入文档。", + "xpack.ingestPipelines.processors.description.fail": "失败时返回定制错误消息。通常用于就必要条件通知请求者。", + "xpack.ingestPipelines.processors.description.foreach": "将采集处理器应用于数组中的每个值。", + "xpack.ingestPipelines.processors.description.geoip": "根据 IP 地址添加地理数据。使用 Maxmind 数据库文件中的地理数据。", + "xpack.ingestPipelines.processors.description.grok": "使用 {grokLink} 表达式从字段中提取匹配项。", + "xpack.ingestPipelines.processors.description.gsub": "使用正则表达式替换字段子字符串。", + "xpack.ingestPipelines.processors.description.htmlStrip": "从字段中移除 HTML 标记。", + "xpack.ingestPipelines.processors.description.inference": "使用预先训练的数据帧分析模型对传入数据进行推理。", + "xpack.ingestPipelines.processors.description.join": "将数组元素联接成字符串。在每个元素之间插入分隔符。", + "xpack.ingestPipelines.processors.description.json": "通过兼容字符串创建 JSON 对象。", + "xpack.ingestPipelines.processors.description.kv": "从包含键值对的字符串中提取字段。", + "xpack.ingestPipelines.processors.description.lowercase": "将字符串转换为小写形式。", + "xpack.ingestPipelines.processors.description.pipeline": "运行其他采集节点管道。", + "xpack.ingestPipelines.processors.description.remove": "移除一个或多个字段。", + "xpack.ingestPipelines.processors.description.rename": "重命名现有字段。", + "xpack.ingestPipelines.processors.description.script": "对传入文档运行脚本。", + "xpack.ingestPipelines.processors.description.set": "设置字段的值。", + "xpack.ingestPipelines.processors.description.setSecurityUser": "将有关当前用户的详情(例如用户名和电子邮件地址)添加到传入文档。对于该索引请求,需要经过身份验证的用户。", + "xpack.ingestPipelines.processors.description.sort": "对字段的数组元素进行排序。", + "xpack.ingestPipelines.processors.description.split": "将字段值拆分成数组。", + "xpack.ingestPipelines.processors.description.trim": "从字符串中移除前导和尾随空格。", + "xpack.ingestPipelines.processors.description.uppercase": "将字符串转换为大写形式。", + "xpack.ingestPipelines.processors.description.urldecode": "对 URL 编码字符串进行解码。", + "xpack.ingestPipelines.processors.description.userAgent": "从浏览器的用户代理字符串中提取值。", "xpack.ingestPipelines.processors.label.append": "追加", "xpack.ingestPipelines.processors.label.bytes": "字节", "xpack.ingestPipelines.processors.label.circle": "圆形", @@ -9376,7 +10674,7 @@ "xpack.ingestPipelines.processors.label.inference": "推理", "xpack.ingestPipelines.processors.label.join": "联接", "xpack.ingestPipelines.processors.label.json": "JSON", - "xpack.ingestPipelines.processors.label.kv": "KV", + "xpack.ingestPipelines.processors.label.kv": "键值 (KV)", "xpack.ingestPipelines.processors.label.lowercase": "小写", "xpack.ingestPipelines.processors.label.pipeline": "管道", "xpack.ingestPipelines.processors.label.remove": "移除", @@ -9394,47 +10692,71 @@ "xpack.ingestPipelines.requestFlyout.descriptionText": "此 Elasticsearch 请求将创建或更新管道。", "xpack.ingestPipelines.requestFlyout.namedTitle": "对“{name}”的请求", "xpack.ingestPipelines.requestFlyout.unnamedTitle": "请求", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.configurationTabTitle": "配置", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.configureOnFailureTitle": "配置失败时处理器", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.configureTitle": "配置处理器", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.manageOnFailureTitle": "配置失败时处理器", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.manageTitle": "管理处理器", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.outputTabTitle": "输出", + "xpack.ingestPipelines.tabs.documentsTabTitle": "文档", "xpack.ingestPipelines.tabs.outputTabTitle": "输出", + "xpack.ingestPipelines.testPipeline.errorNotificationText": "执行管道时出错", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel": "文档", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError": "文档 JSON 无效。", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.noDocumentsError": "需要指定文档。", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.oneDocumentRequiredError": "至少需要一个文档。", "xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldAriaLabel": "文档 JSON 编辑器", + "xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldClearAllButtonLabel": "全部清除", "xpack.ingestPipelines.testPipelineFlyout.documentsTab.runButtonLabel": "运行管道", "xpack.ingestPipelines.testPipelineFlyout.documentsTab.runningButtonLabel": "正在运行", - "xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink": "了解详情", - "xpack.ingestPipelines.testPipelineFlyout.documentsTab.tabDescriptionText": "为管道提供要采集的一系列文档。{learnMoreLink}", + "xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink": "了解详情。", + "xpack.ingestPipelines.testPipelineFlyout.documentsTab.tabDescriptionText": "提供管道要采集的文档。{learnMoreLink}", "xpack.ingestPipelines.testPipelineFlyout.executePipelineError": "无法执行管道", "xpack.ingestPipelines.testPipelineFlyout.outputTab.descriptionLinkLabel": "刷新输出", "xpack.ingestPipelines.testPipelineFlyout.outputTab.descriptionText": "查看输出数据或了解文档通过管道时每个处理器对文档的影响。", "xpack.ingestPipelines.testPipelineFlyout.outputTab.verboseSwitchLabel": "查看详细输出", "xpack.ingestPipelines.testPipelineFlyout.successNotificationText": "管道已执行", "xpack.ingestPipelines.testPipelineFlyout.title": "测试管道", + "xpack.lens.app.addToLibrary": "保存到库", + "xpack.lens.app.cancel": "取消", + "xpack.lens.app.cancelButtonAriaLabel": "返回到上一个应用而不保存更改", "xpack.lens.app.docLoadingError": "加载已保存文档时出错", "xpack.lens.app.docSavingError": "保存文档时出错", "xpack.lens.app.indexPatternLoadingError": "加载索引模式时出错", "xpack.lens.app.save": "保存", "xpack.lens.app.saveAndReturn": "保存并返回", + "xpack.lens.app.saveAndReturnButtonAriaLabel": "保存当前 Lens 可视化并返回到上一应用", "xpack.lens.app.saveAs": "另存为", + "xpack.lens.app.saveButtonAriaLabel": "保存当前的 Lens 可视化", "xpack.lens.app.saveModalType": "Lens 可视化", "xpack.lens.app.unsavedWorkMessage": "离开 Lens,不保存工作?", "xpack.lens.app.unsavedWorkTitle": "未保存更改", + "xpack.lens.app.updatePanel": "更新 {originatingAppName} 中的面板", "xpack.lens.app404": "404 找不到", + "xpack.lens.breadcrumbsByValue": "编辑可视化", "xpack.lens.breadcrumbsCreate": "创建", "xpack.lens.breadcrumbsTitle": "可视化", "xpack.lens.chartSwitch.dataLossDescription": "切换到此图表将会丢失部分配置", "xpack.lens.chartSwitch.dataLossLabel": "数据丢失", + "xpack.lens.chartSwitch.noResults": "找不到 {term} 的结果。", "xpack.lens.chartTitle.unsaved": "未保存", + "xpack.lens.configPanel.chartType": "图表类型", "xpack.lens.configPanel.color.tooltip.auto": "Lens 自动为您选取颜色,除非您指定定制颜色。", "xpack.lens.configPanel.color.tooltip.custom": "清除定制颜色以返回到“自动”模式。", "xpack.lens.configPanel.color.tooltip.disabled": "当图层包括“细分依据”,各个系列无法定制颜色。", "xpack.lens.configPanel.selectVisualization": "选择可视化", - "xpack.lens.configure.editConfig": "编辑配置", - "xpack.lens.configure.emptyConfig": "将字段拖放到此处", + "xpack.lens.configure.configurePanelTitle": "{groupLabel} 配置", + "xpack.lens.configure.editConfig": "单击以编辑配置或进行拖移", + "xpack.lens.configure.emptyConfig": "放置字段或单击以添加", + "xpack.lens.configure.invalidConfigTooltip": "配置无效。", + "xpack.lens.configure.invalidConfigTooltipClick": "单击了解更多详情。", + "xpack.lens.customBucketContainer.dragToReorder": "拖动以重新排序", "xpack.lens.dataPanelWrapper.switchDatasource": "切换到数据源", + "xpack.lens.datatable.breakdown": "细分方式", "xpack.lens.datatable.conjunctionSign": " & ", "xpack.lens.datatable.expressionHelpLabel": "数据表呈现器", "xpack.lens.datatable.label": "数据表", + "xpack.lens.datatable.metrics": "指标", "xpack.lens.datatable.suggestionLabel": "作为表", "xpack.lens.datatable.titleLabel": "标题", "xpack.lens.datatable.visualizationName": "数据表", @@ -9446,10 +10768,13 @@ "xpack.lens.datatypes.record": "记录", "xpack.lens.datatypes.string": "字符串", "xpack.lens.deleteLayer": "删除图层", + "xpack.lens.dimensionContainer.close": "关闭", + "xpack.lens.discover.visualizeFieldLegend": "可视化字段", "xpack.lens.editLayerSettings": "编辑图层设置", "xpack.lens.editorFrame.dataFailure": "加载数据时出错。", "xpack.lens.editorFrame.emptyWorkspace": "将一些字段拖放到此处以开始", "xpack.lens.editorFrame.emptyWorkspaceHeading": "Lens 是用于创建可视化的新工具", + "xpack.lens.editorFrame.emptyWorkspaceSimple": "将字段放到此处", "xpack.lens.editorFrame.expandRenderingErrorButton": "显示错误的详情", "xpack.lens.editorFrame.expressionFailure": "无法成功执行表达式", "xpack.lens.editorFrame.goToForums": "提出请求并提供反馈", @@ -9483,8 +10808,10 @@ "xpack.lens.indexPattern.cardinality": "唯一计数", "xpack.lens.indexPattern.cardinalityOf": "{name} 的唯一计数", "xpack.lens.indexPattern.changeIndexPatternTitle": "更改索引模式", + "xpack.lens.indexPattern.chooseField": "选择字段", + "xpack.lens.indexPattern.chooseFieldLabel": "要使用此函数,请选择字段。", "xpack.lens.indexPattern.columnFormatLabel": "值个是", - "xpack.lens.indexPattern.columnLabel": "标签", + "xpack.lens.indexPattern.columnLabel": "显示名称", "xpack.lens.indexPattern.count": "计数", "xpack.lens.indexPattern.countOf": "文档计数", "xpack.lens.indexPattern.dateHistogram": "Date histogram", @@ -9509,18 +10836,32 @@ "xpack.lens.indexPattern.fieldItemTooltip": "拖放以可视化。", "xpack.lens.indexPattern.fieldPanelEmptyStringValue": "空字符串", "xpack.lens.indexPattern.fieldPlaceholder": "字段", + "xpack.lens.indexPattern.fieldStatsButtonAriaLabel": "{fieldName}:{fieldType}。按 Enter 键可进行字段预览。", + "xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "此字段不包含任何数据,但您仍然可以拖放以进行可视化。", "xpack.lens.indexPattern.fieldStatsButtonLabel": "单击以进行字段预览,或拖放以进行可视化。", "xpack.lens.indexPattern.fieldStatsCountLabel": "计数", "xpack.lens.indexPattern.fieldStatsDisplayToggle": "切换", - "xpack.lens.indexPattern.fieldStatsNoData": "没有可显示的数据", + "xpack.lens.indexPattern.fieldStatsNoData": "此字段为空,因为它不存在于 500 个采样文档中。将此字段添加到配置可能会导致空图表。", "xpack.lens.indexPattern.fieldTimeDistributionLabel": "时间分布", "xpack.lens.indexPattern.fieldTopValuesLabel": "排名最前值", + "xpack.lens.indexPattern.filters": "筛选", + "xpack.lens.indexPattern.filters.addaFilter": "添加筛选", + "xpack.lens.indexPattern.filters.clickToEdit": "单击以编辑", + "xpack.lens.indexPattern.filters.isInvalid": "此查询无效。", + "xpack.lens.indexPattern.filters.label.placeholder": "所有记录", + "xpack.lens.indexPattern.filters.queryPlaceholderKql": "{example}", + "xpack.lens.indexPattern.filters.queryPlaceholderLucene": "{example}", + "xpack.lens.indexPattern.filters.removeFilter": "移除筛选", + "xpack.lens.indexPattern.functionsLabel": "选择函数", "xpack.lens.indexPattern.groupByDropdown": "分组依据", "xpack.lens.indexPattern.indexPatternLoadError": "加载索引模式时出错", + "xpack.lens.indexPattern.intervals": "时间间隔", + "xpack.lens.indexPattern.invalidFieldLabel": "字段无效。检查索引模式或选取其他字段。", "xpack.lens.indexPattern.invalidInterval": "时间间隔值无效", "xpack.lens.indexPattern.invalidOperationLabel": "要使用此函数,请选择不同的字段。", "xpack.lens.indexPattern.max": "最大值", "xpack.lens.indexPattern.maxOf": "{name} 的最大值", + "xpack.lens.indexPattern.metaFieldsLabel": "元字段", "xpack.lens.indexPattern.min": "最小值", "xpack.lens.indexPattern.minOf": "{name} 的最小值", "xpack.lens.indexPattern.noPatternsDescription": "请创建索引模式或切换到其他数据源", @@ -9530,6 +10871,20 @@ "xpack.lens.indexPattern.otherDocsLabel": "其他", "xpack.lens.indexPattern.percentageOfLabel": "{percentage}% 的", "xpack.lens.indexPattern.percentFormatLabel": "百分比", + "xpack.lens.indexPattern.range.isInvalid": "此范围无效", + "xpack.lens.indexPattern.ranges.addRange": "添加范围", + "xpack.lens.indexPattern.ranges.customIntervalsToggle": "创建定制范围", + "xpack.lens.indexPattern.ranges.customRanges": "范围", + "xpack.lens.indexPattern.ranges.customRangesRemoval": "移除定制范围", + "xpack.lens.indexPattern.ranges.decreaseButtonLabel": "减小粒度", + "xpack.lens.indexPattern.ranges.deleteRange": "删除范围", + "xpack.lens.indexPattern.ranges.granularity": "时间间隔粒度", + "xpack.lens.indexPattern.ranges.granularityDescription": "将字段分成间隔均匀的时间间隔。", + "xpack.lens.indexPattern.ranges.increaseButtonLabel": "增加粒度", + "xpack.lens.indexPattern.ranges.lessThanOrEqualAppend": "≤", + "xpack.lens.indexPattern.ranges.lessThanOrEqualTooltip": "小于或等于", + "xpack.lens.indexPattern.ranges.lessThanPrepend": "<", + "xpack.lens.indexPattern.ranges.lessThanTooltip": "小于", "xpack.lens.indexPattern.records": "记录", "xpack.lens.indexPattern.removeColumnLabel": "移除配置", "xpack.lens.indexpattern.suggestions.nestingChangeLabel": "每个 {outerOperation} 的 {innerOperation}", @@ -9546,6 +10901,7 @@ "xpack.lens.indexPattern.terms.size": "值数目", "xpack.lens.indexPattern.termsOf": "{name} 的排名最前值", "xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]", + "xpack.lens.indexPattern.useAsTopLevelAgg": "先按此字段分组", "xpack.lens.indexPatterns.clearFiltersLabel": "清除名称和类型筛选", "xpack.lens.indexPatterns.fieldFiltersLabel": "自动筛选", "xpack.lens.indexPatterns.filterByNameAriaLabel": "搜索字段", @@ -9559,6 +10915,7 @@ "xpack.lens.indexPatterns.noFilteredFieldsLabel": "没有字段匹配选定筛选。", "xpack.lens.indexPatternSuggestion.removeLayerLabel": "仅显示 {indexPatternTitle}", "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "仅显示图层 {layerNumber}", + "xpack.lens.labelInput.label": "标签", "xpack.lens.lensSavedObjectLabel": "Lens 可视化", "xpack.lens.metric.label": "指标", "xpack.lens.pageTitle": "Lens", @@ -9576,14 +10933,28 @@ "xpack.lens.pieChart.categoriesInLegendLabel": "隐藏标签", "xpack.lens.pieChart.fitInsideOnlyLabel": "仅内部", "xpack.lens.pieChart.hiddenNumbersLabel": "在图表中隐藏", - "xpack.lens.pieChart.labelPositionLabel": "标签位置", - "xpack.lens.pieChart.nestedLegendLabel": "嵌套图例", - "xpack.lens.pieChart.numberLabels": "标签值", + "xpack.lens.pieChart.labelPositionLabel": "位置", + "xpack.lens.pieChart.legendVisibility.auto": "自动", + "xpack.lens.pieChart.legendVisibility.hide": "隐藏", + "xpack.lens.pieChart.legendVisibility.show": "显示", + "xpack.lens.pieChart.nestedLegendLabel": "嵌套", + "xpack.lens.pieChart.numberLabels": "值", + "xpack.lens.pieChart.percentDecimalsLabel": "百分比的最大小数位数", "xpack.lens.pieChart.showCategoriesLabel": "内部或外部", "xpack.lens.pieChart.showFormatterValuesLabel": "显示值", "xpack.lens.pieChart.showPercentValuesLabel": "显示百分比", "xpack.lens.pieChart.showTreemapCategoriesLabel": "显示标签", + "xpack.lens.pieChart.valuesLabel": "标签", "xpack.lens.resetLayer": "重置图层", + "xpack.lens.searchTitle": "Lens:创建可视化", + "xpack.lens.shared.legendLabel": "图例", + "xpack.lens.shared.legendPositionBottom": "底部", + "xpack.lens.shared.legendPositionLabel": "位置", + "xpack.lens.shared.legendPositionLeft": "左", + "xpack.lens.shared.legendPositionRight": "右", + "xpack.lens.shared.legendPositionTop": "顶部", + "xpack.lens.shared.legendVisibilityLabel": "显示", + "xpack.lens.shared.nestedLegendLabel": "嵌套", "xpack.lens.sugegstion.refreshSuggestionLabel": "刷新", "xpack.lens.suggestion.refreshSuggestionTooltip": "基于选定可视化刷新建议。", "xpack.lens.suggestions.currentVisLabel": "当前", @@ -9592,30 +10963,70 @@ "xpack.lens.visTypeAlias.promotion.description": "试用 Lens,以全新直观的方式创建可视化。", "xpack.lens.visTypeAlias.title": "Lens 可视化", "xpack.lens.visTypeAlias.type": "Lens", + "xpack.lens.xyChart.addLayer": "添加图层", "xpack.lens.xyChart.addLayerButton": "添加图层", "xpack.lens.xyChart.addLayerTooltip": "使用多个图层以组合图表类型或可视化不同的索引模式。", + "xpack.lens.xyChart.axisNameLabel": "轴名称", "xpack.lens.xyChart.axisSide.auto": "自动", + "xpack.lens.xyChart.axisSide.bottom": "底部", "xpack.lens.xyChart.axisSide.label": "轴侧", "xpack.lens.xyChart.axisSide.left": "左", "xpack.lens.xyChart.axisSide.right": "右", + "xpack.lens.xyChart.axisSide.top": "顶部", + "xpack.lens.xyChart.axisTitlesSettings.help": "显示 x 和 y 轴标题", + "xpack.lens.xyChart.bottomAxisDisabledHelpText": "此设置仅在启用底轴时应用。", + "xpack.lens.xyChart.bottomAxisLabel": "底轴", "xpack.lens.xyChart.chartTypeLabel": "图表类型", "xpack.lens.xyChart.chartTypeLegend": "图表类型", - "xpack.lens.xyChart.fittingDisabledHelpText": "此设置仅适用于折线图和非堆叠面积图。", + "xpack.lens.xyChart.emptyXLabel": "(空)", + "xpack.lens.xyChart.fittingDisabledHelpText": "此设置仅适用于折线图和面积图。", "xpack.lens.xyChart.fittingFunction.help": "定义处理缺失值的方式", + "xpack.lens.xyChart.Gridlines": "网格线", + "xpack.lens.xyChart.gridlinesSettings.help": "显示 x 和 y 轴网格线", "xpack.lens.xyChart.help": "X/Y 图表", "xpack.lens.xyChart.isVisible.help": "指定图例是否可见。", + "xpack.lens.xyChart.leftAxisDisabledHelpText": "此设置仅在启用左轴时应用。", + "xpack.lens.xyChart.leftAxisLabel": "左轴", "xpack.lens.xyChart.legend.help": "配置图表图例。", + "xpack.lens.xyChart.legendVisibility.auto": "自动", + "xpack.lens.xyChart.legendVisibility.hide": "隐藏", + "xpack.lens.xyChart.legendVisibility.show": "显示", + "xpack.lens.xyChart.missingValuesLabel": "缺少的值", "xpack.lens.xyChart.nestUnderRoot": "整个数据集", + "xpack.lens.xyChart.overwriteAxisTitle": "覆盖轴标题", "xpack.lens.xyChart.position.help": "指定图例位置。", "xpack.lens.xyChart.renderer.help": "X/Y 图表呈现器", + "xpack.lens.xyChart.rightAxisDisabledHelpText": "此设置仅在启用右轴时应用。", + "xpack.lens.xyChart.rightAxisLabel": "右轴", "xpack.lens.xyChart.seriesColor.auto": "自动", "xpack.lens.xyChart.seriesColor.label": "系列颜色", + "xpack.lens.xyChart.ShowAxisTitleLabel": "显示", + "xpack.lens.xyChart.showSingleSeries.help": "指定是否应显示只包含一个条目的图例", "xpack.lens.xyChart.splitSeries": "拆分序列", + "xpack.lens.xyChart.tickLabels": "刻度标签", + "xpack.lens.xyChart.tickLabelsSettings.help": "显示 x 和 y 轴刻度标签", "xpack.lens.xyChart.title.help": "轴标题", + "xpack.lens.xyChart.topAxisDisabledHelpText": "此设置仅在启用顶轴时应用。", + "xpack.lens.xyChart.topAxisLabel": "顶轴", + "xpack.lens.xyChart.valuesLabel": "值", + "xpack.lens.xyChart.xAxisGridlines.help": "指定 x 轴的网格线是否可见。", "xpack.lens.xyChart.xAxisLabel": "X 轴", + "xpack.lens.xyChart.xAxisTickLabels.help": "指定 x 轴的刻度标签是否可见。", + "xpack.lens.xyChart.xAxisTitle.help": "指定 x 轴的标题是否可见。", + "xpack.lens.xyChart.xTitle.help": "X 轴标题", "xpack.lens.xyChart.yAxisLabel": "Y 轴", + "xpack.lens.xyChart.yLeftAxisgridlines.help": "指定左侧 y 轴的网格线是否可见。", + "xpack.lens.xyChart.yLeftAxisTickLabels.help": "指定左侧 y 轴的刻度标签是否可见。", + "xpack.lens.xyChart.yLeftAxisTitle.help": "指定左侧 y 轴的标题是否可见。", + "xpack.lens.xyChart.yLeftTitle.help": "左侧 Y 轴标题", + "xpack.lens.xyChart.yRightAxisgridlines.help": "指定右侧 y 轴的网格线是否可见。", + "xpack.lens.xyChart.yRightAxisTickLabels.help": "指定右侧 y 轴的刻度标签是否可见。", + "xpack.lens.xyChart.yRightAxisTitle.help": "指定右侧 y 轴的标题是否可见。", + "xpack.lens.xyChart.yRightTitle.help": "右侧 Y 轴标题", + "xpack.lens.xySuggestions.asPercentageTitle": "百分比", "xpack.lens.xySuggestions.barChartTitle": "条形图", "xpack.lens.xySuggestions.dateSuggestion": "{yTitle} / {xTitle}", + "xpack.lens.xySuggestions.emptyAxisTitle": "(空)", "xpack.lens.xySuggestions.flipTitle": "翻转", "xpack.lens.xySuggestions.lineChartTitle": "折线图", "xpack.lens.xySuggestions.nonDateSuggestion": "{xTitle} 的 {yTitle}", @@ -9623,13 +11034,21 @@ "xpack.lens.xySuggestions.unstackedChartTitle": "非堆叠", "xpack.lens.xySuggestions.yAxixConjunctionSign": " & ", "xpack.lens.xyVisualization.areaLabel": "面积图", + "xpack.lens.xyVisualization.barHorizontalFullLabel": "水平条形", + "xpack.lens.xyVisualization.barHorizontalLabel": "水平条形图", "xpack.lens.xyVisualization.barLabel": "条形图", "xpack.lens.xyVisualization.lineLabel": "折线图", - "xpack.lens.xyVisualization.mixedBarHorizontalLabel": "混合水平条形图", + "xpack.lens.xyVisualization.mixedBarHorizontalLabel": "水平组合条形图", "xpack.lens.xyVisualization.mixedLabel": "混合 XY", "xpack.lens.xyVisualization.noDataLabel": "找不到结果", "xpack.lens.xyVisualization.stackedAreaLabel": "堆叠面积图", + "xpack.lens.xyVisualization.stackedBarHorizontalFullLabel": "水平堆叠条形图", + "xpack.lens.xyVisualization.stackedBarHorizontalLabel": "水平堆叠条形图", "xpack.lens.xyVisualization.stackedBarLabel": "堆叠条形图", + "xpack.lens.xyVisualization.stackedPercentageAreaLabel": "百分比面积图", + "xpack.lens.xyVisualization.stackedPercentageBarHorizontalFullLabel": "水平百分比条形图", + "xpack.lens.xyVisualization.stackedPercentageBarHorizontalLabel": "水平百分比条形图", + "xpack.lens.xyVisualization.stackedPercentageBarLabel": "百分比条形图", "xpack.lens.xyVisualization.xyLabel": "XY", "xpack.licenseMgmt.app.checkingPermissionsErrorMessage": "检查权限时出错", "xpack.licenseMgmt.app.deniedPermissionDescription": "要使用许可管理,必须具有{permissionType}权限。", @@ -9800,11 +11219,14 @@ "xpack.logstash.upgradeFailureActions.goBackButtonLabel": "返回", "xpack.logstash.upstreamPipelineArgumentMustContainAnIdPropertyErrorMessage": "upstreamPipeline 参数必须包含 id 属性", "xpack.logstash.workersTooltip": "并行执行管道的筛选和输出阶段的工作线程数目。如果您发现事件出现积压或 CPU 未饱和,请考虑增大此数值,以更好地利用机器处理能力。\n\n默认值:主机的 CPU 核心数", + "xpack.maps.actionSelect.label": "操作", "xpack.maps.addLayerPanel.addLayer": "添加图层", "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "更改图层", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "鍙栨秷", "xpack.maps.aggs.defaultCountLabel": "计数", "xpack.maps.appTitle": "Maps", + "xpack.maps.badge.readOnly.text": "只读", + "xpack.maps.badge.readOnly.tooltip": "无法保存地图", "xpack.maps.blendedVectorLayer.clusteredLayerName": "集群 {displayName}", "xpack.maps.breadCrumbs.unsavedChangesWarning": "您的地图中包含未保存的更改。是否确定要离开?", "xpack.maps.choropleth.boundaries.elasticsearch": "Elasticsearch 的点、线和多边形", @@ -9822,6 +11244,7 @@ "xpack.maps.common.esSpatialRelation.disjointLabel": "disjoint", "xpack.maps.common.esSpatialRelation.intersectsLabel": "intersects", "xpack.maps.common.esSpatialRelation.withinLabel": "within", + "xpack.maps.discover.visualizeFieldLabel": "在 Maps 中可视化", "xpack.maps.distanceFilterForm.filterLabelLabel": "筛选标签", "xpack.maps.drawTooltip.boundsInstructions": "单击可开始绘制矩形。移动鼠标以调整矩形大小。再次单击以完成。", "xpack.maps.drawTooltip.distanceInstructions": "单击以设置点。移动鼠标以调整距离。单击以完成。", @@ -9843,7 +11266,8 @@ "xpack.maps.esSearch.topHitsEntitiesCountMsg": "找到 {entityCount} 个实体。", "xpack.maps.esSearch.topHitsResultsTrimmedMsg": "结果限制为 ~{totalEntities} 个实体中的前 {entityCount} 个。", "xpack.maps.esSearch.topHitsSizeMsg": "显示每个实体排名前 {topHitsSize} 的文档。", - "xpack.maps.feature.appDescription": "从 Elasticsearch 和 Elastic 地图服务浏览地理空间数据", + "xpack.maps.feature.appDescription": "从 Elasticsearch 和 Elastic Maps Service 浏览地理空间数据。", + "xpack.maps.featureCatalogue.mapsSubtitle": "绘制地理数据。", "xpack.maps.featureRegistry.mapsFeatureName": "Maps", "xpack.maps.fileUploadWizard.description": "在 Elasticsearch 中索引 GeoJSON 数据", "xpack.maps.fileUploadWizard.importFileSetupLabel": "导入文件", @@ -9971,6 +11395,8 @@ "xpack.maps.mapListing.unableToDeleteToastTitle": "无法删除地图", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "选择索引模式", "xpack.maps.mapSavedObjectLabel": "地图", + "xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "使地图自适应数据边界", + "xpack.maps.mapSettingsPanel.autoFitToDataBoundsLabel": "使地图自适应数据边界", "xpack.maps.mapSettingsPanel.browserLocationLabel": "浏览器位置", "xpack.maps.mapSettingsPanel.cancelLabel": "取消", "xpack.maps.mapSettingsPanel.closeLabel": "关闭", @@ -10012,6 +11438,7 @@ "xpack.maps.mvtSource.tooltipsTitle": "工具提示字段", "xpack.maps.mvtSource.trashButtonAriaLabel": "移除字段", "xpack.maps.mvtSource.trashButtonTitle": "移除字段", + "xpack.maps.newMapTitle": "新地图", "xpack.maps.noIndexPattern.doThisLinkTextDescription": "创建索引模式", "xpack.maps.noIndexPattern.doThisPrefixDescription": "您将需要 ", "xpack.maps.noIndexPattern.doThisSuffixDescription": " 使用地理空间字段。", @@ -10088,6 +11515,7 @@ "xpack.maps.source.esGrid.metricsLabel": "指标", "xpack.maps.source.esGrid.noIndexPatternErrorMessage": "找不到索引模式 {id}", "xpack.maps.source.esGrid.resolutionParamErrorMessage": "无法识别网格分辨率参数:{resolution}", + "xpack.maps.source.esGrid.superFineDropDownOption": "超精致(公测版)", "xpack.maps.source.esGridClustersDescription": "地理空间数据以网格进行分组,每个网格单元格都具有指标", "xpack.maps.source.esGridClustersTitle": "集群和网格", "xpack.maps.source.esGridHeatmapDescription": "地理空间数据以网格进行分组以显示密度", @@ -10105,9 +11533,11 @@ "xpack.maps.source.esSearch.geoFieldTypeLabel": "地理空间字段类型", "xpack.maps.source.esSearch.indexPatternLabel": "索引模式", "xpack.maps.source.esSearch.joinsDisabledReason": "按集群缩放时不支持联接", + "xpack.maps.source.esSearch.joinsDisabledReasonMvt": "按 mvt 矢量磁贴缩放时不支持联接", "xpack.maps.source.esSearch.limitScalingLabel": "将结果数限制到 {maxResultWindow}。", "xpack.maps.source.esSearch.loadErrorMessage": "找不到索引模式 {id}", "xpack.maps.source.esSearch.loadTooltipPropertiesErrorMsg": "找不到文档,_id:{docId}", + "xpack.maps.source.esSearch.mvtDescription": "使用矢量磁贴可更快速地显示大型数据集。", "xpack.maps.source.esSearch.selectLabel": "选择地理字段", "xpack.maps.source.esSearch.sortFieldLabel": "字段", "xpack.maps.source.esSearch.sortFieldSelectPlaceholder": "选择排序字段", @@ -10115,6 +11545,7 @@ "xpack.maps.source.esSearch.topHitsSizeLabel": "每个实体的文档", "xpack.maps.source.esSearch.topHitsSplitFieldLabel": "实体", "xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder": "选择实体字段", + "xpack.maps.source.esSearch.useMVTVectorTiles": "使用矢量磁贴", "xpack.maps.source.esSearch.useTopHitsLabel": "显示每个实体最高命中结果。", "xpack.maps.source.esSearchDescription": "Elasticsearch 的点、线和多边形", "xpack.maps.source.esSearchTitle": "文档", @@ -10240,22 +11671,39 @@ "xpack.maps.tooltip.layerFilterLabel": "按图层筛选结果", "xpack.maps.tooltip.loadingMsg": "正在加载", "xpack.maps.tooltip.pageNumerText": "{total} 的 {pageNumber}", + "xpack.maps.tooltip.showAddFilterActionsViewLabel": "筛选操作", "xpack.maps.tooltip.showGeometryFilterViewLinkLabel": "按几何筛选", "xpack.maps.tooltip.toolsControl.cancelDrawButtonLabel": "取消", "xpack.maps.tooltip.unableToLoadContentTitle": "无法加载工具提示内容", + "xpack.maps.tooltip.viewActionsTitle": "查看筛选操作", "xpack.maps.tooltipSelector.addLabelWithCount": "添加 {count} 个", "xpack.maps.tooltipSelector.addLabelWithoutCount": "添加", + "xpack.maps.tooltipSelector.emptyState.description": "添加工具提示字段以通过字段值创建筛选。", "xpack.maps.tooltipSelector.grabButtonAriaLabel": "重新排序属性", "xpack.maps.tooltipSelector.grabButtonTitle": "重新排序属性", "xpack.maps.tooltipSelector.togglePopoverLabel": "添加", "xpack.maps.tooltipSelector.trashButtonAriaLabel": "移除属性", "xpack.maps.tooltipSelector.trashButtonTitle": "移除属性", + "xpack.maps.topNav.fullScreenButtonLabel": "全屏", + "xpack.maps.topNav.fullScreenDescription": "全屏", + "xpack.maps.topNav.openInspectorButtonLabel": "检查", + "xpack.maps.topNav.openInspectorDescription": "打开检查器", + "xpack.maps.topNav.openSettingsButtonLabel": "地图设置", + "xpack.maps.topNav.openSettingsDescription": "打开地图设置", + "xpack.maps.topNav.saveAndReturnButtonLabel": "保存并返回", + "xpack.maps.topNav.saveAsButtonLabel": "另存为", + "xpack.maps.topNav.saveErrorMessage": "保存“{title}”时出错", + "xpack.maps.topNav.saveMapButtonLabel": "保存", + "xpack.maps.topNav.saveMapDescription": "保存地图", + "xpack.maps.topNav.saveMapDisabledButtonTooltip": "保存前确认或取消您的图层更改", + "xpack.maps.topNav.saveModalType": "地图", + "xpack.maps.topNav.saveSuccessMessage": "已保存“{title}”", "xpack.maps.tutorials.ems.downloadStepText": "1.导航到 Elastic Maps Service [登陆页]({emsLandingPageUrl})。\n2.在左边栏中,选择管理边界。\n3.单击`下载 GeoJSON` 按钮。", "xpack.maps.tutorials.ems.downloadStepTitle": "下载 Elastic Maps Service 边界", "xpack.maps.tutorials.ems.longDescription": "[Elastic Maps Service (EMS)](https://www.elastic.co/elastic-maps-service) 托管管理边界的磁贴图层和向量形状。在 Elasticsearch 中索引 EMS 管理边界将允许基于边界属性字段进行搜索。", "xpack.maps.tutorials.ems.nameTitle": "EMS 边界", "xpack.maps.tutorials.ems.shortDescription": "来自 Elastic Maps Service 的管理边界。", - "xpack.maps.tutorials.ems.uploadStepText": "1.打开 [Elastic 地图]({newMapUrl})。\n2.单击`添加图层`,然后选择`上传 GeoJSON`。\n3.上传 GeoJSON 文件,然后单击`导入文件`。", + "xpack.maps.tutorials.ems.uploadStepText": "1.打开 [Maps]({newMapUrl}).\n2.单击`添加图层`,然后选择`上传 GeoJSON`。\n3.上传 GeoJSON 文件,然后单击`导入文件`。", "xpack.maps.tutorials.ems.uploadStepTitle": "索引 Elastic Maps Service 边界", "xpack.maps.validatedRange.rangeErrorMessage": "必须介于 {min} 和 {max} 之间", "xpack.maps.vector.dualSize.unitLabel": "px", @@ -10283,11 +11731,17 @@ "xpack.ml.accessDenied.description": "您无权访问 ML 插件", "xpack.ml.accessDenied.label": "权限不足", "xpack.ml.accessDeniedLabel": "访问被拒绝", + "xpack.ml.accessDeniedTabLabel": "访问被拒绝", "xpack.ml.actions.applyInfluencersFiltersTitle": "筛留值", "xpack.ml.actions.applyTimeRangeSelectionTitle": "应用时间范围选择", "xpack.ml.actions.editSwimlaneTitle": "编辑泳道", "xpack.ml.actions.influencerFilterAliasLabel": "影响因素 {labelValue}", "xpack.ml.actions.openInAnomalyExplorerTitle": "在 Anomaly Explorer 中打开", + "xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeDesc": "在查看异常检测作业结果时要使用的时间筛选选项。", + "xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeName": "异常检测结果的时间筛选默认值", + "xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeDesc": "使用 Single Metric Viewer 和 Anomaly Explorer 中的默认时间筛选。如果未启用,则将显示作业的整个时间范围的结果。", + "xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeName": "对异常检测结果启用时间筛选默认值", + "xpack.ml.analyticList.searchBar.invalidSearchErrorMessage": "搜索无效:{errorMessage}", "xpack.ml.annotationFlyout.applyToPartitionTextLabel": "将标注应用到此系列", "xpack.ml.annotationsTable.actionsColumnName": "操作", "xpack.ml.annotationsTable.annotationColumnName": "注释", @@ -10384,6 +11838,7 @@ "xpack.ml.anomalyDetection.jobManagementLabel": "作业管理", "xpack.ml.anomalyDetection.singleMetricViewerLabel": "Single Metric Viewer", "xpack.ml.anomalyDetectionBreadcrumbLabel": "异常检测", + "xpack.ml.anomalyDetectionTabLabel": "异常检测", "xpack.ml.anomalyExplorerPageLabel": "Anomaly Explorer", "xpack.ml.anomalyResultsViewSelector.anomalyExplorerLabel": "在 Anomaly Explorer 中查看结果", "xpack.ml.anomalyResultsViewSelector.buttonGroupLegend": "异常结果视图选择器", @@ -10449,6 +11904,7 @@ "xpack.ml.calendarsList.deleteCalendars.deletingCalendarSuccessNotificationMessage": "已删除 {messageId}", "xpack.ml.calendarsList.deleteCalendarsModal.cancelButtonLabel": "取消", "xpack.ml.calendarsList.deleteCalendarsModal.deleteButtonLabel": "删除", + "xpack.ml.calendarsList.deleteCalendarsModal.deleteMultipleCalendarsTitle": "删除 {calendarsCount, plural, one {{calendarsList}} other {# 个日历}}?", "xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage": "加载日历列表时出错。", "xpack.ml.calendarsList.table.allJobsLabel": "应用到所有作业", "xpack.ml.calendarsList.table.deleteButtonLabel": "删除", @@ -10511,7 +11967,9 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTestingHelpText": "用于测试数据集的标准化混淆矩阵", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "多类混淆矩阵包含分析使用数据点的实际类正确分类数据点的次数以及分析使用其他类错误分类这些数据点的次数", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTrainingHelpText": "用于训练数据集的标准化混淆矩阵", - "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", + "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobStatusLabel": "作业状态", + "xpack.ml.dataframe.analytics.classificationExploration.evaluateSectionTitle": "模型评估", + "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {文档} other {文档}}已评估", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "显示操作", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "显示所有列", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分类作业 ID {jobId} 的目标索引", @@ -10553,6 +12011,7 @@ "xpack.ml.dataframe.analytics.create.calloutTitle": "分析字段不可用", "xpack.ml.dataframe.analytics.create.chooseSourceTitle": "选择源索引模式", "xpack.ml.dataframe.analytics.create.classificationHelpText": "分类用于预测数据集中的数据点的标签。", + "xpack.ml.dataframe.analytics.create.classificationTitle": "分类", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceFalseValue": "False", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceLabel": "计算功能影响", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceLabelHelpText": "指定是否启用功能影响计算。默认为 true。", @@ -10598,6 +12057,7 @@ "xpack.ml.dataframe.analytics.create.destinationIndexInputAriaLabel": "选择唯一目标索引名称。", "xpack.ml.dataframe.analytics.create.destinationIndexInvalidError": "目标索引名称无效。", "xpack.ml.dataframe.analytics.create.destinationIndexLabel": "目标 IP", + "xpack.ml.dataframe.analytics.create.DestIndexSameAsIdLabel": "目标索引与作业 ID 相同", "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "编辑", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "创建 Kibana 索引模式时发生错误:", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "索引模式 {indexPatternName} 已存在。", @@ -10633,6 +12093,7 @@ "xpack.ml.dataframe.analytics.create.jobIdInvalidMaxLengthErrorMessage": "作业 ID 的长度不得超过 {maxLength, plural, one {# 个字符} other {# 个字符}}。", "xpack.ml.dataframe.analytics.create.jobIdLabel": "作业 ID", "xpack.ml.dataframe.analytics.create.jobIdPlaceholder": "作业 ID", + "xpack.ml.dataframe.analytics.create.jsonEditorDisabledSwitchText": "配置包含表单不支持的高级字段。您无法切换回该表单。", "xpack.ml.dataframe.analytics.create.lambdaHelpText": "在训练数据集上防止过度拟合的正则化参数。必须为非负值。", "xpack.ml.dataframe.analytics.create.lambdaInputAriaLabel": "在训练数据集上防止过度拟合的正则化参数。", "xpack.ml.dataframe.analytics.create.lambdaLabel": "Lambda", @@ -10649,7 +12110,7 @@ "xpack.ml.dataframe.analytics.create.modelMemoryLimitHelpText": "允许用于分析处理的近似最大内存资源量。", "xpack.ml.dataframe.analytics.create.modelMemoryLimitLabel": "模型内存限制", "xpack.ml.dataframe.analytics.create.modelMemoryUnitsInvalidError": "无法识别模型内存限制数据单元。必须为 {str}", - "xpack.ml.dataframe.analytics.create.modelMemoryUnitsMinError": "模型内存限制不能低于 {mml}", + "xpack.ml.dataframe.analytics.create.modelMemoryUnitsMinError": "模型内存限值小于估计值 {mml}", "xpack.ml.dataframe.analytics.create.newAnalyticsTitle": "新建分析作业", "xpack.ml.dataframe.analytics.create.nNeighborsHelpText": "每个离群值检测方法用于计算其离群值分数的近邻数目值。未设置时,不同的值将用于不同组合成员。必须为正整数", "xpack.ml.dataframe.analytics.create.nNeighborsInputAriaLabel": "每个离群值检测方法用于计算其离群值分数的近邻数目值。", @@ -10662,6 +12123,7 @@ "xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesInputAriaLabel": "每文档功能重要性值最大数目。", "xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesLabel": "功能重要性值", "xpack.ml.dataframe.analytics.create.outlierDetectionHelpText": "异常值检测用于识别数据集中的异常数据点。", + "xpack.ml.dataframe.analytics.create.outlierDetectionTitle": "离群值检测", "xpack.ml.dataframe.analytics.create.outlierFractionHelpText": "设置在离群值检测之前被假设为离群的数据集比例。", "xpack.ml.dataframe.analytics.create.outlierFractionInputAriaLabel": "设置在离群值检测之前被假设为离群的数据集比例。", "xpack.ml.dataframe.analytics.create.outlierFractionLabel": "离群值比例", @@ -10670,8 +12132,11 @@ "xpack.ml.dataframe.analytics.create.randomizeSeedInputAriaLabel": "用于选取哪个文档用于训练的随机生成器种子", "xpack.ml.dataframe.analytics.create.randomizeSeedLabel": "随机种子", "xpack.ml.dataframe.analytics.create.randomizeSeedText": "用于选取哪个文档用于训练的随机生成器种子。", + "xpack.ml.dataframe.analytics.create.regressionHelpText": "回归用于预测数据集中的数值。", + "xpack.ml.dataframe.analytics.create.regressionTitle": "回归", "xpack.ml.dataframe.analytics.create.requiredFieldsError": "无效。{message}", "xpack.ml.dataframe.analytics.create.resultsFieldHelpText": "定义用于存储分析结果的字段的名称。默认为 ml。", + "xpack.ml.dataframe.analytics.create.resultsFieldInputAriaLabel": "用于存储分析结果的字段的名称。", "xpack.ml.dataframe.analytics.create.resultsFieldLabel": "结果字段", "xpack.ml.dataframe.analytics.create.savedSearchLabel": "已保存搜索", "xpack.ml.dataFrame.analytics.create.searchSelection.notFoundLabel": "未找到匹配的索引或已保存搜索。", @@ -10693,7 +12158,11 @@ "xpack.ml.dataframe.analytics.create.switchToJsonEditorSwitch": "切换到 json 编辑器", "xpack.ml.dataframe.analytics.create.trainingPercentHelpText": "定义用于训练的合格文档的百分比。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "训练百分比", + "xpack.ml.dataframe.analytics.create.unableToFetchExplainDataMessage": "提取分析字段数据时发生错误。", "xpack.ml.dataframe.analytics.create.unsupportedFieldsError": "无效。{message}", + "xpack.ml.dataframe.analytics.create.UseResultsFieldDefaultLabel": "使用结果字段默认值“{defaultValue}”", + "xpack.ml.dataframe.analytics.create.viewResultsCardDescription": "查看分析作业的结果。", + "xpack.ml.dataframe.analytics.create.viewResultsCardTitle": "查看结果", "xpack.ml.dataframe.analytics.create.wizardCreateButton": "创建", "xpack.ml.dataframe.analytics.create.wizardStartCheckbox": "立即启动", "xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage": "除了因变量之外,还必须在分析中至少包括一个字段。", @@ -10704,6 +12173,8 @@ "xpack.ml.dataframe.analytics.creation.detailsStepTitle": "作业详情", "xpack.ml.dataframe.analytics.creationPageSourceIndexTitle": "源索引模式:{indexTitle}", "xpack.ml.dataframe.analytics.creationPageTitle": "创建作业", + "xpack.ml.dataframe.analytics.decisionPathFeatureBaselineTitle": "基线", + "xpack.ml.dataframe.analytics.decisionPathFeatureOtherTitle": "其他", "xpack.ml.dataframe.analytics.errorCallout.evaluateErrorTitle": "加载数据时出错。", "xpack.ml.dataframe.analytics.errorCallout.generalErrorTitle": "加载数据时出错。", "xpack.ml.dataframe.analytics.errorCallout.noDataCalloutBody": "该索引的查询未返回结果。请确保作业已完成且索引包含文档。", @@ -10711,13 +12182,43 @@ "xpack.ml.dataframe.analytics.errorCallout.noIndexCalloutBody": "该索引的查询未返回结果。请确保目标索引存在且包含文档。", "xpack.ml.dataframe.analytics.errorCallout.queryParsingErrorBody": "查询语法无效,未返回任何结果。请检查查询语法并重试。", "xpack.ml.dataframe.analytics.errorCallout.queryParsingErrorTitle": "无法解析查询。", + "xpack.ml.dataframe.analytics.exploration.analysisDestinationIndexLabel": "目标索引", + "xpack.ml.dataframe.analytics.exploration.analysisSectionTitle": "分析", + "xpack.ml.dataframe.analytics.exploration.analysisSourceIndexLabel": "源索引", + "xpack.ml.dataframe.analytics.exploration.analysisTypeLabel": "类型", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "功能影响分数", + "xpack.ml.dataframe.analytics.exploration.explorationTableTitle": "结果", + "xpack.ml.dataframe.analytics.exploration.explorationTableTotalDocsLabel": "文档总数", + "xpack.ml.dataframe.analytics.exploration.featureImportanceDocsLink": "特征重要性文档", + "xpack.ml.dataframe.analytics.exploration.featureImportanceSummaryTitle": "总特征重要性", + "xpack.ml.dataframe.analytics.exploration.featureImportanceSummaryTooltipContent": "总特征重要性值指示字段对所有训练数据的预测有多大影响。", + "xpack.ml.dataframe.analytics.exploration.featureImportanceXAxisTitle": "特征重要性平均级别", + "xpack.ml.dataframe.analytics.exploration.featureImportanceYSeriesName": "级别", + "xpack.ml.dataframe.analytics.exploration.noTotalFeatureImportanceCalloutMessage": "总特征重要性数据不可用;数据集是统一的,特征对预测没有重大影响。", + "xpack.ml.dataframe.analytics.exploration.querySyntaxError": "加载索引数据时发生错误。请确保您的查询语法有效。", + "xpack.ml.dataframe.analytics.explorationQueryBar.buttonGroupLegend": "分析查询栏筛选按钮", + "xpack.ml.dataframe.analytics.explorationResults.baselineErrorMessageToast": "获取特征重要性基线时发生错误", + "xpack.ml.dataframe.analytics.explorationResults.classificationDecisionPathClassNameTitle": "类名称", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathBaselineText": "基线(训练数据集中所有数据点的预测平均值)", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathJSONTab": "JSON", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathLineTitle": "预测", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotHelpText": "SHAP 决策图使用 {linkedFeatureImportanceValues} 说明模型如何达到“{predictionFieldName}”的预测值。", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotTab": "决策图", + "xpack.ml.dataframe.analytics.explorationResults.decisionPathXAxisTitle": "“{predictionFieldName}”的预测", "xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText": "正在显示有相关预测存在的文档", "xpack.ml.dataframe.analytics.explorationResults.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", + "xpack.ml.dataframe.analytics.explorationResults.linkedFeatureImportanceValues": "特征重要性值", + "xpack.ml.dataframe.analytics.explorationResults.missingBaselineCallout": "无法计算基线值,这可能会导致决策路径偏移。", + "xpack.ml.dataframe.analytics.explorationResults.regressionDecisionPathDataMissingCallout": "无可用决策路径数据。", + "xpack.ml.dataframe.analytics.explorationResults.testingSubsetLabel": "测试", + "xpack.ml.dataframe.analytics.explorationResults.trainingSubsetLabel": "训练", "xpack.ml.dataframe.analytics.indexPatternPromptLinkText": "创建索引模式", "xpack.ml.dataframe.analytics.indexPatternPromptMessage": "不存在索引 {destIndex} 的索引模式。{destIndex} 的{linkToIndexPatternManagement}。", "xpack.ml.dataframe.analytics.jobCaps.errorTitle": "无法提取结果。加载索引的字段数据时发生错误。", "xpack.ml.dataframe.analytics.jobConfig.errorTitle": "无法提取结果。加载作业配置数据时发生错误。", + "xpack.ml.dataframe.analytics.regressionExploration.evaluateNoTestingDocsError": "找不到测试文档", + "xpack.ml.dataframe.analytics.regressionExploration.evaluateNoTrainingDocsError": "找不到训练文档", + "xpack.ml.dataframe.analytics.regressionExploration.evaluateSectionTitle": "模型评估", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差", "xpack.ml.dataframe.analytics.regressionExploration.generalizationFilterText": ".筛留训练数据。", @@ -10735,23 +12236,28 @@ "xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle": "训练误差", "xpack.ml.dataframe.analytics.regressionExploration.trainingFilterText": ".筛留测试数据。", "xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsMessagesLabel": "作业消息", + "xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsStatsLabel": "作业统计信息", + "xpack.ml.dataframe.analyticsList.cloneActionNameText": "克隆", "xpack.ml.dataframe.analyticsList.cloneActionPermissionTooltip": "您无权克隆分析作业。", "xpack.ml.dataframe.analyticsList.completeBatchAnalyticsToolTip": "{analyticsId} 为已完成的分析作业,无法重新启动。", "xpack.ml.dataframe.analyticsList.createDataFrameAnalyticsButton": "创建作业", "xpack.ml.dataframe.analyticsList.deleteActionDisabledToolTipContent": "停止数据帧分析作业,以便将其删除。", + "xpack.ml.dataframe.analyticsList.deleteActionNameText": "删除", "xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage": "删除数据帧分析作业 {analyticsId} 时发生错误", "xpack.ml.dataframe.analyticsList.deleteAnalyticsPrivilegeErrorMessage": "用户无权删除索引 {indexName}:{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage": "删除的数据帧分析作业 {analyticsId} 的请求已确认。", + "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexErrorMessage": "删除目标索引 {destinationIndex} 时发生错误", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternErrorMessage": "删除索引模式 {destinationIndex} 时发生错误:{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternSuccessMessage": "删除索引模式 {destinationIndex} 的请求已确认。", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage": "删除目标索引 {destinationIndex} 的请求已确认。", "xpack.ml.dataframe.analyticsList.deleteDestinationIndexTitle": "删除目标索引 {indexName}", "xpack.ml.dataframe.analyticsList.deleteModalCancelButton": "取消", "xpack.ml.dataframe.analyticsList.deleteModalDeleteButton": "删除", - "xpack.ml.dataframe.analyticsList.deleteModalTitle": "删除 {analyticsId}", + "xpack.ml.dataframe.analyticsList.deleteModalTitle": "删除 {analyticsId}?", "xpack.ml.dataframe.analyticsList.deleteTargetIndexPatternTitle": "删除索引模式 {indexPattern}", "xpack.ml.dataframe.analyticsList.description": "描述", "xpack.ml.dataframe.analyticsList.destinationIndex": "目标 IP", + "xpack.ml.dataframe.analyticsList.editActionNameText": "编辑", "xpack.ml.dataframe.analyticsList.editActionPermissionTooltip": "您无权编辑分析作业。", "xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartAriaLabel": "更新允许惰性启动。", "xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartFalseValue": "False", @@ -10771,11 +12277,12 @@ "xpack.ml.dataframe.analyticsList.editFlyoutSuccessMessage": "分析作业 {jobId} 已更新。", "xpack.ml.dataframe.analyticsList.editFlyoutTitle": "编辑 {jobId}", "xpack.ml.dataframe.analyticsList.editFlyoutUpdateButtonText": "更新", - "xpack.ml.dataFrame.analyticsList.emptyPromptButtonText": "创建您的首个数据帧分析作业", - "xpack.ml.dataFrame.analyticsList.emptyPromptTitle": "未找到任何数据帧分析作业", + "xpack.ml.dataFrame.analyticsList.emptyPromptButtonText": "创建作业", + "xpack.ml.dataFrame.analyticsList.emptyPromptTitle": "创建您的首个数据帧分析作业", "xpack.ml.dataFrame.analyticsList.errorPromptTitle": "获取数据帧分析列表时发生错误。", "xpack.ml.dataframe.analyticsList.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage": "检查索引模式 {indexPattern} 是否存在时发生错误:{error}", "xpack.ml.dataframe.analyticsList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage": "检查用户是否能够删除 {destinationIndex} 时发生错误:{error}", + "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.analysisStats": "分析统计信息", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.phase": "阶段", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.progress": "进度", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state": "状态", @@ -10784,6 +12291,12 @@ "xpack.ml.dataframe.analyticsList.experimentalBadgeLabel": "实验性", "xpack.ml.dataframe.analyticsList.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。", "xpack.ml.dataframe.analyticsList.fetchSourceIndexPatternForCloneErrorMessage": "检查索引模式 {indexPattern} 是否存在时发生错误:{error}", + "xpack.ml.dataframe.analyticsList.forceStopModalBody": "{analyticsId} 处于失败状态。您必须停止该作业并修复失败问题。", + "xpack.ml.dataframe.analyticsList.forceStopModalCancelButton": "取消", + "xpack.ml.dataframe.analyticsList.forceStopModalStartButton": "强制停止", + "xpack.ml.dataframe.analyticsList.forceStopModalTitle": "强制停止此作业?", + "xpack.ml.dataframe.analyticsList.memoryStatus": "内存状态", + "xpack.ml.dataframe.analyticsList.noSourceIndexPatternForClone": "无法克隆分析作业。对于索引 {indexPattern},不存在索引模式。", "xpack.ml.dataframe.analyticsList.progress": "进度", "xpack.ml.dataframe.analyticsList.progressOfPhase": "阶段 {currentPhase} 的进度:{progress}%", "xpack.ml.dataframe.analyticsList.refreshButtonLabel": "刷新", @@ -10791,18 +12304,20 @@ "xpack.ml.dataframe.analyticsList.rowExpand": "显示 {analyticsId} 的详情", "xpack.ml.dataframe.analyticsList.showDetailsColumn.screenReaderDescription": "此列包含可单击控件,用于显示每个作业的更多详情", "xpack.ml.dataframe.analyticsList.sourceIndex": "源索引", + "xpack.ml.dataframe.analyticsList.startActionNameText": "启动", "xpack.ml.dataframe.analyticsList.startAnalyticsErrorTitle": "启动作业时出错", "xpack.ml.dataframe.analyticsList.startAnalyticsSuccessMessage": "数据帧分析 {analyticsId} 启动请求已确认。", - "xpack.ml.dataframe.analyticsList.startModalBody": "数据帧分析作业将增加集群的搜索和索引负荷。如果负荷超载,请停止分析作业。是否确定要启动此分析作业?", + "xpack.ml.dataframe.analyticsList.startModalBody": "数据帧分析作业会增加集群中的搜索和索引负载。如果超负荷,请停止该作业。", "xpack.ml.dataframe.analyticsList.startModalCancelButton": "取消", "xpack.ml.dataframe.analyticsList.startModalStartButton": "开始", - "xpack.ml.dataframe.analyticsList.startModalTitle": "启动 {analyticsId}", + "xpack.ml.dataframe.analyticsList.startModalTitle": "启动 {analyticsId}?", "xpack.ml.dataframe.analyticsList.status": "状态", "xpack.ml.dataframe.analyticsList.statusFilter": "状态", + "xpack.ml.dataframe.analyticsList.stopActionNameText": "停止", "xpack.ml.dataframe.analyticsList.stopAnalyticsErrorMessage": "停止数据帧分析 {analyticsId} 时发生错误:{error}", "xpack.ml.dataframe.analyticsList.stopAnalyticsSuccessMessage": "数据帧分析 {analyticsId} 停止请求已确认。", "xpack.ml.dataframe.analyticsList.tableActionLabel": "操作", - "xpack.ml.dataframe.analyticsList.title": "数据帧分析作业", + "xpack.ml.dataframe.analyticsList.title": "数据帧分析", "xpack.ml.dataframe.analyticsList.type": "类型", "xpack.ml.dataframe.analyticsList.typeFilter": "类型", "xpack.ml.dataframe.analyticsList.viewActionJobFailedToolTipContent": "数据帧分析作业失败。没有可用的结果页面。", @@ -10810,13 +12325,17 @@ "xpack.ml.dataframe.analyticsList.viewActionJobNotStartedToolTipContent": "数据帧分析作业尚未启动。没有可用的结果页面。", "xpack.ml.dataframe.analyticsList.viewActionName": "查看", "xpack.ml.dataframe.analyticsList.viewActionUnknownJobTypeToolTipContent": "没有可用于此类型数据帧分析作业的结果页面。", + "xpack.ml.dataframe.jobsTabLabel": "作业", + "xpack.ml.dataframe.modelsTabLabel": "模型", "xpack.ml.dataframe.stepCreateForm.createDataFrameAnalyticsSuccessMessage": "数据帧分析 {jobId} 创建请求已确认。", "xpack.ml.dataframe.stepDetailsForm.destinationIndexInvalidErrorLink": "详细了解索引名称限制。", "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel": "探查", "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel": "作业管理", "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel": "数据帧分析", "xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel": "索引", + "xpack.ml.dataFrameAnalyticsBreadcrumbs.modelsListLabel": "模型管理", "xpack.ml.dataFrameAnalyticsLabel": "数据帧分析", + "xpack.ml.dataFrameAnalyticsTabLabel": "数据帧分析", "xpack.ml.dataGrid.columnChart.ErrorMessageToast": "提取直方图数据时发生错误:{error}", "xpack.ml.dataGrid.dataGridNoDataCalloutTitle": "索引预览不可用", "xpack.ml.dataGrid.histogramButtonText": "直方图", @@ -10870,6 +12389,7 @@ "xpack.ml.datavisualizer.startTrial.subscriptionsLinkText": "白金级或企业级订阅", "xpack.ml.datavisualizerBreadcrumbLabel": "数据可视化工具", "xpack.ml.dataVisualizerPageLabel": "数据可视化工具", + "xpack.ml.dataVisualizerTabLabel": "数据可视化工具", "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage": "无法加载消息", "xpack.ml.editModelSnapshotFlyout.calloutText": "这是作业 {jobId} 当前正在使用的快照,因此无法删除。", "xpack.ml.editModelSnapshotFlyout.calloutTitle": "当前快照", @@ -10903,6 +12423,7 @@ "xpack.ml.explorer.charts.openInSingleMetricViewerButtonLabel": "在 Single Metric Viewer 中打开", "xpack.ml.explorer.charts.tooManyBucketsDescription": "此选项包含太多要显示的时段。最好设置一个较短的时间范围来查看仪表板。", "xpack.ml.explorer.charts.viewLabel": "查看", + "xpack.ml.explorer.clearSelectionLabel": "清除所选内容", "xpack.ml.explorer.createNewJobLinkText": "创建作业", "xpack.ml.explorer.dashboardsTable.addAndEditDashboardLabel": "添加并编辑仪表板", "xpack.ml.explorer.dashboardsTable.addToDashboardLabel": "添加到仪表板", @@ -10919,6 +12440,7 @@ "xpack.ml.explorer.intervalLabel": "时间间隔", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable": "查询栏中的语法无效。输入必须是有效的 Kibana 查询语言 (KQL)", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageQueryBar": "无效查询", + "xpack.ml.explorer.invalidTimeRangeInUrlCallout": "由于默认时间筛选无效,时间筛选已更改为完整范围。检查 {field} 的高级设置。", "xpack.ml.explorer.jobIdLabel": "作业 ID", "xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(所有影响因素的作业分数)", "xpack.ml.explorer.kueryBar.filterPlaceholder": "按影响因素字段筛选……({queryExample})", @@ -10941,6 +12463,7 @@ "xpack.ml.explorer.singleMetricChart.valueWithoutAnomalyScoreLabel": "值", "xpack.ml.explorer.sortedByMaxAnomalyScoreForTimeFormattedLabel": "(按 {viewByLoadedForTimeFormatted} 的异常分数最大值排序)", "xpack.ml.explorer.sortedByMaxAnomalyScoreLabel": "(按异常分数最大值排序)", + "xpack.ml.explorer.stoppedPartitionsExistCallout": "由于 stop_on_warn 处于打开状态,结果可能比原本有的结果少。对于{jobsWithStoppedPartitions, plural, one {作业} other {作业}}中分类状态已更改为警告的某些分区 [{stoppedPartitions}],分类和后续异常检测已停止。", "xpack.ml.explorer.swimlane.maxAnomalyScoreLabel": "最大异常分数", "xpack.ml.explorer.swimlaneActions": "操作", "xpack.ml.explorer.swimLanePagination": "异常泳道分页", @@ -11001,6 +12524,7 @@ "xpack.ml.fieldTypeIcon.unknownTypeAriaLabel": "未知类型", "xpack.ml.fileDatavisualizer.aboutPanel.analyzingDataTitle": "正在分析数据", "xpack.ml.fileDatavisualizer.aboutPanel.selectOrDragAndDropFileDescription": "选择或拖放文件", + "xpack.ml.fileDatavisualizer.addCombinedFieldsLabel": "添加组合字段", "xpack.ml.fileDatavisualizer.advancedImportSettings.createIndexPatternLabel": "创建索引模式", "xpack.ml.fileDatavisualizer.advancedImportSettings.indexNameAriaLabel": "索引名称,必填字段", "xpack.ml.fileDatavisualizer.advancedImportSettings.indexNameLabel": "索引名称", @@ -11022,6 +12546,11 @@ "xpack.ml.fileDatavisualizer.bottomBar.missingImportPrivilegesMessage": "您需要具有 ingest_admin 角色才能启用数据导入", "xpack.ml.fileDatavisualizer.bottomBar.readMode.cancelButtonLabel": "取消", "xpack.ml.fileDatavisualizer.bottomBar.readMode.importButtonLabel": "导入", + "xpack.ml.fileDatavisualizer.combinedFieldsForm.mappingsParseError": "解析映射时出错:{error}", + "xpack.ml.fileDatavisualizer.combinedFieldsForm.pipelineParseError": "解析管道时出错:{error}", + "xpack.ml.fileDatavisualizer.combinedFieldsLabel": "组合字段", + "xpack.ml.fileDatavisualizer.combinedFieldsReadOnlyHelpTextLabel": "在高级选项卡中编辑组合字段", + "xpack.ml.fileDatavisualizer.combinedFieldsReadOnlyLabel": "组合字段", "xpack.ml.fileDatavisualizer.editFlyout.applyOverrideSettingsButtonLabel": "应用", "xpack.ml.fileDatavisualizer.editFlyout.closeOverrideSettingsButtonLabel": "关闭", "xpack.ml.fileDatavisualizer.editFlyout.overrides.customDelimiterFormRowLabel": "定制分隔符", @@ -11061,11 +12590,19 @@ "xpack.ml.fileDatavisualizer.fileContents.fileContentsTitle": "文件内容", "xpack.ml.fileDatavisualizer.fileContents.firstLinesDescription": "前 {numberOfLines, plural, zero {# 行} one {# 行} other {# 行}}", "xpack.ml.fileDatavisualizer.fileDatavisualizerView.xmlNotCurrentlySupportedErrorMessage": "当前不支持 XML", - "xpack.ml.fileDatavisualizer.fileErrorCallouts.fileCouldNotBeReadTitle": "无法读取文件", + "xpack.ml.fileDatavisualizer.fileErrorCallouts.applyOverridesDescription": "如果您对此数据有所了解,例如文件格式或时间戳格式,则添加初始覆盖可以帮助我们推理结构的其余部分。", + "xpack.ml.fileDatavisualizer.fileErrorCallouts.fileCouldNotBeReadTitle": "无法确定文件结构", "xpack.ml.fileDatavisualizer.fileErrorCallouts.fileSizeExceedsAllowedSizeByDiffFormatErrorMessage": "您选择用于上传的文件大小超过上限值 {maxFileSizeFormatted} 的 {diffFormatted}", "xpack.ml.fileDatavisualizer.fileErrorCallouts.fileSizeExceedsAllowedSizeErrorMessage": "您选择用于上传的文件大小为 {fileSizeFormatted},超过上限值 {maxFileSizeFormatted}", "xpack.ml.fileDatavisualizer.fileErrorCallouts.fileSizeTooLargeTitle": "文件太大", + "xpack.ml.fileDatavisualizer.fileErrorCallouts.overrideButton": "应用覆盖设置", "xpack.ml.fileDatavisualizer.fileErrorCallouts.revertingToPreviousSettingsDescription": "恢复到以前的设置", + "xpack.ml.fileDatavisualizer.geoPointCombinedFieldLabel": "添加地理点字段", + "xpack.ml.fileDatavisualizer.geoPointForm.geoPointFieldAriaLabel": "地理点字段,必填字段", + "xpack.ml.fileDatavisualizer.geoPointForm.geoPointFieldLabel": "地理点字段", + "xpack.ml.fileDatavisualizer.geoPointForm.latFieldLabel": "纬度字段", + "xpack.ml.fileDatavisualizer.geoPointForm.lonFieldLabel": "经度字段", + "xpack.ml.fileDatavisualizer.geoPointForm.submitButtonLabel": "添加", "xpack.ml.fileDatavisualizer.importErrors.checkingPermissionErrorMessage": "导入权限错误", "xpack.ml.fileDatavisualizer.importErrors.creatingIndexErrorMessage": "创建索引时出错", "xpack.ml.fileDatavisualizer.importErrors.creatingIndexPatternErrorMessage": "创建索引模式时出错", @@ -11120,6 +12657,8 @@ "xpack.ml.fileDatavisualizer.importView.parsePipelineError": "解析采集管道时出错:", "xpack.ml.fileDatavisualizer.importView.parseSettingsError": "解析设置时出错:", "xpack.ml.fileDatavisualizer.importView.resetButtonLabel": "重置", + "xpack.ml.fileDatavisualizer.nameCollisionMsg": "“{name}”已存在,请提供唯一名称", + "xpack.ml.fileDatavisualizer.removeCombinedFieldsLabel": "移除组合字段", "xpack.ml.fileDatavisualizer.resultsLinks.createNewMLJobTitle": "新建 ML 作业", "xpack.ml.fileDatavisualizer.resultsLinks.fileBeatConfig": "创建 Filebeat 配置", "xpack.ml.fileDatavisualizer.resultsLinks.fileBeatConfigBottomText": "其中 {password} 是 {user} 用户的密码,{esUrl} 是 Elasticsearch 的 URL。", @@ -11147,6 +12686,8 @@ "xpack.ml.fileDatavisualizer.welcomeContent.uploadedFilesAllowedSizeDescription": "您可以上传不超过 {maxFileSize} 的文件。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileDescription": "File Data Visualizer 可帮助您理解日志文件中的字段和指标。上传文件、分析文件数据,然后选择是否将数据导入 Elasticsearch 索引。", "xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileTitle": "可视化来自日志文件的数据 {experimentalBadge}", + "xpack.ml.fileDataVisualizerDescription": "导入您自己的 CSV、NDJSON 或日志文件。", + "xpack.ml.fileDataVisualizerTitle": "上传文件", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "实际上与典型模式相同", "xpack.ml.formatters.metricChangeDescription.moreThan100xHigherDescription": "高 100 多倍", "xpack.ml.formatters.metricChangeDescription.moreThan100xLowerDescription": "低 100 多倍", @@ -11240,8 +12781,8 @@ "xpack.ml.jobsList.deleteJobModal.cancelButtonLabel": "取消", "xpack.ml.jobsList.deleteJobModal.closeButtonLabel": "关闭", "xpack.ml.jobsList.deleteJobModal.deleteButtonLabel": "删除", - "xpack.ml.jobsList.deleteJobModal.deleteJobsTitle": "删除 {jobsCount, plural, one {{jobId}} other {# 个作业}}", - "xpack.ml.jobsList.deleteJobModal.deleteMultipleJobsDescription": "删除{jobsCount, plural, one {一个作业} other {多个作业}}会非常耗时。将在后台删除{jobsCount, plural, one {该作业} other {这些作业}},但删除的作业可能不会立即从作业列表中消失", + "xpack.ml.jobsList.deleteJobModal.deleteJobsTitle": "删除 {jobsCount, plural, one {{jobId}} other {# 个作业}}?", + "xpack.ml.jobsList.deleteJobModal.deleteMultipleJobsDescription": "删除{jobsCount, plural, one {一个作业} other {多个作业}}可能很费时。将在后台删除{jobsCount, plural, one {该作业} other {这些作业}},但删除的作业可能不会从作业列表中立即消失。", "xpack.ml.jobsList.deleteJobModal.deletingJobsStatusLabel": "正在删除作业", "xpack.ml.jobsList.descriptionLabel": "描述", "xpack.ml.jobsList.editJobFlyout.changesNotSavedNotificationMessage": "无法保存对 {jobId} 所做的更改", @@ -11342,6 +12883,7 @@ "xpack.ml.jobsList.managementActions.deleteJobLabel": "删除作业", "xpack.ml.jobsList.managementActions.editJobDescription": "编辑作业", "xpack.ml.jobsList.managementActions.editJobLabel": "编辑作业", + "xpack.ml.jobsList.managementActions.noSourceIndexPatternForClone": "无法克隆异常检测作业 {jobId}。对于索引 {indexPatternTitle},不存在索引模式。", "xpack.ml.jobsList.managementActions.startDatafeedDescription": "开始数据馈送", "xpack.ml.jobsList.managementActions.startDatafeedLabel": "开始数据馈送", "xpack.ml.jobsList.managementActions.stopDatafeedDescription": "停止数据馈送", @@ -11403,6 +12945,7 @@ "xpack.ml.jobsList.title": "异常检测作业", "xpack.ml.machineLearningBreadcrumbLabel": "机器学习", "xpack.ml.machineLearningDescription": "对时序数据的正常行为自动建模以检测异常。", + "xpack.ml.machineLearningSubtitle": "建模、预测和检测。", "xpack.ml.machineLearningTitle": "Machine Learning", "xpack.ml.management.jobsList.accessDeniedTitle": "访问被拒绝", "xpack.ml.management.jobsList.analyticsDocsLabel": "分析作业文档", @@ -11452,6 +12995,8 @@ "xpack.ml.models.jobValidation.messages.cardinalityPartitionFieldMessage": "{fieldName} 的基数大于 1000,可能会导致高内存用量。", "xpack.ml.models.jobValidation.messages.categorizationFiltersInvalidMessage": "分类筛选配置无效。确保筛选是有效的正则表达式,且已设置 {categorizationFieldName}。", "xpack.ml.models.jobValidation.messages.categorizationFiltersValidMessage": "分类筛选检查已通过。", + "xpack.ml.models.jobValidation.messages.categorizerMissingPerPartitionFieldMessage": "在启用按分区分类时,必须为引用“mlcategory”的检测器设置分区字段。", + "xpack.ml.models.jobValidation.messages.categorizerVaryingPerPartitionFieldNamesMessage": "在启用按分区分类时,关键字为“mlcategory”的检测器不能具有不同的 partition_field_name。找到了 [{fields}]。", "xpack.ml.models.jobValidation.messages.detectorsDuplicatesMessage": "找到重复的检测工具。在同一作业中,不允许存在具有 “{functionParam}”、“{fieldNameParam}”、“{byFieldNameParam}”、“{overFieldNameParam}” 和 “{partitionFieldNameParam}” 相同组合配置的检测工具。", "xpack.ml.models.jobValidation.messages.detectorsEmptyMessage": "未找到任何检测工具。必须至少指定一个检测工具。", "xpack.ml.models.jobValidation.messages.detectorsFunctionEmptyMessage": "检测工具函数之一为空。", @@ -11525,6 +13070,7 @@ "xpack.ml.navMenu.anomalyDetectionTabLinkText": "异常检测", "xpack.ml.navMenu.dataFrameAnalyticsTabLinkText": "分析", "xpack.ml.navMenu.dataVisualizerTabLinkText": "数据可视化工具", + "xpack.ml.navMenu.mlAppNameText": "Machine Learning", "xpack.ml.navMenu.overviewTabLinkText": "概览", "xpack.ml.navMenu.settingsTabLinkText": "设置", "xpack.ml.newJob.page.createJob": "创建作业", @@ -11607,6 +13153,11 @@ "xpack.ml.newJob.wizard.datafeedStep.query.title": "Elasticsearch 查询", "xpack.ml.newJob.wizard.datafeedStep.queryDelay.description": "当前时间和最新输入数据时间之间的时间延迟(秒)。", "xpack.ml.newJob.wizard.datafeedStep.queryDelay.title": "查询延迟", + "xpack.ml.newJob.wizard.datafeedStep.resetQueryButton": "将数据馈送查询重置为默认值", + "xpack.ml.newJob.wizard.datafeedStep.resetQueryConfirm.cancel": "取消", + "xpack.ml.newJob.wizard.datafeedStep.resetQueryConfirm.confirm": "确认", + "xpack.ml.newJob.wizard.datafeedStep.resetQueryConfirm.description": "将数据馈送查询设置为默认值。", + "xpack.ml.newJob.wizard.datafeedStep.resetQueryConfirm.title": "重置数据馈送查询", "xpack.ml.newJob.wizard.datafeedStep.scrollSize.description": "为搜索请求的最大文档数目。", "xpack.ml.newJob.wizard.datafeedStep.scrollSize.title": "滚动条大小", "xpack.ml.newJob.wizard.datafeedStep.timeField.description": "索引模式的默认时间字段将被自动选择,但可以覆盖。", @@ -11614,6 +13165,9 @@ "xpack.ml.newJob.wizard.editCategorizationAnalyzerFlyoutButton": "编辑归类分析器", "xpack.ml.newJob.wizard.editJsonButton": "编辑 JSON", "xpack.ml.newJob.wizard.estimateModelMemoryError": "无法计算模型内存限制", + "xpack.ml.newJob.wizard.extraStep.categorizationJob.categorizationPerPartitionFieldLabel": "分区字段", + "xpack.ml.newJob.wizard.extraStep.categorizationJob.perPartitionCategorizationLabel": "启用按分区分类", + "xpack.ml.newJob.wizard.extraStep.categorizationJob.stopOnWarnLabel": "显示警告时停止", "xpack.ml.newJob.wizard.jobCreatorTitle.advanced": "高级", "xpack.ml.newJob.wizard.jobCreatorTitle.categorization": "归类", "xpack.ml.newJob.wizard.jobCreatorTitle.multiMetric": "多指标", @@ -11680,9 +13234,15 @@ "xpack.ml.newJob.wizard.jobType.useWizardTitle": "使用向导", "xpack.ml.newJob.wizard.jsonFlyout.closeButton": "关闭", "xpack.ml.newJob.wizard.jsonFlyout.datafeed.title": "数据馈送配置 JSON", + "xpack.ml.newJob.wizard.jsonFlyout.indicesChange.calloutText": "在此处无法更改数据馈送正在使用的索引。如果您想选择其他索引模式或已保存的搜索,请重新开始创建作业,以便选择其他索引模式。", + "xpack.ml.newJob.wizard.jsonFlyout.indicesChange.calloutTitle": "索引已更改", "xpack.ml.newJob.wizard.jsonFlyout.job.title": "作业配置 JSON", "xpack.ml.newJob.wizard.jsonFlyout.saveButton": "保存", "xpack.ml.newJob.wizard.nextStepButton": "下一步", + "xpack.ml.newJob.wizard.perPartitionCategorization.enable.description": "如果启用按分区分类,则将独立确定分区字段的每个值的类别。", + "xpack.ml.newJob.wizard.perPartitionCategorization.enable.title": "启用按分区分类", + "xpack.ml.newJob.wizard.perPartitionCategorizationSwitchLabel": "启用按分区分类", + "xpack.ml.newJob.wizard.perPartitionCategorizationtopOnWarnSwitchLabel": "显示警告时停止", "xpack.ml.newJob.wizard.pickFieldsStep.addDetectorButton": "添加检测工具", "xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorList.deleteButton": "删除", "xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorList.editButton": "编辑", @@ -11721,6 +13281,7 @@ "xpack.ml.newJob.wizard.pickFieldsStep.categorizationFieldCalloutTitle.valid": "选定的类别字段有效", "xpack.ml.newJob.wizard.pickFieldsStep.categorizationFieldExamples.title": "示例", "xpack.ml.newJob.wizard.pickFieldsStep.categorizationFieldOptional.description": "可选,用于分析非结构化日志数据。建议使用文本数据类型。", + "xpack.ml.newJob.wizard.pickFieldsStep.categorizationStoppedPartitionsTitle": "已停止的分区", "xpack.ml.newJob.wizard.pickFieldsStep.categorizationTotalCategories": "类别总数:{totalCategories}", "xpack.ml.newJob.wizard.pickFieldsStep.detectorTitle.placeholder": "{title} 由 {field} 分割", "xpack.ml.newJob.wizard.pickFieldsStep.influencers.description": "选择对结果有影响的分类字段。您可能将异常“归咎”于谁/什么因素?建议 1-3 个影响因素。", @@ -11739,6 +13300,9 @@ "xpack.ml.newJob.wizard.pickFieldsStep.splitCards.dataSplitBy": "按 {field} 分割数据", "xpack.ml.newJob.wizard.pickFieldsStep.splitField.description": "选择用于分析分区依据的字段。此字段的每个值将独立进行建模。", "xpack.ml.newJob.wizard.pickFieldsStep.splitField.title": "分割字段", + "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsErrorCallout": "提取已停止分区的列表时发生错误。", + "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsExistCallout": "启用按分区分类和 stop_on_warn 设置。作业“{jobId}”中的某些分区不适合进行分类,已从进一步分类或异常检测分析中排除。", + "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsPreviewColumnName": "已停止的分区名称", "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.description": "可选,用于输入数据已预汇总时,例如 \\{docCountParam\\}。", "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.title": "汇总计数字段", "xpack.ml.newJob.wizard.previewJsonButton": "预览 JSON", @@ -11809,6 +13373,8 @@ "xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeError": "启动作业时出错", "xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeSuccess": "作业 {jobId} 已启动", "xpack.ml.newJob.wizard.summaryStep.resetJobButton": "重置作业", + "xpack.ml.newJob.wizard.summaryStep.startDatafeedCheckbox": "立即启动", + "xpack.ml.newJob.wizard.summaryStep.startDatafeedCheckboxHelpText": "如果未选择,则稍后可从作业列表启动作业。", "xpack.ml.newJob.wizard.summaryStep.timeRange.end.title": "结束", "xpack.ml.newJob.wizard.summaryStep.timeRange.start.title": "开始", "xpack.ml.newJob.wizard.summaryStep.trueLabel": "True", @@ -11817,6 +13383,8 @@ "xpack.ml.newJob.wizard.timeRangeStep.timeRangePicker.endDateLabel": "结束日期", "xpack.ml.newJob.wizard.timeRangeStep.timeRangePicker.startDateLabel": "开始日期", "xpack.ml.newJob.wizard.validateJob.bucketSpanMustBeSetErrorMessage": "必须设置存储桶跨度", + "xpack.ml.newJob.wizard.validateJob.categorizerMissingPerPartitionFieldMessage": "在启用按分区分类时,必须为引用“mlcategory”的检测器设置分区字段。", + "xpack.ml.newJob.wizard.validateJob.categorizerVaryingPerPartitionFieldNamesMessage": "在启用按分区分类时,关键字为“mlcategory”的检测器不能具有不同的 partition_field_name。", "xpack.ml.newJob.wizard.validateJob.duplicatedDetectorsErrorMessage": "找到重复的检测工具。", "xpack.ml.newJob.wizard.validateJob.frequencyInvalidTimeIntervalFormatErrorMessage": "{value} 不是有效地时间间隔格式,例如 {thirtySeconds}、{tenMinutes}、{oneHour}、{sevenDays}。还需要大于零。", "xpack.ml.newJob.wizard.validateJob.groupNameAlreadyExists": "组 ID 已存在。组 ID 不能与现有作业或组相同。", @@ -11829,6 +13397,8 @@ "xpack.ml.newJob.wizard.validateJob.modelMemoryLimitUnitsInvalidErrorMessage": "无法识别模型内存限制数据单元。必须为 {str}", "xpack.ml.newJob.wizard.validateJob.queryCannotBeEmpty": "数据馈送查询不能为空。", "xpack.ml.newJob.wizard.validateJob.queryIsInvalidEsQuery": "数据馈送查询必须是有效的 Elasticsearch 查询。", + "xpack.ml.overview.analytics.resultActions.openJobText": "查看作业结果", + "xpack.ml.overview.analytics.viewActionName": "查看", "xpack.ml.overview.analyticsList.createFirstJobMessage": "创建您的首个数据帧分析作业", "xpack.ml.overview.analyticsList.createJobButtonText": "创建作业", "xpack.ml.overview.analyticsList.emptyPromptText": "数据帧分析允许您对数据执行离群值检测、回归或分类分析并使用结果标注数据。该作业会将标注的数据以及源数据的副本置于新的索引中。", @@ -11858,10 +13428,12 @@ "xpack.ml.overview.anomalyDetection.tableMaxScoreErrorTooltip": "加载最大异常分数时出现问题", "xpack.ml.overview.anomalyDetection.tableMaxScoreTooltip": "最近 24 小时期间组中所有作业的最大分数", "xpack.ml.overview.anomalyDetection.tableNumJobs": "组中的作业", + "xpack.ml.overview.anomalyDetection.viewActionName": "查看", "xpack.ml.overview.feedbackSectionLink": "在线反馈", "xpack.ml.overview.feedbackSectionText": "如果您在体验方面有任何意见或建议,请提交{feedbackLink}。", "xpack.ml.overview.feedbackSectionTitle": "反馈", "xpack.ml.overview.gettingStartedSectionDocs": "文档", + "xpack.ml.overview.gettingStartedSectionText": "欢迎使用 Machine Learning。首先查看我们的{docs}或创建新作业。我们建议使用{transforms}创建分析作业的特征索引。", "xpack.ml.overview.gettingStartedSectionTitle": "入门", "xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearch 的转换", "xpack.ml.overview.overviewLabel": "概览", @@ -11874,6 +13446,7 @@ "xpack.ml.overviewJobsList.statsBar.failedJobsLabel": "失败的作业", "xpack.ml.overviewJobsList.statsBar.openJobsLabel": "打开的作业", "xpack.ml.overviewJobsList.statsBar.totalJobsLabel": "总计作业数", + "xpack.ml.overviewTabLabel": "概览", "xpack.ml.plugin.title": "Machine Learning", "xpack.ml.privilege.licenseHasExpiredTooltip": "您的许可证已过期。", "xpack.ml.privilege.noPermission.createCalendarsTooltip": "您没有权限创建日历。", @@ -11909,7 +13482,7 @@ "xpack.ml.ruleEditor.deleteRuleModal.cancelButtonLabel": "取消", "xpack.ml.ruleEditor.deleteRuleModal.deleteButtonLabel": "删除", "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleLinkText": "删除规则", - "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleTitle": "删除规则", + "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleTitle": "删除规则?", "xpack.ml.ruleEditor.detectorDescriptionList.detectorTitle": "检测工具", "xpack.ml.ruleEditor.detectorDescriptionList.jobIdTitle": "作业 ID", "xpack.ml.ruleEditor.detectorDescriptionList.selectedAnomalyDescription": "实际 {actual}典型 {typical}", @@ -11977,7 +13550,7 @@ "xpack.ml.settings.anomalyDetection.createCalendarLink": "创建", "xpack.ml.settings.anomalyDetection.createFilterListsLink": "创建", "xpack.ml.settings.anomalyDetection.filterListsSummaryCount": "您有 {filterListsCountBadge} 个{filterListsCount, plural, one {筛选列表} other {筛选列表}}", - "xpack.ml.settings.anomalyDetection.filterListsText": " 筛选列表包含可用于在 Machine Learning 分析中包括或排除事件的值。", + "xpack.ml.settings.anomalyDetection.filterListsText": "筛选列表包含可用于在 Machine Learning 分析中包括或排除事件的值。", "xpack.ml.settings.anomalyDetection.filterListsTitle": "筛选列表", "xpack.ml.settings.anomalyDetection.loadingCalendarsCountErrorMessage": "获取日历的计数时发生错误", "xpack.ml.settings.anomalyDetection.loadingFilterListCountErrorMessage": "获取筛选列表的计数时发生错误", @@ -12001,7 +13574,7 @@ "xpack.ml.settings.filterLists.deleteFilterListModal.cancelButtonLabel": "取消", "xpack.ml.settings.filterLists.deleteFilterListModal.confirmButtonLabel": "删除", "xpack.ml.settings.filterLists.deleteFilterListModal.deleteButtonLabel": "删除", - "xpack.ml.settings.filterLists.deleteFilterListModal.modalTitle": "删除 {selectedFilterListsLength, plural, one {{selectedFilterId}} other {# 个筛选列表}}", + "xpack.ml.settings.filterLists.deleteFilterListModal.modalTitle": "删除 {selectedFilterListsLength, plural, one {{selectedFilterId}} other {# 个筛选列表}}?", "xpack.ml.settings.filterLists.deleteFilterLists.deletingErrorMessage": "删除筛选列表 {filterListId} 时出错。{respMessage}", "xpack.ml.settings.filterLists.deleteFilterLists.deletingNotificationMessage": "正在删除 {filterListsToDeleteLength, plural, one {{filterListToDeleteId}} other {# 个筛选列表}}", "xpack.ml.settings.filterLists.deleteFilterLists.filtersSuccessfullyDeletedNotificationMessage": "已删除 {filterListsToDeleteLength, plural, one {{filterListToDeleteId}} other {# 个筛选列表}}", @@ -12039,6 +13612,7 @@ "xpack.ml.settings.filterLists.toolbar.deleteItemButtonLabel": "删除项", "xpack.ml.settings.title": "设置", "xpack.ml.settingsBreadcrumbLabel": "设置", + "xpack.ml.settingsTabLabel": "设置", "xpack.ml.singleMetricViewerPageLabel": "Single Metric Viewer", "xpack.ml.stepDefineForm.invalidQuery": "无效查询", "xpack.ml.stepDefineForm.queryPlaceholderKql": "例如,{example}", @@ -12113,6 +13687,7 @@ "xpack.ml.timeSeriesExplorer.forecastsList.viewColumnName": "查看", "xpack.ml.timeSeriesExplorer.forecastsList.viewForecastAriaLabel": "查看在 {createdDate} 创建的预测", "xpack.ml.timeSeriesExplorer.intervalLabel": "时间间隔", + "xpack.ml.timeSeriesExplorer.invalidTimeRangeInUrlCallout": "由于默认时间筛选无效,时间筛选已更改为此作业的完整范围。检查 {field} 的高级设置。", "xpack.ml.timeSeriesExplorer.loadingLabel": "正在加载", "xpack.ml.timeSeriesExplorer.noResultsFoundLabel": "找不到结果", "xpack.ml.timeSeriesExplorer.noSingleMetricJobsFoundLabel": "未找到单指标作业", @@ -12156,8 +13731,43 @@ "xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomLabel": "缩放:", "xpack.ml.timeSeriesExplorer.tryWideningTheTimeSelectionDescription": "请尝试扩大时间选择范围或进一步向后追溯。", "xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage": "在此仪表板中,一次仅可以查看一个作业", + "xpack.ml.toastNotificationService.errorTitle": "发生错误", "xpack.ml.tooltips.newJobDedicatedIndexTooltip": "将结果存储在此作业的不同索引中。", "xpack.ml.tooltips.newJobRecognizerJobPrefixTooltip": "前缀已添加到每个作业 ID 的开头。", + "xpack.ml.trainedModels.modelsList.actionsHeader": "操作", + "xpack.ml.trainedModels.modelsList.collapseRow": "折叠", + "xpack.ml.trainedModels.modelsList.createdAtHeader": "创建于", + "xpack.ml.trainedModels.modelsList.deleteModal.cancelButtonLabel": "取消", + "xpack.ml.trainedModels.modelsList.deleteModal.deleteButtonLabel": "删除", + "xpack.ml.trainedModels.modelsList.deleteModal.header": "删除 {modelsCount, plural, one {{modelId}} other {# 个模型}}?", + "xpack.ml.trainedModels.modelsList.deleteModal.modelsWithPipelinesWarningMessage": "{modelsWithPipelinesCount, plural, one{模型} other {模型}}{modelsWithPipelines} {modelsWithPipelinesCount, plural, one{有} other {有}}关联的管道!", + "xpack.ml.trainedModels.modelsList.deleteModelActionLabel": "删除模型", + "xpack.ml.trainedModels.modelsList.deleteModelsButtonLabel": "删除", + "xpack.ml.trainedModels.modelsList.disableSelectableMessage": "模型有关联的管道", + "xpack.ml.trainedModels.modelsList.expandedRow.analyticsConfigTitle": "分析配置", + "xpack.ml.trainedModels.modelsList.expandedRow.byPipelineTitle": "按管道", + "xpack.ml.trainedModels.modelsList.expandedRow.byProcessorTitle": "按处理器", + "xpack.ml.trainedModels.modelsList.expandedRow.configTabLabel": "配置", + "xpack.ml.trainedModels.modelsList.expandedRow.detailsTabLabel": "详情", + "xpack.ml.trainedModels.modelsList.expandedRow.detailsTitle": "详情", + "xpack.ml.trainedModels.modelsList.expandedRow.editPipelineLabel": "编辑", + "xpack.ml.trainedModels.modelsList.expandedRow.inferenceConfigTitle": "推理配置", + "xpack.ml.trainedModels.modelsList.expandedRow.inferenceStatsTitle": "推理统计信息", + "xpack.ml.trainedModels.modelsList.expandedRow.ingestStatsTitle": "采集统计信息", + "xpack.ml.trainedModels.modelsList.expandedRow.pipelinesTabLabel": "管道", + "xpack.ml.trainedModels.modelsList.expandedRow.processorsTitle": "处理器", + "xpack.ml.trainedModels.modelsList.expandedRow.statsTabLabel": "统计信息", + "xpack.ml.trainedModels.modelsList.expandRow": "展开", + "xpack.ml.trainedModels.modelsList.fetchFailedErrorMessage": "模型提取失败", + "xpack.ml.trainedModels.modelsList.fetchModelStatsErrorMessage": "提取模型统计信息失败", + "xpack.ml.trainedModels.modelsList.modelIdHeader": "ID", + "xpack.ml.trainedModels.modelsList.selectableMessage": "选择模型", + "xpack.ml.trainedModels.modelsList.selectedModelsMessage": "{modelsCount, plural, one{# 个模型} other {# 个模型}}已选择", + "xpack.ml.trainedModels.modelsList.successfullyDeletedMessage": "{modelsCount, plural, one {Model {modelsToDeleteIds}} other {# 个模型}}{modelsCount, plural, one {已} other {已}}成功删除", + "xpack.ml.trainedModels.modelsList.totalAmountLabel": "已训练的模型总数", + "xpack.ml.trainedModels.modelsList.typeHeader": "类型", + "xpack.ml.trainedModels.modelsList.unableToDeleteModelsErrorMessage": "无法删除模型", + "xpack.ml.trainedModels.modelsList.viewTrainingDataActionLabel": "查看训练数据", "xpack.ml.upgrade.upgradeWarning.upgradeInProgressWarningDescription": "当前正在升级与 Machine Learning 相关的索引。", "xpack.ml.upgrade.upgradeWarning.upgradeInProgressWarningDescriptionExtra": "此次某些操作不可用。", "xpack.ml.upgrade.upgradeWarning.upgradeInProgressWarningTitle": "正在进行索引迁移。", @@ -12181,6 +13791,12 @@ "xpack.monitoring.ajaxErrorHandler.requestFailedNotification.retryButtonLabel": "重试", "xpack.monitoring.ajaxErrorHandler.requestFailedNotificationTitle": "Monitoring 请求失败", "xpack.monitoring.alerts.actionGroups.default": "默认值", + "xpack.monitoring.alerts.actionVariables.action": "此告警的建议操作。", + "xpack.monitoring.alerts.actionVariables.actionPlain": "此告警的建议操作,无任何 Markdown。", + "xpack.monitoring.alerts.actionVariables.clusterName": "节点所属的集群。", + "xpack.monitoring.alerts.actionVariables.internalFullMessage": "Elastic 生成的完整内部消息。", + "xpack.monitoring.alerts.actionVariables.internalShortMessage": "Elastic 生成的简短内部消息。", + "xpack.monitoring.alerts.actionVariables.state": "告警的当前状态。", "xpack.monitoring.alerts.badge.panelTitle": "告警", "xpack.monitoring.alerts.callout.dangerLabel": "危险告警", "xpack.monitoring.alerts.callout.warningLabel": "警告告警", @@ -12212,8 +13828,23 @@ "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.hotThreads": "#start_link检查热线程#end_link", "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.runningTasks": "#start_link检查长时间运行的任务#end_link", "xpack.monitoring.alerts.cpuUsage.ui.resolvedMessage": "节点 {nodeName} 上的 cpu 使用率现在低于阈值,当前报告截止到 #resolved 为 {cpuUsage}%", - "xpack.monitoring.alerts.validation.duration": "必须指定有效的持续时间。", - "xpack.monitoring.alerts.validation.threshold": "必须指定有效数字。", + "xpack.monitoring.alerts.diskUsage.actionVariables.count": "报告高磁盘使用率的节点数目。", + "xpack.monitoring.alerts.diskUsage.actionVariables.nodes": "报告高磁盘使用率的节点列表。", + "xpack.monitoring.alerts.diskUsage.firing.internalFullMessage": "为集群 {clusterName} 中的 {count} 个节点触发了磁盘使用率告警。{action}", + "xpack.monitoring.alerts.diskUsage.firing.internalShortMessage": "为集群 {clusterName} 中的 {count} 个节点触发了磁盘使用率告警。{shortActionText}", + "xpack.monitoring.alerts.diskUsage.fullAction": "查看节点", + "xpack.monitoring.alerts.diskUsage.label": "磁盘使用率", + "xpack.monitoring.alerts.diskUsage.paramDetails.duration.label": "查看以下期间的平均值:", + "xpack.monitoring.alerts.diskUsage.paramDetails.threshold.label": "磁盘容量超过以下值时通知", + "xpack.monitoring.alerts.diskUsage.resolved.internalMessage": "为集群 {clusterName} 中的 {count} 个节点解决了磁盘使用率告警。", + "xpack.monitoring.alerts.diskUsage.shortAction": "验证受影响节点的磁盘使用水平。", + "xpack.monitoring.alerts.diskUsage.ui.firingMessage": "节点 #start_link{nodeName}#end_link 于 #absolute 报告磁盘使用率为 {diskUsage}%", + "xpack.monitoring.alerts.diskUsage.ui.nextSteps.addMoreNodes": "#start_link添加更多数据节点#end_link", + "xpack.monitoring.alerts.diskUsage.ui.nextSteps.identifyIndices": "#start_link识别大型索引#end_link", + "xpack.monitoring.alerts.diskUsage.ui.nextSteps.ilmPolicies": "#start_link实施 ILM 策略#end_link", + "xpack.monitoring.alerts.diskUsage.ui.nextSteps.resizeYourDeployment": "#start_link对您的部署进行大小调整 (ECE)#end_link", + "xpack.monitoring.alerts.diskUsage.ui.nextSteps.tuneDisk": "#start_link调整磁盘使用率#end_link", + "xpack.monitoring.alerts.diskUsage.ui.resolvedMessage": "节点 {nodeName} 的磁盘使用率现在低于阈值,截止到 #resolved 目前报告为 {diskUsage}%", "xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.clusterHealth": "在此集群中运行的 Elasticsearch 版本。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage": "为 {clusterName} 触发了 Elasticsearch 版本不匹配告警。Elasticsearch 正在运行 {versions}。{action}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Elasticsearch 版本不匹配告警。{shortActionText}", @@ -12260,7 +13891,44 @@ "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "在此集群中运行着多个 Logstash ({versions}) 版本。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.resolvedMessage": "在此集群中所有 Logstash 版本都相同。", + "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "报告高内存使用率的节点数目。", + "xpack.monitoring.alerts.memoryUsage.actionVariables.nodes": "报告高内存使用率的节点列表。", + "xpack.monitoring.alerts.memoryUsage.firing.internalFullMessage": "为集群 {clusterName} 中的 {count} 个节点触发了内存使用率告警。{action}", + "xpack.monitoring.alerts.memoryUsage.firing.internalShortMessage": "为集群 {clusterName} 中的 {count} 个节点触发了内存使用率告警。{shortActionText}", + "xpack.monitoring.alerts.memoryUsage.fullAction": "查看节点", + "xpack.monitoring.alerts.memoryUsage.label": "内存使用率 (JVM)", + "xpack.monitoring.alerts.memoryUsage.paramDetails.duration.label": "查看以下期间的平均值:", + "xpack.monitoring.alerts.memoryUsage.paramDetails.threshold.label": "内存使用率超过以下值时通知", + "xpack.monitoring.alerts.memoryUsage.resolved.internalMessage": "为集群 {clusterName} 中的 {count} 个节点解决了内存使用率告警。", + "xpack.monitoring.alerts.memoryUsage.shortAction": "验证受影响节点的内存使用率水平。", + "xpack.monitoring.alerts.memoryUsage.ui.firingMessage": "节点 #start_link{nodeName}#end_link 将于 #absolute 报告 JVM 内存使用率为 {memoryUsage}%", + "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.addMoreNodes": "#start_link添加更多数据节点#end_link", + "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.identifyIndicesShards": "#start_link识别大型索引/分片#end_link", + "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.managingHeap": "#start_link管理 ES 堆#end_link", + "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.resizeYourDeployment": "#start_link对您的部署进行大小调整 (ECE)#end_link", + "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.tuneThreadPools": "#start_link调整线程池#end_link", + "xpack.monitoring.alerts.memoryUsage.ui.resolvedMessage": "节点 {nodeName} 的 JVM 内存使用率现在低于阈值,截止到 #resolved 目标报告为 {memoryUsage}%", "xpack.monitoring.alerts.migrate.manageAction.requiredFieldError": "{field} 是必填字段。", + "xpack.monitoring.alerts.missingData.actionVariables.count": "缺少监测数据的堆栈产品数目。", + "xpack.monitoring.alerts.missingData.actionVariables.stackProducts": "缺少监测数据的堆栈产品。", + "xpack.monitoring.alerts.missingData.firing": "触发", + "xpack.monitoring.alerts.missingData.firing.internalFullMessage": "我们尚未检测到集群 {clusterName} 中 {count} 个堆栈产品的任何监测数据。{action}", + "xpack.monitoring.alerts.missingData.firing.internalShortMessage": "我们尚未检测到集群 {clusterName} 中 {count} 个堆栈产品的任何监测数据。{shortActionText}", + "xpack.monitoring.alerts.missingData.fullAction": "查看我们拥有这些堆栈产品的哪些监测数据。", + "xpack.monitoring.alerts.missingData.label": "缺少监测数据", + "xpack.monitoring.alerts.missingData.paramDetails.duration.label": "缺少以下对象的监测数据时通知", + "xpack.monitoring.alerts.missingData.paramDetails.limit.label": "追溯到遥远的过去以获取监测数据", + "xpack.monitoring.alerts.missingData.resolved": "已解决", + "xpack.monitoring.alerts.missingData.resolved.internalFullMessage": "我们现在看到集群 {clusterName} 中 {count} 个堆栈产品的监测数据。", + "xpack.monitoring.alerts.missingData.resolved.internalShortMessage": "我们现在看到集群 {clusterName} 中 {count} 个堆栈产品的监测数据。", + "xpack.monitoring.alerts.missingData.shortAction": "验证这些堆栈产品是否已启动并正常运行,然后仔细检查监测设置。", + "xpack.monitoring.alerts.missingData.ui.firingMessage": "在过去的 {gapDuration},从 #absolute 开始,我们尚未检测到来自 {stackProduct} {type}: {stackProductName} 的任何监测数据", + "xpack.monitoring.alerts.missingData.ui.nextSteps.verifySettings": "验证 {type} 上的监测设置", + "xpack.monitoring.alerts.missingData.ui.nextSteps.viewAll": "#start_link查看所有 {stackProduct} {type}#end_link", + "xpack.monitoring.alerts.missingData.ui.notQuiteResolvedMessage": "我们还没有看到 {stackProduct} {type}: {stackProductName} 的监测数据,将停止试用。要更改此设置,请配置告警,以追溯到更远的过去以获取数据。", + "xpack.monitoring.alerts.missingData.ui.resolvedMessage": "我们现在看到截止 #resolved 的 {stackProduct} {type}: {stackProductName} 的监测数据", + "xpack.monitoring.alerts.missingData.validation.duration": "需要有效的持续时间。", + "xpack.monitoring.alerts.missingData.validation.limit": "需要有效的限值。", "xpack.monitoring.alerts.nodesChanged.actionVariables.added": "添加到集群的节点列表。", "xpack.monitoring.alerts.nodesChanged.actionVariables.removed": "从集群中移除的节点列表。", "xpack.monitoring.alerts.nodesChanged.actionVariables.restarted": "在集群中重新启动的节点列表。", @@ -12283,13 +13951,25 @@ "xpack.monitoring.alerts.panel.muteAlert.errorTitle": "无法静音告警", "xpack.monitoring.alerts.panel.muteTitle": "静音", "xpack.monitoring.alerts.panel.ummuteAlert.errorTitle": "无法取消告警静音", + "xpack.monitoring.alerts.state.firing": "触发", + "xpack.monitoring.alerts.state.resolved": "已解决", "xpack.monitoring.alerts.status.alertsTooltip": "告警", "xpack.monitoring.alerts.status.clearText": "清除", "xpack.monitoring.alerts.status.clearToolip": "无告警触发", "xpack.monitoring.alerts.status.highSeverityTooltip": "有一些紧急问题需要您立即关注!", "xpack.monitoring.alerts.status.lowSeverityTooltip": "存在一些低紧急问题。", "xpack.monitoring.alerts.status.mediumSeverityTooltip": "有一些问题可能会影响您的堆栈。", + "xpack.monitoring.alerts.typeLabel.instance": "实例", + "xpack.monitoring.alerts.typeLabel.instances": "实例", + "xpack.monitoring.alerts.typeLabel.node": "节点", + "xpack.monitoring.alerts.typeLabel.nodes": "节点", + "xpack.monitoring.alerts.typeLabel.server": "服务器", + "xpack.monitoring.alerts.typeLabel.servers": "服务器", + "xpack.monitoring.alerts.validation.duration": "需要有效的持续时间。", + "xpack.monitoring.alerts.validation.threshold": "需要有效的数字。", "xpack.monitoring.apm.healthStatusLabel": "运行状况:{status}", + "xpack.monitoring.apm.instance.heading": "APM 服务器实例", + "xpack.monitoring.apm.instance.pageTitle": "APM 服务器实例:{instanceName}", "xpack.monitoring.apm.instance.routeTitle": "{apm} - 实例", "xpack.monitoring.apm.instance.status.lastEventDescription": "{timeOfLastEvent}前", "xpack.monitoring.apm.instance.status.lastEventLabel": "最后事件", @@ -12301,11 +13981,13 @@ "xpack.monitoring.apm.instances.allocatedMemoryTitle": "已分配内存", "xpack.monitoring.apm.instances.bytesSentRateTitle": "已发送字节速率", "xpack.monitoring.apm.instances.filterInstancesPlaceholder": "筛选实例……", + "xpack.monitoring.apm.instances.heading": "APM 实例", "xpack.monitoring.apm.instances.lastEventTitle": "最后事件", "xpack.monitoring.apm.instances.lastEventValue": "{timeOfLastEvent}前", "xpack.monitoring.apm.instances.nameTitle": "名称", "xpack.monitoring.apm.instances.outputEnabledTitle": "已启用输出", "xpack.monitoring.apm.instances.outputErrorsTitle": "输出错误", + "xpack.monitoring.apm.instances.pageTitle": "APM 服务器实例", "xpack.monitoring.apm.instances.routeTitle": "{apm} - 实例", "xpack.monitoring.apm.instances.status.lastEventDescription": "{timeOfLastEvent}前", "xpack.monitoring.apm.instances.status.lastEventLabel": "最后事件", @@ -12315,10 +13997,11 @@ "xpack.monitoring.apm.instances.totalEventsRateTitle": "事件合计速率", "xpack.monitoring.apm.instances.versionFilter": "版本", "xpack.monitoring.apm.instances.versionTitle": "版本", + "xpack.monitoring.apm.overview.heading": "APM 服务器概览", + "xpack.monitoring.apm.overview.pageTitle": "APM 服务器概览", + "xpack.monitoring.apm.overview.routeTitle": "APM 服务器", "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": "配置重载", @@ -12330,10 +14013,12 @@ "xpack.monitoring.beats.instance.hostLabel": "主机", "xpack.monitoring.beats.instance.nameLabel": "名称", "xpack.monitoring.beats.instance.outputLabel": "输出", + "xpack.monitoring.beats.instance.pageTitle": "Beat 实例:{beatName}", "xpack.monitoring.beats.instance.routeTitle": "Beats - {instanceName} - 概览", "xpack.monitoring.beats.instance.typeLabel": "类型", "xpack.monitoring.beats.instance.uptimeLabel": "运行时间", "xpack.monitoring.beats.instance.versionLabel": "版本", + "xpack.monitoring.beats.instances.alertsColumnTitle": "告警", "xpack.monitoring.beats.instances.allocatedMemoryTitle": "已分配内存", "xpack.monitoring.beats.instances.bytesSentRateTitle": "已发送字节速率", "xpack.monitoring.beats.instances.nameTitle": "名称", @@ -12345,17 +14030,20 @@ "xpack.monitoring.beats.instances.versionFilter": "版本", "xpack.monitoring.beats.instances.versionTitle": "版本", "xpack.monitoring.beats.listing.heading": "Beats 列表", - "xpack.monitoring.beats.overview.activeBeatsInLastDayTitle": "过去一天里的活动 Beats", + "xpack.monitoring.beats.listing.pageTitle": "Beats 列表", + "xpack.monitoring.beats.overview.activeBeatsInLastDayTitle": "过去一天的活动 Beats", "xpack.monitoring.beats.overview.bytesSentLabel": "已发送字节", + "xpack.monitoring.beats.overview.heading": "Beats 概览", "xpack.monitoring.beats.overview.latestActive.last1DayLabel": "过去 1 天", "xpack.monitoring.beats.overview.latestActive.last1HourLabel": "过去 1 小时", "xpack.monitoring.beats.overview.latestActive.last1MinuteLabel": "过去 1 分钟", "xpack.monitoring.beats.overview.latestActive.last20MinutesLabel": "过去 20 分钟", "xpack.monitoring.beats.overview.latestActive.last5MinutesLabel": "过去 5 分钟", "xpack.monitoring.beats.overview.noActivityDescription": "您好!此区域将显示您最新的 Beats 活动,但似乎在过去一天内您没有任何活动。", + "xpack.monitoring.beats.overview.pageTitle": "Beats 概览", "xpack.monitoring.beats.overview.routeTitle": "Beats - 概览", - "xpack.monitoring.beats.overview.top5BeatTypesInLastDayTitle": "过去一天内排名前 5 的 Beat 类型", - "xpack.monitoring.beats.overview.top5VersionsInLastDayTitle": "过去一天里排名前 5 的版本", + "xpack.monitoring.beats.overview.top5BeatTypesInLastDayTitle": "过去一天排名前 5 Beat 类型", + "xpack.monitoring.beats.overview.top5VersionsInLastDayTitle": "过去一天排名前 5 版本", "xpack.monitoring.beats.overview.totalBeatsLabel": "Beats 合计", "xpack.monitoring.beats.overview.totalEventsLabel": "事件合计", "xpack.monitoring.beats.routeTitle": "Beats", @@ -12363,13 +14051,13 @@ "xpack.monitoring.beatsNavigation.instancesLinkText": "实例", "xpack.monitoring.beatsNavigation.overviewLinkText": "概览", "xpack.monitoring.breadcrumbs.apm.instancesLabel": "实例", - "xpack.monitoring.breadcrumbs.apmLabel": "APM", + "xpack.monitoring.breadcrumbs.apmLabel": "APM 服务器", "xpack.monitoring.breadcrumbs.beats.instancesLabel": "实例", "xpack.monitoring.breadcrumbs.beatsLabel": "Beats", "xpack.monitoring.breadcrumbs.clustersLabel": "集群", "xpack.monitoring.breadcrumbs.es.ccrLabel": "CCR", "xpack.monitoring.breadcrumbs.es.indicesLabel": "索引", - "xpack.monitoring.breadcrumbs.es.jobsLabel": "作业", + "xpack.monitoring.breadcrumbs.es.jobsLabel": "Machine Learning 作业", "xpack.monitoring.breadcrumbs.es.nodesLabel": "节点", "xpack.monitoring.breadcrumbs.kibana.instancesLabel": "实例", "xpack.monitoring.breadcrumbs.logstash.nodesLabel": "节点", @@ -12381,6 +14069,9 @@ "xpack.monitoring.chart.screenReaderUnaccessibleTitle": "此图表不支持屏幕阅读器读取", "xpack.monitoring.chart.seriesScreenReaderListDescription": "时间间隔:{bucketSize}", "xpack.monitoring.chart.timeSeries.zoomOut": "缩小", + "xpack.monitoring.cluster.health.healthy": "运行正常", + "xpack.monitoring.cluster.health.primaryShards": "缺少主分片", + "xpack.monitoring.cluster.health.replicaShards": "缺少副本分片", "xpack.monitoring.cluster.listing.dataColumnTitle": "数据", "xpack.monitoring.cluster.listing.incompatibleLicense.getLicenseLinkLabel": "获取具有完整功能的许可证", "xpack.monitoring.cluster.listing.incompatibleLicense.infoMessage": "需要监测多个集群?{getLicenseInfoLink}以实现多集群监测。", @@ -12397,19 +14088,20 @@ "xpack.monitoring.cluster.listing.logstashColumnTitle": "Logstash", "xpack.monitoring.cluster.listing.nameColumnTitle": "名称", "xpack.monitoring.cluster.listing.nodesColumnTitle": "节点", + "xpack.monitoring.cluster.listing.pageTitle": "集群列表", "xpack.monitoring.cluster.listing.standaloneClusterCallOutDismiss": "消除", "xpack.monitoring.cluster.listing.standaloneClusterCallOutLink": "查看这些实例。", "xpack.monitoring.cluster.listing.standaloneClusterCallOutText": "或者,单击下表中的独立集群", "xpack.monitoring.cluster.listing.standaloneClusterCallOutTitle": "似乎您具有未连接到 Elasticsearch 集群的实例。", "xpack.monitoring.cluster.listing.statusColumnTitle": "状态", "xpack.monitoring.cluster.listing.unknownHealthMessage": "未知", - "xpack.monitoring.cluster.overview.apmPanel.apmTitle": "APM", - "xpack.monitoring.cluster.overview.apmPanel.instancesTotalLinkAriaLabel": "APM 实例:{apmsTotal}", + "xpack.monitoring.cluster.overview.apmPanel.apmTitle": "APM 服务器", + "xpack.monitoring.cluster.overview.apmPanel.instancesTotalLinkAriaLabel": "APM 服务器实例:{apmsTotal}", "xpack.monitoring.cluster.overview.apmPanel.lastEventDescription": "{timeOfLastEvent}前", "xpack.monitoring.cluster.overview.apmPanel.lastEventLabel": "最后事件", "xpack.monitoring.cluster.overview.apmPanel.memoryUsageLabel": "内存利用率", - "xpack.monitoring.cluster.overview.apmPanel.overviewLinkAriaLabel": "APM 概览", - "xpack.monitoring.cluster.overview.apmPanel.overviewLinkLabel": "概览", + "xpack.monitoring.cluster.overview.apmPanel.overviewLinkAriaLabel": "APM 服务器概览", + "xpack.monitoring.cluster.overview.apmPanel.overviewLinkLabel": "APM 服务器概览", "xpack.monitoring.cluster.overview.apmPanel.processedEventsLabel": "已处理事件", "xpack.monitoring.cluster.overview.apmPanel.serversTotalLinkLabel": "APM 服务器:{apmsTotal}", "xpack.monitoring.cluster.overview.beatsPanel.beatsTitle": "Beats", @@ -12430,7 +14122,7 @@ "xpack.monitoring.cluster.overview.esPanel.indicesCountLinkAriaLabel": "Elasticsearch 索引:{indicesCount}", "xpack.monitoring.cluster.overview.esPanel.indicesCountLinkLabel": "索引:{indicesCount}", "xpack.monitoring.cluster.overview.esPanel.infoLogsTooltipText": "信息日志数", - "xpack.monitoring.cluster.overview.esPanel.jobsLabel": "作业", + "xpack.monitoring.cluster.overview.esPanel.jobsLabel": "Machine Learning 作业", "xpack.monitoring.cluster.overview.esPanel.jvmHeapLabel": "{javaVirtualMachine} 堆", "xpack.monitoring.cluster.overview.esPanel.licenseLabel": "许可证", "xpack.monitoring.cluster.overview.esPanel.logsLinkAriaLabel": "Elasticsearch 日志", @@ -12471,6 +14163,7 @@ "xpack.monitoring.cluster.overview.logstashPanel.uptimeLabel": "运行时间", "xpack.monitoring.cluster.overview.logstashPanel.withMemoryQueuesLabel": "内存队列", "xpack.monitoring.cluster.overview.logstashPanel.withPersistentQueuesLabel": "持久性队列", + "xpack.monitoring.cluster.overview.pageTitle": "集群概览", "xpack.monitoring.cluster.overviewTitle": "概览", "xpack.monitoring.clusterAlerts.checkLicense.licenseIsBasicDescription": "如果禁用了 Watcher 或 [{clusterSource}] 集群的当前许可为基本许可,则“集群告警”将不会显示。", "xpack.monitoring.clusterAlerts.checkLicense.licenseNotActiveDescription": "因为 [{clusterSource}] 集群的当前许可 [{type}] 未处于活动状态,所以“集群告警”将不会显示。", @@ -12485,15 +14178,16 @@ "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": "索引", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.lastFetchTimeColumnTitle": "上次提取时间", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.opsSyncedColumnTitle": "已同步操作", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.syncLagOpsColumnTitle": "同步延迟(操作)", + "xpack.monitoring.elasticsearch.ccr.pageTitle": "Elasticsearch Ccr", "xpack.monitoring.elasticsearch.ccr.routeTitle": "Elasticsearch - CCR", "xpack.monitoring.elasticsearch.ccr.shard.instanceTitle": "索引:{followerIndex} 分片:{shardId}", + "xpack.monitoring.elasticsearch.ccr.shard.pageTitle": "Elasticsearch Ccr 分片 - 索引:{followerIndex} 分片:{shardId}", "xpack.monitoring.elasticsearch.ccr.shard.routeTitle": "Elasticsearch - CCR - 分片", "xpack.monitoring.elasticsearch.ccr.shardsTable.errorColumnTitle": "错误", "xpack.monitoring.elasticsearch.ccr.shardsTable.lastFetchTimeColumnTitle": "上次提取时间", @@ -12533,11 +14227,13 @@ "xpack.monitoring.elasticsearch.indices.monitoringTablePlaceholder": "筛选索引……", "xpack.monitoring.elasticsearch.indices.nameTitle": "名称", "xpack.monitoring.elasticsearch.indices.noIndicesMatchYourSelectionDescription": "没有索引匹配您的选择。请尝试更改时间范围选择。", + "xpack.monitoring.elasticsearch.indices.overview.pageTitle": "索引:{indexName}", "xpack.monitoring.elasticsearch.indices.overview.routeTitle": "Elasticsearch - 索引 - {indexName} - 概览", + "xpack.monitoring.elasticsearch.indices.pageTitle": "Elasticsearch 索引", "xpack.monitoring.elasticsearch.indices.routeTitle": "Elasticsearch - 索引", "xpack.monitoring.elasticsearch.indices.searchRateTitle": "搜索速率", "xpack.monitoring.elasticsearch.indices.statusTitle": "状态", - "xpack.monitoring.elasticsearch.indices.systemIndicesLabel": "系统索引", + "xpack.monitoring.elasticsearch.indices.systemIndicesLabel": "筛留系统索引", "xpack.monitoring.elasticsearch.indices.unassignedShardsTitle": "未分配分片", "xpack.monitoring.elasticsearch.mlJobListing.filterJobsPlaceholder": "筛选作业……", "xpack.monitoring.elasticsearch.mlJobListing.forecastsTitle": "预测", @@ -12549,8 +14245,17 @@ "xpack.monitoring.elasticsearch.mlJobListing.processedRecordsTitle": "已处理记录", "xpack.monitoring.elasticsearch.mlJobListing.stateTitle": "状态", "xpack.monitoring.elasticsearch.mlJobListing.statusIconLabel": "作业状态:{status}", + "xpack.monitoring.elasticsearch.mlJobs.pageTitle": "Elasticsearch Machine Learning 作业", "xpack.monitoring.elasticsearch.mlJobs.routeTitle": "Elasticsearch - Machine Learning 作业", "xpack.monitoring.elasticsearch.node.advanced.routeTitle": "Elasticsearch - 节点 - {nodeSummaryName} - 高级", + "xpack.monitoring.elasticsearch.node.cells.tooltip.iconLabel": "有关此指标的更多信息", + "xpack.monitoring.elasticsearch.node.cells.tooltip.max": "最大值", + "xpack.monitoring.elasticsearch.node.cells.tooltip.min": "最小值", + "xpack.monitoring.elasticsearch.node.cells.tooltip.preface": "适用于当前时段", + "xpack.monitoring.elasticsearch.node.cells.tooltip.trending": "趋势", + "xpack.monitoring.elasticsearch.node.cells.trendingDownText": "关闭", + "xpack.monitoring.elasticsearch.node.cells.trendingUpText": "启动", + "xpack.monitoring.elasticsearch.node.overview.pageTitle": "Elasticsearch 节点:{node}", "xpack.monitoring.elasticsearch.node.overview.routeTitle": "Elasticsearch - 节点 - {nodeName} - 概览", "xpack.monitoring.elasticsearch.node.statusIconLabel": "状态:{status}", "xpack.monitoring.elasticsearch.nodeDetailStatus.alerts": "告警", @@ -12576,12 +14281,14 @@ "xpack.monitoring.elasticsearch.nodes.metricbeatMigration.disableInternalCollectionTitle": "Metricbeat 现在正监测您的 Elasticsearch 节点", "xpack.monitoring.elasticsearch.nodes.monitoringTablePlaceholder": "筛选节点……", "xpack.monitoring.elasticsearch.nodes.nameColumnTitle": "名称", + "xpack.monitoring.elasticsearch.nodes.pageTitle": "Elasticsearch 节点", "xpack.monitoring.elasticsearch.nodes.routeTitle": "Elasticsearch - 节点", "xpack.monitoring.elasticsearch.nodes.shardsColumnTitle": "分片", "xpack.monitoring.elasticsearch.nodes.statusColumn.offlineLabel": "脱机", "xpack.monitoring.elasticsearch.nodes.statusColumn.onlineLabel": "联机", "xpack.monitoring.elasticsearch.nodes.statusColumnTitle": "状态", "xpack.monitoring.elasticsearch.nodes.unknownNodeTypeLabel": "未知", + "xpack.monitoring.elasticsearch.overview.pageTitle": "Elasticsearch 概览", "xpack.monitoring.elasticsearch.shardActivity.completedRecoveriesLabel": "已完成恢复", "xpack.monitoring.elasticsearch.shardActivity.noActiveShardRecoveriesMessage.completedRecoveriesLinkText": "已完成恢复", "xpack.monitoring.elasticsearch.shardActivity.noActiveShardRecoveriesMessage.completedRecoveriesLinkTextProblem": "此集群没有活动的分片恢复。", @@ -12612,6 +14319,7 @@ "xpack.monitoring.elasticsearch.shardAllocation.shardLegendTitle": "分片图例", "xpack.monitoring.elasticsearch.shardAllocation.tableBody.noShardsAllocatedDescription": "未分配任何分片。", "xpack.monitoring.elasticsearch.shardAllocation.tableBodyDisplayName": "TableBody", + "xpack.monitoring.elasticsearch.shardAllocation.tableHead.filterSystemIndices": "筛留系统索引", "xpack.monitoring.elasticsearch.shardAllocation.tableHead.indicesLabel": "索引", "xpack.monitoring.elasticsearch.shardAllocation.unassignedDisplayName": "未分配", "xpack.monitoring.elasticsearch.shardAllocation.unassignedPrimaryLabel": "未分配主分片", @@ -12639,7 +14347,7 @@ "xpack.monitoring.esNavigation.indicesLinkText": "索引", "xpack.monitoring.esNavigation.instance.advancedLinkText": "高级", "xpack.monitoring.esNavigation.instance.overviewLinkText": "概览", - "xpack.monitoring.esNavigation.jobsLinkText": "作业", + "xpack.monitoring.esNavigation.jobsLinkText": "Machine Learning 作业", "xpack.monitoring.esNavigation.nodesLinkText": "节点", "xpack.monitoring.esNavigation.overviewLinkText": "概览", "xpack.monitoring.euiSSPTable.setupNewButtonLabel": "为新的 {identifier} 设置监测", @@ -12650,16 +14358,19 @@ "xpack.monitoring.expiredLicenseStatusDescription": "您的许可证已于 {expiryDate}过期", "xpack.monitoring.expiredLicenseStatusTitle": "您的{typeTitleCase}许可证已过期", "xpack.monitoring.feature.reserved.description": "要向用户授予访问权限,还应分配 monitoring_user 角色。", + "xpack.monitoring.featureCatalogueDescription": "跟踪部署的实时运行状况和性能。", + "xpack.monitoring.featureCatalogueTitle": "监测堆栈", "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.tlsAndEncryptionError": "堆栈监测告警需要在 Kibana 和 Elasticsearch 之间启用传输层安全,并在 kibana.yml 文件中配置加密密钥。", "xpack.monitoring.healthCheck.tlsAndEncryptionErrorTitle": "需要其他设置", - "xpack.monitoring.janLabel": "一月", - "xpack.monitoring.julLabel": "七月", - "xpack.monitoring.junLabel": "六月", + "xpack.monitoring.internalAndMetricbeatMonitoringToast.description": "似乎您正在同时使用 Metricbeat 和“Legacy Collection”进行堆栈监测。\n 在 8.0.0 中,必须使用 Metricbeat 收集监测数据。\n 请在设置模式下按照相关步骤将监测的其余部分迁移到 Metricbeat。", + "xpack.monitoring.internalAndMetricbeatMonitoringToast.title": "检测到部分旧版监测", + "xpack.monitoring.internalMonitoringToast.description": "似乎您正在使用“Legacy Collection”进行堆栈监测。\n 下一主要版本 (8.0.0) 将不再支持这种监测方法。\n 请在设置模式下按照相关步骤使用 Metricbeat 开始监测。", + "xpack.monitoring.internalMonitoringToast.enterSetupMode": "进入设置模式", + "xpack.monitoring.internalMonitoringToast.learnMoreAction": "了解详情", + "xpack.monitoring.internalMonitoringToast.title": "检测到内部监测", "xpack.monitoring.kibana.clusterStatus.connectionsLabel": "连接", "xpack.monitoring.kibana.clusterStatus.instancesLabel": "实例", "xpack.monitoring.kibana.clusterStatus.maxResponseTimeLabel": "最大响应时间", @@ -12669,8 +14380,11 @@ "xpack.monitoring.kibana.detailStatus.transportAddressLabel": "传输地址", "xpack.monitoring.kibana.detailStatus.uptimeLabel": "运行时间", "xpack.monitoring.kibana.detailStatus.versionLabel": "版本", + "xpack.monitoring.kibana.instance.pageTitle": "Kibana 实例:{instance}", "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeDescription": "以下实例未受监测。\n 单击下面的“使用 Metricbeat 监测”以开始监测。", "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeTitle": "检测到 Kibana 实例", + "xpack.monitoring.kibana.instances.pageTitle": "Kibana 实例", + "xpack.monitoring.kibana.instances.routeTitle": "Kibana - 实例", "xpack.monitoring.kibana.listing.alertsColumnTitle": "告警", "xpack.monitoring.kibana.listing.filterInstancesPlaceholder": "筛选实例……", "xpack.monitoring.kibana.listing.loadAverageColumnTitle": "负载平均值", @@ -12679,6 +14393,7 @@ "xpack.monitoring.kibana.listing.requestsColumnTitle": "请求", "xpack.monitoring.kibana.listing.responseTimeColumnTitle": "响应时间", "xpack.monitoring.kibana.listing.statusColumnTitle": "状态", + "xpack.monitoring.kibana.overview.pageTitle": "Kibana 概览", "xpack.monitoring.kibana.shardActivity.bytesTitle": "字节", "xpack.monitoring.kibana.shardActivity.filesTitle": "文件", "xpack.monitoring.kibana.shardActivity.indexTitle": "索引", @@ -12692,6 +14407,7 @@ "xpack.monitoring.license.heading": "许可证", "xpack.monitoring.license.howToUpdateLicenseDescription": "要更新此集群的许可,请通过 Elasticsearch {apiText} 提供许可文件:", "xpack.monitoring.license.licenseRouteTitle": "许可证", + "xpack.monitoring.loading.pageTitle": "正在加载", "xpack.monitoring.logs.listing.calloutLinkText": "日志", "xpack.monitoring.logs.listing.calloutTitle": "想要查看更多日志?", "xpack.monitoring.logs.listing.clusterPageDescription": "显示此集群最新的日志,总共最多 {limit} 个日志。", @@ -12746,8 +14462,11 @@ "xpack.monitoring.logstash.detailStatus.versionLabel": "版本", "xpack.monitoring.logstash.filterNodesPlaceholder": "筛选节点……", "xpack.monitoring.logstash.filterPipelinesPlaceholder": "筛选管道……", + "xpack.monitoring.logstash.node.advanced.pageTitle": "Logstash 节点:{nodeName}", "xpack.monitoring.logstash.node.advanced.routeTitle": "Logstash - {nodeName} - 高级", + "xpack.monitoring.logstash.node.pageTitle": "Logstash 节点:{nodeName}", "xpack.monitoring.logstash.node.pipelines.notAvailableDescription": "仅 Logstash 版本 6.0.0 或更高版本提供管道监测功能。此节点正在运行版本 {logstashVersion}。", + "xpack.monitoring.logstash.node.pipelines.pageTitle": "Logstash 节点管道:{nodeName}", "xpack.monitoring.logstash.node.pipelines.routeTitle": "Logstash - {nodeName} - 管道", "xpack.monitoring.logstash.node.routeTitle": "Logstash - {nodeName}", "xpack.monitoring.logstash.nodes.alertsColumnTitle": "告警", @@ -12759,7 +14478,10 @@ "xpack.monitoring.logstash.nodes.jvmHeapUsedTitle": "已使用 {javaVirtualMachine} 堆", "xpack.monitoring.logstash.nodes.loadAverageTitle": "负载平均值", "xpack.monitoring.logstash.nodes.nameTitle": "名称", + "xpack.monitoring.logstash.nodes.pageTitle": "Logstash 节点", + "xpack.monitoring.logstash.nodes.routeTitle": "Logstash - 节点", "xpack.monitoring.logstash.nodes.versionTitle": "版本", + "xpack.monitoring.logstash.overview.pageTitle": "Logstash 概览", "xpack.monitoring.logstash.pipeline.detailDrawer.conditionalStatementDescription": "这是您的管道中的条件语句。", "xpack.monitoring.logstash.pipeline.detailDrawer.eventsEmittedLabel": "已发出事件", "xpack.monitoring.logstash.pipeline.detailDrawer.eventsEmittedRateLabel": "已发出事件速率", @@ -12770,6 +14492,7 @@ "xpack.monitoring.logstash.pipeline.detailDrawer.specifyVertexIdDescription": "没有为此 {vertexType} 显式指定 ID。指定 ID 允许您跟踪管道更改间的差异。您可以为此插件显式指定 ID,如:", "xpack.monitoring.logstash.pipeline.detailDrawer.structureDescription": "这是 Logstash 用来缓冲输入和管道其余部分之间的事件的内部结构。", "xpack.monitoring.logstash.pipeline.detailDrawer.vertexIdDescription": "此 {vertexType} 的 ID 为 {vertexId}。", + "xpack.monitoring.logstash.pipeline.pageTitle": "Logstash 管道:{pipeline}", "xpack.monitoring.logstash.pipeline.queue.noMetricsDescription": "队列指标不可用", "xpack.monitoring.logstash.pipeline.relativeFirstSeenAgoLabel": "{relativeFirstSeen}前", "xpack.monitoring.logstash.pipeline.relativeLastSeenAgoLabel": "直到 {relativeLastSeen}前", @@ -12778,6 +14501,8 @@ "xpack.monitoring.logstash.pipelines.eventsEmittedRateTitle": "已发出事件速率", "xpack.monitoring.logstash.pipelines.idTitle": "ID", "xpack.monitoring.logstash.pipelines.numberOfNodesTitle": "节点数目", + "xpack.monitoring.logstash.pipelines.pageTitle": "Logstash 管道", + "xpack.monitoring.logstash.pipelines.routeTitle": "Logstash 管道", "xpack.monitoring.logstash.pipelineStatement.viewDetailsAriaLabel": "查看详情", "xpack.monitoring.logstash.pipelineViewer.filtersTitle": "筛选", "xpack.monitoring.logstash.pipelineViewer.inputsTitle": "输入", @@ -12789,8 +14514,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 +15137,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 +15175,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 +15218,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": "未找到告警。", @@ -13523,6 +15236,15 @@ "xpack.observability.emptySection.apps.uptime.description": "主动监测站点和服务的可用性。接收告警并更快地解决问题,从而优化用户体验。", "xpack.observability.emptySection.apps.uptime.link": "安装 Heartbeat", "xpack.observability.emptySection.apps.uptime.title": "运行时间", + "xpack.observability.emptySection.apps.ux.description": "性能具有分布特征。衡量所有访问者的 Web 应用程序访问体验,并了解如何改善每个人的体验。", + "xpack.observability.emptySection.apps.ux.link": "安装 Rum 代理", + "xpack.observability.emptySection.apps.ux.title": "用户体验", + "xpack.observability.featureCatalogueDescription": "通过专用 UI 整合您的日志、指标、应用程序跟踪和系统可用性。", + "xpack.observability.featureCatalogueDescription1": "监测基础架构指标。", + "xpack.observability.featureCatalogueDescription2": "跟踪应用程序请求。", + "xpack.observability.featureCatalogueDescription3": "衡量 SLA 并响应问题。", + "xpack.observability.featureCatalogueSubtitle": "集中管理和监测", + "xpack.observability.featureCatalogueTitle": "可观测性", "xpack.observability.home.addData": "添加数据", "xpack.observability.home.breadcrumb": "概览", "xpack.observability.home.feedback": "提供反馈", @@ -13566,6 +15288,8 @@ "xpack.observability.overview.uptime.monitors": "监测", "xpack.observability.overview.uptime.title": "运行时间", "xpack.observability.overview.uptime.up": "运行", + "xpack.observability.overview.ux.appLink": "在应用中查看", + "xpack.observability.overview.ux.title": "用户体验", "xpack.observability.resources.documentation": "文档", "xpack.observability.resources.forum": "讨论论坛", "xpack.observability.resources.title": "资源", @@ -13579,6 +15303,29 @@ "xpack.observability.section.apps.uptime.description": "主动监测站点和服务的可用性。接收告警并更快地解决问题,从而优化用户体验。", "xpack.observability.section.apps.uptime.title": "运行时间", "xpack.observability.section.errorPanel": "尝试提取数据时发生错误。请重试", + "xpack.observability.ux.coreVitals.average": "平均值", + "xpack.observability.ux.coreVitals.averageMessage": " 且小于 {bad}", + "xpack.observability.ux.coreVitals.cls": "累计布局偏移", + "xpack.observability.ux.coreVitals.cls.help": "累计布局偏移 (CLS):衡量视觉稳定性。为了提供良好的用户体验,页面的 CLS 应小于 0.1。", + "xpack.observability.ux.coreVitals.fid.help": "首次输入延迟用于衡量交互性。为了提供良好的用户体验,页面的 FID 应小于 100 毫秒。", + "xpack.observability.ux.coreVitals.fip": "首次输入延迟", + "xpack.observability.ux.coreVitals.good": "良好", + "xpack.observability.ux.coreVitals.is": "是", + "xpack.observability.ux.coreVitals.lcp": "最大内容绘制", + "xpack.observability.ux.coreVitals.lcp.help": "最大内容绘制用于衡量加载性能。为了提供良好的用户体验,LCP 应在页面首次开始加载后的 2.5 秒内执行。", + "xpack.observability.ux.coreVitals.legends.good": "良好", + "xpack.observability.ux.coreVitals.legends.needsImprovement": "需改进", + "xpack.observability.ux.coreVitals.legends.poor": "不良", + "xpack.observability.ux.coreVitals.less": "少于", + "xpack.observability.ux.coreVitals.more": "多于", + "xpack.observability.ux.coreVitals.paletteLegend.rankPercentage": "{labelsInd} ({ranksInd}%)", + "xpack.observability.ux.coreVitals.poor": "不良", + "xpack.observability.ux.coreVitals.takes": "需要", + "xpack.observability.ux.coreWebVitals": "网站体验核心指标", + "xpack.observability.ux.coreWebVitals.service": "服务", + "xpack.observability.ux.dashboard.webCoreVitals.help": "详细了解", + "xpack.observability.ux.dashboard.webVitals.palette.tooltip": "{percentage}% 的用户有{exp}体验,因为{title}{isOrTakes}{moreOrLess} {value}{averageMessage}。", + "xpack.observability.ux.service.help": "选择流量最高的 RUM 服务", "xpack.painlessLab.apiReferenceButtonLabel": "API 参考", "xpack.painlessLab.context.defaultLabel": "脚本结果将转换成字符串", "xpack.painlessLab.context.filterLabel": "使用筛选脚本查询的上下文", @@ -13768,9 +15515,9 @@ "xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "没有该名称的远程集群。", "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "无法编辑集群,ES 未返回任何响应。", "xpack.reporting.breadcrumb": "报告", - "xpack.reporting.browsers.chromium.errorDetected": "Reporting 检测到错误:{err}", - "xpack.reporting.browsers.chromium.pageErrorDetected": "Reporting 在页面上检测到错误:{err}", - "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "接收到禁止的传出 URL:“{interceptedUrl}”,正在退出", + "xpack.reporting.browsers.chromium.errorDetected": "报告时遇到错误:{err}", + "xpack.reporting.browsers.chromium.pageErrorDetected": "报告时在页面上遇到错误:{err}", + "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "收到禁止的传出 URL:“{interceptedUrl}”。请求失败,关闭浏览器。", "xpack.reporting.chromiumDriver.failedToCompleteRequest": "无法完成请求:{error}", "xpack.reporting.chromiumDriver.failedToCompleteRequestUsingHeaders": "无法完成使用 headers 的请求:{error}", "xpack.reporting.dashboard.csvDownloadStartedMessage": "您的 CSV 将很快下载。", @@ -13778,6 +15525,13 @@ "xpack.reporting.dashboard.downloadCsvPanelTitle": "下载 CSV", "xpack.reporting.dashboard.failedCsvDownloadMessage": "我们此次无法生成 CSV。", "xpack.reporting.dashboard.failedCsvDownloadTitle": "CSV 下载失败。", + "xpack.reporting.diagnostic.browserCrashed": "启动期间浏览器已异常退出", + "xpack.reporting.diagnostic.browserErrored": "启动时浏览器进程引发了错误", + "xpack.reporting.diagnostic.browserMissingDependency": "由于缺少系统依赖项,浏览器无法正常启动。请参见 {url}", + "xpack.reporting.diagnostic.browserMissingFonts": "浏览器找不到默认字体。请参见 {url} 以解决此问题。", + "xpack.reporting.diagnostic.configSizeMismatch": "xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} ({kibanaMaxContentBytes}) 大于 ElasticSearch 的 {ES_MAX_SIZE_BYTES_PATH} ({elasticSearchMaxContentBytes})。请在 ElasticSearch 中将 {ES_MAX_SIZE_BYTES_PATH} 设置为匹配或减小 Kibana 中的 xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH}。", + "xpack.reporting.diagnostic.noUsableSandbox": "无法使用 Chromium 沙盒。您自行承担使用“xpack.reporting.capture.browser.chromium.disableSandbox”禁用此项的风险。请参见 {url}", + "xpack.reporting.diagnostic.screenshotFailureMessage": "我们无法拍摄 Kibana 安装的屏幕截图。", "xpack.reporting.errorButton.showReportErrorAriaLabel": "显示报告错误", "xpack.reporting.errorButton.unableToFetchReportContentTitle": "无法提取报告内容", "xpack.reporting.errorButton.unableToGenerateReportTitle": "无法生成报告", @@ -13797,7 +15551,24 @@ "xpack.reporting.jobStatuses.pendingText": "待处理", "xpack.reporting.jobStatuses.processingText": "正在处理", "xpack.reporting.jobStatuses.warningText": "已完成,但有警告", - "xpack.reporting.listing.reports.subtitle": "在此处查找 Kibana 应用程序中生成的报告", + "xpack.reporting.listing.diagnosticApiCallFailure": "运行诊断时出现问题:{error}", + "xpack.reporting.listing.diagnosticBrowserButton": "检查浏览器", + "xpack.reporting.listing.diagnosticBrowserMessage": "报告使用Headless浏览器生成 PDF 和 PNG。验证浏览器是否可以成功启动。", + "xpack.reporting.listing.diagnosticBrowserTitle": "检查浏览器", + "xpack.reporting.listing.diagnosticButton": "运行报告诊断", + "xpack.reporting.listing.diagnosticConfigButton": "验证配置", + "xpack.reporting.listing.diagnosticConfigMessage": "确保是否为报告正确设置 Kibana 配置。", + "xpack.reporting.listing.diagnosticConfigTitle": "验证 Kibana 配置", + "xpack.reporting.listing.diagnosticDescription": "运行诊断以自动解决常见报告问题。", + "xpack.reporting.listing.diagnosticFailureDescription": "下面是一些有关该问题的详细信息:", + "xpack.reporting.listing.diagnosticFailureTitle": "某些功能无法正常运行。", + "xpack.reporting.listing.diagnosticScreenshotButton": "捕获屏幕截图", + "xpack.reporting.listing.diagnosticScreenshotMessage": "确保无标头浏览器可以捕获页面的屏幕截图。", + "xpack.reporting.listing.diagnosticScreenshotTitle": "检查屏幕捕获", + "xpack.reporting.listing.diagnosticSuccessMessage": "一切都好,可以报告。", + "xpack.reporting.listing.diagnosticSuccessTitle": "准备就绪!", + "xpack.reporting.listing.diagnosticTitle": "报告诊断", + "xpack.reporting.listing.reports.subtitle": "获取在 Kibana 应用程序中生成的报告。", "xpack.reporting.listing.reportstitle": "报告", "xpack.reporting.listing.table.csvContainsFormulas": "您的 CSV 包含电子表格应用程序可解释为公式的字符。", "xpack.reporting.listing.table.deleteCancelButton": "取消", @@ -13835,6 +15606,8 @@ "xpack.reporting.panelContent.successfullyQueuedReportNotificationTitle": "已为 {objectType} 排队报告", "xpack.reporting.panelContent.whatCanBeExportedWarningDescription": "请先保存您的工作", "xpack.reporting.panelContent.whatCanBeExportedWarningTitle": "只会导出保存的 {objectType}", + "xpack.reporting.pdfFooterImageDescription": "要在 PDF 的页脚中使用的定制图像", + "xpack.reporting.pdfFooterImageLabel": "PDF 页脚图像", "xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportMessage": "报告包含电子表格应用程序可解释为公式的字符。", "xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportTitle": "报告可能包含公式 {reportObjectType} '{reportObjectTitle}'", "xpack.reporting.publicNotifier.downloadReportButtonLabel": "下载报告", @@ -13852,6 +15625,7 @@ "xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle": "已为 {reportObjectType}“{reportObjectTitle}”创建报告", "xpack.reporting.registerFeature.reportingDescription": "管理您从 Discover、Visualize 和 Dashboard 生成的报告。", "xpack.reporting.registerFeature.reportingTitle": "报告", + "xpack.reporting.screencapture.browserWasClosed": "浏览器已意外关闭!有关更多信息,请查看服务器日志。", "xpack.reporting.screencapture.couldntFinishRendering": "尝试等候 {count} 个可视化完成渲染时发生错误。您可能需要增加“{configKey}”。{error}", "xpack.reporting.screencapture.couldntLoadKibana": "尝试打开 Kibana URL 时发生了错误。您可能需要增加“{configKey}”。{error}", "xpack.reporting.screencapture.injectCss": "尝试为 Reporting 更新 Kibana CSS 时发生错误。{error}", @@ -14159,6 +15933,11 @@ "xpack.security.apiKeys.breadcrumb": "API 密钥", "xpack.security.authentication.login.validateLogin.requiredPasswordErrorMessage": "“密码”必填", "xpack.security.authentication.login.validateLogin.requiredUsernameErrorMessage": "“用户名”必填", + "xpack.security.checkup.dismissButtonText": "关闭", + "xpack.security.checkup.dontShowAgain": "不再显示", + "xpack.security.checkup.enableButtonText": "启用安全功能", + "xpack.security.checkup.insecureClusterMessage": "我们的免费安全功能可防止未经授权的访问。", + "xpack.security.checkup.insecureClusterTitle": "请确保您的安装安全", "xpack.security.common.extendedRoleDeprecationNotice": "{roleName} 角色已过时。{reason}", "xpack.security.components.sessionIdleTimeoutWarning.message": "由于不活动,您会在 {timeout} 后自动注销。单击“确定”可以恢复。", "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "确定", @@ -14240,7 +16019,7 @@ "xpack.security.management.deprecatedBadge": "(已过时)", "xpack.security.management.disabledBadge": "已禁用", "xpack.security.management.editRole.cancelButtonLabel": "取消", - "xpack.security.management.editRole.changeAllPrivilegesLink": "(全部更改)", + "xpack.security.management.editRole.changeAllPrivilegesLink": "批处理操作", "xpack.security.management.editRole.collapsiblePanel.hideLinkText": "隐藏", "xpack.security.management.editRole.collapsiblePanel.showLinkText": "显示", "xpack.security.management.editRole.createRoleText": "创建角色", @@ -14261,7 +16040,12 @@ "xpack.security.management.editRole.elasticSearchPrivileges.learnMoreLinkText": "了解详情", "xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription": "管理此角色可以对您的集群执行的操作。 ", "xpack.security.management.editRole.elasticSearchPrivileges.runAsPrivilegesTitle": "运行身份权限", + "xpack.security.management.editRole.errorDeletingRoleError": "删除角色时出错", + "xpack.security.management.editRole.errorSavingRoleError": "保存角色时出错", "xpack.security.management.editRole.featureTable.customizeSubFeaturePrivilegesSwitchLabel": "定制子功能权限", + "xpack.security.management.editRole.featureTable.featureAccordionSwitchLabel": "{grantedCount} / {featureCount} 项{featureCount, plural, one {功能} other {功能}}已授权", + "xpack.security.management.editRole.featureTable.featureVisibilityTitle": "定制功能权限", + "xpack.security.management.editRole.featureTable.managementCategoryHelpText": "对堆栈管理的访问由 Elasticsearch 和 Kibana 权限共同决定,并且不能显式禁用。", "xpack.security.management.editRole.featureTable.privilegeCustomizationTooltip": "功能已定制子功能权限。展开此行以了解更多信息。", "xpack.security.management.editRole.indexPrivilegeForm.deleteSpacePrivilegeAriaLabel": "删除索引权限", "xpack.security.management.editRole.indexPrivilegeForm.grantedDocumentsQueryFormRowLabel": "已授权文档查询", @@ -14299,7 +16083,7 @@ "xpack.security.management.editRole.simplePrivilegeForm.specifyPrivilegeForRoleDescription": "为此角色指定 Kibana 权限。", "xpack.security.management.editRole.simplePrivilegeForm.unsupportedSpacePrivilegesWarning": "此角色包含工作区的权限定义,但在 Kibana 中未启用工作区。保存此角色将会移除这些权限。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaAdmin} 角色授予的所有权限,然后重试。", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "* 全局(所有工作区)", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "* 所有工作区", "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "您无权查看所有可用工作区。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "权限不足", "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin", @@ -14309,15 +16093,17 @@ "xpack.security.management.editRole.spacePrivilegeForm.featurePrivilegeSummaryDescription": "某些功能可能被工作区隐藏或受全局工作区权限影响。", "xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeNotice": "这些权限将应用到所有当前和未来工作区。", "xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeWarning": "创建全局权限可能会影响您的其他工作区权限。", - "xpack.security.management.editRole.spacePrivilegeForm.modalTitle": "工作区权限", - "xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormLabel": "权限", + "xpack.security.management.editRole.spacePrivilegeForm.modalTitle": "Kibana 权限", + "xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormHelpText": "分配您希望向此工作区的所有现有和未来功能授予的权限级别。", + "xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormLabel": "所有功能的权限", + "xpack.security.management.editRole.spacePrivilegeForm.spaceSelectorFormHelpText": "选择一个或多个希望分配权限的 Kibana 工作区。", "xpack.security.management.editRole.spacePrivilegeForm.spaceSelectorFormLabel": "工作区", "xpack.security.management.editRole.spacePrivilegeForm.summaryOfFeaturePrivileges": "功能权限的摘要", "xpack.security.management.editRole.spacePrivilegeForm.supersededWarning": "声明的权限相对配置的全局权限有较小的宽容度。查看权限摘要以查看有效的权限。", "xpack.security.management.editRole.spacePrivilegeForm.supersededWarningTitle": "已由全局权限取代", - "xpack.security.management.editRole.spacePrivilegeMatrix.globalSpaceName": "全局", + "xpack.security.management.editRole.spacePrivilegeMatrix.globalSpaceName": "所有工作区", "xpack.security.management.editRole.spacePrivilegeMatrix.showNMoreSpacesLink": "另外 {count} 个", - "xpack.security.management.editRole.spacePrivilegeSection.addSpacePrivilegeButton": "添加工作区权限", + "xpack.security.management.editRole.spacePrivilegeSection.addSpacePrivilegeButton": "添加 Kibana 权限", "xpack.security.management.editRole.spacePrivilegeSection.noAccessToKibanaTitle": "此角色未授予对 Kibana 的访问权限", "xpack.security.management.editRole.spacePrivilegeTable.deletePrivilegesLabel": "删除以下工作区的权限:{spaceNames}。", "xpack.security.management.editRole.spacePrivilegeTable.editPrivilegesLabel": "编辑以下工作区的权限:{spaceNames}。", @@ -14435,7 +16221,7 @@ "xpack.security.management.editRoles.indexPrivilegeForm.grantedFieldsFormRowLabel": "已授权字段", "xpack.security.management.editRoles.indexPrivilegeForm.grantFieldPrivilegesLabel": "授予对特定字段的访问权限", "xpack.security.management.editRolespacePrivilegeForm.createGlobalPrivilegeButton": "创建全局权限", - "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "创建工作区权限", + "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "添加 Kibana 权限", "xpack.security.management.editRolespacePrivilegeForm.updateGlobalPrivilegeButton": "更新全局权限", "xpack.security.management.editRolespacePrivilegeForm.updatePrivilegeButton": "更新工作区权限", "xpack.security.management.enabledBadge": "已启用", @@ -14562,8 +16348,12 @@ "xpack.security.overwrittenSession.continueAsUserText": "作为 {username} 继续", "xpack.security.overwrittenSession.title": "您以前以其他用户身份登录。", "xpack.security.overwrittenSessionAppTitle": "已覆盖会话", - "xpack.security.registerFeature.securitySettingsDescription": "保护您的数据,并轻松管理谁有权限以用户和角色身份访问什么内容。", - "xpack.security.registerFeature.securitySettingsTitle": "安全性设置", + "xpack.security.registerFeature.securitySettingsDescription": "控制访问者及其可以执行的任务。", + "xpack.security.registerFeature.securitySettingsTitle": "管理权限", + "xpack.security.resetSession.description": "返回上一页面或以其他用户身份登录。", + "xpack.security.resetSession.goBackButtonLabel": "返回", + "xpack.security.resetSession.logOutButtonLabel": "以其他用户身份登录", + "xpack.security.resetSession.title": "您无权访问请求的页面。", "xpack.security.role_mappings.validation.invalidName": "名称必填。", "xpack.security.role_mappings.validation.invalidRoleRule": "至少需要一个规则。", "xpack.security.role_mappings.validation.invalidRoles": "至少需要一个角色。", @@ -14576,6 +16366,7 @@ "xpack.security.users.createBreadcrumb": "创建", "xpack.securitySolution.add_filter_to_global_search_bar.filterForValueHoverAction": "筛留值", "xpack.securitySolution.add_filter_to_global_search_bar.filterOutValueHoverAction": "筛除值", + "xpack.securitySolution.administration.list.beta": "公测版", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "选择此规则生成的所有告警的风险分数。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "默认风险分数", "xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel": "使用源事件值覆盖默认风险分数。", @@ -14600,8 +16391,10 @@ "xpack.securitySolution.alertsView.errorFetchingAlertsData": "无法查询告警数据", "xpack.securitySolution.alertsView.moduleLabel": "模块", "xpack.securitySolution.alertsView.showing": "正在显示", - "xpack.securitySolution.alertsView.totalCountOfAlerts": "个外部告警匹配搜索条件", + "xpack.securitySolution.alertsView.totalCountOfAlerts": "外部告警", "xpack.securitySolution.alertsView.unit": "个外部{totalCount, plural, =1 {告警} other {告警}}", + "xpack.securitySolution.allHost.errorSearchDescription": "搜索所有主机时发生错误", + "xpack.securitySolution.allHost.failSearchDescription": "无法对所有主机执行搜索", "xpack.securitySolution.andOrBadge.and": "且", "xpack.securitySolution.andOrBadge.or": "或", "xpack.securitySolution.anomaliesTable.table.anomaliesDescription": "异常", @@ -14730,6 +16523,8 @@ "xpack.securitySolution.auditd.violatedSeLinuxPolicyDescription": "已违反 selinux 策略", "xpack.securitySolution.auditd.wasAuthorizedToUseDescription": "有权使用", "xpack.securitySolution.auditd.withResultDescription": ",结果为", + "xpack.securitySolution.authentications.errorSearchDescription": "搜索身份验证时发生错误", + "xpack.securitySolution.authentications.failSearchDescription": "无法对身份验证执行搜索", "xpack.securitySolution.authenticationsTable.authentications": "身份验证", "xpack.securitySolution.authenticationsTable.failures": "错误", "xpack.securitySolution.authenticationsTable.lastFailedDestination": "上一失败目标", @@ -14745,7 +16540,13 @@ "xpack.securitySolution.authenticationsTable.user": "用户", "xpack.securitySolution.authz.mlUnavailable": "Machine Learning 插件不可用。请尝试启用插件。", "xpack.securitySolution.authz.userIsNotMlAdminMessage": "当前用户不是 Machine Learning 管理员。", + "xpack.securitySolution.autocomplete.fieldRequiredError": "值不能为空", + "xpack.securitySolution.autocomplete.invalidDateError": "不是有效日期", + "xpack.securitySolution.autocomplete.invalidNumberError": "不是有效数字", "xpack.securitySolution.autocomplete.loadingDescription": "正在加载……", + "xpack.securitySolution.autocomplete.selectField": "请首先选择字段......", + "xpack.securitySolution.beatFields.errorSearchDescription": "获取 Beat 字段时发生错误", + "xpack.securitySolution.beatFields.failSearchDescription": "无法对 Beat 字段执行搜索", "xpack.securitySolution.case.allCases.actions": "操作", "xpack.securitySolution.case.allCases.comments": "注释", "xpack.securitySolution.case.allCases.noTagsAvailable": "没有可用标记", @@ -14823,6 +16624,7 @@ "xpack.securitySolution.case.caseView.emailBody": "案例参考:{caseUrl}", "xpack.securitySolution.case.caseView.emailSubject": "Security 案例 - {caseTitle}", "xpack.securitySolution.case.caseView.errorsPushServiceCallOutTitle": "要将案例发送到外部系统,您需要:", + "xpack.securitySolution.case.caseView.fieldChanged": "已更改连接器字段", "xpack.securitySolution.case.caseView.fieldRequiredError": "必填字段", "xpack.securitySolution.case.caseView.goToDocumentationButton": "查看文档", "xpack.securitySolution.case.caseView.moveToCommentAria": "高亮显示引用的注释", @@ -14831,8 +16633,6 @@ "xpack.securitySolution.case.caseView.noTags": "当前没有为此案例分配标记。", "xpack.securitySolution.case.caseView.openedOn": "打开时间", "xpack.securitySolution.case.caseView.optional": "可选", - "xpack.securitySolution.case.caseView.pageBadgeLabel": "公测版", - "xpack.securitySolution.case.caseView.pageBadgeTooltip": "案例工作流仍为公测版。请通过在 Kibana 存储库中报告问题或错误,帮助我们改进产品。", "xpack.securitySolution.case.caseView.particpantsLabel": "参与者", "xpack.securitySolution.case.caseView.pushNamedIncident": "推送为 { thirdParty } 事件", "xpack.securitySolution.case.caseView.pushThirdPartyIncident": "推送为外部事件", @@ -14858,6 +16658,7 @@ "xpack.securitySolution.case.caseView.unknown": "未知", "xpack.securitySolution.case.caseView.updateNamedIncident": "更新 { thirdParty } 事件", "xpack.securitySolution.case.caseView.updateThirdPartyIncident": "更新外部事件", + "xpack.securitySolution.case.common.noConnector": "未选择连接器", "xpack.securitySolution.case.configure.errorPushingToService": "推送到服务时出错", "xpack.securitySolution.case.configure.successSaveToast": "已保存外部连接设置", "xpack.securitySolution.case.configureCases.addNewConnector": "添加新连接器", @@ -14896,9 +16697,20 @@ "xpack.securitySolution.case.createCase.fieldTagsHelpText": "为此案例键入一个或多个定制识别标记。在每个标记后按 Enter 键可开始新的标记。", "xpack.securitySolution.case.createCase.titleFieldRequiredError": "标题必填。", "xpack.securitySolution.case.dismissErrorsPushServiceCallOutTitle": "关闭", + "xpack.securitySolution.case.editConnector.editConnectorLinkAria": "单击以编辑连接器", "xpack.securitySolution.case.pageTitle": "案例", "xpack.securitySolution.case.readOnlySavedObjectDescription": "您仅有权查看案例。如果需要创建和更新案例,请联系您的 Kibana 管理员。", "xpack.securitySolution.case.readOnlySavedObjectTitle": "您无法创建新案例或更新现有案例", + "xpack.securitySolution.case.settings.jira.issueTypesSelectFieldLabel": "问题类型", + "xpack.securitySolution.case.settings.jira.parentIssueSearchLabel": "父问题", + "xpack.securitySolution.case.settings.jira.prioritySelectFieldLabel": "优先级", + "xpack.securitySolution.case.settings.resilient.incidentTypesLabel": "事件类型", + "xpack.securitySolution.case.settings.resilient.incidentTypesPlaceholder": "选择类型", + "xpack.securitySolution.case.settings.resilient.severityLabel": "严重性", + "xpack.securitySolution.case.settings.resilient.unableToGetIncidentTypesMessage": "无法获取事件类型", + "xpack.securitySolution.case.settings.resilient.unableToGetSeverityMessage": "无法获取严重性", + "xpack.securitySolution.caseSettingsRegistry.get.missingCaseSettingErrorMessage": "未注册对象类型“{id}”。", + "xpack.securitySolution.caseSettingsRegistry.register.duplicateCaseSettingErrorMessage": "已注册对象类型“{id}”。", "xpack.securitySolution.certificate.fingerprint.clientCertLabel": "客户端证书", "xpack.securitySolution.certificate.fingerprint.serverCertLabel": "服务器证书", "xpack.securitySolution.chart.allOthersGroupingLabel": "所有其他", @@ -14911,6 +16723,8 @@ "xpack.securitySolution.clipboard.copy": "复制", "xpack.securitySolution.clipboard.copy.to.the.clipboard": "复制到剪贴板", "xpack.securitySolution.clipboard.to.the.clipboard": "至剪贴板", + "xpack.securitySolution.components.create.stepOneTitle": "案例字段", + "xpack.securitySolution.components.create.stepTwoTitle": "外部事件管理系统字段", "xpack.securitySolution.components.embeddables.embeddedMap.clientLayerLabel": "客户端点", "xpack.securitySolution.components.embeddables.embeddedMap.destinationLayerLabel": "目标点", "xpack.securitySolution.components.embeddables.embeddedMap.embeddableHeaderHelp": "地图配置帮助", @@ -14968,7 +16782,7 @@ "xpack.securitySolution.components.mlPopup.hooks.errors.siemJobFetchFailureTitle": "Security 作业提取失败", "xpack.securitySolution.components.mlPopup.jobsTable.createCustomJobButtonLabel": "创建定制作业", "xpack.securitySolution.components.mlPopup.jobsTable.jobNameColumn": "作业名称", - "xpack.securitySolution.components.mlPopup.jobsTable.noItemsDescription": "未找到任何 SIEM Machine Learning 作业", + "xpack.securitySolution.components.mlPopup.jobsTable.noItemsDescription": "找不到任何安全 Machine Learning 作业", "xpack.securitySolution.components.mlPopup.jobsTable.runJobColumn": "运行作业", "xpack.securitySolution.components.mlPopup.jobsTable.tagsColumn": "组", "xpack.securitySolution.components.mlPopup.licenseButtonLabel": "管理许可证", @@ -14980,6 +16794,19 @@ "xpack.securitySolution.components.mlPopup.upgradeButtonLabel": "订阅计划", "xpack.securitySolution.components.mlPopup.upgradeDescription": "要访问 SIEM 的异常检测功能,必须将您的许可证更新到白金级、开始 30 天免费试用或在 AWS、GCP 或 Azure 中实施{cloudLink}。然后便可以运行 Machine Learning 作业并查看异常。", "xpack.securitySolution.components.mlPopup.upgradeTitle": "升级到 Elastic 白金级", + "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel": "选择父问题", + "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder": "选择父问题", + "xpack.securitySolution.components.settings.jira.searchIssuesLoading": "正在加载……", + "xpack.securitySolution.components.settings.jira.unableToGetFieldsMessage": "无法获取字段", + "xpack.securitySolution.components.settings.jira.unableToGetIssueMessage": "无法获取 ID 为 {id} 的问题", + "xpack.securitySolution.components.settings.jira.unableToGetIssuesMessage": "无法获取问题", + "xpack.securitySolution.components.settings.jira.unableToGetIssueTypesMessage": "无法获取问题类型", + "xpack.securitySolution.components.settings.serviceNow.impactSelectFieldLabel": "影响", + "xpack.securitySolution.components.settings.serviceNow.severitySelectFieldLabel": "严重性", + "xpack.securitySolution.components.settings.servicenow.severitySelectHighOptionLabel": "高", + "xpack.securitySolution.components.settings.servicenow.severitySelectLowOptionLabel": "低", + "xpack.securitySolution.components.settings.servicenow.severitySelectMediumOptionLabel": "中", + "xpack.securitySolution.components.settings.serviceNow.urgencySelectFieldLabel": "紧急性", "xpack.securitySolution.components.stepDefineRule.ruleTypeField.subscriptionsLink": "白金级订阅", "xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "无法查询异常数据", "xpack.securitySolution.containers.anomalies.stackByJobId": "作业", @@ -15046,6 +16873,7 @@ "xpack.securitySolution.detectionEngine.alerts.actions.addException": "添加规则例外", "xpack.securitySolution.detectionEngine.alerts.actions.closeAlertTitle": "关闭告警", "xpack.securitySolution.detectionEngine.alerts.actions.inProgressAlertTitle": "标记为进行中", + "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineAriaLabel": "将告警发送到时间线", "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineTitle": "在时间线中调查", "xpack.securitySolution.detectionEngine.alerts.actions.openAlertTitle": "打开告警", "xpack.securitySolution.detectionEngine.alerts.closedAlertFailedToastMessage": "无法关闭告警。", @@ -15075,7 +16903,8 @@ "xpack.securitySolution.detectionEngine.alerts.openAlertsTitle": "打开", "xpack.securitySolution.detectionEngine.alerts.openedAlertFailedToastMessage": "无法打开告警", "xpack.securitySolution.detectionEngine.alerts.openedAlertSuccessToastMessage": "已成功打开 {totalAlerts} 个{totalAlerts, plural, =1 {告警} other {告警}}。", - "xpack.securitySolution.detectionEngine.alerts.totalCountOfAlertsTitle": "个告警匹配搜索条件", + "xpack.securitySolution.detectionEngine.alerts.totalCountOfAlertsTitle": "告警", + "xpack.securitySolution.detectionEngine.alerts.updateAlertStatusFailedSingleAlert": "无法更新告警,因为它已被修改。", "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showBuildingBlockTitle": "包括构建块告警", "xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersTitle": "其他筛选", "xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.closeSelectedTitle": "关闭所选", @@ -15104,6 +16933,7 @@ "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle": "创建规则但不激活", "xpack.securitySolution.detectionEngine.createRule.backToRulesDescription": "返回到检测规则", "xpack.securitySolution.detectionEngine.createRule.editRuleButton": "编辑", + "xpack.securitySolution.detectionEngine.createRule.eqlRuleTypeDescription": "事件关联", "xpack.securitySolution.detectionEngine.createRule.filtersLabel": "筛选", "xpack.securitySolution.detectionEngine.createRule.mlRuleTypeDescription": "Machine Learning", "xpack.securitySolution.detectionEngine.createRule.pageTitle": "创建新规则", @@ -15151,10 +16981,16 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customMitreAttackTechniquesFieldRequiredError": "一个策略至少需要一个技术。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError": "KQL 无效", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError": "需要定制查询。", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "所有匹配项都需要字段和威胁索引字段。", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError": "至少需要一个威胁匹配项。", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.EqlQueryBarLabel": "EQL 查询", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel": "异常分数阈值", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel": "Machine Learning 作业", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel": "定制查询", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel": "规则类型", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatIndexPatternsLabel": "威胁索引模式", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatMappingLabel": "威胁映射", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatQueryBarLabel": "威胁索引查询", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "阈值", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.importTimelineModalTitle": "从已保存时间线导入查询", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.importTimelineQueryButton": "从已保存时间线导入查询", @@ -15168,18 +17004,26 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError": "至少需要一个索引模式。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.referencesUrlInvalidError": "Url 的格式无效", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.resetDefaultIndicesButton": "重置为默认索引模式", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.eqlTypeDescription": "使用事件查询语言 (EQL) 可匹配事件,生成序列,以及堆叠数据", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.eqlTypeTitle": "事件关联", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDescription": "选择 ML 作业以检测异常活动。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription": "要访问 ML,需要{subscriptionsLink}。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeTitle": "Machine Learning", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.queryTypeDescription": "使用 KQL 或 Lucene 检测所有索引的问题。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.queryTypeTitle": "定制查询", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchDescription": "上传值列表,以围绕已知错误属性列表编写规则", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchTitle": "威胁匹配", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeDescription": "聚合查询结果以检测匹配数目何时超过阈值。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeTitle": "阈值", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchField.threatMatchFieldPlaceholderText": "所有结果", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription": "选择威胁索引", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError": "至少需要一种索引模式。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText": "所有结果", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText": "选择在规则评估为 true 时应执行自动操作的时间。", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel": "操作频率", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.invalidMustacheTemplateErrorMessage": "{key} 不是有效的 Mustache 模板", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.noConnectorSelectedErrorMessage": "未选择连接器", + "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.noReadActionsPrivileges": "无法创建规则操作。您对“操作”插件没有“读”权限。", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithActivatingTitle": "创建并激活规则", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithoutActivatingTitle": "创建规则但不激活", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText": "增加回查时段的时间以防止错过告警。", @@ -15190,6 +17034,8 @@ "xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.invalidTimeMessageDescription": "时间必填。", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.minutesOptionDescription": "分钟", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.secondsOptionDescription": "秒", + "xpack.securitySolution.detectionEngine.createRule.threatMatchRuleTypeDescription": "威胁匹配", + "xpack.securitySolution.detectionEngine.createRule.threatQueryLabel": "威胁查询", "xpack.securitySolution.detectionEngine.createRule.thresholdRuleTypeDescription": "阈值", "xpack.securitySolution.detectionEngine.details.stepAboutRule.aboutText": "关于", "xpack.securitySolution.detectionEngine.details.stepAboutRule.detailsLabel": "详情", @@ -15207,9 +17053,12 @@ "xpack.securitySolution.detectionEngine.emptyActionBeats": "查看设置说明", "xpack.securitySolution.detectionEngine.emptyActionSecondary": "前往文档", "xpack.securitySolution.detectionEngine.emptyTitle": "似乎在 Security 应用程序中没有与检测引擎相关的索引", + "xpack.securitySolution.detectionEngine.eqlOverViewLink.text": "事件查询语言 (EQL) 概览", + "xpack.securitySolution.detectionEngine.eqlQueryBar.label": "输入 EQL 查询", + "xpack.securitySolution.detectionEngine.eqlValidation.requestError": "验证 EQL 查询时发生错误", + "xpack.securitySolution.detectionEngine.eqlValidation.showErrorsLabel": "显示 EQL 验证错误", + "xpack.securitySolution.detectionEngine.eqlValidation.title": "EQL 验证错误", "xpack.securitySolution.detectionEngine.goToDocumentationButton": "查看文档", - "xpack.securitySolution.detectionEngine.headerPage.pageBadgeLabel": "公测版", - "xpack.securitySolution.detectionEngine.headerPage.pageBadgeTooltip": "告警仍为公测版。请通过在 Kibana 存储库中报告问题或错误,帮助我们改进产品。", "xpack.securitySolution.detectionEngine.lastSignalTitle": "上一告警", "xpack.securitySolution.detectionEngine.mitreAttack.addTitle": "添加 MITRE ATT&CK\\u2122 威胁", "xpack.securitySolution.detectionEngine.mitreAttack.tacticPlaceHolderDescription": "选择策略......", @@ -15507,6 +17356,20 @@ "xpack.securitySolution.detectionEngine.noWriteAlertsCallOutTitle": "您无法更改告警状态", "xpack.securitySolution.detectionEngine.pageTitle": "检测引擎", "xpack.securitySolution.detectionEngine.panelSubtitleShowing": "正在显示", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "计数", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer": "注意:此预览不包括规则例外和时间戳覆盖的影响。", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql": "注意:此预览不包括规则例外和时间戳覆盖的影响,且仅显示 100 个结果。", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphHitsTitle": "命中数", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "提取预览时出错", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "查询预览", + "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "噪音警告:此规则可能会导致大量噪音。考虑缩小您的查询范围。这基于每小时 1 条告警的线性级数。", + "xpack.securitySolution.detectionEngine.queryPreview.queryNoHits": "找不到任何命中。", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphCapHitWarning": "命中查询上限大小为 {cap}。此查询生成的命中数可能大于显示的 {cap}。", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphThresholdWithFieldTitle": "{buckets} 个{buckets, plural, =1 {唯一命中} other {唯一命中}}", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTimestampWarning": "在事件中找不到“@timestamp”字段。", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTitle": "{hits} 个{hits, plural, =1 {命中} other {命中}}", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "选择数据的时间范围以预览查询结果", + "xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "快速查询预览", "xpack.securitySolution.detectionEngine.readOnlyCallOutMsg": "您当前缺少所需的权限,无法创建/编辑检测引擎规则。有关进一步帮助,请联系您的管理员。", "xpack.securitySolution.detectionEngine.readOnlyCallOutTitle": "需要规则权限", "xpack.securitySolution.detectionEngine.rule.editRule.errorMsgDescription": "您在{countError, plural, one {以下选项卡} other {以下选项卡}}中的输入无效:{tabHasError}", @@ -15532,21 +17395,21 @@ "xpack.securitySolution.detectionEngine.rules.aboutRuleTitle": "关于规则", "xpack.securitySolution.detectionEngine.rules.addNewRuleTitle": "创建新规则", "xpack.securitySolution.detectionEngine.rules.addPageTitle": "创建", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteeRuleDescription": "删除规则……", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "复制规则……", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleErrorDescription": "复制规则时出错……", + "xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteeRuleDescription": "删除规则", + "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "复制规则", + "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleErrorDescription": "复制规则时出错", "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateTitle": "复制", "xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription": "编辑规则设置", "xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "导出规则", "xpack.securitySolution.detectionEngine.rules.allRules.activeRuleDescription": "活动", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedErrorTitle": "激活{totalRules, plural, =1 {规则} other {规则}}时出错……", + "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedErrorTitle": "激活{totalRules, plural, =1 {规则} other {规则}}时出错", "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedTitle": "激活所选", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedErrorTitle": "停用{totalRules, plural, =1 {规则} other {规则}}时出错……", + "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedErrorTitle": "停用{totalRules, plural, =1 {规则} other {规则}}时出错", "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedTitle": "停用所选", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedErrorTitle": "删除{totalRules, plural, =1 {规则} other {规则}}时出错……", + "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedErrorTitle": "删除{totalRules, plural, =1 {规则} other {规则}}时出错", "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "选择内容包含无法删除的不可变规则", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle": "删除所选……", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle": "复制所选……", + "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle": "删除所选", + "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle": "复制所选", "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.exportSelectedTitle": "导出所选", "xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "批处理操作", "xpack.securitySolution.detectionEngine.rules.allRules.columns.activateTitle": "已激活", @@ -15555,11 +17418,14 @@ "xpack.securitySolution.detectionEngine.rules.allRules.columns.lastLookBackDate": "最后回查日期", "xpack.securitySolution.detectionEngine.rules.allRules.columns.lastResponseTitle": "上次响应", "xpack.securitySolution.detectionEngine.rules.allRules.columns.lastRunTitle": "上次运行", + "xpack.securitySolution.detectionEngine.rules.allRules.columns.lastUpdateTitle": "上次更新时间", "xpack.securitySolution.detectionEngine.rules.allRules.columns.queryTimes": "查询时间 (ms)", "xpack.securitySolution.detectionEngine.rules.allRules.columns.riskScoreTitle": "风险分数", "xpack.securitySolution.detectionEngine.rules.allRules.columns.ruleTitle": "规则", "xpack.securitySolution.detectionEngine.rules.allRules.columns.severityTitle": "严重性", + "xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsPopoverTitle": "查看全部", "xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsTitle": "标记", + "xpack.securitySolution.detectionEngine.rules.allRules.columns.versionTitle": "版本", "xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle": "rules_export", "xpack.securitySolution.detectionEngine.rules.allRules.filters.customRulesTitle": "定制规则", "xpack.securitySolution.detectionEngine.rules.allRules.filters.elasticRulesTitle": "Elastic 规则", @@ -15586,14 +17452,13 @@ "xpack.securitySolution.detectionEngine.rules.defineRuleTitle": "定义规则", "xpack.securitySolution.detectionEngine.rules.deleteDescription": "删除", "xpack.securitySolution.detectionEngine.rules.editPageTitle": "编辑", - "xpack.securitySolution.detectionEngine.rules.importRuleTitle": "导入规则……", + "xpack.securitySolution.detectionEngine.rules.importRuleTitle": "导入规则", "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton": "加载 Elastic 预构建规则和时间线模板", "xpack.securitySolution.detectionEngine.rules.optionalFieldDescription": "可选", "xpack.securitySolution.detectionEngine.rules.pageTitle": "检测规则", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton": "创建自己的规则", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elastic Security 附带预置检测规则,这些规则在后台运行,并在条件满足时创建告警。默认情况下,除 Elastic Endpoint Security 规则外,所有预置规则都处于禁用状态。您可以选择其他要激活的规则。", + "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elastic Security 附带预置检测规则,这些规则在后台运行,并在条件得到满足时创建告警。默认情况下,除 Endpoint Security 规则外,所有预置规则都处于禁用状态。您可以选择其他要激活的规则。", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle": "加载 Elastic 预构建检测规则", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "加载预置检测规则和时间线模板", "xpack.securitySolution.detectionEngine.rules.releaseNotesHelp": "发行说明", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton": "安装 {missingRules} 个 Elastic 预构建{missingRules, plural, =1 {规则} other {规则}}以及 {missingTimelines} 个 Elastic 预构建{missingTimelines, plural, =1 {时间线} other {时间线}} ", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton": "安装 {missingRules} 个 Elastic 预构建{missingRules, plural, =1 {规则} other {规则}} ", @@ -15644,10 +17509,91 @@ "xpack.securitySolution.editDataProvider.selectAnOperatorPlaceholder": "选择运算符", "xpack.securitySolution.editDataProvider.valueLabel": "值", "xpack.securitySolution.editDataProvider.valuePlaceholder": "值", - "xpack.securitySolution.emptyMessage": "Elastic Security 将免费且开放的 Elastic SIEM 和 Elastic Endpoint Security 整合在一起,从而防御、检测并响应威胁。首先,您需要将安全解决方案相关数据添加到 Elastic Stack。有关更多信息,请查看我们的 ", + "xpack.securitySolution.emptyMessage": "Elastic Security 将免费开放的 Elastic SIEM 和 Endpoint Security 相集成,以预防、检测并响应威胁。首先,您需要将安全解决方案相关数据添加到 Elastic Stack。有关更多信息,您可以查看我们的 ", "xpack.securitySolution.emptyString.emptyStringDescription": "空字符串", + "xpack.securitySolution.endpoint.details.endpointVersion": "Endpoint 版本", + "xpack.securitySolution.endpoint.details.errorBody": "请退出浮出控件并选择可用主机。", + "xpack.securitySolution.endpoint.details.errorTitle": "找不到主机", + "xpack.securitySolution.endpoint.details.hostname": "主机名", + "xpack.securitySolution.endpoint.details.ipAddress": "IP 地址", + "xpack.securitySolution.endpoint.details.lastSeen": "最后看到时间", + "xpack.securitySolution.endpoint.details.linkToIngestTitle": "重新分配策略", + "xpack.securitySolution.endpoint.details.noPolicyResponse": "没有可用策略响应", + "xpack.securitySolution.endpoint.details.os": "OS", + "xpack.securitySolution.endpoint.details.policy": "集成策略", + "xpack.securitySolution.endpoint.details.policyResponse.configure_dns_events": "配置 DNS 事件", + "xpack.securitySolution.endpoint.details.policyResponse.configure_elasticsearch_connection": "配置 Elastic Search 连接", + "xpack.securitySolution.endpoint.details.policyResponse.configure_file_events": "配置文件事件", + "xpack.securitySolution.endpoint.details.policyResponse.configure_imageload_events": "配置映像加载事件", + "xpack.securitySolution.endpoint.details.policyResponse.configure_kernel": "配置内核", + "xpack.securitySolution.endpoint.details.policyResponse.configure_logging": "配置日志记录", + "xpack.securitySolution.endpoint.details.policyResponse.configure_malware": "配置恶意软件", + "xpack.securitySolution.endpoint.details.policyResponse.configure_network_events": "配置网络事件", + "xpack.securitySolution.endpoint.details.policyResponse.configure_process_events": "配置进程事件", + "xpack.securitySolution.endpoint.details.policyResponse.configure_registry_events": "配置注册表事件", + "xpack.securitySolution.endpoint.details.policyResponse.configure_security_events": "配置安全事件", + "xpack.securitySolution.endpoint.details.policyResponse.connect_kernel": "连接内核", + "xpack.securitySolution.endpoint.details.policyResponse.detect_async_image_load_events": "检测异步映像加载事件", + "xpack.securitySolution.endpoint.details.policyResponse.detect_file_open_events": "检测文件打开事件", + "xpack.securitySolution.endpoint.details.policyResponse.detect_file_write_events": "检测文件写入事件", + "xpack.securitySolution.endpoint.details.policyResponse.detect_network_events": "检测网络事件", + "xpack.securitySolution.endpoint.details.policyResponse.detect_process_events": "检测进程事件", + "xpack.securitySolution.endpoint.details.policyResponse.detect_registry_events": "检测注册表事件", + "xpack.securitySolution.endpoint.details.policyResponse.detect_sync_image_load_events": "检测同步映像加载事件", + "xpack.securitySolution.endpoint.details.policyResponse.download_global_artifacts": "下载全局项目", + "xpack.securitySolution.endpoint.details.policyResponse.download_user_artifacts": "下载用户项目", + "xpack.securitySolution.endpoint.details.policyResponse.events": "事件", + "xpack.securitySolution.endpoint.details.policyResponse.failed": "失败", + "xpack.securitySolution.endpoint.details.policyResponse.load_config": "加载配置", + "xpack.securitySolution.endpoint.details.policyResponse.load_malware_model": "加载恶意软件模型", + "xpack.securitySolution.endpoint.details.policyResponse.logging": "日志记录", + "xpack.securitySolution.endpoint.details.policyResponse.malware": "恶意软件", + "xpack.securitySolution.endpoint.details.policyResponse.read_elasticsearch_config": "读取 ElasticSearch 配置", + "xpack.securitySolution.endpoint.details.policyResponse.read_events_config": "读取事件配置", + "xpack.securitySolution.endpoint.details.policyResponse.read_kernel_config": "读取内核配置", + "xpack.securitySolution.endpoint.details.policyResponse.read_logging_config": "读取日志记录配置", + "xpack.securitySolution.endpoint.details.policyResponse.read_malware_config": "读取恶意软件配置", + "xpack.securitySolution.endpoint.details.policyResponse.streaming": "流式传输", + "xpack.securitySolution.endpoint.details.policyResponse.success": "成功", + "xpack.securitySolution.endpoint.details.policyResponse.warning": "警告", + "xpack.securitySolution.endpoint.details.policyResponse.workflow": "工作流", + "xpack.securitySolution.endpoint.details.policyStatus": "策略响应", + "xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失败} other {未知}}", + "xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "我们将使用建议的默认值保存您的集成。稍后,您可以通过在代理策略中编辑 Endpoint Security 集成对其进行更改。", + "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.actionSecurityPolicy": "编辑安全策略", + "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.actionTrustedApps": "查看受信任的应用程序", + "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.menuButton": "操作", + "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.message": "通过从菜单中选择操作可找到更多高级配置选项", + "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.trustedAppsMessageReturnBackLabel": "返回以编辑集成", "xpack.securitySolution.endpoint.ingestToastMessage": "采集管理器在其设置期间失败。", "xpack.securitySolution.endpoint.ingestToastTitle": "应用无法初始化", + "xpack.securitySolution.endpoint.list.actionmenu": "打开", + "xpack.securitySolution.endpoint.list.actions": "操作", + "xpack.securitySolution.endpoint.list.actions.agentDetails": "查看代理详情", + "xpack.securitySolution.endpoint.list.actions.agentPolicy": "查看代理策略", + "xpack.securitySolution.endpoint.list.actions.hostDetails": "查看主机详情", + "xpack.securitySolution.endpoint.list.endpointsEnrolling": "正在注册终端。{agentsLink}以跟踪进度。", + "xpack.securitySolution.endpoint.list.endpointsEnrolling.viewAgentsLink": "查看代理", + "xpack.securitySolution.endpoint.list.endpointVersion": "版本", + "xpack.securitySolution.endpoint.list.hostname": "主机名", + "xpack.securitySolution.endpoint.list.hostStatus": "代理状态", + "xpack.securitySolution.endpoint.list.hostStatusValue": "{hostStatus, select, online {联机} error {错误} unenrolling {正在取消注册} other {脱机}}", + "xpack.securitySolution.endpoint.list.ip": "IP 地址", + "xpack.securitySolution.endpoint.list.lastActive": "上次活动时间", + "xpack.securitySolution.endpoint.list.loadingPolicies": "正在加载集成", + "xpack.securitySolution.endpoint.list.noEndpointsInstructions": "您已添加 Endpoint Security 集成。现在,按照以下步骤注册您的代理。", + "xpack.securitySolution.endpoint.list.noEndpointsPrompt": "下一步:将代理注册到 Endpoint Security", + "xpack.securitySolution.endpoint.list.noPolicies": "没有集成。", + "xpack.securitySolution.endpoint.list.os": "操作系统", + "xpack.securitySolution.endpoint.list.pageSubTitle": "运行 Endpoint Security 的主机", + "xpack.securitySolution.endpoint.list.pageTitle": "终端", + "xpack.securitySolution.endpoint.list.policy": "集成策略", + "xpack.securitySolution.endpoint.list.policyStatus": "策略状态", + "xpack.securitySolution.endpoint.list.stepOne": "从现有的集成中选择。稍后可以对其进行更改。", + "xpack.securitySolution.endpoint.list.stepOneTitle": "选择要使用的集成", + "xpack.securitySolution.endpoint.list.stepTwo": "您将会获得开始使用时所需的命令。", + "xpack.securitySolution.endpoint.list.stepTwoTitle": "通过采集管理器注册启用 Endpoint Security 的代理", + "xpack.securitySolution.endpoint.list.totalCount": "{totalItemCount, plural, one {# 个主机} other {# 个主机}}", "xpack.securitySolution.endpoint.policy.details.backToListTitle": "返回到终端主机", "xpack.securitySolution.endpoint.policy.details.cancel": "取消", "xpack.securitySolution.endpoint.policy.details.detect": "检测", @@ -15667,7 +17613,7 @@ "xpack.securitySolution.endpoint.policy.details.updateConfirm.confirmButtonTitle": "保存并部署更改", "xpack.securitySolution.endpoint.policy.details.updateConfirm.message": "此操作无法撤消。是否确定要继续?", "xpack.securitySolution.endpoint.policy.details.updateConfirm.title": "保存并部署更改", - "xpack.securitySolution.endpoint.policy.details.updateConfirm.warningMessage": "保存这些更改会将更新应用于分配到此代理配置的所有终端。", + "xpack.securitySolution.endpoint.policy.details.updateConfirm.warningMessage": "保存这些更改会将更新应用于分配到此代理策略的所有终端。", "xpack.securitySolution.endpoint.policy.details.updateConfirm.warningTitle": "此操作将更新 {hostCount, plural, one {# 个主机} other {# 个主机}}", "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失败!", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "集成 {name} 已更新。", @@ -15678,7 +17624,7 @@ "xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle": "错误", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle": "脱机", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.onlineTitle": "联机", - "xpack.securitySolution.endpoint.policyDetails.agentsSummary.totalTitle": "主机", + "xpack.securitySolution.endpoint.policyDetails.agentsSummary.totalTitle": "终端", "xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents": "事件", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file": "文件", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network": "网络", @@ -15697,6 +17643,7 @@ "xpack.securitySolution.endpoint.policyDetailType": "类型", "xpack.securitySolution.endpoint.policyList.actionButtonText": "添加 Endpoint Security", "xpack.securitySolution.endpoint.policyList.actionMenu": "打开", + "xpack.securitySolution.endpoint.policyList.agentPolicyAction": "查看代理策略", "xpack.securitySolution.endpoint.policyList.createdAt": "创建日期", "xpack.securitySolution.endpoint.policyList.createdBy": "创建者", "xpack.securitySolution.endpoint.policyList.createNewButton": "创建新策略", @@ -15714,10 +17661,10 @@ "xpack.securitySolution.endpoint.policyList.emptyCreateNewButton": "注册代理", "xpack.securitySolution.endpoint.policyList.nameField": "策略名称", "xpack.securitySolution.endpoint.policyList.onboardingDocsLink": "查看 Security 应用文档", - "xpack.securitySolution.endpoint.policyList.onboardingSectionOne": "Elastic Endpoint Security 使用威胁防御、检测和深度安全数据可见性来保护您的主机。", - "xpack.securitySolution.endpoint.policyList.onboardingSectionThree": "首先,将 Elastic Endpoint Security 集成添加到您的代理。有关更多信息, ", - "xpack.securitySolution.endpoint.policyList.onboardingSectionTwo": "从此页面,您将能够查看环境中运行 Elastic Endpoint Security 的主机。", - "xpack.securitySolution.endpoint.policyList.onboardingTitle": "开始使用 Elastic Endpoint Security", + "xpack.securitySolution.endpoint.policyList.onboardingSectionOne": "Endpoint Security 使用威胁防御、检测和深度安全数据可见性来保护您的主机。", + "xpack.securitySolution.endpoint.policyList.onboardingSectionThree": "首先,将 Endpoint Security 集成添加到您的代理。有关更多信息, ", + "xpack.securitySolution.endpoint.policyList.onboardingSectionTwo": "从此页面,您将可以查看和管理环境中运行 Endpoint Security 的主机。", + "xpack.securitySolution.endpoint.policyList.onboardingTitle": "开始使用 Endpoint Security", "xpack.securitySolution.endpoint.policyList.policyDeleteAction": "删除策略", "xpack.securitySolution.endpoint.policyList.revision": "修订 {revNumber}", "xpack.securitySolution.endpoint.policyList.updatedAt": "最后更新时间", @@ -15725,17 +17672,23 @@ "xpack.securitySolution.endpoint.policyList.versionField": "v{version}", "xpack.securitySolution.endpoint.policyList.versionFieldLabel": "版本", "xpack.securitySolution.endpoint.policyList.viewTitleTotalCount": "{totalItemCount, plural, one {# 个策略} other {# 个策略}}", + "xpack.securitySolution.endpoint.policyResponse.backLinkTitle": "终端详情", + "xpack.securitySolution.endpoint.policyResponse.title": "策略响应", "xpack.securitySolution.endpoint.resolver.eitherLineageLimitExceeded": "下面可视化和事件列表中的一些进程事件无法显示,因为已达到数据限制。", "xpack.securitySolution.endpoint.resolver.elapsedTime": "{duration} {durationType}", "xpack.securitySolution.endpoint.resolver.loadingError": "加载数据时出错。", "xpack.securitySolution.endpoint.resolver.panel.error.error": "错误", "xpack.securitySolution.endpoint.resolver.panel.error.events": "事件", - "xpack.securitySolution.endpoint.resolver.panel.error.goBack": "单击此链接以返回到所有进程的列表。", + "xpack.securitySolution.endpoint.resolver.panel.error.goBack": "查看所有进程", + "xpack.securitySolution.endpoint.resolver.panel.nodeEventsByType.errorPrimary": "无法加载事件。", + "xpack.securitySolution.endpoint.resolver.panel.nodeEventsByType.errorSecondary": "提取事件时发生错误。", + "xpack.securitySolution.endpoint.resolver.panel.nodeEventsByType.loadMore": "加载更多数据", "xpack.securitySolution.endpoint.resolver.panel.processDescList.events": "事件", + "xpack.securitySolution.endpoint.resolver.panel.processDescList.numberOfEvents": "{relatedEventTotal} 个事件", "xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events": "事件", "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events": "事件", "xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb": "{totalCount} 个事件", - "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "等候事件......", + "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "正在加载事件......", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime": "@ {date}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType": "{category} {eventType}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.countByCategory": "{count} 个{category}", @@ -15749,6 +17702,7 @@ "xpack.securitySolution.endpoint.resolver.panel.table.row.processNameTitle": "进程名称", "xpack.securitySolution.endpoint.resolver.panel.table.row.timestampTitle": "时间戳", "xpack.securitySolution.endpoint.resolver.panel.table.row.valueMissingDescription": "值缺失", + "xpack.securitySolution.endpoint.resolver.processDescription": "{isEventBeingAnalyzed, select, true {已分析的事件 · {descriptionText}} false {{descriptionText}}}", "xpack.securitySolution.endpoint.resolver.relatedEventLimitExceeded": "{numberOfEventsMissing} 个{category}事件无法显示,因为已达到数据限制。", "xpack.securitySolution.endpoint.resolver.relatedEventLimitTitle": "此列表包括 {numberOfEntries} 个进程事件。", "xpack.securitySolution.endpoint.resolver.relatedEvents": "事件", @@ -15761,9 +17715,11 @@ "xpack.securitySolution.endpoint.resolver.terminatedTrigger": "已终止触发器", "xpack.securitySolution.endpointManagement.noPermissionsSubText": "似乎采集管理器已禁用。必须启用采集管理器,才能使用此功能。如果您无权启用采集管理器,请联系您的 Kibana 管理员。", "xpack.securitySolution.endpointManagemnet.noPermissionsText": "您没有所需的 Kibana 权限,无法使用 Elastic Security 管理", + "xpack.securitySolution.endpointsTab": "终端", "xpack.securitySolution.enpdoint.resolver.panelutils.betaBadgeLabel": "公测版", "xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate": "日期无效", - "xpack.securitySolution.event.module.linkToElasticEndpointSecurityDescription": "在 Elastic Endpoint Security 中打开", + "xpack.securitySolution.enpdoint.resolver.panelutils.noTimestampRetrieved": "未检索时间戳", + "xpack.securitySolution.event.module.linkToElasticEndpointSecurityDescription": "在 Endpoint Security 中打开", "xpack.securitySolution.eventDetails.blank": " ", "xpack.securitySolution.eventDetails.copyToClipboard": "复制到剪贴板", "xpack.securitySolution.eventDetails.copyToClipboardTooltip": "复制到剪贴板", @@ -15817,15 +17773,16 @@ "xpack.securitySolution.eventsViewer.showingLabel": "正在显示", "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, =1 {事件} other {事件}}", "xpack.securitySolution.exceptions.addException.addEndpointException": "添加终端例外", - "xpack.securitySolution.exceptions.addException.addException": "添加例外", - "xpack.securitySolution.exceptions.addException.bulkCloseLabel": "关闭所有与此例外匹配的告警,包括其他规则所生成的告警", - "xpack.securitySolution.exceptions.addException.bulkCloseLabel.disabled": "关闭匹配此例外中的属性的所有告警(不支持列表和非 ECS 字段)", + "xpack.securitySolution.exceptions.addException.addException": "添加规则例外", + "xpack.securitySolution.exceptions.addException.bulkCloseLabel": "关闭所有与此例外匹配且根据此规则生成的告警", + "xpack.securitySolution.exceptions.addException.bulkCloseLabel.disabled": "关闭所有与此例外匹配且根据此规则生成的告警(不支持列表和非 ECS 字段)", "xpack.securitySolution.exceptions.addException.cancel": "取消", "xpack.securitySolution.exceptions.addException.endpointQuarantineText": "在所有终端主机上,与该例外匹配的已隔离文件会自动还原到其原始位置。此例外适用于使用终端例外的所有规则。", "xpack.securitySolution.exceptions.addException.error": "添加例外失败", "xpack.securitySolution.exceptions.addException.fetchError": "提取例外列表时出错", "xpack.securitySolution.exceptions.addException.fetchError.title": "错误", "xpack.securitySolution.exceptions.addException.infoLabel": "满足规则的条件时生成告警,但以下情况除外:", + "xpack.securitySolution.exceptions.addException.sequenceWarning": "此规则的查询包含 EQL 序列语句。创建的例外将应用于序列中的所有事件。", "xpack.securitySolution.exceptions.addException.success": "已成功添加例外", "xpack.securitySolution.exceptions.andDescription": "且", "xpack.securitySolution.exceptions.builder.addNestedDescription": "添加嵌套条件", @@ -15838,29 +17795,37 @@ "xpack.securitySolution.exceptions.builder.fieldDescription": "字段", "xpack.securitySolution.exceptions.builder.operatorDescription": "运算符", "xpack.securitySolution.exceptions.builder.valueDescription": "值", + "xpack.securitySolution.exceptions.cancelLabel": "取消", + "xpack.securitySolution.exceptions.clearExceptionsLabel": "移除例外列表", "xpack.securitySolution.exceptions.commentEventLabel": "已添加注释", "xpack.securitySolution.exceptions.commentLabel": "注释", "xpack.securitySolution.exceptions.createdByLabel": "创建者", "xpack.securitySolution.exceptions.dateCreatedLabel": "创建日期", "xpack.securitySolution.exceptions.descriptionLabel": "描述", "xpack.securitySolution.exceptions.detectionListLabel": "检测列表", + "xpack.securitySolution.exceptions.dissasociateExceptionListError": "无法移除例外列表", + "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外列表 ({id}) 已成功移除", "xpack.securitySolution.exceptions.doesNotExistOperatorLabel": "不存在", "xpack.securitySolution.exceptions.editButtonLabel": "编辑", - "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "关闭所有与此例外匹配的告警,包括其他规则所生成的告警", - "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "关闭匹配此例外中的属性的所有告警(不支持列表和非 ECS 字段)", + "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "关闭所有与此例外匹配且根据此规则生成的告警", + "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "关闭所有与此例外匹配且根据此规则生成的告警(不支持列表和非 ECS 字段)", "xpack.securitySolution.exceptions.editException.cancel": "取消", "xpack.securitySolution.exceptions.editException.editEndpointExceptionTitle": "编辑终端例外", "xpack.securitySolution.exceptions.editException.editExceptionSaveButton": "保存", - "xpack.securitySolution.exceptions.editException.editExceptionTitle": "编辑例外", + "xpack.securitySolution.exceptions.editException.editExceptionTitle": "编辑规则例外", "xpack.securitySolution.exceptions.editException.endpointQuarantineText": "在所有终端主机上,与该例外匹配的已隔离文件会自动还原到其原始位置。此例外适用于使用终端例外的所有规则。", "xpack.securitySolution.exceptions.editException.error": "更新例外失败", "xpack.securitySolution.exceptions.editException.infoLabel": "满足规则的条件时生成告警,但以下情况除外:", + "xpack.securitySolution.exceptions.editException.sequenceWarning": "此规则的查询包含 EQL 序列语句。修改的例外将应用于序列中的所有事件。", "xpack.securitySolution.exceptions.editException.success": "已成功更新例外", "xpack.securitySolution.exceptions.editException.versionConflictDescription": "此例外可能自您首次选择编辑后已更新。尝试单击“取消”,重新编辑该例外。", "xpack.securitySolution.exceptions.editException.versionConflictTitle": "抱歉,有错误", "xpack.securitySolution.exceptions.endpointListLabel": "终端列表", + "xpack.securitySolution.exceptions.errorLabel": "错误", "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "每页项数:{items}", "xpack.securitySolution.exceptions.existsOperatorLabel": "存在", + "xpack.securitySolution.exceptions.fetch404Error": "关联的例外列表 ({listId}) 已不存在。请移除缺少的例外列表,以将其他例外添加到检测规则。", + "xpack.securitySolution.exceptions.fetchError": "提取例外列表时出错", "xpack.securitySolution.exceptions.fieldDescription": "字段", "xpack.securitySolution.exceptions.hideCommentsLabel": "隐藏 ({comments}) 个{comments, plural, =1 {注释} other {注释}}", "xpack.securitySolution.exceptions.isInListOperatorLabel": "在列表中", @@ -15869,6 +17834,7 @@ "xpack.securitySolution.exceptions.isNotOperatorLabel": "不是", "xpack.securitySolution.exceptions.isOneOfOperatorLabel": "属于", "xpack.securitySolution.exceptions.isOperatorLabel": "是", + "xpack.securitySolution.exceptions.modalErrorAccordionText": "显示规则引用信息:", "xpack.securitySolution.exceptions.operatingSystemLabel": "OS", "xpack.securitySolution.exceptions.operatorDescription": "运算符", "xpack.securitySolution.exceptions.orDescription": "或", @@ -15895,6 +17861,11 @@ "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "找不到搜索结果。", "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "搜索字段(例如:host.name)", "xpack.securitySolution.exitFullScreenButton": "退出全屏", + "xpack.securitySolution.featureCatalogue.subtitle": "SIEM 和 Endpoint Security", + "xpack.securitySolution.featureCatalogueDescription": "预防、收集、检测和响应威胁,以对整个基础架构提供统一的保护。", + "xpack.securitySolution.featureCatalogueDescription1": "自主预防威胁。", + "xpack.securitySolution.featureCatalogueDescription2": "检测和响应。", + "xpack.securitySolution.featureCatalogueDescription3": "调查事件。", "xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle": "安全", "xpack.securitySolution.fieldBrowser.categoriesCountTitle": "{totalCount} 个{totalCount, plural, =1 {类别} other {类别}}", "xpack.securitySolution.fieldBrowser.categoriesTitle": "类别", @@ -15912,6 +17883,8 @@ "xpack.securitySolution.fieldBrowser.toggleColumnTooltip": "切换列", "xpack.securitySolution.fieldBrowser.viewCategoryTooltip": "查看所有 {categoryId} 字段", "xpack.securitySolution.fieldRenderers.moreLabel": "更多", + "xpack.securitySolution.firstLastSeenHost.errorSearchDescription": "搜索上次看到的首个主机时发生错误", + "xpack.securitySolution.firstLastSeenHost.failSearchDescription": "无法对上次看到的首个主机执行搜索", "xpack.securitySolution.flyout.button.text": "时间线", "xpack.securitySolution.flyout.button.timeline": "时间线", "xpack.securitySolution.footer.autoRefreshActiveDescription": "自动刷新已启用", @@ -15923,7 +17896,7 @@ "xpack.securitySolution.footer.loadingTimelineData": "正在加载时间线数据", "xpack.securitySolution.footer.of": "/", "xpack.securitySolution.footer.rows": "行", - "xpack.securitySolution.footer.totalCountOfEvents": "个事件匹配搜索条件", + "xpack.securitySolution.footer.totalCountOfEvents": "事件", "xpack.securitySolution.footer.updated": "已更新", "xpack.securitySolution.formatted.duration.aFewMillisecondsTooltip": "几毫秒", "xpack.securitySolution.formatted.duration.aFewNanosecondsTooltip": "几纳秒", @@ -15962,6 +17935,8 @@ "xpack.securitySolution.host.details.overview.platformTitle": "平台", "xpack.securitySolution.host.details.overview.regionTitle": "地区", "xpack.securitySolution.host.details.versionLabel": "版本", + "xpack.securitySolution.hostOverview.errorSearchDescription": "搜索主机概览时发生错误", + "xpack.securitySolution.hostOverview.failSearchDescription": "无法对主机概览执行搜索", "xpack.securitySolution.hosts.kqlPlaceholder": "例如 host.name:“foo”", "xpack.securitySolution.hosts.navigation.alertsTitle": "外部告警", "xpack.securitySolution.hosts.navigation.allHostsTitle": "所有主机", @@ -15973,6 +17948,12 @@ "xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingAuthenticationsData": "无法查询身份验证数据", "xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingEventsData": "无法查询事件数据", "xpack.securitySolution.hosts.pageTitle": "主机", + "xpack.securitySolution.hostsKpiAuthentications.errorSearchDescription": "搜索主机 KPI 身份验证时发生错误", + "xpack.securitySolution.hostsKpiAuthentications.failSearchDescription": "无法对主机 KPI 身份验证执行搜索", + "xpack.securitySolution.hostsKpiHosts.errorSearchDescription": "搜索主机 KPI 主机时发生错误", + "xpack.securitySolution.hostsKpiHosts.failSearchDescription": "无法对主机 KPI 主机执行搜索", + "xpack.securitySolution.hostsKpiUniqueIps.errorSearchDescription": "搜索主机 KPI 唯一 IP 时发生错误", + "xpack.securitySolution.hostsKpiUniqueIps.failSearchDescription": "无法对主机 KPI 唯一 IP 执行搜索", "xpack.securitySolution.hostsTable.firstLastSeenToolTip": "相对于选定日期范围", "xpack.securitySolution.hostsTable.hostsTitle": "所有主机", "xpack.securitySolution.hostsTable.lastSeenTitle": "最后看到时间", @@ -15981,6 +17962,14 @@ "xpack.securitySolution.hostsTable.rows": "{numRows} {numRows, plural, =0 {行} =1 {行} other {行}}", "xpack.securitySolution.hostsTable.unit": "{totalCount, plural, =1 {主机} other {主机}}", "xpack.securitySolution.hostsTable.versionTitle": "版本", + "xpack.securitySolution.indexPatterns.allDefault": "所有默认值", + "xpack.securitySolution.indexPatterns.dataSourcesLabel": "数据源", + "xpack.securitySolution.indexPatterns.disabled": "在此页面上建议使用已禁用的索引模式,但是首先需要在 Kibana 索引模式设置中配置这些模式", + "xpack.securitySolution.indexPatterns.help": "数据源的选择", + "xpack.securitySolution.indexPatterns.pickIndexPatternsCombo": "选取索引模式", + "xpack.securitySolution.indexPatterns.resetButton": "重置", + "xpack.securitySolution.indexPatterns.save": "保存", + "xpack.securitySolution.indexPatterns.selectionLabel": "在此页面上选择数据源。", "xpack.securitySolution.insert.timeline.insertTimelineButton": "插入时间线链接", "xpack.securitySolution.inspect.modal.closeTitle": "关闭", "xpack.securitySolution.inspect.modal.indexPatternDescription": "连接到 Elasticsearch 索引的索引模式。可以在“Kibana”>“高级设置”中配置这些索引。", @@ -16013,6 +18002,8 @@ "xpack.securitySolution.kpiNetwork.uniquePrivateIps.sourceChartLabel": "源", "xpack.securitySolution.kpiNetwork.uniquePrivateIps.sourceUnitLabel": "源", "xpack.securitySolution.kpiNetwork.uniquePrivateIps.title": "唯一专用 IP", + "xpack.securitySolution.lastEventTime.errorSearchDescription": "搜索上次事件时间时发生错误", + "xpack.securitySolution.lastEventTime.failSearchDescription": "无法对上次事件时间执行搜索", "xpack.securitySolution.licensing.unsupportedMachineLearningMessage": "您的许可证不支持 Machine Learning。请升级您的许可证。", "xpack.securitySolution.lists.cancelValueListsUploadTitle": "取消上传", "xpack.securitySolution.lists.closeValueListsModalTitle": "关闭", @@ -16036,6 +18027,7 @@ "xpack.securitySolution.lists.valueListsTable.exportActionName": "导出", "xpack.securitySolution.lists.valueListsTable.fileNameColumn": "文件名", "xpack.securitySolution.lists.valueListsTable.title": "值列表", + "xpack.securitySolution.lists.valueListsTable.typeColumn": "类型", "xpack.securitySolution.lists.valueListsTable.uploadDateColumn": "上传数据", "xpack.securitySolution.lists.valueListsUploadButton": "上传列表", "xpack.securitySolution.lists.valueListsUploadError": "上传值列表时出错。", @@ -16054,7 +18046,15 @@ "xpack.securitySolution.markdown.toolTip.timelineId": "时间线 id:{ timelineId }", "xpack.securitySolution.markdownEditor.markdown": "Markdown", "xpack.securitySolution.markdownEditor.markdownInputHelp": "Markdown 语法帮助", + "xpack.securitySolution.markdownEditor.plugins.timeline.insertTimelineButtonLabel": "插入时间线链接", + "xpack.securitySolution.markdownEditor.plugins.timeline.noParenthesesErrorMsg": "应为左括号", + "xpack.securitySolution.markdownEditor.plugins.timeline.noTimelineIdFoundErrorMsg": "找不到时间线 ID", + "xpack.securitySolution.markdownEditor.plugins.timeline.noTimelineNameFoundErrorMsg": "找不到时间线名称", + "xpack.securitySolution.markdownEditor.plugins.timeline.toolTip.timelineId": "时间线 ID:{ timelineId }", + "xpack.securitySolution.markdownEditor.plugins.timeline.toolTip.timelineUrlIsNotValidErrorMsg": "时间线 URL 无效 => {timelineUrl}", "xpack.securitySolution.markdownEditor.preview": "预览", + "xpack.securitySolution.matrixHistogram.errorSearchDescription": "搜索矩阵直方图时发生错误", + "xpack.securitySolution.matrixHistogram.failSearchDescription": "无法对矩阵直方图执行搜索", "xpack.securitySolution.ml.score.anomalousEntityTitle": "异常实体", "xpack.securitySolution.ml.score.anomalyJobTitle": "作业", "xpack.securitySolution.ml.score.detectedTitle": "已检测到", @@ -16120,6 +18120,10 @@ "xpack.securitySolution.network.navigation.httpTitle": "HTTP", "xpack.securitySolution.network.navigation.tlsTitle": "TLS", "xpack.securitySolution.network.pageTitle": "网络", + "xpack.securitySolution.networkDetails.errorSearchDescription": "搜索网络详情时发生错误", + "xpack.securitySolution.networkDetails.failSearchDescription": "无法对网络详情执行搜索", + "xpack.securitySolution.networkDns.errorSearchDescription": "搜索网络 DNS 时发生错误", + "xpack.securitySolution.networkDns.failSearchDescription": "无法对网络 DNS 执行搜索", "xpack.securitySolution.networkDnsTable.column.bytesInTitle": "DNS 传入字节", "xpack.securitySolution.networkDnsTable.column.bytesOutTitle": "DNS 传出字节", "xpack.securitySolution.networkDnsTable.column.registeredDomain": "已注册域", @@ -16130,6 +18134,8 @@ "xpack.securitySolution.networkDnsTable.select.includePtrRecords": "包括 PTR 记录", "xpack.securitySolution.networkDnsTable.title": "排名靠前的 DNS 域", "xpack.securitySolution.networkDnsTable.unit": "{totalCount, plural, =1 {域} other {域}}", + "xpack.securitySolution.networkHttp.errorSearchDescription": "搜索网络 HTTP 时发生错误", + "xpack.securitySolution.networkHttp.failSearchDescription": "无法对网络 HTTP 执行搜索", "xpack.securitySolution.networkHttpTable.column.domainTitle": "域", "xpack.securitySolution.networkHttpTable.column.lastHostTitle": "上一主机", "xpack.securitySolution.networkHttpTable.column.lastSourceIpTitle": "上一源 IP", @@ -16140,6 +18146,20 @@ "xpack.securitySolution.networkHttpTable.rows": "{numRows} {numRows, plural, =0 {行} =1 {行} other {行}}", "xpack.securitySolution.networkHttpTable.title": "HTTP 请求", "xpack.securitySolution.networkHttpTable.unit": "{totalCount, plural, =1 {请求} other {请求}}", + "xpack.securitySolution.networkKpiDns.errorSearchDescription": "搜索网络 KPI DNS 时发生错误", + "xpack.securitySolution.networkKpiDns.failSearchDescription": "无法对网络 KPI DNS 执行搜索", + "xpack.securitySolution.networkKpiNetworkEvents.errorSearchDescription": "搜索网络 KPI 网络事件时发生错误", + "xpack.securitySolution.networkKpiNetworkEvents.failSearchDescription": "无法对网络 KPI 网络事件执行搜索", + "xpack.securitySolution.networkKpiTlsHandshakes.errorSearchDescription": "搜索网络 KPI TLS 握手时发生错误", + "xpack.securitySolution.networkKpiTlsHandshakes.failSearchDescription": "无法对网络 KPI TLS 握手执行搜索", + "xpack.securitySolution.networkKpiUniqueFlows.errorSearchDescription": "搜索网络 KPI 唯一流时发生错误", + "xpack.securitySolution.networkKpiUniqueFlows.failSearchDescription": "无法对网络 KPI 唯一流执行搜索", + "xpack.securitySolution.networkKpiUniquePrivateIps.errorSearchDescription": "搜索网络 KPI 唯一专用 IP 时发生错误", + "xpack.securitySolution.networkKpiUniquePrivateIps.failSearchDescription": "无法对网络 KPI 唯一专用 IP 执行搜索", + "xpack.securitySolution.networkTls.errorSearchDescription": "搜索网络 TLS 时发生错误", + "xpack.securitySolution.networkTls.failSearchDescription": "无法对网络 TLS 执行搜索", + "xpack.securitySolution.networkTopCountries.errorSearchDescription": "搜索网络热门国家/地区时发生错误", + "xpack.securitySolution.networkTopCountries.failSearchDescription": "无法对网络热门国家/地区执行搜索", "xpack.securitySolution.networkTopCountriesTable.column.bytesInTitle": "传入字节", "xpack.securitySolution.networkTopCountriesTable.column.bytesOutTitle": "传出字节", "xpack.securitySolution.networkTopCountriesTable.column.countryTitle": "国家/地区", @@ -16150,6 +18170,8 @@ "xpack.securitySolution.networkTopCountriesTable.heading.sourceCountries": "源国家/地区", "xpack.securitySolution.networkTopCountriesTable.heading.unit": "{totalCount, plural, =1 {国家或地区} other {国家或地区}}", "xpack.securitySolution.networkTopCountriesTable.rows": "{numRows} {numRows, plural, =0 {行} =1 {行} other {行}}", + "xpack.securitySolution.networkTopNFlow.errorSearchDescription": "搜索网络排名前 n 个流时发生错误", + "xpack.securitySolution.networkTopNFlow.failSearchDescription": "无法对网络排名前 n 个流执行搜索", "xpack.securitySolution.networkTopNFlowTable.column.asTitle": "自治系统", "xpack.securitySolution.networkTopNFlowTable.column.bytesInTitle": "传入字节", "xpack.securitySolution.networkTopNFlowTable.column.bytesOutTitle": "传出字节", @@ -16162,8 +18184,11 @@ "xpack.securitySolution.networkTopNFlowTable.rows": "{numRows} {numRows, plural, =0 {行} =1 {行} other {行}}", "xpack.securitySolution.networkTopNFlowTable.sourceIps": "源 IP", "xpack.securitySolution.networkTopNFlowTable.unit": "{totalCount, plural, =1 {IP} other {IP}}", + "xpack.securitySolution.networkUsers.errorSearchDescription": "搜索网络用户时发生错误", + "xpack.securitySolution.networkUsers.failSearchDescription": "无法对网络用户执行搜索", "xpack.securitySolution.newsFeed.advancedSettingsLinkTitle": "SIEM 高级设置", - "xpack.securitySolution.newsFeed.noNewsMessage": "您当前的新闻源 URL 未返回最近的新闻。要更新 URL 或禁用安全新闻,您可以通过", + "xpack.securitySolution.newsFeed.noNewsMessage": "您当前的新闻源 URL 未返回最近的新闻。", + "xpack.securitySolution.newsFeed.noNewsMessageForAdmin": "您当前的新闻源 URL 未返回最近的新闻。要更新 URL 或禁用安全新闻,您可以通过", "xpack.securitySolution.notes.addANotePlaceholder": "添加备注", "xpack.securitySolution.notes.addedANoteLabel": "已添加备注", "xpack.securitySolution.notes.addNoteButtonLabel": "添加备注", @@ -16214,6 +18239,7 @@ "xpack.securitySolution.open.timeline.singleTemplateLabel": "模板", "xpack.securitySolution.open.timeline.singleTimelineLabel": "时间线", "xpack.securitySolution.open.timeline.successfullyExportedTimelinesTitle": "已成功导出{totalTimelines, plural, =0 {全部时间线} =1 { {totalTimelines} 条时间线} other { {totalTimelines} 条时间线}}", + "xpack.securitySolution.open.timeline.successfullyExportedTimelineTemplatesTitle": "已成功导出 {totalTimelineTemplates, plural, =0 {所有时间线} =1 {{totalTimelineTemplates} 个时间线模板} other {{totalTimelineTemplates} 个时间线模板}}", "xpack.securitySolution.open.timeline.timelineNameTableHeader": "时间线名称", "xpack.securitySolution.open.timeline.timelineTemplateNameTableHeader": "模板名称", "xpack.securitySolution.open.timeline.untitledTimelineLabel": "未命名时间线", @@ -16236,8 +18262,8 @@ "xpack.securitySolution.overview.endpointNotice.dismiss": "关闭消息", "xpack.securitySolution.overview.endpointNotice.introducing": "即将引入: ", "xpack.securitySolution.overview.endpointNotice.message": "使用威胁防御、检测和深度安全数据可见性来保护您的主机。", - "xpack.securitySolution.overview.endpointNotice.title": "Elastic Endpoint Security(公测版)", - "xpack.securitySolution.overview.endpointNotice.tryButton": "试用 Elastic Endpoint Security(公测版)", + "xpack.securitySolution.overview.endpointNotice.title": "Endpoint Security(公测版)", + "xpack.securitySolution.overview.endpointNotice.tryButton": "试用 Endpoint Security(公测版)", "xpack.securitySolution.overview.eventsTitle": "事件计数", "xpack.securitySolution.overview.feedbackText": "如果您对 Elastic SIEM 体验有任何建议,请随时{feedback}。", "xpack.securitySolution.overview.feedbackText.feedbackLinkText": "在线提交反馈", @@ -16250,7 +18276,7 @@ "xpack.securitySolution.overview.fileBeatZeekTitle": "Zeek", "xpack.securitySolution.overview.hostsAction": "查看主机", "xpack.securitySolution.overview.hostStatGroupAuditbeat": "Auditbeat", - "xpack.securitySolution.overview.hostStatGroupElasticEndpointSecurity": "Elastic Endpoint Security", + "xpack.securitySolution.overview.hostStatGroupElasticEndpointSecurity": "Endpoint Security", "xpack.securitySolution.overview.hostStatGroupFilebeat": "Filebeat", "xpack.securitySolution.overview.hostStatGroupWinlogbeat": "Winlogbeat", "xpack.securitySolution.overview.hostsTitle": "主机事件", @@ -16283,14 +18309,18 @@ "xpack.securitySolution.overview.viewEventsButtonLabel": "查看事件", "xpack.securitySolution.overview.winlogbeatMWSysmonOperational": "Microsoft-Windows-Sysmon/Operational", "xpack.securitySolution.overview.winlogbeatSecurityTitle": "安全", + "xpack.securitySolution.overviewHost.errorSearchDescription": "搜索主机概览时发生错误", + "xpack.securitySolution.overviewHost.failSearchDescription": "无法对主机概览执行搜索", "xpack.securitySolution.pages.common.emptyActionBeats": "使用 Beats 添加数据", "xpack.securitySolution.pages.common.emptyActionBeatsDescription": "轻量型 Beats 可以发送来自成百上千的机器和系统中的数据", "xpack.securitySolution.pages.common.emptyActionElasticAgent": "使用 Elastic 代理添加数据", "xpack.securitySolution.pages.common.emptyActionElasticAgentDescription": "通过 Elastic 代理,可以简单统一的方式将监测添加到主机。", - "xpack.securitySolution.pages.common.emptyActionEndpoint": "添加 Elastic Endpoint Security", + "xpack.securitySolution.pages.common.emptyActionEndpoint": "添加 Endpoint Security", "xpack.securitySolution.pages.common.emptyActionEndpointDescription": "使用威胁防御、检测和深度安全数据可见性功能保护您的主机。", "xpack.securitySolution.pages.common.emptyActionSecondary": "入门指南。", "xpack.securitySolution.pages.common.emptyTitle": "欢迎使用 Elastic Security。让我们帮您如何入门。", + "xpack.securitySolution.pages.common.updateAlertStatusFailed": "无法更新{ conflicts } 个{conflicts, plural, =1 {告警} other {告警}}。", + "xpack.securitySolution.pages.common.updateAlertStatusFailedDetailed": "{ updated } 个{updated, plural, =1 {告警} other {告警}}已成功更新,但是 { conflicts } 个无法更新,\n 因为{ conflicts, plural, =1 {其} other {其}}已被修改。", "xpack.securitySolution.pages.fourohfour.noContentFoundDescription": "未找到任何内容", "xpack.securitySolution.paginatedTable.rowsButtonLabel": "每页行数", "xpack.securitySolution.paginatedTable.showingSubtitle": "正在显示", @@ -16317,8 +18347,22 @@ "xpack.securitySolution.recentTimelines.pinnedEventsTooltip": "置顶事件", "xpack.securitySolution.recentTimelines.untitledTimelineLabel": "未命名时间线", "xpack.securitySolution.recentTimelines.viewAllTimelinesLink": "查看所有时间线", + "xpack.securitySolution.resolver.eventDescription.dnsQuestionNameLabel": "{ dnsQuestionName }", + "xpack.securitySolution.resolver.eventDescription.entityIDLabel": "{ entityID }", + "xpack.securitySolution.resolver.eventDescription.fileEventLabel": "{ filePath }", + "xpack.securitySolution.resolver.eventDescription.legacyEventLabel": "{ processName }", + "xpack.securitySolution.resolver.eventDescription.networkEventLabel": "{ networkDirection } { forwardedIP }", + "xpack.securitySolution.resolver.eventDescription.registryKeyLabel": "{ registryKey }", + "xpack.securitySolution.resolver.eventDescription.registryPathLabel": "{ registryPath }", + "xpack.securitySolution.resolver.node_icon": "{running, select, true {正在运行的进程} false {已终止的进程}}", + "xpack.securitySolution.resolver.panel.copyToClipboard": "复制到剪贴板", + "xpack.securitySolution.resolver.panel.eventDetail.requestError": "无法检索事件详情", + "xpack.securitySolution.resolver.panel.nodeList.title": "所有进程事件", + "xpack.securitySolution.resolver.panel.table.row.analyzedEvent": "已分析的事件", "xpack.securitySolution.security.title": "安全", "xpack.securitySolution.source.destination.packetsLabel": "pkts", + "xpack.securitySolution.stepDefineRule.previewQueryAriaLabel": "查询预览时间范围选择", + "xpack.securitySolution.stepDefineRule.previewQueryLabel": "预览结果", "xpack.securitySolution.system.acceptedAConnectionViaDescription": "已接受连接 - 通过", "xpack.securitySolution.system.acceptedDescription": "已接受该用户 - 通过", "xpack.securitySolution.system.attemptedLoginDescription": "已尝试登录 - 通过", @@ -16354,6 +18398,12 @@ "xpack.securitySolution.system.withExitCodeDescription": "退出代码为", "xpack.securitySolution.system.withResultDescription": ",结果为", "xpack.securitySolution.tables.rowItemHelper.moreDescription": "未显示", + "xpack.securitySolution.threatMatch.andDescription": "和", + "xpack.securitySolution.threatMatch.fieldDescription": "字段", + "xpack.securitySolution.threatMatch.fieldPlaceholderDescription": "搜索", + "xpack.securitySolution.threatMatch.matchesLabel": "匹配", + "xpack.securitySolution.threatMatch.orDescription": "或", + "xpack.securitySolution.threatMatch.threatFieldDescription": "威胁索引字段", "xpack.securitySolution.timeline.autosave.warning.description": "其他用户已更改此时间线。您所做的任何更改不会自动保存,直至您刷新了此时间线以吸收这些更改。", "xpack.securitySolution.timeline.autosave.warning.refresh.title": "刷新时间线", "xpack.securitySolution.timeline.autosave.warning.title": "刷新后才会启用自动保存", @@ -16415,7 +18465,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": "附加到新案例", @@ -16450,14 +18499,22 @@ "xpack.securitySolution.timeline.rangePicker.oneWeek": "1 周", "xpack.securitySolution.timeline.rangePicker.oneYear": "1 年", "xpack.securitySolution.timeline.searchBoxPlaceholder": "例如 {timeline} 名称或描述", - "xpack.securitySolution.timeline.searchOrFilter.eventTypeAllEvent": "全部", + "xpack.securitySolution.timeline.searchOrFilter.customeIndexNames": "定制", + "xpack.securitySolution.timeline.searchOrFilter.eventTypeAllEvent": "所有数据源", "xpack.securitySolution.timeline.searchOrFilter.eventTypeDetectionAlertsEvent": "检测告警", - "xpack.securitySolution.timeline.searchOrFilter.eventTypeRawEvent": "原始事件", + "xpack.securitySolution.timeline.searchOrFilter.eventTypeRawEvent": "事件", "xpack.securitySolution.timeline.searchOrFilter.filterDescription": "上述数据提供程序的事件按相邻 KQL 进行筛选", "xpack.securitySolution.timeline.searchOrFilter.filterKqlPlaceholder": "筛选事件", "xpack.securitySolution.timeline.searchOrFilter.filterKqlSelectedText": "筛选", "xpack.securitySolution.timeline.searchOrFilter.filterKqlTooltip": "上述数据提供程序的事件按此 KQL 进行筛选", "xpack.securitySolution.timeline.searchOrFilter.filterOrSearchWithKql": "使用 KQL 筛选或搜索", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.configure": "查看与以上每项所选内容关联的数据源", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.help": "数据源的选择", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.hideAdvancedSettings": "隐藏“高级”", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.pickIndexPatternsCombo": "选取索引模式", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.resetSettings": "重置", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.save": "保存", + "xpack.securitySolution.timeline.searchOrFilter.indexPatterns.showAdvancedSettings": "显示“高级”", "xpack.securitySolution.timeline.searchOrFilter.searchDescription": "上述数据提供程序的事件与相邻 KQL 的结果进行组合", "xpack.securitySolution.timeline.searchOrFilter.searchKqlPlaceholder": "搜索事件", "xpack.securitySolution.timeline.searchOrFilter.searchKqlSelectedText": "搜索", @@ -16465,6 +18522,8 @@ "xpack.securitySolution.timeline.source": "源", "xpack.securitySolution.timeline.tcp": "TCP", "xpack.securitySolution.timeline.typeTooltip": "类型", + "xpack.securitySolution.timelineEvents.errorSearchDescription": "搜索时间线事件时发生错误", + "xpack.securitySolution.timelineEvents.failSearchDescription": "无法对时间线事件执行搜索", "xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "无法查询所有时间线数据", "xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "导入时间线", "xpack.securitySolution.timelines.allTimelines.panelTitle": "所有时间线", @@ -16483,10 +18542,64 @@ "xpack.securitySolution.timelines.pageTitle": "时间线", "xpack.securitySolution.timelines.updateTimelineErrorText": "出问题了", "xpack.securitySolution.timelines.updateTimelineErrorTitle": "时间线错误", - "xpack.securitySolution.topN.alertEventsSelectLabel": "告警事件", + "xpack.securitySolution.topN.alertEventsSelectLabel": "检测告警", "xpack.securitySolution.topN.allEventsSelectLabel": "所有事件", "xpack.securitySolution.topN.closeButtonLabel": "关闭", "xpack.securitySolution.topN.rawEventsSelectLabel": "原始事件", + "xpack.securitySolution.trustedapps.aboutInfo": "添加受信任的应用程序,以提高性能或缓解与主机上运行的其他应用程序的冲突。受信任的应用程序将应用于运行 Endpoint Security 的主机。", + "xpack.securitySolution.trustedapps.card.operator.includes": "is", + "xpack.securitySolution.trustedapps.card.removeButtonLabel": "移除", + "xpack.securitySolution.trustedapps.create.conditionFieldValueRequiredMsg": "[{row}] 字段条目必须包含值", + "xpack.securitySolution.trustedapps.create.conditionRequiredMsg": "至少需要一个字段定义", + "xpack.securitySolution.trustedapps.create.description": "描述", + "xpack.securitySolution.trustedapps.create.name": "命名受信任的应用程序", + "xpack.securitySolution.trustedapps.create.nameRequiredMsg": "“名称”必填", + "xpack.securitySolution.trustedapps.create.os": "选择操作系统", + "xpack.securitySolution.trustedapps.create.osRequiredMsg": "“操作系统”必填", + "xpack.securitySolution.trustedapps.createTrustedAppFlyout.cancelButton": "取消", + "xpack.securitySolution.trustedapps.createTrustedAppFlyout.saveButton": "添加受信任的应用程序", + "xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle": "“{name}”已添加到受信任的应用程序列表。", + "xpack.securitySolution.trustedapps.createTrustedAppFlyout.title": "添加受信任的应用程序", + "xpack.securitySolution.trustedapps.deletionDialog.cancelButton": "取消", + "xpack.securitySolution.trustedapps.deletionDialog.confirmButton": "移除受信任的应用程序", + "xpack.securitySolution.trustedapps.deletionDialog.mainMessage": "您正在移除受信任的应用程序“{name}”。", + "xpack.securitySolution.trustedapps.deletionDialog.subMessage": "此操作无法撤消。是否确定要继续?", + "xpack.securitySolution.trustedapps.deletionDialog.title": "移除受信任的应用程序", + "xpack.securitySolution.trustedapps.deletionError.text": "无法从受信任的应用程序列表中移除“{name}”。原因:{message}", + "xpack.securitySolution.trustedapps.deletionError.title": "移除失败", + "xpack.securitySolution.trustedapps.deletionSuccess.text": "“{name}”已从受信任的应用程序列表中移除。", + "xpack.securitySolution.trustedapps.deletionSuccess.title": "已成功移除", + "xpack.securitySolution.trustedapps.list.actions.delete": "移除", + "xpack.securitySolution.trustedapps.list.actions.delete.description": "移除此条目", + "xpack.securitySolution.trustedapps.list.addButton": "添加受信任的应用程序", + "xpack.securitySolution.trustedapps.list.backButton": "后退", + "xpack.securitySolution.trustedapps.list.columns.actions": "操作", + "xpack.securitySolution.trustedapps.list.pageTitle": "受信任的应用程序", + "xpack.securitySolution.trustedapps.list.totalCount": "{totalItemCount, plural, one {# 个受信任的应用程序} other {# 个受信任的应用程序}}", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field": "字段", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash": "哈希", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path": "路径", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator": "运算符", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator.is": "is", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "移除条目", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.value": "值", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", + "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件", + "xpack.securitySolution.trustedapps.noResults": "找不到项目", + "xpack.securitySolution.trustedapps.os.linux": "Linux", + "xpack.securitySolution.trustedapps.os.macos": "Mac OS", + "xpack.securitySolution.trustedapps.os.windows": "Windows", + "xpack.securitySolution.trustedapps.trustedapp.createdAt": "创建日期", + "xpack.securitySolution.trustedapps.trustedapp.createdBy": "创建者", + "xpack.securitySolution.trustedapps.trustedapp.description": "描述", + "xpack.securitySolution.trustedapps.trustedapp.entry.field": "字段", + "xpack.securitySolution.trustedapps.trustedapp.entry.operator": "运算符", + "xpack.securitySolution.trustedapps.trustedapp.entry.value": "值", + "xpack.securitySolution.trustedapps.trustedapp.name": "名称", + "xpack.securitySolution.trustedapps.trustedapp.os": "OS", + "xpack.securitySolution.trustedapps.view.toggle.grid": "网格视图", + "xpack.securitySolution.trustedapps.view.toggle.list": "列表视图", + "xpack.securitySolution.trustedAppsTab": "受信任的应用程序", "xpack.securitySolution.uiSettings.defaultAnomalyScoreDescription": "

要在 Security 应用中显示的 Machine Learning 作业异常所需超过的值。

有效值:0 到 100。

", "xpack.securitySolution.uiSettings.defaultAnomalyScoreLabel": "异常阈值", "xpack.securitySolution.uiSettings.defaultIndexDescription": "

Security 应用要从中收集事件的 Elasticsearch 索引逗号分隔列表。

", @@ -16501,6 +18614,8 @@ "xpack.securitySolution.uiSettings.ipReputationLinksDescription": "用于构建要在“IP 详细信息”页面上显示的信誉 URL 列表的 URL 模板数组。", "xpack.securitySolution.uiSettings.newsFeedUrl": "新闻源 URL", "xpack.securitySolution.uiSettings.newsFeedUrlDescription": "

将从此 URL 检索新闻源内容

", + "xpack.securitySolution.uncommonProcesses.errorSearchDescription": "搜索不常见进程时发生错误", + "xpack.securitySolution.uncommonProcesses.failSearchDescription": "无法对不常见进程执行搜索", "xpack.securitySolution.uncommonProcessTable.hostsTitle": "主机名", "xpack.securitySolution.uncommonProcessTable.lastCommandTitle": "上一命令", "xpack.securitySolution.uncommonProcessTable.lastUserTitle": "上一用户", @@ -16603,6 +18718,8 @@ "xpack.snapshotRestore.executeRetention.confirmModal.executeRetentionTitle": "立即运行快照保留?", "xpack.snapshotRestore.executeRetention.errorMessage": "运行保留时出错", "xpack.snapshotRestore.executeRetention.successMessage": "保留正在运行", + "xpack.snapshotRestore.featureCatalogueDescription": "将快照保存到备份存储库,然后还原以恢复索引和集群状态。", + "xpack.snapshotRestore.featureCatalogueTitle": "备份和还原", "xpack.snapshotRestore.home.breadcrumbTitle": "快照存储库", "xpack.snapshotRestore.home.policiesTabTitle": "策略", "xpack.snapshotRestore.home.repositoriesTabTitle": "存储库", @@ -16700,6 +18817,8 @@ "xpack.snapshotRestore.policyForm.stepLogistics.repositoryDescriptionTitle": "存储库", "xpack.snapshotRestore.policyForm.stepLogistics.scheduleDescription": "拍取快照的频率。", "xpack.snapshotRestore.policyForm.stepLogistics.scheduleDescriptionTitle": "计划", + "xpack.snapshotRestore.policyForm.stepLogistics.selectRepository.policyRepositoryNotFoundDescription": "存储库 {repo} 不存在。请选择现有的存储库。", + "xpack.snapshotRestore.policyForm.stepLogistics.selectRepository.policyRepositoryNotFoundTitle": "找不到存储库", "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescription": "快照的名称。唯一标识符将自动添加到每个名称中。", "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescriptionTitle": "快照名称", "xpack.snapshotRestore.policyForm.stepLogisticsTitle": "运筹", @@ -17029,6 +19148,7 @@ "xpack.snapshotRestore.repositoryForm.typeHDFS.uriDescription": "HDFS 的 URI 地址。", "xpack.snapshotRestore.repositoryForm.typeHDFS.uriLabel": "URI(必填)", "xpack.snapshotRestore.repositoryForm.typeHDFS.uriTitle": "URI", + "xpack.snapshotRestore.repositoryForm.typeReadonly.urlAllowedDescription": "必须在 {settingKey} 设置中注册此 URL。", "xpack.snapshotRestore.repositoryForm.typeReadonly.urlDescription": "快照的位置。", "xpack.snapshotRestore.repositoryForm.typeReadonly.urlFilePathDescription": "必须在 {settingKey} 设置中注册此文件位置。", "xpack.snapshotRestore.repositoryForm.typeReadonly.urlLabel": "路径(必填)", @@ -17319,32 +19439,63 @@ "xpack.spaces.management.confirmDeleteModal.deletingSpaceWarningMessage": "删除空间会永久删除空间及其 {allContents}。此操作无法撤消。", "xpack.spaces.management.confirmDeleteModal.redirectAfterDeletingCurrentSpaceWarningMessage": "您即将删除当前空间 {name}。如果继续,系统会将您重定向到选择其他空间的位置。", "xpack.spaces.management.confirmDeleteModal.spaceNamesDoNoMatchErrorMessage": "空间名称不匹配。", - "xpack.spaces.management.copyToSpace.actionDescription": "将此已保存对象复制到一个或多个工作区", + "xpack.spaces.management.copyToSpace.actionDescription": "在一个或多个工作区中创建此已保存对象的副本", "xpack.spaces.management.copyToSpace.actionTitle": "复制到工作区", + "xpack.spaces.management.copyToSpace.cancelButton": "取消", + "xpack.spaces.management.copyToSpace.copyDetail.overwriteSwitch": "覆盖?", + "xpack.spaces.management.copyToSpace.copyDetail.selectControlLabel": "对象 ID", "xpack.spaces.management.copyToSpace.copyErrorTitle": "复制已保存对象时出错", - "xpack.spaces.management.copyToSpace.copyResultsLabel": "复制结果", - "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "已保存对象将被覆盖。单击“跳过”可取消此操作。", - "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "已保存对象成功复制。", - "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "复制此已保存对象时出错。", - "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "在 {space} 工作区中检测到一个或多个冲突。展开此部分以进行解决。", + "xpack.spaces.management.copyToSpace.copyModeControl.copyOptionsTitle": "复制选项", + "xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.disabledText": "检查对象以前是否已复制或导入到工作区中。", + "xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.disabledTitle": "检查现有对象", + "xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.enabledText": "使用此选项可在相同的工作区中创建该对象的一个或多个副本。", + "xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.enabledTitle": "使用随机 ID 创建新对象", + "xpack.spaces.management.copyToSpace.copyModeControl.includeRelated.text": "复制此对象及其相关对象。对于仪表板,还将复制相关可视化、索引模式和已保存的搜索。", + "xpack.spaces.management.copyToSpace.copyModeControl.includeRelated.title": "包括相关对象", + "xpack.spaces.management.copyToSpace.copyModeControl.overwrite.disabledLabel": "冲突时请求操作", + "xpack.spaces.management.copyToSpace.copyModeControl.overwrite.enabledLabel": "自动覆盖冲突", + "xpack.spaces.management.copyToSpace.copyModeControl.relationshipOptionsTitle": "关系", + "xpack.spaces.management.copyToSpace.copyResultsLabel": "结果", + "xpack.spaces.management.copyToSpace.copyStatus.ambiguousConflictMessage": "这与多个现有对象冲突。启用“覆盖”可进行替换。", + "xpack.spaces.management.copyToSpace.copyStatus.conflictMessage": "这与现有对象冲突。启用“覆盖”可进行替换。", + "xpack.spaces.management.copyToSpace.copyStatus.missingReferencesAutomaticOverwriteMessage": "将覆盖对象,但缺少一个或多个引用。", + "xpack.spaces.management.copyToSpace.copyStatus.missingReferencesMessage": "将复制对象,但缺少一个或多个引用。", + "xpack.spaces.management.copyToSpace.copyStatus.missingReferencesOverwriteMessage": "将覆盖对象,但缺少一个或多个引用。禁用“覆盖”可跳过。", + "xpack.spaces.management.copyToSpace.copyStatus.pendingAutomaticOverwriteMessage": "将覆盖对象。", + "xpack.spaces.management.copyToSpace.copyStatus.pendingMessage": "将复制对象。", + "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "将覆盖对象。禁用“覆盖”可跳过。", + "xpack.spaces.management.copyToSpace.copyStatus.successAutomaticOverwriteMessage": "已覆盖对象。", + "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "已复制对象。", + "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "复制此对象时发生错误。", + "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "在 {space} 工作区中检测到冲突。展开此部分可进行解决。", "xpack.spaces.management.copyToSpace.copyStatusSummary.failedMessage": "复制到 {space} 工作区失败。展开此部分以获取详情。", + "xpack.spaces.management.copyToSpace.copyStatusSummary.missingReferencesMessage": "在 {space} 工作区中检测到缺少的引用。展开此部分可获取详情。", "xpack.spaces.management.copyToSpace.copyStatusSummary.successMessage": "已成功复制到 {space} 工作区。", "xpack.spaces.management.copyToSpace.copyToSpacesButton": "复制到 {spaceCount} {spaceCount, plural, one {个工作区} other {个工作区}}", + "xpack.spaces.management.copyToSpace.createNewCopiesLabel": "使用随机 ID 创建新对象", "xpack.spaces.management.copyToSpace.disabledCopyToSpacesButton": "复制", + "xpack.spaces.management.copyToSpace.dontCreateNewCopiesLabel": "检查现有对象", "xpack.spaces.management.copyToSpace.finishCopyToSpacesButton": "完成", "xpack.spaces.management.copyToSpace.finishedButtonLabel": "复制已完成。", - "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "覆盖 {overwriteCount} 个对象", + "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "复制 {overwriteCount} 个对象", "xpack.spaces.management.copyToSpace.includeRelatedLabel": "包括相关已保存对象", "xpack.spaces.management.copyToSpace.inProgressButtonLabel": "复制正在进行中。请稍候。", "xpack.spaces.management.copyToSpace.noSpacesBody": "没有可向其中进行复制的合格工作区。", "xpack.spaces.management.copyToSpace.noSpacesTitle": "没有可用的工作区", + "xpack.spaces.management.copyToSpace.overwriteAllConflictsText": "全部覆盖", + "xpack.spaces.management.copyToSpace.overwriteLabel": "自动覆盖冲突", + "xpack.spaces.management.copyToSpace.resolveAllConflictsLink": "(全部解决)", "xpack.spaces.management.copyToSpace.resolveCopyErrorTitle": "解决已保存对象冲突时出错", - "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "覆盖成功", + "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "复制成功", + "xpack.spaces.management.copyToSpace.selectSpacesControl.disabledTooltip": "该对象已存在于此工作区中。", + "xpack.spaces.management.copyToSpace.selectSpacesLabel": "选择工作区", + "xpack.spaces.management.copyToSpace.skipAllConflictsText": "全部跳过", "xpack.spaces.management.copyToSpace.spacesLoadErrorTitle": "加载可用工作区时出错", "xpack.spaces.management.copyToSpaceFlyoutFooter.errorCount": "错误", "xpack.spaces.management.copyToSpaceFlyoutFooter.pendingCount": "待处理", + "xpack.spaces.management.copyToSpaceFlyoutFooter.skippedCount": "已跳过", "xpack.spaces.management.copyToSpaceFlyoutFooter.successCount": "已复制", - "xpack.spaces.management.copyToSpaceFlyoutHeader": "将已保存对象复制到工作区", + "xpack.spaces.management.copyToSpaceFlyoutHeader": "复制到工作区", "xpack.spaces.management.createSpaceBreadcrumb": "创建", "xpack.spaces.management.customizeSpaceAvatar.colorFormRowLabel": "颜色", "xpack.spaces.management.customizeSpaceAvatar.imageUrl": "定制图像", @@ -17355,22 +19506,30 @@ "xpack.spaces.management.deleteSpacesButton.deleteSpaceButtonLabel": "删除空间", "xpack.spaces.management.deleteSpacesButton.deleteSpaceErrorTitle": "删除空间时出错:{errorMessage}", "xpack.spaces.management.deleteSpacesButton.spaceSuccessfullyDeletedNotificationMessage": "已删除 {spaceName} 空间。", + "xpack.spaces.management.deselectAllFeaturesLink": "取消全选", + "xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "类别切换", "xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": "(所有可见功能)", - "xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "定制功能显示", - "xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "控制哪些功能在此工作区中可见。", + "xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "功能", + "xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "为此工作区设置功能可见性", + "xpack.spaces.management.enabledSpaceFeatures.goToRolesLink": "如果您希望保护对功能的访问,请{manageSecurityRoles}。", "xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": "(没有可见功能)", "xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "该功能在 UI 中已隐藏,但未禁用。", - "xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "角色", + "xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "管理安全角色", "xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": "({enabledCount} / {featureCount} 个功能可见)", + "xpack.spaces.management.featureAccordionSwitchLabel": "{enabledCount} / {featureCount} 个功能可见", + "xpack.spaces.management.featureVisibilityTitle": "功能可见性", "xpack.spaces.management.hideAllFeaturesText": "全部隐藏", + "xpack.spaces.management.managementCategoryHelpText": "对堆栈管理的访问由您的权限决定,并且不能被工作区隐藏。", "xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "头像", "xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "超卓的空间", "xpack.spaces.management.manageSpacePage.cancelSpaceButton": "取消", "xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip": "单击可定制此工作区头像", "xpack.spaces.management.manageSpacePage.createSpaceButton": "创建工作区", "xpack.spaces.management.manageSpacePage.createSpaceTitle": "创建一个空间", + "xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "命名您的工作区并定制其头像。", "xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierEditable": "记下 URL 标识符。创建工作区后,将不能更改它。", "xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierNotEditable": "URL 标识符无法更改。", + "xpack.spaces.management.manageSpacePage.customizeSpaceTitle": "定制您的工作区", "xpack.spaces.management.manageSpacePage.customizeVisibleFeatures": "定制可见功能", "xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "加载空间时出错:{message}", "xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "保存空间时出错:{message}", @@ -17385,6 +19544,43 @@ "xpack.spaces.management.secureSpaceMessage.howToAssignRoleToSpaceDescription": "想要为工作区分配角色?前往 {rolesLink}。", "xpack.spaces.management.secureSpaceMessage.rolesLinkText": "角色", "xpack.spaces.management.secureSpaceMessage.rolesLinkTextAriaLabel": "角色管理页面", + "xpack.spaces.management.selectAllFeaturesLink": "全选", + "xpack.spaces.management.shareToSpace.actionDescription": "将此已保存对象共享到一个或多个工作区", + "xpack.spaces.management.shareToSpace.actionTitle": "共享到工作区", + "xpack.spaces.management.shareToSpace.allSpacesLabel": "* 所有工作区", + "xpack.spaces.management.shareToSpace.cancelButton": "取消", + "xpack.spaces.management.shareToSpace.columnDescription": "目前将此对象共享到的其他工作区", + "xpack.spaces.management.shareToSpace.columnTitle": "共享工作区", + "xpack.spaces.management.shareToSpace.columnUnauthorizedLabel": "您无权查看这些工作区。", + "xpack.spaces.management.shareToSpace.noAvailableSpaces.canCreateNewSpace.linkText": "创建新工作区", + "xpack.spaces.management.shareToSpace.noAvailableSpaces.canCreateNewSpace.text": "您可以{createANewSpaceLink},用于共享您的对象。", + "xpack.spaces.management.shareToSpace.shareAddSuccessTextPlural": "“{object}”已添加到 {spaceTargets} 个工作区。", + "xpack.spaces.management.shareToSpace.shareAddSuccessTextSingular": "“{object}”已添加到 1 个工作区。", + "xpack.spaces.management.shareToSpace.shareEditSuccessTitle": "对象已更新", + "xpack.spaces.management.shareToSpace.shareErrorTitle": "更新已保存对象时出错", + "xpack.spaces.management.shareToSpace.shareModeControl.hiddenCountLabel": "+{hiddenCount} 个已隐藏", + "xpack.spaces.management.shareToSpace.shareModeControl.selectedCountLabel": "{selectedCount} 个已选择", + "xpack.spaces.management.shareToSpace.shareModeControl.selectSpacesLabel": "选择工作区", + "xpack.spaces.management.shareToSpace.shareModeControl.shareOptionsTitle": "共享选项", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToAllSpaces.cannotCheckTooltip": "您还需要其他权限,才能使用此选项。", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToAllSpaces.cannotUncheckTooltip": "您还需要其他权限,才能更改此选项。", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToAllSpaces.text": "使对象在当前和将来的所有空间中可用。", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToAllSpaces.title": "所有工作区", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToExplicitSpaces.text": "仅使对象在选定工作区中可用。", + "xpack.spaces.management.shareToSpace.shareModeControl.shareToExplicitSpaces.title": "选择工作区", + "xpack.spaces.management.shareToSpace.shareNewSuccessTitle": "对象现已共享", + "xpack.spaces.management.shareToSpace.shareRemoveSuccessTextPlural": "“{object}”已从 {spaceTargets} 个工作区中移除。", + "xpack.spaces.management.shareToSpace.shareRemoveSuccessTextSingular": "“{object}”已从 1 个工作区中移除。", + "xpack.spaces.management.shareToSpace.shareToSpacesButton": "保存并关闭", + "xpack.spaces.management.shareToSpace.shareWarningBody": "要仅在一个工作区中编辑,请改为{makeACopyLink}。", + "xpack.spaces.management.shareToSpace.shareWarningLink": "创建副本", + "xpack.spaces.management.shareToSpace.shareWarningTitle": "编辑共享对象会在所有工作区中应用更改", + "xpack.spaces.management.shareToSpace.showLessSpacesLink": "显示更少", + "xpack.spaces.management.shareToSpace.showMoreSpacesLink": "另外 {count} 个", + "xpack.spaces.management.shareToSpace.spacesLoadErrorTitle": "加载可用工作区时出错", + "xpack.spaces.management.shareToSpace.unknownSpacesLabel.additionalPrivilegesLink": "其他权限", + "xpack.spaces.management.shareToSpace.unknownSpacesLabel.text": "要查看隐藏的工作区,您需要{additionalPrivilegesLink}。", + "xpack.spaces.management.shareToSpaceFlyoutHeader": "共享到工作区", "xpack.spaces.management.showAllFeaturesText": "全部显示", "xpack.spaces.management.spaceIdentifier.customizeSpaceLinkText": "[定制]", "xpack.spaces.management.spaceIdentifier.customizeSpaceNameLinkLabel": "定制 URL 标识符", @@ -17413,6 +19609,7 @@ "xpack.spaces.management.spacesGridPage.spacesTitle": "工作区", "xpack.spaces.management.spacesGridPage.spaceSuccessfullyDeletedNotificationMessage": "已删除 “{spaceName}” 空间。", "xpack.spaces.management.toggleAllFeaturesLink": "(全部更改)", + "xpack.spaces.management.unauthorizedPrompt.permissionDeniedDescription": "您无权管理工作区。", "xpack.spaces.management.unauthorizedPrompt.permissionDeniedTitle": "权限被拒绝", "xpack.spaces.management.validateSpace.describeMaxLengthErrorMessage": "描述不能超过 2000 个字符。", "xpack.spaces.management.validateSpace.nameMaxLengthErrorMessage": "名称不能超过 1024 个字符。", @@ -17425,10 +19622,54 @@ "xpack.spaces.navControl.spacesMenu.noSpacesFoundTitle": " 未找到工作区 ", "xpack.spaces.spaceSelector.appTitle": "选择工作区", "xpack.spaces.spaceSelector.changeSpaceAnytimeAvailabilityText": "您可以随时更改您的空间", + "xpack.spaces.spaceSelector.contactSysAdminDescription": "请联系您的系统管理员。", + "xpack.spaces.spaceSelector.errorLoadingSpacesDescription": "加载工作区时出错 ({message})", "xpack.spaces.spaceSelector.findSpacePlaceholder": "查找工作区", "xpack.spaces.spaceSelector.noSpacesMatchSearchCriteriaDescription": "没有匹配搜索条件的空间", "xpack.spaces.spaceSelector.selectSpacesTitle": "选择您的空间", "xpack.spaces.spacesTitle": "工作区", + "xpack.stackAlerts.featureRegistry.actionsFeatureName": "堆栈告警", + "xpack.stackAlerts.geoThreshold.actionGroupThresholdMetTitle": "已达到跟踪阈值", + "xpack.stackAlerts.geoThreshold.actionVariableContextCrossingDocumentIdLabel": "穿越实体文档的 ID", + "xpack.stackAlerts.geoThreshold.actionVariableContextCrossingLineLabel": "连接用于确定穿越事件的两个位置的 GeoJSON 线", + "xpack.stackAlerts.geoThreshold.actionVariableContextCurrentBoundaryIdLabel": "包含实体的当前边界 ID(如果有)", + "xpack.stackAlerts.geoThreshold.actionVariableContextEntityIdLabel": "触发了告警的文档的实体 ID", + "xpack.stackAlerts.geoThreshold.actionVariableContextFromBoundaryIdLabel": "包含实体的上一边界 ID(如果有)", + "xpack.stackAlerts.geoThreshold.actionVariableContextFromBoundaryNameLabel": "实体从中穿越出且先前所位于的边界(如果有)", + "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityDateTimeLabel": "实体上次在上一边界中记录的时间", + "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityDocumentIdLabel": "穿越实体文档的 ID", + "xpack.stackAlerts.geoThreshold.actionVariableContextFromEntityLocationLabel": "实体的先前捕获位置", + "xpack.stackAlerts.geoThreshold.actionVariableContextTimeOfDetectionLabel": "记录此更改的告警时间间隔结束时间", + "xpack.stackAlerts.geoThreshold.actionVariableContextToBoundaryNameLabel": "实体已穿越进且当前位于的边界(如果有)", + "xpack.stackAlerts.geoThreshold.actionVariableContextToEntityDateTimeLabel": "在当前边界中检测到实体的时间", + "xpack.stackAlerts.geoThreshold.actionVariableContextToEntityLocationLabel": "实体的最近捕获位置", + "xpack.stackAlerts.geoThreshold.alertTypeTitle": "地理跟踪阈值", + "xpack.stackAlerts.indexThreshold.actionGroupThresholdMetTitle": "已达到阈值", + "xpack.stackAlerts.indexThreshold.actionVariableContextDateLabel": "告警超过阈值的日期。", + "xpack.stackAlerts.indexThreshold.actionVariableContextGroupLabel": "超过阈值的组。", + "xpack.stackAlerts.indexThreshold.actionVariableContextMessageLabel": "告警的预构造消息。", + "xpack.stackAlerts.indexThreshold.actionVariableContextThresholdComparatorLabel": "用于确定是否已达到阈值的比较函数。", + "xpack.stackAlerts.indexThreshold.actionVariableContextThresholdLabel": "用作阈值的值数组;“between”和“notBetween”需要两个值,其他则需要一个值。", + "xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "告警的预构造标题。", + "xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "超过阈值的值。", + "xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage": "[aggField]:当 [aggType] 为“{aggType}”时必须有值", + "xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "告警 {name} 组 {group} 值 {value} 在 {window} 于 {date}超过了阈值 {function}", + "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "告警 {name} 组 {group} 超过了阈值", + "xpack.stackAlerts.indexThreshold.alertTypeTitle": "索引阈值", + "xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage": "[dateStart]:晚于 [dateEnd]", + "xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage": "{fieldName} 的 {formatName} 格式无效:“{fieldValue}”", + "xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage": "[interval]:如果 [dateStart] 不等于 [dateEnd],则必须指定", + "xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage": "aggType 无效:“{aggType}”", + "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}", + "xpack.stackAlerts.indexThreshold.invalidDateErrorMessage": "日期 {date} 无效", + "xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage": "持续时间无效:“{duration}”", + "xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage": "groupBy 无效:“{groupBy}”", + "xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage": "[termSize]:必须小于或等于 {maxGroups}", + "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]:对于“{thresholdComparator}”比较运算符,必须包含两个元素", + "xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage": "timeWindowUnit 无效:“{timeWindowUnit}”", + "xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage": "时间间隔 {intervals} 的计算数目大于最大值 {maxIntervals}", + "xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage": "[termField]:[groupBy] 为 top 时,termField 为必需", + "xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage": "[termSize]:[groupBy] 为 top 时,termSize 为必需", "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "删除目标索引", "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "删除目标索引模式", "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "删除目标索引 {destinationIndex}", @@ -17458,8 +19699,10 @@ "xpack.transform.capability.noPermission.deleteTransformTooltip": "您无权删除数据帧转换。", "xpack.transform.capability.noPermission.startOrStopTransformTooltip": "您无权启动或停止转换。", "xpack.transform.capability.pleaseContactAdministratorTooltip": "{message}请联系您的管理员。", - "xpack.transform.clone.errorPromptText": "无法提取 Kibana 索引模式 ID。", + "xpack.transform.clone.errorPromptText": "检查源索引模式是否存在时发生错误", "xpack.transform.clone.errorPromptTitle": "获取转换配置时发生错误。", + "xpack.transform.clone.fetchErrorPromptText": "无法提取 Kibana 索引模式 ID。", + "xpack.transform.clone.noIndexPatternErrorPromptText": "无法克隆转换。对于 {indexPattern},不存在索引模式。", "xpack.transform.cloneTransform.breadcrumbTitle": "克隆转换", "xpack.transform.createTransform.breadcrumbTitle": "创建转换", "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "删除目标索引 {destinationIndex} 时发生错误", @@ -17530,6 +19773,7 @@ "xpack.transform.stepCreateForm.startTransformButton": "开始", "xpack.transform.stepCreateForm.startTransformDescription": "启动转换。转换将增加集群的搜索和索引负荷。如果负荷超载,请停止转换。转换启动后,系统将为您提供继续浏览转换的选项。", "xpack.transform.stepCreateForm.startTransformErrorMessage": "启动转换 {transformId} 时发生错误:", + "xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage": "调用启动转换请求时发生错误。", "xpack.transform.stepCreateForm.startTransformSuccessMessage": "启动转换 {transformId} 的请求已确认。", "xpack.transform.stepCreateForm.transformListCardDescription": "返回数据帧作业管理页面。", "xpack.transform.stepCreateForm.transformListCardTitle": "数据帧作业", @@ -17573,6 +19817,7 @@ "xpack.transform.stepDefineSummary.queryCodeBlockLabel": "查询", "xpack.transform.stepDefineSummary.queryLabel": "查询", "xpack.transform.stepDefineSummary.savedSearchLabel": "已保存搜索", + "xpack.transform.stepDetailsForm.advancedSettingsAccordionButtonContent": "高级设置", "xpack.transform.stepDetailsForm.continuousModeAriaLabel": "选择延迟。", "xpack.transform.stepDetailsForm.continuousModeDateFieldHelpText": "选择可用于标识新文档的日期字段。", "xpack.transform.stepDetailsForm.continuousModeDateFieldLabel": "日期字段", @@ -17585,11 +19830,23 @@ "xpack.transform.stepDetailsForm.destinationIndexInvalidError": "目标索引名称无效。", "xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink": "详细了解索引名称限制。", "xpack.transform.stepDetailsForm.destinationIndexLabel": "目标 IP", + "xpack.transform.stepDetailsForm.editFlyoutFormFrequencyPlaceholderText": "默认值:{defaultValue}", + "xpack.transform.stepDetailsForm.editFlyoutFormMaxPageSearchSizePlaceholderText": "默认值:{defaultValue}", "xpack.transform.stepDetailsForm.errorGettingIndexNames": "获取现有索引名称时发生错误:", "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "获取现有转换 ID 时发生错误:", - "xpack.transform.stepDetailsForm.errorGettingTransformPreview": "获取转换预览时发生错误", + "xpack.transform.stepDetailsForm.errorGettingTransformPreview": "提取转换预览时发生错误", + "xpack.transform.stepDetailsForm.frequencyAriaLabel": "选择频率。", + "xpack.transform.stepDetailsForm.frequencyError": "频率格式无效", + "xpack.transform.stepDetailsForm.frequencyHelpText": "在转换不间断地执行时检查源索引更改的时间间隔。还确定在转换搜索或索引时发生暂时失败时的重试时间间隔。最小值为 1 秒,最大值为 1 小时。", + "xpack.transform.stepDetailsForm.frequencyLabel": "频率", + "xpack.transform.stepDetailsForm.indexPatternTimeFieldHelpText": "选择用于全局时间筛选的主要时间字段。", + "xpack.transform.stepDetailsForm.indexPatternTimeFieldLabel": "时间字段", "xpack.transform.stepDetailsForm.indexPatternTitleError": "具有此名称的索引模式已存在。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeAriaLabel": "选择最大页面搜索大小。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeError": "max_page_search_size 必须是介于 10 到 10000 之间的数字。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeHelpText": "定义用于每个检查点的组合聚合的初始页面大小。", + "xpack.transform.stepDetailsForm.maxPageSearchSizeLabel": "最大页面搜索大小", "xpack.transform.stepDetailsForm.noTimeFieldOptionLabel": "我不想使用时间筛选", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "选择可选的转换描述。", "xpack.transform.stepDetailsForm.transformDescriptionLabel": "转换描述", @@ -17598,9 +19855,13 @@ "xpack.transform.stepDetailsForm.transformIdInputAriaLabel": "选择唯一的作业 ID。", "xpack.transform.stepDetailsForm.transformIdInvalidError": "只能包含小写字母数字字符(a-z 和 0-9)、连字符和下划线,并且必须以字母数字字符开头和结尾。", "xpack.transform.stepDetailsForm.transformIdLabel": "作业 ID", + "xpack.transform.stepDetailsSummary.advancedSettingsAccordionButtonContent": "高级设置", "xpack.transform.stepDetailsSummary.continuousModeDateFieldLabel": "连续模式日期字段", "xpack.transform.stepDetailsSummary.createIndexPatternMessage": "将为此作业创建 Kibana 索引模式。", "xpack.transform.stepDetailsSummary.destinationIndexLabel": "目标 IP", + "xpack.transform.stepDetailsSummary.frequencyLabel": "频率", + "xpack.transform.stepDetailsSummary.indexPatternTimeFieldLabel": "Kibana 索引模式时间字段", + "xpack.transform.stepDetailsSummary.maxPageSearchSizeLabel": "最大页面搜索大小", "xpack.transform.stepDetailsSummary.transformDescriptionLabel": "转换描述", "xpack.transform.stepDetailsSummary.transformIdLabel": "作业 ID", "xpack.transform.tableActionLabel": "操作", @@ -17613,25 +19874,40 @@ "xpack.transform.transformList.bulkDeleteModalTitle": "删除 {count} 个 {count, plural, one {转换} other {转换}}?", "xpack.transform.transformList.bulkDeleteTransformSuccessMessage": "已成功删除 {count} 个{count, plural, one {转换} other {转换}}。", "xpack.transform.transformList.bulkStartModalTitle": "启动 {count} 个 {count, plural, one {转换} other {转换}}?", + "xpack.transform.transformList.cloneActionNameText": "克隆", "xpack.transform.transformList.completeBatchTransformBulkActionToolTip": "一个或多个转换为已完成批量转换,无法重新启动。", "xpack.transform.transformList.completeBatchTransformToolTip": "{transformId} 为已完成批量转换,无法重新启动。", "xpack.transform.transformList.createTransformButton": "创建转换", "xpack.transform.transformList.deleteActionDisabledToolTipContent": "停止数据帧作业,以便将其删除。", + "xpack.transform.transformList.deleteActionNameText": "删除", "xpack.transform.transformList.deleteBulkActionDisabledToolTipContent": "一个或多个选定数据帧转换必须停止,才能删除。", "xpack.transform.transformList.deleteModalCancelButton": "取消", "xpack.transform.transformList.deleteModalDeleteButton": "删除", - "xpack.transform.transformList.deleteModalTitle": "删除 {transformId}", + "xpack.transform.transformList.deleteModalTitle": "删除 {transformId}?", "xpack.transform.transformList.deleteTransformErrorMessage": "删除转换 {transformId} 时发生错误", "xpack.transform.transformList.deleteTransformGenericErrorMessage": "调用用于删除转换的 API 终端节点时发生错误。", "xpack.transform.transformList.deleteTransformSuccessMessage": "删除转换 {transformId} 的请求已确认。", + "xpack.transform.transformList.editActionNameText": "编辑", "xpack.transform.transformList.editFlyoutCalloutDocs": "查看文档", "xpack.transform.transformList.editFlyoutCalloutText": "此表单允许您更新转换。可以更新的属性列表是创建转换时可以定义的列表子集。", "xpack.transform.transformList.editFlyoutCancelButtonText": "取消", + "xpack.transform.transformList.editFlyoutFormAdvancedSettingsButtonContent": "高级设置", "xpack.transform.transformList.editFlyoutFormDescriptionLabel": "描述", - "xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "要启用节流,请设置输入文档限制(每秒文档数)。", + "xpack.transform.transformList.editFlyoutFormDestinationButtonContent": "目标配置", + "xpack.transform.transformList.editFlyoutFormDestinationIndexLabel": "目标索引", + "xpack.transform.transformList.editFlyoutFormDestinationPipelineLabel": "管道", + "xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "要启用节流,请设置每秒要输入的文档限值。", + "xpack.transform.transformList.editFlyoutFormDocsPerSecondLabel": "每秒文档数", + "xpack.transform.transformList.editFlyoutFormFrequencyHelpText": "在转换不间断地执行时检查源索引更改的时间间隔。还确定在转换搜索或索引时发生暂时失败时的重试时间间隔。最小值为 1 秒,最大值为 1 小时。", "xpack.transform.transformList.editFlyoutFormFrequencyLabel": "频率", "xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage": "频率值无效。", - "xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "值需要是大于零的数字。", + "xpack.transform.transformList.editFlyoutFormFrequencyPlaceholderText": "默认值:{defaultValue}", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelptext": "定义用于每个检查点的组合聚合的初始页面大小。", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeLabel": "最大页面搜索大小", + "xpack.transform.transformList.editFlyoutFormMaxPageSearchSizePlaceholderText": "默认值:{defaultValue}", + "xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "值必须是大于零的整数。", + "xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "值必须是介于 10 到 10000 之间的整数。", + "xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必填字段。", "xpack.transform.transformList.editFlyoutFormStringNotValidErrorMessage": "值需要为字符串类型。", "xpack.transform.transformList.editFlyoutTitle": "编辑 {transformId}", "xpack.transform.transformList.editFlyoutUpdateButtonText": "更新", @@ -17641,17 +19917,22 @@ "xpack.transform.transformList.refreshButtonLabel": "刷新", "xpack.transform.transformList.rowCollapse": "隐藏 {transformId} 的详情", "xpack.transform.transformList.rowExpand": "显示 {transformId} 的详情", + "xpack.transform.transformList.searchBar.invalidSearchErrorMessage": "搜索无效:{errorMessage}", "xpack.transform.transformList.showDetailsColumn.screenReaderDescription": "此列包含可单击控件,用于显示每个转换的更多详情", + "xpack.transform.transformList.startActionNameText": "启动", "xpack.transform.transformList.startedTransformBulkToolTip": "一个或多个选定数据帧转换已启动。", "xpack.transform.transformList.startedTransformToolTip": "{transformId} 已启动。", + "xpack.transform.transformList.startModalBody": "转换将增加集群的搜索和索引负载。如果超负荷,请停止转换。", "xpack.transform.transformList.startModalCancelButton": "取消", "xpack.transform.transformList.startModalStartButton": "启动", - "xpack.transform.transformList.startModalTitle": "启动 {transformId}", + "xpack.transform.transformList.startModalTitle": "启动 {transformId}?", "xpack.transform.transformList.startTransformErrorMessage": "启动转换 {transformId} 时发生错误", "xpack.transform.transformList.startTransformSuccessMessage": "启动转换 {transformId} 的请求已确认。", + "xpack.transform.transformList.stopActionNameText": "停止", "xpack.transform.transformList.stoppedTransformBulkToolTip": "一个或多个选定数据帧转换已停止。", "xpack.transform.transformList.stoppedTransformToolTip": "{transformId} 已停止。", "xpack.transform.transformList.stopTransformErrorMessage": "停止数据帧转换 {transformId} 时发生错误", + "xpack.transform.transformList.stopTransformResponseSchemaErrorMessage": "调用停止转换请求时发生错误。", "xpack.transform.transformList.stopTransformSuccessMessage": "停止数据帧转换 {transformId} 的请求已确认。", "xpack.transform.transformList.transformDescription": "使用转换将现有 Elasticsearch 索引切换到摘要式或以实体为中心的索引。", "xpack.transform.transformList.transformDetails.messagesPane.errorMessage": "无法加载消息", @@ -17681,6 +19962,7 @@ "xpack.triggersActionsUI.actionVariables.tagsLabel": "告警的标记。", "xpack.triggersActionsUI.alerts.breadcrumbTitle": "告警", "xpack.triggersActionsUI.appName": "告警和操作", + "xpack.triggersActionsUI.case.configureCases.mappingFieldSummary": "摘要", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "连接器已由 Kibana 配置禁用。", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "此连接器需要{minimumLicenseRequired}许可证。", "xpack.triggersActionsUI.common.constants.comparators.groupByTypes.allDocumentsLabel": "所有文档", @@ -17719,6 +20001,9 @@ "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.configureAccountsHelpLabel": "配置电子邮件帐户。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "从您的服务器发送电子邮件。", "xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "发送者电子邮件地址无效。", + "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText": "“密码”必填。", + "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthUserNameText": "“用户名”必填。", + "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson": "“文档”必填,并且应为有效的 JSON 对象。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "未输入收件人、抄送、密送。 至少需要输入一个。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText": "“发送者”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText": "“主机”必填。", @@ -17758,6 +20043,37 @@ "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshLabel": "刷新索引", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshTooltip": "刷新影响的分片以使此操作对搜索可见。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText": "将数据索引到 Elasticsearch 中。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle": "Jira", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel": "API 令牌或密码", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel": "URL", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel": "其他注释(可选)", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.descriptionTextAreaFieldLabel": "描述(可选)", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel": "电子邮件或用户名", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.impactSelectFieldLabel": "标签(可选)", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField": "URL 无效。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments": "注释", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription": "描述", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.parentIssueSearchLabel": "父问题", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey": "项目键", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField": "“API 令牌”或“密码”必填", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField": "“URL”必填。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "“描述”必填。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "“电子邮件”或“用户名”必填", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "“项目键”必填", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredTitleTextField": "“标题”必填。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldHelp": "JIRA 将此操作与 Kibana 已保存对象的 ID 关联。", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldLabel": "对象 ID(可选)", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "选择父问题", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder": "选择父问题", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesLoading": "正在加载……", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText": "将数据推送或更新到 Jira 中的新问题", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.severitySelectFieldLabel": "优先级", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel": "摘要", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetFieldsMessage": "无法获取字段", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueMessage": "无法获取 ID 为 {id} 的问题", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssuesMessage": "无法获取问题", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage": "无法获取问题类型", + "xpack.triggersActionsUI.components.builtinActionTypes.jira.urgencySelectFieldLabel": "问题类型", "xpack.triggersActionsUI.components.builtinActionTypes.mappingFieldNotMapped": "未映射", "xpack.triggersActionsUI.components.builtinActionTypes.noConnector": "未选择连接器", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "发送到 PagerDuty", @@ -17765,8 +20081,10 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "类(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.componentTextFieldLabel": "组件(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.dedupKeyTextFieldLabel": "DedupKey(可选)", + "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.dedupKeyTextRequiredFieldLabel": "DedupKey", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.invalidTimestamp": "时间戳必须是有效的日期,例如 {nowShortFormat} 或 {nowLongFormat}。", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText": "路由键必填。", + "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredDedupKeyText": "解决或确认事件时需要 DedupKey。", + "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText": "需要集成密钥/路由密钥。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText": "“摘要”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventActionSelectFieldLabel": "事件操作", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectAcknowledgeOptionLabel": "确认", @@ -17784,6 +20102,29 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.sourceTextFieldLabel": "源(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "摘要", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "时间戳(可选)", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle": "Resilient", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId": "API 密钥 ID", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret": "API 密钥密码", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel": "URL", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel": "其他注释(可选)", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.descriptionTextAreaFieldLabel": "描述(可选)", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField": "URL 无效。", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments": "注释", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription": "描述", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription": "名称", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId": "组织 ID", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField": "“API 密钥 ID”必填", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField": "“API 密钥密码”必填", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField": "“URL”必填。", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField": "“组织 ID”必填", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.savedObjectIdFieldHelp": "IBM Resilient 将此操作与 Kibana 已保存对象的 ID 关联。", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.savedObjectIdFieldLabel": "对象 ID(可选)", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText": "将数据推送或更新到 Resilient 中的新事件。", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.severity": "严重性", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.titleFieldLabel": "名称", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage": "无法获取事件类型", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetSeverityMessage": "无法获取严重性", + "xpack.triggersActionsUI.components.builtinActionTypes.resilient.urgencySelectFieldLabel": "事件类型", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle": "发送到服务器日志", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logLevelFieldLabel": "级别", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "消息", @@ -17805,11 +20146,14 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredEmailTextField": "“电子邮件”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "“密码”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "“用户名”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText": "将数据推送或更新到 ServiceNow 中的新事件。", + "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.savedObjectIdFieldHelp": "ServiceNow 将此操作与 Kibana 已保存对象的 ID 关联。", + "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.savedObjectIdFieldLabel": "对象 ID(可选)", + "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText": "在 ServiceNow 中创建事件。", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.severitySelectFieldLabel": "严重性", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectHighOptionLabel": "高", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectLawOptionLabel": "低", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectMediumOptionLabel": "中", + "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.title": "事件", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.titleFieldLabel": "简短描述", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.urgencySelectFieldLabel": "紧急度", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel": "用户名", @@ -17863,6 +20207,42 @@ "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "取消", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "删除{numIdsToDelete, plural, one {{singleTitle}} other { # 个{multipleTitle}}} ", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText": "无法恢复{numIdsToDelete, plural, one {删除的{singleTitle}} other {删除的{multipleTitle}}}。", + "xpack.triggersActionsUI.geoThreshold.boundaryNameSelect": "选择边界名称", + "xpack.triggersActionsUI.geoThreshold.boundaryNameSelectLabel": "可人工读取的边界名称(可选)", + "xpack.triggersActionsUI.geoThreshold.delayOffset": "已延迟的评估偏移", + "xpack.triggersActionsUI.geoThreshold.delayOffsetTooltip": "评估延迟周期内的告警,以针对数据延迟进行调整", + "xpack.triggersActionsUI.geoThreshold.entityByLabel": "方式", + "xpack.triggersActionsUI.geoThreshold.entityIndexLabel": "索引", + "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryGeoFieldText": "“边界地理”字段必填。", + "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryIndexTitleText": "“边界索引模式标题”必填。", + "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryTypeText": "“边界类型”必填。", + "xpack.triggersActionsUI.geoThreshold.error.requiredDateFieldText": "“日期”字段必填。", + "xpack.triggersActionsUI.geoThreshold.error.requiredEntityText": "“实体”必填。", + "xpack.triggersActionsUI.geoThreshold.error.requiredGeoFieldText": "“地理”字段必填。", + "xpack.triggersActionsUI.geoThreshold.error.requiredIndexTitleText": "“索引模式”必填。", + "xpack.triggersActionsUI.geoThreshold.error.requiredTrackingEventText": "“跟踪事件”必填。", + "xpack.triggersActionsUI.geoThreshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", + "xpack.triggersActionsUI.geoThreshold.geofieldLabel": "地理空间字段", + "xpack.triggersActionsUI.geoThreshold.indexLabel": "索引", + "xpack.triggersActionsUI.geoThreshold.indexPatternSelectLabel": "索引模式", + "xpack.triggersActionsUI.geoThreshold.indexPatternSelectPlaceholder": "选择索引模式", + "xpack.triggersActionsUI.geoThreshold.name.trackingThreshold": "跟踪阈值", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisLinkTextDescription": "创建索引模式", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisPrefixDescription": "您将需要 ", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisSuffixDescription": " (包含地理空间字段)。", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.getStartedLinkText": "开始使用一些样例数据集。", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.hintDescription": "没有任何地理空间数据集? ", + "xpack.triggersActionsUI.geoThreshold.noIndexPattern.messageTitle": "找不到任何具有地理空间字段的索引模式", + "xpack.triggersActionsUI.geoThreshold.selectBoundaryIndex": "选择边界:", + "xpack.triggersActionsUI.geoThreshold.selectEntity": "选择实体", + "xpack.triggersActionsUI.geoThreshold.selectGeoLabel": "选择地理字段", + "xpack.triggersActionsUI.geoThreshold.selectIndex": "定义条件", + "xpack.triggersActionsUI.geoThreshold.selectLabel": "选择地理字段", + "xpack.triggersActionsUI.geoThreshold.selectOffset": "选择偏移(可选)", + "xpack.triggersActionsUI.geoThreshold.selectTimeLabel": "选择时间字段", + "xpack.triggersActionsUI.geoThreshold.timeFieldLabel": "时间字段", + "xpack.triggersActionsUI.geoThreshold.topHitsSplitFieldSelectPlaceholder": "选择实体字段", + "xpack.triggersActionsUI.geoThreshold.whenEntityLabel": "当实体", "xpack.triggersActionsUI.home.alertsTabTitle": "告警", "xpack.triggersActionsUI.home.appTitle": "告警和操作", "xpack.triggersActionsUI.home.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", @@ -17873,6 +20253,7 @@ "xpack.triggersActionsUI.sections.actionAdd.indexAction.indexTextFieldLabel": "标记(可选)", "xpack.triggersActionsUI.sections.actionConnectorAdd.cancelButtonLabel": "取消", "xpack.triggersActionsUI.sections.actionConnectorAdd.manageLicensePlanBannerLinkTitle": "管理许可证", + "xpack.triggersActionsUI.sections.actionConnectorAdd.saveAndTestButtonLabel": "保存并测试", "xpack.triggersActionsUI.sections.actionConnectorAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerLinkTitle": "订阅选项", "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerMessage": "升级您的许可证或开始为期 30 天的免费试用,以便可以立即使用所有第三方连接器。", @@ -17881,6 +20262,7 @@ "xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningDescriptionText": "要创建此连接器,必须至少配置一个 {actionType} 帐户。{docLink}", "xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningHelpLinkText": "了解详情。", "xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionTypeConfigurationWarningTitleText": "未注册操作类型", + "xpack.triggersActionsUI.sections.actionConnectorForm.connectorSettingsLabel": "连接器设置", "xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "名称必填。", "xpack.triggersActionsUI.sections.actionForm.getMoreActionsTitle": "获取更多的操作", "xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage": "(预配置)", @@ -17890,10 +20272,14 @@ "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDescription": "删除此连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDisabledDescription": "无法删除连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionName": "删除", + "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorDescription": "运行此连接器", + "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorDisabledDescription": "无法运行连接器", + "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorName": "运行", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actionTypeTitle": "类型", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.nameTitle": "名称", "xpack.triggersActionsUI.sections.actionsConnectorsList.filters.actionTypeIdName": "类型", "xpack.triggersActionsUI.sections.actionsConnectorsList.multipleTitle": "连接器", + "xpack.triggersActionsUI.sections.actionsConnectorsList.noPermissionToCreateDescription": "请联系您的系统管理员。", "xpack.triggersActionsUI.sections.actionsConnectorsList.noPermissionToCreateTitle": "无权创建连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.singleTitle": "连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.unableToLoadActionsMessage": "无法加载连接器", @@ -17928,7 +20314,9 @@ "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "定义条件", "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "无法加载告警可视化", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "创建告警", + "xpack.triggersActionsUI.sections.alertAdd.geoThreshold.closePopoverLabel": "关闭", "xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription": "正在加载告警可视化……", + "xpack.triggersActionsUI.sections.alertAdd.operationName": "创建", "xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription": "完成表达式以生成预览。", "xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "无法创建告警。", @@ -17952,11 +20340,13 @@ "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.start": "启动", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.status": "状态", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active": "活动", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "非活动", + "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "确定", "xpack.triggersActionsUI.sections.alertDetails.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.disableTitle": "禁用", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle": "静音", + "xpack.triggersActionsUI.sections.alertDetails.dismissButtonTitle": "关闭", "xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel": "编辑", + "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertInstanceSummaryMessage": "无法加载告警实例摘要:{message}", "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage": "无法加载告警:{message}", "xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel": "在应用中查看", "xpack.triggersActionsUI.sections.alertEdit.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", @@ -17981,7 +20371,9 @@ "xpack.triggersActionsUI.sections.alertForm.changeAlertTypeAriaLabel": "删除", "xpack.triggersActionsUI.sections.alertForm.checkFieldLabel": "检查频率", "xpack.triggersActionsUI.sections.alertForm.checkWithTooltip": "定义评估条件的频率。", - "xpack.triggersActionsUI.sections.alertForm.emptyConnectorsLabel": "无 {actionTypeName} 连接器。", + "xpack.triggersActionsUI.sections.alertForm.emptyConnectorsLabel": "无 {actionTypeName} 连接器", + "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedAlertTypes": "为了{operation}告警,您需要获得相应的权限。", + "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedAlertTypesTitle": "您尚无权{operation}任何告警类型", "xpack.triggersActionsUI.sections.alertForm.error.requiredAlertTypeIdText": "“告警触发器”必填。", "xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText": "“检查时间间隔”必填。", "xpack.triggersActionsUI.sections.alertForm.error.requiredNameText": "“名称”必填。", @@ -18000,14 +20392,27 @@ "xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage": "无法加载操作类型", "xpack.triggersActionsUI.sections.alertForm.unableToLoadAlertTypesMessage": "无法加载告警类型", "xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle": "无法加载连接器。", + "xpack.triggersActionsUI.sections.alertForm.unauthorizedToCreateForEmptyConnectors": "只有获得授权的用户才能配置连接器。请联系您的管理员。", "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "操作类型", "xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "创建告警", + "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonDecrypting": "解密告警时发生错误。", + "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonReading": "读取告警时发生错误。", + "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "运行告警时发生错误。", + "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "由于未知原因发生错误。", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "操作", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsText": "操作", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "类型", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "运行间隔", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名称", + "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle": "状态", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "标记", + "xpack.triggersActionsUI.sections.alertsList.alertStatusActive": "活动", + "xpack.triggersActionsUI.sections.alertsList.alertStatusError": "错误", + "xpack.triggersActionsUI.sections.alertsList.alertStatusFilterLabel": "状态", + "xpack.triggersActionsUI.sections.alertsList.alertStatusOk": "确定", + "xpack.triggersActionsUI.sections.alertsList.alertStatusPending": "待处理", + "xpack.triggersActionsUI.sections.alertsList.alertStatusUnknown": "未知", + "xpack.triggersActionsUI.sections.alertsList.attentionBannerTitle": "在 {totalStausesError} 个 {totalStausesError, plural, one {{singleTitle}} other {# 个 {multipleTitle}}}中发现错误。", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.buttonTitle": "管理告警", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.deleteAllTitle": "删除", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.disableAllTitle": "禁用", @@ -18025,16 +20430,28 @@ "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteHelpText": "静音后,将检查告警,但不执行操作。", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "静音", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "操作", + "xpack.triggersActionsUI.sections.alertsList.dismissBunnerButtonLabel": "关闭", "xpack.triggersActionsUI.sections.alertsList.multipleTitle": "告警", + "xpack.triggersActionsUI.sections.alertsList.noPermissionToCreateDescription": "请联系您的系统管理员。", + "xpack.triggersActionsUI.sections.alertsList.noPermissionToCreateTitle": "无权创建告警", "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "搜索", "xpack.triggersActionsUI.sections.alertsList.singleTitle": "告警", + "xpack.triggersActionsUI.sections.alertsList.totalItemsCountDescription": "正在显示:{pageSize} 个告警(共 {totalItemCount} 个)。", + "xpack.triggersActionsUI.sections.alertsList.totalStausesActiveDescription": "活动:{totalStausesActive}", + "xpack.triggersActionsUI.sections.alertsList.totalStausesErrorDescription": "错误:{totalStausesError}", + "xpack.triggersActionsUI.sections.alertsList.totalStausesOkDescription": "确定:{totalStausesOk}", + "xpack.triggersActionsUI.sections.alertsList.totalStausesPendingDescription": "待处理:{totalStausesPending}", + "xpack.triggersActionsUI.sections.alertsList.totalStausesUnknownDescription": "未知:{totalStausesUnknown}", "xpack.triggersActionsUI.sections.alertsList.typeFilterLabel": "类型", "xpack.triggersActionsUI.sections.alertsList.unableToLoadActionTypesMessage": "无法加载操作类型", "xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertsMessage": "无法加载告警", + "xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertsStatusesInfoMessage": "无法加载告警状态信息", "xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertTypesMessage": "无法加载告警类型", - "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "{titleBcc}", + "xpack.triggersActionsUI.sections.alertsList.viewBunnerButtonLabel": "查看", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "添加抄送收件人", + "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "身份验证", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel": "发送者", + "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hasAuthSwitchLabel": "需要对此服务器进行身份验证", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel": "主机", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.messageTextAreaFieldLabel": "消息", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel": "密码", @@ -18051,20 +20468,37 @@ "xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "此连接器为只读。", "xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "编辑连接器", "xpack.triggersActionsUI.sections.editConnectorForm.preconfiguredHelpLabel": "详细了解预配置的连接器。", + "xpack.triggersActionsUI.sections.editConnectorForm.saveAndCloseButtonLabel": "保存并关闭", "xpack.triggersActionsUI.sections.editConnectorForm.saveButtonLabel": "保存", + "xpack.triggersActionsUI.sections.editConnectorForm.tabText": "配置", "xpack.triggersActionsUI.sections.editConnectorForm.updateErrorNotificationText": "无法更新连接器。", "xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText": "已更新“{connectorName}”", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.flyoutTitle": "{connectorName}", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.tooltipContent": "这是预配置连接器,无法编辑", + "xpack.triggersActionsUI.sections.testConnectorForm.awaitingExecutionDescription": "执行该操作时,结果将显示在此处。", + "xpack.triggersActionsUI.sections.testConnectorForm.executeTestButton": "运行", + "xpack.triggersActionsUI.sections.testConnectorForm.executeTestDisabled": "测试连接器前保存所做的更改。", + "xpack.triggersActionsUI.sections.testConnectorForm.executionFailureAdditionalDetails": "详情:", + "xpack.triggersActionsUI.sections.testConnectorForm.executionFailureDescription": "找到以下错误:", + "xpack.triggersActionsUI.sections.testConnectorForm.executionFailureTitle": "操作无法执行", + "xpack.triggersActionsUI.sections.testConnectorForm.executionFailureUnknownReason": "未知原因", + "xpack.triggersActionsUI.sections.testConnectorForm.executionSuccessfulDescription": "确保结果符合预期。", + "xpack.triggersActionsUI.sections.testConnectorForm.executionSuccessfulTitle": "操作成功", + "xpack.triggersActionsUI.sections.testConnectorForm.tabText": "测试", "xpack.triggersActionsUI.timeUnits.dayLabel": "{timeValue, plural, one {天} other {天}}", "xpack.triggersActionsUI.timeUnits.hourLabel": "{timeValue, plural, one {小时} other {小时}}", "xpack.triggersActionsUI.timeUnits.minuteLabel": "{timeValue, plural, one {分钟} other {分钟}}", "xpack.triggersActionsUI.timeUnits.secondLabel": "{timeValue, plural, one {秒} other {秒}}", "xpack.triggersActionsUI.typeRegistry.get.missingActionTypeErrorMessage": "未注册对象类型“{id}”。", "xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage": "已注册对象类型“{id}”。", + "xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel": "公测版", + "xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip": "此操作位于公测版中,可能会有所更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。请通过报告任何错误或提供其他反馈来帮助我们。", "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "更改", "xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "许可证级别不够", + "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpText": "这是什么?", + "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip": "确定向下钻取显示在上下文菜单中的时间", + "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel": "在以下时间显示选项:", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "添加到面板", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "取消", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "时间范围", @@ -18102,6 +20536,18 @@ "xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.deleteDrilldownsButtonLabel": "删除 ({count})", "xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.editDrilldownButtonLabel": "编辑", "xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.selectThisDrilldownCheckboxLabel": "选择此向下钻取", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle": "添加变量", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel": "在新选项卡中打开", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText": "请注意,在预览模式下,\\{\\{event.*\\}\\} 变量将替换为虚拟值。", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel": "URL 预览:", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText": "预览", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel": "输入 URL 模板:", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText": "例如:{exampleUrl}", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText": "语法帮助", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "筛选变量", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "帮助", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "格式无效:{message}", + "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "格式无效。例如:{exampleUrl}", "xpack.upgradeAssistant.appTitle": "{version} 升级助手", "xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.calloutDetail": "使用 {snapshotRestoreDocsButton} 备份您的数据。", "xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.snapshotRestoreDocsButtonLabel": "快照和还原 API", @@ -18233,15 +20679,26 @@ "xpack.uptime.alerts.durationAnomaly.clientName": "Uptime 持续时间异常", "xpack.uptime.alerts.durationAnomaly.defaultActionMessage": "{anomalyStartTimestamp} 在 url {monitorUrl} 的 {monitor} 上检测到异常({severity} 级别)响应时间。异常严重性分数为 {severityScore}。\n从位置 {observerLocation} 检测到高达 {slowestAnomalyResponse} 的响应时间。预期响应时间为 {expectedResponseTime}。", "xpack.uptime.alerts.monitorStatus": "运行时间监测状态", + "xpack.uptime.alerts.monitorStatus.actionVariables.availabilityMessage": "低于阈值,{availabilityRatio}% 可用性应为 {expectedAvailability}%", "xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description": "生成的摘要,显示告警已检测为“关闭”的部分或所有监测", "xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description": "生成的消息,汇总当前关闭的监测", + "xpack.uptime.alerts.monitorStatus.actionVariables.down": "关闭", + "xpack.uptime.alerts.monitorStatus.actionVariables.downAndAvailabilityMessage": "{statusMessage} 以及 {availabilityMessage}", "xpack.uptime.alerts.monitorStatus.actionVariables.state.currentTriggerStarted": "表示告警触发时当前触发状况开始的时间戳", "xpack.uptime.alerts.monitorStatus.actionVariables.state.firstCheckedAt": "表示此告警首次检查的时间戳", "xpack.uptime.alerts.monitorStatus.actionVariables.state.firstTriggeredAt": "表示告警首次触发的时间戳", "xpack.uptime.alerts.monitorStatus.actionVariables.state.isTriggered": "表示告警当前是否触发的标志", "xpack.uptime.alerts.monitorStatus.actionVariables.state.lastCheckedAt": "表示告警最近检查时间的时间戳", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.lastErrorMessage": "监测最新错误消息", "xpack.uptime.alerts.monitorStatus.actionVariables.state.lastResolvedAt": "表示此告警最近解决时间的时间戳", "xpack.uptime.alerts.monitorStatus.actionVariables.state.lastTriggeredAt": "表示告警最近触发时间的时间戳", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.monitor": "名称或 ID 的友好呈现,建议类似于 My Monitor 的名称", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.monitorId": "监测的 ID。", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.monitorType": "监测的类型(例如 HTTP/TCP)。", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.monitorUrl": "监测的 URL。", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.observerHostname": "执行 Heartbeat 检查的观察者主机名。", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.observerLocation": "执行 Heartbeat 检查的观察者位置。", + "xpack.uptime.alerts.monitorStatus.actionVariables.state.statusMessage": "状态消息,例如关闭和/或低于可用性阈值(如果执行可用性检查)。", "xpack.uptime.alerts.monitorStatus.addFilter": "添加筛选", "xpack.uptime.alerts.monitorStatus.addFilter.location": "位置", "xpack.uptime.alerts.monitorStatus.addFilter.port": "端口", @@ -18258,6 +20715,7 @@ "xpack.uptime.alerts.monitorStatus.availability.unit.headline": "选择时间范围单位", "xpack.uptime.alerts.monitorStatus.availability.unit.selectable": "使用此选择来设置此告警的可用性范围单位", "xpack.uptime.alerts.monitorStatus.clientName": "运行时间监测状态", + "xpack.uptime.alerts.monitorStatus.defaultActionMessage": "在 {observerLocation},URL 为 {monitorUrl} 的监测 {monitorName} 是 {statusMessage}。最新错误消息是 {latestErrorMessage}", "xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel": "允许对监测状态告警使用筛选条件的输入", "xpack.uptime.alerts.monitorStatus.filters.anyLocation": "任意位置", "xpack.uptime.alerts.monitorStatus.filters.anyPort": "任意端口", @@ -18293,6 +20751,7 @@ "xpack.uptime.alerts.monitorStatus.timerangeValueField.expression": "之内", "xpack.uptime.alerts.monitorStatus.timerangeValueField.value": "上一 {value}", "xpack.uptime.alerts.monitorStatus.title.label": "运行时间监测状态", + "xpack.uptime.alerts.settings.createConnector": "创建连接器", "xpack.uptime.alerts.timerangeUnitSelectable.daysOption.ariaLabel": "“天”时间范围选择项", "xpack.uptime.alerts.timerangeUnitSelectable.hoursOption.ariaLabel": "“小时”时间范围选择项", "xpack.uptime.alerts.timerangeUnitSelectable.minutesOption.ariaLabel": "“分钟”时间范围选择项", @@ -18325,7 +20784,7 @@ "xpack.uptime.alerts.toggleAlertFlyoutButtonText": "告警", "xpack.uptime.alertsPopover.toggleButton.ariaLabel": "打开告警上下文菜单", "xpack.uptime.apmIntegrationAction.description": "在 APM 中搜索此监测", - "xpack.uptime.apmIntegrationAction.text": "在 APM 中查找域", + "xpack.uptime.apmIntegrationAction.text": "显示 APM 数据", "xpack.uptime.availabilityLabelText": "{value} %", "xpack.uptime.badge.readOnly.text": "只读", "xpack.uptime.badge.readOnly.tooltip": "无法保存", @@ -18396,6 +20855,7 @@ "xpack.uptime.ml.enableAnomalyDetectionPanel.createNewJobButtonLabel": "创建新作业", "xpack.uptime.ml.enableAnomalyDetectionPanel.disableAnomalyAlert": "禁用异常告警", "xpack.uptime.ml.enableAnomalyDetectionPanel.disableAnomalyDetectionTitle": "禁用异常检测", + "xpack.uptime.ml.enableAnomalyDetectionPanel.enable_or_manage_job": "您可以启用异常检测作业,或者如果此处已有作业,则可以管理该作业或告警。", "xpack.uptime.ml.enableAnomalyDetectionPanel.enableAnomalyAlert": "启用异常告警", "xpack.uptime.ml.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle": "启用异常检测", "xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreatedNotificationText": "现在正在运行响应持续时间图表的分析。可能要花费点时间,才会将结果添加到响应时间图表。", @@ -18412,7 +20872,7 @@ "xpack.uptime.ml.enableAnomalyDetectionPanel.manageMLJobDescription.noteText": "注意:可能要过几分钟后,作业才会开始计算结果。", "xpack.uptime.ml.enableAnomalyDetectionPanel.startTrial": "开始为期 14 天的免费试用", "xpack.uptime.ml.enableAnomalyDetectionPanel.startTrialDesc": "要访问持续时间异常检测,必须订阅 Elastic 白金级许可证。", - "xpack.uptime.monitorCharts.durationChart.leftAxis.title": "持续时间 (ms)", + "xpack.uptime.monitorCharts.durationChart.leftAxis.title": "持续时间(毫秒)", "xpack.uptime.monitorCharts.monitorDuration.titleLabel": "监测持续时间(毫秒)", "xpack.uptime.monitorCharts.monitorDuration.titleLabelWithAnomaly": "监测持续时间(异常:{noOfAnomalies})", "xpack.uptime.monitorDetails.ml.confirmAlertDeleteMessage": "确定要删除异常告警?", @@ -18420,10 +20880,15 @@ "xpack.uptime.monitorDetails.ml.deleteJobWarning": "删除作业可能会非常耗时。删除将在后台进行,数据可能不会马上消失。", "xpack.uptime.monitorDetails.ml.deleteMessage": "正在删除作业......", "xpack.uptime.monitorList.anomalyColumn.label": "响应异常分数", + "xpack.uptime.monitorList.defineConnector.description": "要开始启用告警,请在以下位置定义默认告警操作连接器", + "xpack.uptime.monitorList.disableDownAlert": "禁用状态告警", "xpack.uptime.monitorList.downLineSeries.downLabel": "关闭", "xpack.uptime.monitorList.drawer.locations.statusDown": "在 {locations} 已关闭", "xpack.uptime.monitorList.drawer.locations.statusUp": "在 {locations} 正运行", "xpack.uptime.monitorList.drawer.missingLocation": "某些 Heartbeat 实例未定义位置。{link}到您的 Heartbeat 配置。", + "xpack.uptime.monitorList.enabledAlerts.noAlert": "没有为此监测启用告警。", + "xpack.uptime.monitorList.enabledAlerts.title": "已启用的告警:", + "xpack.uptime.monitorList.enableDownAlert": "启用状态告警", "xpack.uptime.monitorList.expandDrawerButton.ariaLabel": "展开 ID {id} 的监测行", "xpack.uptime.monitorList.geoName.helpLinkAnnotation": "添加位置", "xpack.uptime.monitorList.infraIntegrationAction.container.message": "显示容器指标", @@ -18453,10 +20918,15 @@ "xpack.uptime.monitorList.noDownHistory": "在选定时间范围内此监测从未{emphasizedText}。", "xpack.uptime.monitorList.noItemForSelectedFiltersMessage": "未找到匹配选定筛选条件的监测", "xpack.uptime.monitorList.noItemMessage": "未找到任何运行时间监测", - "xpack.uptime.monitorList.observabilityIntegrationsColumn.apmIntegrationLink.tooltip": "点击此处以在 APM 中查找“{domain}”。", + "xpack.uptime.monitorList.observabilityIntegrationsColumn.apmIntegrationLink.tooltip": "单击此处可检查 APM 中的域“{domain}”或显式定义的“服务名称”。", "xpack.uptime.monitorList.observabilityIntegrationsColumn.popoverIconButton.ariaLabel": "打开 url {monitorUrl} 的监测的集成弹出式窗口", "xpack.uptime.monitorList.pageSizePopoverButtonText": "每页行数:{size}", "xpack.uptime.monitorList.pageSizeSelect.numRowsItemMessage": "{numRows} 行", + "xpack.uptime.monitorList.redirects.description": "执行 ping 时,Heartbeat 在 {number} 次重定向后运行。", + "xpack.uptime.monitorList.redirects.openWindow": "将在新窗口中打开链接。", + "xpack.uptime.monitorList.redirects.title": "重定向", + "xpack.uptime.monitorList.redirects.title.number": "{number}", + "xpack.uptime.monitorList.statusAlert.label": "状态告警", "xpack.uptime.monitorList.statusColumn.downLabel": "关闭", "xpack.uptime.monitorList.statusColumn.locStatusMessage": "在 {noLoc} 个位置", "xpack.uptime.monitorList.statusColumn.locStatusMessage.multiple": "在 {noLoc} 个位置", @@ -18471,8 +20941,8 @@ "xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel": "运行", "xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel": "检测状态", "xpack.uptime.monitorStatusBar.loadingMessage": "正在加载……", - "xpack.uptime.monitorStatusBar.locations.oneLocStatus": "在 {loc} 位置{status}", - "xpack.uptime.monitorStatusBar.locations.upStatus": "在 {loc} 位置{status}", + "xpack.uptime.monitorStatusBar.locations.oneLocStatus": "在 {loc} 位置处于 {status}", + "xpack.uptime.monitorStatusBar.locations.upStatus": "在 {loc} 位置处于 {status}", "xpack.uptime.monitorStatusBar.monitor.availability": "总体可用性", "xpack.uptime.monitorStatusBar.monitor.availabilityReport.availability": "可用性", "xpack.uptime.monitorStatusBar.monitor.availabilityReport.lastCheck": "上次检查", @@ -18484,11 +20954,22 @@ "xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel": "监测 URL 链接", "xpack.uptime.monitorStatusBar.sslCertificate.title": "TLS 证书", "xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "自上次检查以来经过的时间", + "xpack.uptime.monitorStatusBar.type.ariaLabel": "监测类型", + "xpack.uptime.monitorStatusBar.type.label": "类型", "xpack.uptime.navigateToAlertingButton.content": "管理告警", "xpack.uptime.navigateToAlertingUi": "离开 Uptime 并前往“Alerting 管理”页面", "xpack.uptime.notFountPage.homeLinkText": "返回主页", "xpack.uptime.openAlertContextPanel.ariaLabel": "打开告警上下文面板,以便可以选择告警类型", "xpack.uptime.openAlertContextPanel.label": "创建告警", + "xpack.uptime.overview.alerts.disabled.failed": "无法禁用告警!", + "xpack.uptime.overview.alerts.disabled.success": "已成功禁用告警!", + "xpack.uptime.overview.alerts.enabled.failed": "无法启用告警!", + "xpack.uptime.overview.alerts.enabled.success": "已成功启用告警! ", + "xpack.uptime.overview.alerts.enabled.success.description": "监测关闭时,消息将发送到 {actionConnectors}。", + "xpack.uptime.overview.pageHeader.syntheticsCallout.announcementLink": "阅读公告", + "xpack.uptime.overview.pageHeader.syntheticsCallout.content": "Uptime 现在正在预览对脚本化多步骤可用性检查的支持。这意味着您可以与网页元素进行交互,并检查整个过程(例如购买或登录系统)的可用性,而不仅仅是简单的单个页面启动/关闭检查。请单击下面的内容以了解详情,如果您想率先使用这些功能,则可以下载我们的预览组合代理,并在 Uptime 中查看组合检查。", + "xpack.uptime.overview.pageHeader.syntheticsCallout.dismissButtonText": "关闭", + "xpack.uptime.overview.pageHeader.syntheticsCallout.title": "Elastic Synthetics", "xpack.uptime.overviewPage.headerText": "概览", "xpack.uptime.overviewPageLink.disabled.ariaLabel": "禁用的分页按钮表示在监测列表中无法进行进一步导航。", "xpack.uptime.overviewPageLink.next.ariaLabel": "下页结果", @@ -18530,6 +21011,7 @@ "xpack.uptime.settings.invalid.error": "值必须大于 0。", "xpack.uptime.settings.invalid.nanError": "值必须为整数。", "xpack.uptime.settings.returnToOverviewLinkLabel": "返回到概览", + "xpack.uptime.settings.saveSuccess": "设置已保存!", "xpack.uptime.settingsBreadcrumbText": "设置", "xpack.uptime.snapshot.donutChart.ariaLabel": "显示当前状态的饼图。{total} 个监测中有 {down} 个已关闭。", "xpack.uptime.snapshot.donutChart.legend.downRowLabel": "关闭", @@ -18549,10 +21031,15 @@ "xpack.uptime.sourceConfiguration.ageLimit.units.days": "天", "xpack.uptime.sourceConfiguration.ageLimitThresholdInput.ariaLabel": "该输入控制 Kibana 显示警告之前 TLS 证书有效的最大天数。", "xpack.uptime.sourceConfiguration.ageThresholdDefaultValue": "默认值为 {defaultValue}", + "xpack.uptime.sourceConfiguration.alertConnectors": "告警连接器", + "xpack.uptime.sourceConfiguration.alertDefaultForm.selectConnector": "请选择一个或多个连接器", + "xpack.uptime.sourceConfiguration.alertDefaults": "告警默认值", "xpack.uptime.sourceConfiguration.applySettingsButtonLabel": "应用更改", "xpack.uptime.sourceConfiguration.certificateExpirationThresholdInput.ariaLabel": "该输入控制 Kibana 显示警告之前离 TLS 证书到期剩余的最小天数。", "xpack.uptime.sourceConfiguration.certificateThresholdDescription": "更改显示并告警证书错误的阈值。注意:这会影响任何配置的告警。", "xpack.uptime.sourceConfiguration.certificationSectionTitle": "证书到期", + "xpack.uptime.sourceConfiguration.defaultConnectors": "默认连接器", + "xpack.uptime.sourceConfiguration.defaultConnectors.description": "要用于发送告警的默认连接器。", "xpack.uptime.sourceConfiguration.discardSettingsButtonLabel": "取消", "xpack.uptime.sourceConfiguration.errorStateLabel": "到期阈值", "xpack.uptime.sourceConfiguration.expirationThreshold": "到期/使用时间阈值", @@ -18563,12 +21050,38 @@ "xpack.uptime.sourceConfiguration.heartbeatIndicesTitle": "Uptime 索引", "xpack.uptime.sourceConfiguration.indicesSectionTitle": "索引", "xpack.uptime.sourceConfiguration.warningStateLabel": "使用时间限制", + "xpack.uptime.synthetics.consoleStepList.message": "此过程无法运行,记录的控制台输出如下所示:", + "xpack.uptime.synthetics.consoleStepList.title": "未执行步骤", + "xpack.uptime.synthetics.emptyJourney.message.checkGroupField": "该过程的检查组是 {codeBlock}。", + "xpack.uptime.synthetics.emptyJourney.message.footer": "没有更多可显示的信息。", + "xpack.uptime.synthetics.emptyJourney.message.heading": "此过程不包含任何步骤。", + "xpack.uptime.synthetics.emptyJourney.title": "没有此过程的任何步骤", + "xpack.uptime.synthetics.executedJourney.heading": "摘要信息", + "xpack.uptime.synthetics.executedStep.errorHeading": "错误", + "xpack.uptime.synthetics.executedStep.scriptHeading": "步骤脚本", + "xpack.uptime.synthetics.executedStep.stackTrace": "堆栈跟踪", + "xpack.uptime.synthetics.executedStep.stepName": "{stepNumber}:{stepName}", + "xpack.uptime.synthetics.experimentalCallout.title": "实验功能", + "xpack.uptime.synthetics.journey.allFailedMessage": "{total} 个步骤 - 全部失败或跳过", + "xpack.uptime.synthetics.journey.allSucceededMessage": "{total} 个步骤 - 全部成功", + "xpack.uptime.synthetics.journey.partialSuccessMessage": "{total} 个步骤 - {succeeded} 个成功", + "xpack.uptime.synthetics.screenshot.noImageMessage": "没有可用图像", + "xpack.uptime.synthetics.screenshotDisplay.altText": "名称为“{stepName}”的步骤的屏幕截图", + "xpack.uptime.synthetics.screenshotDisplay.altTextWithoutName": "屏幕截图", + "xpack.uptime.synthetics.screenshotDisplay.fullScreenshotAltText": "名称为“{stepName}”的步骤的完整屏幕截图", + "xpack.uptime.synthetics.screenshotDisplay.fullScreenshotAltTextWithoutName": "完整屏幕截图", + "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltText": "名称为“{stepName}”的步骤的缩略屏幕截图", + "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltTextWithoutName": "缩略屏幕截图", + "xpack.uptime.synthetics.statusBadge.failedMessage": "失败", + "xpack.uptime.synthetics.statusBadge.skippedMessage": "已跳过", + "xpack.uptime.synthetics.statusBadge.succeededMessage": "成功", "xpack.uptime.title": "运行时间", "xpack.uptime.toggleAlertButton.content": "监测状态告警", "xpack.uptime.toggleAlertFlyout.ariaLabel": "打开添加告警浮出控件", "xpack.uptime.toggleTlsAlertButton.ariaLabel": "打开 TLS 告警浮出控件", "xpack.uptime.toggleTlsAlertButton.content": "TLS 告警", "xpack.uptime.uptimeFeatureCatalogueTitle": "运行时间", + "xpack.urlDrilldown.DisplayName": "前往 URL", "xpack.watcher.app.licenseErrorLinkText": "管理您的许可。", "xpack.watcher.app.licenseErrorTitle": "许可错误", "xpack.watcher.appName": "Watcher", @@ -18711,6 +21224,7 @@ "xpack.watcher.sections.watchEdit.errorTitle": "加载监视时出错", "xpack.watcher.sections.watchEdit.json.cancelButtonLabel": "鍙栨秷", "xpack.watcher.sections.watchEdit.json.createButtonLabel": "创建监视", + "xpack.watcher.sections.watchEdit.json.createSuccessNotificationText": "已创建“{watchDisplayName}”", "xpack.watcher.sections.watchEdit.json.editTabLabel": "编辑", "xpack.watcher.sections.watchEdit.json.error.invalidActionType": "为操作“{action}”提供的操作类型未知。", "xpack.watcher.sections.watchEdit.json.error.invalidIdText": "ID 只能包含字母、下划线、短划线、句点和数字。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx index 39c59a10fbc8..a91cf3e7552b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx @@ -52,7 +52,7 @@ export const EmailParamsFields = ({ {!addCC ? ( setAddCC(true)}> @@ -60,9 +60,8 @@ export const EmailParamsFields = ({ {!addBCC ? ( setAddBCC(true)}> ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 2517552304d8..019133b03d55 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -151,14 +151,14 @@ export const GET_ISSUE_API_ERROR = (id: string) => export const SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); export const SEARCH_ISSUES_PLACEHOLDER = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 7ee1e0d3f3fa..3e229c6a2333 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -114,7 +114,7 @@ describe('action_form', () => { describe('action_form in alert', () => { let wrapper: ReactWrapper; - async function setup() { + async function setup(customActions?: AlertAction[]) { const { loadAllActions } = jest.requireMock('../../lib/action_connector_api'); loadAllActions.mockResolvedValueOnce([ { @@ -177,6 +177,7 @@ describe('action_form', () => { show: true, }, }, + setHasActionsWithBrokenConnector: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; @@ -198,16 +199,18 @@ describe('action_form', () => { schedule: { interval: '1m', }, - actions: [ - { - group: 'default', - id: 'test', - actionTypeId: actionType.id, - params: { - message: '', - }, - }, - ], + actions: customActions + ? customActions + : [ + { + group: 'default', + id: 'test', + actionTypeId: actionType.id, + params: { + message: '', + }, + }, + ], tags: [], muteAll: false, enabled: false, @@ -229,6 +232,7 @@ describe('action_form', () => { setActionParamsProperty={(key: string, value: any, index: number) => (initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value }) } + setHasActionsWithBrokenConnector={deps!.setHasActionsWithBrokenConnector} http={deps!.http} actionTypeRegistry={deps!.actionTypeRegistry} defaultActionMessage={'Alert [{{ctx.metadata.name}}] has exceeded the threshold'} @@ -306,6 +310,7 @@ describe('action_form', () => { .find(`EuiToolTip [data-test-subj="${actionType.id}-ActionTypeSelectOption"]`) .exists() ).toBeFalsy(); + expect(deps.setHasActionsWithBrokenConnector).toHaveBeenLastCalledWith(false); }); it('does not render action types disabled by config', async () => { @@ -361,7 +366,7 @@ describe('action_form', () => { `); }); - it('does not render "Add new" button for preconfigured only action type', async () => { + it('does not render "Add connector" button for preconfigured only action type', async () => { await setup(); const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); actionOption.first().simulate('click'); @@ -392,5 +397,27 @@ describe('action_form', () => { ); expect(actionOption.exists()).toBeFalsy(); }); + + it('recognizes actions with broken connectors', async () => { + await setup([ + { + group: 'default', + id: 'test', + actionTypeId: actionType.id, + params: { + message: '', + }, + }, + { + group: 'default', + id: 'connector-doesnt-exist', + actionTypeId: actionType.id, + params: { + message: 'broken', + }, + }, + ]); + expect(deps.setHasActionsWithBrokenConnector).toHaveBeenLastCalledWith(true); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 1b176e0f63db..61cf3f2d3792 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -62,6 +62,7 @@ interface ActionAccordionFormProps { messageVariables?: ActionVariable[]; defaultActionMessage?: string; setHasActionsDisabled?: (value: boolean) => void; + setHasActionsWithBrokenConnector?: (value: boolean) => void; capabilities: ApplicationStart['capabilities']; } @@ -83,6 +84,7 @@ export const ActionForm = ({ defaultActionMessage, toastNotifications, setHasActionsDisabled, + setHasActionsWithBrokenConnector, capabilities, docLinks, }: ActionAccordionFormProps) => { @@ -171,6 +173,16 @@ export const ActionForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [connectors, actionTypesIndex]); + useEffect(() => { + const hasActionWithBrokenConnector = actions.some( + (action) => !connectors.find((connector) => connector.id === action.id) + ); + if (setHasActionsWithBrokenConnector) { + setHasActionsWithBrokenConnector(hasActionWithBrokenConnector); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actions, connectors]); + const preconfiguredMessage = i18n.translate( 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage', { @@ -267,7 +279,7 @@ export const ActionForm = ({ }} > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index f60199bc47f4..0863465833c0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -75,6 +75,8 @@ describe('connector_add_flyout', () => { ); expect(wrapper.find('ActionTypeMenu')).toHaveLength(1); expect(wrapper.find(`[data-test-subj="${actionType.id}-card"]`).exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="cancelButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="backButton"]').exists()).toBeFalsy(); }); it('renders banner with subscription links when gold features are disabled due to licensing ', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 9bb9d07307e1..060a751677de 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -251,14 +251,31 @@ export const ConnectorAddFlyout = ({ - - {i18n.translate( - 'xpack.triggersActionsUI.sections.actionConnectorAdd.cancelButtonLabel', - { - defaultMessage: 'Cancel', - } - )} - + {!actionType ? ( + + {i18n.translate( + 'xpack.triggersActionsUI.sections.actionConnectorAdd.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + )} + + ) : ( + { + setActionType(undefined); + setConnector(initialConnector); + }} + > + {i18n.translate( + 'xpack.triggersActionsUI.sections.actionConnectorAdd.backButtonLabel', + { + defaultMessage: 'Back', + } + )} + + )} 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/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 999873a650f0..b60aa04ee9f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -38,6 +38,9 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); const [isSaving, setIsSaving] = useState(false); const [hasActionsDisabled, setHasActionsDisabled] = useState(false); + const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState( + false + ); const setAlert = (key: string, value: any) => { dispatch({ command: { type: 'setAlert' }, payload: { key, value } }); }; @@ -155,6 +158,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { errors={errors} canChangeTrigger={false} setHasActionsDisabled={setHasActionsDisabled} + setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector} operation="i18n.translate('xpack.triggersActionsUI.sections.alertEdit.operationName', { defaultMessage: 'edit', })" @@ -176,7 +180,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { data-test-subj="saveEditedAlertButton" type="submit" iconType="check" - isDisabled={hasErrors || hasActionErrors} + isDisabled={hasErrors || hasActionErrors || hasActionsWithBrokenConnector} isLoading={isSaving} onClick={async () => { setIsSaving(true); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index c69c33c0fe22..8800f149c033 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -81,6 +81,7 @@ interface AlertFormProps { errors: IErrorObject; canChangeTrigger?: boolean; // to hide Change trigger button setHasActionsDisabled?: (value: boolean) => void; + setHasActionsWithBrokenConnector?: (value: boolean) => void; operation: string; } @@ -90,6 +91,7 @@ export const AlertForm = ({ dispatch, errors, setHasActionsDisabled, + setHasActionsWithBrokenConnector, operation, }: AlertFormProps) => { const alertsContext = useAlertsContext(); @@ -260,6 +262,7 @@ export const AlertForm = ({ diff --git a/x-pack/plugins/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/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts index 64b8cc49292b..68a9654316d4 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts @@ -139,3 +139,161 @@ describe('date helper', () => { ); }); }); + +describe('formatNumber helper', () => { + test('formats string numbers', () => { + const url = 'https://elastic.co/{{formatNumber value "0.0"}}'; + expect(compile(url, { value: '32.9999' })).toMatchInlineSnapshot(`"https://elastic.co/33.0"`); + expect(compile(url, { value: '32.555' })).toMatchInlineSnapshot(`"https://elastic.co/32.6"`); + }); + + test('formats numbers', () => { + const url = 'https://elastic.co/{{formatNumber value "0.0"}}'; + expect(compile(url, { value: 32.9999 })).toMatchInlineSnapshot(`"https://elastic.co/33.0"`); + expect(compile(url, { value: 32.555 })).toMatchInlineSnapshot(`"https://elastic.co/32.6"`); + }); + + test("doesn't fail on Nan", () => { + const url = 'https://elastic.co/{{formatNumber value "0.0"}}'; + expect(compile(url, { value: null })).toMatchInlineSnapshot(`"https://elastic.co/"`); + expect(compile(url, { value: undefined })).toMatchInlineSnapshot(`"https://elastic.co/"`); + expect(compile(url, { value: 'not a number' })).toMatchInlineSnapshot( + `"https://elastic.co/not%20a%20number"` + ); + }); + + test('fails on missing format string', () => { + const url = 'https://elastic.co/{{formatNumber value}}'; + expect(() => compile(url, { value: 12 })).toThrowError(); + }); + + // this doesn't work and doesn't seem + // possible to validate with our version of numeral + test.skip('fails on malformed format string', () => { + const url = 'https://elastic.co/{{formatNumber value "not a real format string"}}'; + expect(() => compile(url, { value: 12 })).toThrowError(); + }); +}); + +describe('replace helper', () => { + test('replaces all occurrences', () => { + const url = 'https://elastic.co/{{replace value "replace-me" "with-me"}}'; + + expect(compile(url, { value: 'replace-me test replace-me' })).toMatchInlineSnapshot( + `"https://elastic.co/with-me%20test%20with-me"` + ); + }); + + test('can be used to remove a substring', () => { + const url = 'https://elastic.co/{{replace value "Label:" ""}}'; + + expect(compile(url, { value: 'Label:Feature:Something' })).toMatchInlineSnapshot( + `"https://elastic.co/Feature:Something"` + ); + }); + + test('works if no matches', () => { + const url = 'https://elastic.co/{{replace value "Label:" ""}}'; + + expect(compile(url, { value: 'No matches' })).toMatchInlineSnapshot( + `"https://elastic.co/No%20matches"` + ); + }); + + test('throws on incorrect args', () => { + expect(() => + compile('https://elastic.co/{{replace value "Label:"}}', { value: 'No matches' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[replace]: \\"searchString\\" and \\"valueString\\" parameters expected to be strings, but not a string or missing"` + ); + expect(() => + compile('https://elastic.co/{{replace value "Label:" 4}}', { value: 'No matches' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[replace]: \\"searchString\\" and \\"valueString\\" parameters expected to be strings, but not a string or missing"` + ); + expect(() => + compile('https://elastic.co/{{replace value 4 ""}}', { value: 'No matches' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[replace]: \\"searchString\\" and \\"valueString\\" parameters expected to be strings, but not a string or missing"` + ); + expect(() => + compile('https://elastic.co/{{replace value}}', { value: 'No matches' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[replace]: \\"searchString\\" and \\"valueString\\" parameters expected to be strings, but not a string or missing"` + ); + }); +}); + +describe('basic string formatting helpers', () => { + test('lowercase', () => { + const compileUrl = (value: unknown) => + compile('https://elastic.co/{{lowercase value}}', { value }); + + expect(compileUrl('Some String Value')).toMatchInlineSnapshot( + `"https://elastic.co/some%20string%20value"` + ); + expect(compileUrl(4)).toMatchInlineSnapshot(`"https://elastic.co/4"`); + expect(compileUrl(null)).toMatchInlineSnapshot(`"https://elastic.co/null"`); + }); + test('uppercase', () => { + const compileUrl = (value: unknown) => + compile('https://elastic.co/{{uppercase value}}', { value }); + + expect(compileUrl('Some String Value')).toMatchInlineSnapshot( + `"https://elastic.co/SOME%20STRING%20VALUE"` + ); + expect(compileUrl(4)).toMatchInlineSnapshot(`"https://elastic.co/4"`); + expect(compileUrl(null)).toMatchInlineSnapshot(`"https://elastic.co/NULL"`); + }); + test('trim', () => { + const compileUrl = (fn: 'trim' | 'trimLeft' | 'trimRight', value: unknown) => + compile(`https://elastic.co/{{${fn} value}}`, { value }); + + expect(compileUrl('trim', ' trim-me ')).toMatchInlineSnapshot(`"https://elastic.co/trim-me"`); + expect(compileUrl('trimRight', ' trim-me ')).toMatchInlineSnapshot( + `"https://elastic.co/%20%20trim-me"` + ); + expect(compileUrl('trimLeft', ' trim-me ')).toMatchInlineSnapshot( + `"https://elastic.co/trim-me%20%20"` + ); + }); + test('left,right,mid', () => { + const compileExpression = (expression: string, value: unknown) => + compile(`https://elastic.co/${expression}`, { value }); + + expect(compileExpression('{{left value 3}}', '12345')).toMatchInlineSnapshot( + `"https://elastic.co/123"` + ); + expect(compileExpression('{{right value 3}}', '12345')).toMatchInlineSnapshot( + `"https://elastic.co/345"` + ); + expect(compileExpression('{{mid value 1 3}}', '12345')).toMatchInlineSnapshot( + `"https://elastic.co/234"` + ); + }); + + test('concat', () => { + expect( + compile(`https://elastic.co/{{concat value1 "," value2}}`, { value1: 'v1', value2: 'v2' }) + ).toMatchInlineSnapshot(`"https://elastic.co/v1,v2"`); + + expect( + compile(`https://elastic.co/{{concat valueArray}}`, { valueArray: ['1', '2', '3'] }) + ).toMatchInlineSnapshot(`"https://elastic.co/1,2,3"`); + }); + + test('split', () => { + expect( + compile( + `https://elastic.co/{{lookup (split value ",") 0 }}&{{lookup (split value ",") 1 }}`, + { + value: '47.766201,-122.257057', + } + ) + ).toMatchInlineSnapshot(`"https://elastic.co/47.766201&-122.257057"`); + + expect(() => + compile(`https://elastic.co/{{split value}}`, { value: '47.766201,-122.257057' }) + ).toThrowErrorMatchingInlineSnapshot(`"[split] \\"splitter\\" expected to be a string"`); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts index 2c3537636b9d..f4a1acff8762 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts @@ -8,6 +8,7 @@ import { create as createHandlebars, HelperDelegate, HelperOptions } from 'handl import { encode, RisonValue } from 'rison-node'; import dateMath from '@elastic/datemath'; import moment, { Moment } from 'moment'; +import numeral from '@elastic/numeral'; const handlebars = createHandlebars(); @@ -69,6 +70,52 @@ handlebars.registerHelper('date', (...args) => { return format ? momentDate.format(format) : momentDate.toISOString(); }); +handlebars.registerHelper('formatNumber', (rawValue: unknown, pattern: string) => { + if (!pattern || typeof pattern !== 'string') + throw new Error(`[formatNumber]: pattern string is required`); + const value = Number(rawValue); + if (rawValue == null || Number.isNaN(value)) return rawValue; + return numeral(value).format(pattern); +}); + +handlebars.registerHelper('lowercase', (rawValue: unknown) => String(rawValue).toLowerCase()); +handlebars.registerHelper('uppercase', (rawValue: unknown) => String(rawValue).toUpperCase()); +handlebars.registerHelper('trim', (rawValue: unknown) => String(rawValue).trim()); +handlebars.registerHelper('trimLeft', (rawValue: unknown) => String(rawValue).trimLeft()); +handlebars.registerHelper('trimRight', (rawValue: unknown) => String(rawValue).trimRight()); +handlebars.registerHelper('left', (rawValue: unknown, numberOfChars: number) => { + if (typeof numberOfChars !== 'number') + throw new Error('[left]: expected "number of characters to extract" to be a number'); + return String(rawValue).slice(0, numberOfChars); +}); +handlebars.registerHelper('right', (rawValue: unknown, numberOfChars: number) => { + if (typeof numberOfChars !== 'number') + throw new Error('[left]: expected "number of characters to extract" to be a number'); + return String(rawValue).slice(-numberOfChars); +}); +handlebars.registerHelper('mid', (rawValue: unknown, start: number, length: number) => { + if (typeof start !== 'number') throw new Error('[left]: expected "start" to be a number'); + if (typeof length !== 'number') throw new Error('[left]: expected "length" to be a number'); + return String(rawValue).substr(start, length); +}); +handlebars.registerHelper('concat', (...args) => { + const values = args.slice(0, -1) as unknown[]; + return values.join(''); +}); +handlebars.registerHelper('split', (...args) => { + const [str, splitter] = args.slice(0, -1) as [string, string]; + if (typeof splitter !== 'string') throw new Error('[split] "splitter" expected to be a string'); + return String(str).split(splitter); +}); +handlebars.registerHelper('replace', (...args) => { + const [str, searchString, valueString] = args.slice(0, -1) as [string, string, string]; + if (typeof searchString !== 'string' || typeof valueString !== 'string') + throw new Error( + '[replace]: "searchString" and "valueString" parameters expected to be strings, but not a string or missing' + ); + return String(str).split(searchString).join(valueString); +}); + export function compile(url: string, context: object): string { const template = handlebars.compile(url, { strict: true, noEscape: true }); return encodeURI(template(context)); 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/public/components/monitor/synthetics/__tests__/browser_expanded_row.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/browser_expanded_row.test.tsx index 191632d6ab71..07c3afdf50ee 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/browser_expanded_row.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/browser_expanded_row.test.tsx @@ -28,7 +28,7 @@ describe('BrowserExpandedRowComponent', () => { it('returns empty step state when no journey', () => { expect(shallowWithIntl()).toMatchInlineSnapshot( - `` + `` ); }); @@ -43,7 +43,7 @@ describe('BrowserExpandedRowComponent', () => { }} /> ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); it('displays loading spinner when loading', () => { @@ -111,6 +111,27 @@ describe('BrowserExpandedRowComponent', () => { `); }); + it('handles case where synth type is somehow missing', () => { + expect( + shallowWithIntl( + + ) + ).toMatchInlineSnapshot(`""`); + }); + it('renders console output step list when only console steps are present', () => { expect( shallowWithIntl( diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_event.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_event.test.tsx new file mode 100644 index 000000000000..ad905076a06c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_event.test.tsx @@ -0,0 +1,107 @@ +/* + * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { ConsoleEvent } from '../console_event'; + +describe('ConsoleEvent component', () => { + it('renders danger color for errors', () => { + expect( + shallowWithIntl( + + ) + ).toMatchInlineSnapshot(` + + + 123 + + + stderr + + + catastrophic error + + + `); + }); + + it('uses default color for non-errors', () => { + expect( + shallowWithIntl( + + ) + ).toMatchInlineSnapshot(` + + + 123 + + + cmd/status + + + not a catastrophic error + + + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_output_event_list.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_output_event_list.test.tsx new file mode 100644 index 000000000000..776fd0a5fb94 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_output_event_list.test.tsx @@ -0,0 +1,150 @@ +/* + * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { ConsoleOutputEventList } from '../console_output_event_list'; + +describe('ConsoleOutputEventList component', () => { + it('renders a component per console event', () => { + expect( + shallowWithIntl( + + ).find('EuiCodeBlock') + ).toMatchInlineSnapshot(` + + + + + + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/empty_journey.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/empty_journey.test.tsx new file mode 100644 index 000000000000..0157229b3c21 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/empty_journey.test.tsx @@ -0,0 +1,94 @@ +/* + * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { EmptyJourney } from '../empty_journey'; + +describe('EmptyJourney component', () => { + it('omits check group element when undefined', () => { + expect(shallowWithIntl()).toMatchInlineSnapshot(` + +

+ +

+

+ +

+ + } + iconType="cross" + title={ +

+ +

+ } + /> + `); + }); + + it('includes check group element when present', () => { + expect(shallowWithIntl()).toMatchInlineSnapshot(` + +

+ +

+

+ + check_group + , + } + } + /> +

+

+ +

+ + } + iconType="cross" + title={ +

+ +

+ } + /> + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx new file mode 100644 index 000000000000..5ab815a3c0b5 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx @@ -0,0 +1,259 @@ +/* + * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { ExecutedJourney } from '../executed_journey'; +import { Ping } from '../../../../../common/runtime_types'; + +const MONITOR_BOILERPLATE = { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', +}; + +describe('ExecutedJourney component', () => { + let steps: Ping[]; + + beforeEach(() => { + steps = [ + { + docId: '1', + timestamp: '123', + monitor: MONITOR_BOILERPLATE, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + }, + }, + { + docId: '2', + timestamp: '124', + monitor: MONITOR_BOILERPLATE, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + }, + }, + ]; + }); + + it('creates expected message for all failed', () => { + const wrapper = shallowWithIntl( + + ); + expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` + +

+ +

+

+ 2 Steps - all failed or skipped +

+
+ `); + }); + + it('creates expected message for all succeeded', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps[1].synthetics!.payload!.status = 'succeeded'; + const wrapper = shallowWithIntl( + + ); + expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` + +

+ +

+

+ 2 Steps - all succeeded +

+
+ `); + }); + + it('creates appropriate message for mixed results', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + const wrapper = shallowWithIntl( + + ); + expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` + +

+ +

+

+ 2 Steps - 1 succeeded +

+
+ `); + }); + + it('tallies skipped steps', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps[1].synthetics!.payload!.status = 'skipped'; + const wrapper = shallowWithIntl( + + ); + expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` + +

+ +

+

+ 2 Steps - 1 succeeded +

+
+ `); + }); + + it('uses appropriate count when non-step/end steps are included', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps.push({ + docId: '3', + timestamp: '125', + monitor: MONITOR_BOILERPLATE, + synthetics: { + type: 'stderr', + error: { + message: `there was an error, that's all we know`, + stack: 'your.error.happened.here', + }, + }, + }); + const wrapper = shallowWithIntl( + + ); + expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` + +

+ +

+

+ 2 Steps - 1 succeeded +

+
+ `); + }); + + it('renders a component per step', () => { + expect( + shallowWithIntl( + + ).find('EuiFlexGroup') + ).toMatchInlineSnapshot(` + + + + + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx index 2546c5fb9a5d..4b7461604b30 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx @@ -11,7 +11,7 @@ import { Ping } from '../../../../common/runtime_types'; import { getJourneySteps } from '../../../state/actions/journey'; import { JourneyState } from '../../../state/reducers/journey'; import { journeySelector } from '../../../state/selectors'; -import { EmptyStepState } from './empty_journey'; +import { EmptyJourney } from './empty_journey'; import { ExecutedJourney } from './executed_journey'; import { ConsoleOutputEventList } from './console_output_event_list'; @@ -51,7 +51,7 @@ export const BrowserExpandedRowComponent: FC = ({ checkGroup, jo } if (!journey || journey.steps.length === 0) { - return ; + return ; } if (journey.steps.some(stepEnd)) return ; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx index 9159c61532f1..8f3d6cec9932 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx @@ -7,6 +7,7 @@ import { EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; +import { Ping } from '../../../../common/runtime_types'; import { JourneyState } from '../../../state/reducers/journey'; import { ConsoleEvent } from './console_event'; @@ -14,6 +15,11 @@ interface Props { journey: JourneyState; } +const isConsoleStep = (step: Ping) => + step.synthetics?.type === 'stderr' || + step.synthetics?.type === 'stdout' || + step.synthetics?.type === 'cmd/status'; + export const ConsoleOutputEventList: FC = ({ journey }) => (
@@ -33,8 +39,8 @@ export const ConsoleOutputEventList: FC = ({ journey }) => (

- {journey.steps.map((consoleEvent) => ( - + {journey.steps.filter(isConsoleStep).map((consoleEvent) => ( + ))}
diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx index b6fead2bbbe0..4076d9ff7dfd 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx @@ -8,11 +8,11 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; -interface EmptyStepStateProps { +interface Props { checkGroup?: string; } -export const EmptyStepState: FC = ({ checkGroup }) => ( +export const EmptyJourney: FC = ({ checkGroup }) => ( = ({ journey }) => (

{statusMessage( - journey.steps.reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) + journey.steps + .filter(isStepEnd) + .reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) )}

- {journey.steps - .filter((step) => step.synthetics?.type === 'step/end') - .map((step, index) => ( - - ))} + {journey.steps.filter(isStepEnd).map((step, index) => ( + + ))}
); diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index cd2dc5018e11..730bb2277227 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -67,7 +67,7 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor read: [umDynamicSettings.name], }, alerting: { - all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + read: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], }, management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/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/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 6c0edd904b0e..b15a2cf8d1f1 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -35,6 +35,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/security_api_integration/session_idle.config.ts'), require.resolve('../test/security_api_integration/session_lifespan.config.ts'), require.resolve('../test/security_api_integration/login_selector.config.ts'), + require.resolve('../test/security_api_integration/audit.config.ts'), require.resolve('../test/token_api_integration/config.js'), require.resolve('../test/oidc_api_integration/config.ts'), require.resolve('../test/oidc_api_integration/implicit_flow.config.ts'), diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts index 110201674b39..280769bc09bc 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/home.ts @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const globalNav = getService('globalNav'); const testSubjects = getService('testSubjects'); - describe('Kibana Home', () => { + // FLAKY: https://github.com/elastic/kibana/issues/80929 + describe.skip('Kibana Home', () => { before(async () => { await PageObjects.common.navigateToApp('home'); }); 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/alerting_api_integration/spaces_only/tests/actions/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts index f3542c728845..2fa9fbe18730 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts @@ -54,5 +54,31 @@ export default function createActionTests({ getService }: FtrProviderContext) { id: response.body.id, }); }); + + it('should notify feature usage when creating a gold action type', async () => { + const testStart = new Date(); + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type', + actionTypeId: 'test.noop', + secrets: {}, + config: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, response.body.id, 'action', 'actions'); + + const { + body: { features }, + } = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const noopFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Noop' + ); + expect(noopFeature).to.be.ok(); + expect(noopFeature.last_used).to.be.a('string'); + expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(testStart.getTime()); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index f74c6eaa3298..2316585d2d0f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -216,6 +216,40 @@ export default function ({ getService }: FtrProviderContext) { }, }); }); + + it('should notify feature usage when executing a gold action type', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type', + actionTypeId: 'test.noop', + secrets: {}, + config: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const executionStart = new Date(); + await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .expect(200); + + const { + body: { features }, + } = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const noopFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Noop' + ); + expect(noopFeature).to.be.ok(); + expect(noopFeature.last_used).to.be.a('string'); + expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(executionStart.getTime()); + }); }); interface ValidateEventLogParams { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts index 81db8177b2c1..e06aec72f187 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -120,5 +121,41 @@ export default function updateActionTests({ getService }: FtrProviderContext) { message: `Preconfigured action custom-system-abc-connector is not allowed to update.`, }); }); + + it('should notify feature usage when editing a gold action type', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type', + actionTypeId: 'test.noop', + secrets: {}, + config: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const updateStart = new Date(); + await supertest + .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type updated', + secrets: {}, + config: {}, + }) + .expect(200); + + const { + body: { features }, + } = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const noopFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Noop' + ); + expect(noopFeature).to.be.ok(); + expect(noopFeature.last_used).to.be.a('string'); + expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(updateStart.getTime()); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index b94a54745237..40d88a6bface 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -343,5 +343,30 @@ instanceStateValue: true }, }); }); + + it('should notify feature usage when using a gold action type', async () => { + const testStart = new Date(); + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ reference }); + expect(response.statusCode).to.eql(200); + + // Wait for alert to run + await esTestIndexTool.waitForDocs('action:test.index-record', reference); + + const { + body: { features }, + } = await supertestWithoutAuth.get(`${getUrlPrefix(space.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const indexRecordFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Index Record' + ); + expect(indexRecordFeature).to.be.ok(); + expect(indexRecordFeature.last_used).to.be.a('string'); + expect(new Date(indexRecordFeature.last_used).getTime()).to.be.greaterThan( + testStart.getTime() + ); + + await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); + }); }); } diff --git a/x-pack/test/api_integration/apis/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/api_integration/apis/spaces/get_active_space.ts b/x-pack/test/api_integration/apis/spaces/get_active_space.ts index b925df391882..16cb03fe8a31 100644 --- a/x-pack/test/api_integration/apis/spaces/get_active_space.ts +++ b/x-pack/test/api_integration/apis/spaces/get_active_space.ts @@ -35,6 +35,20 @@ export default function ({ getService }: FtrProviderContext) { }); }); + it('returns the default space when explicitly referenced', async () => { + await supertest + .get('/s/default/internal/spaces/_active_space') + .set('kbn-xsrf', 'xxx') + .expect(200, { + id: 'default', + name: 'Default', + description: 'This is your default space!', + color: '#00bfb3', + disabledFeatures: [], + _reserved: true, + }); + }); + it('returns the foo space', async () => { await supertest .get('/s/foo-space/internal/spaces/_active_space') diff --git a/x-pack/test/apm_api_integration/common/authentication.ts b/x-pack/test/apm_api_integration/common/authentication.ts index 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/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index 6fd5e7e0c3ea..9a6c6f94dbb6 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -99,6 +99,25 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(definedHealthStatuses.length).to.be(0); }); }); + + describe('and fetching a list of services with a filter', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${encodeURIComponent( + `{"kuery":"service.name:opbeans-java","environment":"ENVIRONMENT_ALL"}` + )}` + ); + }); + + it('does not return health statuses for services that are not found in APM data', () => { + expect(response.status).to.be(200); + + expect(response.body.items.length).to.be(1); + + expect(response.body.items[0].serviceName).to.be('opbeans-java'); + }); + }); }); }); } diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts index 5ba1aac4c8f9..5195d28d8483 100644 --- a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts @@ -13,6 +13,8 @@ import { getServiceNowConnector, getJiraConnector, getResilientConnector, + getConnectorWithoutCaseOwned, + getConnectorWithoutMapping, } from '../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -36,13 +38,13 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return the correct connectors', async () => { - const { body: connectorOne } = await supertest + const { body: snConnector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') .send(getServiceNowConnector()) .expect(200); - const { body: connectorTwo } = await supertest + const { body: emailConnector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') .send({ @@ -59,22 +61,36 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - const { body: connectorThree } = await supertest + const { body: jiraConnector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') .send(getJiraConnector()) .expect(200); - const { body: connectorFour } = await supertest + const { body: resilientConnector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') .send(getResilientConnector()) .expect(200); - actionsRemover.add('default', connectorOne.id, 'action', 'actions'); - actionsRemover.add('default', connectorTwo.id, 'action', 'actions'); - actionsRemover.add('default', connectorThree.id, 'action', 'actions'); - actionsRemover.add('default', connectorFour.id, 'action', 'actions'); + const { body: connectorWithoutCaseOwned } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getConnectorWithoutCaseOwned()) + .expect(200); + + const { body: connectorNoMapping } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getConnectorWithoutMapping()) + .expect(200); + + actionsRemover.add('default', snConnector.id, 'action', 'actions'); + actionsRemover.add('default', emailConnector.id, 'action', 'actions'); + actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); + actionsRemover.add('default', resilientConnector.id, 'action', 'actions'); + actionsRemover.add('default', connectorWithoutCaseOwned.id, 'action', 'actions'); + actionsRemover.add('default', connectorNoMapping.id, 'action', 'actions'); const { body: connectors } = await supertest .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) @@ -82,16 +98,131 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(200); - expect(connectors.length).to.equal(3); - expect( - connectors.some((c: { actionTypeId: string }) => c.actionTypeId === '.servicenow') - ).to.equal(true); - expect(connectors.some((c: { actionTypeId: string }) => c.actionTypeId === '.jira')).to.equal( - true - ); - expect( - connectors.some((c: { actionTypeId: string }) => c.actionTypeId === '.resilient') - ).to.equal(true); + expect(connectors).to.eql([ + { + id: connectorWithoutCaseOwned.id, + actionTypeId: '.resilient', + name: 'Connector without isCaseOwned', + config: { + apiUrl: 'http://some.non.existent.com', + orgId: 'pkey', + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'name', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + isCaseOwned: null, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: jiraConnector.id, + actionTypeId: '.jira', + name: 'Jira Connector', + config: { + apiUrl: 'http://some.non.existent.com', + projectKey: 'pkey', + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'summary', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + isCaseOwned: true, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: resilientConnector.id, + actionTypeId: '.resilient', + name: 'Resilient Connector', + config: { + apiUrl: 'http://some.non.existent.com', + orgId: 'pkey', + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'name', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + isCaseOwned: true, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: snConnector.id, + actionTypeId: '.servicenow', + name: 'ServiceNow Connector', + config: { + apiUrl: 'http://some.non.existent.com', + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + isCaseOwned: true, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + ]); }); }); }; diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 8d28f647ce43..262e14fac6d8 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -116,7 +116,7 @@ export const getResilientConnector = () => ({ mapping: [ { source: 'title', - target: 'summary', + target: 'name', actionType: 'overwrite', }, { @@ -135,6 +135,51 @@ export const getResilientConnector = () => ({ }, }); +export const getConnectorWithoutCaseOwned = () => ({ + name: 'Connector without isCaseOwned', + actionTypeId: '.resilient', + secrets: { + apiKeyId: 'id', + apiKeySecret: 'secret', + }, + config: { + apiUrl: 'http://some.non.existent.com', + orgId: 'pkey', + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'name', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, +}); + +export const getConnectorWithoutMapping = () => ({ + name: 'Connector without mapping', + actionTypeId: '.resilient', + secrets: { + apiKeyId: 'id', + apiKeySecret: 'secret', + }, + config: { + apiUrl: 'http://some.non.existent.com', + orgId: 'pkey', + }, +}); + export const removeServerGeneratedPropertiesFromConfigure = ( config: Partial ): Partial => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts new file mode 100644 index 000000000000..42d4b86119bb --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts @@ -0,0 +1,704 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import expect from '@kbn/expect'; +import { SearchResponse } from 'elasticsearch'; +import { Signal } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; +import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { deleteAllExceptions } from '../../../lists_api_integration/utils'; +import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { CreateExceptionListItemSchema } from '../../../../plugins/lists/common'; +import { + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, +} from '../../../../plugins/lists/common/constants'; + +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_RULES_STATUS_URL, + DETECTION_ENGINE_QUERY_SIGNALS_URL, + DETECTION_ENGINE_PREPACKAGED_URL, +} from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + waitFor, + getQueryAllSignals, + downgradeImmutableRule, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('create_rules_with_exceptions', () => { + describe('creating rules with exceptions', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllExceptions(es); + }); + + it('should create a single rule with a rule_id and add an exception list to the rule', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + const expected: Partial = { + ...getSimpleRuleOutput(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(expected); + }); + + it('should create a single rule with an exception list and validate it ran successfully', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + // wait for Task Manager to execute the rule and update status + await waitFor(async () => { + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + return statusBody[body.id]?.current_status?.status === 'succeeded'; + }); + + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + const expected: Partial = { + ...getSimpleRuleOutput(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + expect(bodyToCompare).to.eql(expected); + expect(statusBody[body.id].current_status.status).to.eql('succeeded'); + }); + + it('should allow removing an exception list from an immutable rule through patch', async () => { + // add all the immutable rules from the pre-packaged url + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to use + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one + + // remove the exceptions list as a user is allowed to remove it from an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] }) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + expect(body.exceptions_list.length).to.eql(0); + }); + + it('should allow adding a second exception list to an immutable rule through patch', async () => { + // add all the immutable rules from the pre-packaged url + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to use + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one + + // add a second exceptions list as a user is allowed to add a second list to an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + expect(body.exceptions_list.length).to.eql(2); + }); + + it('should override any updates to pre-packaged rules if the user removes the exception list through the API but the new version of a rule has an exception list again', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to use + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one exception list + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + // remove the exceptions list as a user is allowed to remove it + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // get the pre-packaged rule after we upgraded it + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // We should have a length of 1 and it should be the same as our original before we tried to remove it using patch + expect(body.exceptions_list.length).to.eql(1); + expect(body.exceptions_list).to.eql(immutableRule.exceptions_list); + }); + + it('should merge back an exceptions_list if it was removed from the immutable rule through PATCH', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // remove the exception list and only have a single list that is not an endpoint_list + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // get the immutable rule after we installed it a second time + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // The installed rule should have both the original immutable exceptions list back and the + // new list the user added. + expect(body.exceptions_list).to.eql([ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ]); + }); + + it('should NOT add an extra exceptions_list that already exists on a rule during an upgrade', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // get the immutable rule after we installed it a second time + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // The installed rule should have both the original immutable exceptions list back and the + // new list the user added. + expect(body.exceptions_list).to.eql([...immutableRule.exceptions_list]); + }); + + it('should NOT allow updates to pre-packaged rules to overwrite existing exception based rules when the user adds an additional exception list', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // add a second exceptions list as a user is allowed to add a second list to an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // It should be the same as what the user added originally + expect(body.exceptions_list).to.eql([ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ]); + }); + + it('should not remove any exceptions added to a pre-packaged/immutable rule during an update if that rule has no existing exception lists', async () => { + // add all the immutable rules from the pre-packaged url + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_reg_beacon.json + // since this rule does not have existing exceptions_list that we are going to use for tests + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).eql(0); // make sure we have no exceptions_list + + // add a second exceptions list as a user is allowed to add a second list to an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6', + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade of the rule to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // ensure that the same exception is still on the rule + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + expect(body.exceptions_list).to.eql([ + { + id, + list_id, + namespace_type, + type, + }, + ]); + }); + + describe('tests with auditbeat data', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await esArchiver.load('auditbeat/hosts'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllExceptions(es); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should be able to execute against an exception list that does not include valid entries and get back 10 signals', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const exceptionListItem: CreateExceptionListItemSchema = { + ...getCreateExceptionListItemMinimalSchemaMock(), + entries: [ + { + field: 'some.none.existent.field', // non-existent field where we should not exclude anything + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + }; + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(exceptionListItem) + .expect(200); + + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + from: '1900-01-01T00:00:00.000Z', + query: 'host.name: "suricata-sensor-amsterdam"', + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + // wait until rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length > 0; + }); + + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + // expect there to be 10 + expect(signalsOpen.hits.hits.length).equal(10); + }); + + it('should be able to execute against an exception list that does include valid entries and get back 0 signals', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const exceptionListItem: CreateExceptionListItemSchema = { + ...getCreateExceptionListItemMinimalSchemaMock(), + entries: [ + { + field: 'host.name', // This matches the query below which will exclude everything + operator: 'included', + type: 'match', + value: 'suricata-sensor-amsterdam', + }, + ], + }; + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(exceptionListItem) + .expect(200); + + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + from: '1900-01-01T00:00:00.000Z', + query: 'host.name: "suricata-sensor-amsterdam"', // this matches all the exceptions we should exclude + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + const { body: resBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + // wait for Task Manager to finish executing the rule + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [resBody.id] }) + .expect(200); + return body[resBody.id]?.current_status?.status === 'succeeded'; + }); + + // Get the signals now that we are done running and expect the result to always be zero + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + // expect there to be 10 + expect(signalsOpen.hits.hits.length).equal(0); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts new file mode 100644 index 000000000000..6d3a0ce683cd --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { SearchResponse } from 'elasticsearch'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_RULES_STATUS_URL, + DETECTION_ENGINE_QUERY_SIGNALS_URL, +} from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getQueryAllSignals, + removeServerGeneratedProperties, + waitFor, +} from '../../utils'; + +import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks'; +import { Signal } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + /** + * Specific api integration tests for threat matching rule type + */ + describe('create_threat_matching', () => { + describe('validation errors', () => { + it('should give an error that the index must exist first if it does not exist before creating a rule', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getCreateThreatMatchRulesSchemaMock()) + .expect(400); + + expect(body).to.eql({ + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }); + }); + }); + + describe('creating threat match rule', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should create a single rule with a rule_id', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getCreateThreatMatchRulesSchemaMock()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock()); + }); + + it('should create a single rule with a rule_id and validate it ran successfully', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getCreateThreatMatchRulesSchemaMock()) + .expect(200); + + // wait for Task Manager to execute the rule and update status + await waitFor(async () => { + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + return statusBody[body.id]?.current_status?.status === 'succeeded'; + }); + + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock()); + expect(statusBody[body.id].current_status.status).to.eql('succeeded'); + }); + }); + + describe('tests with auditbeat data', () => { + beforeEach(async () => { + await deleteAllAlerts(es); + await createSignalsIndex(supertest); + await esArchiver.load('auditbeat/hosts'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should be able to execute and get 10 signals when doing a specific query', async () => { + const rule: CreateRulesSchema = { + ...getCreateThreatMatchRulesSchemaMock(), + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait until rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length > 0; + }); + + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + // expect there to be 10 + expect(signalsOpen.hits.hits.length).equal(10); + }); + + it('should return zero matches if the mapping does not match against anything in the mapping', async () => { + const rule: CreateRulesSchema = { + ...getCreateThreatMatchRulesSchemaMock(), + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'invalid.mapping.value', // invalid mapping value + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + // create the threat match rule + const { body: resBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait for Task Manager to finish executing the rule + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [resBody.id] }) + .expect(200); + return body[resBody.id]?.current_status?.status === 'succeeded'; + }); + + // Get the signals now that we are done running and expect the result to always be zero + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + expect(signalsOpen.hits.hits.length).equal(0); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index 779205377621..24b76853164f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -15,6 +15,8 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./add_prepackaged_rules')); loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./create_rules_bulk')); + loadTestFile(require.resolve('./create_threat_matching')); + loadTestFile(require.resolve('./create_exceptions')); loadTestFile(require.resolve('./delete_rules')); loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 5d82eed41d3c..db91529b8a2c 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Client } from '@elastic/elasticsearch'; +import { ApiResponse, Client } from '@elastic/elasticsearch'; import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; +import { Context } from '@elastic/elasticsearch/lib/Transport'; import { Status, SignalIds, @@ -14,7 +15,10 @@ import { import { CreateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema'; import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema'; import { RulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema'; -import { DETECTION_ENGINE_INDEX_URL } from '../../plugins/security_solution/common/constants'; +import { + DETECTION_ENGINE_INDEX_URL, + INTERNAL_RULE_ID_KEY, +} from '../../plugins/security_solution/common/constants'; /** * This will remove server generated properties such as date times, etc... @@ -245,34 +249,38 @@ export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial = * This will retry 20 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle */ -export const deleteAllAlerts = async (es: Client, retryCount = 20): Promise => { - if (retryCount > 0) { - try { - const result = await es.deleteByQuery({ - index: '.kibana', - q: 'type:alert', - wait_for_completion: true, - refresh: true, - conflicts: 'proceed', - body: {}, - }); - // deleteByQuery will cause version conflicts as alerts are being updated - // by background processes; the code below accounts for that - if (result.body.version_conflicts !== 0) { - throw new Error(`Version conflicts for ${result.body.version_conflicts} alerts`); - } - } catch (err) { - // eslint-disable-next-line no-console - console.log(`Error in deleteAllAlerts(), retries left: ${retryCount - 1}`, err); +export const deleteAllAlerts = async (es: Client): Promise => { + return countDownES(async () => { + return es.deleteByQuery({ + index: '.kibana', + q: 'type:alert', + wait_for_completion: true, + refresh: true, + conflicts: 'proceed', + body: {}, + }); + }, 'deleteAllAlerts'); +}; - // retry, counting down, and delay a bit before - await new Promise((resolve) => setTimeout(resolve, 250)); - await deleteAllAlerts(es, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteAllAlerts, no retries are left'); - } +export const downgradeImmutableRule = async (es: Client, ruleId: string): Promise => { + return countDownES(async () => { + return es.updateByQuery({ + index: '.kibana', + refresh: true, + wait_for_completion: true, + body: { + script: { + lang: 'painless', + source: 'ctx._source.alert.params.version--', + }, + query: { + term: { + 'alert.tags': `${INTERNAL_RULE_ID_KEY}:${ruleId}`, + }, + }, + }, + }); + }, 'downgradeImmutableRule'); }; /** @@ -295,27 +303,15 @@ export const deleteAllTimelines = async (es: Client): Promise => { * @param es The ElasticSearch handle */ export const deleteAllRulesStatuses = async (es: Client, retryCount = 20): Promise => { - if (retryCount > 0) { - try { - await es.deleteByQuery({ - index: '.kibana', - q: 'type:siem-detection-engine-rule-status', - wait_for_completion: true, - refresh: true, - body: {}, - }); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to deleteAllRulesStatuses, retries left are: ${retryCount - 1}`, - err - ); - await deleteAllRulesStatuses(es, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteAllRulesStatuses, no retries are left'); - } + return countDownES(async () => { + return es.deleteByQuery({ + index: '.kibana', + q: 'type:siem-detection-engine-rule-status', + wait_for_completion: true, + refresh: true, + body: {}, + }); + }, 'deleteAllRulesStatuses'); }; /** @@ -324,24 +320,12 @@ export const deleteAllRulesStatuses = async (es: Client, retryCount = 20): Promi * @param supertest The supertest client library */ export const createSignalsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.post(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to create the signals index, retries left are: ${retryCount - 1}`, - err - ); - await createSignalsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not createSignalsIndex, no retries are left'); - } + await countDownTest(async () => { + await supertest.post(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); + return true; + }, 'createSignalsIndex'); }; /** @@ -349,21 +333,12 @@ export const createSignalsIndex = async ( * @param supertest The supertest client library */ export const deleteSignalsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.delete(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log(`Failure trying to deleteSignalsIndex, retries left are: ${retryCount - 1}`, err); - await deleteSignalsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteSignalsIndex, no retries are left'); - } + await countDownTest(async () => { + await supertest.delete(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); + return true; + }, 'deleteSignalsIndex'); }; /** @@ -616,7 +591,7 @@ export const waitFor = async ( functionToTest: () => Promise, maxTimeout: number = 5000, timeoutWait: number = 10 -) => { +): Promise => { await new Promise(async (resolve, reject) => { let found = false; let numberOfTries = 0; @@ -636,3 +611,82 @@ export const waitFor = async ( } }); }; + +/** + * Does a plain countdown and checks against es queries for either conflicts in the error + * or for any over the wire issues such as timeouts or temp 404's to make the tests more + * reliant. + * @param esFunction The function to test against + * @param esFunctionName The name of the function to print if we encounter errors + * @param retryCount The number of times to retry before giving up (has default) + * @param timeoutWait Time to wait before trying again (has default) + */ +export const countDownES = async ( + esFunction: () => Promise, Context>>, + esFunctionName: string, + retryCount: number = 20, + timeoutWait = 250 +): Promise => { + await countDownTest( + async () => { + const result = await esFunction(); + if (result.body.version_conflicts !== 0) { + // eslint-disable-next-line no-console + console.log(`Version conflicts for ${result.body.version_conflicts}`); + return false; + } else { + return true; + } + }, + esFunctionName, + retryCount, + timeoutWait + ); +}; + +/** + * Does a plain countdown and checks against a boolean to determine if to wait and try again. + * This is useful for over the wire things that can cause issues such as conflict or timeouts + * for testing resiliency. + * @param functionToTest The function to test against + * @param name The name of the function to print if we encounter errors + * @param retryCount The number of times to retry before giving up (has default) + * @param timeoutWait Time to wait before trying again (has default) + */ +export const countDownTest = async ( + functionToTest: () => Promise, + name: string, + retryCount: number = 20, + timeoutWait = 250, + ignoreThrow: boolean = false +) => { + if (retryCount > 0) { + try { + const passed = await functionToTest(); + if (!passed) { + // eslint-disable-next-line no-console + console.log(`Failure trying to ${name}, retries left are: ${retryCount - 1}`); + // retry, counting down, and delay a bit before + await new Promise((resolve) => setTimeout(resolve, timeoutWait)); + await countDownTest(functionToTest, name, retryCount - 1, timeoutWait, ignoreThrow); + } + } catch (err) { + if (ignoreThrow) { + throw err; + } else { + // eslint-disable-next-line no-console + console.log( + `Failure trying to ${name}, with exception message of:`, + err.message, + `retries left are: ${retryCount - 1}` + ); + // retry, counting down, and delay a bit before + await new Promise((resolve) => setTimeout(resolve, timeoutWait)); + await countDownTest(functionToTest, name, retryCount - 1, timeoutWait, ignoreThrow); + } + } + } else { + // eslint-disable-next-line no-console + console.log(`Could not ${name}, no retries are left`); + } +}; diff --git a/x-pack/test/functional/apps/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/apps/saved_objects_management/index.ts b/x-pack/test/functional/apps/saved_objects_management/index.ts index ce550d3a5fa2..3f08d8713549 100644 --- a/x-pack/test/functional/apps/saved_objects_management/index.ts +++ b/x-pack/test/functional/apps/saved_objects_management/index.ts @@ -9,6 +9,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderContext) { describe('Saved objects management', function savedObjectsManagementAppTestSuite() { this.tags(['ciGroup2', 'skipFirefox']); + + loadTestFile(require.resolve('./spaces_integration')); loadTestFile(require.resolve('./feature_controls/saved_objects_management_security')); }); } diff --git a/x-pack/test/functional/apps/saved_objects_management/spaces_integration.ts b/x-pack/test/functional/apps/saved_objects_management/spaces_integration.ts new file mode 100644 index 000000000000..7cdf823d77f7 --- /dev/null +++ b/x-pack/test/functional/apps/saved_objects_management/spaces_integration.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const getSpacePrefix = (spaceId: string) => { + return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; +}; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects([ + 'common', + 'security', + 'savedObjects', + 'spaceSelector', + 'settings', + ]); + + const spaceId = 'space_1'; + + describe('spaces integration', () => { + before(async () => { + await esArchiver.load('saved_objects_management/spaces_integration'); + }); + + after(async () => { + await esArchiver.unload('saved_objects_management/spaces_integration'); + }); + + beforeEach(async () => { + await PageObjects.common.navigateToUrl('settings', 'kibana/objects', { + basePath: getSpacePrefix(spaceId), + shouldUseHashForSubUrl: false, + }); + await PageObjects.savedObjects.waitTableIsLoaded(); + }); + + it('redirects to correct url when inspecting an object from a non-default space', async () => { + const objects = await PageObjects.savedObjects.getRowTitles(); + expect(objects.includes('A Pie')).to.be(true); + + await PageObjects.savedObjects.clickInspectByTitle('A Pie'); + + await PageObjects.common.waitUntilUrlIncludes(getSpacePrefix(spaceId)); + + expect(await testSubjects.getAttribute(`savedObjects-editField-title`, 'value')).to.eql( + 'A Pie' + ); + }); + }); +} diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index 60c166d83793..96f16aebd11b 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -17,6 +17,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Role Mappings', function () { before(async () => { + // Delete any existing role mappings. ESS commonly sets up a role mapping automatically. + const existingMappings = await security.roleMappings.getAll(); + await Promise.all(existingMappings.map((m) => security.roleMappings.delete(m.name))); + await pageObjects.common.navigateToApp('roleMappings'); }); diff --git a/x-pack/test/functional/es_archives/saved_objects_management/spaces_integration/data.json b/x-pack/test/functional/es_archives/saved_objects_management/spaces_integration/data.json new file mode 100644 index 000000000000..e6f4a0d00e19 --- /dev/null +++ b/x-pack/test/functional/es_archives/saved_objects_management/spaces_integration/data.json @@ -0,0 +1,96 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana", + "source": { + "space": { + "_reserved": true, + "description": "This is the default space", + "name": "Default Space" + }, + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "space:space_1", + "index": ".kibana", + "source": { + "space": { + "description": "This is the first test space", + "name": "Space 1" + }, + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space_1:visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", + "source": { + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "namespace": "space_1", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/saved_objects_management/spaces_integration/mappings.json b/x-pack/test/functional/es_archives/saved_objects_management/spaces_integration/mappings.json new file mode 100644 index 000000000000..84ea7d0aaca9 --- /dev/null +++ b/x-pack/test/functional/es_archives/saved_objects_management/spaces_integration/mappings.json @@ -0,0 +1,473 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape", + "tree": "quadtree" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "references": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } +} 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/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index f56e0e2629d4..f55114cf11d1 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -40,6 +40,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.clickCreateConnectorButton(); + await testSubjects.click('.index-card'); + + await find.clickByCssSelector('[data-test-subj="backButton"]'); + await testSubjects.click('.slack-card'); await testSubjects.setValue('nameInput', connectorName); 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/epm/data_stream.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts index b9558240ca00..d1d909f773a2 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts @@ -12,6 +12,8 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); const es = getService('es'); + const dockerServers = getService('dockerServers'); + const server = dockerServers.get('registry'); const pkgName = 'datastreams'; const pkgVersion = '0.1.0'; const pkgUpdateVersion = '0.2.0'; @@ -63,6 +65,7 @@ export default function (providerContext: FtrProviderContext) { }); }); afterEach(async () => { + if (!server) return; await es.transport.request({ method: 'DELETE', path: `/_data_stream/${logsTemplateName}-default`, diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts index 6b43c9d74c6b..6fd4b64f0ee5 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts @@ -9,6 +9,7 @@ import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL, } from '../../../../plugins/ingest_manager/common'; +import { skipIfNoDockerRegistry } from '../../helpers'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; export default function (providerContext: FtrProviderContext) { @@ -19,12 +20,14 @@ export default function (providerContext: FtrProviderContext) { const pkgVersion = '0.1.0'; const pkgUpdateVersion = '0.2.0'; describe('setup checks packages completed install', async () => { + skipIfNoDockerRegistry(providerContext); describe('package install', async () => { before(async () => { await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.1.0`) .set('kbn-xsrf', 'xxxx') - .send({ force: true }); + .send({ force: true }) + .expect(200); }); it('should have not reinstalled if package install completed', async function () { const packageBeforeSetup = await kibanaServer.savedObjects.get({ @@ -32,7 +35,7 @@ export default function (providerContext: FtrProviderContext) { id: pkgName, }); const installStartedAtBeforeSetup = packageBeforeSetup.attributes.install_started_at; - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -51,7 +54,7 @@ export default function (providerContext: FtrProviderContext) { install_started_at: previousInstallDate, }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -71,7 +74,7 @@ export default function (providerContext: FtrProviderContext) { install_started_at: previousInstallDate, }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -83,7 +86,8 @@ export default function (providerContext: FtrProviderContext) { after(async () => { await supertest .delete(`/api/fleet/epm/packages/multiple_versions-0.1.0`) - .set('kbn-xsrf', 'xxxx'); + .set('kbn-xsrf', 'xxxx') + .expect(200); }); }); describe('package update', async () => { @@ -91,11 +95,13 @@ export default function (providerContext: FtrProviderContext) { await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.1.0`) .set('kbn-xsrf', 'xxxx') - .send({ force: true }); + .send({ force: true }) + .expect(200); await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.2.0`) .set('kbn-xsrf', 'xxxx') - .send({ force: true }); + .send({ force: true }) + .expect(200); }); it('should have not reinstalled if package update completed', async function () { const packageBeforeSetup = await kibanaServer.savedObjects.get({ @@ -103,7 +109,7 @@ export default function (providerContext: FtrProviderContext) { id: pkgName, }); const installStartedAtBeforeSetup = packageBeforeSetup.attributes.install_started_at; - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -124,7 +130,7 @@ export default function (providerContext: FtrProviderContext) { install_version: pkgUpdateVersion, // set version back }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -147,7 +153,7 @@ export default function (providerContext: FtrProviderContext) { version: pkgVersion, // set version back }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -160,7 +166,8 @@ export default function (providerContext: FtrProviderContext) { after(async () => { await supertest .delete(`/api/fleet/epm/packages/multiple_versions-0.1.0`) - .set('kbn-xsrf', 'xxxx'); + .set('kbn-xsrf', 'xxxx') + .expect(200); }); }); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts index f90c1d7bcbd6..c5426168eb78 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 @@ -11,6 +11,10 @@ import { setupIngest } from './services'; import { skipIfNoDockerRegistry } from '../../../helpers'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../../../plugins/ingest_manager/common'; +const makeSnapshotVersion = (version: string) => { + return version.endsWith('-SNAPSHOT') ? version : `${version}-SNAPSHOT`; +}; + export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); @@ -48,6 +52,43 @@ export default function (providerContext: FtrProviderContext) { const res = await supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'); expect(typeof res.body.item.upgrade_started_at).to.be('string'); }); + it('should respond 400 if upgrading agent with version the same as snapshot version', async () => { + const kibanaVersion = await kibanaServer.version.get(); + const kibanaVersionSnapshot = makeSnapshotVersion(kibanaVersion); + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: kibanaVersion } } }, + }, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersionSnapshot, + }) + .expect(400); + }); + it('should respond 200 if upgrading agent with version less than kibana snapshot version', async () => { + const kibanaVersion = await kibanaServer.version.get(); + const kibanaVersionSnapshot = makeSnapshotVersion(kibanaVersion); + + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersionSnapshot, + }) + .expect(200); + }); it('should respond 200 to upgrade agent and update the agent SO without source_uri', async () => { const kibanaVersion = await kibanaServer.version.get(); await kibanaServer.savedObjects.update({ @@ -121,9 +162,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 +194,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 +228,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 +236,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 +271,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 +299,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/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 54a13fc027c9..5870239b73ed 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -15,6 +15,7 @@ import { } from '../../plugins/lists/common/schemas'; import { ListSchema } from '../../plugins/lists/common'; import { LIST_INDEX } from '../../plugins/lists/common/constants'; +import { countDownES, countDownTest } from '../detection_engine_api_integration/utils'; /** * Creates the lists and lists items index for use inside of beforeEach blocks of tests @@ -22,24 +23,12 @@ import { LIST_INDEX } from '../../plugins/lists/common/constants'; * @param supertest The supertest client library */ export const createListsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to create the lists index, retries left are: ${retryCount - 1}`, - err - ); - await createListsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not createListsIndex, no retries are left'); - } + return countDownTest(async () => { + await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send(); + return true; + }, 'createListsIndex'); }; /** @@ -47,21 +36,26 @@ export const createListsIndex = async ( * @param supertest The supertest client library */ export const deleteListsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.delete(LIST_INDEX).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log(`Failure trying to deleteListsIndex, retries left are: ${retryCount - 1}`, err); - await deleteListsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteListsIndex, no retries are left'); - } + return countDownTest(async () => { + await supertest.delete(LIST_INDEX).set('kbn-xsrf', 'true').send(); + return true; + }, 'deleteListsIndex'); +}; + +/** + * Creates the exception lists and lists items index for use inside of beforeEach blocks of tests + * This will retry 20 times before giving up and hopefully still not interfere with other tests + * @param supertest The supertest client library + */ +export const createExceptionListsIndex = async ( + supertest: SuperTest +): Promise => { + return countDownTest(async () => { + await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send(); + return true; + }, 'createListsIndex'); }; /** @@ -159,26 +153,14 @@ export const binaryToString = (res: any, callback: any): void => { * This will retry 20 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle */ -export const deleteAllExceptions = async (es: Client, retryCount = 20): Promise => { - if (retryCount > 0) { - try { - await es.deleteByQuery({ - index: '.kibana', - q: 'type:exception-list or type:exception-list-agnostic', - wait_for_completion: true, - refresh: true, - body: {}, - }); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to deleteAllExceptions, retries left are: ${retryCount - 1}`, - err - ); - await deleteAllExceptions(es, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteAllExceptions, no retries are left'); - } +export const deleteAllExceptions = async (es: Client): Promise => { + return countDownES(async () => { + return es.deleteByQuery({ + index: '.kibana', + q: 'type:exception-list or type:exception-list-agnostic', + wait_for_completion: true, + refresh: true, + body: {}, + }); + }, 'deleteAllExceptions'); }; diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index 40a3b3cf1877..e7d96023f365 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -27,7 +27,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { // list paths to the files that contain your plugins tests testFiles: [ - resolve(__dirname, './test_suites/audit_trail'), resolve(__dirname, './test_suites/resolver'), resolve(__dirname, './test_suites/global_search'), ], @@ -50,12 +49,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { )}`, // Required to load new platform plugins via `--plugin-path` flag. '--env.name=development', - - '--xpack.audit_trail.enabled=true', - '--xpack.audit_trail.logger.enabled=true', - '--xpack.audit_trail.appender.kind=file', - '--xpack.audit_trail.appender.path=x-pack/test/plugin_functional/plugins/audit_trail_test/server/pattern_debug.log', - '--xpack.audit_trail.appender.layout.kind=json', ], }, uiSettings: xpackFunctionalConfig.get('uiSettings'), diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/.gitignore b/x-pack/test/plugin_functional/plugins/audit_trail_test/server/.gitignore deleted file mode 100644 index 9a3d28117919..000000000000 --- a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/*debug.log diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/plugin.ts b/x-pack/test/plugin_functional/plugins/audit_trail_test/server/plugin.ts deleted file mode 100644 index 264f436fb1dc..000000000000 --- a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/plugin.ts +++ /dev/null @@ -1,65 +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 { Plugin, CoreSetup } from 'src/core/server'; - -export class AuditTrailTestPlugin implements Plugin { - public setup(core: CoreSetup) { - core.savedObjects.registerType({ - name: 'audit_trail_test', - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: {}, - }, - }); - - const router = core.http.createRouter(); - router.get( - { path: '/audit_trail_test/context/as_current_user', validate: false }, - async (context, request, response) => { - context.core.auditor.withAuditScope('audit_trail_test/context/as_current_user'); - await context.core.elasticsearch.legacy.client.callAsCurrentUser('ping'); - return response.noContent(); - } - ); - - router.get( - { path: '/audit_trail_test/context/as_internal_user', validate: false }, - async (context, request, response) => { - context.core.auditor.withAuditScope('audit_trail_test/context/as_internal_user'); - await context.core.elasticsearch.legacy.client.callAsInternalUser('ping'); - return response.noContent(); - } - ); - - router.get( - { path: '/audit_trail_test/contract/as_current_user', validate: false }, - async (context, request, response) => { - const [coreStart] = await core.getStartServices(); - const auditor = coreStart.auditTrail.asScoped(request); - auditor.withAuditScope('audit_trail_test/contract/as_current_user'); - - await context.core.elasticsearch.legacy.client.callAsCurrentUser('ping'); - return response.noContent(); - } - ); - - router.get( - { path: '/audit_trail_test/contract/as_internal_user', validate: false }, - async (context, request, response) => { - const [coreStart] = await core.getStartServices(); - const auditor = coreStart.auditTrail.asScoped(request); - auditor.withAuditScope('audit_trail_test/contract/as_internal_user'); - - await context.core.elasticsearch.legacy.client.callAsInternalUser('ping'); - return response.noContent(); - } - ); - } - - public start() {} -} diff --git a/x-pack/test/plugin_functional/test_suites/audit_trail/index.ts b/x-pack/test/plugin_functional/test_suites/audit_trail/index.ts deleted file mode 100644 index fb66f0dffc12..000000000000 --- a/x-pack/test/plugin_functional/test_suites/audit_trail/index.ts +++ /dev/null @@ -1,129 +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 Path from 'path'; -import Fs from 'fs'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -class FileWrapper { - constructor(private readonly path: string) {} - async reset() { - // "touch" each file to ensure it exists and is empty before each test - await Fs.promises.writeFile(this.path, ''); - } - async read() { - const content = await Fs.promises.readFile(this.path, { encoding: 'utf8' }); - return content.trim().split('\n'); - } - async readJSON() { - const content = await this.read(); - return content.map((l) => JSON.parse(l)); - } - // writing in a file is an async operation. we use this method to make sure logs have been written. - async isNotEmpty() { - const content = await this.read(); - const line = content[0]; - return line.length > 0; - } -} - -export default function ({ getPageObjects, getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const retry = getService('retry'); - - describe('Audit trail service', function () { - this.tags('ciGroup7'); - const logFilePath = Path.resolve( - __dirname, - '../../plugins/audit_trail_test/server/pattern_debug.log' - ); - const logFile = new FileWrapper(logFilePath); - - beforeEach(async () => { - await logFile.reset(); - }); - - it('logs current user access to elasticsearch via RequestHandlerContext', async () => { - await supertest - .get('/audit_trail_test/context/as_current_user') - .set('kbn-xsrf', 'foo') - .expect(204); - - await retry.waitFor('logs event in the dest file', async () => { - return await logFile.isNotEmpty(); - }); - - const content = await logFile.readJSON(); - const pingCall = content.find( - (c) => c.meta.scope === 'audit_trail_test/context/as_current_user' - ); - expect(pingCall).to.be.ok(); - expect(pingCall.meta.type).to.be('elasticsearch.call.currentUser'); - expect(pingCall.meta.user).to.be('elastic'); - expect(pingCall.meta.space).to.be('default'); - }); - - it('logs internal user access to elasticsearch via RequestHandlerContext', async () => { - await supertest - .get('/audit_trail_test/context/as_internal_user') - .set('kbn-xsrf', 'foo') - .expect(204); - - await retry.waitFor('logs event in the dest file', async () => { - return await logFile.isNotEmpty(); - }); - - const content = await logFile.readJSON(); - const pingCall = content.find( - (c) => c.meta.scope === 'audit_trail_test/context/as_internal_user' - ); - expect(pingCall).to.be.ok(); - expect(pingCall.meta.type).to.be('elasticsearch.call.internalUser'); - expect(pingCall.meta.user).to.be('elastic'); - expect(pingCall.meta.space).to.be('default'); - }); - - it('logs current user access to elasticsearch via coreStart contract', async () => { - await supertest - .get('/audit_trail_test/contract/as_current_user') - .set('kbn-xsrf', 'foo') - .expect(204); - - await retry.waitFor('logs event in the dest file', async () => { - return await logFile.isNotEmpty(); - }); - - const content = await logFile.readJSON(); - const pingCall = content.find( - (c) => c.meta.scope === 'audit_trail_test/contract/as_current_user' - ); - expect(pingCall).to.be.ok(); - expect(pingCall.meta.type).to.be('elasticsearch.call.currentUser'); - expect(pingCall.meta.user).to.be('elastic'); - expect(pingCall.meta.space).to.be('default'); - }); - - it('logs internal user access to elasticsearch via coreStart contract', async () => { - await supertest - .get('/audit_trail_test/contract/as_internal_user') - .set('kbn-xsrf', 'foo') - .expect(204); - - await retry.waitFor('logs event in the dest file', async () => { - return await logFile.isNotEmpty(); - }); - - const content = await logFile.readJSON(); - const pingCall = content.find( - (c) => c.meta.scope === 'audit_trail_test/contract/as_internal_user' - ); - expect(pingCall).to.be.ok(); - expect(pingCall.meta.type).to.be('elasticsearch.call.internalUser'); - expect(pingCall.meta.user).to.be('elastic'); - expect(pingCall.meta.space).to.be('default'); - }); - }); -} 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_api_integration/audit.config.ts b/x-pack/test/security_api_integration/audit.config.ts new file mode 100644 index 000000000000..c2011fafd1c9 --- /dev/null +++ b/x-pack/test/security_api_integration/audit.config.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); + + const auditLogPlugin = resolve(__dirname, './fixtures/audit/audit_log'); + const auditLogPath = resolve(__dirname, './fixtures/audit/audit.log'); + + return { + testFiles: [require.resolve('./tests/audit')], + servers: xPackAPITestsConfig.get('servers'), + security: { disableTestUser: true }, + services: xPackAPITestsConfig.get('services'), + junit: { + reportName: 'X-Pack Security API Integration Tests (Audit Log)', + }, + esTestCluster: xPackAPITestsConfig.get('esTestCluster'), + kbnTestServer: { + ...xPackAPITestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${auditLogPlugin}`, + '--xpack.security.audit.enabled=true', + '--xpack.security.audit.appender.kind=file', + `--xpack.security.audit.appender.path=${auditLogPath}`, + '--xpack.security.audit.appender.layout.kind=json', + ], + }, + }; +} diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/kibana.json b/x-pack/test/security_api_integration/fixtures/audit/audit_log/kibana.json similarity index 62% rename from x-pack/test/plugin_functional/plugins/audit_trail_test/kibana.json rename to x-pack/test/security_api_integration/fixtures/audit/audit_log/kibana.json index f53aa57ad670..fbec5108ee48 100644 --- a/x-pack/test/plugin_functional/plugins/audit_trail_test/kibana.json +++ b/x-pack/test/security_api_integration/fixtures/audit/audit_log/kibana.json @@ -1,9 +1,9 @@ { - "id": "audit_trail_test", + "id": "auditLog", "version": "1.0.0", "kibanaVersion": "kibana", "configPath": [], - "requiredPlugins": ["auditTrail"], + "requiredPlugins": [], "server": true, "ui": false } diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/index.ts b/x-pack/test/security_api_integration/fixtures/audit/audit_log/server/index.ts similarity index 100% rename from x-pack/test/plugin_functional/plugins/audit_trail_test/server/index.ts rename to x-pack/test/security_api_integration/fixtures/audit/audit_log/server/index.ts diff --git a/x-pack/test/security_api_integration/fixtures/audit/audit_log/server/plugin.ts b/x-pack/test/security_api_integration/fixtures/audit/audit_log/server/plugin.ts new file mode 100644 index 000000000000..9f594cd5889b --- /dev/null +++ b/x-pack/test/security_api_integration/fixtures/audit/audit_log/server/plugin.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup } from 'src/core/server'; + +export class AuditTrailTestPlugin implements Plugin { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + router.get({ path: '/audit_log', validate: false }, async (context, request, response) => { + await context.core.savedObjects.client.create('dashboard', {}); + await context.core.savedObjects.client.find({ type: 'dashboard' }); + return response.noContent(); + }); + } + + public start() {} +} diff --git a/x-pack/test/security_api_integration/tests/audit/audit_log.ts b/x-pack/test/security_api_integration/tests/audit/audit_log.ts new file mode 100644 index 000000000000..136854eab286 --- /dev/null +++ b/x-pack/test/security_api_integration/tests/audit/audit_log.ts @@ -0,0 +1,118 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +class FileWrapper { + constructor(private readonly path: string) {} + async reset() { + // "touch" each file to ensure it exists and is empty before each test + await Fs.promises.writeFile(this.path, ''); + } + async read() { + const content = await Fs.promises.readFile(this.path, { encoding: 'utf8' }); + return content.trim().split('\n'); + } + async readJSON() { + const content = await this.read(); + return content.map((l) => JSON.parse(l)); + } + // writing in a file is an async operation. we use this method to make sure logs have been written. + async isNotEmpty() { + const content = await this.read(); + const line = content[0]; + return line.length > 0; + } +} + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + const { username, password } = getService('config').get('servers.kibana'); + + describe('Audit Log', function () { + const logFilePath = Path.resolve(__dirname, '../../fixtures/audit/audit.log'); + const logFile = new FileWrapper(logFilePath); + + beforeEach(async () => { + await logFile.reset(); + }); + + it('logs audit events when reading and writing saved objects', async () => { + await supertest.get('/audit_log?query=param').set('kbn-xsrf', 'foo').expect(204); + await retry.waitFor('logs event in the dest file', async () => await logFile.isNotEmpty()); + + const content = await logFile.readJSON(); + + const httpEvent = content.find((c) => c.event.action === 'http_request'); + expect(httpEvent).to.be.ok(); + expect(httpEvent.trace.id).to.be.ok(); + expect(httpEvent.user.name).to.be(username); + expect(httpEvent.kibana.space_id).to.be('default'); + expect(httpEvent.http.request.method).to.be('get'); + expect(httpEvent.url.path).to.be('/audit_log'); + expect(httpEvent.url.query).to.be('query=param'); + + const createEvent = content.find((c) => c.event.action === 'saved_object_create'); + expect(createEvent).to.be.ok(); + expect(createEvent.trace.id).to.be.ok(); + expect(createEvent.user.name).to.be(username); + expect(createEvent.kibana.space_id).to.be('default'); + + const findEvent = content.find((c) => c.event.action === 'saved_object_find'); + expect(findEvent).to.be.ok(); + expect(findEvent.trace.id).to.be.ok(); + expect(findEvent.user.name).to.be(username); + expect(findEvent.kibana.space_id).to.be('default'); + }); + + it('logs audit events when logging in successfully', async () => { + await supertest + .post('/internal/security/login') + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { username, password }, + }) + .expect(200); + await retry.waitFor('logs event in the dest file', async () => await logFile.isNotEmpty()); + + const content = await logFile.readJSON(); + + const loginEvent = content.find((c) => c.event.action === 'user_login'); + expect(loginEvent).to.be.ok(); + expect(loginEvent.event.outcome).to.be('success'); + expect(loginEvent.trace.id).to.be.ok(); + expect(loginEvent.user.name).to.be(username); + }); + + it('logs audit events when failing to log in', async () => { + await supertest + .post('/internal/security/login') + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { username, password: 'invalid_password' }, + }) + .expect(401); + await retry.waitFor('logs event in the dest file', async () => await logFile.isNotEmpty()); + + const content = await logFile.readJSON(); + + const loginEvent = content.find((c) => c.event.action === 'user_login'); + expect(loginEvent).to.be.ok(); + expect(loginEvent.event.outcome).to.be('failure'); + expect(loginEvent.trace.id).to.be.ok(); + expect(loginEvent.user).not.to.be.ok(); + }); + }); +} diff --git a/x-pack/test/security_api_integration/tests/audit/index.ts b/x-pack/test/security_api_integration/tests/audit/index.ts new file mode 100644 index 000000000000..e4bec88ba490 --- /dev/null +++ b/x-pack/test/security_api_integration/tests/audit/index.ts @@ -0,0 +1,14 @@ +/* + * 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 ({ loadTestFile }: FtrProviderContext) { + describe('security APIs - Audit Log', function () { + this.tags('ciGroup6'); + loadTestFile(require.resolve('./audit_log')); + }); +} 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..77f32063d41b 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -103,7 +103,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('finds page title', async () => { const title = await testSubjects.getVisibleText('header-page-title'); - expect(title).to.equal('Endpoints BETA'); + expect(title).to.equal('Endpoints'); }); it('displays table data', async () => { @@ -162,7 +162,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe.skip("has a url with an endpoint host's id", () => { before(async () => { await pageObjects.endpoint.navigateToEndpointList( - 'selected_host=fc0ff548-feba-41b6-8367-65e8790d0eaf' + 'selected_endpoint=3838df35-a095-4af4-8fce-0b6d78793f2e' ); }); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/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/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index 49a7a2155a70..70958d7ca763 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -30,7 +30,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('displays page title', async () => { const policyTitle = await testSubjects.getVisibleText('header-page-title'); - expect(policyTitle).to.equal('Policies BETA'); + expect(policyTitle).to.equal('Policies'); }); it('shows header create policy button', async () => { const createButtonTitle = await testSubjects.getVisibleText('headerCreateNewPolicyButton'); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts index 5f749ac27247..3a0f0b91bddb 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' + ); + }); + + 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/package.ts b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts index afbf0dcd7bd1..8dc78ed71d0b 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts @@ -74,7 +74,9 @@ export default function ({ getService }: FtrProviderContext) { }); it('handles events without the `network.protocol` field being defined', async () => { - const eventWithoutNetworkObject = generator.generateEvent(); + const eventWithoutNetworkObject = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(networkIndex), + }); // ensure that `network.protocol` does not exist in the event to test that the pipeline handles those type of events delete eventWithoutNetworkObject.network; @@ -137,8 +139,10 @@ export default function ({ getService }: FtrProviderContext) { let genData: InsertedEvents; before(async () => { - event = generator.generateEvent(); - genData = await resolver.insertEvents([event]); + event = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); + genData = await resolver.insertEvents([event], processEventsIndex); }); after(async () => { @@ -158,20 +162,29 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { // 46.239.193.5 should be in Iceland // 8.8.8.8 should be in the US - const eventWithBothIPs = generator.generateEvent({ + const eventWithBothIPsNetwork = generator.generateEvent({ extensions: { source: { ip: '8.8.8.8' }, destination: { ip: '46.239.193.5' } }, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(networkIndex), }); - const eventWithSourceOnly = generator.generateEvent({ + const eventWithSourceOnlyNetwork = generator.generateEvent({ extensions: { source: { ip: '8.8.8.8' } }, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(networkIndex), }); networkIndexData = await resolver.insertEvents( - [eventWithBothIPs, eventWithSourceOnly], + [eventWithBothIPsNetwork, eventWithSourceOnlyNetwork], networkIndex ); - processIndexData = await resolver.insertEvents([eventWithBothIPs], processEventsIndex); + const eventWithBothIPsProcess = generator.generateEvent({ + extensions: { source: { ip: '8.8.8.8' }, destination: { ip: '46.239.193.5' } }, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); + processIndexData = await resolver.insertEvents( + [eventWithBothIPsProcess], + processEventsIndex + ); }); after(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/policy.ts b/x-pack/test/security_solution_endpoint_api_int/apis/policy.ts index 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/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts index 49e24ff67fa7..b56dea94ab56 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts @@ -22,7 +22,7 @@ import { Event, EndpointDocGenerator, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; -import { InsertedEvents } from '../../services/resolver'; +import { InsertedEvents, processEventsIndex } from '../../services/resolver'; import { createAncestryArray } from './common'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { @@ -42,25 +42,33 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC before(async () => { // Construct the following tree: // Origin -> infoEvent -> startEvent -> execEvent - origin = generator.generateEvent(); + origin = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); infoEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), eventType: ['info'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); startEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(infoEvent), ancestry: createAncestryArray([infoEvent, origin]), eventType: ['start'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); execEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(startEvent), ancestry: createAncestryArray([startEvent, infoEvent]), eventType: ['change'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); - genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + genData = await resolver.insertEvents( + [origin, infoEvent, startEvent, execEvent], + processEventsIndex + ); }); after(async () => { @@ -88,11 +96,14 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC before(async () => { // Construct the following tree: // Origin -> (infoEvent, startEvent, execEvent are all for the same node) - origin = generator.generateEvent(); + origin = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); startEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), eventType: ['start'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); infoEvent = generator.generateEvent({ @@ -100,6 +111,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), entityID: entityIDSafeVersion(startEvent), eventType: ['info'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); execEvent = generator.generateEvent({ @@ -107,8 +119,12 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), eventType: ['change'], entityID: entityIDSafeVersion(startEvent), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); - genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + genData = await resolver.insertEvents( + [origin, infoEvent, startEvent, execEvent], + processEventsIndex + ); }); after(async () => { @@ -141,11 +157,14 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC before(async () => { // Construct the following tree: // Origin -> (infoEvent, startEvent, execEvent are all for the same node) - origin = generator.generateEvent(); + origin = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); startEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), eventType: ['start'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); infoEvent = generator.generateEvent({ @@ -154,6 +173,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), entityID: entityIDSafeVersion(startEvent), eventType: ['info'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); execEvent = generator.generateEvent({ @@ -162,8 +182,12 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), eventType: ['change'], entityID: entityIDSafeVersion(startEvent), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); - genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + genData = await resolver.insertEvents( + [origin, infoEvent, startEvent, execEvent], + processEventsIndex + ); }); after(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index e6d5e8fccd00..f9492e629168 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -15,7 +15,7 @@ import { EndpointDocGenerator, Event, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; -import { InsertedEvents } from '../../services/resolver'; +import { InsertedEvents, processEventsIndex } from '../../services/resolver'; import { createAncestryArray } from './common'; export default function ({ getService }: FtrProviderContext) { @@ -34,9 +34,12 @@ export default function ({ getService }: FtrProviderContext) { let origin: Event; let genData: InsertedEvents; before(async () => { - origin = generator.generateEvent({ parentEntityID: 'a' }); + origin = generator.generateEvent({ + parentEntityID: 'a', + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); setEntityIDEmptyString(origin); - genData = await resolver.insertEvents([origin]); + genData = await resolver.insertEvents([origin], processEventsIndex); }); after(async () => { @@ -63,10 +66,14 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { // construct a tree with an origin and two direct children. One child will not have an entity_id. That child // should not be returned by the backend. - origin = generator.generateEvent({ entityID: 'a' }); + origin = generator.generateEvent({ + entityID: 'a', + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); childNoEntityID = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); // force it to be empty setEntityIDEmptyString(childNoEntityID); @@ -75,9 +82,10 @@ export default function ({ getService }: FtrProviderContext) { entityID: 'b', parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); events = [origin, childNoEntityID, childWithEntityID]; - genData = await resolver.insertEvents(events); + genData = await resolver.insertEvents(events, processEventsIndex); }); after(async () => { @@ -106,17 +114,20 @@ export default function ({ getService }: FtrProviderContext) { // entity_ids in the ancestry array. This is to make sure that the backend will not query for that event. ancestor2 = generator.generateEvent({ entityID: '2', + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); ancestor1 = generator.generateEvent({ entityID: '1', parentEntityID: entityIDSafeVersion(ancestor2), ancestry: createAncestryArray([ancestor2]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); // we'll insert an event that doesn't have an entity id so if the backend does search for it, it should be // returned and our test should fail ancestorNoEntityID = generator.generateEvent({ ancestry: createAncestryArray([ancestor2]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); setEntityIDEmptyString(ancestorNoEntityID); @@ -124,10 +135,11 @@ export default function ({ getService }: FtrProviderContext) { entityID: 'a', parentEntityID: entityIDSafeVersion(ancestor1), ancestry: ['', ...createAncestryArray([ancestor2])], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); events = [origin, ancestor1, ancestor2, ancestorNoEntityID]; - genData = await resolver.insertEvents(events); + genData = await resolver.insertEvents(events, processEventsIndex); }); after(async () => { diff --git a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts index f233bc1d11d7..c8e13f6bada7 100644 --- a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts +++ b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts @@ -13,3 +13,24 @@ export function getUrlPrefix(spaceId?: string) { export function getIdPrefix(spaceId?: string) { return spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}-`; } + +export function getTestScenariosForSpace(spaceId: string) { + const explicitScenario = { + spaceId, + urlPrefix: `/s/${spaceId}`, + scenario: `when referencing the ${spaceId} space explicitly in the URL`, + }; + + if (spaceId === DEFAULT_SPACE_ID) { + return [ + { + spaceId, + urlPrefix: ``, + scenario: 'when referencing the default space implicitly', + }, + explicitScenario, + ]; + } + + return [explicitScenario]; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts index 4de638c78414..7c2120ce6eea 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface CreateTest { @@ -67,56 +67,58 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest { describeFn(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); + beforeEach(() => esArchiver.load('saved_objects/spaces')); + afterEach(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.newSpace.statusCode}`, async () => { - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .auth(user.username, user.password) - .send({ - name: 'marketing', - id: 'marketing', - description: 'a description', - color: '#5c5959', - disabledFeatures: [], - }) - .expect(tests.newSpace.statusCode) - .then(tests.newSpace.response); - }); - - describe('when it already exists', () => { - it(`should return ${tests.alreadyExists.statusCode}`, async () => { + getTestScenariosForSpace(spaceId).forEach(({ urlPrefix, scenario }) => { + it(`should return ${tests.newSpace.statusCode} ${scenario}`, async () => { return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) + .post(`${urlPrefix}/api/spaces/space`) .auth(user.username, user.password) .send({ - name: 'space_1', - id: 'space_1', - color: '#ffffff', + name: 'marketing', + id: 'marketing', description: 'a description', + color: '#5c5959', disabledFeatures: [], }) - .expect(tests.alreadyExists.statusCode) - .then(tests.alreadyExists.response); + .expect(tests.newSpace.statusCode) + .then(tests.newSpace.response); }); - }); - describe('when _reserved is specified', () => { - it(`should return ${tests.reservedSpecified.statusCode} and ignore _reserved`, async () => { - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .auth(user.username, user.password) - .send({ - name: 'reserved space', - id: 'reserved', - description: 'a description', - color: '#5c5959', - _reserved: true, - disabledFeatures: [], - }) - .expect(tests.reservedSpecified.statusCode) - .then(tests.reservedSpecified.response); + describe('when it already exists', () => { + it(`should return ${tests.alreadyExists.statusCode} ${scenario}`, async () => { + return supertest + .post(`${urlPrefix}/api/spaces/space`) + .auth(user.username, user.password) + .send({ + name: 'space_1', + id: 'space_1', + color: '#ffffff', + description: 'a description', + disabledFeatures: [], + }) + .expect(tests.alreadyExists.statusCode) + .then(tests.alreadyExists.response); + }); + }); + + describe('when _reserved is specified', () => { + it(`should return ${tests.reservedSpecified.statusCode} and ignore _reserved ${scenario}`, async () => { + return supertest + .post(`${urlPrefix}/api/spaces/space`) + .auth(user.username, user.password) + .send({ + name: 'reserved space', + id: 'reserved', + description: 'a description', + color: '#5c5959', + _reserved: true, + disabledFeatures: [], + }) + .expect(tests.reservedSpecified.statusCode) + .then(tests.reservedSpecified.response); + }); }); }); }); diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index 69b5697d8a9a..2a6b2c0e69d1 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; @@ -176,7 +176,7 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe { user = {}, spaceId, tests }: DeleteTestDefinition ) => { describeFn(description, () => { - before(async () => { + beforeEach(async () => { await esArchiver.load('saved_objects/spaces'); // since we want to verify that we only delete the right things @@ -189,33 +189,35 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe .auth(user.username, user.password) .expect(200); }); - after(() => esArchiver.unload('saved_objects/spaces')); + afterEach(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.exists.statusCode}`, async () => { - return supertest - .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/space_2`) - .auth(user.username, user.password) - .expect(tests.exists.statusCode) - .then(tests.exists.response); - }); - - describe(`when the space is reserved`, () => { - it(`should return ${tests.reservedSpace.statusCode}`, async () => { + getTestScenariosForSpace(spaceId).forEach(({ urlPrefix, scenario }) => { + it(`should return ${tests.exists.statusCode} ${scenario}`, async () => { return supertest - .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/default`) + .delete(`${urlPrefix}/api/spaces/space/space_2`) .auth(user.username, user.password) - .expect(tests.reservedSpace.statusCode) - .then(tests.reservedSpace.response); + .expect(tests.exists.statusCode) + .then(tests.exists.response); }); - }); - describe(`when the space doesn't exist`, () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => { - return supertest - .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/space_3`) - .auth(user.username, user.password) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response); + describe(`when the space is reserved`, () => { + it(`should return ${tests.reservedSpace.statusCode} ${scenario}`, async () => { + return supertest + .delete(`${urlPrefix}/api/spaces/space/default`) + .auth(user.username, user.password) + .expect(tests.reservedSpace.statusCode) + .then(tests.reservedSpace.response); + }); + }); + + describe(`when the space doesn't exist`, () => { + it(`should return ${tests.doesntExist.statusCode} ${scenario}`, async () => { + return supertest + .delete(`${urlPrefix}/api/spaces/space/space_3`) + .auth(user.username, user.password) + .expect(tests.doesntExist.statusCode) + .then(tests.doesntExist.response); + }); }); }); }); diff --git a/x-pack/test/spaces_api_integration/common/suites/get.ts b/x-pack/test/spaces_api_integration/common/suites/get.ts index bd0e2a18d5c5..6bf5b0f18023 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; import { SuperAgent } from 'superagent'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface GetTest { @@ -80,12 +80,14 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperAgent) before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.default.statusCode}`, async () => { - return supertest - .get(`${getUrlPrefix(currentSpaceId)}/api/spaces/space/${spaceId}`) - .auth(user.username, user.password) - .expect(tests.default.statusCode) - .then(tests.default.response); + getTestScenariosForSpace(currentSpaceId).forEach(({ urlPrefix, scenario }) => { + it(`should return ${tests.default.statusCode} ${scenario}`, async () => { + return supertest + .get(`${urlPrefix}/api/spaces/space/${spaceId}`) + .auth(user.username, user.password) + .expect(tests.default.statusCode) + .then(tests.default.response); + }); }); }); }; diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index d41d73bba90b..fce48e4938ba 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface GetAllTest { @@ -71,33 +71,35 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.exists.statusCode}`, async () => { - return supertest - .get(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .auth(user.username, user.password) - .expect(tests.exists.statusCode) - .then(tests.exists.response); - }); - - describe('copySavedObjects purpose', () => { - it(`should return ${tests.copySavedObjectsPurpose.statusCode}`, async () => { + getTestScenariosForSpace(spaceId).forEach(({ scenario, urlPrefix }) => { + it(`should return ${tests.exists.statusCode} ${scenario}`, async () => { return supertest - .get(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .query({ purpose: 'copySavedObjectsIntoSpace' }) + .get(`${urlPrefix}/api/spaces/space`) .auth(user.username, user.password) - .expect(tests.copySavedObjectsPurpose.statusCode) - .then(tests.copySavedObjectsPurpose.response); + .expect(tests.exists.statusCode) + .then(tests.exists.response); }); - }); - describe('copySavedObjects purpose', () => { - it(`should return ${tests.shareSavedObjectsPurpose.statusCode}`, async () => { - return supertest - .get(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .query({ purpose: 'shareSavedObjectsIntoSpace' }) - .auth(user.username, user.password) - .expect(tests.copySavedObjectsPurpose.statusCode) - .then(tests.copySavedObjectsPurpose.response); + describe('copySavedObjects purpose', () => { + it(`should return ${tests.copySavedObjectsPurpose.statusCode} ${scenario}`, async () => { + return supertest + .get(`${urlPrefix}/api/spaces/space`) + .query({ purpose: 'copySavedObjectsIntoSpace' }) + .auth(user.username, user.password) + .expect(tests.copySavedObjectsPurpose.statusCode) + .then(tests.copySavedObjectsPurpose.response); + }); + }); + + describe('copySavedObjects purpose', () => { + it(`should return ${tests.shareSavedObjectsPurpose.statusCode} ${scenario}`, async () => { + return supertest + .get(`${urlPrefix}/api/spaces/space`) + .query({ purpose: 'shareSavedObjectsIntoSpace' }) + .auth(user.username, user.password) + .expect(tests.copySavedObjectsPurpose.statusCode) + .then(tests.copySavedObjectsPurpose.response); + }); }); }); }); diff --git a/yarn.lock b/yarn.lock index 7520f9f176d5..4ed8f513da7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2759,17 +2759,39 @@ "@types/node" ">=8.9.0" axios "^0.18.0" -"@storybook/addon-actions@6.0.16", "@storybook/addon-actions@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.0.16.tgz#869c90291fdfec4a0644e8415f5004cc57e59145" - integrity sha512-kyPGMP2frdhUgJAm6ChqvndaUawwQE9Vx7pN1pk/Q4qnyVlWCneZVojQf0iAgL45q0az0XI1tOPr4ooroaniYg== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-a11y@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-6.0.26.tgz#b71761d9b8f8b340894eb9826d51ce319ce65116" + integrity sha512-sx1Ethl9W3Kfns0qB1v0CoAymKTC+odB+rCjUKM1h/ILS/n8ZzwkzAj0L7DU/6wA0nZwZDQ+1wL2ZN7r+vxr8A== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" + axe-core "^3.5.2" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + react-sizeme "^2.5.2" + regenerator-runtime "^0.13.3" + ts-dedent "^1.1.1" + util-deprecate "^1.0.2" + +"@storybook/addon-actions@6.0.26", "@storybook/addon-actions@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.0.26.tgz#d0de9e4d78a8f8f5bf8730c04d0b6d1741c29273" + integrity sha512-9tWbAqSwzWWVz8zwAndZFusZYjIcRYgZUC0LqC8QlH79DgF3ASjw9y97+w1VTTAzdb6LYnsMuSpX6+8m5hrR4g== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" fast-deep-equal "^3.1.1" global "^4.3.2" @@ -2783,40 +2805,40 @@ util-deprecate "^1.0.2" uuid "^8.0.0" -"@storybook/addon-backgrounds@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.0.16.tgz#cbf909992a86dbbdfea172d3950300e8c2a7de01" - integrity sha512-0sH7hlZh4bHt6zV6QyG3ryNGJsxD42iXVwWdwAShzfWJKGfLy5XwdvHUKkMEBbY9bOPeoI9oMli2RAfsD6juLQ== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-backgrounds@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.0.26.tgz#97cea86cc4fe88b6c0ad8addb2d01712e535aa10" + integrity sha512-Y9t1s4N2PgTxLhO85w+WFnnclZNRdxGpsoiLDPkb63HajZvUa5/ogmIOrfFl5DX2zCpkgNLlskmDcEP6n835cA== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" memoizerific "^1.11.3" react "^16.8.3" regenerator-runtime "^0.13.3" -"@storybook/addon-controls@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.0.16.tgz#c7fc765a01cc3a0de397f8b55bfeda3f328e5495" - integrity sha512-RgBOply9o3PYoWI7TNKef2AQixw7l620pT1fCJbXykp/lu17eqKaIa5KYHRE9vEajun5RuEQxGnSzQOV3OZAsA== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/node-logger" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-controls@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.0.26.tgz#4cc4c30ee7bf89ab873158ead4d25d6f7e07ffba" + integrity sha512-K3Oik9ClpShv8Qc6JeNwtmd4yJJcnO2nyaAYYFiyNt+Vsg7zMaDtE2wfvViThNKjX7nUXIeh+OscseIkdWgLuA== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/node-logger" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" ts-dedent "^1.1.1" -"@storybook/addon-docs@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.0.16.tgz#b24983a63c6c9469a418bb1478606626aff42dff" - integrity sha512-7gM/0lQ3mSybpOpQbgR8fjAU+u3zgAWyOM1a+LR7zVn5lNjgBhZD2pfHuwViTeAGG/IIpvmOsd57BKlFJw5TPA== +"@storybook/addon-docs@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.0.26.tgz#bd7fc1fcdc47bb7992fa8d3254367e8c3bba373d" + integrity sha512-3t8AOPkp8ZW74h7FnzxF3wAeb1wRyYjMmgJZxqzgi/x7K0i1inbCq8MuJnytuTcZ7+EK4HR6Ih7o9tJuAtIBLw== dependencies: "@babel/generator" "^7.9.6" "@babel/parser" "^7.9.6" @@ -2826,18 +2848,18 @@ "@mdx-js/loader" "^1.5.1" "@mdx-js/mdx" "^1.5.1" "@mdx-js/react" "^1.5.1" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/node-logger" "6.0.16" - "@storybook/postinstall" "6.0.16" - "@storybook/source-loader" "6.0.16" - "@storybook/theming" "6.0.16" + "@storybook/node-logger" "6.0.26" + "@storybook/postinstall" "6.0.26" + "@storybook/source-loader" "6.0.26" + "@storybook/theming" "6.0.26" acorn "^7.1.0" acorn-jsx "^5.1.0" acorn-walk "^7.0.0" @@ -2857,36 +2879,36 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/addon-essentials@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.0.16.tgz#031b05f6a9947fd93a86f28767b1c354e8ea4237" - integrity sha512-tHH2B4cGYihaPytzIlcFlc/jDSu1PUMgaQM4uzIDOn6SCYZJMp5vygK97zF7hf41x/TXv+8i9ZMN5iUJ7l1+fw== - dependencies: - "@storybook/addon-actions" "6.0.16" - "@storybook/addon-backgrounds" "6.0.16" - "@storybook/addon-controls" "6.0.16" - "@storybook/addon-docs" "6.0.16" - "@storybook/addon-toolbars" "6.0.16" - "@storybook/addon-viewport" "6.0.16" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/node-logger" "6.0.16" +"@storybook/addon-essentials@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.0.26.tgz#1962f4fd19a9d9a1d1fad152bbfc3bba90f45216" + integrity sha512-AsKcPrVFksYNWq07jKXX/GRcdTa6Uo3sTEwuV5ZazWltlbOIcI0YdQs6mCFaCElB5Dqg1jqyxZ3vcd+VHiRSkA== + dependencies: + "@storybook/addon-actions" "6.0.26" + "@storybook/addon-backgrounds" "6.0.26" + "@storybook/addon-controls" "6.0.26" + "@storybook/addon-docs" "6.0.26" + "@storybook/addon-toolbars" "6.0.26" + "@storybook/addon-viewport" "6.0.26" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/node-logger" "6.0.26" core-js "^3.0.1" regenerator-runtime "^0.13.3" ts-dedent "^1.1.1" -"@storybook/addon-knobs@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-6.0.16.tgz#ef7b9a67c5f3f75579af1d3c2c1f36205f77f505" - integrity sha512-//4Fq70M7LLOghM6+eugL53QHVmlbBm5240u+Aq2nWQLUtaszrPW6/7Vj0XRwLyp/DQtEHetTE/fFfCLoGK+dw== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-knobs@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-6.0.26.tgz#c574a817c8d791aced89a272eb0c6baaee9a0bdf" + integrity sha512-a25hOepctnsqX1nym80HLKrn8fvXFqsbcL3ZkAZWAIXZhf+zPYTJFrw25FxvbyhktmjQv6l89jteAfGfC0g8DA== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" copy-to-clipboard "^3.0.8" core-js "^3.0.1" escape-html "^1.0.3" @@ -2900,15 +2922,15 @@ react-select "^3.0.8" regenerator-runtime "^0.13.3" -"@storybook/addon-storyshots@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-storyshots/-/addon-storyshots-6.0.16.tgz#e912273966d4c7cba1a9053d6a76e8856e3b834f" - integrity sha512-wQhM6pnjUCLTr/6BMXTptGeqiMPnnTrvLeaRwG1cDChGK/qs3YqTsa2QqLXQ17IvNUDTHLUNQlYk5af+HrCGhg== +"@storybook/addon-storyshots@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-storyshots/-/addon-storyshots-6.0.26.tgz#529a557b4a8e4558da22a8ce847b88f9fb3ab5fa" + integrity sha512-XLt7aqjp3lH9ye5zfgbcZIDe8B9riu9shOsJfhZ1DpzfXxb8NVgAcvsXyMW/7dJZ/paAadXAeZZtWnOBuqNnmw== dependencies: "@jest/transform" "^26.0.0" - "@storybook/addons" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/core" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/core" "6.0.26" "@types/glob" "^7.1.1" "@types/jest" "^25.1.1" "@types/jest-specific-snapshot" "^0.5.3" @@ -2922,62 +2944,62 @@ regenerator-runtime "^0.13.3" ts-dedent "^1.1.1" -"@storybook/addon-toolbars@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.0.16.tgz#704a5d506b8d952eca6e5dca96c00b22aedf495f" - integrity sha512-6ulvPqe38NJRbQp0zajeNsDJQKZzGqbCMsSw3gtkFOMt8D/V625MF8YY/Y9UZ+xHWor17GUgE1k9hljdyZe1Nw== +"@storybook/addon-toolbars@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.0.26.tgz#650a1793caac6616f4481116f4dfb79f2d3c336b" + integrity sha512-f9OI7ix0lQWg4eEHheWYB3dz7kYO6qCGkzp+oIQkPpjnYmY8ZghyRM+ui6vfq+G8BwxrAKGR0CB8ttNxVsd/9A== dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" core-js "^3.0.1" -"@storybook/addon-viewport@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.0.16.tgz#574cc0a3f991ce405ba4a3540132fb05edf488f6" - integrity sha512-3vk6lBZrKJrK9rwxglLT1p579WkLvoJxgW5ddpvSsu31NPAKfDufkDqOZOQGyMmcgIFzZJEc9eKjoTcLiHxppw== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-viewport@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.0.26.tgz#c913dadcb55b31d2df21a580e932b85b1a200a8b" + integrity sha512-LdVW61iZhUf2npNk3qPH9DTunVMhKcyiFq2CRlgxcA5FwjdkAbcPiYMc18rfyTvp/Zd2idartvwYalBYpJhAhw== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" global "^4.3.2" memoizerific "^1.11.3" prop-types "^15.7.2" regenerator-runtime "^0.13.3" -"@storybook/addons@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.0.16.tgz#a20a219bd5b1474ad02b92e79a74652898a684d9" - integrity sha512-jGMaOJYTM2yZeX1tI6whEn+4xpI1aAybZBrc+OD21CcGoQrbF/jplZMq7xKI0Y6vOMguuTGulpUNCezD3LbBjA== - dependencies: - "@storybook/api" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/router" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addons@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.0.26.tgz#343cbea3eee2d39413b80bc2d66535a7f61488fc" + integrity sha512-OhAApFKgsj9an7FLYfHI4cJQuZ4Zm6yoGOpaxhOvKQMw7dXUPsLvbCyw/6dZOLvaFhjJjQiXtbxtZG+UjR8nvA== + dependencies: + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/router" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" global "^4.3.2" regenerator-runtime "^0.13.3" -"@storybook/api@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.0.16.tgz#56cdfc6f7a21d62d1a4ab06b4741c1560160d320" - integrity sha512-RTC4BKmH5i8bJUQejOHEtjebVKtOaHkmEagI2HQRalsokBc1GLAf84EGrO2TaZiRrItAPL5zZQgEnKUblsGJGw== +"@storybook/api@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.0.26.tgz#c45222c132eb8bc2e383536adfebbeb7a89867d0" + integrity sha512-aszDoz1c6T+eRtTUwWvySoyd3gRXmQxsingD084NnEp4VfFLA5H7VS/0sre0ZvU5GWh8d9COxY0DS2Ry/QSKvw== dependencies: "@reach/router" "^1.3.3" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/router" "6.0.16" + "@storybook/router" "6.0.26" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.0.16" + "@storybook/theming" "6.0.26" "@types/reach__router" "^1.3.5" core-js "^3.0.1" fast-deep-equal "^3.1.1" @@ -2991,38 +3013,38 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/channel-postmessage@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.0.16.tgz#a617578c49543b0de9f53eb28daae2bd3c9e1754" - integrity sha512-66B4FH5R7k9i7LBhGsr/hYOxwE4UBM1JMPGV0rhAnFY8m91GiUWl4YWTRdbYIkeaZxf/0oT4sgPScqz44hnw6Q== +"@storybook/channel-postmessage@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.0.26.tgz#a98a0132d6bdf06741afac2607e9feabe34ab98b" + integrity sha512-FT6lC8M5JlNBxPT0rYfmF1yl9mBv04nfYs82TZpp1CzpLxf7wxdCBZ8SSRmvWIVBoNwGZPDhIk5+6JWyDEISBg== dependencies: - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" core-js "^3.0.1" global "^4.3.2" qs "^6.6.0" telejson "^5.0.2" -"@storybook/channels@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.0.16.tgz#94e521b9eae535da80afb23feae593aa69bfe75d" - integrity sha512-TsI4GA7lKD4L2w6IjODMRfnEOkmvEp4eJDgf3MKm7+sMbxwi1y1d6yrW1UQbnmwoNJWk60ArMN2yqDBV+5MNJQ== +"@storybook/channels@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.0.26.tgz#3e8678b4b40085081257a39b9e85fab13a19943c" + integrity sha512-H0iUorayYqS+zfhbjd+cYRzAdRLGLWUeWFu2Aa+oJ4/zeAQNL+DafWboHc567RQ4Vb5KqE5QZoCFskWUUYqJYA== dependencies: core-js "^3.0.1" ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/client-api@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.0.16.tgz#4af47caccf92a31326ab77c5094dd4f90f888b91" - integrity sha512-fFsp53lt9W2QHSumqdfFRbh+DI9fvd7li0GDxqLeNESXaUVw48yg8lQiyRNK+j5Pl4VBS3AqytLugJ+0MGm2cA== +"@storybook/client-api@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.0.26.tgz#ac9334ba86834e5cb23fc4fb577de60bda66164d" + integrity sha512-Qd5wR5b5lio/EchuJMhAmmJAE1pfvnEyu+JnyFGwMZLV9mN9NSspz+YsqbSCCDZsYcP5ewvPEnumIWqmj/wagQ== dependencies: - "@storybook/addons" "6.0.16" - "@storybook/channel-postmessage" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/channel-postmessage" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" "@types/qs" "^6.9.0" "@types/webpack-env" "^1.15.2" @@ -3036,22 +3058,22 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/client-logger@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.0.16.tgz#6265d2b869a82be64538eaac39470e3845c9e069" - integrity sha512-xM61Aewxqoo8500UxV7iPpfqwikITojiCX3+w8ZiCJ2NizSaXkis95TEFAeHqyozfNym5CqG+6v2NWvGYV3ncQ== +"@storybook/client-logger@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.0.26.tgz#e3d28bd8dc02ec2c53a9d69773a68189590b746f" + integrity sha512-VNoL6/oehVhn3hZi9vrTNT+C/3oAZKV+smfZFnPtsCR/Fq7CKbmsBd0pGPL57f81RU8e8WygwrIlAGJTDSNIjw== dependencies: core-js "^3.0.1" global "^4.3.2" -"@storybook/components@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.0.16.tgz#d4c797f7897cefa11bbdb8dfd07bb3d4fa66b3e9" - integrity sha512-zpYGt3tWiN0yT7V0VhBl2T5Mr0COiNnTQUGCpA9Gl3pUBmAov2jCVf1sUxsIcBcMMZmDRcfo6NbJ/LqCFeUg+Q== +"@storybook/components@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.0.26.tgz#e1f6e16aae850a71c9ac7bdd1d44a068ec9cfdc1" + integrity sha512-8wigI1pDFJO1m1IQWPguOK+nOsaAVRWkVdu+2te/rDcIR9QNvMzzou0+Lhfp3zKSVT4E6mEoGB/TWXXF5Iq0sQ== dependencies: - "@storybook/client-logger" "6.0.16" + "@storybook/client-logger" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/theming" "6.0.16" + "@storybook/theming" "6.0.26" "@types/overlayscrollbars" "^1.9.0" "@types/react-color" "^3.0.1" "@types/react-syntax-highlighter" "11.0.4" @@ -3072,17 +3094,17 @@ react-textarea-autosize "^8.1.1" ts-dedent "^1.1.1" -"@storybook/core-events@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.0.16.tgz#3f8cd525c15fd80c9f327389851cce82a4b96850" - integrity sha512-ib+58N4OY8AOix2qcBH9ICRmVHUocpGaGRVlIo79WxJrpnB/HNQ8pEaniD+OAavDRq1B7uJqFlMkTXCC0GoFiQ== +"@storybook/core-events@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.0.26.tgz#61181c9a8610d26cc85d47f133a563879044ca2d" + integrity sha512-nWjS/+kMiw31OPgeJQaiFsJk9ZJJo3/d4c+kc6GOl2iC1H3Q4/5cm3NvJBn/7bUtKHmSFwfbDouj+XjUk5rZbQ== dependencies: core-js "^3.0.1" -"@storybook/core@6.0.16", "@storybook/core@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.0.16.tgz#ec9aa8c0fd1c23d29bf8401b650c0876c41d1b5f" - integrity sha512-dVgw03bB8rSMrYDw+v07Yiqyy4yas1olnXpytscWCWdbBuflSAQU+mtqcHMIH9YlhucIT2dYiErDDDNmqP+6tw== +"@storybook/core@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.0.26.tgz#ff587929d0f55cefa8405686e831e79aeeb6870e" + integrity sha512-2kmkxbzDJVrjzCjlseffoQJwZRH9bHZUumo5m8gpbN9kVnADER7yd6RUf2Zle5BK3ExC+0PPI1Whfg0qkiXvqw== dependencies: "@babel/plugin-proposal-class-properties" "^7.8.3" "@babel/plugin-proposal-decorators" "^7.8.3" @@ -3105,20 +3127,20 @@ "@babel/preset-react" "^7.8.3" "@babel/preset-typescript" "^7.9.0" "@babel/register" "^7.10.5" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/channel-postmessage" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channel-postmessage" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/node-logger" "6.0.16" - "@storybook/router" "6.0.16" + "@storybook/node-logger" "6.0.26" + "@storybook/router" "6.0.26" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.0.16" - "@storybook/ui" "6.0.16" + "@storybook/theming" "6.0.26" + "@storybook/ui" "6.0.26" "@types/glob-base" "^0.3.0" "@types/micromatch" "^4.0.1" "@types/node-fetch" "^2.5.4" @@ -3189,10 +3211,10 @@ dependencies: lodash "^4.17.15" -"@storybook/node-logger@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.0.16.tgz#805e0748355d13535c3295455f568ea94e57d1ad" - integrity sha512-mD6so/puFV5oByBkDp9rv2mV/WyGy21QdrwXpXdtLDKNgqPuJjHZuF1RA/+MmDK4P1CjvP1no2H5WDKg+aW4QQ== +"@storybook/node-logger@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.0.26.tgz#2ef95ea1e2defd4efcba6b23431ea5c5cbaa110b" + integrity sha512-mdILu91d/2ZgYfICoAMBjwBAYOgjk2URsPudrs5+23lFoPPIwf4CPWcfgs0f4GdfoICk3kV0W7+8bIARhRKp3g== dependencies: "@types/npmlog" "^4.1.2" chalk "^4.0.0" @@ -3200,23 +3222,23 @@ npmlog "^4.1.2" pretty-hrtime "^1.0.3" -"@storybook/postinstall@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.0.16.tgz#77c428534dd10074778dc669f7ffce9f387acc93" - integrity sha512-gZgPNJK/58VepIBodK0pSlD1jPQgIVTEFWot5/iDjxv9cnSl9V+LbIEW5jZp/lzoAONSj8AS646ZZjAM87S4RQ== +"@storybook/postinstall@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.0.26.tgz#3ba9f6fa598d92daf5823361186c4b1369f16ebe" + integrity sha512-B9Dh66MfserWw1J4KbLqfxpnanN//yeDjrrkowzqa3OFLqEPQCekv0ALocovnCkQ13+TcVGjPprxnWXfGhEMpg== dependencies: core-js "^3.0.1" -"@storybook/react@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.0.16.tgz#21464749f7bd90dc6026235b2ee47acf168d974a" - integrity sha512-cxnBwewx37rL1BjXo3TQFIvvCv9z26r3yuRRWh527/0QODfwGz8TT+/sJHeqBA5JIQzLwAHNqNJhLp6xzfr5Dw== +"@storybook/react@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.0.26.tgz#5d4b8f2c6d8003912d371298a6e5a945e24680b4" + integrity sha512-X02VpIEhpVc4avYiff861c015++tvMVSXJSrDP5J1xTAglVEiRFcU0Kn5h96o9N8FTup2n2xyj6Y7e8oC9yLXQ== dependencies: "@babel/preset-flow" "^7.0.0" "@babel/preset-react" "^7.0.0" - "@storybook/addons" "6.0.16" - "@storybook/core" "6.0.16" - "@storybook/node-logger" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/core" "6.0.26" + "@storybook/node-logger" "6.0.26" "@storybook/semver" "^7.3.2" "@svgr/webpack" "^5.4.0" "@types/webpack-env" "^1.15.2" @@ -3233,10 +3255,10 @@ ts-dedent "^1.1.1" webpack "^4.43.0" -"@storybook/router@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.0.16.tgz#b18cc0b1bba477f16f9f2ae8f0eaa0d5ba4b0a0e" - integrity sha512-zijPJ3CR4ytHE0v+pGdaWT3H+es+mLHRkR6hkqcD0ABT5HVfwMlmXJ9FkQGCVpnnNeBOz7+QKCdE13HMelQpqg== +"@storybook/router@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.0.26.tgz#5b991001afa7d7eb5e40c53cd4c58266b6f9edfd" + integrity sha512-kQ1LF/2gX3IkjS1wX7CsoeBc9ptHQzOsyax16rUyJa769DT5vMNtFtQxjNXMqSiSapPg2yrXJFKQNaoWvKgQEQ== dependencies: "@reach/router" "^1.3.3" "@types/reach__router" "^1.3.5" @@ -3253,31 +3275,31 @@ core-js "^3.6.5" find-up "^4.1.0" -"@storybook/source-loader@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.0.16.tgz#a3eb2b0cbede7d9121387738a530d71df645db5d" - integrity sha512-Ub6bU7o2JJUigzu9MSrFH1RD2SmpZZnym+WEidWI9A1gseKp1Rd4KDq36AqJo/oL3hAzoAOirrv3ZixIwXLFMg== +"@storybook/source-loader@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.0.26.tgz#0c9a20b9e018c49d559c56e1bdae8350b8175371" + integrity sha512-axNYEHEj7c9oHUFTMKZ6xRyKZCEEP7Aa9sFPzV5Q3Vrq6/3qhih5fOPXhst6/s4XZC1eIoKKHb/Gk4hmjYOEYA== dependencies: - "@storybook/addons" "6.0.16" - "@storybook/client-logger" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/client-logger" "6.0.26" "@storybook/csf" "0.0.1" core-js "^3.0.1" estraverse "^4.2.0" global "^4.3.2" loader-utils "^2.0.0" lodash "^4.17.15" - prettier "^2.0.5" + prettier "~2.0.5" regenerator-runtime "^0.13.3" -"@storybook/theming@6.0.16", "@storybook/theming@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.0.16.tgz#dd6de4f29316a6a2380018978b7b4a0ef9ea33c8" - integrity sha512-6D7oMEbeABYZdDY8e3i+O39XLrk6fvG3GBaSGp31BE30d269NcPkGPxMKY/nzc6MY30a+/LbBbM7b6gRKe6b4Q== +"@storybook/theming@6.0.26", "@storybook/theming@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.0.26.tgz#e5b545fb2653dfd1b043b567197d490b1c3c0da3" + integrity sha512-9yon2ofb9a+RT1pdvn8Njydy7XRw0qXcIsMqGsJRKoZecmRRozqB6DxH9Gbdf1vRSbM9gYUUDjbiMDFz7+4RiQ== dependencies: "@emotion/core" "^10.0.20" "@emotion/is-prop-valid" "^0.8.6" "@emotion/styled" "^10.0.17" - "@storybook/client-logger" "6.0.16" + "@storybook/client-logger" "6.0.26" core-js "^3.0.1" deep-object-diff "^1.1.0" emotion-theming "^10.0.19" @@ -3287,21 +3309,21 @@ resolve-from "^5.0.0" ts-dedent "^1.1.1" -"@storybook/ui@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.0.16.tgz#448d2286404554afb13e27fecd9efb0861fa9286" - integrity sha512-4F21kwQVaMwgqoJmO+566j7MXmvPp+7jfWBMPAvyGsf5uIZ4q6V29h5mMLvTOFA4qHw0lHZk2k8V0g5gk/tjCA== +"@storybook/ui@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.0.26.tgz#60e97d2044a3f63b489d7ad0b0529d93373b71ee" + integrity sha512-Jb7oUJs6uyW+rM4zA8xDn9T0/0XtUAOC/zBl6ofdhYU9rVjYKAQUJqmYgUHNOggq1NGS7BVp1RJIzDWGYEagsA== dependencies: "@emotion/core" "^10.0.20" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/router" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/router" "6.0.26" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.0.16" + "@storybook/theming" "6.0.26" "@types/markdown-to-jsx" "^6.11.0" copy-to-clipboard "^3.0.8" core-js "^3.0.1" @@ -6735,6 +6757,11 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +axe-core@^3.5.2: + version "3.5.5" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" + integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== + axe-core@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.0.2.tgz#c7cf7378378a51fcd272d3c09668002a4990b1cb" @@ -7649,7 +7676,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browser-resolve@^1.11.3, browser-resolve@^1.8.1: +browser-resolve@^1.8.1: version "1.11.3" resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== @@ -11199,7 +11226,7 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -element-resize-detector@^1.1.12: +element-resize-detector@^1.1.12, element-resize-detector@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.1.tgz#b0305194447a4863155e58f13323a0aef30851d1" integrity sha512-BdFsPepnQr9fznNPF9nF4vQ457U/ZJXQDSNF1zBe7yaga8v9AdZf3/NElYxFdUh7SitSGt040QygiTo6dtatIw== @@ -12262,7 +12289,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect@^24.8.0, expect@^24.9.0: +expect@^24.8.0: version "24.9.0" resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== @@ -12462,7 +12489,7 @@ fast-equals@^2.0.0: resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.0.tgz#bef2c423af3939f2c54310df54c57e64cd2adefc" integrity sha512-u6RBd8cSiLLxAiC04wVsLV6GBFDOXcTCgWkd3wEoFXgidPSoAJENqC9m7Jb2vewSvjBIfXV6icKeh3GTKfIaXA== -fast-glob@2.2.7, fast-glob@^2.0.2, fast-glob@^2.2.6: +fast-glob@^2.0.2, fast-glob@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== @@ -13672,7 +13699,7 @@ glob-to-regexp@^0.4.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.0.tgz#49bd677b1671022bd10921c3788f23cdebf9c7e6" integrity sha512-fyPCII4vn9Gvjq2U/oDAfP433aiE64cyP/CJjRJcpVGjqqNdioUYn9+r0cSzT1XPwmGAHuTT7iv+rQT8u/YHKQ== -glob-watcher@5.0.3, glob-watcher@^5.0.3: +glob-watcher@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.3.tgz#88a8abf1c4d131eb93928994bc4a593c2e5dd626" integrity sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg== @@ -17092,7 +17119,7 @@ jest-mock@^26.3.0: "@jest/types" "^26.3.0" "@types/node" "*" -jest-pnp-resolver@^1.2.1, jest-pnp-resolver@^1.2.2: +jest-pnp-resolver@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== @@ -17121,17 +17148,6 @@ jest-resolve-dependencies@^26.4.2: jest-regex-util "^26.0.0" jest-snapshot "^26.4.2" -jest-resolve@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" - integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== - dependencies: - "@jest/types" "^24.9.0" - browser-resolve "^1.11.3" - chalk "^2.0.1" - jest-pnp-resolver "^1.2.1" - realpath-native "^1.1.0" - jest-resolve@^26.4.0, jest-resolve@^26.5.2: version "26.5.2" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.5.2.tgz#0d719144f61944a428657b755a0e5c6af4fc8602" @@ -17212,25 +17228,6 @@ jest-serializer@^26.5.0: "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^24.1.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" - integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew== - dependencies: - "@babel/types" "^7.0.0" - "@jest/types" "^24.9.0" - chalk "^2.0.1" - expect "^24.9.0" - jest-diff "^24.9.0" - jest-get-type "^24.9.0" - jest-matcher-utils "^24.9.0" - jest-message-util "^24.9.0" - jest-resolve "^24.9.0" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - pretty-format "^24.9.0" - semver "^6.2.0" - jest-snapshot@^26.3.0, jest-snapshot@^26.4.2: version "26.4.2" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.4.2.tgz#87d3ac2f2bd87ea8003602fbebd8fcb9e94104f6" @@ -17252,13 +17249,6 @@ jest-snapshot@^26.3.0, jest-snapshot@^26.4.2: pretty-format "^26.4.2" semver "^7.3.2" -jest-specific-snapshot@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/jest-specific-snapshot/-/jest-specific-snapshot-2.0.0.tgz#425fe524b25df154aa39f97fa6fe9726faaac273" - integrity sha512-aXaNqBg/svwEpY5iQEzEHc5I85cUBKgfeVka9KmpznxLnatpjiqjr7QLb/BYNYlsrZjZzgRHTjQJ+Svx+dbdvg== - dependencies: - jest-snapshot "^24.1.0" - jest-specific-snapshot@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jest-specific-snapshot/-/jest-specific-snapshot-4.0.0.tgz#a52a2e223e7576e610dbeaf341207c557ac20554" @@ -18868,10 +18858,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 +18883,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" @@ -21921,11 +21911,16 @@ prettier@1.16.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717" integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g== -prettier@^2.0.5, prettier@^2.1.1: +prettier@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== +prettier@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" + integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== + pretty-bytes@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" @@ -23003,6 +22998,16 @@ react-sizeme@^2.3.6: invariant "^2.2.2" lodash "^4.17.4" +react-sizeme@^2.5.2: + version "2.6.12" + resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.6.12.tgz#ed207be5476f4a85bf364e92042520499455453e" + integrity sha512-tL4sCgfmvapYRZ1FO2VmBmjPVzzqgHA7kI8lSJ6JS6L78jXFNRdOZFpXyK6P1NBZvKPPCZxReNgzZNUajAerZw== + dependencies: + element-resize-detector "^1.2.1" + invariant "^2.2.4" + shallowequal "^1.1.0" + throttle-debounce "^2.1.0" + react-sizeme@^2.6.7: version "2.6.10" resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.6.10.tgz#9993dcb5e67fab94a8e5d078a0d3820609010f17" @@ -23373,13 +23378,6 @@ readline2@^1.0.1: is-fullwidth-code-point "^1.0.0" mute-stream "0.0.5" -realpath-native@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" - integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== - dependencies: - util.promisify "^1.0.0" - recast@^0.14.7: version "0.14.7" resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d" @@ -26009,10 +26007,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 +27004,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" @@ -27840,7 +27843,7 @@ util-extend@^1.0.1: resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" integrity sha1-p8IW0mdUUWljeztu3GypEZ4v+T8= -util.promisify@1.0.0, util.promisify@^1.0.0, util.promisify@~1.0.0: +util.promisify@1.0.0, util.promisify@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== @@ -28090,10 +28093,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 +28105,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 +28246,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"