diff --git a/NOTICE.txt b/NOTICE.txt index 0504b7f7d6db2..24940e232e88f 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -118,6 +118,212 @@ 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/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc index 4887eb6ca870d..f49e2a944c900 100644 --- a/docs/canvas/canvas-share-workpad.asciidoc +++ b/docs/canvas/canvas-share-workpad.asciidoc @@ -10,7 +10,7 @@ When you've finished your workpad, you can share it outside of {kib}. Create a JSON file of your workpad that you can export outside of {kib}. -Click *Share > Download as JSON*. +To begin, click *Share > Download as JSON*. [role="screenshot"] image::images/canvas-export-workpad.png[Export single workpad through JSON, from Share dropdown] @@ -23,7 +23,7 @@ Want to export multiple workpads? Go to the *Canvas* home page, select the workp If you have a subscription that supports the {report-features}, you can create a PDF copy of your workpad that you can save and share outside {kib}. -Click *Share > PDF reports > Generate PDF*. +To begin, click *Share > PDF reports > Generate PDF*. [role="screenshot"] image::images/canvas-generate-pdf.gif[Image showing how to generate a PDF] @@ -36,7 +36,7 @@ For more information, refer to <> or a script. -Click *Share > PDF reports > Copy POST URL*. +To begin, click *Share > PDF reports > Copy POST URL*. [role="screenshot"] image::images/canvas-create-URL.gif[Image showing how to create POST URL] diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc index ea4d2c8cc6a83..312391541a777 100644 --- a/docs/canvas/canvas-tutorial.asciidoc +++ b/docs/canvas/canvas-tutorial.asciidoc @@ -2,7 +2,7 @@ [[canvas-tutorial]] == Tutorial: Create a workpad for monitoring sales -To get up and running with Canvas, use the following tutorial where you'll create a workpad for monitoring sales at an eCommerce store. +To get up and running with Canvas, add the Sample eCommerce orders data, then use the data to create a workpad for monitoring sales at an eCommerce store. [float] === Before you begin @@ -114,18 +114,16 @@ image::images/canvas-timefilter-element.png[Image showing Canvas workpad with fi To see how the data changes, set the time filter to *Last 7 days*. As you change the time filter options, the elements automatically update. -Your workpad is now complete! +Your workpad is complete! [float] -=== Next steps +=== What's next? Now that you know the Canvas basics, you're ready to explore on your own. Here are some things to try: * Play with the {kibana-ref}/add-sample-data.html[sample Canvas workpads]. -* Build presentations of your own live data with <>. - -* Learn more about <> — the building blocks of your workpad. +* Build presentations of your own data with <>. * Deep dive into the {kibana-ref}/canvas-function-reference.html[expression language and functions] that drive Canvas. diff --git a/docs/canvas/images/canvas-autoplay-interval.png b/docs/canvas/images/canvas-autoplay-interval.png index 68a7ca248d9ee..a7b1251efc808 100644 Binary files a/docs/canvas/images/canvas-autoplay-interval.png and b/docs/canvas/images/canvas-autoplay-interval.png differ diff --git a/docs/canvas/images/canvas-gs-example.png b/docs/canvas/images/canvas-gs-example.png index 90beccd322aa4..a9b960342709f 100644 Binary files a/docs/canvas/images/canvas-gs-example.png and b/docs/canvas/images/canvas-gs-example.png differ diff --git a/docs/canvas/images/canvas-refresh-interval.png b/docs/canvas/images/canvas-refresh-interval.png index 62e88ad4bf7d0..c097d950a7ec7 100644 Binary files a/docs/canvas/images/canvas-refresh-interval.png and b/docs/canvas/images/canvas-refresh-interval.png differ diff --git a/docs/canvas/images/canvas-zoom-controls.png b/docs/canvas/images/canvas-zoom-controls.png index 5c72d118041e4..1407ca3cd8627 100644 Binary files a/docs/canvas/images/canvas-zoom-controls.png and b/docs/canvas/images/canvas-zoom-controls.png differ diff --git a/docs/developer/contributing/development-documentation.asciidoc b/docs/developer/contributing/development-documentation.asciidoc index 99e55963f57af..70dd756ca808e 100644 --- a/docs/developer/contributing/development-documentation.asciidoc +++ b/docs/developer/contributing/development-documentation.asciidoc @@ -26,6 +26,13 @@ README for getting the docs tooling set up. ```bash node scripts/docs.js --open ``` +[discrete] +==== REST APIs + +REST APIs should be documented using the following recommended formats: + +* https://raw.githubusercontent.com/elastic/docs/master/shared/api-ref-ex.asciidoc[API doc templaate] +* https://raw.githubusercontent.com/elastic/docs/master/shared/api-definitions-ex.asciidoc[API object definition template] [discrete] === General developer documentation and guidelines diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md index 1fe551def29ba..5cfd5e1bc9929 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md @@ -7,10 +7,10 @@ Signature: ```typescript -export declare type IndexPatternSelectProps = Required, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>, 'onChange' | 'placeholder'> & { +export declare type IndexPatternSelectProps = Required, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange'>, 'placeholder'> & { + onChange: (indexPatternId?: string) => void; indexPatternId: string; fieldTypes?: string[]; onNoIndexPatterns?: () => void; - savedObjectsClient: SavedObjectsClientContract; }; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md index d35dc3aa11000..e7c331bad64e8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (kibanaRequest: KibanaRequest) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract) => Promise; }; ``` @@ -22,6 +22,6 @@ start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): Returns: `{ - indexPatternsServiceFactory: (kibanaRequest: KibanaRequest) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract) => Promise; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index e44cb5c657747..215eac9829451 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attribute_service_key.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attribute_service_key.md new file mode 100644 index 0000000000000..9504d50cf92d3 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attribute_service_key.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [ATTRIBUTE\_SERVICE\_KEY](./kibana-plugin-plugins-embeddable-public.attribute_service_key.md) + +## ATTRIBUTE\_SERVICE\_KEY variable + +The attribute service is a shared, generic service that embeddables can use to provide the functionality required to fulfill the requirements of the ReferenceOrValueEmbeddable interface. The attribute\_service can also be used as a higher level wrapper to transform an embeddable input shape that references a saved object into an embeddable input shape that contains that saved object's attributes by value. + +Signature: + +```typescript +ATTRIBUTE_SERVICE_KEY = "attributes" +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice._constructor_.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice._constructor_.md new file mode 100644 index 0000000000000..930250be2018b --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice._constructor_.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [(constructor)](./kibana-plugin-plugins-embeddable-public.attributeservice._constructor_.md) + +## AttributeService.(constructor) + +Constructs a new instance of the `AttributeService` class + +Signature: + +```typescript +constructor(type: string, showSaveModal: (saveModal: React.ReactElement, I18nContext: I18nStart['Context']) => void, i18nContext: I18nStart['Context'], toasts: NotificationsStart['toasts'], options: AttributeServiceOptions, getEmbeddableFactory?: (embeddableFactoryId: string) => EmbeddableFactory); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| showSaveModal | (saveModal: React.ReactElement, I18nContext: I18nStart['Context']) => void | | +| i18nContext | I18nStart['Context'] | | +| toasts | NotificationsStart['toasts'] | | +| options | AttributeServiceOptions<SavedObjectAttributes> | | +| getEmbeddableFactory | (embeddableFactoryId: string) => EmbeddableFactory | | + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getexplicitinputfromembeddable.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getexplicitinputfromembeddable.md new file mode 100644 index 0000000000000..e3f27723e1a70 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getexplicitinputfromembeddable.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [getExplicitInputFromEmbeddable](./kibana-plugin-plugins-embeddable-public.attributeservice.getexplicitinputfromembeddable.md) + +## AttributeService.getExplicitInputFromEmbeddable() method + +Signature: + +```typescript +getExplicitInputFromEmbeddable(embeddable: IEmbeddable): ValType | RefType; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| embeddable | IEmbeddable | | + +Returns: + +`ValType | RefType` + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasreftype.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasreftype.md new file mode 100644 index 0000000000000..7908327c594d8 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasreftype.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [getInputAsRefType](./kibana-plugin-plugins-embeddable-public.attributeservice.getinputasreftype.md) + +## AttributeService.getInputAsRefType property + +Signature: + +```typescript +getInputAsRefType: (input: ValType | RefType, saveOptions?: { + showSaveModal: boolean; + saveModalTitle?: string | undefined; + } | { + title: string; + } | undefined) => Promise; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasvaluetype.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasvaluetype.md new file mode 100644 index 0000000000000..939194575cbb7 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.getinputasvaluetype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [getInputAsValueType](./kibana-plugin-plugins-embeddable-public.attributeservice.getinputasvaluetype.md) + +## AttributeService.getInputAsValueType property + +Signature: + +```typescript +getInputAsValueType: (input: ValType | RefType) => Promise; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.inputisreftype.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.inputisreftype.md new file mode 100644 index 0000000000000..c17ad97c3eeed --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.inputisreftype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [inputIsRefType](./kibana-plugin-plugins-embeddable-public.attributeservice.inputisreftype.md) + +## AttributeService.inputIsRefType property + +Signature: + +```typescript +inputIsRefType: (input: ValType | RefType) => input is RefType; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.md new file mode 100644 index 0000000000000..b63516c909d3c --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.md @@ -0,0 +1,40 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) + +## AttributeService class + +Signature: + +```typescript +export declare class AttributeService +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(type, showSaveModal, i18nContext, toasts, options, getEmbeddableFactory)](./kibana-plugin-plugins-embeddable-public.attributeservice._constructor_.md) | | Constructs a new instance of the AttributeService class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [getInputAsRefType](./kibana-plugin-plugins-embeddable-public.attributeservice.getinputasreftype.md) | | (input: ValType | RefType, saveOptions?: {
showSaveModal: boolean;
saveModalTitle?: string | undefined;
} | {
title: string;
} | undefined) => Promise<RefType> | | +| [getInputAsValueType](./kibana-plugin-plugins-embeddable-public.attributeservice.getinputasvaluetype.md) | | (input: ValType | RefType) => Promise<ValType> | | +| [inputIsRefType](./kibana-plugin-plugins-embeddable-public.attributeservice.inputisreftype.md) | | (input: ValType | RefType) => input is RefType | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [getExplicitInputFromEmbeddable(embeddable)](./kibana-plugin-plugins-embeddable-public.attributeservice.getexplicitinputfromembeddable.md) | | | +| [unwrapAttributes(input)](./kibana-plugin-plugins-embeddable-public.attributeservice.unwrapattributes.md) | | | +| [wrapAttributes(newAttributes, useRefType, input)](./kibana-plugin-plugins-embeddable-public.attributeservice.wrapattributes.md) | | | + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.unwrapattributes.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.unwrapattributes.md new file mode 100644 index 0000000000000..f08736a2240a3 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.unwrapattributes.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [unwrapAttributes](./kibana-plugin-plugins-embeddable-public.attributeservice.unwrapattributes.md) + +## AttributeService.unwrapAttributes() method + +Signature: + +```typescript +unwrapAttributes(input: RefType | ValType): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| input | RefType | ValType | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.wrapattributes.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.wrapattributes.md new file mode 100644 index 0000000000000..e22a2ec3faeb4 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.attributeservice.wrapattributes.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) > [wrapAttributes](./kibana-plugin-plugins-embeddable-public.attributeservice.wrapattributes.md) + +## AttributeService.wrapAttributes() method + +Signature: + +```typescript +wrapAttributes(newAttributes: SavedObjectAttributes, useRefType: boolean, input?: ValType | RefType): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| newAttributes | SavedObjectAttributes | | +| useRefType | boolean | | +| input | ValType | RefType | | + +Returns: + +`Promise>` + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.getattributeservice.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.getattributeservice.md new file mode 100644 index 0000000000000..ca75b756d199e --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.getattributeservice.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [EmbeddableStart](./kibana-plugin-plugins-embeddable-public.embeddablestart.md) > [getAttributeService](./kibana-plugin-plugins-embeddable-public.embeddablestart.getattributeservice.md) + +## EmbeddableStart.getAttributeService property + +Signature: + +```typescript +getAttributeService: (type: string, options: AttributeServiceOptions) => AttributeService; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md index f8e0028d8344b..541575566d3f7 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md @@ -15,6 +15,7 @@ export interface EmbeddableStart extends PersistableState | Property | Type | Description | | --- | --- | --- | | [EmbeddablePanel](./kibana-plugin-plugins-embeddable-public.embeddablestart.embeddablepanel.md) | EmbeddablePanelHOC | | +| [getAttributeService](./kibana-plugin-plugins-embeddable-public.embeddablestart.getattributeservice.md) | <A extends {
title: string;
}, V extends EmbeddableInput & {
[ATTRIBUTE_SERVICE_KEY]: A;
} = EmbeddableInput & {
[ATTRIBUTE_SERVICE_KEY]: A;
}, R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput>(type: string, options: AttributeServiceOptions<A>) => AttributeService<A, V, R> | | | [getEmbeddableFactories](./kibana-plugin-plugins-embeddable-public.embeddablestart.getembeddablefactories.md) | () => IterableIterator<EmbeddableFactory> | | | [getEmbeddableFactory](./kibana-plugin-plugins-embeddable-public.embeddablestart.getembeddablefactory.md) | <I extends EmbeddableInput = EmbeddableInput, O extends EmbeddableOutput = EmbeddableOutput, E extends IEmbeddable<I, O> = IEmbeddable<I, O>>(embeddableFactoryId: string) => EmbeddableFactory<I, O, E> | undefined | | | [getEmbeddablePanel](./kibana-plugin-plugins-embeddable-public.embeddablestart.getembeddablepanel.md) | (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md index 64dfdd1c6dc22..df67eda5074b9 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md @@ -9,6 +9,7 @@ | Class | Description | | --- | --- | | [AddPanelAction](./kibana-plugin-plugins-embeddable-public.addpanelaction.md) | | +| [AttributeService](./kibana-plugin-plugins-embeddable-public.attributeservice.md) | | | [Container](./kibana-plugin-plugins-embeddable-public.container.md) | | | [EditPanelAction](./kibana-plugin-plugins-embeddable-public.editpanelaction.md) | | | [Embeddable](./kibana-plugin-plugins-embeddable-public.embeddable.md) | | @@ -71,6 +72,7 @@ | --- | --- | | [ACTION\_ADD\_PANEL](./kibana-plugin-plugins-embeddable-public.action_add_panel.md) | | | [ACTION\_EDIT\_PANEL](./kibana-plugin-plugins-embeddable-public.action_edit_panel.md) | | +| [ATTRIBUTE\_SERVICE\_KEY](./kibana-plugin-plugins-embeddable-public.attribute_service_key.md) | The attribute service is a shared, generic service that embeddables can use to provide the functionality required to fulfill the requirements of the ReferenceOrValueEmbeddable interface. The attribute\_service can also be used as a higher level wrapper to transform an embeddable input shape that references a saved object into an embeddable input shape that contains that saved object's attributes by value. | | [CONTEXT\_MENU\_TRIGGER](./kibana-plugin-plugins-embeddable-public.context_menu_trigger.md) | | | [contextMenuTrigger](./kibana-plugin-plugins-embeddable-public.contextmenutrigger.md) | | | [defaultEmbeddableFactoryProvider](./kibana-plugin-plugins-embeddable-public.defaultembeddablefactoryprovider.md) | | diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md new file mode 100644 index 0000000000000..9cd77ca6e3a36 --- /dev/null +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) > [EmbeddableSetup](./kibana-plugin-plugins-embeddable-server.embeddablesetup.md) > [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) + +## EmbeddableSetup.getAttributeService property + +Signature: + +```typescript +getAttributeService: any; +``` diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md index 59ca4a2bbca75..bd024095e80be 100644 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md @@ -14,6 +14,7 @@ export interface EmbeddableSetup | Property | Type | Description | | --- | --- | --- | +| [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) | any | | | [registerEmbeddableFactory](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerembeddablefactory.md) | (factory: EmbeddableRegistryDefinition) => void | | | [registerEnhancement](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerenhancement.md) | (enhancement: EnhancementRegistryDefinition) => void | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md index 9dbd18ae687b4..ab0273be71402 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md @@ -18,5 +18,6 @@ export interface IInterpreterRenderHandlers | [event](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.event.md) | (event: any) => void | | | [onDestroy](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void | | | [reload](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.reload.md) | () => void | | +| [uiState](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md) | PersistedState | | | [update](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.update.md) | (params: any) => void | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md new file mode 100644 index 0000000000000..8d74c8e555fee --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md) > [uiState](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md) + +## IInterpreterRenderHandlers.uiState property + +Signature: + +```typescript +uiState?: PersistedState; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md index cbaffa04bae8f..ccf6271f712b9 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md @@ -18,5 +18,6 @@ export interface IInterpreterRenderHandlers | [event](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.event.md) | (event: any) => void | | | [onDestroy](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void | | | [reload](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.reload.md) | () => void | | +| [uiState](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md) | PersistedState | | | [update](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.update.md) | (params: any) => void | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md new file mode 100644 index 0000000000000..b09433c6454ad --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md) > [uiState](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md) + +## IInterpreterRenderHandlers.uiState property + +Signature: + +```typescript +uiState?: PersistedState; +``` diff --git a/docs/getting-started/images/add-sample-data.png b/docs/getting-started/images/add-sample-data.png index 1878550bc3169..b8c2002b9c4cd 100644 Binary files a/docs/getting-started/images/add-sample-data.png and b/docs/getting-started/images/add-sample-data.png differ diff --git a/docs/getting-started/images/gs_maps_time_filter.png b/docs/getting-started/images/gs_maps_time_filter.png deleted file mode 100644 index 83e20c279906e..0000000000000 Binary files a/docs/getting-started/images/gs_maps_time_filter.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-dashboard.png b/docs/getting-started/images/tutorial-dashboard.png deleted file mode 100644 index 8193d410bc5f1..0000000000000 Binary files a/docs/getting-started/images/tutorial-dashboard.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-discover-2.png b/docs/getting-started/images/tutorial-discover-2.png index 681e4834de830..cf217562c37fd 100644 Binary files a/docs/getting-started/images/tutorial-discover-2.png and b/docs/getting-started/images/tutorial-discover-2.png differ diff --git a/docs/getting-started/images/tutorial-discover-3.png b/docs/getting-started/images/tutorial-discover-3.png index bbab47acaf9d4..b024ad6dc39fe 100644 Binary files a/docs/getting-started/images/tutorial-discover-3.png and b/docs/getting-started/images/tutorial-discover-3.png differ diff --git a/docs/getting-started/images/tutorial-discover-4.png b/docs/getting-started/images/tutorial-discover-4.png new file mode 100644 index 0000000000000..945a6155c02cd Binary files /dev/null and b/docs/getting-started/images/tutorial-discover-4.png differ diff --git a/docs/getting-started/images/tutorial-final-dashboard.gif b/docs/getting-started/images/tutorial-final-dashboard.gif new file mode 100644 index 0000000000000..53b7bc04c5f65 Binary files /dev/null and b/docs/getting-started/images/tutorial-final-dashboard.gif differ diff --git a/docs/getting-started/images/tutorial-full-inspect1.png b/docs/getting-started/images/tutorial-full-inspect1.png deleted file mode 100644 index 94c9f2566f624..0000000000000 Binary files a/docs/getting-started/images/tutorial-full-inspect1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-pattern-1.png b/docs/getting-started/images/tutorial-pattern-1.png deleted file mode 100644 index 0026b18775518..0000000000000 Binary files a/docs/getting-started/images/tutorial-pattern-1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-dashboard.png b/docs/getting-started/images/tutorial-sample-dashboard.png index ccce8c3bb3208..9f287640f201c 100644 Binary files a/docs/getting-started/images/tutorial-sample-dashboard.png and b/docs/getting-started/images/tutorial-sample-dashboard.png differ diff --git a/docs/getting-started/images/tutorial-sample-discover1.png b/docs/getting-started/images/tutorial-sample-discover1.png deleted file mode 100644 index 1bad8774ba584..0000000000000 Binary files a/docs/getting-started/images/tutorial-sample-discover1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-discover2.png b/docs/getting-started/images/tutorial-sample-discover2.png deleted file mode 100644 index a439f1d76a991..0000000000000 Binary files a/docs/getting-started/images/tutorial-sample-discover2.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-edit1.png b/docs/getting-started/images/tutorial-sample-edit1.png deleted file mode 100644 index b5ae56b5c0d83..0000000000000 Binary files a/docs/getting-started/images/tutorial-sample-edit1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-edit2.png b/docs/getting-started/images/tutorial-sample-edit2.png deleted file mode 100644 index 17a029a17e1b4..0000000000000 Binary files a/docs/getting-started/images/tutorial-sample-edit2.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-filter.png b/docs/getting-started/images/tutorial-sample-filter.png index 770e26e951b3a..7c1d041448557 100644 Binary files a/docs/getting-started/images/tutorial-sample-filter.png and b/docs/getting-started/images/tutorial-sample-filter.png differ diff --git a/docs/getting-started/images/tutorial-sample-filter2.png b/docs/getting-started/images/tutorial-sample-filter2.png new file mode 100644 index 0000000000000..21402feacdecd Binary files /dev/null and b/docs/getting-started/images/tutorial-sample-filter2.png differ diff --git a/docs/getting-started/images/tutorial-sample-inspect1.png b/docs/getting-started/images/tutorial-sample-inspect1.png deleted file mode 100644 index 6a3d41ae03584..0000000000000 Binary files a/docs/getting-started/images/tutorial-sample-inspect1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-query.png b/docs/getting-started/images/tutorial-sample-query.png index 847542c0b17ff..4f1ca24924b28 100644 Binary files a/docs/getting-started/images/tutorial-sample-query.png and b/docs/getting-started/images/tutorial-sample-query.png differ diff --git a/docs/getting-started/images/tutorial-sample-query2.png b/docs/getting-started/images/tutorial-sample-query2.png new file mode 100644 index 0000000000000..0e91e1069a201 Binary files /dev/null and b/docs/getting-started/images/tutorial-sample-query2.png differ diff --git a/docs/getting-started/images/tutorial-treemap.png b/docs/getting-started/images/tutorial-treemap.png new file mode 100644 index 0000000000000..32e14fd2308e3 Binary files /dev/null and b/docs/getting-started/images/tutorial-treemap.png differ diff --git a/docs/getting-started/images/tutorial-visualization-dropdown.png b/docs/getting-started/images/tutorial-visualization-dropdown.png new file mode 100644 index 0000000000000..29d1b99700964 Binary files /dev/null and b/docs/getting-started/images/tutorial-visualization-dropdown.png differ diff --git a/docs/getting-started/images/tutorial-visualize-bar-1.5.png b/docs/getting-started/images/tutorial-visualize-bar-1.5.png deleted file mode 100644 index 009152f9407e4..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-bar-1.5.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-visualize-map-2.png b/docs/getting-started/images/tutorial-visualize-map-2.png deleted file mode 100644 index ed2fd47cb27de..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-map-2.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-visualize-md-2.png b/docs/getting-started/images/tutorial-visualize-md-2.png deleted file mode 100644 index af56faa3b0516..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-md-2.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-visualize-pie-2.png b/docs/getting-started/images/tutorial-visualize-pie-2.png deleted file mode 100644 index ca8f5e92146bc..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-pie-2.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-visualize-pie-3.png b/docs/getting-started/images/tutorial-visualize-pie-3.png deleted file mode 100644 index 59fce360096c0..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-pie-3.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-visualize-wizard-step-1.png b/docs/getting-started/images/tutorial-visualize-wizard-step-1.png deleted file mode 100644 index afc9dda648265..0000000000000 Binary files a/docs/getting-started/images/tutorial-visualize-wizard-step-1.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial_index_patterns.png b/docs/getting-started/images/tutorial_index_patterns.png deleted file mode 100644 index 430baf898b612..0000000000000 Binary files a/docs/getting-started/images/tutorial_index_patterns.png and /dev/null differ diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc new file mode 100644 index 0000000000000..6386feac5ab49 --- /dev/null +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -0,0 +1,142 @@ +[[get-started]] +== Quick start + +To quickly get up and running with {kib}, set up on Cloud, then add a sample data set that you can explore and analyze. + +When you've finished, you'll know how to: + +* <> + +* <> + +[float] +=== Before you begin +When security is enabled, you must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices. For more information, refer to {ref}/security-privileges.html[Security privileges]. + +[float] +[[set-up-on-cloud]] +== Set up on cloud + +include::{docs-root}/shared/cloud/ess-getting-started.asciidoc[] + +[float] +[[gs-get-data-into-kibana]] +== Add the sample data + +Sample data sets come with sample visualizations, dashboards, and more to help you explore {kib} without adding your own data. + +. From the home page, click *Try our sample data*. + +. On the *Sample eCommerce orders* card, click *Add data*. ++ +[role="screenshot"] +image::getting-started/images/add-sample-data.png[] + +[float] +[[explore-the-data]] +== Explore the data + +*Discover* displays an interactive histogram that shows the distribution of of data, or documents, over time, and a table that lists the fields for each document that matches the index. By default, all fields are shown for each matching document. + +. Open the menu, then click *Discover*. + +. Change the <> to *Last 7 days*. ++ +[role="screenshot"] +image::images/tutorial-discover-2.png[] + +. To focus in on the documents you want to view, use the <>. In the *KQL* search field, enter: ++ +[source,text] +products.taxless_price >= 60 AND category : Women's Clothing ++ +The query returns the women's clothing orders for $60 and more. ++ +[role="screenshot"] +image::images/tutorial-discover-4.png[] + +. Hover over the list of *Available fields*, then click *+* next to the fields you want to view in the table. ++ +For example, when you add the *category* field, the table displays the product categories for the orders. ++ +[role="screenshot"] +image::images/tutorial-discover-3.png[] ++ +For more information, refer to <>. + +[float] +[[view-and-analyze-the-data]] +== View and analyze the data + +A dashboard is a collection of panels that you can use to view and analyze the data. Panels contain visualizations, interactive controls, Markdown, and more. + +. Open the menu, then click *Dashboard*. + +. Click *[eCommerce] Revenue Dashboard*. ++ +[role="screenshot"] +image::getting-started/images/tutorial-sample-dashboard.png[] + +[float] +[[filter-and-query-the-data]] +=== Filter the data + +To focus in on the data you want to view on the dashboard, use filters. + +. From the *Controls* visualization, make a selection from the *Manufacturer* and *Category* dropdowns, then click *Apply changes*. ++ +For example, the following dashboard shows the data for women's clothing from Gnomehouse. ++ +[role="screenshot"] +image::getting-started/images/tutorial-sample-filter.png[] + +. To manually add a filter, click *Add filter*, then specify the options. ++ +For example, to view the orders for Wednesday, select *day_of_week* from the *Field* dropdown, select *is* from the *Operator* dropdown, then select *Wednesday* from the *Value* dropdown. ++ +[role="screenshot"] +image::getting-started/images/tutorial-sample-filter2.png[] + +. When you are done, remove the filters. ++ +For more information, refer to <>. + +[float] +[[create-a-visualization]] +=== Create a visualization + +To create a treemap that shows the top regions and manufacturers, use *Lens*, then add the treemap to the dashboard. + +. From the {kib} toolbar, click *Edit*, then click *Create new*. + +. On the *New Visualization* window, click *Lens*. + +. From the *Available fields* list, drag and drop the following fields to the visualization builder: + +* *geoip.city_name* + +* *manufacturer.keyword* ++ +. From the visualization dropdown, select *Treemap*. ++ +[role="screenshot"] +image::getting-started/images/tutorial-visualization-dropdown.png[Visualization dropdown with Treemap selected] + +. Click *Save*. + +. On the *Save Lens visualization*, enter a title and make sure *Add to Dashboard after saving* is selected, then click *Save and return*. ++ +The treemap appears as the last visualization on the dashboard. ++ +[role="screenshot"] +image::getting-started/images/tutorial-final-dashboard.gif[Final dashboard with new treemap visualization] ++ +For more information, refer to <>. + +[float] +[[quick-start-whats-next]] +== What's next? + +If you are you ready to add your own data, refer to <>. + +If you want to ingest your data, refer to {ingest-guide}/ingest-management-getting-started.html[Quick start: Get logs and metrics into the Elastic Stack]. diff --git a/docs/getting-started/tutorial-define-index.asciidoc b/docs/getting-started/tutorial-define-index.asciidoc deleted file mode 100644 index 215952c2d3595..0000000000000 --- a/docs/getting-started/tutorial-define-index.asciidoc +++ /dev/null @@ -1,56 +0,0 @@ -[[tutorial-define-index]] -=== Define your index patterns - -Index patterns tell {kib} which {es} indices you want to explore. -An index pattern can match the name of a single index, or include a wildcard -(*) to match multiple indices. - -For example, Logstash typically creates a -series of indices in the format `logstash-YYYY.MMM.DD`. To explore all -of the log data from May 2018, you could specify the index pattern -`logstash-2018.05*`. - -[float] -==== Create the index patterns - -First you'll create index patterns for the Shakespeare data set, which has an -index named `shakespeare,` and the accounts data set, which has an index named -`bank`. These data sets don't contain time series data. - -. Open the menu, then go to *Stack Management > {kib} > Index Patterns*. - -. If this is your first index pattern, the *Create index pattern* page opens. - -. In the *Index pattern name* field, enter `shakes*`. -+ -[role="screenshot"] -image::images/tutorial-pattern-1.png[Image showing how to enter shakes* in Index Pattern Name field] - -. Click *Next step*. - -. On the *Configure settings* page, *Create index pattern*. -+ -You’re presented a table of all fields and associated data types in the index. - -. Create a second index pattern named `ba*`. - -[float] -==== Create an index pattern for the time series data - -Create an index pattern for the Logstash index, which -contains the time series data. - -. Create an index pattern named `logstash*`, then click *Next step*. - -. From the *Time field* dropdown, select *@timestamp, then click *Create index pattern*. -+ -[role="screenshot"] -image::images/tutorial_index_patterns.png[Image showing how to create an index pattern] - -NOTE: When you define an index pattern, the indices that match that pattern must -exist in Elasticsearch and they must contain data. To check if the indices are -available, open the menu, go to *Dev Tools > Console*, then enter `GET _cat/indices`. Alternately, use -`curl -XGET "http://localhost:9200/_cat/indices"`. -For Windows, run `Invoke-RestMethod -Uri "http://localhost:9200/_cat/indices"` in Powershell. - - diff --git a/docs/getting-started/tutorial-discovering.asciidoc b/docs/getting-started/tutorial-discovering.asciidoc deleted file mode 100644 index 99a07acf98791..0000000000000 --- a/docs/getting-started/tutorial-discovering.asciidoc +++ /dev/null @@ -1,35 +0,0 @@ -[[explore-your-data]] -=== Explore your data - -With *Discover*, you use {ref}/query-dsl-query-string-query.html#query-string-syntax[Elasticsearch -queries] to explore your data and narrow the results with filters. - -. Open the menu, then go to *Discover*. -+ -The `shakes*` index pattern appears. - -. To make `ba*` the index, click the *Change Index Pattern* dropdown, then select `ba*`. -+ -By default, all fields are shown for each matching document. - -. In the *Search* field, enter the following, then click *Update*: -+ -[source,text] -account_number<100 AND balance>47500 -+ -The search returns all account numbers between zero and 99 with balances in -excess of 47,500. Results appear for account numbers 8, 32, 78, 85, and 97. -+ -[role="screenshot"] -image::images/tutorial-discover-2.png[Image showing the search results for account numbers between zero and 99, with balances in excess of 47,500] -+ -. Hover over the list of *Available fields*, then -click *Add* next to each field you want include in the table. -+ -For example, when you add the `account_number` field, the display changes to a list of five -account numbers. -+ -[role="screenshot"] -image::images/tutorial-discover-3.png[Image showing a dropdown with five account numbers, which match the previous query for account balance] - -Now that you know what your documents contain, it's time to gain insight into your data with visualizations. diff --git a/docs/getting-started/tutorial-full-experience.asciidoc b/docs/getting-started/tutorial-full-experience.asciidoc deleted file mode 100644 index a7d5412ae0632..0000000000000 --- a/docs/getting-started/tutorial-full-experience.asciidoc +++ /dev/null @@ -1,219 +0,0 @@ -[[create-your-own-dashboard]] -== Create your own dashboard - -Ready to add data to {kib} and create your own dashboard? In this tutorial, you'll use three types of data sets that'll help you learn to: - -* <> -* <> -* <> -* <> - -[float] -[[download-the-data]] -=== Download the data - -To complete the tutorial, you'll download and use the following data sets: - -* The complete works of William Shakespeare, suitably parsed into fields -* A set of fictitious bank accounts with randomly generated data -* A set of randomly generated log files - -Create a new working directory where you want to download the files. From that directory, run the following commands: - -[source,shell] -curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/shakespeare.json -curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/accounts.zip -curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/logs.jsonl.gz - -Alternatively, for Windows users, run the following commands in Powershell: - -[source,shell] -Invoke-RestMethod https://download.elastic.co/demos/kibana/gettingstarted/8.x/shakespeare.json -OutFile shakespeare.json -Invoke-RestMethod https://download.elastic.co/demos/kibana/gettingstarted/8.x/accounts.zip -OutFile accounts.zip -Invoke-RestMethod https://download.elastic.co/demos/kibana/gettingstarted/8.x/logs.jsonl.gz -OutFile logs.jsonl.gz - -Two of the data sets are compressed. To extract the files, use these commands: - -[source,shell] -unzip accounts.zip -gunzip logs.jsonl.gz - -[float] -==== Structure of the data sets - -The Shakespeare data set has the following structure: - -[source,json] -{ - "line_id": INT, - "play_name": "String", - "speech_number": INT, - "line_number": "String", - "speaker": "String", - "text_entry": "String", -} - -The accounts data set has the following structure: - -[source,json] -{ - "account_number": INT, - "balance": INT, - "firstname": "String", - "lastname": "String", - "age": INT, - "gender": "M or F", - "address": "String", - "employer": "String", - "email": "String", - "city": "String", - "state": "String" -} - -The logs data set has dozens of different fields. The notable fields include the following: - -[source,json] -{ - "memory": INT, - "geo.coordinates": "geo_point" - "@timestamp": "date" -} - -[float] -==== Set up mappings - -Before you load the Shakespeare and logs data sets, you must set up {ref}/mapping.html[_mappings_] for the fields. -Mappings divide the documents in the index into logical groups and specify the characteristics -of the fields. These characteristics include the searchability of the field -and whether it's _tokenized_, or broken up into separate words. - -NOTE: If security is enabled, you must have the `all` Kibana privilege to run this tutorial. -You must also have the `create`, `manage` `read`, `write,` and `delete` -index privileges. See {ref}/security-privileges.html[Security privileges] -for more information. - -Open the menu, then go to *Dev Tools*. On the *Console* page, set up a mapping for the Shakespeare data set: - -[source,js] -PUT /shakespeare -{ - "mappings": { - "properties": { - "speaker": {"type": "keyword"}, - "play_name": {"type": "keyword"}, - "line_id": {"type": "integer"}, - "speech_number": {"type": "integer"} - } - } -} - -//CONSOLE - -The mapping specifies field characteristics for the data set: - -* The `speaker` and `play_name` fields are keyword fields. These fields are not analyzed. -The strings are treated as a single unit even if they contain multiple words. - -* The `line_id` and `speech_number` fields are integers. - -The logs data set requires a mapping to label the latitude and longitude pairs -as geographic locations by applying the `geo_point` type. - -[source,js] -PUT /logstash-2015.05.18 -{ - "mappings": { - "properties": { - "geo": { - "properties": { - "coordinates": { - "type": "geo_point" - } - } - } - } - } -} - -//CONSOLE - -[source,js] -PUT /logstash-2015.05.19 -{ - "mappings": { - "properties": { - "geo": { - "properties": { - "coordinates": { - "type": "geo_point" - } - } - } - } - } -} - -//CONSOLE - -[source,js] -PUT /logstash-2015.05.20 -{ - "mappings": { - "properties": { - "geo": { - "properties": { - "coordinates": { - "type": "geo_point" - } - } - } - } - } -} - -//CONSOLE - -The accounts data set doesn't require any mappings. - -[float] -[[load-the-data-sets]] -==== Load the data sets - -At this point, you're ready to use the Elasticsearch {ref}/docs-bulk.html[bulk] -API to load the data sets: - -[source,shell] -curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/bank/_bulk?pretty' --data-binary @accounts.json -curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/shakespeare/_bulk?pretty' --data-binary @shakespeare.json -curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/_bulk?pretty' --data-binary @logs.jsonl - -Or for Windows users, in Powershell: -[source,shell] -Invoke-RestMethod "http://:/bank/account/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "accounts.json" -Invoke-RestMethod "http://:/shakespeare/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "shakespeare.json" -Invoke-RestMethod "http://:/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "logs.jsonl" - -These commands might take some time to execute, depending on the available computing resources. - -When you define an index pattern, the indices that match the pattern must -exist in {es} and contain data. - -To verify the availability of the indices, open the menu, go to *Dev Tools > Console*, then enter: - -[source,js] -GET /_cat/indices?v - -Alternately, use: - -[source,shell] -`curl -XGET "http://localhost:9200/_cat/indices"`. - -The output should look similar to: - -[source,shell] -health status index pri rep docs.count docs.deleted store.size pri.store.size -yellow open bank 1 1 1000 0 418.2kb 418.2kb -yellow open shakespeare 1 1 111396 0 17.6mb 17.6mb -yellow open logstash-2015.05.18 1 1 4631 0 15.6mb 15.6mb -yellow open logstash-2015.05.19 1 1 4624 0 15.7mb 15.7mb -yellow open logstash-2015.05.20 1 1 4750 0 16.4mb 16.4mb diff --git a/docs/getting-started/tutorial-sample-data.asciidoc b/docs/getting-started/tutorial-sample-data.asciidoc deleted file mode 100644 index 18ef862272f85..0000000000000 --- a/docs/getting-started/tutorial-sample-data.asciidoc +++ /dev/null @@ -1,159 +0,0 @@ -[[explore-kibana-using-sample-data]] -== Explore {kib} using sample data - -Ready to get some hands-on experience with {kib}? -In this tutorial, you’ll work with {kib} sample data and learn to: - -* <> - -* <> - -* <> - -NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges -on the `kibana_sample_data_*` indices. For more information, refer to -{ref}/security-privileges.html[Security privileges]. - -[float] -[[add-the-sample-data]] -=== Add the sample data - -Add the *Sample flight data*. - -. On the home page, click *Load a data set and a {kib} dashboard*. - -. On the *Sample flight data* card, click *Add data*. - -[float] -[[explore-the-data]] -=== Explore the data - -Explore the documents in the index that -match the selected index pattern. The index pattern tells {kib} which {es} index you want to -explore. - -. Open the menu, then go to *Discover*. - -. Make sure `kibana_sample_data_flights` is the current index pattern. -You might need to click *New* in the {kib} toolbar to refresh the data. -+ -You'll see a histogram that shows the distribution of -documents over time. A table lists the fields for -each document that matches the index. By default, all fields are shown. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-discover1.png[] - -. Hover over the list of *Available fields*, then click *Add* next -to each field you want explore in the table. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-discover2.png[] - -[float] -[[view-and-analyze-the-data]] -=== View and analyze the data - -A _dashboard_ is a collection of panels that provide you with an overview of your data that you can -use to analyze your data. Panels contain everything you need, including visualizations, -interactive controls, Markdown, and more. - -To open the *Global Flight* dashboard, open the menu, then go to *Dashboard*. - -[role="screenshot"] -image::getting-started/images/tutorial-sample-dashboard.png[] - -[float] -[[change-the-panel-data]] -==== Change the panel data - -To gain insights into your data, change the appearance and behavior of the panels. -For example, edit the metric panel to find the airline that has the lowest average fares. - -. In the {kib} toolbar, click *Edit*. - -. In the *Average Ticket Price* metric panel, open the panel menu, then select *Edit visualization*. - -. To change the data on the panel, use an {es} {ref}/search-aggregations.html[bucket aggregation], -which sorts the documents that match your search criteria into different categories or buckets. - -.. In the *Buckets* pane, select *Add > Split group*. - -.. From the *Aggregation* dropdown, select *Terms*. - -.. From the *Field* dropdown, select *Carrier*. - -.. Set *Descending* to *4*, then click *Update*. -+ -The average ticket price for all four airlines appear in the visualization builder. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-edit1.png[] - -. To save your changes, click *Save and return* in the {kib} toolbar. - -. To save the dashboard, click *Save* in the {kib} toolbar. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-edit2.png[] - -[float] -[[filter-and-query-the-data]] -==== Filter and query the data - -To focus in on the data you want to explore, use filters and queries. -For more information, refer to -{ref}/query-filter-context.html[Query and filter context]. - -To filter the data: - -. In the *Controls* visualization, select an *Origin City* and *Destination City*, then click *Apply changes*. -+ -The `OriginCityName` and the `DestCityName` fields filter the data in the panels. -+ -For example, the following dashboard shows the data for flights from London to Milan. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-filter.png[] - -. To manually add a filter, click *Add filter*, -then specify the data you want to view. - -. When you are finished experimenting, remove all filters. - -[[query-the-data]] -To query the data: - -. To view all flights out of Rome, enter the following in the *KQL* query bar, then click *Update*: -+ -[source,text] -OriginCityName: Rome - -. For a more complex query with AND and OR, enter: -+ -[source,text] -OriginCityName:Rome AND (Carrier:JetBeats OR Carrier:"Kibana Airlines") -+ -The dashboard panels update to display the flights out of Rome on JetBeats and -{kib} Airlines. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-query.png[] - -. When you are finished exploring, remove the query by -clearing the contents in the *KQL* query bar, then click *Update*. - -[float] -=== Next steps - -Now that you know the {kib} basics, try out the <> tutorial, where you'll learn to: - -* Add a data set to {kib} - -* Define an index pattern - -* Discover and explore data - -* Create and add panels to a dashboard - - diff --git a/docs/getting-started/tutorial-visualizing.asciidoc b/docs/getting-started/tutorial-visualizing.asciidoc deleted file mode 100644 index a53c8cb6bc23d..0000000000000 --- a/docs/getting-started/tutorial-visualizing.asciidoc +++ /dev/null @@ -1,193 +0,0 @@ -[[tutorial-visualizing]] -=== Visualize your data - -Shape your data using a variety -of {kib} supported visualizations, tables, and more. In this tutorial, you'll create four -visualizations that you'll use to create a dashboard. - -To begin, open the menu, go to *Dashboard*, then click *Create new dashboard*. - -[float] -[[compare-the-number-of-speaking-parts-in-the-play]] -=== Compare the number of speaking parts in the plays - -To visualize the Shakespeare data and compare the number of speaking parts in the plays, create a bar chart using *Lens*. - -. Click *Create new*, then click *Lens* on the *New Visualization* window. -+ -[role="screenshot"] -image::images/tutorial-visualize-wizard-step-1.png[Image showing different options for your new visualization] - -. Make sure the index pattern is *shakes*. - -. Display the play data along the x-axis. - -.. From the *Available fields* list, drag and drop *play_name* to the *X-axis* field. - -.. Click *Top values of play_name*. - -.. From the *Order direction* dropdown, select *Ascending*. - -.. In the *Label* field, enter `Play Name`. - -. Display the number of speaking parts per play along the y-axis. - -.. From the *Available fields* list, drag and drop *speaker* to the *Y-axis* field. - -.. Click *Unique count of speaker*. - -.. In the *Label* field, enter `Speaking Parts`. -+ -[role="screenshot"] -image::images/tutorial-visualize-bar-1.5.png[Bar chart showing the speaking parts data] - -. *Save* the chart with the name `Bar Example`. -+ -To show a tooltip with the number of speaking parts for that play, hover over a bar. -+ -Notice how the individual play names show up as whole phrases, instead of -broken up into individual words. This is the result of the mapping -you did at the beginning of the tutorial, when you marked the `play_name` field -as `not analyzed`. - -[float] -[[view-the-average-account-balance-by-age]] -=== View the average account balance by age - -To gain insight into the account balances in the bank account data, create a pie chart. In this tutorial, you'll use the {es} -{ref}/search-aggregations.html[bucket aggregation] to specify the pie slices to display. The bucket aggregation sorts the documents that match your search criteria into different -categories and establishes multiple ranges of account balances so that you can find how many accounts fall into each range. - -. Click *Create new*, then click *Pie* on the *New Visualization* window. - -. On the *Choose a source* window, select `ba*`. -+ -Since the default search matches all documents, the pie contains a single slice. - -. In the *Buckets* pane, click *Add > Split slices.* - -.. From the *Aggregation* dropdown, select *Range*. - -.. From the *Field* dropdown, select *balance*. - -.. Click *Add range* until there are six rows of fields, then define the following ranges: -+ -[source,text] -0 999 -1000 2999 -3000 6999 -7000 14999 -15000 30999 -31000 50000 - -. Click *Update*. -+ -The pie chart displays the proportion of the 1,000 accounts that fall into each of the ranges. -+ -[role="screenshot"] -image::images/tutorial-visualize-pie-2.png[Pie chart displaying accounts that fall into each of the ranges, scaled to 1000 accounts] - -. Add another bucket aggregation that displays the ages of the account holders. - -.. In the *Buckets* pane, click *Add*, then click *Split slices*. - -.. From the *Sub aggregation* dropdown, select *Terms*. - -.. From the *Field* dropdown, select *age*, then click *Update*. -+ -The break down of the ages of the account holders are displayed -in a ring around the balance ranges. -+ -[role="screenshot"] -image::images/tutorial-visualize-pie-3.png[Final pie chart showing all of the changes] - -. Click *Save*, then enter `Pie Example` in the *Title* field. - -[float] -[role="xpack"] -[[visualize-geographic-information]] -=== Visualize geographic information - -To visualize geographic information in the log file data, use <>. - -. Click *Create new*, then click *Maps* on the *New Visualization* window. - -. To change the time, use the time filter. - -.. Set the *Start date* to `May 18, 2015 @ 12:00:00.000`. - -.. Set the *End date* to `May 20, 2015 @ 12:00:00.000`. -+ -[role="screenshot"] -image::images/gs_maps_time_filter.png[Image showing the time filter for Maps tutorial] - -.. Click *Update* - -. Map the geo coordinates from the log files. - -.. Click *Add layer > Clusters and grids*. - -.. From the *Index pattern* dropdown, select *logstash*. - -.. Click *Add layer*. - -. Specify the *Layer Style*. - -.. From the *Fill color* dropdown, select the yellow to red color ramp. - -.. In the *Border width* field, enter `3`. - -.. From the *Border color* dropdown, select *#FFF*, then click *Save & close*. -+ -[role="screenshot"] -image::images/tutorial-visualize-map-2.png[Example of a map visualization] - -. Click *Save*, then enter `Map Example` in the *Title* field. - -. Add the map to your dashboard. - -.. Open the menu, go to *Dashboard*, then click *Add*. - -.. On the *Add panels* flyout, click *Map Example*. - -[float] -[[tutorial-visualize-markdown]] -=== Add context to your visualizations with Markdown - -Add context to your new visualizations with Markdown text. - -. Click *Create new*, then click *Markdown* on the *New Visualization* window. - -. In the *Markdown* text field, enter: -+ -[source,markdown] -# This is a tutorial dashboard! -The Markdown widget uses **markdown** syntax. -> Blockquotes in Markdown use the > character. - -. Click *Update*. -+ -The Markdown renders in the preview pane. -+ -[role="screenshot"] -image::images/tutorial-visualize-md-2.png[Image showing example markdown editing field] - -. Click *Save*, then enter `Markdown Example` in the *Title* field. - -[role="screenshot"] -image::images/tutorial-dashboard.png[Final visualization with bar chart, pie chart, map, and markdown text field] - -[float] -=== Next steps - -Now that you have the basics, you're ready to start exploring your own system data with {kib}. - -* To add your own data to {kib}, refer to <>. - -* To search and filter your data, refer to {kibana-ref}/discover.html[Discover]. - -* To create a dashboard with your own data, refer to <>. - -* To create maps that you can add to your dashboards, refer to <>. - -* To create presentations of your live data, refer to <>. diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index e20f384b5ed18..7324f45594bd7 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -67,7 +67,7 @@ You can read more at {ref}/rollup-job-config.html[rollup job configuration]. === Try it: Create and visualize rolled up data This example creates a rollup job to capture log data from sample web logs. -To follow along, add the <>. +To follow along, add the sample web logs data set. In this example, you want data that is older than 7 days in the target index pattern `kibana_sample_data_logs` to roll up once a day into the index `rollup_logstash`. You’ll bucket the diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 5067bc08bec99..92e02de89f181 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -59,7 +59,7 @@ This page has moved. Please see <>. [role="exclude",id="add-sample-data"] == Add sample data -This page has moved. Please see <>. +This page has moved. Please see <>. [role="exclude",id="tilemap"] == Coordinate map @@ -112,3 +112,9 @@ This content has moved. See This content has moved. See {ref}/ccr-getting-started.html#ccr-getting-started-remote-cluster[Connect to a remote cluster]. + +[role="exclude",id="tutorial-define-index"] +== Define your index patterns + +This content has moved. See +<>. diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index ea02afb8a9fda..0daa3f1e0e55e 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -11,7 +11,7 @@ To start working with your data in {kib}, you can: * Connect {kib} with existing {es} indices. -If you're not ready to use your own data, you can add a <> +If you're not ready to use your own data, you can add a <> to see all that you can do in {kib}. [float] diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index 0b0eb7a318495..297dfac5b10bd 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -17,6 +17,8 @@ With Canvas, you can: * Focus the data you want to display with filters. +To begin, open the menu, then go to *Canvas*. + [role="screenshot"] image::images/canvas-gs-example.png[Getting started example] @@ -26,7 +28,8 @@ For a quick overview of Canvas, watch link:https://www.youtube.com/watch?v=ZqvF_ [[create-workpads]] == Create workpads -A _workpad_ provides you with a space where you can build presentations of your live data. +A _workpad_ provides you with a space where you can build presentations of your live data. With Canvas, +you can create a workpad from scratch, start with a preconfigured workpad, import an existing workpad, or use a sample data workpad. [float] [[start-with-a-blank-workpad]] @@ -34,19 +37,15 @@ A _workpad_ provides you with a space where you can build presentations of your To use the background colors, images, and data of your choice, start with a blank workpad. -. Open the menu, then go to *Canvas*. - -. On the *Canvas workpads* view, click *Create workpad*. +. On the *Canvas workpads* page, click *Create workpad*. -. Add a *Name* to your workpad. +. Specify the *Workpad settings*. -. In the *Width* and *Height* fields, specify the size. +.. Add a *Name* to your workpad. -. Select the layout. -+ -For example, click *720p* for a traditional presentation layout. +.. In the *Width* and *Height* fields, specify the size, or select one of default layouts. -. Click the *Background color* picker, then select the background color for your workpad. +.. Click the *Background* color picker, then select the color for your workpad. + [role="screenshot"] image::images/canvas-background-color-picker.png[Canvas color picker] @@ -57,9 +56,7 @@ image::images/canvas-background-color-picker.png[Canvas color picker] If you're unsure about where to start, you can use one of the preconfigured templates that come with Canvas. -. Open the menu, then go to *Canvas*. - -. On the *Canvas workpads* view, select *Templates*. +. On the *Canvas workpads* page, select *Templates*. . Click the preconfigured template that you want to use. @@ -69,17 +66,15 @@ If you're unsure about where to start, you can use one of the preconfigured temp [[import-existing-workpads]] === Import existing workpads -When you want to use a workpad that someone else has already started, import the JSON file into Canvas. - -. Open the menu, then go to *Canvas*. +When you want to use a workpad that someone else has already started, import the JSON file. -. On the *Canvas workpads* view, click and drag the file to the *Import workpad JSON file* field. +To begin, drag the file to the *Import workpad JSON file* field on the *Canvas workpads* page. [float] [[use-sample-data-workpads]] === Use sample data workpads -Each of the sample data sets comes with a Canvas workpad that you can use for your own workpad inspiration. +Each of the {kib} sample data sets comes with a workpad that you can use for your own workpad inspiration. . Add a {kibana-ref}/add-sample-data.html[sample data set]. @@ -123,12 +118,12 @@ To save a group of elements, press and hold Shift, select the elements you want Elements are saved in *Add element > My elements*. [float] -[[add-existing-visuualizations]] -=== Add existing visualizations +[[add-saved-objects]] +=== Add saved objects Add <> to your workpad, such as maps and visualizations. -. Click *Add element > Add from Visualize Library*. +. Click *Add element > Add from {kib}*. . Select the saved object you want to add. + diff --git a/docs/user/dashboard/dashboard-drilldown.asciidoc b/docs/user/dashboard/dashboard-drilldown.asciidoc index e50c1281beede..5e928fd731bb4 100644 --- a/docs/user/dashboard/dashboard-drilldown.asciidoc +++ b/docs/user/dashboard/dashboard-drilldown.asciidoc @@ -39,7 +39,7 @@ Create the *Host Overview* drilldown shown above. *Set up the dashboards* -. Add the <> data set. +. Add the sample web logs data set. . Create a new dashboard, called `Host Overview`, and include these visualizations from the sample data set: diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index 620a2d2056bf1..b71dfb016c765 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -36,7 +36,7 @@ The following panels support URL drilldowns: This example shows how to create the "Show on Github" drilldown shown above. -. Add the <> data set. +. Add the sample web logs data set. . Open the *[Logs] Web traffic* dashboard. This isn’t data from Github, but it should work for demonstration purposes. . In the dashboard menu bar, click *Edit*. . In *[Logs] Visitors by OS*, open the panel menu, and then select *Create drilldown*. diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 6fd30690b988e..378f7a53a6650 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -64,16 +64,17 @@ Override it by providing a different `stroke`, `fill`, or `color` (Vega-Lite) va [[vega-queries]] ==== Writing {es} queries in Vega -experimental[] {kib} extends the Vega https://vega.github.io/vega/docs/data/[data] elements -with support for direct {es} queries specified as a `url`. +{kib} extends the Vega https://vega.github.io/vega/docs/data/[data] elements +with support for direct {es} queries specified as `url`. -Because of this, {kib} is **unable to support dynamically loaded data**, +{kib} is **unable to support dynamically loaded data**, which would otherwise work in Vega. All data is fetched before it's passed to the Vega renderer. -To define an {es} query in Vega, set the `url` to an object. {kib} will parse +To define an {es} query in Vega, set the `url` to an object. {kib} parses the object looking for special tokens that allow your query to integrate with {kib}. -These tokens are: + +Tokens include the following: * `%context%: true`: Set at the top level, and replaces the `query` section with filters from dashboard * `%timefield%: `: Set at the top level, integrates the query with the dashboard time filter @@ -87,8 +88,7 @@ These tokens are: * `"%dashboard_context-filter_clause%"`: String replaced by an object containing filters * `"%dashboard_context-must_not_clause%"`: String replaced by an object containing filters -Putting this together, an example query that counts the number of documents in -a specific index: +For example, the following query counts the number of documents in a specific index: [source,yaml] ---- diff --git a/docs/user/getting-started.asciidoc b/docs/user/getting-started.asciidoc deleted file mode 100644 index a877f6a66a79a..0000000000000 --- a/docs/user/getting-started.asciidoc +++ /dev/null @@ -1,61 +0,0 @@ -[[get-started]] -= Get started - -[partintro] --- - -Ready to try out {kib} and see what it can do? The quickest way to get started with {kib} is to set up on Cloud, then add a sample data set to explore the full range of {kib} features. - -[float] -[[set-up-on-cloud]] -== Set up on cloud - -include::{docs-root}/shared/cloud/ess-getting-started.asciidoc[] - -[float] -[[gs-get-data-into-kibana]] -== Get data into {kib} - -The easiest way to get data into {kib} is to add a sample data set. - -{kib} has several sample data sets that you can use before loading your own data: - -* *Sample eCommerce orders* includes visualizations for tracking product-related information, -such as cost, revenue, and price. - -* *Sample flight data* includes visualizations for monitoring flight routes. - -* *Sample web logs* includes visualizations for monitoring website traffic. - -To use the sample data sets: - -. Go to the home page. - -. Click *Load a data set and a {kib} dashboard*. - -. Click *View data* and view the prepackaged dashboards, maps, and more. - -[role="screenshot"] -image::getting-started/images/add-sample-data.png[] - -NOTE: The timestamps in the sample data sets are relative to when they are installed. -If you uninstall and reinstall a data set, the timestamps change to reflect the most recent installation. - -[float] -== Next steps - -* To get a hands-on experience creating visualizations, follow the <> tutorial. - -* If you're ready to load an actual data set and build a dashboard, follow the <> tutorial. - --- - -include::{kib-repo-dir}/getting-started/tutorial-sample-data.asciidoc[] - -include::{kib-repo-dir}/getting-started/tutorial-full-experience.asciidoc[] - -include::{kib-repo-dir}/getting-started/tutorial-define-index.asciidoc[] - -include::{kib-repo-dir}/getting-started/tutorial-discovering.asciidoc[] - -include::{kib-repo-dir}/getting-started/tutorial-visualizing.asciidoc[] diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index b0f3dbfa0c9e9..c186704dd2f16 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -2,6 +2,8 @@ include::introduction.asciidoc[] include::whats-new.asciidoc[] +include::{kib-repo-dir}/getting-started/quick-start-guide.asciidoc[] + include::setup.asciidoc[] include::monitoring/configuring-monitoring.asciidoc[leveloffset=+1] @@ -11,8 +13,6 @@ include::monitoring/monitoring-kibana.asciidoc[leveloffset=+2] include::security/securing-kibana.asciidoc[] -include::getting-started.asciidoc[] - include::discover.asciidoc[] include::dashboard/dashboard.asciidoc[] diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index 079d183dd959d..7e5dc59b03a2c 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -155,6 +155,6 @@ and start exploring data in minutes. You can also <> — no code, no additional infrastructure required. -Our <> and in-product guidance can +Our <> and in-product guidance can help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[] in the top navigation bar for help with questions or to provide feedback. diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc index cc4af9041bcd9..bf7be6284b1a9 100644 --- a/docs/user/security/rbac_tutorial.asciidoc +++ b/docs/user/security/rbac_tutorial.asciidoc @@ -28,7 +28,7 @@ To complete this tutorial, you'll need the following: * **A space**: In this tutorial, use `Dev Mortgage` as the space name. See <> for details on creating a space. -* **Data**: You can use <> or +* **Data**: You can use <> or live data. In the following steps, Filebeat and Metricbeat data are used. [float] diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index 223b8c55a5fde..6025d24665901 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["embeddable", "uiActions", "dashboard", "savedObjects"], + "requiredPlugins": ["embeddable", "uiActions", "savedObjects", "dashboard"], "optionalPlugins": [], "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"], "requiredBundles": ["kibanaReact"] diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx index 65ec22a2759e2..8d633a892ec6d 100644 --- a/examples/embeddable_examples/public/book/book_embeddable.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -26,10 +26,10 @@ import { EmbeddableOutput, SavedObjectEmbeddableInput, ReferenceOrValueEmbeddable, + AttributeService, } from '../../../../src/plugins/embeddable/public'; import { BookSavedObjectAttributes } from '../../common'; import { BookEmbeddableComponent } from './book_component'; -import { AttributeService } from '../../../../src/plugins/dashboard/public'; export const BOOK_EMBEDDABLE = 'book'; export type BookEmbeddableInput = BookByValueInput | BookByReferenceInput; diff --git a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx index a535552282150..f569e2e8d154d 100644 --- a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx @@ -25,6 +25,8 @@ import { EmbeddableFactoryDefinition, IContainer, EmbeddableFactory, + EmbeddableStart, + AttributeService, } from '../../../../src/plugins/embeddable/public'; import { BookEmbeddable, @@ -38,11 +40,10 @@ import { SavedObjectsClientContract, SimpleSavedObject, } from '../../../../src/core/public'; -import { DashboardStart, AttributeService } from '../../../../src/plugins/dashboard/public'; import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; interface StartServices { - getAttributeService: DashboardStart['getAttributeService']; + getAttributeService: EmbeddableStart['getAttributeService']; openModal: OverlayStart['openModal']; savedObjectsClient: SavedObjectsClientContract; overlays: OverlayStart; diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx index 77035b6887734..e2133a8d51ea2 100644 --- a/examples/embeddable_examples/public/book/edit_book_action.tsx +++ b/examples/embeddable_examples/public/book/edit_book_action.tsx @@ -22,7 +22,11 @@ import { i18n } from '@kbn/i18n'; import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common'; import { createAction } from '../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { ViewMode, SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/public'; +import { + ViewMode, + SavedObjectEmbeddableInput, + EmbeddableStart, +} from '../../../../src/plugins/embeddable/public'; import { BookEmbeddable, BOOK_EMBEDDABLE, @@ -30,13 +34,12 @@ import { BookByValueInput, } from './book_embeddable'; import { CreateEditBookComponent } from './create_edit_book_component'; -import { DashboardStart } from '../../../../src/plugins/dashboard/public'; import { OnSaveProps } from '../../../../src/plugins/saved_objects/public'; import { SavedObjectsClientContract } from '../../../../src/core/target/types/public/saved_objects'; interface StartServices { openModal: OverlayStart['openModal']; - getAttributeService: DashboardStart['getAttributeService']; + getAttributeService: EmbeddableStart['getAttributeService']; savedObjectsClient: SavedObjectsClientContract; } diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 6d1b119e741bd..9b9770e40611e 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -62,7 +62,6 @@ import { ACTION_ADD_BOOK_TO_LIBRARY, createAddBookToLibraryAction, } from './book/add_book_to_library_action'; -import { DashboardStart } from '../../../src/plugins/dashboard/public'; import { ACTION_UNLINK_BOOK_FROM_LIBRARY, createUnlinkBookFromLibraryAction, @@ -75,7 +74,6 @@ export interface EmbeddableExamplesSetupDependencies { export interface EmbeddableExamplesStartDependencies { embeddable: EmbeddableStart; - dashboard: DashboardStart; savedObjectsClient: SavedObjectsClient; } @@ -157,7 +155,7 @@ export class EmbeddableExamplesPlugin this.exampleEmbeddableFactories.getBookEmbeddableFactory = deps.embeddable.registerEmbeddableFactory( BOOK_EMBEDDABLE, new BookEmbeddableFactoryDefinition(async () => ({ - getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, + getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, overlays: (await core.getStartServices())[0].overlays, @@ -165,7 +163,7 @@ export class EmbeddableExamplesPlugin ); const editBookAction = createEditBookAction(async () => ({ - getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, + getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, })); diff --git a/examples/search_examples/public/components/app.tsx b/examples/search_examples/public/components/app.tsx index 61e50d3379d03..2425f3bbad8a9 100644 --- a/examples/search_examples/public/components/app.tsx +++ b/examples/search_examples/public/components/app.tsx @@ -232,7 +232,6 @@ export const SearchExamplesApp = ({ Index Pattern { const event$ = Rx.of(1, 2, 3, 4, 5); const initial = 0; - const values = await summarizeEventStream(event$, initial, (state, event) => { - if (event % 2) { - return state + event; - } - }) - .pipe(toArray()) - .toPromise(); + const values = await allValuesFrom( + summarizeEventStream(event$, initial, (state, event) => { + if (event % 2) { + return state + event; + } + }) + ); expect(values).toMatchInlineSnapshot(` Array [ @@ -57,15 +58,15 @@ it('emits each state with each event, ignoring events when summarizer returns un it('interleaves injected events when source is synchronous', async () => { const event$ = Rx.of(1, 7); const initial = 0; - const values = await summarizeEventStream(event$, initial, (state, event, injectEvent) => { - if (event < 5) { - injectEvent(event + 2); - } + const values = await allValuesFrom( + summarizeEventStream(event$, initial, (state, event, injectEvent) => { + if (event < 5) { + injectEvent(event + 2); + } - return state + event; - }) - .pipe(toArray()) - .toPromise(); + return state + event; + }) + ); expect(values).toMatchInlineSnapshot(` Array [ @@ -95,15 +96,15 @@ it('interleaves injected events when source is synchronous', async () => { it('interleaves injected events when source is asynchronous', async () => { const event$ = Rx.of(1, 7, Rx.asyncScheduler); const initial = 0; - const values = await summarizeEventStream(event$, initial, (state, event, injectEvent) => { - if (event < 5) { - injectEvent(event + 2); - } + const values = await allValuesFrom( + summarizeEventStream(event$, initial, (state, event, injectEvent) => { + if (event < 5) { + injectEvent(event + 2); + } - return state + event; - }) - .pipe(toArray()) - .toPromise(); + return state + event; + }) + ); expect(values).toMatchInlineSnapshot(` Array [ @@ -133,17 +134,17 @@ it('interleaves injected events when source is asynchronous', async () => { it('interleaves mulitple injected events in order', async () => { const event$ = Rx.of(1); const initial = 0; - const values = await summarizeEventStream(event$, initial, (state, event, injectEvent) => { - if (event < 10) { - injectEvent(10); - injectEvent(20); - injectEvent(30); - } - - return state + event; - }) - .pipe(toArray()) - .toPromise(); + const values = await allValuesFrom( + summarizeEventStream(event$, initial, (state, event, injectEvent) => { + if (event < 10) { + injectEvent(10); + injectEvent(20); + injectEvent(30); + } + + return state + event; + }) + ); expect(values).toMatchInlineSnapshot(` Array [ @@ -179,9 +180,9 @@ it('stops an infinite stream when unsubscribed', async () => { return prev + event; }); - const values = await summarizeEventStream(event$, initial, summarize) - .pipe(take(11), toArray()) - .toPromise(); + const values = await allValuesFrom( + summarizeEventStream(event$, initial, summarize).pipe(take(11)) + ); expect(values).toMatchInlineSnapshot(` Array [ diff --git a/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts b/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts index dda66c999b8f1..457da9290bbd0 100644 --- a/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts +++ b/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts @@ -19,6 +19,7 @@ import * as Rx from 'rxjs'; import { toArray, map } from 'rxjs/operators'; +import { lastValueFrom } from '@kbn/std'; import { pipeClosure, debounceTimeBuffer, maybeMap, maybe } from './rxjs_helpers'; @@ -36,21 +37,21 @@ describe('pipeClosure()', () => { toArray() ); - await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + await expect(lastValueFrom(foo$)).resolves.toMatchInlineSnapshot(` Array [ 1, 2, 3, ] `); - await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + await expect(lastValueFrom(foo$)).resolves.toMatchInlineSnapshot(` Array [ 2, 4, 6, ] `); - await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + await expect(lastValueFrom(foo$)).resolves.toMatchInlineSnapshot(` Array [ 3, 6, @@ -64,7 +65,7 @@ describe('maybe()', () => { it('filters out undefined values from the stream', async () => { const foo$ = Rx.of(1, undefined, 2, undefined, 3).pipe(maybe(), toArray()); - await expect(foo$.toPromise()).resolves.toEqual([1, 2, 3]); + await expect(lastValueFrom(foo$)).resolves.toEqual([1, 2, 3]); }); }); @@ -75,7 +76,7 @@ describe('maybeMap()', () => { toArray() ); - await expect(foo$.toPromise()).resolves.toEqual([1, 3, 5]); + await expect(lastValueFrom(foo$)).resolves.toEqual([1, 3, 5]); }); }); diff --git a/packages/kbn-optimizer/src/common/rxjs_helpers.ts b/packages/kbn-optimizer/src/common/rxjs_helpers.ts index c6385c22518aa..49bf2d8f145dd 100644 --- a/packages/kbn-optimizer/src/common/rxjs_helpers.ts +++ b/packages/kbn-optimizer/src/common/rxjs_helpers.ts @@ -18,7 +18,8 @@ */ import * as Rx from 'rxjs'; -import { mergeMap, tap, debounceTime, map } from 'rxjs/operators'; +import { mergeMap, tap, debounceTime, map, toArray } from 'rxjs/operators'; +import { firstValueFrom } from '@kbn/std'; type Operator = (source: Rx.Observable) => Rx.Observable; type MapFn = (item: T1, index: number) => T2; @@ -73,3 +74,6 @@ export const debounceTimeBuffer = (ms: number) => }) ); }); + +export const allValuesFrom = (observable: Rx.Observable) => + firstValueFrom(observable.pipe(toArray())); diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index 3dff034af886c..a89f84e5c543d 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -24,7 +24,7 @@ import { inspect } from 'util'; import cpy from 'cpy'; import del from 'del'; -import { toArray, tap, filter } from 'rxjs/operators'; +import { tap, filter } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/utils'; import { ToolingLog } from '@kbn/dev-utils'; import { @@ -35,6 +35,8 @@ import { readLimits, } from '@kbn/optimizer'; +import { allValuesFrom } from '../common'; + const TMP_DIR = Path.resolve(__dirname, '../__fixtures__/__tmp__'); const MOCK_REPO_SRC = Path.resolve(__dirname, '../__fixtures__/mock_repo'); const MOCK_REPO_DIR = Path.resolve(TMP_DIR, 'mock_repo'); @@ -83,13 +85,12 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(config).toMatchSnapshot('OptimizerConfig'); - const msgs = await runOptimizer(config) - .pipe( + const msgs = await allValuesFrom( + runOptimizer(config).pipe( logOptimizerState(log, config), - filter((x) => x.event?.type !== 'worker stdio'), - toArray() + filter((x) => x.event?.type !== 'worker stdio') ) - .toPromise(); + ); const assert = (statement: string, truth: boolean, altStates?: OptimizerUpdate[]) => { if (!truth) { @@ -208,17 +209,16 @@ it('uses cache on second run and exist cleanly', async () => { dist: false, }); - const msgs = await runOptimizer(config) - .pipe( + const msgs = await allValuesFrom( + runOptimizer(config).pipe( tap((state) => { if (state.event?.type === 'worker stdio') { // eslint-disable-next-line no-console console.log('worker', state.event.stream, state.event.line); } - }), - toArray() + }) ) - .toPromise(); + ); expect(msgs.map((m) => m.state.phase)).toMatchInlineSnapshot(` Array [ @@ -240,7 +240,7 @@ it('prepares assets for distribution', async () => { dist: true, }); - await runOptimizer(config).pipe(logOptimizerState(log, config), toArray()).toPromise(); + await allValuesFrom(runOptimizer(config).pipe(logOptimizerState(log, config))); expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); expectFileMatchesSnapshotWithCompression( diff --git a/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts b/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts index 48cab508954a0..00e6782128dd9 100644 --- a/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts @@ -21,12 +21,11 @@ import Path from 'path'; import cpy from 'cpy'; import del from 'del'; -import { toArray } from 'rxjs/operators'; import { createAbsolutePathSerializer } from '@kbn/dev-utils'; import { getMtimes } from '../optimizer/get_mtimes'; import { OptimizerConfig } from '../optimizer/optimizer_config'; -import { Bundle } from '../common/bundle'; +import { allValuesFrom, Bundle } from '../common'; import { getBundleCacheEvent$ } from '../optimizer/bundle_cache'; const TMP_DIR = Path.resolve(__dirname, '../__fixtures__/__tmp__'); @@ -78,9 +77,7 @@ it('emits "bundle cached" event when everything is updated', async () => { bundleRefExportIds: [], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -119,9 +116,7 @@ it('emits "bundle not cached" event when cacheKey is up to date but caching is d bundleRefExportIds: [], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -160,9 +155,7 @@ it('emits "bundle not cached" event when optimizerCacheKey is missing', async () bundleRefExportIds: [], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -201,9 +194,7 @@ it('emits "bundle not cached" event when optimizerCacheKey is outdated, includes bundleRefExportIds: [], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -247,9 +238,7 @@ it('emits "bundle not cached" event when bundleRefExportIds is outdated, include bundleRefExportIds: ['plugin/bar/public'], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -292,9 +281,7 @@ it('emits "bundle not cached" event when cacheKey is missing', async () => { bundleRefExportIds: [], }); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ @@ -333,9 +320,7 @@ it('emits "bundle not cached" event when cacheKey is outdated', async () => { jest.spyOn(bundle, 'createCacheKey').mockImplementation(() => 'new'); - const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) - .pipe(toArray()) - .toPromise(); + const cacheEvents = await allValuesFrom(getBundleCacheEvent$(config, optimizerCacheKey)); expect(cacheEvents).toMatchInlineSnapshot(` Array [ diff --git a/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts b/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts index 176b17c979da9..00f3c780adc0a 100644 --- a/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts @@ -20,6 +20,7 @@ import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; import ActualWatchpack from 'watchpack'; +import { lastValueFrom } from '@kbn/std'; import { Bundle, ascending } from '../common'; import { watchBundlesForChanges$ } from '../optimizer/watch_bundles_for_changes'; @@ -78,8 +79,8 @@ afterEach(async () => { it('notifies of changes and completes once all bundles have changed', async () => { expect.assertions(18); - const promise = watchBundlesForChanges$(bundleCacheEvent$, Date.now()) - .pipe( + const promise = lastValueFrom( + watchBundlesForChanges$(bundleCacheEvent$, Date.now()).pipe( map((event, i) => { // each time we trigger a change event we get a 'changed detected' event if (i === 0 || i === 2 || i === 4 || i === 6) { @@ -116,7 +117,7 @@ it('notifies of changes and completes once all bundles have changed', async () = } }) ) - .toPromise(); + ); expect(MockWatchPack.mock.instances).toHaveLength(1); const [watcher] = (MockWatchPack.mock.instances as any) as Array>; diff --git a/packages/kbn-optimizer/src/limits.ts b/packages/kbn-optimizer/src/limits.ts index 4040a0c37d3b6..b0fae0901251d 100644 --- a/packages/kbn-optimizer/src/limits.ts +++ b/packages/kbn-optimizer/src/limits.ts @@ -23,19 +23,28 @@ import dedent from 'dedent'; import Yaml from 'js-yaml'; import { createFailError, ToolingLog } from '@kbn/dev-utils'; -import { OptimizerConfig, getMetrics } from './optimizer'; +import { OptimizerConfig, getMetrics, Limits } from './optimizer'; const LIMITS_PATH = require.resolve('../limits.yml'); const DEFAULT_BUDGET = 15000; const diff = (a: T[], b: T[]): T[] => a.filter((item) => !b.includes(item)); -export function readLimits() { - return Yaml.safeLoad(Fs.readFileSync(LIMITS_PATH, 'utf8')); +export function readLimits(): Limits { + let yaml; + try { + yaml = Fs.readFileSync(LIMITS_PATH, 'utf8'); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + return yaml ? Yaml.safeLoad(yaml) : {}; } export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerConfig) { - const limitBundleIds = Object.keys(config.limits.pageLoadAssetSize); + const limitBundleIds = Object.keys(config.limits.pageLoadAssetSize || {}); const configBundleIds = config.bundles.map((b) => b.id); const missingBundleIds = diff(configBundleIds, limitBundleIds); @@ -56,7 +65,11 @@ export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerCo ${issues.join('\n ')} - To validate your changes locally, run: + To automatically update the limits file locally run: + + node scripts/build_kibana_platform_plugins.js --update-limits + + To validate your changes locally run: node scripts/build_kibana_platform_plugins.js --validate-limits ` + '\n' @@ -69,15 +82,22 @@ export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerCo export function updateBundleLimits(log: ToolingLog, config: OptimizerConfig) { const metrics = getMetrics(log, config); - const number = (input: number) => input.toLocaleString('en').split(',').join('_'); + const pageLoadAssetSize: NonNullable = {}; - let yaml = `pageLoadAssetSize:\n`; for (const metric of metrics.sort((a, b) => a.id.localeCompare(b.id))) { if (metric.group === 'page load bundle size') { - yaml += ` ${metric.id}: ${number(metric.value + DEFAULT_BUDGET)}\n`; + const existingLimit = config.limits.pageLoadAssetSize?.[metric.id]; + pageLoadAssetSize[metric.id] = + existingLimit != null && existingLimit >= metric.value + ? existingLimit + : metric.value + DEFAULT_BUDGET; } } - Fs.writeFileSync(LIMITS_PATH, yaml); + const newLimits: Limits = { + pageLoadAssetSize, + }; + + Fs.writeFileSync(LIMITS_PATH, Yaml.safeDump(newLimits)); log.success(`wrote updated limits to ${LIMITS_PATH}`); } diff --git a/packages/kbn-optimizer/src/optimizer/get_mtimes.ts b/packages/kbn-optimizer/src/optimizer/get_mtimes.ts index 07777c323637a..b6c3678709880 100644 --- a/packages/kbn-optimizer/src/optimizer/get_mtimes.ts +++ b/packages/kbn-optimizer/src/optimizer/get_mtimes.ts @@ -20,7 +20,8 @@ import Fs from 'fs'; import * as Rx from 'rxjs'; -import { mergeMap, toArray, map, catchError } from 'rxjs/operators'; +import { mergeMap, map, catchError } from 'rxjs/operators'; +import { allValuesFrom } from '../common'; const stat$ = Rx.bindNodeCallback(Fs.stat); @@ -28,20 +29,22 @@ const stat$ = Rx.bindNodeCallback(Fs.stat); * get mtimes of referenced paths concurrently, limit concurrency to 100 */ export async function getMtimes(paths: Iterable) { - return await Rx.from(paths) - .pipe( - // map paths to [path, mtimeMs] entries with concurrency of - // 100 at a time, ignoring missing paths - mergeMap( - (path) => - stat$(path).pipe( - map((stat) => [path, stat.mtimeMs] as const), - catchError((error: any) => (error?.code === 'ENOENT' ? Rx.EMPTY : Rx.throwError(error))) - ), - 100 - ), - toArray(), - map((entries) => new Map(entries)) + return new Map( + await allValuesFrom( + Rx.from(paths).pipe( + // map paths to [path, mtimeMs] entries with concurrency of + // 100 at a time, ignoring missing paths + mergeMap( + (path) => + stat$(path).pipe( + map((stat) => [path, stat.mtimeMs] as const), + catchError((error: any) => + error?.code === 'ENOENT' ? Rx.EMPTY : Rx.throwError(error) + ) + ), + 100 + ) + ) ) - .toPromise(); + ); } diff --git a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts index 24847a03edb52..cc4cd05f42c3f 100644 --- a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts +++ b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts @@ -101,7 +101,7 @@ export function getMetrics(log: ToolingLog, config: OptimizerConfig) { group: `page load bundle size`, id: bundle.id, value: entry.stats!.size, - limit: config.limits.pageLoadAssetSize[bundle.id], + limit: config.limits.pageLoadAssetSize?.[bundle.id], limitConfigPath: `packages/kbn-optimizer/limits.yml`, }, { diff --git a/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.test.ts b/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.test.ts index 6edcde56e26de..b92eee0a51fd5 100644 --- a/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.test.ts +++ b/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.test.ts @@ -20,12 +20,11 @@ import * as Rx from 'rxjs'; import { REPO_ROOT } from '@kbn/utils'; -import { Update } from '../common'; +import { Update, allValuesFrom } from '../common'; import { OptimizerState } from './optimizer_state'; import { OptimizerConfig } from './optimizer_config'; import { handleOptimizerCompletion } from './handle_optimizer_completion'; -import { toArray } from 'rxjs/operators'; const createUpdate$ = (phase: OptimizerState['phase']) => Rx.of>({ @@ -44,13 +43,12 @@ const config = (watch?: boolean) => repoRoot: REPO_ROOT, watch, }); -const collect = (stream: Rx.Observable): Promise => stream.pipe(toArray()).toPromise(); it('errors if the optimizer completes when in watch mode', async () => { const update$ = createUpdate$('success'); await expect( - collect(update$.pipe(handleOptimizerCompletion(config(true)))) + allValuesFrom(update$.pipe(handleOptimizerCompletion(config(true)))) ).rejects.toThrowErrorMatchingInlineSnapshot( `"optimizer unexpectedly completed when in watch mode"` ); @@ -60,7 +58,7 @@ it('errors if the optimizer completes in phase "issue"', async () => { const update$ = createUpdate$('issue'); await expect( - collect(update$.pipe(handleOptimizerCompletion(config()))) + allValuesFrom(update$.pipe(handleOptimizerCompletion(config()))) ).rejects.toThrowErrorMatchingInlineSnapshot(`"webpack issue"`); }); @@ -68,7 +66,7 @@ it('errors if the optimizer completes in phase "initializing"', async () => { const update$ = createUpdate$('initializing'); await expect( - collect(update$.pipe(handleOptimizerCompletion(config()))) + allValuesFrom(update$.pipe(handleOptimizerCompletion(config()))) ).rejects.toThrowErrorMatchingInlineSnapshot( `"optimizer unexpectedly exit in phase \\"initializing\\""` ); @@ -78,7 +76,7 @@ it('errors if the optimizer completes in phase "reallocating"', async () => { const update$ = createUpdate$('reallocating'); await expect( - collect(update$.pipe(handleOptimizerCompletion(config()))) + allValuesFrom(update$.pipe(handleOptimizerCompletion(config()))) ).rejects.toThrowErrorMatchingInlineSnapshot( `"optimizer unexpectedly exit in phase \\"reallocating\\""` ); @@ -88,7 +86,7 @@ it('errors if the optimizer completes in phase "running"', async () => { const update$ = createUpdate$('running'); await expect( - collect(update$.pipe(handleOptimizerCompletion(config()))) + allValuesFrom(update$.pipe(handleOptimizerCompletion(config()))) ).rejects.toThrowErrorMatchingInlineSnapshot( `"optimizer unexpectedly exit in phase \\"running\\""` ); @@ -98,7 +96,7 @@ it('passes through errors on the source stream', async () => { const error = new Error('foo'); const update$ = Rx.throwError(error); - await expect(collect(update$.pipe(handleOptimizerCompletion(config())))).rejects.toThrowError( - error - ); + await expect( + allValuesFrom(update$.pipe(handleOptimizerCompletion(config()))) + ).rejects.toThrowError(error); }); diff --git a/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts b/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts index 9bf8f9db1fe45..a7c07358fa6d6 100644 --- a/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts +++ b/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts @@ -19,7 +19,7 @@ import { Readable } from 'stream'; -import { toArray } from 'rxjs/operators'; +import { allValuesFrom } from '../common'; import { observeStdio$ } from './observe_stdio'; @@ -27,18 +27,18 @@ it('notifies on every line, uncluding partial content at the end without a newli const chunks = [`foo\nba`, `r\nb`, `az`]; await expect( - observeStdio$( - new Readable({ - read() { - this.push(chunks.shift()!); - if (!chunks.length) { - this.push(null); - } - }, - }) + allValuesFrom( + observeStdio$( + new Readable({ + read() { + this.push(chunks.shift()!); + if (!chunks.length) { + this.push(null); + } + }, + }) + ) ) - .pipe(toArray()) - .toPromise() ).resolves.toMatchInlineSnapshot(` Array [ "foo", diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 01a20bec52a04..b685d6ea01591 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -35,7 +35,7 @@ import { filterById } from './filter_by_id'; import { readLimits } from '../limits'; export interface Limits { - pageLoadAssetSize: { + pageLoadAssetSize?: { [id: string]: number | undefined; }; } diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index d78eb8214f607..0024d2801d173 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -182,7 +182,7 @@ const observeCompiler = ( ); bundle.cache.set({ - bundleRefExportIds, + bundleRefExportIds: bundleRefExportIds.sort(ascending((p) => p)), optimizerCacheKey: workerConfig.optimizerCacheKey, cacheKey: bundle.createCacheKey(files, mtimes), moduleCount, diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts index d9d3ec4b0d52b..c111428017539 100644 --- a/packages/kbn-std/src/index.ts +++ b/packages/kbn-std/src/index.ts @@ -27,3 +27,4 @@ export { withTimeout } from './promise'; export { isRelativeUrl, modifyUrl, getUrlOrigin, URLMeaningfulParts } from './url'; export { unset } from './unset'; export { getFlattenedObject } from './get_flattened_object'; +export * from './rxjs_7'; diff --git a/packages/kbn-std/src/rxjs_7.test.ts b/packages/kbn-std/src/rxjs_7.test.ts new file mode 100644 index 0000000000000..dcc73602613fa --- /dev/null +++ b/packages/kbn-std/src/rxjs_7.test.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as Rx from 'rxjs'; + +import { firstValueFrom, lastValueFrom } from './rxjs_7'; + +// create an empty observable that completes with no notifications +// after a delay to ensure helpers aren't checking for the EMPTY constant +function empty() { + return new Rx.Observable((subscriber) => { + setTimeout(() => { + subscriber.complete(); + }, 0); + }); +} + +describe('firstValueFrom()', () => { + it('resolves to the first value from the observable', async () => { + await expect(firstValueFrom(Rx.of(1, 2, 3))).resolves.toBe(1); + }); + + it('rejects if the observable is empty', async () => { + await expect(firstValueFrom(empty())).rejects.toThrowErrorMatchingInlineSnapshot( + `"no elements in sequence"` + ); + }); + + it('does not unsubscribe from the source observable that emits synchronously', async () => { + const values = [1, 2, 3, 4]; + let unsubscribed = false; + const source = new Rx.Observable((subscriber) => { + while (!subscriber.closed && values.length) { + subscriber.next(values.shift()!); + } + unsubscribed = subscriber.closed; + subscriber.complete(); + }); + + await expect(firstValueFrom(source)).resolves.toMatchInlineSnapshot(`1`); + if (unsubscribed) { + throw new Error('expected source to not be unsubscribed'); + } + expect(values).toEqual([]); + }); + + it('unsubscribes from the source observable after first async notification', async () => { + const values = [1, 2, 3, 4]; + let unsubscribed = false; + const source = new Rx.Observable((subscriber) => { + setTimeout(() => { + while (!subscriber.closed) { + subscriber.next(values.shift()!); + } + unsubscribed = subscriber.closed; + }); + }); + + await expect(firstValueFrom(source)).resolves.toMatchInlineSnapshot(`1`); + if (!unsubscribed) { + throw new Error('expected source to be unsubscribed'); + } + expect(values).toEqual([2, 3, 4]); + }); +}); + +describe('lastValueFrom()', () => { + it('resolves to the last value from the observable', async () => { + await expect(lastValueFrom(Rx.of(1, 2, 3))).resolves.toBe(3); + }); + + it('rejects if the observable is empty', async () => { + await expect(lastValueFrom(empty())).rejects.toThrowErrorMatchingInlineSnapshot( + `"no elements in sequence"` + ); + }); +}); diff --git a/packages/kbn-std/src/rxjs_7.ts b/packages/kbn-std/src/rxjs_7.ts new file mode 100644 index 0000000000000..f0a1be9125cc8 --- /dev/null +++ b/packages/kbn-std/src/rxjs_7.ts @@ -0,0 +1,251 @@ +/* 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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'; + +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()); + }, + }) + ); + }); +} + +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()); + } + }, + }); + }); +} diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 4e86ec4bd72e0..8422c34c9ed08 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -11,6 +11,7 @@ }, "devDependencies": { "@babel/cli": "^7.10.5", + "@jest/types": "^26.5.2", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/utils": "1.0.0", @@ -22,14 +23,18 @@ "diff": "^4.0.1" }, "dependencies": { + "@jest/reporters": "^26.5.2", "chalk": "^4.1.0", "dedent": "^0.7.0", "del": "^5.1.0", + "execa": "^4.0.2", "exit-hook": "^2.2.0", "getopts": "^2.2.5", "glob": "^7.1.2", + "globby": "^8.0.1", "joi": "^13.5.2", "lodash": "^4.17.20", + "mustache": "^2.3.2", "parse-link-header": "^1.0.1", "rxjs": "^6.5.5", "strip-ansi": "^6.0.0", diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 50ef521a2d811..a57b92fbdde25 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -55,8 +55,8 @@ export { export { runFailedTestsReporterCli } from './failed_tests_reporter'; -export { makeJunitReportPath } from './junit_report_path'; - export { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix'; export * from './functional_test_runner'; + +export * from './jest'; diff --git a/packages/kbn-test/src/jest/index.ts b/packages/kbn-test/src/jest/index.ts new file mode 100644 index 0000000000000..c6d680863d9c4 --- /dev/null +++ b/packages/kbn-test/src/jest/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './junit_reporter'; + +export * from './report_path'; diff --git a/packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js b/packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js new file mode 100644 index 0000000000000..50016c883d378 --- /dev/null +++ b/packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { resolve } = require('path'); +const { REPO_ROOT } = require('@kbn/utils'); + +module.exports = { + reporters: [ + 'default', + [ + `${REPO_ROOT}/packages/kbn-test/target/jest/junit_reporter`, + { + reportName: 'JUnit Reporter Integration Test', + rootDirectory: resolve( + REPO_ROOT, + 'packages/kbn-test/src/jest/integration_tests/__fixtures__' + ), + }, + ], + ], +}; diff --git a/src/dev/jest/integration_tests/__fixtures__/test.js b/packages/kbn-test/src/jest/integration_tests/__fixtures__/test.js similarity index 89% rename from src/dev/jest/integration_tests/__fixtures__/test.js rename to packages/kbn-test/src/jest/integration_tests/__fixtures__/test.js index fe0c65fd45b74..161012fb3a91c 100644 --- a/src/dev/jest/integration_tests/__fixtures__/test.js +++ b/packages/kbn-test/src/jest/integration_tests/__fixtures__/test.js @@ -17,6 +17,8 @@ * under the License. */ -it('fails', () => { - throw new Error('failure'); +describe('JUnit Reporter', () => { + it('fails', () => { + throw new Error('failure'); + }); }); diff --git a/src/dev/jest/integration_tests/junit_reporter.test.js b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts similarity index 82% rename from src/dev/jest/integration_tests/junit_reporter.test.js rename to packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts index 3280482a54203..1390c44d84a07 100644 --- a/src/dev/jest/integration_tests/junit_reporter.test.js +++ b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts @@ -25,27 +25,26 @@ import del from 'del'; import execa from 'execa'; import xml2js from 'xml2js'; import { makeJunitReportPath } from '@kbn/test'; +import { REPO_ROOT } from '@kbn/utils'; const MINUTE = 1000 * 60; -const ROOT_DIR = resolve(__dirname, '../../../../'); const FIXTURE_DIR = resolve(__dirname, '__fixtures__'); const TARGET_DIR = resolve(FIXTURE_DIR, 'target'); -const XML_PATH = makeJunitReportPath(FIXTURE_DIR, 'Jest Tests'); +const XML_PATH = makeJunitReportPath(FIXTURE_DIR, 'JUnit Reporter Integration Test'); afterAll(async () => { await del(TARGET_DIR); }); const parseXml = promisify(xml2js.parseString); - it( 'produces a valid junit report for failures', async () => { const result = await execa( - process.execPath, - ['-r', require.resolve('../../../setup_node_env'), require.resolve('jest/bin/jest')], + './node_modules/.bin/jest', + ['--config', 'packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js'], { - cwd: FIXTURE_DIR, + cwd: REPO_ROOT, env: { CI: 'true', }, @@ -57,6 +56,7 @@ it( await expect(parseXml(readFileSync(XML_PATH, 'utf8'))).resolves.toEqual({ testsuites: { $: { + failures: '1', name: 'jest', skipped: '0', tests: '1', @@ -67,7 +67,7 @@ it( { $: { failures: '1', - file: resolve(ROOT_DIR, 'src/dev/jest/integration_tests/__fixtures__/test.js'), + file: resolve(FIXTURE_DIR, './test.js'), name: 'test.js', skipped: '0', tests: '1', @@ -77,8 +77,8 @@ it( testcase: [ { $: { - classname: 'Jest Tests.·', - name: 'fails', + classname: 'JUnit Reporter Integration Test.·', + name: 'JUnit Reporter fails', time: expect.anything(), }, failure: [expect.stringMatching(/Error: failure\s+at /m)], diff --git a/src/dev/jest/junit_reporter.js b/packages/kbn-test/src/jest/junit_reporter.ts similarity index 74% rename from src/dev/jest/junit_reporter.js rename to packages/kbn-test/src/jest/junit_reporter.ts index f33358c081a06..8deb6751bc268 100644 --- a/src/dev/jest/junit_reporter.js +++ b/packages/kbn-test/src/jest/junit_reporter.ts @@ -22,20 +22,31 @@ import { writeFileSync, mkdirSync } from 'fs'; import xmlBuilder from 'xmlbuilder'; -import { escapeCdata, makeJunitReportPath } from '@kbn/test'; +import type { Config } from '@jest/types'; +import { AggregatedResult, Test, BaseReporter } from '@jest/reporters'; -const ROOT_DIR = dirname(require.resolve('../../../package.json')); +import { escapeCdata } from '../mocha/xml'; +import { makeJunitReportPath } from './report_path'; + +interface ReporterOptions { + reportName?: string; + rootDirectory?: string; +} /** * Jest reporter that produces JUnit report when running on CI * @class JestJUnitReporter */ -export default class JestJUnitReporter { - constructor(globalConfig, options = {}) { - const { reportName = 'Jest Tests', rootDirectory = ROOT_DIR } = options; - this._reportName = reportName; - this._rootDirectory = resolve(rootDirectory); +// eslint-disable-next-line import/no-default-export +export default class JestJUnitReporter extends BaseReporter { + private _reportName: string; + private _rootDirectory: string; + + constructor(globalConfig: Config.GlobalConfig, { rootDirectory, reportName }: ReporterOptions) { + super(); + this._reportName = reportName || 'Jest Tests'; + this._rootDirectory = rootDirectory ? resolve(rootDirectory) : resolve(__dirname, '../..'); } /** @@ -44,7 +55,7 @@ export default class JestJUnitReporter { * @param {JestResults} results see https://facebook.github.io/jest/docs/en/configuration.html#testresultsprocessor-string * @return {undefined} */ - onRunComplete(contexts, results) { + onRunComplete(contexts: Set, results: AggregatedResult): void { if (!process.env.CI || process.env.DISABLE_JUNIT_REPORTER || !results.testResults.length) { return; } @@ -55,18 +66,19 @@ export default class JestJUnitReporter { 'testsuites', { encoding: 'utf-8' }, {}, - { skipNullAttributes: true } + { keepNullAttributes: false } ); - const msToIso = (ms) => (ms ? new Date(ms).toISOString().slice(0, -5) : undefined); - const msToSec = (ms) => (ms ? (ms / 1000).toFixed(3) : undefined); + const msToIso = (ms: number | null | undefined) => + ms ? new Date(ms).toISOString().slice(0, -5) : undefined; + const msToSec = (ms: number | null | undefined) => (ms ? (ms / 1000).toFixed(3) : undefined); root.att({ name: 'jest', timestamp: msToIso(results.startTime), time: msToSec(Date.now() - results.startTime), tests: results.numTotalTests, - failures: results.numFailingTests, + failures: results.numFailedTests, skipped: results.numPendingTests, }); diff --git a/packages/kbn-test/src/junit_report_path.ts b/packages/kbn-test/src/jest/report_path.ts similarity index 93% rename from packages/kbn-test/src/junit_report_path.ts rename to packages/kbn-test/src/jest/report_path.ts index 90405d7a89c02..fe122c349c193 100644 --- a/packages/kbn-test/src/junit_report_path.ts +++ b/packages/kbn-test/src/jest/report_path.ts @@ -18,7 +18,7 @@ */ import { resolve } from 'path'; -import { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix'; +import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix'; export function makeJunitReportPath(rootDirectory: string, reportName: string) { return resolve( diff --git a/src/dev/jest/config.integration.js b/src/dev/jest/config.integration.js index 2fd6bd120d553..970c00bb68b98 100644 --- a/src/dev/jest/config.integration.js +++ b/src/dev/jest/config.integration.js @@ -31,7 +31,10 @@ export default { ), reporters: [ 'default', - ['/src/dev/jest/junit_reporter.js', { reportName: 'Jest Integration Tests' }], + [ + '/packages/kbn-test/target/jest/junit_reporter', + { reportName: 'Jest Integration Tests' }, + ], ], setupFilesAfterEnv: ['/src/dev/jest/setup/after_env.integration.js'], }; diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 3c556a4f1ba3c..f582a8f3d4410 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -99,5 +99,5 @@ export default { '/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts', '/node_modules/enzyme-to-json/serializer', ], - reporters: ['default', '/src/dev/jest/junit_reporter.js'], + reporters: ['default', '/packages/kbn-test/target/jest/junit_reporter'], }; diff --git a/src/dev/jest/integration_tests/__fixtures__/package.json b/src/dev/jest/integration_tests/__fixtures__/package.json deleted file mode 100644 index 1a9a446d524bc..0000000000000 --- a/src/dev/jest/integration_tests/__fixtures__/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "fixture", - "jest": { - "testMatch": [ - "**/test.js" - ], - "transform": { - "^.+\\.js$": "/../../babel_transform.js" - }, - "reporters": [ - ["/../../junit_reporter.js", {"rootDirectory": "."}] - ] - } -} diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts index 9f7eb9d9e1aa4..e362427682ec0 100644 --- a/src/dev/notice/generate_notice_from_source.ts +++ b/src/dev/notice/generate_notice_from_source.ts @@ -52,7 +52,7 @@ export async function generateNoticeFromSource({ productName, directory, log }: 'src/plugins/*/{node_modules,build,dist}/**', 'x-pack/{node_modules,build,dist,data}/**', 'x-pack/packages/*/{node_modules,build,dist}/**', - 'x-pack/plugins/*/{node_modules,build,dist}/**', + 'x-pack/plugins/**/{node_modules,build,dist}/**', '**/target/**', ], }; diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index bf9a3b2b8a217..004b1a901bca9 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -45,7 +45,6 @@ export { export { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; export { SavedObjectDashboard } from './saved_dashboards'; export { SavedDashboardPanel } from './types'; -export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './attribute_service'; export function plugin(initializerContext: PluginInitializerContext) { return new DashboardPlugin(initializerContext); diff --git a/src/plugins/dashboard/public/mocks.tsx b/src/plugins/dashboard/public/mocks.tsx index 07f29eca53042..be7460f3110a5 100644 --- a/src/plugins/dashboard/public/mocks.tsx +++ b/src/plugins/dashboard/public/mocks.tsx @@ -20,13 +20,10 @@ import { DashboardStart } from './plugin'; export type Start = jest.Mocked; -export { mockAttributeService } from './attribute_service/attribute_service.mock'; const createStartContract = (): DashboardStart => { // @ts-ignore - const startContract: DashboardStart = { - getAttributeService: jest.fn(), - }; + const startContract: DashboardStart = {}; return startContract; }; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 3325d193e56ed..2fda8008788e6 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -39,8 +39,6 @@ import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart, - SavedObjectEmbeddableInput, - EmbeddableInput, PANEL_NOTIFICATION_TRIGGER, } from '../../embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from '../../data/public'; @@ -53,7 +51,6 @@ import { getSavedObjectFinder, SavedObjectLoader, SavedObjectsStart, - showSaveModal, } from '../../saved_objects/public'; import { ExitFullScreenButton as ExitFullScreenButtonUi, @@ -103,11 +100,6 @@ import { DashboardConstants } from './dashboard_constants'; import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; -import { AttributeService } from '.'; -import { - AttributeServiceOptions, - ATTRIBUTE_SERVICE_KEY, -} from './attribute_service/attribute_service'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -156,16 +148,6 @@ export interface DashboardStart { dashboardUrlGenerator?: DashboardUrlGenerator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; DashboardContainerByValueRenderer: ReturnType; - getAttributeService: < - A extends { title: string }, - V extends EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: A } = EmbeddableInput & { - [ATTRIBUTE_SERVICE_KEY]: A; - }, - R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput - >( - type: string, - options: AttributeServiceOptions
- ) => AttributeService; } declare module '../../../plugins/ui_actions/public' { @@ -433,7 +415,6 @@ export class DashboardPlugin const { uiActions, data: { indexPatterns, search }, - embeddable, } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -483,15 +464,6 @@ export class DashboardPlugin DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), - getAttributeService: (type: string, options) => - new AttributeService( - type, - showSaveModal, - core.i18n.Context, - core.notifications.toasts, - options, - embeddable.getEmbeddableFactory - ), }; } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 13f658b562d39..0477fb68b3c1e 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1290,11 +1290,11 @@ export type IndexPatternsContract = PublicMethodsOf; // Warning: (ae-missing-release-tag) "IndexPatternSelectProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type IndexPatternSelectProps = Required, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>, 'onChange' | 'placeholder'> & { +export type IndexPatternSelectProps = Required, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange'>, 'placeholder'> & { + onChange: (indexPatternId?: string) => void; indexPatternId: string; fieldTypes?: string[]; onNoIndexPatterns?: () => void; - savedObjectsClient: SavedObjectsClientContract; }; // Warning: (ae-missing-release-tag) "IndexPatternSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/data/public/query/timefilter/timefilter.test.ts b/src/plugins/data/public/query/timefilter/timefilter.test.ts index 1280664ac8389..6c1a4eff786f6 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.test.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.test.ts @@ -54,6 +54,10 @@ function clearNowTimeStub() { delete global.nowTime; } +test('isTimeTouched is initially set to false', () => { + expect(timefilter.isTimeTouched()).toBe(false); +}); + describe('setTime', () => { let update: sinon.SinonSpy; let fetch: sinon.SinonSpy; @@ -84,6 +88,10 @@ describe('setTime', () => { }); }); + test('should update isTimeTouched', () => { + expect(timefilter.isTimeTouched()).toBe(true); + }); + test('should not add unexpected object keys to time state', () => { const unexpectedKey = 'unexpectedKey'; timefilter.setTime({ diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 5eb1546fa015f..01b82087cf354 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -41,6 +41,8 @@ export class Timefilter { private fetch$ = new Subject(); private _time: TimeRange; + // Denotes whether setTime has been called, can be used to determine if the constructor defaults are being used. + private _isTimeTouched: boolean = false; private _refreshInterval!: RefreshInterval; private _history: TimeHistoryContract; @@ -68,6 +70,10 @@ export class Timefilter { return this._isAutoRefreshSelectorEnabled; } + public isTimeTouched() { + return this._isTimeTouched; + } + public getEnabledUpdated$ = () => { return this.enabledUpdated$.asObservable(); }; @@ -112,6 +118,7 @@ export class Timefilter { from: newTime.from, to: newTime.to, }; + this._isTimeTouched = true; this._history.add(this._time); this.timeUpdate$.next(); this.fetch$.next(); diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts index 7863000b1ace4..060257a880528 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts @@ -26,6 +26,7 @@ const createSetupContractMock = () => { const timefilterMock: jest.Mocked = { isAutoRefreshSelectorEnabled: jest.fn(), isTimeRangeSelectorEnabled: jest.fn(), + isTimeTouched: jest.fn(), getEnabledUpdated$: jest.fn(), getTimeUpdate$: jest.fn(), getRefreshIntervalUpdate$: jest.fn(), diff --git a/src/plugins/data/public/ui/index_pattern_select/create_index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/create_index_pattern_select.tsx index b49cc24ba90b1..a48c2dabf1506 100644 --- a/src/plugins/data/public/ui/index_pattern_select/create_index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/create_index_pattern_select.tsx @@ -25,7 +25,7 @@ import { IndexPatternSelect, IndexPatternSelectProps } from './'; // Takes in stateful runtime dependencies and pre-wires them to the component export function createIndexPatternSelect(savedObjectsClient: SavedObjectsClientContract) { - return (props: Omit) => ( + return (props: IndexPatternSelectProps) => ( ); } 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 2912ec401b8b6..f0db37eb963fd 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; -import type { IndexPatternSelectProps } from './index_pattern_select'; +import type { IndexPatternSelectInternalProps } from './index_pattern_select'; const Fallback = () => ( @@ -28,7 +28,7 @@ const Fallback = () => ( ); const LazyIndexPatternSelect = React.lazy(() => import('./index_pattern_select')); -export const IndexPatternSelect = (props: IndexPatternSelectProps) => ( +export const IndexPatternSelect = (props: IndexPatternSelectInternalProps) => ( }> diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 8de1e2c16f159..1e0e8934778ad 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -27,12 +27,19 @@ import { SavedObjectsClientContract, SimpleSavedObject } from 'src/core/public'; import { getTitle } from '../../../common/index_patterns/lib'; export type IndexPatternSelectProps = Required< - Omit, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>, - 'onChange' | 'placeholder' + Omit< + EuiComboBoxProps, + 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange' + >, + 'placeholder' > & { + onChange: (indexPatternId?: string) => void; indexPatternId: string; fieldTypes?: string[]; onNoIndexPatterns?: () => void; +}; + +export type IndexPatternSelectInternalProps = IndexPatternSelectProps & { savedObjectsClient: SavedObjectsClientContract; }; @@ -60,11 +67,11 @@ const getIndexPatterns = async ( // Needed for React.lazy // eslint-disable-next-line import/no-default-export -export default class IndexPatternSelect extends Component { +export default class IndexPatternSelect extends Component { private isMounted: boolean = false; state: IndexPatternSelectState; - constructor(props: IndexPatternSelectProps) { + constructor(props: IndexPatternSelectInternalProps) { super(props); this.state = { @@ -86,7 +93,7 @@ export default class IndexPatternSelect extends Component Promise; } @@ -47,11 +47,10 @@ export class IndexPatternsService implements Plugin { - const savedObjectsClient = savedObjects.getScopedClient(kibanaRequest); + indexPatternsServiceFactory: async (savedObjectsClient: SavedObjectsClientContract) => { const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 1a9e7d83bc956..6e66f8027207b 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -162,7 +162,9 @@ export class SearchService implements Plugin { asScoped: async (request: KibanaRequest) => { const esClient = elasticsearch.client.asScoped(request); const savedObjectsClient = savedObjects.getScopedClient(request); - const scopedIndexPatterns = await indexPatterns.indexPatternsServiceFactory(request); + const scopedIndexPatterns = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient + ); const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); // cache ui settings, only including items which are explicitly needed by SearchSource diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 45dbdee0f846b..07afad1c96a06 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -23,7 +23,6 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; import { KibanaRequest } from 'src/core/server'; -import { KibanaRequest as KibanaRequest_2 } from 'kibana/server'; import { LegacyAPICaller } from 'kibana/server'; import { Logger } from 'kibana/server'; import { LoggerFactory } from '@kbn/logging'; @@ -42,6 +41,7 @@ import { RequestHandlerContext } from 'src/core/server'; import { RequestStatistics } from 'src/plugins/inspector/common'; import { SavedObject } from 'src/core/server'; import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract as SavedObjectsClientContract_2 } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; @@ -675,7 +675,7 @@ export class IndexPatternsService implements Plugin_3 Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract_2) => Promise; }; } @@ -879,7 +879,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 7609f07d660bc..789353ca4abd7 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -77,6 +77,8 @@ export { EmbeddableRendererProps, } from './lib'; +export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './lib/attribute_service'; + export { EnhancementRegistryDefinition } from './types'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.mock.tsx similarity index 89% rename from src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx rename to src/plugins/embeddable/public/lib/attribute_service/attribute_service.mock.tsx index 09d6f5b4f1e0d..9b08d52ed517c 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx +++ b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.mock.tsx @@ -17,11 +17,11 @@ * under the License. */ -import { EmbeddableInput, SavedObjectEmbeddableInput } from '../embeddable_plugin'; -import { coreMock } from '../../../../core/public/mocks'; +import { EmbeddableInput, SavedObjectEmbeddableInput } from '../index'; +import { coreMock } from '../../../../../core/public/mocks'; import { AttributeServiceOptions } from './attribute_service'; -import { CoreStart } from '../../../../core/public'; -import { AttributeService, ATTRIBUTE_SERVICE_KEY } from '..'; +import { CoreStart } from 'src/core/public'; +import { AttributeService, ATTRIBUTE_SERVICE_KEY } from './index'; export const mockAttributeService = < A extends { title: string }, diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.test.ts similarity index 98% rename from src/plugins/dashboard/public/attribute_service/attribute_service.test.ts rename to src/plugins/embeddable/public/lib/attribute_service/attribute_service.test.ts index d7368b299c411..868501adb9687 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts +++ b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.test.ts @@ -19,8 +19,8 @@ import { ATTRIBUTE_SERVICE_KEY } from './attribute_service'; import { mockAttributeService } from './attribute_service.mock'; -import { coreMock } from '../../../../core/public/mocks'; -import { OnSaveProps } from '../../../saved_objects/public/save_modal'; +import { coreMock } from '../../../../../core/public/mocks'; +import { OnSaveProps } from 'src/plugins/saved_objects/public/save_modal'; interface TestAttributes { title: string; diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx similarity index 95% rename from src/plugins/dashboard/public/attribute_service/attribute_service.tsx rename to src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx index b46226ec4ab02..c4628ab7fbdba 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx +++ b/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx @@ -20,17 +20,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; +import { I18nStart, NotificationsStart } from 'src/core/public'; +import { SavedObjectSaveModal, OnSaveProps, SaveResult } from '../../../../saved_objects/public'; import { EmbeddableInput, SavedObjectEmbeddableInput, isSavedObjectEmbeddableInput, IEmbeddable, Container, - EmbeddableStart, EmbeddableFactoryNotFoundError, -} from '../embeddable_plugin'; -import { I18nStart, NotificationsStart } from '../../../../core/public'; -import { SavedObjectSaveModal, OnSaveProps, SaveResult } from '../../../saved_objects/public'; + EmbeddableFactory, +} from '../index'; /** * The attribute service is a shared, generic service that embeddables can use to provide the functionality @@ -66,7 +66,7 @@ export class AttributeService< private i18nContext: I18nStart['Context'], private toasts: NotificationsStart['toasts'], private options: AttributeServiceOptions, - getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory'] + getEmbeddableFactory?: (embeddableFactoryId: string) => EmbeddableFactory ) { if (getEmbeddableFactory) { const factory = getEmbeddableFactory(this.type); @@ -113,7 +113,7 @@ export class AttributeService< return { ...originalInput } as RefType; } catch (error) { this.toasts.addDanger({ - title: i18n.translate('dashboard.attributeService.saveToLibraryError', { + title: i18n.translate('embeddableApi.attributeService.saveToLibraryError', { defaultMessage: `Panel was not saved to the library. Error: {errorMessage}`, values: { errorMessage: error.message, diff --git a/src/plugins/dashboard/public/attribute_service/index.ts b/src/plugins/embeddable/public/lib/attribute_service/index.ts similarity index 100% rename from src/plugins/dashboard/public/attribute_service/index.ts rename to src/plugins/embeddable/public/lib/attribute_service/index.ts diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index a2da31773696c..137f8c24b1fae 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -20,6 +20,7 @@ import { EuiContextMenuPanelDescriptor, EuiPanel, htmlIdGenerator } from '@elast import classNames from 'classnames'; import React from 'react'; import { Subscription } from 'rxjs'; +import deepEqual from 'fast-deep-equal'; import { buildContextMenuForActions, UiActionsService, Action } from '../ui_actions'; import { CoreStart, OverlayStart } from '../../../../../core/public'; import { toMountPoint } from '../../../../kibana_react/public'; @@ -123,9 +124,11 @@ export class EmbeddablePanel extends React.Component { badges = badges.filter((badge) => disabledActions.indexOf(badge.id) === -1); } - this.setState({ - badges, - }); + if (!deepEqual(this.state.badges, badges)) { + this.setState({ + badges, + }); + } } private async refreshNotifications() { @@ -139,9 +142,11 @@ export class EmbeddablePanel extends React.Component { notifications = notifications.filter((badge) => disabledActions.indexOf(badge.id) === -1); } - this.setState({ - notifications, - }); + if (!deepEqual(this.state.notifications, notifications)) { + this.setState({ + notifications, + }); + } } public UNSAFE_componentWillMount() { diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 26c10121adb3d..62063cb480338 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -39,6 +39,7 @@ import { dataPluginMock } from '../../data/public/mocks'; import { inspectorPluginMock } from '../../inspector/public/mocks'; import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; +export { mockAttributeService } from './lib/attribute_service/attribute_service.mock'; export type Setup = jest.Mocked; export type Start = jest.Mocked; @@ -125,6 +126,7 @@ const createStartContract = (): Start => { EmbeddablePanel: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), + getAttributeService: jest.fn(), }; return startContract; }; diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 00eb923c26662..aa4d66c43c9db 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { Subscription } from 'rxjs'; import { identity } from 'lodash'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; -import { getSavedObjectFinder } from '../../saved_objects/public'; +import { getSavedObjectFinder, showSaveModal } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStart } from '../../inspector/public'; import { @@ -47,6 +47,7 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, + SavedObjectEmbeddableInput, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { EmbeddableStateTransfer } from './lib/state_transfer'; @@ -56,6 +57,8 @@ import { telemetryBaseEmbeddableInput, } from '../common/lib/migrate_base_input'; import { PersistableState, SerializableState } from '../../kibana_utils/common'; +import { ATTRIBUTE_SERVICE_KEY, AttributeService } from './lib/attribute_service'; +import { AttributeServiceOptions } from './lib/attribute_service/attribute_service'; export interface EmbeddableSetupDependencies { data: DataPublicPluginSetup; @@ -93,6 +96,16 @@ export interface EmbeddableStart extends PersistableState { EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; + getAttributeService: < + A extends { title: string }, + V extends EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: A } = EmbeddableInput & { + [ATTRIBUTE_SERVICE_KEY]: A; + }, + R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput + >( + type: string, + options: AttributeServiceOptions + ) => AttributeService; } export type EmbeddablePanelHOC = React.FC<{ embeddable: IEmbeddable; hideHeader?: boolean }>; @@ -178,6 +191,15 @@ export class EmbeddablePublicPlugin implements Plugin + new AttributeService( + type, + showSaveModal, + core.i18n.Context, + core.notifications.toasts, + options, + this.getEmbeddableFactory + ), getStateTransfer: (history?: ScopedHistory) => { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history, this.appList) diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index b01995ccaab08..6280d3a2e4a50 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -31,6 +31,7 @@ import { ExclusiveUnion } from '@elastic/eui'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { History } from 'history'; import { Href } from 'history'; +import { I18nStart as I18nStart_2 } from 'src/core/public'; import { IconType } from '@elastic/eui'; import { ISearchOptions } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; @@ -119,6 +120,42 @@ export class AddPanelAction implements Action_3 { readonly type = "ACTION_ADD_PANEL"; } +// Warning: (ae-missing-release-tag) "ATTRIBUTE_SERVICE_KEY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const ATTRIBUTE_SERVICE_KEY = "attributes"; + +// Warning: (ae-missing-release-tag) "AttributeService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class AttributeService { + // Warning: (ae-forgotten-export) The symbol "AttributeServiceOptions" needs to be exported by the entry point index.d.ts + constructor(type: string, showSaveModal: (saveModal: React.ReactElement, I18nContext: I18nStart_2['Context']) => void, i18nContext: I18nStart_2['Context'], toasts: NotificationsStart_2['toasts'], options: AttributeServiceOptions, getEmbeddableFactory?: (embeddableFactoryId: string) => EmbeddableFactory); + // (undocumented) + getExplicitInputFromEmbeddable(embeddable: IEmbeddable): ValType | RefType; + // (undocumented) + getInputAsRefType: (input: ValType | RefType, saveOptions?: { + showSaveModal: boolean; + saveModalTitle?: string | undefined; + } | { + title: string; + } | undefined) => Promise; + // (undocumented) + getInputAsValueType: (input: ValType | RefType) => Promise; + // (undocumented) + inputIsRefType: (input: ValType | RefType) => input is RefType; + // (undocumented) + unwrapAttributes(input: RefType | ValType): Promise; + // (undocumented) + wrapAttributes(newAttributes: SavedObjectAttributes, useRefType: boolean, input?: ValType | RefType): Promise>; +} + // Warning: (ae-missing-release-tag) "ChartActionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -527,6 +564,14 @@ export interface EmbeddableStart extends PersistableState { // (undocumented) EmbeddablePanel: EmbeddablePanelHOC; // (undocumented) + getAttributeService: (type: string, options: AttributeServiceOptions) => AttributeService; + // (undocumented) getEmbeddableFactories: () => IterableIterator; // (undocumented) getEmbeddableFactory: = IEmbeddable>(embeddableFactoryId: string) => EmbeddableFactory | undefined; diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts index f79c4b7620110..1e93561e4d063 100644 --- a/src/plugins/embeddable/server/plugin.ts +++ b/src/plugins/embeddable/server/plugin.ts @@ -35,6 +35,7 @@ import { SerializableState } from '../../kibana_utils/common'; import { EmbeddableInput } from '../common/types'; export interface EmbeddableSetup { + getAttributeService: any; registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; } diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index c4fad2917343b..d051793382ab7 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -23,6 +23,8 @@ export interface EmbeddableRegistryDefinition

void; // (undocumented) diff --git a/src/plugins/expressions/common/expression_renderers/types.ts b/src/plugins/expressions/common/expression_renderers/types.ts index b760e7b32a7d2..0ea3d72e75609 100644 --- a/src/plugins/expressions/common/expression_renderers/types.ts +++ b/src/plugins/expressions/common/expression_renderers/types.ts @@ -17,6 +17,8 @@ * under the License. */ +import { PersistedState } from 'src/plugins/visualizations/public'; + export interface ExpressionRenderDefinition { /** * Technical name of the renderer, used as ID to identify renderer in @@ -68,4 +70,5 @@ export interface IInterpreterRenderHandlers { reload: () => void; update: (params: any) => void; event: (event: any) => void; + uiState?: PersistedState; } diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 5c0fd8ab1a572..763147df6d922 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -11,6 +11,7 @@ import { EnvironmentMode } from '@kbn/config'; import { EventEmitter } from 'events'; import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; +import { PersistedState } from 'src/plugins/visualizations/public'; import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import React from 'react'; @@ -883,6 +884,8 @@ export interface IInterpreterRenderHandlers { // (undocumented) reload: () => void; // (undocumented) + uiState?: PersistedState; + // (undocumented) update: (params: any) => void; } diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index d8872ee416017..8266789664d44 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -9,6 +9,7 @@ import { CoreStart } from 'src/core/server'; import { Ensure } from '@kbn/utility-types'; import { EventEmitter } from 'events'; import { Observable } from 'rxjs'; +import { PersistedState } from 'src/plugins/visualizations/public'; import { Plugin as Plugin_2 } from 'src/core/server'; import { PluginInitializerContext } from 'src/core/server'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; @@ -717,6 +718,8 @@ export interface IInterpreterRenderHandlers { // (undocumented) reload: () => void; // (undocumented) + uiState?: PersistedState; + // (undocumented) update: (params: any) => void; } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap index 69b192a81d097..38f630358d064 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap @@ -4,6 +4,7 @@ exports[`LabelTemplateFlyout should not render if not visible 1`] = `""`; exports[`LabelTemplateFlyout should render normally 1`] = ` diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap index f862d0ebe8477..13be0353e1640 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap @@ -1,544 +1,431 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UrlFormatEditor should render label template help 1`] = ` - - - - - } - labelType="label" +exports[`UrlFormatEditor should render normally 1`] = ` +

+
- - - + - + Type + + +
+
+
- - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - -`; - -exports[`UrlFormatEditor should render normally 1`] = ` - - - - - } - labelType="label" +
+ +
+ + +
+
+
+
+
- - - + - + Open in a new tab + + +
+
+
- - - } - isInvalid={false} - label={ - - } - labelType="label" + + + + Off + + +
+
+
+
- - - - -`; - -exports[`UrlFormatEditor should render url template help 1`] = ` - - - - - } - labelType="label" - > - - - + - + URL template + + +
+
+
- - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - -`; - -exports[`UrlFormatEditor should render width and height fields if image 1`] = ` - - - - - } - labelType="label" - > - - - + +
+
+
- - - } - isInvalid={false} - label={ - - } - labelType="label" + +
+ + +
- - - + - - } - labelType="label" - > - - - - } - labelType="label" + + Label template + + +
+
+
+
+ +
+
+
+ +
+
+ +
- - - - +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Input + +
+
+
+ + Output + +
+
+
+ Input +
+
+ john +
+
+
+ Output +
+
+
+ converted url for john +
+
+
+
+ Input +
+
+ /some/pathname/asset.png +
+
+
+ Output +
+
+
+ converted url for /some/pathname/asset.png +
+
+
+
+ Input +
+
+ 1234 +
+
+
+ Output +
+
+
+ converted url for 1234 +
+
+
+
+
+
+
+
`; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap index 14e5012e9a554..83e815dd72661 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap @@ -4,6 +4,7 @@ exports[`UrlTemplateFlyout should not render if not visible 1`] = `""`; exports[`UrlTemplateFlyout should render normally 1`] = ` diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx index d04ee58f26b0a..4dd3fb8f1b695 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx @@ -63,7 +63,7 @@ const items: LabelTemplateExampleItem[] = [ export const LabelTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { return isVisible ? ( - +

diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx index a1a1655949432..eb5cac111928f 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx @@ -18,13 +18,22 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; import { FieldFormat } from 'src/plugins/data/public'; - +import { IntlProvider } from 'react-intl'; import { UrlFormatEditor } from './url'; +import { coreMock } from '../../../../../../../../../core/public/mocks'; +import { createKibanaReactContext } from '../../../../../../../../kibana_react/public'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +jest.mock('@elastic/eui/lib/services/accessibility', () => { + return { + htmlIdGenerator: () => () => `generated-id`, + }; +}); const fieldType = 'string'; -const format = { +const format = ({ getConverterFor: jest .fn() .mockImplementation(() => (input: string) => `converted url for ${input}`), @@ -35,78 +44,115 @@ const format = { { kind: 'audio', text: 'Audio' }, ], }, -}; +} as unknown) as FieldFormat; const formatParams = { openLinkInCurrentTab: true, urlTemplate: '', labelTemplate: '', width: '', height: '', + type: 'a', }; + const onChange = jest.fn(); const onError = jest.fn(); +const renderWithContext = (Element: React.ReactElement) => + render( + + {Element} + + ); + +const MY_BASE_PATH = 'my-base-path'; +const KibanaReactContext = createKibanaReactContext( + coreMock.createStart({ basePath: 'my-base-path' }) +); + describe('UrlFormatEditor', () => { it('should have a formatId', () => { expect(UrlFormatEditor.formatId).toEqual('url'); }); it('should render normally', async () => { - const component = shallow( + const { container } = renderWithContext( ); - - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); it('should render url template help', async () => { - const component = shallow( + const { getByText, getByTestId } = renderWithContext( ); - (component.instance() as UrlFormatEditor).showUrlTemplateHelp(); - component.update(); - expect(component).toMatchSnapshot(); + getByText('URL template help'); + userEvent.click(getByText('URL template help')); + expect(getByTestId('urlTemplateFlyoutTestSubj')).toBeVisible(); }); it('should render label template help', async () => { - const component = shallow( + const { getByText, getByTestId } = renderWithContext( ); - (component.instance() as UrlFormatEditor).showLabelTemplateHelp(); - component.update(); - expect(component).toMatchSnapshot(); + getByText('Label template help'); + userEvent.click(getByText('Label template help')); + expect(getByTestId('labelTemplateFlyoutTestSubj')).toBeVisible(); }); it('should render width and height fields if image', async () => { - const component = shallow( + const { getByLabelText } = renderWithContext( ); - expect(component).toMatchSnapshot(); + expect(getByLabelText('Width')).toBeInTheDocument(); + expect(getByLabelText('Height')).toBeInTheDocument(); + }); + + it('should append base path to preview images', async () => { + let sampleImageUrlTemplate = ''; + const { getByLabelText } = renderWithContext( + { + sampleImageUrlTemplate = urlTemplate; + }} + onError={onError} + /> + ); + + // TODO: sample image url emitted only during change event + // So can't just path `type: img` and check rendered value + userEvent.selectOptions(getByLabelText('Type'), 'img'); + expect(sampleImageUrlTemplate).toContain(MY_BASE_PATH); + expect(sampleImageUrlTemplate).toMatchInlineSnapshot( + `"my-base-path/plugins/indexPatternManagement/assets/icons/{{value}}.png"` + ); }); }); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.tsx index 30acf09526f85..95b5fc3955280 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.tsx @@ -36,6 +36,8 @@ import { FormatEditorSamples } from '../../samples'; import { LabelTemplateFlyout } from './label_template_flyout'; import { UrlTemplateFlyout } from './url_template_flyout'; +import type { IndexPatternManagmentContextValue } from '../../../../../../types'; +import { context as contextType } from '../../../../../../../../kibana_react/public'; interface OnChangeParam { type: string; @@ -66,14 +68,21 @@ export class UrlFormatEditor extends DefaultFormatEditor< UrlFormatEditorFormatParams, UrlFormatEditorFormatState > { + static contextType = contextType; static formatId = 'url'; - iconPattern: string; + // TODO: @kbn/optimizer can't compile this + // declare context: IndexPatternManagmentContextValue; + context: IndexPatternManagmentContextValue | undefined; + private get sampleIconPath() { + const sampleIconPath = `/plugins/indexPatternManagement/assets/icons/{{value}}.png`; + return this.context?.services.http + ? this.context.services.http.basePath.prepend(sampleIconPath) + : sampleIconPath; + } constructor(props: FormatEditorProps) { super(props); - this.iconPattern = `/plugins/indexPatternManagement/assets/icons/{{value}}.png`; - this.state = { ...this.state, sampleInputsByType: { @@ -104,9 +113,9 @@ export class UrlFormatEditor extends DefaultFormatEditor< params.width = width; params.height = height; if (!urlTemplate) { - params.urlTemplate = this.iconPattern; + params.urlTemplate = this.sampleIconPath; } - } else if (newType !== 'img' && urlTemplate === this.iconPattern) { + } else if (newType !== 'img' && urlTemplate === this.sampleIconPath) { params.urlTemplate = undefined; } this.onChange(params); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx index c1b144b0d9eac..b1c66874d69cf 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx @@ -26,7 +26,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; export const UrlTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { return isVisible ? ( - +

diff --git a/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx b/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx index 6da5be450beb0..f424b25526b16 100644 --- a/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx +++ b/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx @@ -49,8 +49,6 @@ function IndexPatternSelectFormRowUi(props: IndexPatternSelectFormRowUiProps) { indexPatternId={indexPatternId} onChange={onChange} data-test-subj={selectId} - // TODO: supply actual savedObjectsClient here - savedObjectsClient={{} as any} /> ); diff --git a/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap b/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap index dc6571de969f0..a32609c2e3d34 100644 --- a/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap +++ b/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap @@ -2,12 +2,9 @@ exports[`interpreter/functions#table returns an object with the correct structure 1`] = ` Object { - "as": "visualization", + "as": "table_vis", "type": "render", "value": Object { - "params": Object { - "listenOnChange": true, - }, "visConfig": Object { "dimensions": Object { "buckets": Array [], diff --git a/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 0000000000000..d2298e6fb3eb5 --- /dev/null +++ b/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`table vis toExpressionAst function should match snapshot based on params & dimensions 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[]", + ], + "includeFormatHints": Array [ + false, + ], + "index": Array [ + "123", + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + true, + ], + }, + "function": "esaggs", + "type": "function", + }, + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"perPage\\":20,\\"percentageCol\\":\\"Count\\",\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"showTotal\\":true,\\"sort\\":{\\"columnIndex\\":null,\\"direction\\":null},\\"totalFunc\\":\\"sum\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"number\\"},\\"params\\":{},\\"label\\":\\"Count\\",\\"aggType\\":\\"count\\"}],\\"buckets\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"YYYY-MM-DD HH:mm\\"}},\\"params\\":{},\\"label\\":\\"order_date per 3 hours\\",\\"aggType\\":\\"date_histogram\\"}]}}", + ], + }, + "function": "kibana_table", + "type": "function", + }, + ], + "type": "expression", +} +`; + +exports[`table vis toExpressionAst function should match snapshot without params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[]", + ], + "includeFormatHints": Array [ + false, + ], + "index": Array [ + "123", + ], + "metricsAtAllLevels": Array [ + false, + ], + }, + "function": "esaggs", + "type": "function", + }, + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"showLabel\\":false,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"number\\"},\\"params\\":{},\\"label\\":\\"Count\\",\\"aggType\\":\\"count\\"}],\\"buckets\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"YYYY-MM-DD HH:mm\\"}},\\"params\\":{},\\"label\\":\\"order_date per 3 hours\\",\\"aggType\\":\\"date_histogram\\"}]}}", + ], + }, + "function": "kibana_table", + "type": "function", + }, + ], + "type": "expression", +} +`; diff --git a/src/plugins/vis_type_table/public/index.ts b/src/plugins/vis_type_table/public/index.ts index 5621fdb094772..6493c967165db 100644 --- a/src/plugins/vis_type_table/public/index.ts +++ b/src/plugins/vis_type_table/public/index.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import './index.scss'; import { PluginInitializerContext } from 'kibana/public'; import { TableVisPlugin as Plugin } from './plugin'; diff --git a/src/plugins/vis_type_table/public/_table_vis.scss b/src/plugins/vis_type_table/public/legacy/_table_vis.scss similarity index 91% rename from src/plugins/vis_type_table/public/_table_vis.scss rename to src/plugins/vis_type_table/public/legacy/_table_vis.scss index 8a36b9818c2a3..fa12ef9a1cf39 100644 --- a/src/plugins/vis_type_table/public/_table_vis.scss +++ b/src/plugins/vis_type_table/public/legacy/_table_vis.scss @@ -4,8 +4,10 @@ .table-vis { display: flex; flex-direction: column; - flex: 1 0 100%; + flex: 1 1 0; overflow: auto; + + @include euiScrollBar; } .table-vis-container { diff --git a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss b/src/plugins/vis_type_table/public/legacy/agg_table/_agg_table.scss similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/_agg_table.scss rename to src/plugins/vis_type_table/public/legacy/agg_table/_agg_table.scss diff --git a/src/plugins/vis_type_table/public/agg_table/_index.scss b/src/plugins/vis_type_table/public/legacy/agg_table/_index.scss similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/_index.scss rename to src/plugins/vis_type_table/public/legacy/agg_table/_index.scss diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.html b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.html similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/agg_table.html rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table.html diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js similarity index 99% rename from src/plugins/vis_type_table/public/agg_table/agg_table.js rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js index 1e98a06c2a6a9..a9ec431e9d940 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js @@ -17,9 +17,9 @@ * under the License. */ import _ from 'lodash'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../share/public'; import aggTableTemplate from './agg_table.html'; -import { getFormatService } from '../services'; +import { getFormatService } from '../../services'; import { i18n } from '@kbn/i18n'; export function KbnAggTable(config, RecursionHelper) { diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js similarity index 97% rename from src/plugins/vis_type_table/public/agg_table/agg_table.test.js rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js index 29a10151a9418..c93fb4f8bd568 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.test.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js @@ -24,14 +24,14 @@ import 'angular-mocks'; import sinon from 'sinon'; import { round } from 'lodash'; -import { getFieldFormatsRegistry } from '../../../data/public/test_utils'; -import { coreMock } from '../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../kibana_legacy/public'; -import { setUiSettings } from '../../../data/public/services'; -import { UI_SETTINGS } from '../../../data/public/'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; - -import { setFormatService } from '../services'; +import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; +import { coreMock } from '../../../../../core/public/mocks'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public'; +import { setUiSettings } from '../../../../data/public/services'; +import { UI_SETTINGS } from '../../../../data/public/'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../share/public'; + +import { setFormatService } from '../../services'; import { getInnerAngular } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; import { tabifiedData } from './tabified_data'; diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.html b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.html similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/agg_table_group.html rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.html diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.js similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/agg_table_group.js rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.js diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js similarity index 92% rename from src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js rename to src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js index 04cf624331d81..833f51b446ac1 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js @@ -22,11 +22,11 @@ import angular from 'angular'; import 'angular-mocks'; import expect from '@kbn/expect'; -import { getFieldFormatsRegistry } from '../../../data/public/test_utils'; -import { coreMock } from '../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../kibana_legacy/public'; -import { setUiSettings } from '../../../data/public/services'; -import { setFormatService } from '../services'; +import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; +import { coreMock } from '../../../../../core/public/mocks'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public'; +import { setUiSettings } from '../../../../data/public/services'; +import { setFormatService } from '../../services'; import { getInnerAngular } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; import { tabifiedData } from './tabified_data'; diff --git a/src/plugins/vis_type_table/public/agg_table/tabified_data.js b/src/plugins/vis_type_table/public/legacy/agg_table/tabified_data.js similarity index 100% rename from src/plugins/vis_type_table/public/agg_table/tabified_data.js rename to src/plugins/vis_type_table/public/legacy/agg_table/tabified_data.js diff --git a/src/plugins/vis_type_table/public/get_inner_angular.ts b/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts similarity index 98% rename from src/plugins/vis_type_table/public/get_inner_angular.ts rename to src/plugins/vis_type_table/public/legacy/get_inner_angular.ts index 4e4269a1f44f4..f3d88a2a217b3 100644 --- a/src/plugins/vis_type_table/public/get_inner_angular.ts +++ b/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts @@ -33,7 +33,7 @@ import { PrivateProvider, watchMultiDecorator, KbnAccessibleClickProvider, -} from '../../kibana_legacy/public'; +} from '../../../kibana_legacy/public'; initAngularBootstrap(); diff --git a/src/plugins/vis_type_table/public/index.scss b/src/plugins/vis_type_table/public/legacy/index.scss similarity index 100% rename from src/plugins/vis_type_table/public/index.scss rename to src/plugins/vis_type_table/public/legacy/index.scss diff --git a/src/plugins/vis_type_table/public/paginated_table/_index.scss b/src/plugins/vis_type_table/public/legacy/paginated_table/_index.scss similarity index 100% rename from src/plugins/vis_type_table/public/paginated_table/_index.scss rename to src/plugins/vis_type_table/public/legacy/paginated_table/_index.scss diff --git a/src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss b/src/plugins/vis_type_table/public/legacy/paginated_table/_table_cell_filter.scss similarity index 100% rename from src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss rename to src/plugins/vis_type_table/public/legacy/paginated_table/_table_cell_filter.scss diff --git a/src/plugins/vis_type_table/public/paginated_table/paginated_table.html b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.html similarity index 100% rename from src/plugins/vis_type_table/public/paginated_table/paginated_table.html rename to src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.html diff --git a/src/plugins/vis_type_table/public/paginated_table/paginated_table.js b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.js similarity index 100% rename from src/plugins/vis_type_table/public/paginated_table/paginated_table.js rename to src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.js diff --git a/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts similarity index 98% rename from src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts rename to src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts index de253f26ff9e7..3bc5f79557041 100644 --- a/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts +++ b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts @@ -25,11 +25,7 @@ import 'angular-mocks'; import { getAngularModule } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { coreMock } from '../../../../core/public/mocks'; - -jest.mock('../../../kibana_legacy/public/angular/angular_config', () => ({ - configureAppAngularModule: () => {}, -})); +import { coreMock } from '../../../../../core/public/mocks'; interface Sort { columnIndex: number; diff --git a/src/plugins/vis_type_table/public/paginated_table/rows.js b/src/plugins/vis_type_table/public/legacy/paginated_table/rows.js similarity index 91% rename from src/plugins/vis_type_table/public/paginated_table/rows.js rename to src/plugins/vis_type_table/public/legacy/paginated_table/rows.js index d8f01a10c63fa..54e754adcc170 100644 --- a/src/plugins/vis_type_table/public/paginated_table/rows.js +++ b/src/plugins/vis_type_table/public/legacy/paginated_table/rows.js @@ -44,15 +44,18 @@ export function KbnRows($compile) { } $scope.filter({ - data: [ - { - table: $scope.table, - row: $scope.rows.findIndex((r) => r === row), - column: $scope.table.columns.findIndex((c) => c.id === column.id), - value, - }, - ], - negate, + name: 'filterBucket', + data: { + data: [ + { + table: $scope.table, + row: $scope.rows.findIndex((r) => r === row), + column: $scope.table.columns.findIndex((c) => c.id === column.id), + value, + }, + ], + negate, + }, }); }; diff --git a/src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html b/src/plugins/vis_type_table/public/legacy/paginated_table/table_cell_filter.html similarity index 100% rename from src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html rename to src/plugins/vis_type_table/public/legacy/paginated_table/table_cell_filter.html diff --git a/src/plugins/vis_type_table/public/table_vis.html b/src/plugins/vis_type_table/public/legacy/table_vis.html similarity index 96% rename from src/plugins/vis_type_table/public/table_vis.html rename to src/plugins/vis_type_table/public/legacy/table_vis.html index f721b670400d6..c469cd250755c 100644 --- a/src/plugins/vis_type_table/public/table_vis.html +++ b/src/plugins/vis_type_table/public/legacy/table_vis.html @@ -15,7 +15,7 @@
({ - configureAppAngularModule: () => {}, -})); - interface TableVisScope extends IScope { [key: string]: any; } @@ -112,10 +107,6 @@ describe('Table Vis - Controller', () => { coreMock.createSetup() ); }); - const tableVisTypeDefinition = getTableVisTypeDefinition( - coreMock.createSetup(), - coreMock.createPluginInitializerContext() - ); function getRangeVis(params?: object) { return ({ diff --git a/src/plugins/vis_type_table/public/table_vis_legacy_module.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_module.ts similarity index 100% rename from src/plugins/vis_type_table/public/table_vis_legacy_module.ts rename to src/plugins/vis_type_table/public/legacy/table_vis_legacy_module.ts diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_renderer.tsx b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_renderer.tsx new file mode 100644 index 0000000000000..ab633bd5137ba --- /dev/null +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_renderer.tsx @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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, PluginInitializerContext } from 'kibana/public'; +import { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { TablePluginStartDependencies } from '../plugin'; +import { TableVisRenderValue } from '../table_vis_fn'; +import { TableVisLegacyController } from './vis_controller'; + +const tableVisRegistry = new Map(); + +export const getTableVisLegacyRenderer: ( + core: CoreSetup, + context: PluginInitializerContext +) => ExpressionRenderDefinition = (core, context) => ({ + name: 'table_vis', + reuseDomNode: true, + render: async (domNode, config, handlers) => { + let registeredController = tableVisRegistry.get(domNode); + + if (!registeredController) { + const { getTableVisualizationControllerClass } = await import('./vis_controller'); + + const Controller = getTableVisualizationControllerClass(core, context); + registeredController = new Controller(domNode); + tableVisRegistry.set(domNode, registeredController); + + handlers.onDestroy(() => { + registeredController?.destroy(); + tableVisRegistry.delete(domNode); + }); + } + + await registeredController.render(config.visData, config.visConfig, handlers); + handlers.done(); + }, +}); diff --git a/src/plugins/vis_type_table/public/vis_controller.ts b/src/plugins/vis_type_table/public/legacy/vis_controller.ts similarity index 68% rename from src/plugins/vis_type_table/public/vis_controller.ts rename to src/plugins/vis_type_table/public/legacy/vis_controller.ts index 1781808660260..eff8e34f3e778 100644 --- a/src/plugins/vis_type_table/public/vis_controller.ts +++ b/src/plugins/vis_type_table/public/legacy/vis_controller.ts @@ -20,35 +20,43 @@ import { CoreSetup, PluginInitializerContext } from 'kibana/public'; import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; import $ from 'jquery'; -import { VisParams, ExprVis } from '../../visualizations/public'; +import './index.scss'; + +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { getAngularModule } from './get_inner_angular'; -import { getKibanaLegacy } from './services'; import { initTableVisLegacyModule } from './table_vis_legacy_module'; +// @ts-ignore +import tableVisTemplate from './table_vis.html'; +import { TablePluginStartDependencies } from '../plugin'; +import { TableVisConfig } from '../types'; +import { TableContext } from '../table_vis_response_handler'; const innerAngularName = 'kibana/table_vis'; +export type TableVisLegacyController = InstanceType< + ReturnType +>; + export function getTableVisualizationControllerClass( - core: CoreSetup, + core: CoreSetup, context: PluginInitializerContext ) { return class TableVisualizationController { private tableVisModule: IModule | undefined; private injector: auto.IInjectorService | undefined; el: JQuery; - vis: ExprVis; $rootScope: IRootScopeService | null = null; $scope: (IScope & { [key: string]: any }) | undefined; $compile: ICompileService | undefined; - constructor(domeElement: Element, vis: ExprVis) { + constructor(domeElement: Element) { this.el = $(domeElement); - this.vis = vis; } getInjector() { if (!this.injector) { const mountpoint = document.createElement('div'); - mountpoint.setAttribute('style', 'height: 100%; width: 100%;'); + mountpoint.className = 'visualization'; this.injector = angular.bootstrap(mountpoint, [innerAngularName]); this.el.append(mountpoint); } @@ -58,14 +66,18 @@ export function getTableVisualizationControllerClass( async initLocalAngular() { if (!this.tableVisModule) { - const [coreStart] = await core.getStartServices(); + const [coreStart, { kibanaLegacy }] = await core.getStartServices(); this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); initTableVisLegacyModule(this.tableVisModule); + kibanaLegacy.loadFontAwesome(); } } - async render(esResponse: object, visParams: VisParams): Promise { - getKibanaLegacy().loadFontAwesome(); + async render( + esResponse: TableContext, + visParams: TableVisConfig, + handlers: IInterpreterRenderHandlers + ): Promise { await this.initLocalAngular(); return new Promise(async (resolve, reject) => { @@ -79,16 +91,6 @@ export function getTableVisualizationControllerClass( return; } - // How things get into this $scope? - // To inject variables into this $scope there's the following pipeline of stuff to check: - // - visualize_embeddable => that's what the editor creates to wrap this Angular component - // - build_pipeline => it serialize all the params into an Angular template compiled on the fly - // - table_vis_fn => unserialize the params and prepare them for the final React/Angular bridge - // - visualization_renderer => creates the wrapper component for this controller and passes the params - // - // In case some prop is missing check into the top of the chain if they are available and check - // the list above that it is passing through - this.$scope.vis = this.vis; this.$scope.visState = { params: visParams, title: visParams.title }; this.$scope.esResponse = esResponse; @@ -101,11 +103,10 @@ export function getTableVisualizationControllerClass( if (!this.$scope && this.$compile) { this.$scope = this.$rootScope.$new(); - this.$scope.uiState = this.vis.getUiState(); + this.$scope.uiState = handlers.uiState; + this.$scope.filter = handlers.event; updateScope(); - this.el - .find('div') - .append(this.$compile(this.vis.type.visConfig?.template ?? '')(this.$scope)); + this.el.find('div').append(this.$compile(tableVisTemplate)(this.$scope)); this.$scope.$apply(); } else { updateScope(); diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts index 28f823df79d91..35ef5fc831cb7 100644 --- a/src/plugins/vis_type_table/public/plugin.ts +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -21,10 +21,11 @@ import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { createTableVisFn } from './table_vis_fn'; -import { getTableVisTypeDefinition } from './table_vis_type'; +import { tableVisTypeDefinition } from './table_vis_type'; import { DataPublicPluginStart } from '../../data/public'; -import { setFormatService, setKibanaLegacy } from './services'; +import { setFormatService } from './services'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; +import { getTableVisLegacyRenderer } from './legacy/table_vis_legacy_renderer'; /** @internal */ export interface TablePluginSetupDependencies { @@ -39,7 +40,9 @@ export interface TablePluginStartDependencies { } /** @internal */ -export class TableVisPlugin implements Plugin, void> { +export class TableVisPlugin + implements + Plugin, void, TablePluginSetupDependencies, TablePluginStartDependencies> { initializerContext: PluginInitializerContext; createBaseVisualization: any; @@ -48,17 +51,15 @@ export class TableVisPlugin implements Plugin, void> { } public async setup( - core: CoreSetup, + core: CoreSetup, { expressions, visualizations }: TablePluginSetupDependencies ) { expressions.registerFunction(createTableVisFn); - visualizations.createBaseVisualization( - getTableVisTypeDefinition(core, this.initializerContext) - ); + expressions.registerRenderer(getTableVisLegacyRenderer(core, this.initializerContext)); + visualizations.createBaseVisualization(tableVisTypeDefinition); } - public start(core: CoreStart, { data, kibanaLegacy }: TablePluginStartDependencies) { + public start(core: CoreStart, { data }: TablePluginStartDependencies) { setFormatService(data.fieldFormats); - setKibanaLegacy(kibanaLegacy); } } diff --git a/src/plugins/vis_type_table/public/services.ts b/src/plugins/vis_type_table/public/services.ts index b4f996f078f6b..3aaffe75e27f1 100644 --- a/src/plugins/vis_type_table/public/services.ts +++ b/src/plugins/vis_type_table/public/services.ts @@ -19,12 +19,7 @@ import { createGetterSetter } from '../../kibana_utils/public'; import { DataPublicPluginStart } from '../../data/public'; -import { KibanaLegacyStart } from '../../kibana_legacy/public'; export const [getFormatService, setFormatService] = createGetterSetter< DataPublicPluginStart['fieldFormats'] >('table data.fieldFormats'); - -export const [getKibanaLegacy, setKibanaLegacy] = createGetterSetter( - 'table kibanaLegacy' -); 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 9739a7a284e6c..2e446ba4e4fcf 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { TableVisConfig } from './types'; export type Input = KibanaDatatable; @@ -27,23 +28,20 @@ interface Arguments { visConfig: string | null; } -type VisParams = Required; - -interface RenderValue { +export interface TableVisRenderValue { visData: TableContext; visType: 'table'; - visConfig: VisParams; - params: { - listenOnChange: boolean; - }; + visConfig: TableVisConfig; } -export const createTableVisFn = (): ExpressionFunctionDefinition< +export type TableExpressionFunctionDefinition = ExpressionFunctionDefinition< 'kibana_table', Input, Arguments, - Render -> => ({ + Render +>; + +export const createTableVisFn = (): TableExpressionFunctionDefinition => ({ name: 'kibana_table', type: 'render', inputTypes: ['kibana_datatable'], @@ -63,14 +61,11 @@ export const createTableVisFn = (): ExpressionFunctionDefinition< return { type: 'render', - as: 'visualization', + as: 'table_vis', value: { visData: convertedData, visType: 'table', visConfig, - params: { - listenOnChange: true, - }, }, }; }, diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index 95f4f06ee6111..bfc7abac02895 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -16,91 +16,82 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup, PluginInitializerContext } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; import { BaseVisTypeOptions } from '../../visualizations/public'; -import { tableVisResponseHandler } from './table_vis_response_handler'; -// @ts-ignore -import tableVisTemplate from './table_vis.html'; + import { TableOptions } from './components/table_vis_options_lazy'; -import { getTableVisualizationControllerClass } from './vis_controller'; import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; +import { TableVisParams } from './types'; -export function getTableVisTypeDefinition( - core: CoreSetup, - context: PluginInitializerContext -): BaseVisTypeOptions { - return { - name: 'table', - title: i18n.translate('visTypeTable.tableVisTitle', { - defaultMessage: 'Data Table', - }), - icon: 'visTable', - description: i18n.translate('visTypeTable.tableVisDescription', { - defaultMessage: 'Display values in a table', - }), - visualization: getTableVisualizationControllerClass(core, context), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter]; - }, - visConfig: { - defaults: { - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { - columnIndex: null, - direction: null, - }, - showTotal: false, - totalFunc: 'sum', - percentageCol: '', +export const tableVisTypeDefinition: BaseVisTypeOptions = { + name: 'table', + title: i18n.translate('visTypeTable.tableVisTitle', { + defaultMessage: 'Data Table', + }), + icon: 'visTable', + description: i18n.translate('visTypeTable.tableVisDescription', { + defaultMessage: 'Display values in a table', + }), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, + visConfig: { + defaults: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, }, - template: tableVisTemplate, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', }, - editorConfig: { - optionsTemplate: TableOptions, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { - defaultMessage: 'Metric', - }), - aggFilter: ['!geo_centroid', '!geo_bounds'], - aggSettings: { - top_hits: { - allowStrings: true, - }, + }, + editorConfig: { + optionsTemplate: TableOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { + defaultMessage: 'Metric', + }), + aggFilter: ['!geo_centroid', '!geo_bounds'], + aggSettings: { + top_hits: { + allowStrings: true, }, - min: 1, - defaults: [{ type: 'count', schema: 'metric' }], - }, - { - group: AggGroupNames.Buckets, - name: 'bucket', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { - defaultMessage: 'Split rows', - }), - aggFilter: ['!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { - defaultMessage: 'Split table', - }), - min: 0, - max: 1, - aggFilter: ['!filter'], }, - ]), - }, - responseHandler: tableVisResponseHandler, - hierarchicalData: (vis) => { - return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); - }, - }; -} + min: 1, + defaults: [{ type: 'count', schema: 'metric' }], + }, + { + group: AggGroupNames.Buckets, + name: 'bucket', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { + defaultMessage: 'Split rows', + }), + aggFilter: ['!filter'], + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { + defaultMessage: 'Split table', + }), + min: 0, + max: 1, + aggFilter: ['!filter'], + }, + ]), + }, + toExpressionAst, + hierarchicalData: (vis) => { + return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); + }, +}; diff --git a/src/plugins/vis_type_table/public/to_ast.test.ts b/src/plugins/vis_type_table/public/to_ast.test.ts new file mode 100644 index 0000000000000..045b5814944b0 --- /dev/null +++ b/src/plugins/vis_type_table/public/to_ast.test.ts @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Vis } from 'src/plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; +import { AggTypes, TableVisParams } from './types'; + +const mockSchemas = { + metric: [{ accessor: 1, format: { id: 'number' }, params: {}, label: 'Count', aggType: 'count' }], + bucket: [ + { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: {}, + label: 'order_date per 3 hours', + aggType: 'date_histogram', + }, + ], +}; + +jest.mock('../../visualizations/public', () => ({ + getVisSchemas: () => mockSchemas, +})); + +describe('table vis toExpressionAst function', () => { + let vis: Vis; + + beforeEach(() => { + vis = { + isHierarchical: () => false, + type: {}, + params: { + showLabel: false, + }, + data: { + indexPattern: { id: '123' }, + aggs: { + getResponseAggs: () => [], + aggs: [], + }, + }, + } as any; + }); + + it('should match snapshot without params', () => { + const actual = toExpressionAst(vis, {} as any); + expect(actual).toMatchSnapshot(); + }); + + it('should match snapshot based on params & dimensions', () => { + vis.params = { + perPage: 20, + percentageCol: 'Count', + showMetricsAtAllLevels: true, + showPartialRows: true, + showTotal: true, + sort: { columnIndex: null, direction: null }, + totalFunc: AggTypes.SUM, + }; + const actual = toExpressionAst(vis, {} as any); + expect(actual).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_type_table/public/to_ast.ts b/src/plugins/vis_type_table/public/to_ast.ts new file mode 100644 index 0000000000000..449e2dde7f7c9 --- /dev/null +++ b/src/plugins/vis_type_table/public/to_ast.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EsaggsExpressionFunctionDefinition } from '../../data/common/search/expressions'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { getVisSchemas, Vis, BuildPipelineParams } from '../../visualizations/public'; +import { TableExpressionFunctionDefinition } from './table_vis_fn'; +import { TableVisConfig, TableVisParams } from './types'; + +const buildTableVisConfig = ( + schemas: ReturnType, + visParams: TableVisParams +) => { + const visConfig = {} as any; + const metrics = schemas.metric; + const buckets = schemas.bucket || []; + visConfig.dimensions = { + metrics, + buckets, + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }; + + if (visParams.showPartialRows && !visParams.showMetricsAtAllLevels) { + // Handle case where user wants to see partial rows but not metrics at all levels. + // This requires calculating how many metrics will come back in the tabified response, + // and removing all metrics from the dimensions except the last set. + const metricsPerBucket = metrics.length / buckets.length; + visConfig.dimensions.metrics.splice(0, metricsPerBucket * buckets.length - metricsPerBucket); + } + return visConfig; +}; + +export const toExpressionAst = (vis: Vis, params: BuildPipelineParams) => { + const esaggs = buildExpressionFunction('esaggs', { + index: vis.data.indexPattern!.id!, + metricsAtAllLevels: vis.isHierarchical(), + partialRows: vis.params.showPartialRows, + aggConfigs: JSON.stringify(vis.data.aggs!.aggs), + includeFormatHints: false, + }); + + const schemas = getVisSchemas(vis, params); + + const visConfig: TableVisConfig = { + ...vis.params, + ...buildTableVisConfig(schemas, vis.params), + title: vis.title, + }; + + const table = buildExpressionFunction('kibana_table', { + visConfig: JSON.stringify(visConfig), + }); + + const ast = buildExpression([esaggs, table]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index 39023d1305cb6..c0a995ad5da69 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -33,7 +33,6 @@ export interface Dimensions { } export interface TableVisParams { - type: 'table'; perPage: number | ''; showPartialRows: boolean; showMetricsAtAllLevels: boolean; @@ -44,5 +43,9 @@ export interface TableVisParams { showTotal: boolean; totalFunc: AggTypes; percentageCol: string; +} + +export interface TableVisConfig extends TableVisParams { + title: string; dimensions: Dimensions; } diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index 0950938423134..40f776050617e 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -165,6 +165,7 @@ export const seriesItems = schema.object({ hide_in_legend: numberIntegerOptional, hidden: schema.maybe(schema.boolean()), id: stringRequired, + ignore_global_filter: numberOptional, label: stringOptionalNullable, line_width: numberOptionalOrEmptyString, metrics: schema.arrayOf(metricsItems), diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config.js b/src/plugins/vis_type_timeseries/public/application/components/series_config.js index c3e18ec202c0f..a219d7e8ee174 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/series_config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/series_config.js @@ -36,8 +36,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getDefaultQueryLanguage } from './lib/get_default_query_language'; -import { QueryBarWrapper } from './query_bar_wrapper'; +import { SeriesConfigQueryBarWithIgnoreGlobalFilter } from './series_config_query_bar_with_ignore_global_filter'; export const SeriesConfig = (props) => { const defaults = { offset_time: '', value_template: '' }; @@ -56,28 +55,12 @@ export const SeriesConfig = (props) => { - - } - fullWidth - > - props.onChange({ filter })} - indexPatterns={[seriesIndexPattern]} - /> - + @@ -162,6 +145,7 @@ export const SeriesConfig = (props) => { SeriesConfig.propTypes = { fields: PropTypes.object, + panel: PropTypes.object, model: PropTypes.object, onChange: PropTypes.func, indexPatternForQuery: PropTypes.string, diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js b/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js new file mode 100644 index 0000000000000..b1275335522e6 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 PropTypes from 'prop-types'; +import React from 'react'; +import { htmlIdGenerator, EuiFlexItem, EuiFormRow, EuiToolTip, EuiFlexGroup } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { YesNo } from './yes_no'; +import { getDefaultQueryLanguage } from './lib/get_default_query_language'; +import { QueryBarWrapper } from './query_bar_wrapper'; + +export function SeriesConfigQueryBarWithIgnoreGlobalFilter({ + panel, + model, + onChange, + indexPatternForQuery, +}) { + const htmlId = htmlIdGenerator(); + const yesNoOption = ( + + ); + return ( + + + + } + fullWidth + > + onChange({ filter })} + indexPatterns={[indexPatternForQuery]} + /> + + + + + } + fullWidth + > + {panel.ignore_global_filter ? ( + + } + > + {yesNoOption} + + ) : ( + yesNoOption + )} + + + + ); +} + +SeriesConfigQueryBarWithIgnoreGlobalFilter.propTypes = { + onChange: PropTypes.func, + model: PropTypes.object, + panel: PropTypes.object, + indexPatternForQuery: PropTypes.string, +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js index 851e900b5a622..7550f48386378 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js @@ -90,6 +90,7 @@ function GaugeSeriesUi(props) { seriesBody = ( - - - } - fullWidth - > - props.onChange({ filter })} - indexPatterns={[seriesIndexPattern]} - /> - - + {type} @@ -584,6 +565,7 @@ export const TimeseriesConfig = injectI18n(function (props) { TimeseriesConfig.propTypes = { fields: PropTypes.object, model: PropTypes.object, + panel: PropTypes.object, onChange: PropTypes.func, indexPatternForQuery: PropTypes.string, seriesQuantity: PropTypes.object, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js index 06ad7e14bf358..680c1c5e78ad4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js @@ -97,6 +97,7 @@ const TimeseriesSeriesUI = injectI18n(function (props) { seriesBody = ( { }, }); }); + + test('returns doc with panel filter (ignoring globals from series)', () => { + req.payload.filters = [ + { + bool: { + must: [ + { + term: { + host: 'example', + }, + }, + ], + }, + }, + ]; + panel.filter = { query: 'host:web-server', language: 'lucene' }; + series.ignore_global_filter = true; + const next = (doc) => doc; + const doc = query(req, panel, series, config)(next)({}); + expect(doc).toEqual({ + size: 0, + query: { + bool: { + filter: [], + must: [ + { + range: { + timestamp: { + gte: '2017-01-01T00:00:00.000Z', + lte: '2017-01-01T01:00:00.000Z', + format: 'strict_date_optional_time', + }, + }, + }, + { + bool: { + filter: [], + must: [ + { + query_string: { + query: panel.filter.query, + analyze_wildcard: true, + }, + }, + ], + must_not: [], + should: [], + }, + }, + ], + must_not: [], + should: [], + }, + }, + }); + }); }); diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index bf36bb35d0563..688987b1104a1 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "inspector", "dashboard"], + "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "inspector" ], "optionalPlugins": ["usageCollection"], "requiredBundles": ["kibanaUtils", "discover", "savedObjects"] } diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index b27d24d980e8d..36f3f7d6ed22e 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -25,7 +25,11 @@ import { VisualizeByReferenceInput, VisualizeSavedObjectAttributes, } from './visualize_embeddable'; -import { IContainer, ErrorEmbeddable } from '../../../../plugins/embeddable/public'; +import { + IContainer, + ErrorEmbeddable, + AttributeService, +} from '../../../../plugins/embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { getSavedVisualizationsLoader, @@ -37,7 +41,6 @@ import { import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; import { SavedVisualizationsLoader } from '../saved_visualizations'; -import { AttributeService } from '../../../dashboard/public'; export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDeps) => async ( vis: Vis, diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index fe8a9adff4052..a810b4b65528f 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -38,6 +38,7 @@ import { Adapters, SavedObjectEmbeddableInput, ReferenceOrValueEmbeddable, + AttributeService, } from '../../../../plugins/embeddable/public'; import { IExpressionLoaderParams, @@ -51,7 +52,6 @@ import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { TriggerId } from '../../../ui_actions/public'; import { SavedObjectAttributes } from '../../../../core/types'; -import { AttributeService } from '../../../dashboard/public'; import { SavedVisualizationsLoader } from '../saved_visualizations'; import { VisSavedObject } from '../types'; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index 87f78f5639ff0..4b851da6be70e 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -26,6 +26,7 @@ import { EmbeddableOutput, ErrorEmbeddable, IContainer, + AttributeService, } from '../../../embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { @@ -50,7 +51,6 @@ import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_obje import { StartServicesGetter } from '../../../kibana_utils/public'; import { VisualizationsStartDeps } from '../plugin'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; -import { AttributeService } from '../../../dashboard/public'; import { checkForDuplicateTitle } from '../../../saved_objects/public'; interface VisualizationAttributes extends SavedObjectAttributes { @@ -126,7 +126,7 @@ export class VisualizeEmbeddableFactory if (!this.attributeService) { this.attributeService = await this.deps .start() - .plugins.dashboard.getAttributeService< + .plugins.embeddable.getAttributeService< VisualizeSavedObjectAttributes, VisualizeByValueInput, VisualizeByReferenceInput diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index c0c37e2262f9c..cbdecd4aac747 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -12,16 +12,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=false 1`] = `"kibana_table visConfig='{\\"showMetricsAtAllLevels\\":false,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":4,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=true 1`] = `"kibana_table visConfig='{\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":2,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":4,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[],\\"splitRow\\":[1,2]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits and buckets 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[3],\\"splitRow\\":[2,4]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function without splits or buckets 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[]}}' "`; - exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"geohash\\":1,\\"geocentroid\\":3}}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index a1fea45f51781..c744043ed155b 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -117,84 +117,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { expect(actual).toMatchSnapshot(); }); - describe('handles table function', () => { - it('without splits or buckets', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 0 }, - { ...schemaConfig, accessor: 1 }, - ], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with splits', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - split_row: [1, 2], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with splits and buckets', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 0 }, - { ...schemaConfig, accessor: 1 }, - ], - split_row: [2, 4], - bucket: [3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with showPartialRows=true and showMetricsAtAllLevels=true', () => { - const params = { - showMetricsAtAllLevels: true, - showPartialRows: true, - }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 1 }, - { ...schemaConfig, accessor: 2 }, - { ...schemaConfig, accessor: 4 }, - { ...schemaConfig, accessor: 5 }, - ], - bucket: [0, 3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with showPartialRows=true and showMetricsAtAllLevels=false', () => { - const params = { - showMetricsAtAllLevels: false, - showPartialRows: true, - }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 1 }, - { ...schemaConfig, accessor: 2 }, - { ...schemaConfig, accessor: 4 }, - { ...schemaConfig, accessor: 5 }, - ], - bucket: [0, 3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - }); - describe('handles region_map function', () => { it('without buckets', () => { const params = { metric: {} }; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 9f6a4d5553292..eb431212166a3 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -39,14 +39,14 @@ export interface SchemaConfig { export interface Schemas { metric: SchemaConfig[]; - bucket?: any[]; + bucket?: SchemaConfig[]; geo_centroid?: any[]; group?: any[]; params?: any[]; radius?: any[]; segment?: any[]; - split_column?: any[]; - split_row?: any[]; + split_column?: SchemaConfig[]; + split_row?: SchemaConfig[]; width?: any[]; // catch all for schema name [key: string]: any[] | undefined; @@ -267,13 +267,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { const paramsArray = [paramsJson, uiStateJson].filter((param) => Boolean(param)); return `tsvb ${paramsArray.join(' ')}`; }, - table: (params, schemas) => { - const visConfig = { - ...params, - ...buildVisConfig.table(schemas, params), - }; - return `kibana_table ${prepareJson('visConfig', visConfig)}`; - }, region_map: (params, schemas) => { const visConfig = { ...params, @@ -298,26 +291,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { }; const buildVisConfig: BuildVisConfigFunction = { - table: (schemas, visParams = {}) => { - const visConfig = {} as any; - const metrics = schemas.metric; - const buckets = schemas.bucket || []; - visConfig.dimensions = { - metrics, - buckets, - splitRow: schemas.split_row, - splitColumn: schemas.split_column, - }; - - if (visParams.showMetricsAtAllLevels === false && visParams.showPartialRows === true) { - // Handle case where user wants to see partial rows but not metrics at all levels. - // This requires calculating how many metrics will come back in the tabified response, - // and removing all metrics from the dimensions except the last set. - const metricsPerBucket = metrics.length / buckets.length; - visConfig.dimensions.metrics.splice(0, metricsPerBucket * buckets.length - metricsPerBucket); - } - return visConfig; - }, region_map: (schemas) => { const visConfig = {} as any; visConfig.metric = schemas.metric[0]; diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 37a9972983421..be7629ef41145 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -112,7 +112,7 @@ export interface VisualizationsStartDeps { uiActions: UiActionsStart; application: ApplicationStart; dashboard: DashboardStart; - getAttributeService: DashboardStart['getAttributeService']; + getAttributeService: EmbeddableStart['getAttributeService']; savedObjectsClient: SavedObjectsClientContract; } diff --git a/test/accessibility/services/a11y/analyze_with_axe.js b/test/accessibility/services/a11y/analyze_with_axe.js index 6d4aa1491278e..aaecdbeb0e95d 100644 --- a/test/accessibility/services/a11y/analyze_with_axe.js +++ b/test/accessibility/services/a11y/analyze_with_axe.js @@ -33,6 +33,14 @@ export function analyzeWithAxe(context, options, callback) { id: 'aria-required-children', selector: '[data-skip-axe="aria-required-children"] > *', }, + { + id: 'label', + selector: '[data-test-subj="comboBoxSearchInput"] *', + }, + { + id: 'aria-roles', + selector: '[data-test-subj="comboBoxSearchInput"] *', + }, ], }); return window.axe.run(context, options); diff --git a/test/plugin_functional/plugins/data_search/server/plugin.ts b/test/plugin_functional/plugins/data_search/server/plugin.ts index 4252008dcd7ee..e016ef56802f3 100644 --- a/test/plugin_functional/plugins/data_search/server/plugin.ts +++ b/test/plugin_functional/plugins/data_search/server/plugin.ts @@ -58,12 +58,15 @@ export class DataSearchTestPlugin }, }, async (context, req, res) => { - const [, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const service = await data.search.searchSource.asScoped(req); + const savedObjectsClient = savedObjects.getScopedClient(req); // Since the index pattern ID can change on each test run, we need // to look it up on the fly and insert it into the request. - const indexPatterns = await data.indexPatterns.indexPatternsServiceFactory(req); + const indexPatterns = await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient + ); const ids = await indexPatterns.getIds(); // @ts-expect-error Force overwriting the request req.body.index = ids[0]; diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts index ddf9acb259983..a54502b740211 100644 --- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -39,8 +39,9 @@ export class IndexPatternsTestPlugin router.get( { path: '/api/index-patterns-plugin/get-all', validate: false }, async (context, req, res) => { - const [, { data }] = await core.getStartServices(); - const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const [{ savedObjects }, { data }] = await core.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(req); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ids = await service.getIds(); return res.ok({ body: ids }); } @@ -57,8 +58,9 @@ export class IndexPatternsTestPlugin }, async (context, req, res) => { const id = (req.params as Record).id; - const [, { data }] = await core.getStartServices(); - const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const [{ savedObjects }, { data }] = await core.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(req); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ip = await service.get(id); return res.ok({ body: ip.toSpec() }); } @@ -74,9 +76,10 @@ export class IndexPatternsTestPlugin }, }, async (context, req, res) => { - const [, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; - const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const savedObjectsClient = savedObjects.getScopedClient(req); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ip = await service.get(id); await service.updateSavedObject(ip); return res.ok(); @@ -93,9 +96,10 @@ export class IndexPatternsTestPlugin }, }, async (context, req, res) => { - const [, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; - const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const savedObjectsClient = savedObjects.getScopedClient(req); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); await service.delete(id); return res.ok(); } diff --git a/test/scripts/jenkins_test_setup_oss.sh b/test/scripts/jenkins_test_setup_oss.sh index b7eac33f35176..53626ce89462a 100755 --- a/test/scripts/jenkins_test_setup_oss.sh +++ b/test/scripts/jenkins_test_setup_oss.sh @@ -3,11 +3,7 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then - - destDir="build/kibana-build-oss" - if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then - destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" - fi + destDir="$WORKSPACE/kibana-build-oss-${TASK_QUEUE_PROCESS_ID:-$CI_PARALLEL_PROCESS_NUMBER}" if [[ ! -d $destDir ]]; then mkdir -p $destDir diff --git a/test/scripts/jenkins_test_setup_xpack.sh b/test/scripts/jenkins_test_setup_xpack.sh index 74a3de77e3a76..b9227fd8ff416 100755 --- a/test/scripts/jenkins_test_setup_xpack.sh +++ b/test/scripts/jenkins_test_setup_xpack.sh @@ -3,11 +3,7 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then - - destDir="build/kibana-build-xpack" - if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then - destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" - fi + destDir="$WORKSPACE/kibana-build-xpack-${TASK_QUEUE_PROCESS_ID:-$CI_PARALLEL_PROCESS_NUMBER}" if [[ ! -d $destDir ]]; then mkdir -p $destDir diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index eec7b0246d026..1495d52fceae8 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -72,7 +72,7 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector reporters: [ 'default', [ - `${kibanaDirectory}/src/dev/jest/junit_reporter.js`, + `${kibanaDirectory}/packages/kbn-test/target/jest/junit_reporter`, { reportName: 'X-Pack Jest Tests', }, diff --git a/x-pack/plugins/alerts/common/alert_instance_summary.ts b/x-pack/plugins/alerts/common/alert_instance_summary.ts index 333db3ccda963..08c3b2fc2c241 100644 --- a/x-pack/plugins/alerts/common/alert_instance_summary.ts +++ b/x-pack/plugins/alerts/common/alert_instance_summary.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -type AlertStatusValues = 'OK' | 'Active' | 'Error'; -type AlertInstanceStatusValues = 'OK' | 'Active'; +export type AlertStatusValues = 'OK' | 'Active' | 'Error'; +export type AlertInstanceStatusValues = 'OK' | 'Active'; export interface AlertInstanceSummary { id: string; diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.ts index 20d8bb7a19c1b..5d8113b685741 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.ts @@ -66,7 +66,7 @@ export function initPostCaseApi({ actionAt: createdDate, actionBy: { username, full_name, email }, caseId: newCase.id, - fields: ['description', 'status', 'tags', 'title'], + fields: ['description', 'status', 'tags', 'title', 'connector'], newValue: JSON.stringify(query), }), ], diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts index 732786b5f9249..1e3a45a83853c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts @@ -9,6 +9,10 @@ jest.mock('react', () => ({ useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior })); +// Helper for calling the returned useEffect unmount handler +import { useEffect } from 'react'; +export const unmountHandler = () => (useEffect as jest.Mock).mock.calls[0][0]()(); + /** * Example usage within a component test using shallow(): * @@ -19,3 +23,14 @@ jest.mock('react', () => ({ * * // ... etc. */ +/** + * Example unmount() usage: + * + * import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; + * + * it('unmounts', () => { + * shallow(SomeComponent); + * unmountHandler(); + * // expect something to have been done on unmount (NOTE: the component is not actually unmounted) + * }); + */ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index 92d14f7275185..374a2420f5ba7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -5,37 +5,79 @@ */ import { i18n } from '@kbn/i18n'; -export const ADMIN = 'admin'; -export const PRIVATE = 'private'; -export const SEARCH = 'search'; +export enum ApiTokenTypes { + Admin = 'admin', + Private = 'private', + Search = 'search', +} -export const TOKEN_TYPE_DESCRIPTION = { - [SEARCH]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.search.description', { - defaultMessage: 'Public Search Keys are used for search endpoints only.', - }), - [PRIVATE]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.private.description', { - defaultMessage: - 'Private API Keys are used for read and/or write access on one or more Engines.', - }), - [ADMIN]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.admin.description', { - defaultMessage: 'Private Admin Keys are used to interact with the Credentials API.', - }), +export const SEARCH_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.tokens.permissions.display.search', + { + defaultMessage: 'search', + } +); +export const ALL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.tokens.permissions.display.all', + { + defaultMessage: 'all', + } +); +export const READ_WRITE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.tokens.permissions.display.readwrite', + { + defaultMessage: 'read/write', + } +); +export const READ_ONLY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.tokens.permissions.display.readonly', + { + defaultMessage: 'read-only', + } +); +export const WRITE_ONLY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.tokens.permissions.display.writeonly', + { + defaultMessage: 'write-only', + } +); + +export const TOKEN_TYPE_DESCRIPTION: { [key: string]: string } = { + [ApiTokenTypes.Search]: i18n.translate( + 'xpack.enterpriseSearch.appSearch.tokens.search.description', + { + defaultMessage: 'Public Search Keys are used for search endpoints only.', + } + ), + [ApiTokenTypes.Private]: i18n.translate( + 'xpack.enterpriseSearch.appSearch.tokens.private.description', + { + defaultMessage: + 'Private API Keys are used for read and/or write access on one or more Engines.', + } + ), + [ApiTokenTypes.Admin]: i18n.translate( + 'xpack.enterpriseSearch.appSearch.tokens.admin.description', + { + defaultMessage: 'Private Admin Keys are used to interact with the Credentials API.', + } + ), }; -export const TOKEN_TYPE_DISPLAY_NAMES = { - [SEARCH]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.search.name', { +export const TOKEN_TYPE_DISPLAY_NAMES: { [key: string]: string } = { + [ApiTokenTypes.Search]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.search.name', { defaultMessage: 'Public Search Key', }), - [PRIVATE]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.private.name', { + [ApiTokenTypes.Private]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.private.name', { defaultMessage: 'Private API Key', }), - [ADMIN]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.admin.name', { + [ApiTokenTypes.Admin]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.admin.name', { defaultMessage: 'Private Admin Key', }), }; export const TOKEN_TYPE_INFO = [ - { value: SEARCH, text: TOKEN_TYPE_DISPLAY_NAMES[SEARCH] }, - { value: PRIVATE, text: TOKEN_TYPE_DISPLAY_NAMES[PRIVATE] }, - { value: ADMIN, text: TOKEN_TYPE_DISPLAY_NAMES[ADMIN] }, + { value: ApiTokenTypes.Search, text: TOKEN_TYPE_DISPLAY_NAMES[ApiTokenTypes.Search] }, + { value: ApiTokenTypes.Private, text: TOKEN_TYPE_DISPLAY_NAMES[ApiTokenTypes.Private] }, + { value: ApiTokenTypes.Admin, text: TOKEN_TYPE_DISPLAY_NAMES[ApiTokenTypes.Admin] }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx new file mode 100644 index 0000000000000..7b24f6d20a58f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx @@ -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 { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; +import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Credentials } from './credentials'; +import { EuiCopy, EuiPageContentBody } from '@elastic/eui'; + +import { externalUrl } from '../../../shared/enterprise_search_url'; + +describe('Credentials', () => { + // Kea mocks + const values = { + dataLoading: false, + }; + const actions = { + initializeCredentialsData: jest.fn(), + resetCredentials: jest.fn(), + showCredentialsForm: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiPageContentBody)).toHaveLength(1); + }); + + it('initializes data on mount', () => { + shallow(); + expect(actions.initializeCredentialsData).toHaveBeenCalledTimes(1); + }); + + it('calls resetCredentials on unmount', () => { + shallow(); + unmountHandler(); + expect(actions.resetCredentials).toHaveBeenCalledTimes(1); + }); + + it('renders nothing if data is still loading', () => { + setMockValues({ dataLoading: true }); + const wrapper = shallow(); + expect(wrapper.find(EuiPageContentBody)).toHaveLength(0); + }); + + it('renders the API endpoint and a button to copy it', () => { + externalUrl.enterpriseSearchUrl = 'http://localhost:3002'; + const copyMock = jest.fn(); + const wrapper = shallow(); + // We wrap children in a div so that `shallow` can render it. + const copyEl = shallow(
{wrapper.find(EuiCopy).props().children(copyMock)}
); + expect(copyEl.find('EuiButtonIcon').props().onClick).toEqual(copyMock); + expect(copyEl.text().replace('', '')).toEqual('http://localhost:3002'); + }); + + it('will show the Crendentials Flyout when the Create API Key button is pressed', () => { + const wrapper = shallow(); + const button: any = wrapper.find('[data-test-subj="CreateAPIKeyButton"]'); + button.props().onClick(); + expect(actions.showCredentialsForm).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx new file mode 100644 index 0000000000000..ae95482e0f855 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { useActions, useValues } from 'kea'; + +import { + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, + EuiPageContentBody, + EuiPanel, + EuiCopy, + EuiButtonIcon, + EuiSpacer, + EuiButton, + EuiPageContentHeader, + EuiPageContentHeaderSection, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { CredentialsLogic } from './credentials_logic'; +import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; +import { CredentialsList } from './credentials_list'; + +export const Credentials: React.FC = () => { + const { initializeCredentialsData, resetCredentials, showCredentialsForm } = useActions( + CredentialsLogic + ); + + const { dataLoading } = useValues(CredentialsLogic); + + useEffect(() => { + initializeCredentialsData(); + return () => { + resetCredentials(); + }; + }, []); + + // TODO + // if (dataLoading) { return } + if (dataLoading) { + return null; + } + return ( + <> + + + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.title', { + defaultMessage: 'Credentials', + })} +

+
+
+
+ + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiEndpoint', { + defaultMessage: 'Endpoint', + })} +

+
+ + {(copy) => ( + <> + + {externalUrl.enterpriseSearchUrl} + + )} + +
+ + + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiKeys', { + defaultMessage: 'API Keys', + })} +

+
+
+ + showCredentialsForm()} + > + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.createKey', { + defaultMessage: 'Create a key', + })} + + +
+ + + + +
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx new file mode 100644 index 0000000000000..7b7d89164662e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx @@ -0,0 +1,243 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { CredentialsList } from './credentials_list'; +import { EuiBasicTable, EuiCopy } from '@elastic/eui'; +import { IApiToken } from '../types'; +import { ApiTokenTypes } from '../constants'; + +describe('Credentials', () => { + const apiToken: IApiToken = { + name: '', + type: ApiTokenTypes.Private, + read: true, + write: true, + access_all_engines: true, + key: 'abc-1234', + }; + + // Kea mocks + const values = { + apiTokens: [], + meta: { + page: { + current: 1, + size: 10, + total_pages: 1, + total_results: 1, + }, + }, + }; + const actions = { + deleteApiKey: jest.fn(), + fetchCredentials: jest.fn(), + showCredentialsForm: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiBasicTable)).toHaveLength(1); + }); + + describe('items', () => { + it('sorts items by id', () => { + setMockValues({ + ...values, + apiTokens: [ + { + ...apiToken, + id: 2, + }, + { + ...apiToken, + id: undefined, + }, + { + ...apiToken, + id: 1, + }, + ], + }); + const wrapper = shallow(); + const { items } = wrapper.find(EuiBasicTable).props(); + expect(items.map((i: IApiToken) => i.id)).toEqual([undefined, 1, 2]); + }); + }); + + describe('pagination', () => { + it('derives pagination from meta object', () => { + setMockValues({ + ...values, + meta: { + page: { + current: 6, + size: 55, + total_pages: 1, + total_results: 1004, + }, + }, + }); + const wrapper = shallow(); + const { pagination } = wrapper.find(EuiBasicTable).props(); + expect(pagination).toEqual({ + pageIndex: 5, + pageSize: 55, + totalItemCount: 1004, + hidePerPageOptions: true, + }); + }); + + it('will default pagination values if `page` is not available', () => { + setMockValues({ ...values, meta: {} }); + const wrapper = shallow(); + const { pagination } = wrapper.find(EuiBasicTable).props(); + expect(pagination).toEqual({ + pageIndex: 0, + pageSize: 0, + totalItemCount: 0, + hidePerPageOptions: true, + }); + }); + }); + + describe('columns', () => { + let columns: any[]; + + beforeAll(() => { + const wrapper = shallow(); + columns = wrapper.find(EuiBasicTable).props().columns; + }); + + describe('column 1 (name)', () => { + const token = { + ...apiToken, + name: 'some-name', + }; + + it('renders correctly', () => { + const column = columns[0]; + const wrapper = shallow(
{column.render(token)}
); + expect(wrapper.text()).toEqual('some-name'); + }); + }); + + describe('column 2 (type)', () => { + const token = { + ...apiToken, + type: ApiTokenTypes.Private, + }; + + it('renders correctly', () => { + const column = columns[1]; + const wrapper = shallow(
{column.render(token)}
); + expect(wrapper.text()).toEqual('Private API Key'); + }); + }); + + describe('column 3 (key)', () => { + const testToken = { + ...apiToken, + key: 'abc-123', + }; + + it('renders the credential and a button to copy it', () => { + const copyMock = jest.fn(); + const column = columns[2]; + const wrapper = shallow(
{column.render(testToken)}
); + const children = wrapper.find(EuiCopy).props().children; + const copyEl = shallow(
{children(copyMock)}
); + expect(copyEl.find('EuiButtonIcon').props().onClick).toEqual(copyMock); + expect(copyEl.text()).toContain('abc-123'); + }); + + it('renders nothing if no key is present', () => { + const tokenWithNoKey = { + key: undefined, + }; + const column = columns[2]; + const wrapper = shallow(
{column.render(tokenWithNoKey)}
); + expect(wrapper.text()).toBe(''); + }); + }); + + describe('column 4 (modes)', () => { + const token = { + ...apiToken, + type: ApiTokenTypes.Admin, + }; + + it('renders correctly', () => { + const column = columns[3]; + const wrapper = shallow(
{column.render(token)}
); + expect(wrapper.text()).toEqual('--'); + }); + }); + + describe('column 5 (engines)', () => { + const token = { + ...apiToken, + type: ApiTokenTypes.Private, + access_all_engines: true, + }; + + it('renders correctly', () => { + const column = columns[4]; + const wrapper = shallow(
{column.render(token)}
); + expect(wrapper.text()).toEqual('all'); + }); + }); + + describe('column 6 (edit action)', () => { + const token = apiToken; + + it('calls showCredentialsForm when clicked', () => { + const action = columns[5].actions[0]; + action.onClick(token); + expect(actions.showCredentialsForm).toHaveBeenCalledWith(token); + }); + }); + + describe('column 7 (delete action)', () => { + const token = { + ...apiToken, + name: 'some-name', + }; + + it('calls deleteApiKey when clicked', () => { + const action = columns[5].actions[1]; + action.onClick(token); + expect(actions.deleteApiKey).toHaveBeenCalledWith('some-name'); + }); + }); + }); + + describe('onChange', () => { + it('will handle pagination by calling `fetchCredentials`', () => { + const wrapper = shallow(); + const { onChange } = wrapper.find(EuiBasicTable).props(); + + onChange({ + page: { + size: 10, + index: 2, + }, + }); + + expect(actions.fetchCredentials).toHaveBeenCalledWith(3); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx new file mode 100644 index 0000000000000..065601feeb4d2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx @@ -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 React, { useMemo } from 'react'; +import { EuiBasicTable, EuiBasicTableColumn, EuiButtonIcon, EuiCopy } from '@elastic/eui'; +import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table'; +import { useActions, useValues } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { CredentialsLogic } from '../credentials_logic'; +import { IApiToken } from '../types'; +import { TOKEN_TYPE_DISPLAY_NAMES } from '../constants'; +import { apiTokenSort } from '../utils/api_token_sort'; +import { getModeDisplayText, getEnginesDisplayText } from '../utils'; + +export const CredentialsList: React.FC = () => { + const { deleteApiKey, fetchCredentials, showCredentialsForm } = useActions(CredentialsLogic); + + const { apiTokens, meta } = useValues(CredentialsLogic); + + const items = useMemo(() => apiTokens.slice().sort(apiTokenSort), [apiTokens]); + + const columns: Array> = [ + { + name: 'Name', + width: '12%', + render: (token: IApiToken) => token.name, + }, + { + name: 'Type', + width: '15%', + render: (token: IApiToken) => TOKEN_TYPE_DISPLAY_NAMES[token.type], + }, + { + name: 'Key', + width: '36%', + render: (token: IApiToken) => { + if (!token.key) return null; + return ( + + {(copy) => ( + <> + + {token.key} + + )} + + ); + }, + }, + { + name: 'Modes', + width: '10%', + render: (token: IApiToken) => getModeDisplayText(token), + }, + { + name: 'Engines', + width: '18%', + render: (token: IApiToken) => getEnginesDisplayText(token), + }, + { + actions: [ + { + name: i18n.translate('xpack.enterpriseSearch.actions.edit', { + defaultMessage: 'Edit', + }), + description: i18n.translate('xpack.enterpriseSearch.appSearch.credentials.editKey', { + defaultMessage: 'Edit API Key', + }), + type: 'icon', + icon: 'pencil', + color: 'primary', + onClick: (token: IApiToken) => showCredentialsForm(token), + }, + { + name: i18n.translate('xpack.enterpriseSearch.actions.delete', { + defaultMessage: 'Delete', + }), + description: i18n.translate('xpack.enterpriseSearch.appSearch.credentials.deleteKey', { + defaultMessage: 'Delete API Key', + }), + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: (token: IApiToken) => deleteApiKey(token.name), + }, + ], + }, + ]; + + const pagination = { + pageIndex: meta.page ? meta.page.current - 1 : 0, + pageSize: meta.page ? meta.page.size : 0, + totalItemCount: meta.page ? meta.page.total_results : 0, + hidePerPageOptions: true, + }; + + const onTableChange = ({ page }: CriteriaWithPagination) => { + const { index: current } = page; + fetchCredentials(current + 1); + }; + + return ( + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/index.ts similarity index 76% rename from x-pack/plugins/index_lifecycle_management/server/shared_imports.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/index.ts index 454beda5394c7..5f254c1c716b4 100644 --- a/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { isEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { CredentialsList } from './credentials_list'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index 56fc825493b80..11b1253332cb2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -7,7 +7,7 @@ import { resetContext } from 'kea'; import { CredentialsLogic } from './credentials_logic'; -import { ADMIN, PRIVATE } from './constants'; +import { ApiTokenTypes } from './constants'; jest.mock('../../../shared/http', () => ({ HttpLogic: { values: { http: { get: jest.fn(), delete: jest.fn() } } }, @@ -22,7 +22,7 @@ describe('CredentialsLogic', () => { const DEFAULT_VALUES = { activeApiToken: { name: '', - type: PRIVATE, + type: ApiTokenTypes.Private, read: true, write: true, access_all_engines: true, @@ -62,7 +62,7 @@ describe('CredentialsLogic', () => { const newToken = { id: 1, name: 'myToken', - type: PRIVATE, + type: ApiTokenTypes.Private, read: true, write: true, access_all_engines: true, @@ -270,7 +270,7 @@ describe('CredentialsLogic', () => { describe('apiTokens', () => { const existingToken = { name: 'some_token', - type: PRIVATE, + type: ApiTokenTypes.Private, }; it('should add the provided token to the apiTokens list', () => { @@ -376,7 +376,7 @@ describe('CredentialsLogic', () => { describe('apiTokens', () => { const existingToken = { name: 'some_token', - type: PRIVATE, + type: ApiTokenTypes.Private, }; it('should replace the existing token with the new token by name', () => { @@ -385,7 +385,7 @@ describe('CredentialsLogic', () => { }); const updatedExistingToken = { ...existingToken, - type: ADMIN, + type: ApiTokenTypes.Admin, }; CredentialsLogic.actions.onApiTokenUpdateSuccess(updatedExistingToken); @@ -402,7 +402,7 @@ describe('CredentialsLogic', () => { }); const brandNewToken = { name: 'brand new token', - type: ADMIN, + type: ApiTokenTypes.Admin, }; CredentialsLogic.actions.onApiTokenUpdateSuccess(brandNewToken); @@ -419,7 +419,10 @@ describe('CredentialsLogic', () => { activeApiToken: newToken, }); - CredentialsLogic.actions.onApiTokenUpdateSuccess({ ...newToken, type: ADMIN }); + CredentialsLogic.actions.onApiTokenUpdateSuccess({ + ...newToken, + type: ApiTokenTypes.Admin, + }); expect(CredentialsLogic.values).toEqual({ ...values, activeApiToken: DEFAULT_VALUES.activeApiToken, @@ -433,7 +436,10 @@ describe('CredentialsLogic', () => { activeApiTokenRawName: 'foo', }); - CredentialsLogic.actions.onApiTokenUpdateSuccess({ ...newToken, type: ADMIN }); + CredentialsLogic.actions.onApiTokenUpdateSuccess({ + ...newToken, + type: ApiTokenTypes.Admin, + }); expect(CredentialsLogic.values).toEqual({ ...values, activeApiTokenRawName: DEFAULT_VALUES.activeApiTokenRawName, @@ -447,7 +453,10 @@ describe('CredentialsLogic', () => { shouldShowCredentialsForm: true, }); - CredentialsLogic.actions.onApiTokenUpdateSuccess({ ...newToken, type: ADMIN }); + CredentialsLogic.actions.onApiTokenUpdateSuccess({ + ...newToken, + type: ApiTokenTypes.Admin, + }); expect(CredentialsLogic.values).toEqual({ ...values, shouldShowCredentialsForm: false, @@ -650,7 +659,7 @@ describe('CredentialsLogic', () => { }; describe('activeApiToken.access_all_engines', () => { - describe('when value is ADMIN', () => { + describe('when value is admin', () => { it('updates access_all_engines to false', () => { mount({ activeApiToken: { @@ -659,7 +668,7 @@ describe('CredentialsLogic', () => { }, }); - CredentialsLogic.actions.setTokenType(ADMIN); + CredentialsLogic.actions.setTokenType(ApiTokenTypes.Admin); expect(CredentialsLogic.values).toEqual({ ...values, activeApiToken: { @@ -670,7 +679,7 @@ describe('CredentialsLogic', () => { }); }); - describe('when value is not ADMIN', () => { + describe('when value is not admin', () => { it('will maintain access_all_engines value when true', () => { mount({ activeApiToken: { @@ -679,7 +688,7 @@ describe('CredentialsLogic', () => { }, }); - CredentialsLogic.actions.setTokenType(PRIVATE); + CredentialsLogic.actions.setTokenType(ApiTokenTypes.Private); expect(CredentialsLogic.values).toEqual({ ...values, activeApiToken: { @@ -697,7 +706,7 @@ describe('CredentialsLogic', () => { }, }); - CredentialsLogic.actions.setTokenType(PRIVATE); + CredentialsLogic.actions.setTokenType(ApiTokenTypes.Private); expect(CredentialsLogic.values).toEqual({ ...values, activeApiToken: { @@ -710,7 +719,7 @@ describe('CredentialsLogic', () => { }); describe('activeApiToken.engines', () => { - describe('when value is ADMIN', () => { + describe('when value is admin', () => { it('clears the array', () => { mount({ activeApiToken: { @@ -719,7 +728,7 @@ describe('CredentialsLogic', () => { }, }); - CredentialsLogic.actions.setTokenType(ADMIN); + CredentialsLogic.actions.setTokenType(ApiTokenTypes.Admin); expect(CredentialsLogic.values).toEqual({ ...values, activeApiToken: { @@ -730,7 +739,7 @@ describe('CredentialsLogic', () => { }); }); - describe('when value is not ADMIN', () => { + describe('when value is not admin', () => { it('will maintain engines array', () => { mount({ activeApiToken: { @@ -739,7 +748,7 @@ describe('CredentialsLogic', () => { }, }); - CredentialsLogic.actions.setTokenType(PRIVATE); + CredentialsLogic.actions.setTokenType(ApiTokenTypes.Private); expect(CredentialsLogic.values).toEqual({ ...values, activeApiToken: { @@ -752,7 +761,7 @@ describe('CredentialsLogic', () => { }); describe('activeApiToken.write', () => { - describe('when value is PRIVATE', () => { + describe('when value is private', () => { it('sets this to true', () => { mount({ activeApiToken: { @@ -761,7 +770,7 @@ describe('CredentialsLogic', () => { }, }); - CredentialsLogic.actions.setTokenType(PRIVATE); + CredentialsLogic.actions.setTokenType(ApiTokenTypes.Private); expect(CredentialsLogic.values).toEqual({ ...values, activeApiToken: { @@ -772,7 +781,7 @@ describe('CredentialsLogic', () => { }); }); - describe('when value is not PRIVATE', () => { + describe('when value is not private', () => { it('sets this to false', () => { mount({ activeApiToken: { @@ -781,7 +790,7 @@ describe('CredentialsLogic', () => { }, }); - CredentialsLogic.actions.setTokenType(ADMIN); + CredentialsLogic.actions.setTokenType(ApiTokenTypes.Admin); expect(CredentialsLogic.values).toEqual({ ...values, activeApiToken: { @@ -794,7 +803,7 @@ describe('CredentialsLogic', () => { }); describe('activeApiToken.read', () => { - describe('when value is PRIVATE', () => { + describe('when value is private', () => { it('sets this to true', () => { mount({ activeApiToken: { @@ -803,7 +812,7 @@ describe('CredentialsLogic', () => { }, }); - CredentialsLogic.actions.setTokenType(PRIVATE); + CredentialsLogic.actions.setTokenType(ApiTokenTypes.Private); expect(CredentialsLogic.values).toEqual({ ...values, activeApiToken: { @@ -814,7 +823,7 @@ describe('CredentialsLogic', () => { }); }); - describe('when value is not PRIVATE', () => { + describe('when value is not private', () => { it('sets this to false', () => { mount({ activeApiToken: { @@ -823,7 +832,7 @@ describe('CredentialsLogic', () => { }, }); - CredentialsLogic.actions.setTokenType(ADMIN); + CredentialsLogic.actions.setTokenType(ApiTokenTypes.Admin); expect(CredentialsLogic.values).toEqual({ ...values, activeApiToken: { @@ -840,16 +849,16 @@ describe('CredentialsLogic', () => { mount({ activeApiToken: { ...newToken, - type: ADMIN, + type: ApiTokenTypes.Admin, }, }); - CredentialsLogic.actions.setTokenType(PRIVATE); + CredentialsLogic.actions.setTokenType(ApiTokenTypes.Private); expect(CredentialsLogic.values).toEqual({ ...values, activeApiToken: { ...values.activeApiToken, - type: PRIVATE, + type: ApiTokenTypes.Private, }, }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts index 41897b8edbc1e..c6f929c45eb23 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts @@ -7,7 +7,7 @@ import { kea, MakeLogicType } from 'kea'; import { formatApiName } from '../../utils/format_api_name'; -import { ADMIN, PRIVATE } from './constants'; +import { ApiTokenTypes } from './constants'; import { HttpLogic } from '../../../shared/http'; import { IMeta } from '../../../../../common/types'; @@ -17,7 +17,7 @@ import { IApiToken, ICredentialsDetails, ITokenReadWrite } from './types'; const defaultApiToken: IApiToken = { name: '', - type: PRIVATE, + type: ApiTokenTypes.Private, read: true, write: true, access_all_engines: true, @@ -164,11 +164,12 @@ export const CredentialsLogic = kea< }), setTokenType: (activeApiToken, tokenType) => ({ ...activeApiToken, - access_all_engines: tokenType === ADMIN ? false : activeApiToken.access_all_engines, - engines: tokenType === ADMIN ? [] : activeApiToken.engines, - write: tokenType === PRIVATE, - read: tokenType === PRIVATE, - type: tokenType, + access_all_engines: + tokenType === ApiTokenTypes.Admin ? false : activeApiToken.access_all_engines, + engines: tokenType === ApiTokenTypes.Admin ? [] : activeApiToken.engines, + write: tokenType === ApiTokenTypes.Private, + read: tokenType === ApiTokenTypes.Private, + type: tokenType as ApiTokenTypes, }), showCredentialsForm: (_, activeApiToken) => activeApiToken, }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/index.ts new file mode 100644 index 0000000000000..bceda234175a7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/index.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 { Credentials } from './credentials'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts index bbf7a54da10da..9ca4d086d55c8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts @@ -5,6 +5,7 @@ */ import { IEngine } from '../../types'; +import { ApiTokenTypes } from './constants'; export interface ICredentialsDetails { engines: IEngine[]; @@ -17,7 +18,7 @@ export interface IApiToken { id?: number; name: string; read?: boolean; - type: string; + type: ApiTokenTypes; write?: boolean; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts new file mode 100644 index 0000000000000..84818322b3570 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { apiTokenSort } from '.'; +import { ApiTokenTypes } from '../constants'; + +import { IApiToken } from '../types'; + +describe('apiTokenSort', () => { + const apiToken: IApiToken = { + name: '', + type: ApiTokenTypes.Private, + read: true, + write: true, + access_all_engines: true, + key: 'abc-1234', + }; + + it('sorts items by id', () => { + const apiTokens = [ + { + ...apiToken, + id: 2, + }, + { + ...apiToken, + id: undefined, + }, + { + ...apiToken, + id: 1, + }, + ]; + + expect(apiTokens.sort(apiTokenSort).map((t) => t.id)).toEqual([undefined, 1, 2]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.ts new file mode 100644 index 0000000000000..80a46f30e0930 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IApiToken } from '../types'; + +export const apiTokenSort = (apiTokenA: IApiToken, apiTokenB: IApiToken): number => { + if (!apiTokenA.id) { + return -1; + } + if (!apiTokenB.id) { + return 1; + } + return apiTokenA.id - apiTokenB.id; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx new file mode 100644 index 0000000000000..b06ed63f8616c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { getEnginesDisplayText } from './get_engines_display_text'; +import { IApiToken } from '../types'; +import { ApiTokenTypes } from '../constants'; + +const apiToken: IApiToken = { + name: '', + type: ApiTokenTypes.Private, + read: true, + write: true, + access_all_engines: true, + engines: ['engine1', 'engine2', 'engine3'], +}; + +describe('getEnginesDisplayText', () => { + it('returns "--" when the token is an admin token', () => { + const wrapper = shallow( +
{getEnginesDisplayText({ ...apiToken, type: ApiTokenTypes.Admin })}
+ ); + expect(wrapper.text()).toEqual('--'); + }); + + it('returns "all" when access_all_engines is true', () => { + const wrapper = shallow( +
{getEnginesDisplayText({ ...apiToken, access_all_engines: true })}
+ ); + expect(wrapper.text()).toEqual('all'); + }); + + it('returns a list of engines if access_all_engines is false', () => { + const wrapper = shallow( +
{getEnginesDisplayText({ ...apiToken, access_all_engines: false })}
+ ); + + expect(wrapper.find('li').map((e) => e.text())).toEqual(['engine1', 'engine2', 'engine3']); + }); + + it('returns "--" when the token is an admin token, even if access_all_engines is true', () => { + const wrapper = shallow( +
+ {getEnginesDisplayText({ + ...apiToken, + access_all_engines: true, + type: ApiTokenTypes.Admin, + })} +
+ ); + expect(wrapper.text()).toEqual('--'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx new file mode 100644 index 0000000000000..1b216c46307db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ApiTokenTypes, ALL } from '../constants'; +import { IApiToken } from '../types'; + +export const getEnginesDisplayText = (apiToken: IApiToken): JSX.Element | string => { + const { type, access_all_engines: accessAll, engines = [] } = apiToken; + if (type === ApiTokenTypes.Admin) { + return '--'; + } + if (accessAll) { + return ALL; + } + return ( +
    + {engines.map((engine) => ( +
  • {engine}
  • + ))} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.test.ts new file mode 100644 index 0000000000000..b2083f22c8e1c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ApiTokenTypes } from '../constants'; +import { IApiToken } from '../types'; + +import { getModeDisplayText } from './get_mode_display_text'; + +const apiToken: IApiToken = { + name: '', + type: ApiTokenTypes.Private, + read: true, + write: true, + access_all_engines: true, + engines: ['engine1', 'engine2', 'engine3'], +}; + +describe('getModeDisplayText', () => { + it('will return read/write when read and write are enabled', () => { + expect(getModeDisplayText({ ...apiToken, read: true, write: true })).toEqual('read/write'); + }); + + it('will return read-only when only read is enabled', () => { + expect(getModeDisplayText({ ...apiToken, read: true, write: false })).toEqual('read-only'); + }); + + it('will return write-only when only write is enabled', () => { + expect(getModeDisplayText({ ...apiToken, read: false, write: true })).toEqual('write-only'); + }); + + it('will return "search" if the key is a search key, regardless of read/write state', () => { + expect( + getModeDisplayText({ ...apiToken, type: ApiTokenTypes.Search, read: false, write: true }) + ).toEqual('search'); + }); + + it('will return "--" if the key is an admin key, regardless of read/write state', () => { + expect( + getModeDisplayText({ ...apiToken, type: ApiTokenTypes.Admin, read: false, write: true }) + ).toEqual('--'); + }); + + it('will default read and write to false', () => { + expect( + getModeDisplayText({ + name: 'test', + type: ApiTokenTypes.Private, + }) + ).toEqual('read-only'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.ts new file mode 100644 index 0000000000000..9c8758d83882d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_mode_display_text.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ApiTokenTypes, READ_ONLY, READ_WRITE, SEARCH_DISPLAY, WRITE_ONLY } from '../constants'; +import { IApiToken } from '../types'; + +export const getModeDisplayText = (apiToken: IApiToken): string => { + const { read = false, write = false, type } = apiToken; + + switch (type) { + case ApiTokenTypes.Admin: + return '--'; + case ApiTokenTypes.Search: + return SEARCH_DISPLAY; + default: + if (read && write) { + return READ_WRITE; + } + return write ? WRITE_ONLY : READ_ONLY; + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/index.ts new file mode 100644 index 0000000000000..9aca7f44be03b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { apiTokenSort } from './api_token_sort'; +export { getEnginesDisplayText } from './get_engines_display_text'; +export { getModeDisplayText } from './get_mode_display_text'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index ab5b3c9faeea7..546ea311ad33e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -121,9 +121,7 @@ describe('AppSearchNav', () => { setMockValues({ myRole: { canViewAccountCredentials: true } }); const wrapper = shallow(); - expect(wrapper.find(SideNavLink).last().prop('to')).toEqual( - 'http://localhost:3002/as/credentials' - ); + expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/credentials'); }); it('renders the Role Mappings link', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 9aa2cce9c74df..ec5f5b164a7f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -32,6 +32,7 @@ import { SetupGuide } from './components/setup_guide'; import { ErrorConnecting } from './components/error_connecting'; import { NotFound } from '../shared/not_found'; import { EngineOverview } from './components/engine_overview'; +import { Credentials } from './components/credentials'; export const AppSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); @@ -75,6 +76,9 @@ export const AppSearchConfigured: React.FC = (props) => { + + + @@ -106,7 +110,7 @@ export const AppSearchNav: React.FC = () => { )} {canViewAccountCredentials && ( - + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.credentials', { defaultMessage: 'Credentials', })} diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 84b8fa35cfe9b..40037d0c1e777 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -22,10 +22,10 @@ import { Dependencies } from './types'; import { registerApiRoutes } from './routes'; import { License } from './services'; import { IndexLifecycleManagementConfig } from './config'; -import { isEsError } from './shared_imports'; const indexLifecycleDataEnricher = async ( indicesList: IndexWithoutIlm[], + // TODO replace deprecated ES client after Index Management is updated callAsCurrentUser: LegacyAPICaller ): Promise => { if (!indicesList || !indicesList.length) { @@ -99,9 +99,6 @@ export class IndexLifecycleManagementServerPlugin implements Plugin { @@ -45,17 +41,17 @@ export function registerAddPolicyRoute({ router, license, lib }: RouteDependenci try { await addLifecyclePolicy( - context.core.elasticsearch.legacy.client.callAsCurrentUser, + context.core.elasticsearch.client.asCurrentUser, indexName, policyName, alias ); return response.ok(); } catch (e) { - if (lib.isEsError(e)) { + if (e.name === 'ResponseError') { return response.customError({ statusCode: e.statusCode, - body: e, + body: { message: e.body.error?.reason }, }); } // Case: default 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 2601775f5d76e..a83a3fa1378c8 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 @@ -5,22 +5,19 @@ */ import { schema } from '@kbn/config-schema'; -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'kibana/server'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; -async function removeLifecycle(callAsCurrentUser: LegacyAPICaller, indexNames: string[]) { +async function removeLifecycle(client: ElasticsearchClient, indexNames: string[]) { + const options = { + ignore: [404], + }; const responses = []; for (let i = 0; i < indexNames.length; i++) { const indexName = indexNames[i]; - const params = { - method: 'POST', - path: `/${encodeURIComponent(indexName)}/_ilm/remove`, - ignore: [404], - }; - - responses.push(callAsCurrentUser('transport.request', params)); + responses.push(client.ilm.removePolicy({ index: indexName }, options)); } return Promise.all(responses); } @@ -29,7 +26,7 @@ const bodySchema = schema.object({ indexNames: schema.arrayOf(schema.string()), }); -export function registerRemoveRoute({ router, license, lib }: RouteDependencies) { +export function registerRemoveRoute({ router, license }: RouteDependencies) { router.post( { path: addBasePath('/index/remove'), validate: { body: bodySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -37,16 +34,13 @@ export function registerRemoveRoute({ router, license, lib }: RouteDependencies) const { indexNames } = body; try { - await removeLifecycle( - context.core.elasticsearch.legacy.client.callAsCurrentUser, - indexNames - ); + await removeLifecycle(context.core.elasticsearch.client.asCurrentUser, indexNames); return response.ok(); } catch (e) { - if (lib.isEsError(e)) { + if (e.name === 'ResponseError') { return response.customError({ statusCode: e.statusCode, - body: e, + body: { message: e.body.error?.reason }, }); } // Case: default 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 09fd1d6068285..cdcf5ed4b7ac4 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 @@ -5,22 +5,20 @@ */ import { schema } from '@kbn/config-schema'; -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'kibana/server'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; -async function retryLifecycle(callAsCurrentUser: LegacyAPICaller, indexNames: string[]) { +async function retryLifecycle(client: ElasticsearchClient, indexNames: string[]) { + const options = { + ignore: [404], + }; const responses = []; for (let i = 0; i < indexNames.length; i++) { const indexName = indexNames[i]; - const params = { - method: 'POST', - path: `/${encodeURIComponent(indexName)}/_ilm/retry`, - ignore: [404], - }; - responses.push(callAsCurrentUser('transport.request', params)); + responses.push(client.ilm.retry({ index: indexName }, options)); } return Promise.all(responses); } @@ -29,7 +27,7 @@ const bodySchema = schema.object({ indexNames: schema.arrayOf(schema.string()), }); -export function registerRetryRoute({ router, license, lib }: RouteDependencies) { +export function registerRetryRoute({ router, license }: RouteDependencies) { router.post( { path: addBasePath('/index/retry'), validate: { body: bodySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -37,16 +35,13 @@ export function registerRetryRoute({ router, license, lib }: RouteDependencies) const { indexNames } = body; try { - await retryLifecycle( - context.core.elasticsearch.legacy.client.callAsCurrentUser, - indexNames - ); + await retryLifecycle(context.core.elasticsearch.client.asCurrentUser, indexNames); return response.ok(); } catch (e) { - if (lib.isEsError(e)) { + if (e.name === 'ResponseError') { return response.customError({ statusCode: e.statusCode, - body: e, + body: { message: e.body.error?.reason }, }); } // Case: default 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 f8d4a9681b3d8..57034af324ed5 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 @@ -5,7 +5,6 @@ */ import { schema } from '@kbn/config-schema'; -import { LegacyAPICaller } from 'src/core/server'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; @@ -26,19 +25,11 @@ function findMatchingNodes(stats: any, nodeAttrs: string): any { }, []); } -async function fetchNodeStats(callAsCurrentUser: LegacyAPICaller): Promise { - const params = { - format: 'json', - }; - - return await callAsCurrentUser('nodes.stats', params); -} - const paramsSchema = schema.object({ nodeAttrs: schema.string(), }); -export function registerDetailsRoute({ router, license, lib }: RouteDependencies) { +export function registerDetailsRoute({ router, license }: RouteDependencies) { router.get( { path: addBasePath('/nodes/{nodeAttrs}/details'), validate: { params: paramsSchema } }, license.guardApiRoute(async (context, request, response) => { @@ -46,16 +37,14 @@ export function registerDetailsRoute({ router, license, lib }: RouteDependencies const { nodeAttrs } = params; try { - const stats = await fetchNodeStats( - context.core.elasticsearch.legacy.client.callAsCurrentUser - ); - const okResponse = { body: findMatchingNodes(stats, nodeAttrs) }; + const statsResponse = await context.core.elasticsearch.client.asCurrentUser.nodes.stats(); + const okResponse = { body: findMatchingNodes(statsResponse.body, nodeAttrs) }; return response.ok(okResponse); } catch (e) { - if (lib.isEsError(e)) { + if (e.name === 'ResponseError') { return response.customError({ statusCode: e.statusCode, - body: e, + body: { message: e.body.error?.reason }, }); } // Case: default 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 99df70e7df82d..f7f048e809d75 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 @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller } from 'src/core/server'; - import { ListNodesRouteResponse, NodeDataRole } from '../../../../common/types'; import { RouteDependencies } from '../../../types'; @@ -47,15 +45,7 @@ function convertStatsIntoList( ); } -async function fetchNodeStats(callAsCurrentUser: LegacyAPICaller): Promise { - const params = { - format: 'json', - }; - - return await callAsCurrentUser('nodes.stats', params); -} - -export function registerListRoute({ router, config, license, lib }: RouteDependencies) { +export function registerListRoute({ router, config, license }: RouteDependencies) { const { filteredNodeAttributes } = config; const NODE_ATTRS_KEYS_TO_IGNORE: string[] = [ @@ -74,16 +64,19 @@ export function registerListRoute({ router, config, license, lib }: RouteDepende { path: addBasePath('/nodes/list'), validate: false }, license.guardApiRoute(async (context, request, response) => { try { - const stats = await fetchNodeStats( - context.core.elasticsearch.legacy.client.callAsCurrentUser + const statsResponse = await context.core.elasticsearch.client.asCurrentUser.nodes.stats< + Stats + >(); + const body: ListNodesRouteResponse = convertStatsIntoList( + statsResponse.body, + disallowedNodeAttributes ); - const body: ListNodesRouteResponse = convertStatsIntoList(stats, disallowedNodeAttributes); return response.ok({ body }); } catch (e) { - if (lib.isEsError(e)) { + if (e.name === 'ResponseError') { return response.customError({ statusCode: e.statusCode, - body: e, + body: { message: e.body.error?.reason }, }); } // Case: default 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 d9e0a6e218de5..359b275622f0c 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 @@ -5,29 +5,22 @@ */ import { schema } from '@kbn/config-schema'; -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'kibana/server'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; -async function createPolicy( - callAsCurrentUser: LegacyAPICaller, - name: string, - phases: any -): Promise { +async function createPolicy(client: ElasticsearchClient, name: string, phases: any): Promise { const body = { policy: { phases, }, }; - const params = { - method: 'PUT', - path: `/_ilm/policy/${encodeURIComponent(name)}`, + const options = { ignore: [404], - body, }; - return await callAsCurrentUser('transport.request', params); + return client.ilm.putLifecycle({ policy: name, body }, options); } const minAgeSchema = schema.maybe(schema.string()); @@ -141,7 +134,7 @@ const bodySchema = schema.object({ }), }); -export function registerCreateRoute({ router, license, lib }: RouteDependencies) { +export function registerCreateRoute({ router, license }: RouteDependencies) { router.post( { path: addBasePath('/policies'), validate: { body: bodySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -149,17 +142,13 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) const { name, phases } = body; try { - await createPolicy( - context.core.elasticsearch.legacy.client.callAsCurrentUser, - name, - phases - ); + await createPolicy(context.core.elasticsearch.client.asCurrentUser, name, phases); return response.ok(); } catch (e) { - if (lib.isEsError(e)) { + if (e.name === 'ResponseError') { return response.customError({ statusCode: e.statusCode, - body: e, + body: { message: e.body.error?.reason }, }); } // Case: default 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 992a0fab452ec..cb394c12c46fa 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 @@ -5,30 +5,25 @@ */ import { schema } from '@kbn/config-schema'; -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'kibana/server'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; -async function deletePolicies( - callAsCurrentUser: LegacyAPICaller, - policyNames: string -): Promise { - const params = { - method: 'DELETE', - path: `/_ilm/policy/${encodeURIComponent(policyNames)}`, +async function deletePolicies(client: ElasticsearchClient, policyName: string): Promise { + const options = { // we allow 404 since they may have no policies ignore: [404], }; - return await callAsCurrentUser('transport.request', params); + return client.ilm.deleteLifecycle({ policy: policyName }, options); } const paramsSchema = schema.object({ policyNames: schema.string(), }); -export function registerDeleteRoute({ router, license, lib }: RouteDependencies) { +export function registerDeleteRoute({ router, license }: RouteDependencies) { router.delete( { path: addBasePath('/policies/{policyNames}'), validate: { params: paramsSchema } }, license.guardApiRoute(async (context, request, response) => { @@ -36,16 +31,13 @@ export function registerDeleteRoute({ router, license, lib }: RouteDependencies) const { policyNames } = params; try { - await deletePolicies( - context.core.elasticsearch.legacy.client.callAsCurrentUser, - policyNames - ); + await deletePolicies(context.core.elasticsearch.client.asCurrentUser, policyNames); return response.ok(); } catch (e) { - if (lib.isEsError(e)) { + if (e.name === 'ResponseError') { return response.customError({ statusCode: e.statusCode, - body: e, + body: { message: e.body.error?.reason }, }); } // Case: default 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 4fb21ea8c6a62..8cbea25666378 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 @@ -5,22 +5,17 @@ */ import { schema } from '@kbn/config-schema'; -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'kibana/server'; +import { ApiResponse } from '@elastic/elasticsearch'; import { IndexLifecyclePolicy, PolicyFromES } from '../../../../common/types'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; -type PoliciesMap = { +interface PoliciesMap { [K: string]: Omit; -} & { - status?: number; -}; +} function formatPolicies(policiesMap: PoliciesMap): PolicyFromES[] { - if (policiesMap.status === 404) { - return []; - } - return Object.keys(policiesMap).reduce((accum: PolicyFromES[], lifecycleName: string) => { const policyEntry = policiesMap[lifecycleName]; accum.push({ @@ -31,31 +26,25 @@ function formatPolicies(policiesMap: PoliciesMap): PolicyFromES[] { }, []); } -async function fetchPolicies(callAsCurrentUser: LegacyAPICaller): Promise { - const params = { - method: 'GET', - path: '/_ilm/policy', +async function fetchPolicies(client: ElasticsearchClient): Promise> { + const options = { // we allow 404 since they may have no policies ignore: [404], }; - return await callAsCurrentUser('transport.request', params); + return client.ilm.getLifecycle({}, options); } -async function addLinkedIndices(callAsCurrentUser: LegacyAPICaller, policiesMap: PoliciesMap) { - if (policiesMap.status === 404) { - return policiesMap; - } - const params = { - method: 'GET', - path: '/*/_ilm/explain', +async function addLinkedIndices(client: ElasticsearchClient, policiesMap: PoliciesMap) { + const options = { // we allow 404 since they may have no policies ignore: [404], }; - const policyExplanation: { + const response = await client.ilm.explainLifecycle<{ indices: { [indexName: string]: IndexLifecyclePolicy }; - } = await callAsCurrentUser('transport.request', params); + }>({ index: '*' }, options); + const policyExplanation = response.body; Object.entries(policyExplanation.indices).forEach(([indexName, { policy }]) => { if (policy && policiesMap[policy]) { policiesMap[policy].linkedIndices = policiesMap[policy].linkedIndices || []; @@ -68,26 +57,29 @@ const querySchema = schema.object({ withIndices: schema.boolean({ defaultValue: false }), }); -export function registerFetchRoute({ router, license, lib }: RouteDependencies) { +export function registerFetchRoute({ router, license }: RouteDependencies) { router.get( { path: addBasePath('/policies'), validate: { query: querySchema } }, license.guardApiRoute(async (context, request, response) => { const query = request.query as typeof querySchema.type; const { withIndices } = query; - const { callAsCurrentUser } = context.core.elasticsearch.legacy.client; + const { asCurrentUser } = context.core.elasticsearch.client; try { - const policiesMap = await fetchPolicies(callAsCurrentUser); + const policiesResponse = await fetchPolicies(asCurrentUser); + if (policiesResponse.statusCode === 404) { + return response.ok({ body: [] }); + } + const { body: policiesMap } = policiesResponse; if (withIndices) { - await addLinkedIndices(callAsCurrentUser, policiesMap); + await addLinkedIndices(asCurrentUser, policiesMap); } - const okResponse = { body: formatPolicies(policiesMap) }; - return response.ok(okResponse); + return response.ok({ body: formatPolicies(policiesMap) }); } catch (e) { - if (lib.isEsError(e)) { + if (e.name === 'ResponseError') { return response.customError({ statusCode: e.statusCode, - body: e, + body: { message: e.body.error?.reason }, }); } // Case: default 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 7a52648e29ee8..869be3d557040 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 @@ -4,34 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'kibana/server'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; -async function fetchSnapshotPolicies(callAsCurrentUser: LegacyAPICaller): Promise { - const params = { - method: 'GET', - path: '/_slm/policy', - }; - - return await callAsCurrentUser('transport.request', params); +async function fetchSnapshotPolicies(client: ElasticsearchClient): Promise { + const response = await client.slm.getLifecycle(); + return response.body; } -export function registerFetchRoute({ router, license, lib }: RouteDependencies) { +export function registerFetchRoute({ router, license }: RouteDependencies) { router.get( { path: addBasePath('/snapshot_policies'), validate: false }, license.guardApiRoute(async (context, request, response) => { try { const policiesByName = await fetchSnapshotPolicies( - context.core.elasticsearch.legacy.client.callAsCurrentUser + context.core.elasticsearch.client.asCurrentUser ); return response.ok({ body: Object.keys(policiesByName) }); } catch (e) { - if (lib.isEsError(e)) { + if (e.name === 'ResponseError') { return response.customError({ statusCode: e.statusCode, - body: e, + body: { message: e.body.error?.reason }, }); } // Case: default 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 b47f346c6492d..7e7f3f1f725f8 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 @@ -6,7 +6,7 @@ import { merge } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { TemplateFromEs, TemplateSerialized } from '../../../../../index_management/common/types'; @@ -15,32 +15,37 @@ import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; async function getLegacyIndexTemplate( - callAsCurrentUser: LegacyAPICaller, + client: ElasticsearchClient, templateName: string ): Promise { - const response = await callAsCurrentUser('indices.getTemplate', { name: templateName }); - return response[templateName]; + const response = await client.indices.getTemplate({ name: templateName }); + return response.body[templateName]; } async function getIndexTemplate( - callAsCurrentUser: LegacyAPICaller, + client: ElasticsearchClient, templateName: string ): Promise { - const params = { - method: 'GET', - path: `/_index_template/${encodeURIComponent(templateName)}`, + const options = { // we allow 404 incase the user shutdown security in-between the check and now ignore: [404], }; - const { index_templates: templates } = await callAsCurrentUser<{ + const response = await client.indices.getIndexTemplate<{ index_templates: TemplateFromEs[]; - }>('transport.request', params); + }>( + { + name: templateName, + }, + options + ); + + const { index_templates: templates } = response.body; return templates?.find((template) => template.name === templateName)?.index_template; } async function updateIndexTemplate( - callAsCurrentUser: LegacyAPICaller, + client: ElasticsearchClient, isLegacy: boolean, templateName: string, policyName: string, @@ -56,8 +61,8 @@ async function updateIndexTemplate( }; const indexTemplate = isLegacy - ? await getLegacyIndexTemplate(callAsCurrentUser, templateName) - : await getIndexTemplate(callAsCurrentUser, templateName); + ? await getLegacyIndexTemplate(client, templateName) + : await getIndexTemplate(client, templateName); if (!indexTemplate) { return false; } @@ -71,15 +76,10 @@ async function updateIndexTemplate( }); } - const pathPrefix = isLegacy ? '/_template/' : '/_index_template/'; - const params = { - method: 'PUT', - path: `${pathPrefix}${encodeURIComponent(templateName)}`, - ignore: [404], - body: indexTemplate, - }; - - return await callAsCurrentUser('transport.request', params); + if (isLegacy) { + return client.indices.putTemplate({ name: templateName, body: indexTemplate }); + } + return client.indices.putIndexTemplate({ name: templateName, body: indexTemplate }); } const bodySchema = schema.object({ @@ -92,7 +92,7 @@ const querySchema = schema.object({ legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); -export function registerAddPolicyRoute({ router, license, lib }: RouteDependencies) { +export function registerAddPolicyRoute({ router, license }: RouteDependencies) { router.post( { path: addBasePath('/template'), validate: { body: bodySchema, query: querySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -101,7 +101,7 @@ export function registerAddPolicyRoute({ router, license, lib }: RouteDependenci const isLegacy = (request.query as TypeOf).legacy === 'true'; try { const updatedTemplate = await updateIndexTemplate( - context.core.elasticsearch.legacy.client.callAsCurrentUser, + context.core.elasticsearch.client.asCurrentUser, isLegacy, templateName, policyName, @@ -119,10 +119,10 @@ export function registerAddPolicyRoute({ router, license, lib }: RouteDependenci } return response.ok(); } catch (e) { - if (lib.isEsError(e)) { + if (e.name === 'ResponseError') { return response.customError({ statusCode: e.statusCode, - body: e, + body: { message: e.body.error?.reason }, }); } // Case: default 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 b60892428b969..fbd102d3be1eb 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { schema, TypeOf } from '@kbn/config-schema'; import { IndexSettings, @@ -60,42 +60,43 @@ function filterTemplates( } async function fetchTemplates( - callAsCurrentUser: LegacyAPICaller, + client: ElasticsearchClient, isLegacy: boolean ): Promise< { index_templates: TemplateFromEs[] } | { [templateName: string]: LegacyTemplateSerialized } > { - const params = { - method: 'GET', - path: isLegacy ? '/_template' : '/_index_template', + const options = { // we allow 404 incase the user shutdown security in-between the check and now ignore: [404], }; - return await callAsCurrentUser('transport.request', params); + const response = isLegacy + ? await client.indices.getTemplate({}, options) + : await client.indices.getIndexTemplate({}, options); + return response.body; } const querySchema = schema.object({ legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); -export function registerFetchRoute({ router, license, lib }: RouteDependencies) { +export function registerFetchRoute({ router, license }: RouteDependencies) { router.get( { path: addBasePath('/templates'), validate: { query: querySchema } }, license.guardApiRoute(async (context, request, response) => { const isLegacy = (request.query as TypeOf).legacy === 'true'; try { const templates = await fetchTemplates( - context.core.elasticsearch.legacy.client.callAsCurrentUser, + context.core.elasticsearch.client.asCurrentUser, isLegacy ); const okResponse = { body: filterTemplates(templates, isLegacy) }; return response.ok(okResponse); } catch (e) { - if (lib.isEsError(e)) { + if (e.name === 'ResponseError') { return response.customError({ statusCode: e.statusCode, - body: e, + body: { message: e.body.error?.reason }, }); } // Case: default diff --git a/x-pack/plugins/index_lifecycle_management/server/types.ts b/x-pack/plugins/index_lifecycle_management/server/types.ts index d3a2e0124949e..e34dc8e4b1a52 100644 --- a/x-pack/plugins/index_lifecycle_management/server/types.ts +++ b/x-pack/plugins/index_lifecycle_management/server/types.ts @@ -11,7 +11,6 @@ import { LicensingPluginSetup } from '../../licensing/server'; import { IndexManagementPluginSetup } from '../../index_management/server'; import { License } from './services'; import { IndexLifecycleManagementConfig } from './config'; -import { isEsError } from './shared_imports'; export interface Dependencies { licensing: LicensingPluginSetup; @@ -23,7 +22,4 @@ export interface RouteDependencies { router: IRouter; config: IndexLifecycleManagementConfig; license: License; - lib: { - isEsError: typeof isEsError; - }; } diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts index 5ac34e5df70ec..4b110fbc4e51e 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts @@ -9,6 +9,7 @@ import createContainer from 'constate'; import { useSetState } from 'react-use'; import { TimeKey } from '../../../../common/time'; import { datemathToEpochMillis, isValidDatemath } from '../../../utils/datemath'; +import { useKibanaTimefilterTime } from '../../../hooks/use_kibana_timefilter_time'; type TimeKeyOrNull = TimeKey | null; @@ -55,7 +56,6 @@ export interface LogPositionCallbacks { updateDateRange: (newDateRage: Partial) => void; } -const DEFAULT_DATE_RANGE = { startDateExpression: 'now-1d', endDateExpression: 'now' }; const DESIRED_BUFFER_PAGES = 2; const useVisibleMidpoint = (middleKey: TimeKeyOrNull, targetPosition: TimeKeyOrNull) => { @@ -80,7 +80,17 @@ const useVisibleMidpoint = (middleKey: TimeKeyOrNull, targetPosition: TimeKeyOrN return store.currentValue; }; +const TIME_DEFAULTS = { from: 'now-1d', to: 'now' }; + export const useLogPositionState: () => LogPositionStateParams & LogPositionCallbacks = () => { + const [getTime, setTime] = useKibanaTimefilterTime(TIME_DEFAULTS); + const { from: start, to: end } = getTime(); + + const DEFAULT_DATE_RANGE = { + startDateExpression: start, + endDateExpression: end, + }; + // Flag to determine if `LogPositionState` has been fully initialized. // // When the page loads, there might be initial state in the URL. We want to @@ -110,6 +120,17 @@ export const useLogPositionState: () => LogPositionStateParams & LogPositionCall timestampsLastUpdate: Date.now(), }); + useEffect(() => { + if (isInitialized) { + if ( + TIME_DEFAULTS.from !== dateRange.startDateExpression || + TIME_DEFAULTS.to !== dateRange.endDateExpression + ) { + setTime({ from: dateRange.startDateExpression, to: dateRange.endDateExpression }); + } + } + }, [isInitialized, dateRange.startDateExpression, dateRange.endDateExpression, setTime]); + const { startKey, middleKey, endKey, pagesBeforeStart, pagesAfterEnd } = visiblePositions; const visibleMidpoint = useVisibleMidpoint(middleKey, targetPosition); diff --git a/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx b/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx new file mode 100644 index 0000000000000..e9537370f12cd --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.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 { useCallback } from 'react'; +import { useUpdateEffect, useMount } from 'react-use'; +import { useKibanaContextForPlugin } from './use_kibana'; +import { TimeRange, TimefilterContract } from '../../../../../src/plugins/data/public'; + +export const useKibanaTimefilterTime = ({ + from: fromDefault, + to: toDefault, +}: TimeRange): [typeof getTime, TimefilterContract['setTime']] => { + const { services } = useKibanaContextForPlugin(); + + const getTime = useCallback(() => { + const timefilterService = services.data.query.timefilter.timefilter; + return timefilterService.isTimeTouched() + ? timefilterService.getTime() + : { from: fromDefault, to: toDefault }; + }, [services.data.query.timefilter.timefilter, fromDefault, toDefault]); + + return [getTime, services.data.query.timefilter.timefilter.setTime]; +}; + +export const useSyncKibanaTimeFilterTime = (defaults: TimeRange, currentTimeRange: TimeRange) => { + const [, setTime] = useKibanaTimefilterTime(defaults); + + // On first mount we only want to sync time with Kibana if the derived currentTimeRange (e.g. from URL params) + // differs from our defaults. + useMount(() => { + if (defaults.from !== currentTimeRange.from || defaults.to !== currentTimeRange.to) { + setTime({ from: currentTimeRange.from, to: currentTimeRange.to }); + } + }); + + // Sync explicit changes *after* mount back to Kibana + useUpdateEffect(() => { + setTime({ from: currentTimeRange.from, to: currentTimeRange.to }); + }, [currentTimeRange.from, currentTimeRange.to, setTime]); +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results_url_state.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results_url_state.tsx index bf30f96e4b741..fa3e5eb22448f 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results_url_state.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results_url_state.tsx @@ -8,8 +8,11 @@ import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; - import { useUrlState } from '../../../utils/use_url_state'; +import { + useKibanaTimefilterTime, + useSyncKibanaTimeFilterTime, +} from '../../../hooks/use_kibana_timefilter_time'; const autoRefreshRT = rt.union([ rt.type({ @@ -29,12 +32,16 @@ const urlTimeRangeRT = rt.union([stringTimeRangeRT, rt.undefined]); const TIME_RANGE_URL_STATE_KEY = 'timeRange'; const AUTOREFRESH_URL_STATE_KEY = 'autoRefresh'; +const TIME_DEFAULTS = { from: 'now-2w', to: 'now' }; export const useLogEntryCategoriesResultsUrlState = () => { + const [getTime] = useKibanaTimefilterTime(TIME_DEFAULTS); + const { from: start, to: end } = getTime(); + const [timeRange, setTimeRange] = useUrlState({ defaultState: { - startTime: 'now-2w', - endTime: 'now', + startTime: start, + endTime: end, }, decodeUrlState: (value: unknown) => pipe(urlTimeRangeRT.decode(value), fold(constant(undefined), identity)), @@ -43,6 +50,8 @@ export const useLogEntryCategoriesResultsUrlState = () => { writeDefaultState: true, }); + useSyncKibanaTimeFilterTime(TIME_DEFAULTS, { from: timeRange.startTime, to: timeRange.endTime }); + const [autoRefresh, setAutoRefresh] = useUrlState({ defaultState: { isPaused: false, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx index 6d4495c8d9e0f..2aca3fc58d7c7 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx @@ -10,6 +10,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; import { useUrlState } from '../../../utils/use_url_state'; +import { + useKibanaTimefilterTime, + useSyncKibanaTimeFilterTime, +} from '../../../hooks/use_kibana_timefilter_time'; const autoRefreshRT = rt.union([ rt.type({ @@ -29,12 +33,16 @@ const urlTimeRangeRT = rt.union([stringTimeRangeRT, rt.undefined]); const TIME_RANGE_URL_STATE_KEY = 'timeRange'; const AUTOREFRESH_URL_STATE_KEY = 'autoRefresh'; +const TIME_DEFAULTS = { from: 'now-2w', to: 'now' }; export const useLogAnalysisResultsUrlState = () => { + const [getTime] = useKibanaTimefilterTime(TIME_DEFAULTS); + const { from: start, to: end } = getTime(); + const [timeRange, setTimeRange] = useUrlState({ defaultState: { - startTime: 'now-2w', - endTime: 'now', + startTime: start, + endTime: end, }, decodeUrlState: (value: unknown) => pipe(urlTimeRangeRT.decode(value), fold(constant(undefined), identity)), @@ -43,6 +51,8 @@ export const useLogAnalysisResultsUrlState = () => { writeDefaultState: true, }); + useSyncKibanaTimeFilterTime(TIME_DEFAULTS, { from: timeRange.startTime, to: timeRange.endTime }); + const [autoRefresh, setAutoRefresh] = useUrlState({ defaultState: { isPaused: false, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx index e07f467d5f037..e321dfb1826f7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx @@ -38,8 +38,7 @@ export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => {

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx index ca4dfcb685e7b..62158483518dd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx @@ -34,7 +34,7 @@ export const AlphaMessaging: React.FC<{}> = () => { {' – '} {' '} setIsAlphaFlyoutOpen(true)}> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_header_link.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_header_link.tsx index 9cfc08fff2f64..2fd99a88c3c89 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_header_link.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_header_link.tsx @@ -33,8 +33,8 @@ const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo(( return hasIngestManager && noticeState.settingsDataLoaded && noticeState.hasSeenNotice ? ( ) : null; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx index 919f2632c61eb..64dbb3f43312c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx @@ -61,7 +61,7 @@ const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => { title={ @@ -98,8 +98,8 @@ const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => {

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx index c11ab98a799e5..7fec1909ba22b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx @@ -27,7 +27,7 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }

= ({ onClose }) => {

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 563c4b4750c37..12d6e1c9ed0b4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -122,7 +122,7 @@ const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basep error={i18n.translate( 'xpack.ingestManager.permissionsRequestErrorMessageDescription', { - defaultMessage: 'There was a problem checking Ingest Manager permissions', + defaultMessage: 'There was a problem checking Fleet permissions', } )} /> @@ -150,13 +150,13 @@ const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basep {permissionsError === 'MISSING_SUPERUSER_ROLE' ? ( superuser }} /> ) : ( )}

@@ -176,7 +176,7 @@ const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basep title={ } error={initializationError} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index 2aa28d23cf857..4804701df23a8 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -85,7 +85,6 @@ export async function installIndexPatterns( savedObjectsClient, InstallationStatus.installed ); - // TODO: move to install package // cache all installed packages if they don't exist const packagePromises = installedPackages.map((pkg) => @@ -95,26 +94,32 @@ export async function installIndexPatterns( ); await Promise.all(packagePromises); + const packageVersionsToFetch = [...installedPackages]; if (pkgName && pkgVersion) { - // add this package to the array if it doesn't already exist - const foundPkg = installedPackages.find((pkg) => pkg.pkgName === pkgName); - // this may be removed if we add the packged to saved objects before installing index patterns - // otherwise this is a first time install - // TODO: handle update case when versions are different - if (!foundPkg) { - installedPackages.push({ pkgName, pkgVersion }); + const packageToInstall = packageVersionsToFetch.find((pkg) => pkg.pkgName === pkgName); + + if (packageToInstall) { + // set the version to the one we want to install + // if we're installing for the first time the number will be the same + // if this is an upgrade then we'll be modifying the version number to the upgrade version + packageToInstall.pkgVersion = pkgVersion; + } else { + // this will likely not happen because the saved objects should already have the package we're trying + // install which means that it should have been found in the case above + packageVersionsToFetch.push({ pkgName, pkgVersion }); } } // get each package's registry info - const installedPackagesFetchInfoPromise = installedPackages.map((pkg) => + const packageVersionsFetchInfoPromise = packageVersionsToFetch.map((pkg) => Registry.fetchInfo(pkg.pkgName, pkg.pkgVersion) ); - const installedPackagesInfo = await Promise.all(installedPackagesFetchInfoPromise); + + const packageVersionsInfo = await Promise.all(packageVersionsFetchInfoPromise); // for each index pattern type, create an index pattern const indexPatternTypes = [IndexPatternType.logs, IndexPatternType.metrics]; indexPatternTypes.forEach(async (indexPatternType) => { - // if this is an update because a package is being unisntalled (no pkgkey argument passed) and no other packages are installed, remove the index pattern + // if this is an update because a package is being uninstalled (no pkgkey argument passed) and no other packages are installed, remove the index pattern if (!pkgName && installedPackages.length === 0) { try { await savedObjectsClient.delete(INDEX_PATTERN_SAVED_OBJECT_TYPE, `${indexPatternType}-*`); @@ -125,8 +130,7 @@ export async function installIndexPatterns( } // get all data stream fields from all installed packages - const fields = await getAllDataStreamFieldsByType(installedPackagesInfo, indexPatternType); - + const fields = await getAllDataStreamFieldsByType(packageVersionsInfo, indexPatternType); const kibanaIndexPattern = createIndexPattern(indexPatternType, fields); // create or overwrite the index pattern await savedObjectsClient.create(INDEX_PATTERN_SAVED_OBJECT_TYPE, kibanaIndexPattern, { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index 74ee25eace736..2cf94e9c16079 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -89,8 +89,15 @@ export async function getPackageKeysByStatus( const allPackages = await getPackages({ savedObjectsClient }); return allPackages.reduce>((acc, pkg) => { if (pkg.status === status) { - acc.push({ pkgName: pkg.name, pkgVersion: pkg.version }); + if (pkg.status === InstallationStatus.installed) { + // if we're looking for installed packages grab the version from the saved object because `getPackages` will + // return the latest package information from the registry + acc.push({ pkgName: pkg.name, pkgVersion: pkg.savedObject.attributes.version }); + } else { + acc.push({ pkgName: pkg.name, pkgVersion: pkg.version }); + } } + return acc; }, []); } diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 46a0b56a03ec5..100527accd1b9 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -12,9 +12,10 @@ "visualizations", "dashboard", "charts", - "uiActions" + "uiActions", + "embeddable" ], - "optionalPlugins": ["embeddable", "usageCollection", "taskManager", "globalSearch"], + "optionalPlugins": ["usageCollection", "taskManager", "globalSearch"], "configPath": ["xpack", "lens"], "extraPublicDirs": ["common/constants"], "requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable"] 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 70f3f767930ec..e9e6bf43d9f1b 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -36,7 +36,7 @@ import { LensByReferenceInput, } from '../editor_frame_service/embeddable/embeddable'; import { SavedObjectReference } from '../../../../../src/core/types'; -import { mockAttributeService } from '../../../../../src/plugins/dashboard/public/mocks'; +import { mockAttributeService } from '../../../../../src/plugins/embeddable/public/mocks'; import { LensAttributeService } from '../lens_attribute_service'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 4966fce590542..d91865c21a2a6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -25,7 +25,7 @@ import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks' import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable'; import { coreMock, httpServiceMock } from '../../../../../../src/core/public/mocks'; import { IBasePath } from '../../../../../../src/core/public'; -import { AttributeService } from '../../../../../../src/plugins/dashboard/public'; +import { AttributeService } from '../../../../../../src/plugins/embeddable/public'; import { LensAttributeService } from '../../lens_attribute_service'; import { OnSaveProps } from '../../../../../../src/plugins/saved_objects/public/save_modal'; diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts index 9e1ce535d6675..9f6feee2877a8 100644 --- a/x-pack/plugins/lens/public/lens_attribute_service.ts +++ b/x-pack/plugins/lens/public/lens_attribute_service.ts @@ -6,7 +6,7 @@ import { CoreStart } from '../../../../src/core/public'; import { LensPluginStartDependencies } from './plugin'; -import { AttributeService } from '../../../../src/plugins/dashboard/public'; +import { AttributeService } from '../../../../src/plugins/embeddable/public'; import { LensSavedObjectAttributes, LensByValueInput, @@ -26,7 +26,7 @@ export function getLensAttributeService( startDependencies: LensPluginStartDependencies ): LensAttributeService { const savedObjectStore = new SavedObjectIndexStore(core.savedObjects.client); - return startDependencies.dashboard.getAttributeService< + return startDependencies.embeddable.getAttributeService< LensSavedObjectAttributes, LensByValueInput, LensByReferenceInput diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 90b0f0a2bde84..ef84ca2698ee6 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -58,7 +58,7 @@ export interface LensPluginStartDependencies { navigation: NavigationPublicPluginStart; uiActions: UiActionsStart; dashboard: DashboardStart; - embeddable?: EmbeddableStart; + embeddable: EmbeddableStart; } export class LensPlugin { private datatableVisualization: DatatableVisualization; diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index d7d9259e1539e..2c59424ec174b 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -5,7 +5,8 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { Dispatch } from 'redux'; +import { AnyAction, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import bbox from '@turf/bbox'; import uuid from 'uuid/v4'; import { multiPoint } from '@turf/helpers'; @@ -70,9 +71,12 @@ export function clearDataRequests(layer: ILayer) { } export function cancelAllInFlightRequests() { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { getLayerList(getState()).forEach((layer) => { - dispatch(clearDataRequests(layer)); + dispatch(clearDataRequests(layer)); }); }; } @@ -100,20 +104,20 @@ export function updateStyleMeta(layerId: string | null) { } function getDataRequestContext( - dispatch: Dispatch, + dispatch: ThunkDispatch, getState: () => MapStoreState, layerId: string ): DataRequestContext { return { dataFilters: getDataFilters(getState()), startLoading: (dataId: string, requestToken: symbol, meta: DataMeta) => - dispatch(startDataLoad(layerId, dataId, requestToken, meta)), + dispatch(startDataLoad(layerId, dataId, requestToken, meta)), stopLoading: (dataId: string, requestToken: symbol, data: object, meta: DataMeta) => - dispatch(endDataLoad(layerId, dataId, requestToken, data, meta)), + dispatch(endDataLoad(layerId, dataId, requestToken, data, meta)), onLoadError: (dataId: string, requestToken: symbol, errorMessage: string) => - dispatch(onDataLoadError(layerId, dataId, requestToken, errorMessage)), + dispatch(onDataLoadError(layerId, dataId, requestToken, errorMessage)), updateSourceData: (newData: unknown) => { - dispatch(updateSourceDataRequest(layerId, newData)); + dispatch(updateSourceDataRequest(layerId, newData)); }, isRequestStillActive: (dataId: string, requestToken: symbol) => { const dataRequest = getDataRequestDescriptor(getState(), layerId, dataId); @@ -128,22 +132,28 @@ function getDataRequestContext( } export function syncDataForAllLayers() { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const syncPromises = getLayerList(getState()).map((layer) => { - return dispatch(syncDataForLayer(layer)); + return dispatch(syncDataForLayer(layer)); }); await Promise.all(syncPromises); }; } function syncDataForAllJoinLayers() { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const syncPromises = getLayerList(getState()) .filter((layer) => { return 'hasJoins' in layer ? (layer as IVectorLayer).hasJoins() : false; }) .map((layer) => { - return dispatch(syncDataForLayer(layer)); + return dispatch(syncDataForLayer(layer)); }); await Promise.all(syncPromises); }; @@ -160,10 +170,13 @@ export function syncDataForLayer(layer: ILayer) { } export function syncDataForLayerId(layerId: string | null) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const layer = getLayerById(layerId, getState()); if (layer) { - dispatch(syncDataForLayer(layer)); + dispatch(syncDataForLayer(layer)); } }; } @@ -180,10 +193,13 @@ function setLayerDataLoadErrorStatus(layerId: string, errorMessage: string | nul } function startDataLoad(layerId: string, dataId: string, requestToken: symbol, meta: DataMeta) { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const layer = getLayerById(layerId, getState()); if (layer) { - dispatch(cancelRequest(layer.getPrevRequestToken(dataId))); + dispatch(cancelRequest(layer.getPrevRequestToken(dataId))); } const eventHandlers = getEventHandlers(getState()); @@ -211,7 +227,10 @@ function endDataLoad( data: object, meta: DataMeta ) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { dispatch(unregisterCancelCallback(requestToken)); const features = data && 'features' in data ? (data as FeatureCollection).features : []; @@ -231,7 +250,7 @@ function endDataLoad( }); } - dispatch(cleanTooltipStateForLayer(layerId, features)); + dispatch(cleanTooltipStateForLayer(layerId, features)); dispatch({ type: LAYER_DATA_LOAD_ENDED, layerId, @@ -244,9 +263,9 @@ function endDataLoad( // Clear any data-load errors when there is a succesful data return. // Co this on end-data-load iso at start-data-load to avoid blipping the error status between true/false. // This avoids jitter in the warning icon of the TOC when the requests continues to return errors. - dispatch(setLayerDataLoadErrorStatus(layerId, null)); + dispatch(setLayerDataLoadErrorStatus(layerId, null)); - dispatch(updateStyleMeta(layerId)); + dispatch(updateStyleMeta(layerId)); }; } @@ -256,7 +275,10 @@ function onDataLoadError( requestToken: symbol, errorMessage: string ) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { dispatch(unregisterCancelCallback(requestToken)); const eventHandlers = getEventHandlers(getState()); @@ -268,7 +290,7 @@ function onDataLoadError( }); } - dispatch(cleanTooltipStateForLayer(layerId)); + dispatch(cleanTooltipStateForLayer(layerId)); dispatch({ type: LAYER_DATA_LOAD_ERROR, data: null, @@ -277,12 +299,12 @@ function onDataLoadError( requestToken, }); - dispatch(setLayerDataLoadErrorStatus(layerId, errorMessage)); + dispatch(setLayerDataLoadErrorStatus(layerId, errorMessage)); }; } export function updateSourceDataRequest(layerId: string, newData: unknown) { - return (dispatch: Dispatch) => { + return (dispatch: ThunkDispatch) => { dispatch({ type: UPDATE_SOURCE_DATA_REQUEST, dataId: SOURCE_DATA_REQUEST_ID, @@ -290,7 +312,7 @@ export function updateSourceDataRequest(layerId: string, newData: unknown) { newData, }); - dispatch(updateStyleMeta(layerId)); + dispatch(updateStyleMeta(layerId)); }; } @@ -385,7 +407,7 @@ export function fitToDataBounds(onNoBounds?: () => void) { let lastSetQueryCallId: string = ''; export function autoFitToBounds() { - return async (dispatch: Dispatch) => { + return async (dispatch: ThunkDispatch) => { // Method can be triggered before async actions complete // Use localSetQueryCallId to only continue execution path if method has not been re-triggered. const localSetQueryCallId = uuid(); @@ -394,17 +416,17 @@ export function autoFitToBounds() { // Joins are performed on the client. // As a result, bounds for join layers must also be performed on the client. // Therefore join layers need to fetch data prior to auto fitting bounds. - await dispatch(syncDataForAllJoinLayers()); + await dispatch(syncDataForAllJoinLayers()); if (localSetQueryCallId === lastSetQueryCallId) { // In cases where there are no bounds, such as no matching documents, fitToDataBounds does not trigger setGotoWithBounds. // Ensure layer syncing occurs when setGotoWithBounds is not triggered. function onNoBounds() { if (localSetQueryCallId === lastSetQueryCallId) { - dispatch(syncDataForAllLayers()); + dispatch(syncDataForAllLayers()); } } - dispatch(fitToDataBounds(onNoBounds)); + dispatch(fitToDataBounds(onNoBounds)); } }; } diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index 19c9adfadd45a..b7a07e98437a1 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch } from 'redux'; +import { AnyAction, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { Query } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { @@ -53,7 +54,10 @@ export function trackCurrentLayerState(layerId: string) { } export function rollbackToTrackedLayerStateForSelectedLayer() { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const layerId = getSelectedLayerId(getState()); await dispatch({ type: ROLLBACK_TO_TRACKED_LAYER_STATE, @@ -62,9 +66,9 @@ export function rollbackToTrackedLayerStateForSelectedLayer() { // Ensure updateStyleMeta is triggered // syncDataForLayer may not trigger endDataLoad if no re-fetch is required - dispatch(updateStyleMeta(layerId)); + dispatch(updateStyleMeta(layerId)); - dispatch(syncDataForLayerId(layerId)); + dispatch(syncDataForLayerId(layerId)); }; } @@ -79,7 +83,10 @@ export function removeTrackedLayerStateForSelectedLayer() { } export function replaceLayerList(newLayerList: LayerDescriptor[]) { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const isMapReady = getMapReady(getState()); if (!isMapReady) { dispatch({ @@ -87,30 +94,36 @@ export function replaceLayerList(newLayerList: LayerDescriptor[]) { }); } else { getLayerListRaw(getState()).forEach(({ id }) => { - dispatch(removeLayerFromLayerList(id)); + dispatch(removeLayerFromLayerList(id)); }); } newLayerList.forEach((layerDescriptor) => { - dispatch(addLayer(layerDescriptor)); + dispatch(addLayer(layerDescriptor)); }); }; } export function cloneLayer(layerId: string) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const layer = getLayerById(layerId, getState()); if (!layer) { return; } const clonedDescriptor = await layer.cloneDescriptor(); - dispatch(addLayer(clonedDescriptor)); + dispatch(addLayer(clonedDescriptor)); }; } export function addLayer(layerDescriptor: LayerDescriptor) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const isMapReady = getMapReady(getState()); if (!isMapReady) { dispatch({ @@ -124,7 +137,7 @@ export function addLayer(layerDescriptor: LayerDescriptor) { type: ADD_LAYER, layer: layerDescriptor, }); - dispatch(syncDataForLayerId(layerDescriptor.id)); + dispatch(syncDataForLayerId(layerDescriptor.id)); const layer = createLayerInstance(layerDescriptor); const features = await layer.getLicensedFeatures(); @@ -140,20 +153,23 @@ export function addLayerWithoutDataSync(layerDescriptor: LayerDescriptor) { } export function addPreviewLayers(layerDescriptors: LayerDescriptor[]) { - return (dispatch: Dispatch) => { - dispatch(removePreviewLayers()); + return (dispatch: ThunkDispatch) => { + dispatch(removePreviewLayers()); layerDescriptors.forEach((layerDescriptor) => { - dispatch(addLayer({ ...layerDescriptor, __isPreviewLayer: true })); + dispatch(addLayer({ ...layerDescriptor, __isPreviewLayer: true })); }); }; } export function removePreviewLayers() { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { getLayerList(getState()).forEach((layer) => { if (layer.isPreviewLayer()) { - dispatch(removeLayer(layer.getId())); + dispatch(removeLayer(layer.getId())); } }); }; @@ -175,7 +191,10 @@ export function promotePreviewLayers() { } export function setLayerVisibility(layerId: string, makeVisible: boolean) { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { // if the current-state is invisible, we also want to sync data // e.g. if a layer was invisible at start-up, it won't have any data loaded const layer = getLayerById(layerId, getState()); @@ -186,7 +205,7 @@ export function setLayerVisibility(layerId: string, makeVisible: boolean) { } if (!makeVisible) { - dispatch(cleanTooltipStateForLayer(layerId)); + dispatch(cleanTooltipStateForLayer(layerId)); } dispatch({ @@ -195,28 +214,34 @@ export function setLayerVisibility(layerId: string, makeVisible: boolean) { visibility: makeVisible, }); if (makeVisible) { - dispatch(syncDataForLayerId(layerId)); + dispatch(syncDataForLayerId(layerId)); } }; } export function toggleLayerVisible(layerId: string) { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const layer = getLayerById(layerId, getState()); if (!layer) { return; } const makeVisible = !layer.isVisible(); - dispatch(setLayerVisibility(layerId, makeVisible)); + dispatch(setLayerVisibility(layerId, makeVisible)); }; } export function setSelectedLayer(layerId: string | null) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const oldSelectedLayer = getSelectedLayerId(getState()); if (oldSelectedLayer) { - await dispatch(rollbackToTrackedLayerStateForSelectedLayer()); + await dispatch(rollbackToTrackedLayerStateForSelectedLayer()); } if (layerId) { dispatch(trackCurrentLayerState(layerId)); @@ -229,12 +254,15 @@ export function setSelectedLayer(layerId: string | null) { } export function setFirstPreviewLayerToSelectedLayer() { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const firstPreviewLayer = getLayerList(getState()).find((layer) => { return layer.isPreviewLayer(); }); if (firstPreviewLayer) { - dispatch(setSelectedLayer(firstPreviewLayer.getId())); + dispatch(setSelectedLayer(firstPreviewLayer.getId())); } }; } @@ -252,7 +280,7 @@ export function updateSourceProp( value: unknown, newLayerType?: LAYER_TYPE ) { - return async (dispatch: Dispatch) => { + return async (dispatch: ThunkDispatch) => { dispatch({ type: UPDATE_SOURCE_PROP, layerId, @@ -260,20 +288,23 @@ export function updateSourceProp( value, }); if (newLayerType) { - dispatch(updateLayerType(layerId, newLayerType)); + dispatch(updateLayerType(layerId, newLayerType)); } - await dispatch(clearMissingStyleProperties(layerId)); - dispatch(syncDataForLayerId(layerId)); + await dispatch(clearMissingStyleProperties(layerId)); + dispatch(syncDataForLayerId(layerId)); }; } function updateLayerType(layerId: string, newLayerType: string) { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const layer = getLayerById(layerId, getState()); if (!layer || layer.getType() === newLayerType) { return; } - dispatch(clearDataRequests(layer)); + dispatch(clearDataRequests(layer)); dispatch({ type: UPDATE_LAYER_PROP, id: layerId, @@ -329,7 +360,7 @@ export function updateLabelsOnTop(id: string, areLabelsOnTop: boolean) { } export function setLayerQuery(id: string, query: Query) { - return (dispatch: Dispatch) => { + return (dispatch: ThunkDispatch) => { dispatch({ type: UPDATE_LAYER_PROP, id, @@ -337,32 +368,41 @@ export function setLayerQuery(id: string, query: Query) { newValue: query, }); - dispatch(syncDataForLayerId(id)); + dispatch(syncDataForLayerId(id)); }; } export function removeSelectedLayer() { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const state = getState(); const layerId = getSelectedLayerId(state); - dispatch(removeLayer(layerId)); + dispatch(removeLayer(layerId)); }; } export function removeLayer(layerId: string | null) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const state = getState(); const selectedLayerId = getSelectedLayerId(state); if (layerId === selectedLayerId) { dispatch(updateFlyout(FLYOUT_STATE.NONE)); - await dispatch(setSelectedLayer(null)); + await dispatch(setSelectedLayer(null)); } - dispatch(removeLayerFromLayerList(layerId)); + dispatch(removeLayerFromLayerList(layerId)); }; } function removeLayerFromLayerList(layerId: string | null) { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const layerGettingRemoved = getLayerById(layerId, getState()); if (!layerGettingRemoved) { return; @@ -371,7 +411,7 @@ function removeLayerFromLayerList(layerId: string | null) { layerGettingRemoved.getInFlightRequestTokens().forEach((requestToken) => { dispatch(cancelRequest(requestToken)); }); - dispatch(cleanTooltipStateForLayer(layerId!)); + dispatch(cleanTooltipStateForLayer(layerId!)); layerGettingRemoved.destroy(); dispatch({ type: REMOVE_LAYER, @@ -381,7 +421,10 @@ function removeLayerFromLayerList(layerId: string | null) { } export function clearMissingStyleProperties(layerId: string) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const targetLayer = getLayerById(layerId, getState()); if (!targetLayer || !('getFields' in targetLayer)) { return; @@ -401,13 +444,13 @@ export function clearMissingStyleProperties(layerId: string) { getMapColors(getState()) ); if (hasChanges && nextStyleDescriptor) { - dispatch(updateLayerStyle(layerId, nextStyleDescriptor)); + dispatch(updateLayerStyle(layerId, nextStyleDescriptor)); } }; } export function updateLayerStyle(layerId: string, styleDescriptor: StyleDescriptor) { - return (dispatch: Dispatch) => { + return (dispatch: ThunkDispatch) => { dispatch({ type: UPDATE_LAYER_STYLE, layerId, @@ -418,45 +461,51 @@ export function updateLayerStyle(layerId: string, styleDescriptor: StyleDescript // Ensure updateStyleMeta is triggered // syncDataForLayer may not trigger endDataLoad if no re-fetch is required - dispatch(updateStyleMeta(layerId)); + dispatch(updateStyleMeta(layerId)); // Style update may require re-fetch, for example ES search may need to retrieve field used for dynamic styling - dispatch(syncDataForLayerId(layerId)); + dispatch(syncDataForLayerId(layerId)); }; } export function updateLayerStyleForSelectedLayer(styleDescriptor: StyleDescriptor) { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const selectedLayerId = getSelectedLayerId(getState()); if (!selectedLayerId) { return; } - dispatch(updateLayerStyle(selectedLayerId, styleDescriptor)); + dispatch(updateLayerStyle(selectedLayerId, styleDescriptor)); }; } export function setJoinsForLayer(layer: ILayer, joins: JoinDescriptor[]) { - return async (dispatch: Dispatch) => { + return async (dispatch: ThunkDispatch) => { await dispatch({ type: SET_JOINS, layer, joins, }); - await dispatch(clearMissingStyleProperties(layer.getId())); - dispatch(syncDataForLayerId(layer.getId())); + await dispatch(clearMissingStyleProperties(layer.getId())); + dispatch(syncDataForLayerId(layer.getId())); }; } export function setHiddenLayers(hiddenLayerIds: string[]) { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const isMapReady = getMapReady(getState()); if (!isMapReady) { dispatch({ type: SET_WAITING_FOR_READY_HIDDEN_LAYERS, hiddenLayerIds }); } else { getLayerListRaw(getState()).forEach((layer) => - dispatch(setLayerVisibility(layer.id, !hiddenLayerIds.includes(layer.id))) + dispatch(setLayerVisibility(layer.id, !hiddenLayerIds.includes(layer.id))) ); } }; diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js index 50e583f00ae81..58621b0a70e04 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.test.js +++ b/x-pack/plugins/maps/public/actions/map_actions.test.js @@ -289,7 +289,7 @@ describe('map_actions', () => { type: 'SET_QUERY', }, ], - [undefined], // dispatch(syncDataForAllLayers()); + [undefined], // dispatch(syncDataForAllLayers()); ]); }); diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 09491e5c3a7b3..4efdd3dda344e 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import _ from 'lodash'; -import { Dispatch } from 'redux'; +import { AnyAction, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; @@ -90,7 +91,10 @@ export function updateMapSetting( } export function mapReady() { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { dispatch({ type: MAP_READY, }); @@ -102,12 +106,12 @@ export function mapReady() { if (getMapSettings(getState()).initialLocation === INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS) { waitingForMapReadyLayerList.forEach((layerDescriptor) => { - dispatch(addLayerWithoutDataSync(layerDescriptor)); + dispatch(addLayerWithoutDataSync(layerDescriptor)); }); - dispatch(autoFitToBounds()); + dispatch(autoFitToBounds()); } else { waitingForMapReadyLayerList.forEach((layerDescriptor) => { - dispatch(addLayer(layerDescriptor)); + dispatch(addLayer(layerDescriptor)); }); } }; @@ -120,7 +124,10 @@ export function mapDestroyed() { } export function mapExtentChanged(newMapConstants: { zoom: number; extent: MapExtent }) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const state = getState(); const dataFilters = getDataFilters(state); const { extent, zoom: newZoom } = newMapConstants; @@ -157,7 +164,7 @@ export function mapExtentChanged(newMapConstants: { zoom: number; extent: MapExt ...newMapConstants, }, }); - await dispatch(syncDataForAllLayers()); + await dispatch(syncDataForAllLayers()); }; } @@ -212,7 +219,10 @@ export function setQuery({ timeFilters?: TimeRange; forceRefresh?: boolean; }) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return async ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const prevQuery = getQuery(getState()); const prevTriggeredAt = prevQuery && prevQuery.queryLastTriggeredAt @@ -246,9 +256,9 @@ export function setQuery({ }); if (getMapSettings(getState()).autoFitToDataBounds) { - dispatch(autoFitToBounds()); + dispatch(autoFitToBounds()); } else { - await dispatch(syncDataForAllLayers()); + await dispatch(syncDataForAllLayers()); } }; } @@ -262,12 +272,12 @@ export function setRefreshConfig({ isPaused, interval }: MapRefreshConfig) { } export function triggerRefreshTimer() { - return async (dispatch: Dispatch) => { + return async (dispatch: ThunkDispatch) => { dispatch({ type: TRIGGER_REFRESH_TIMER, }); - await dispatch(syncDataForAllLayers()); + await dispatch(syncDataForAllLayers()); }; } diff --git a/x-pack/plugins/maps/public/actions/ui_actions.ts b/x-pack/plugins/maps/public/actions/ui_actions.ts index 8f2650beb012d..b120b69f9215d 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.ts +++ b/x-pack/plugins/maps/public/actions/ui_actions.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch } from 'redux'; +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { MapStoreState } from '../reducers/store'; import { getFlyoutDisplay } from '../selectors/ui_selectors'; import { FLYOUT_STATE } from '../reducers/ui'; @@ -35,12 +36,15 @@ export function updateFlyout(display: FLYOUT_STATE) { }; } export function openMapSettings() { - return (dispatch: Dispatch, getState: () => MapStoreState) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { const flyoutDisplay = getFlyoutDisplay(getState()); if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) { return; } - dispatch(setSelectedLayer(null)); + dispatch(setSelectedLayer(null)); dispatch(trackMapSettings()); dispatch(updateFlyout(FLYOUT_STATE.MAP_SETTINGS_PANEL)); }; diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx index 72618781902d2..9f936bdfde266 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx @@ -60,7 +60,7 @@ interface State { leftGeoField: string | null; leftEmsJoinField: string | null; leftElasticsearchJoinField: string | null; - rightIndexPatternId: string | null; + rightIndexPatternId: string; rightIndexPatternTitle: string | null; rightTermsFields: IFieldType[]; rightJoinField: string | null; @@ -79,7 +79,7 @@ export class LayerTemplate extends Component { leftGeoField: null, leftEmsJoinField: null, leftElasticsearchJoinField: null, - rightIndexPatternId: null, + rightIndexPatternId: '', rightIndexPatternTitle: null, rightTermsFields: [], rightJoinField: null, @@ -201,7 +201,7 @@ export class LayerTemplate extends Component { this.setState({ leftEmsJoinField: selectedOptions[0].value! }, this._previewLayer); }; - _onRightIndexPatternChange = (indexPatternId: string) => { + _onRightIndexPatternChange = (indexPatternId?: string) => { if (!indexPatternId) { return; } @@ -254,14 +254,14 @@ export class LayerTemplate extends Component { leftIndexPatternId: this.state.leftIndexPattern!.id, leftGeoField: this.state.leftGeoField!, leftJoinField: this.state.leftElasticsearchJoinField!, - rightIndexPatternId: this.state.rightIndexPatternId!, + rightIndexPatternId: this.state.rightIndexPatternId, rightIndexPatternTitle: this.state.rightIndexPatternTitle!, rightTermField: this.state.rightJoinField!, }) : createEmsChoroplethLayerDescriptor({ leftEmsFileId: this.state.leftEmsFileId!, leftEmsField: this.state.leftEmsJoinField!, - rightIndexPatternId: this.state.rightIndexPatternId!, + rightIndexPatternId: this.state.rightIndexPatternId, rightIndexPatternTitle: this.state.rightIndexPatternTitle!, rightTermField: this.state.rightJoinField!, }); diff --git a/x-pack/plugins/maps/public/components/geo_index_pattern_select.tsx b/x-pack/plugins/maps/public/components/geo_index_pattern_select.tsx index ae23d9d97de86..2e750e0648e53 100644 --- a/x-pack/plugins/maps/public/components/geo_index_pattern_select.tsx +++ b/x-pack/plugins/maps/public/components/geo_index_pattern_select.tsx @@ -40,7 +40,7 @@ export class GeoIndexPatternSelect extends Component { this._isMounted = true; } - _onIndexPatternSelect = async (indexPatternId: string) => { + _onIndexPatternSelect = async (indexPatternId?: string) => { if (!indexPatternId || indexPatternId.length === 0) { return; } @@ -123,7 +123,7 @@ export class GeoIndexPatternSelect extends Component { > ) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { addPreviewLayers: (layerDescriptors: LayerDescriptor[]) => { - dispatch(addPreviewLayers(layerDescriptors)); + dispatch(addPreviewLayers(layerDescriptors)); }, promotePreviewLayers: () => { - dispatch(setFirstPreviewLayerToSelectedLayer()); + dispatch(setFirstPreviewLayerToSelectedLayer()); dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); - dispatch(promotePreviewLayers()); + dispatch(promotePreviewLayers()); }, closeFlyout: () => { dispatch(updateFlyout(FLYOUT_STATE.NONE)); - dispatch(removePreviewLayers()); + dispatch(removePreviewLayers()); }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.tsx b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.tsx index 0348b38351971..b813dc55cdec9 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.tsx +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AnyAction, Dispatch } from 'redux'; +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { JoinEditor } from './join_editor'; import { getSelectedLayerJoinDescriptors } from '../../../selectors/map_selectors'; @@ -19,10 +20,10 @@ function mapStateToProps(state: MapStoreState) { }; } -function mapDispatchToProps(dispatch: Dispatch) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { onChange: (layer: ILayer, joins: JoinDescriptor[]) => { - dispatch(setJoinsForLayer(layer, joins)); + dispatch(setJoinsForLayer(layer, joins)); }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/map_container/index.ts b/x-pack/plugins/maps/public/connected_components/map_container/index.ts index c3b49f1e807eb..c4b5cc51fb210 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/index.ts +++ b/x-pack/plugins/maps/public/connected_components/map_container/index.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AnyAction, Dispatch } from 'redux'; +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { MapContainer } from './map_container'; import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors'; @@ -31,14 +32,14 @@ function mapStateToProps(state: MapStoreState) { }; } -function mapDispatchToProps(dispatch: Dispatch) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { - triggerRefreshTimer: () => dispatch(triggerRefreshTimer()), + triggerRefreshTimer: () => dispatch(triggerRefreshTimer()), exitFullScreen: () => { dispatch(exitFullScreen()); getCoreChrome().setIsVisible(true); }, - cancelAllInFlightRequests: () => dispatch(cancelAllInFlightRequests()), + cancelAllInFlightRequests: () => dispatch(cancelAllInFlightRequests()), }; } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts index 8790f6f35c574..1f28673b5e24f 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AnyAction, Dispatch } from 'redux'; +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { MapStoreState } from '../../../reducers/store'; import { fitToDataBounds } from '../../../actions'; @@ -17,10 +18,10 @@ function mapStateToProps(state: MapStoreState) { }; } -function mapDispatchToProps(dispatch: Dispatch) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { fitToBounds: () => { - dispatch(fitToDataBounds()); + dispatch(fitToDataBounds()); }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.ts index 70101f1ce6eba..a04314206aeb3 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AnyAction, Dispatch } from 'redux'; +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { ToolsControl } from './tools_control'; import { isDrawingFilter } from '../../../selectors/map_selectors'; @@ -18,13 +19,13 @@ function mapStateToProps(state: MapStoreState) { }; } -function mapDispatchToProps(dispatch: Dispatch) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { initiateDraw: (drawState: DrawState) => { - dispatch(updateDrawState(drawState)); + dispatch(updateDrawState(drawState)); }, cancelDraw: () => { - dispatch(updateDrawState(null)); + dispatch(updateDrawState(null)); }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts index 17a6dbf02b878..d8d43e1e1b27a 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AnyAction, Dispatch } from 'redux'; +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { MapStoreState } from '../../../../../../reducers/store'; import { @@ -25,19 +26,19 @@ function mapStateToProps(state: MapStoreState) { }; } -function mapDispatchToProps(dispatch: Dispatch) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { cloneLayer: (layerId: string) => { - dispatch(cloneLayer(layerId)); + dispatch(cloneLayer(layerId)); }, fitToBounds: (layerId: string) => { - dispatch(fitToLayerExtent(layerId)); + dispatch(fitToLayerExtent(layerId)); }, removeLayer: (layerId: string) => { - dispatch(removeLayer(layerId)); + dispatch(removeLayer(layerId)); }, toggleVisible: (layerId: string) => { - dispatch(toggleLayerVisible(layerId)); + dispatch(toggleLayerVisible(layerId)); }, }; } diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index b520e0cb2df01..5de018a4b59be 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -28,7 +28,7 @@ export const getFileUploadComponent = async () => { }; export const getUiSettings = () => coreStart.uiSettings; export const getIsDarkMode = () => getUiSettings().get('theme:darkMode', false); -export const getIndexPatternSelectComponent = (): any => pluginsStart.data.ui.IndexPatternSelect; +export const getIndexPatternSelectComponent = () => pluginsStart.data.ui.IndexPatternSelect; export const getHttp = () => coreStart.http; export const getTimeFilter = () => pluginsStart.data.query.timefilter.timefilter; export const getToasts = () => coreStart.notifications.toasts; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx index 91925b7f0afe1..61ebdca60707b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx @@ -12,6 +12,7 @@ import { ml } from '../../../../../services/ml_api_service'; import { useRefreshAnalyticsList } from '../../../../common'; import { JobMessages } from '../../../../../components/job_messages'; import { JobMessage } from '../../../../../../../common/types/audit_message'; +import { useToastNotificationService } from '../../../../../services/toast_notification_service'; interface Props { analyticsId: string; @@ -21,6 +22,7 @@ export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); + const toastNotificationService = useToastNotificationService(); const getMessages = useCallback(async () => { try { @@ -30,6 +32,16 @@ export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { setMessages(messagesResp); } catch (error) { setIsLoading(false); + toastNotificationService.displayErrorToast( + error, + i18n.translate( + 'xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorToastMessageTitle', + { + defaultMessage: 'Error loading job messages', + } + ) + ); + setErrorMessage( i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage', { defaultMessage: 'Messages could not be loaded', diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx index 486de90d2299c..ae745f409e0db 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -5,9 +5,12 @@ */ import React, { FC, useCallback, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { ml } from '../../../../services/ml_api_service'; import { JobMessages } from '../../../../components/job_messages'; import { JobMessage } from '../../../../../../common/types/audit_message'; +import { extractErrorMessage } from '../../../../../../common/util/errors'; +import { useToastNotificationService } from '../../../../services/toast_notification_service'; interface JobMessagesPaneProps { jobId: string; } @@ -16,17 +19,23 @@ export const JobMessagesPane: FC = ({ jobId }) => { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); + const toastNotificationService = useToastNotificationService(); const fetchMessages = async () => { setIsLoading(true); try { setMessages(await ml.jobs.jobAuditMessages(jobId)); setIsLoading(false); - } catch (e) { + } catch (error) { setIsLoading(false); - setErrorMessage(e); - // eslint-disable-next-line no-console - console.error('Job messages could not be loaded', e); + toastNotificationService.displayErrorToast( + error, + i18n.translate('xpack.ml.jobService.jobAuditMessagesErrorTitle', { + defaultMessage: 'Error loading job messages', + }) + ); + + setErrorMessage(extractErrorMessage(error)); } }; diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index f4378e29ef826..822aa4671302e 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -152,11 +152,18 @@ export function jobsProvider(client: IScopedClusterClient) { async function jobsSummary(jobIds: string[] = []) { const fullJobsList: CombinedJobWithStats[] = await createFullJobsList(); const fullJobsIds = fullJobsList.map((job) => job.job_id); - const auditMessages: AuditMessage[] = await getAuditMessagesSummary(fullJobsIds); - const auditMessagesByJob = auditMessages.reduce((acc, cur) => { - acc[cur.job_id] = cur; - return acc; - }, {} as { [id: string]: AuditMessage }); + let auditMessagesByJob: { [id: string]: AuditMessage } = {}; + + // even if there are errors getting the audit messages, we still want to show the full list + try { + const auditMessages: AuditMessage[] = await getAuditMessagesSummary(fullJobsIds); + auditMessagesByJob = auditMessages.reduce((acc, cur) => { + acc[cur.job_id] = cur; + return acc; + }, auditMessagesByJob); + } catch (e) { + // fail silently + } const deletingStr = i18n.translate('xpack.ml.models.jobService.deletingJob', { defaultMessage: 'deleting', diff --git a/x-pack/plugins/monitoring/public/services/clusters.js b/x-pack/plugins/monitoring/public/services/clusters.js index 7f772ac1e1bcd..d0ca1bc6bbde6 100644 --- a/x-pack/plugins/monitoring/public/services/clusters.js +++ b/x-pack/plugins/monitoring/public/services/clusters.js @@ -69,9 +69,11 @@ export function monitoringClustersProvider($injector) { if (Legacy.shims.isCloud) { return Promise.resolve(); } - + const globalState = $injector.get('globalState'); return $http - .get('../api/monitoring/v1/elasticsearch_settings/check/internal_monitoring') + .post('../api/monitoring/v1/elasticsearch_settings/check/internal_monitoring', { + ccs: globalState.ccs, + }) .then(({ data }) => { showInternalMonitoringToast({ legacyIndices: data.legacy_indices, diff --git a/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js b/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js index 915d2e9accf99..36e36de974342 100644 --- a/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js @@ -69,7 +69,8 @@ export class MonitoringViewBaseEuiTableController extends MonitoringViewBaseCont }); }; - this.updateData(); + // For pages where we do not fetch immediately, we want to fetch after pagination is applied + args.fetchDataImmediately === false && this.updateData(); } setPagination(page) { diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js index 0b2e833933177..ea37ff7783ad7 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js @@ -86,7 +86,7 @@ export async function getApmInfo(req, apmIndexPattern, { clusterUuid, apmUuid, s inner_hits: { name: 'first_hit', size: 1, - sort: { 'beats_stats.timestamp': 'asc' }, + sort: { 'beats_stats.timestamp': { order: 'asc', unmapped_type: 'long' } }, }, }, }, diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms.js b/x-pack/plugins/monitoring/server/lib/apm/get_apms.js index 03a395e87d860..2d59bfea72eb2 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms.js @@ -124,7 +124,7 @@ export async function getApms(req, apmIndexPattern, clusterUuid) { inner_hits: { name: 'earliest', size: 1, - sort: [{ 'beats_stats.timestamp': 'asc' }], + sort: [{ 'beats_stats.timestamp': { order: 'asc', unmapped_type: 'long' } }], }, }, sort: [ diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js index 962018f88354d..5d6c38e19bef2 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js @@ -90,7 +90,7 @@ export async function getBeatSummary( inner_hits: { name: 'first_hit', size: 1, - sort: { 'beats_stats.timestamp': 'asc' }, + sort: { 'beats_stats.timestamp': { order: 'asc', unmapped_type: 'long' } }, }, }, }, diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.js b/x-pack/plugins/monitoring/server/lib/ccs_utils.js index 96910dd86a94d..649611742df2c 100644 --- a/x-pack/plugins/monitoring/server/lib/ccs_utils.js +++ b/x-pack/plugins/monitoring/server/lib/ccs_utils.js @@ -5,7 +5,10 @@ */ import { isFunction, get } from 'lodash'; -export function appendMetricbeatIndex(config, indexPattern) { +export function appendMetricbeatIndex(config, indexPattern, bypass = false) { + if (bypass) { + return indexPattern; + } // Leverage this function to also append the dynamic metricbeat index too let mbIndex = null; // TODO: NP @@ -16,8 +19,7 @@ export function appendMetricbeatIndex(config, indexPattern) { mbIndex = get(config, 'ui.metricbeat.index'); } - const newIndexPattern = `${indexPattern},${mbIndex}`; - return newIndexPattern; + return `${indexPattern},${mbIndex}`; } /** @@ -31,7 +33,7 @@ export function appendMetricbeatIndex(config, indexPattern) { * @param {String} ccs The optional cluster-prefix to prepend. * @return {String} The index pattern with the {@code cluster} prefix appropriately prepended. */ -export function prefixIndexPattern(config, indexPattern, ccs) { +export function prefixIndexPattern(config, indexPattern, ccs, monitoringIndicesOnly = false) { let ccsEnabled = false; // TODO: NP // This function is called with both NP config and LP config @@ -42,7 +44,7 @@ export function prefixIndexPattern(config, indexPattern, ccs) { } if (!ccsEnabled || !ccs) { - return appendMetricbeatIndex(config, indexPattern); + return appendMetricbeatIndex(config, indexPattern, monitoringIndicesOnly); } const patterns = indexPattern.split(','); @@ -50,10 +52,14 @@ export function prefixIndexPattern(config, indexPattern, ccs) { // if a wildcard is used, then we also want to search the local indices if (ccs === '*') { - return appendMetricbeatIndex(config, `${prefixedPattern},${indexPattern}`); + return appendMetricbeatIndex( + config, + `${prefixedPattern},${indexPattern}`, + monitoringIndicesOnly + ); } - return appendMetricbeatIndex(config, prefixedPattern); + return appendMetricbeatIndex(config, prefixedPattern, monitoringIndicesOnly); } /** diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js index cc3dec9f085b7..efea687ef8037 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js @@ -126,7 +126,7 @@ export function buildGetIndicesQuery( inner_hits: { name: 'earliest', size: 1, - sort: [{ timestamp: 'asc' }], + sort: [{ timestamp: { order: 'asc', unmapped_type: 'long' } }], }, }, sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_ccs_availability.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_ccs_availability.js deleted file mode 100644 index ed9094dbb22fb..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_ccs_availability.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -export async function verifyCcsAvailability(req) { - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); - - const response = await callWithRequest(req, 'cluster.remoteInfo'); - for (const remoteName in response) { - if (!response.hasOwnProperty(remoteName)) { - continue; - } - const remoteInfo = response[remoteName]; - if (!remoteInfo.connected) { - throw Boom.serverUnavailable( - `There seems to be some issues with ${remoteName} ` + - `cluster. Please make sure it's connected and has at least one node.` - ); - } - } -} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js index 0035411d92f66..92e1257576d93 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js @@ -8,7 +8,6 @@ import { schema } from '@kbn/config-schema'; import { getClustersFromRequest } from '../../../../lib/cluster/get_clusters_from_request'; import { handleError } from '../../../../lib/errors'; import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; -import { verifyCcsAvailability } from '../../../../lib/elasticsearch/verify_ccs_availability'; export function clusterRoute(server) { /* @@ -34,7 +33,6 @@ export function clusterRoute(server) { }, handler: async (req) => { const config = server.config(); - await verifyCcsAvailability(req); const indexPatterns = getIndexPatterns(server, { filebeatIndexPattern: config.get('monitoring.ui.logs.index'), diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.js b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.js index 67de8d79df95d..acc40796058ee 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.js @@ -7,7 +7,6 @@ import { schema } from '@kbn/config-schema'; import { getClustersFromRequest } from '../../../../lib/cluster/get_clusters_from_request'; import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth'; -import { verifyCcsAvailability } from '../../../../lib/elasticsearch/verify_ccs_availability'; import { handleError } from '../../../../lib/errors'; import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; @@ -39,7 +38,6 @@ export function clustersRoute(server) { // the monitoring data. `try/catch` makes it a little more explicit. try { await verifyMonitoringAuth(req); - await verifyCcsAvailability(req); const indexPatterns = getIndexPatterns(server, { filebeatIndexPattern: config.get('monitoring.ui.logs.index'), }); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js index fbaac56aa7400..9f69ea1465c2d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js @@ -126,7 +126,7 @@ function buildRequest(req, config, esIndexPattern) { field: 'ccr_stats.follower_index', inner_hits: { name: 'by_shard', - sort: [{ timestamp: 'desc' }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], size: maxBucketSize, collapse: { field: 'ccr_stats.shard_id', diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js index 0a4b60b173254..92458a31c6bd8 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js @@ -59,7 +59,7 @@ async function getCcrStat(req, esIndexPattern, filters) { inner_hits: { name: 'oldest', size: 1, - sort: [{ timestamp: 'asc' }], + sort: [{ timestamp: { order: 'asc', unmapped_type: 'long' } }], }, }, }, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts index 4473d824c9e30..ef2bd8209a469 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts @@ -4,15 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ +import { schema } from '@kbn/config-schema'; import { RequestHandlerContext } from 'kibana/server'; +import { + INDEX_PATTERN_ELASTICSEARCH, + INDEX_PATTERN_KIBANA, + INDEX_PATTERN_LOGSTASH, +} from '../../../../../../common/constants'; // @ts-ignore -import { getIndexPatterns } from '../../../../../lib/cluster/get_index_patterns'; +import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; // @ts-ignore import { handleError } from '../../../../../lib/errors'; import { RouteDependencies } from '../../../../../types'; const queryBody = { size: 0, + query: { + bool: { + must: [ + { + range: { + timestamp: { + gte: 'now-12h', + }, + }, + }, + ], + }, + }, aggs: { types: { terms: { @@ -49,20 +68,31 @@ const checkLatestMonitoringIsLegacy = async (context: RequestHandlerContext, ind return counts; }; -export function internalMonitoringCheckRoute(server: unknown, npRoute: RouteDependencies) { - npRoute.router.get( +export function internalMonitoringCheckRoute( + server: { config: () => unknown }, + npRoute: RouteDependencies +) { + npRoute.router.post( { path: '/api/monitoring/v1/elasticsearch_settings/check/internal_monitoring', - validate: false, + validate: { + body: schema.object({ + ccs: schema.maybe(schema.string()), + }), + }, }, - async (context, _request, response) => { + async (context, request, response) => { try { const typeCount = { legacy_indices: 0, mb_indices: 0, }; - const { esIndexPattern, kbnIndexPattern, lsIndexPattern } = getIndexPatterns(server); + const config = server.config(); + const { ccs } = request.body; + const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs, true); + const kbnIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_KIBANA, ccs, true); + const lsIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_LOGSTASH, ccs, true); const indexCounts = await Promise.all([ checkLatestMonitoringIsLegacy(context, esIndexPattern), checkLatestMonitoringIsLegacy(context, kbnIndexPattern), diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap index e220998c24fe4..0f560612a6167 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap @@ -26,6 +26,7 @@ exports[`it renders without blowing up 1`] = ` grow={false} > { - + {this.state.collapsed ? ( { toolsRight: this.renderToolsRight(), box: { incremental: true, + 'data-test-subj': 'searchRoles', }, onChange: (query: Record) => { this.setState({ diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index db841d2a732c4..07d0d63e57059 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -30,7 +30,8 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -describe('Alerts', () => { +// FLAKY: https://github.com/elastic/kibana/issues/77957 +describe.skip('Alerts', () => { context('Closing alerts', () => { beforeEach(() => { esArchiverLoad('alerts'); 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 e262d12770d3a..91255d6110d59 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 @@ -43,7 +43,8 @@ import { openTimeline } from '../tasks/timelines'; import { OVERVIEW_URL } from '../urls/navigation'; -describe('Timeline Templates', () => { +// FLAKY: https://github.com/elastic/kibana/issues/79967 +describe.skip('Timeline Templates', () => { before(() => { cy.server(); cy.route('PATCH', '**/api/timeline').as('timeline'); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx index 60b471b1a99c4..344ca88f5ab37 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx @@ -35,7 +35,7 @@ const ConnectorCardDisplay: React.FC = ({ {listItems.length > 0 && listItems.map((item, i) => ( - + {`${item.title}: `} {item.description} diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/components/settings/jira/__mocks__/api.ts index f6d404b9b08b1..d6f18450e2130 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/cases/components/settings/jira/__mocks__/api.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GetIssueTypesProps, GetFieldsByIssueTypeProps } from '../api'; -import { IssueTypes, Fields } from '../types'; +import { GetIssueTypesProps, GetFieldsByIssueTypeProps, GetIssueTypeProps } from '../api'; +import { IssueTypes, Fields, Issues, Issue } from '../types'; +import { issues } from '../../mock'; const issueTypes = [ { @@ -31,6 +32,10 @@ const fieldsByIssueType = { }, }; +export const getIssue = async (props: GetIssueTypeProps): Promise<{ data: Issue }> => + Promise.resolve({ data: issues[0] }); +export const getIssues = async (props: GetIssueTypesProps): Promise<{ data: Issues }> => + Promise.resolve({ data: issues }); export const getIssueTypes = async (props: GetIssueTypesProps): Promise<{ data: IssueTypes }> => Promise.resolve({ data: issueTypes }); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.test.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.test.tsx index b476b88ea3db4..c4f67f860fecc 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.test.tsx @@ -8,19 +8,27 @@ import React from 'react'; import { mount } from 'enzyme'; import { omit } from 'lodash/fp'; -import { connector } from '../mock'; +import { connector, issues } from '../mock'; import { useGetIssueTypes } from './use_get_issue_types'; import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; import Fields from './fields'; +import { waitFor } from '@testing-library/dom'; +import { useGetSingleIssue } from './use_get_single_issue'; +import { useGetIssues } from './use_get_issues'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; jest.mock('../../../../common/lib/kibana'); jest.mock('./use_get_issue_types'); jest.mock('./use_get_fields_by_issue_type'); +jest.mock('./use_get_single_issue'); +jest.mock('./use_get_issues'); const useGetIssueTypesMock = useGetIssueTypes as jest.Mock; const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; +const useGetSingleIssueMock = useGetSingleIssue as jest.Mock; +const useGetIssuesMock = useGetIssues as jest.Mock; -describe('JiraParamsFields renders', () => { +describe('Jira Fields', () => { const useGetIssueTypesResponse = { isLoading: false, issueTypes: [ @@ -57,21 +65,32 @@ describe('JiraParamsFields renders', () => { }, }; + const useGetSingleIssueResponse = { + isLoading: false, + issue: { title: 'Parent Task', key: 'parentId' }, + }; + const fields = { issueType: '10006', priority: 'High', parent: null, }; + const useGetIssuesResponse = { + isLoading: false, + issues, + }; + const onChange = jest.fn(); beforeEach(() => { useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); + useGetSingleIssueMock.mockReturnValue(useGetSingleIssueResponse); jest.clearAllMocks(); }); - test('all params fields are rendered', () => { + test('all params fields are rendered - isEdit: true', () => { const wrapper = mount(); expect(wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('value')).toStrictEqual( '10006' @@ -79,6 +98,71 @@ describe('JiraParamsFields renders', () => { expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('value')).toStrictEqual( 'High' ); + expect(wrapper.find('[data-test-subj="search-parent-issues"]').first().exists()).toBeFalsy(); + }); + + test('all params fields are rendered - isEdit: false', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( + 'Issue type: Task' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( + 'Parent issue: Parent Task' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( + 'Priority: High' + ); + }); + + test('it sets parent correctly', async () => { + useGetFieldsByIssueTypeMock.mockReturnValue({ + ...useGetFieldsByIssueTypeResponse, + fields: { + ...useGetFieldsByIssueTypeResponse.fields, + parent: {}, + }, + }); + useGetIssuesMock.mockReturnValue(useGetIssuesResponse); + const wrapper = mount(); + + await waitFor(() => + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: 'parentId', value: 'parentId' }]) + ); + wrapper.update(); + expect(onChange).toHaveBeenCalledWith({ + issueType: '10006', + parent: 'parentId', + priority: 'High', + }); + }); + test('it searches parent correctly', async () => { + useGetFieldsByIssueTypeMock.mockReturnValue({ + ...useGetFieldsByIssueTypeResponse, + fields: { + ...useGetFieldsByIssueTypeResponse.fields, + parent: {}, + }, + }); + useGetSingleIssueMock.mockReturnValue({ useGetSingleIssueResponse, issue: null }); + useGetIssuesMock.mockReturnValue(useGetIssuesResponse); + const wrapper = mount(); + + await waitFor(() => + ((wrapper.find(EuiComboBox).props() as unknown) as { + onSearchChange: (a: string) => void; + }).onSearchChange('womanId') + ); + wrapper.update(); + expect(useGetIssuesMock.mock.calls[2][0].query).toEqual('womanId'); }); test('it disabled the fields when loading issue types', () => { @@ -116,7 +200,7 @@ describe('JiraParamsFields renders', () => { expect(wrapper.find('[data-test-subj="prioritySelect"]').first().exists()).toBeFalsy(); }); - test('it sets issue type correctly', async () => { + test('it sets issue type correctly', () => { const wrapper = mount(); wrapper @@ -129,7 +213,29 @@ describe('JiraParamsFields renders', () => { expect(onChange).toHaveBeenCalledWith({ issueType: '10007', parent: null, priority: null }); }); - test('it sets priority correctly', async () => { + test('it sets issue type when it comes as null', () => { + const wrapper = mount( + + ); + expect(wrapper.find('select[data-test-subj="issueTypeSelect"]').first().props().value).toEqual( + '10006' + ); + }); + + test('it sets issue type when it comes as unknown value', () => { + const wrapper = mount( + + ); + expect(wrapper.find('select[data-test-subj="issueTypeSelect"]').first().props().value).toEqual( + '10006' + ); + }); + + test('it sets priority correctly', () => { const wrapper = mount(); wrapper @@ -142,7 +248,7 @@ describe('JiraParamsFields renders', () => { expect(onChange).toHaveBeenCalledWith({ issueType: '10006', parent: null, priority: '2' }); }); - test('it resets priority when changing issue type', async () => { + test('it resets priority when changing issue type', () => { const wrapper = mount(); wrapper .find('select[data-test-subj="issueTypeSelect"]') diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.tsx index b19c1bfdd3f03..08d4da617ed03 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.tsx @@ -136,6 +136,7 @@ const JiraSettingFieldsComponent: React.FunctionComponent diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/search_issues.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/jira/search_issues.tsx index 367ed2001bd4a..0024930a4c619 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/search_issues.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/settings/jira/search_issues.tsx @@ -80,7 +80,7 @@ const SearchIssuesComponent: React.FC = ({ selectedValue, actionConnector singleSelection fullWidth placeholder={inputPlaceholder} - data-test-sub={'search-parent-issues'} + data-test-subj={'search-parent-issues'} aria-label={i18n.SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL} options={options} isLoading={isLoadingIssues || isLoadingSingleIssue} diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issues.test.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issues.test.tsx new file mode 100644 index 0000000000000..84b5fa16bb7db --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issues.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { connector as actionConnector, issues } from '../mock'; +import { useGetIssues, UseGetIssues } from './use_get_issues'; +import * as api from './api'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('./api'); + +const useKibanaMock = useKibana as jest.Mocked; + +describe('useGetIssues', () => { + const { http, notifications } = useKibanaMock().services; + beforeEach(() => jest.clearAllMocks()); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIssues({ + http, + toastNotifications: notifications.toasts, + actionConnector, + query: null, + }) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ isLoading: false, issues: [] }); + }); + }); + + test('fetch issues', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIssues({ + http, + toastNotifications: notifications.toasts, + actionConnector, + query: 'Task', + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + issues, + }); + }); + }); + + test('unhappy path', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getIssues'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIssues({ + http, + toastNotifications: notifications.toasts, + actionConnector, + query: 'oh no', + }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ isLoading: false, issues: [] }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_single_issue.test.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_single_issue.test.tsx new file mode 100644 index 0000000000000..ae8e5a3f0b652 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_single_issue.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { connector as actionConnector, issues } from '../mock'; +import { useGetSingleIssue, UseGetSingleIssue } from './use_get_single_issue'; +import * as api from './api'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('./api'); + +const useKibanaMock = useKibana as jest.Mocked; + +describe('useGetSingleIssue', () => { + const { http, notifications } = useKibanaMock().services; + beforeEach(() => jest.clearAllMocks()); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetSingleIssue({ + http, + toastNotifications: notifications.toasts, + actionConnector, + id: null, + }) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ isLoading: false, issue: null }); + }); + }); + + test('fetch issues', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetSingleIssue({ + http, + toastNotifications: notifications.toasts, + actionConnector, + id: '123', + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + issue: issues[0], + }); + }); + }); + + test('unhappy path', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getIssue'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetSingleIssue({ + http, + toastNotifications: notifications.toasts, + actionConnector, + id: '123', + }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ isLoading: false, issue: null }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/mock.ts b/x-pack/plugins/security_solution/public/cases/components/settings/mock.ts index 938335146dd9f..9b94e287c9edd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/components/settings/mock.ts @@ -11,3 +11,10 @@ export const connector = { config: {}, isPreconfigured: false, }; +export const issues = [ + { id: 'personId', title: 'Person Task', key: 'personKey' }, + { id: 'womanId', title: 'Woman Task', key: 'womanKey' }, + { id: 'manId', title: 'Man Task', key: 'manKey' }, + { id: 'cameraId', title: 'Camera Task', key: 'cameraKey' }, + { id: 'tvId', title: 'TV Task', key: 'tvKey' }, +]; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.test.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.test.tsx new file mode 100644 index 0000000000000..3a49f03155167 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 Fields from './fields'; +import { connector } from '../mock'; +import { waitFor } from '@testing-library/dom'; +import { EuiSelect } from '@elastic/eui'; + +describe('ServiceNow Fields', () => { + const fields = { severity: '1', urgency: '2', impact: '3' }; + const onChange = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + }); + it('all params fields are rendered - isEdit: true', () => { + const wrapper = mount(); + expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toEqual('1'); + expect(wrapper.find('[data-test-subj="urgencySelect"]').first().prop('value')).toEqual('2'); + expect(wrapper.find('[data-test-subj="impactSelect"]').first().prop('value')).toEqual('3'); + }); + + test('all params fields are rendered - isEdit: false', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( + 'Urgency: Medium' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( + 'Severity: High' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual('Impact: Low'); + }); + + describe('onChange calls', () => { + const wrapper = mount(); + + expect(onChange).toHaveBeenCalledWith(fields); + + const testers = ['severity', 'urgency', 'impact']; + testers.forEach((subj) => + test(`${subj.toUpperCase()}`, async () => { + await waitFor(() => { + const select = wrapper.find(EuiSelect).filter(`[data-test-subj="${subj}Select"]`)!; + select.prop('onChange')!({ + target: { + value: '9', + }, + } as React.ChangeEvent); + }); + wrapper.update(); + expect(onChange).toHaveBeenCalledWith({ + ...fields, + [subj]: '9', + }); + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx index 34e41a6cee060..8b2e24628a760 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { EuiFormRow, EuiSelect, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import * as i18n from './translations'; @@ -68,6 +68,13 @@ const ServiceNowSettingFieldsComponent: React.FunctionComponent { + onChange({ ...fields, [key]: value }); + }, + [fields, onChange] + ); + return isEdit ? ( @@ -77,9 +84,7 @@ const ServiceNowSettingFieldsComponent: React.FunctionComponent { - onChange({ ...fields, urgency: e.target.value }); - }} + onChange={(e) => onChangeCb('urgency', e.target.value)} /> @@ -92,9 +97,7 @@ const ServiceNowSettingFieldsComponent: React.FunctionComponent { - onChange({ ...fields, severity: e.target.value }); - }} + onChange={(e) => onChangeCb('severity', e.target.value)} /> @@ -106,9 +109,7 @@ const ServiceNowSettingFieldsComponent: React.FunctionComponent { - onChange({ ...fields, impact: e.target.value }); - }} + onChange={(e) => onChangeCb('impact', e.target.value)} /> diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.test.tsx index b00df5524c8b5..fdfe740e5123d 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.test.tsx @@ -111,6 +111,7 @@ describe('useGetCaseUserActions', () => { }); }); }); + describe('getPushedInfo', () => { it('Correctly marks first/last index - hasDataToPush: false', () => { const userActions = [...caseUserActions, getUserAction(['pushed'], 'push-to-service')]; @@ -226,7 +227,7 @@ describe('useGetCaseUserActions', () => { }); }); - it('Does not count connector_id update as a reason to push', () => { + it('Does not count connector update as a reason to push', () => { const userActions = [ ...caseUserActions, getUserAction(['pushed'], 'push-to-service'), @@ -246,6 +247,7 @@ describe('useGetCaseUserActions', () => { }, }); }); + it('Correctly handles multiple push actions', () => { const userActions = [ ...caseUserActions, @@ -267,6 +269,7 @@ describe('useGetCaseUserActions', () => { }, }); }); + it('Correctly handles comment update with multiple push actions', () => { const userActions = [ ...caseUserActions, @@ -298,6 +301,7 @@ describe('useGetCaseUserActions', () => { connector_name: 'other connector name', external_id: 'other_external_id', }; + const pushAction456 = { ...getUserAction(['pushed'], 'push-to-service'), newValue: JSON.stringify(push456), @@ -309,7 +313,9 @@ describe('useGetCaseUserActions', () => { getUserAction(['comment'], 'create'), pushAction456, ]; + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ hasDataToPush: true, caseServices: { @@ -342,6 +348,7 @@ describe('useGetCaseUserActions', () => { connector_name: 'other connector name', external_id: 'other_external_id', }; + const pushAction456 = { ...getUserAction(['pushed'], 'push-to-service'), newValue: JSON.stringify(push456), @@ -353,6 +360,7 @@ describe('useGetCaseUserActions', () => { getUserAction(['comment'], 'create'), pushAction456, ]; + const result = getPushedInfo(userActions, '456'); expect(result).toEqual({ hasDataToPush: false, @@ -377,5 +385,325 @@ describe('useGetCaseUserActions', () => { }, }); }); + + it('Change fields of current connector - hasDataToPush: true', () => { + const userActions = [ + ...caseUserActions, + getUserAction(['pushed'], 'push-to-service'), + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + }, + ]; + + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: true, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 3, + lastPushIndex: 3, + commentsToUpdate: [], + hasDataToPush: true, + }, + }, + }); + }); + + it('Change current connector - hasDataToPush: true', () => { + const userActions = [ + ...caseUserActions, + getUserAction(['pushed'], 'push-to-service'), + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), + newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + }, + ]; + + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: false, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 3, + lastPushIndex: 3, + commentsToUpdate: [], + hasDataToPush: false, + }, + }, + }); + }); + + it('Change connector and back - hasDataToPush: true', () => { + const userActions = [ + ...caseUserActions, + getUserAction(['pushed'], 'push-to-service'), + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), + newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + }, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), + }, + ]; + + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: false, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 3, + lastPushIndex: 3, + commentsToUpdate: [], + hasDataToPush: false, + }, + }, + }); + }); + + it('Change fields and connector after push - hasDataToPush: true', () => { + const userActions = [ + ...caseUserActions, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + }, + getUserAction(['pushed'], 'push-to-service'), + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + }, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), + }, + ]; + + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: true, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 4, + lastPushIndex: 4, + commentsToUpdate: [], + hasDataToPush: true, + }, + }, + }); + }); + + it('Change only connector after push - hasDataToPush: false', () => { + const userActions = [ + ...caseUserActions, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + }, + getUserAction(['pushed'], 'push-to-service'), + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + }, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + }, + ]; + + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: false, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 4, + lastPushIndex: 4, + commentsToUpdate: [], + hasDataToPush: false, + }, + }, + }); + }); + + it('Change connectors and fields - multiple pushes', () => { + const pushAction123 = getUserAction(['pushed'], 'push-to-service'); + const push456 = { + ...basicPushSnake, + connector_id: '456', + connector_name: 'other connector name', + external_id: 'other_external_id', + }; + + const pushAction456 = { + ...getUserAction(['pushed'], 'push-to-service'), + newValue: JSON.stringify(push456), + }; + + const userActions = [ + ...caseUserActions, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + }, + pushAction123, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + }, + pushAction456, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), + }, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), + newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + }, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), + }, + ]; + + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: true, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 4, + lastPushIndex: 4, + commentsToUpdate: [], + hasDataToPush: true, + }, + '456': { + ...basicPush, + connectorId: '456', + connectorName: 'other connector name', + externalId: 'other_external_id', + firstPushIndex: 6, + lastPushIndex: 6, + commentsToUpdate: [], + hasDataToPush: false, + }, + }, + }); + }); + + it('pushing other connectors does not count as an update', () => { + const pushAction123 = getUserAction(['pushed'], 'push-to-service'); + const push456 = { + ...basicPushSnake, + connector_id: '456', + connector_name: 'other connector name', + external_id: 'other_external_id', + }; + + const pushAction456 = { + ...getUserAction(['pushed'], 'push-to-service'), + newValue: JSON.stringify(push456), + }; + const userActions = [ + ...caseUserActions, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + }, + pushAction123, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + }, + pushAction456, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + }, + ]; + + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: false, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 4, + lastPushIndex: 4, + commentsToUpdate: [], + hasDataToPush: false, + }, + '456': { + ...basicPush, + connectorId: '456', + connectorName: 'other connector name', + externalId: 'other_external_id', + firstPushIndex: 6, + lastPushIndex: 6, + commentsToUpdate: [], + hasDataToPush: false, + }, + }, + }); + }); + + it('Changing other connectors fields does not count as an update', () => { + const userActions = [ + ...caseUserActions, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), + newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + }, + getUserAction(['pushed'], 'push-to-service'), + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), + newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + }, + { + ...getUserAction(['connector'], 'update'), + oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), + newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '3' } }), + }, + ]; + + const result = getPushedInfo(userActions, '123'); + expect(result).toEqual({ + hasDataToPush: false, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 4, + lastPushIndex: 4, + commentsToUpdate: [], + hasDataToPush: false, + }, + }, + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx index afbd1b163cec6..ccc8a69df96ee 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx @@ -12,7 +12,7 @@ import { errorToToaster, useStateToaster } from '../../common/components/toaster import { CaseFullExternalService } from '../../../../case/common/api/cases'; import { getCaseUserActions } from './api'; import * as i18n from './translations'; -import { CaseExternalService, CaseUserActions, ElasticUser } from './types'; +import { CaseConnector, CaseExternalService, CaseUserActions, ElasticUser } from './types'; import { convertToCamelCase, parseString } from './utils'; export interface CaseService extends CaseExternalService { @@ -51,27 +51,65 @@ export interface UseGetCaseUserActions extends CaseUserActionsState { const getExternalService = (value: string): CaseExternalService | null => convertToCamelCase(parseString(`${value}`)); -const connectorHasChangedFields = (action: CaseUserActions, connectorId: string): boolean => { - if (action.action !== 'update' || action.actionField[0] !== 'connector') { - return false; - } +const groupConnectorFields = ( + userActions: CaseUserActions[] +): Record> => + userActions.reduce((acc, mua) => { + if (mua.actionField[0] !== 'connector') { + return acc; + } - const oldValue = parseString(`${action.oldValue}`); - const newValue = parseString(`${action.newValue}`); + const oldValue = parseString(`${mua.oldValue}`); + const newValue = parseString(`${mua.newValue}`); + + if (oldValue == null || newValue == null) { + return acc; + } - if (oldValue == null || newValue == null) { + return { + ...acc, + [oldValue.id]: [ + ...(acc[oldValue.id] || []), + ...(oldValue.id === newValue.id ? [oldValue.fields, newValue.fields] : [oldValue.fields]), + ], + [newValue.id]: [ + ...(acc[newValue.id] || []), + ...(oldValue.id === newValue.id ? [oldValue.fields, newValue.fields] : [newValue.fields]), + ], + }; + }, {} as Record>); + +const connectorHasChangedFields = ({ + connectorFieldsBeforePush, + connectorFieldsAfterPush, + connectorId, +}: { + connectorFieldsBeforePush: Record> | null; + connectorFieldsAfterPush: Record> | null; + connectorId: string; +}): boolean => { + if (connectorFieldsAfterPush == null || connectorFieldsAfterPush[connectorId] == null) { return false; } - if (oldValue.id !== connectorId || newValue.id !== connectorId) { - return false; + const fieldsAfterPush = connectorFieldsAfterPush[connectorId]; + + if (connectorFieldsBeforePush != null && connectorFieldsBeforePush[connectorId] != null) { + const fieldsBeforePush = connectorFieldsBeforePush[connectorId]; + return !deepEqual( + fieldsBeforePush[fieldsBeforePush.length - 1], + fieldsAfterPush[fieldsAfterPush.length - 1] + ); } - if (oldValue.id !== newValue.id) { - return false; + if (fieldsAfterPush.length >= 2) { + return !deepEqual( + fieldsAfterPush[fieldsAfterPush.length - 2], + fieldsAfterPush[fieldsAfterPush.length - 1] + ); } - return !deepEqual(oldValue.fields, newValue.fields); + return false; }; interface CommentsAndIndex { @@ -86,22 +124,40 @@ export const getPushedInfo = ( caseServices: CaseServices; hasDataToPush: boolean; } => { - const hasDataToPushForConnector = (connectorId: string) => { - const userActionsForPushLessServiceUpdates = caseUserActions.filter((mua) => { - if (mua.action !== 'push-to-service') { - if (mua.action === 'update' && mua.actionField[0] === 'connector') { - return connectorHasChangedFields(mua, connectorId); - } else { - return true; - } - } else { - return connectorId === getExternalService(`${mua.newValue}`)?.connectorId; - } + const hasDataToPushForConnector = (connectorId: string): boolean => { + const caseUserActionsReversed = [...caseUserActions].reverse(); + const lastPushOfConnectorReversedIndex = caseUserActionsReversed.findIndex( + (mua) => + mua.action === 'push-to-service' && + getExternalService(`${mua.newValue}`)?.connectorId === connectorId + ); + + if (lastPushOfConnectorReversedIndex === -1) { + return true; + } + + const lastPushOfConnectorIndex = + caseUserActionsReversed.length - lastPushOfConnectorReversedIndex - 1; + + const actionsBeforePush = caseUserActions.slice(0, lastPushOfConnectorIndex); + const actionsAfterPush = caseUserActions.slice( + lastPushOfConnectorIndex + 1, + caseUserActionsReversed.length + ); + + const connectorFieldsBeforePush = groupConnectorFields(actionsBeforePush); + const connectorFieldsAfterPush = groupConnectorFields(actionsAfterPush); + + const connectorHasChanged = connectorHasChangedFields({ + connectorFieldsBeforePush, + connectorFieldsAfterPush, + connectorId, }); return ( - userActionsForPushLessServiceUpdates[userActionsForPushLessServiceUpdates.length - 1] - .action !== 'push-to-service' + actionsAfterPush.some( + (mua) => mua.actionField[0] !== 'connector' && mua.action !== 'push-to-service' + ) || connectorHasChanged ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 6c5e39b3e6aad..b2f8426413b12 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -391,6 +391,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ data-test-subj="alert-exception-builder" id-aria="alert-exception-builder" onChange={handleBuilderOnChange} + ruleType={maybeRule?.type} /> diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx index 8f00763f91411..8b5e0555b57b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx @@ -7,6 +7,8 @@ import React, { useCallback } from 'react'; import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; +import { isEqlRule } from '../../../../../common/detection_engine/utils'; +import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; import { FieldComponent } from '../../autocomplete/field'; import { OperatorComponent } from '../../autocomplete/operator'; @@ -42,6 +44,7 @@ interface EntryItemProps { onChange: (arg: BuilderEntry, i: number) => void; setErrorsExist: (arg: boolean) => void; onlyShowListOperators?: boolean; + ruleType?: Type; } export const BuilderEntryItem: React.FC = ({ @@ -52,6 +55,7 @@ export const BuilderEntryItem: React.FC = ({ onChange, setErrorsExist, onlyShowListOperators = false, + ruleType, }): JSX.Element => { const handleError = useCallback( (err: boolean): void => { @@ -145,7 +149,7 @@ export const BuilderEntryItem: React.FC = ({ entry, listType, entry.field != null && entry.field.type === 'boolean', - isFirst + isFirst && !isEqlRule(ruleType) ); const comboBox = ( void; setErrorsExist: (arg: boolean) => void; onlyShowListOperators?: boolean; + ruleType?: Type; } export const BuilderExceptionListItemComponent = React.memo( @@ -58,6 +60,7 @@ export const BuilderExceptionListItemComponent = React.memo { const handleEntryChange = useCallback( (entry: BuilderEntry, entryIndex: number): void => { @@ -122,6 +125,7 @@ export const BuilderExceptionListItemComponent = React.memo void; + ruleType?: Type; } export const ExceptionBuilderComponent = ({ @@ -85,6 +87,7 @@ export const ExceptionBuilderComponent = ({ isAndDisabled, isNestedDisabled, onChange, + ruleType, }: ExceptionBuilderProps) => { const [ { @@ -382,6 +385,7 @@ export const ExceptionBuilderComponent = ({ onChangeExceptionItem={handleExceptionItemChange} onlyShowListOperators={containsValueListEntry(exceptions)} setErrorsExist={setErrorsExist} + ruleType={ruleType} /> diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 8842503d3f3b5..ab0c566aa55c6 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -307,6 +307,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ id-aria="edit-exception-modal-builder" onChange={handleBuilderOnChange} indexPatterns={indexPatterns} + ruleType={maybeRule?.type} /> diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.test.tsx new file mode 100644 index 0000000000000..70f9eb6dd10ac --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { compactNotationParts } from './submenu'; + +describe('The Resolver node pills number presentation', () => { + describe('When given a small number under 1000', () => { + it('does not change the presentation of small numbers', () => { + expect(compactNotationParts(1)).toEqual([1, '', '']); + expect(compactNotationParts(100)).toEqual([100, '', '']); + expect(compactNotationParts(999)).toEqual([999, '', '']); + }); + }); + describe('When given a number greater or equal to 1000 but less than 1000000', () => { + it('presents the number as untis of k', () => { + expect(compactNotationParts(1000)).toEqual([1, 'k', '']); + expect(compactNotationParts(1001)).toEqual([1, 'k', '+']); + expect(compactNotationParts(10000)).toEqual([10, 'k', '']); + expect(compactNotationParts(10001)).toEqual([10, 'k', '+']); + expect(compactNotationParts(999999)).toEqual([999, 'k', '+']); + }); + }); + describe('When given a number greater or equal to 1000000 but less than 1000000000', () => { + it('presents the number as untis of M', () => { + expect(compactNotationParts(1000000)).toEqual([1, 'M', '']); + expect(compactNotationParts(1000001)).toEqual([1, 'M', '+']); + expect(compactNotationParts(10000000)).toEqual([10, 'M', '']); + expect(compactNotationParts(10000001)).toEqual([10, 'M', '+']); + expect(compactNotationParts(999999999)).toEqual([999, 'M', '+']); + }); + }); + describe('When given a number greater or equal to 1000000000 but less than 1000000000000', () => { + it('presents the number as untis of B', () => { + expect(compactNotationParts(1000000000)).toEqual([1, 'B', '']); + expect(compactNotationParts(1000000001)).toEqual([1, 'B', '+']); + expect(compactNotationParts(10000000000)).toEqual([10, 'B', '']); + expect(compactNotationParts(10000000001)).toEqual([10, 'B', '+']); + expect(compactNotationParts(999999999999)).toEqual([999, 'B', '+']); + }); + }); + describe('When given a number greater or equal to 1000000000000', () => { + it('presents the number as untis of T', () => { + expect(compactNotationParts(1000000000000)).toEqual([1, 'T', '']); + expect(compactNotationParts(1000000000001)).toEqual([1, 'T', '+']); + expect(compactNotationParts(10000000000000)).toEqual([10, 'T', '']); + expect(compactNotationParts(10000000000001)).toEqual([10, 'T', '+']); + expect(compactNotationParts(999999999999999)).toEqual([999, 'T', '+']); + expect(compactNotationParts(9999999999999990)).toEqual([9999, 'T', '+']); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index a613588aa4aa9..b5324b82faa71 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; +import { FormattedMessage } from 'react-intl'; import { EuiI18nNumber } from '@elastic/eui'; import { ResolverNodeStats } from '../../../common/endpoint/types'; import { useRelatedEventByCategoryNavigation } from './use_related_event_by_category_navigation'; @@ -39,6 +40,51 @@ interface ResolverSubmenuOption { prefix?: number | JSX.Element; } +/** + * Until browser support accomodates the `notation="compact"` feature of Intl.NumberFormat... + * exported for testing + * @param num The number to format + * @returns [mantissa ("12" in "12k+"), Scalar of compact notation (k,M,B,T), remainder indicator ("+" in "12k+")] + */ +export function compactNotationParts(num: number): [number, string, string] { + if (!Number.isFinite(num)) { + return [num, '', '']; + } + + // "scale" here will be a term indicating how many thousands there are in the number + // e.g. 1001 will be 1000, 1000002 will be 1000000, etc. + const scale = Math.pow(10, 3 * Math.min(Math.floor(Math.floor(Math.log10(num)) / 3), 4)); + + const compactPrefixTranslations = { + compactThousands: i18n.translate('xpack.securitySolution.endpoint.resolver.compactThousands', { + defaultMessage: 'k', + }), + compactMillions: i18n.translate('xpack.securitySolution.endpoint.resolver.compactMillions', { + defaultMessage: 'M', + }), + + compactBillions: i18n.translate('xpack.securitySolution.endpoint.resolver.compactBillions', { + defaultMessage: 'B', + }), + + compactTrillions: i18n.translate('xpack.securitySolution.endpoint.resolver.compactTrillions', { + defaultMessage: 'T', + }), + }; + const prefixMap: Map = new Map([ + [1, ''], + [1000, compactPrefixTranslations.compactThousands], + [1000000, compactPrefixTranslations.compactMillions], + [1000000000, compactPrefixTranslations.compactBillions], + [1000000000000, compactPrefixTranslations.compactTrillions], + ]); + const hasRemainder = i18n.translate('xpack.securitySolution.endpoint.resolver.compactOverflow', { + defaultMessage: '+', + }); + const prefix = prefixMap.get(scale) ?? ''; + return [Math.floor(num / scale), prefix, (num / scale) % 1 > Number.EPSILON ? hasRemainder : '']; +} + export type ResolverSubmenuOptionList = ResolverSubmenuOption[] | string; /** @@ -70,8 +116,17 @@ export const NodeSubMenuComponents = React.memo( return []; } else { return Object.entries(relatedEventStats.events.byCategory).map(([category, total]) => { + const [mantissa, scale, hasRemainder] = compactNotationParts(total || 0); + const prefix = ( + , scale, hasRemainder }} + /> + ); return { - prefix: , + prefix, optionTitle: category, action: () => relatedEventCallbacks(category), }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index edae2d16dde95..bcdc9e0133666 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3966,7 +3966,6 @@ "visTypeTimeseries.timeseries.dataTab.dataButtonLabel": "データ", "visTypeTimeseries.timeSeries.defaultPaletteLabel": "既定のパレット", "visTypeTimeseries.timeSeries.deleteSeriesTooltip": "数列を削除", - "visTypeTimeseries.timeSeries.filterLabel": "フィルター", "visTypeTimeseries.timeSeries.gradientLabel": "グラデーション", "visTypeTimeseries.timeSeries.hideInLegendLabel": "凡例で非表示", "visTypeTimeseries.timeSeries.labelPlaceholder": "ラベル", @@ -9101,7 +9100,6 @@ "xpack.ingestManager.epmList.updatesAvailableFilterLinkText": "更新が可能です", "xpack.ingestManager.genericActionsMenuText": "開く", "xpack.ingestManager.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "メッセージを消去", - "xpack.ingestManager.homeIntegration.tutorialDirectory.ingestManagerAppButtonText": "Ingest Managerベータを試す", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeText": "Elasticエージェントでは、シンプルかつ統合された方法で、ログ、メトリック、他の種類のデータの監視をホストに追加することができます。複数のBeatsと他のエージェントをインストールする必要はありません。このため、インフラストラクチャ全体での構成のデプロイが簡単で高速になりました。詳細については、{blogPostLink}をお読みください。", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "発表ブログ投稿", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix} ElasticエージェントおよびIngest Managerベータ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8ea4577619ac6..97caed949d4e5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3967,7 +3967,6 @@ "visTypeTimeseries.timeseries.dataTab.dataButtonLabel": "数据", "visTypeTimeseries.timeSeries.defaultPaletteLabel": "默认调色板", "visTypeTimeseries.timeSeries.deleteSeriesTooltip": "删除序列", - "visTypeTimeseries.timeSeries.filterLabel": "筛选", "visTypeTimeseries.timeSeries.gradientLabel": "渐变", "visTypeTimeseries.timeSeries.hideInLegendLabel": "在图例中隐藏", "visTypeTimeseries.timeSeries.labelPlaceholder": "标签", @@ -9107,7 +9106,6 @@ "xpack.ingestManager.epmList.updatesAvailableFilterLinkText": "有可用更新", "xpack.ingestManager.genericActionsMenuText": "打开", "xpack.ingestManager.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "关闭消息", - "xpack.ingestManager.homeIntegration.tutorialDirectory.ingestManagerAppButtonText": "试用采集管理器公测版", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeText": "通过 Elastic 代理,您能够以简单统一的方式将日志、指标和其他类型数据的监测添加到主机。不再需要安装多个 Beats 和其他代理,这样在整个基础设施中部署配置会更轻松更快速。有关更多信息,请阅读我们的{blogPostLink}。", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "公告博客", "xpack.ingestManager.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix}Elastic 代理和采集管理器公测版", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx index 2c1020ff1d5b3..e1287d299b6e9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx @@ -41,24 +41,26 @@ describe('alert_instances', () => { muted: false, }, second_instance: { - status: 'OK', + status: 'Active', muted: false, }, }, }); const instances: AlertInstanceListItem[] = [ + // active first alertInstanceToListItem( fakeNow.getTime(), alert, - 'first_instance', - alertInstanceSummary.instances.first_instance + 'second_instance', + alertInstanceSummary.instances.second_instance ), + // ok second alertInstanceToListItem( fakeNow.getTime(), alert, - 'second_instance', - alertInstanceSummary.instances.second_instance + 'first_instance', + alertInstanceSummary.instances.first_instance ), ]; @@ -176,6 +178,7 @@ describe('alertInstanceToListItem', () => { instance: 'id', status: { label: 'Active', healthColor: 'primary' }, start, + sortPriority: 0, duration: fakeNow.getTime() - fake2MinutesAgo.getTime(), isMuted: false, }); @@ -196,6 +199,7 @@ describe('alertInstanceToListItem', () => { instance: 'id', status: { label: 'Active', healthColor: 'primary' }, start, + sortPriority: 0, duration: fakeNow.getTime() - fake2MinutesAgo.getTime(), isMuted: true, }); @@ -213,6 +217,7 @@ describe('alertInstanceToListItem', () => { status: { label: 'Active', healthColor: 'primary' }, start: undefined, duration: 0, + sortPriority: 0, isMuted: false, }); }); @@ -230,6 +235,7 @@ describe('alertInstanceToListItem', () => { status: { label: 'OK', healthColor: 'subdued' }, start: undefined, duration: 0, + sortPriority: 1, isMuted: true, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index 44d65eafc2412..0648f34927db3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -11,6 +11,7 @@ import { EuiBasicTable, EuiHealth, EuiSpacer, EuiSwitch } from '@elastic/eui'; // @ts-ignore import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services'; import { padStart, chunk } from 'lodash'; +import { AlertInstanceStatusValues } from '../../../../../../alerts/common'; import { Alert, AlertInstanceSummary, AlertInstanceStatus, Pagination } from '../../../../types'; import { ComponentOpts as AlertApis, @@ -124,11 +125,12 @@ export function AlertInstances({ size: DEFAULT_SEARCH_PAGE_SIZE, }); - const alertInstances = Object.entries( - alertInstanceSummary.instances - ).map(([instanceId, instance]) => - alertInstanceToListItem(durationEpoch, alert, instanceId, instance) - ); + const alertInstances = Object.entries(alertInstanceSummary.instances) + .map(([instanceId, instance]) => + alertInstanceToListItem(durationEpoch, alert, instanceId, instance) + ) + .sort((leftInstance, rightInstance) => leftInstance.sortPriority - rightInstance.sortPriority); + const pageOfAlertInstances = getPage(alertInstances, pagination); const onMuteAction = async (instance: AlertInstanceListItem) => { @@ -185,6 +187,7 @@ export interface AlertInstanceListItem { start?: Date; duration: number; isMuted: boolean; + sortPriority: number; } const ACTIVE_LABEL = i18n.translate( @@ -210,11 +213,23 @@ export function alertInstanceToListItem( : { label: INACTIVE_LABEL, healthColor: 'subdued' }; const start = instance?.activeStartDate ? new Date(instance.activeStartDate) : undefined; const duration = start ? durationEpoch - start.valueOf() : 0; + const sortPriority = getSortPriorityByStatus(instance?.status); return { instance: instanceId, status, start, duration, isMuted, + sortPriority, }; } + +function getSortPriorityByStatus(status?: AlertInstanceStatusValues): number { + switch (status) { + case 'Active': + return 0; + case 'OK': + return 1; + } + return 2; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.test.tsx index 01791ef6147bf..73efba6929b71 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.test.tsx @@ -5,6 +5,7 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ThresholdExpression } from './threshold'; describe('threshold expression', () => { @@ -52,7 +53,7 @@ describe('threshold expression', () => { `); }); - it('renders with treshold title', () => { + it('renders with threshold title', () => { const onChangeSelectedThreshold = jest.fn(); const onChangeSelectedThresholdComparator = jest.fn(); const wrapper = shallow( @@ -65,4 +66,46 @@ describe('threshold expression', () => { ); expect(wrapper.contains('Is between')).toBeTruthy(); }); + + it('fires onChangeSelectedThreshold only when threshold actually changed', async () => { + const onChangeSelectedThreshold = jest.fn(); + const onChangeSelectedThresholdComparator = jest.fn(); + + const wrapper = mountWithIntl( + '} + threshold={[10]} + errors={{ threshold0: [], threshold1: [] }} + onChangeSelectedThreshold={onChangeSelectedThreshold} + onChangeSelectedThresholdComparator={onChangeSelectedThresholdComparator} + /> + ); + + wrapper.find('[data-test-subj="thresholdPopover"]').first().simulate('click'); + expect(wrapper.find('[data-test-subj="comparatorOptionsComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="alertThresholdInput"]').exists()).toBeTruthy(); + + wrapper + .find('[data-test-subj="alertThresholdInput"]') + .last() + .simulate('change', { target: { value: 1000 } }); + expect(onChangeSelectedThreshold).toHaveBeenCalled(); + expect(onChangeSelectedThresholdComparator).not.toHaveBeenCalled(); + + jest.clearAllMocks(); + wrapper + .find('[data-test-subj="comparatorOptionsComboBox"]') + .last() + .simulate('change', { target: { value: '<' } }); + expect(onChangeSelectedThreshold).not.toHaveBeenCalled(); + expect(onChangeSelectedThresholdComparator).toHaveBeenCalled(); + + jest.clearAllMocks(); + wrapper + .find('[data-test-subj="comparatorOptionsComboBox"]') + .last() + .simulate('change', { target: { value: 'between' } }); + expect(onChangeSelectedThreshold).toHaveBeenCalled(); + expect(onChangeSelectedThresholdComparator).toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx index fe592aadb37a5..2b5cec98b16a1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx @@ -111,12 +111,17 @@ export const ThresholdExpression = ({ data-test-subj="comparatorOptionsComboBox" value={thresholdComparator} onChange={(e) => { + const updateThresholdValue = + comparators[thresholdComparator].requiredValues !== + comparators[e.target.value].requiredValues; onChangeSelectedThresholdComparator(e.target.value); - const thresholdValues = threshold.slice( - 0, - comparators[e.target.value].requiredValues - ); - onChangeSelectedThreshold(thresholdValues); + if (updateThresholdValue) { + const thresholdValues = threshold.slice( + 0, + comparators[e.target.value].requiredValues + ); + onChangeSelectedThreshold(thresholdValues); + } }} options={Object.values(comparators).map(({ text, value }) => { return { text, value }; diff --git a/x-pack/test/accessibility/apps/roles.ts b/x-pack/test/accessibility/apps/roles.ts new file mode 100644 index 0000000000000..5abb437ba2e55 --- /dev/null +++ b/x-pack/test/accessibility/apps/roles.ts @@ -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. + */ + +// a11y tests for spaces, space selection and spacce creation and feature controls + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['security', 'settings']); + const a11y = getService('a11y'); + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const kibanaServer = getService('kibanaServer'); + + describe('Kibana roles page a11y tests', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.update({ + defaultIndex: 'logstash-*', + }); + await PageObjects.security.clickElasticsearchRoles(); + }); + + after(async () => { + await esArchiver.unload('logstash_functional'); + }); + + it('a11y test for Roles main page', async () => { + await a11y.testAppSnapshot(); + }); + + it('a11y test for searching a user', async () => { + await testSubjects.setValue('searchRoles', 'apm_user'); + await a11y.testAppSnapshot(); + await testSubjects.setValue('searchRoles', ''); + }); + + it('a11y test for toggle button for show reserved users only', async () => { + await retry.waitFor( + 'show reserved roles toggle button is visible', + async () => await testSubjects.exists('showReservedRolesSwitch') + ); + await testSubjects.click('showReservedRolesSwitch'); + await a11y.testAppSnapshot(); + await testSubjects.click('showReservedRolesSwitch'); + }); + + it('a11y test for creating a role form', async () => { + await testSubjects.click('createRoleButton'); + await a11y.testAppSnapshot(); + }); + + it('a11y test for show/hide privilege toggle button', async () => { + await testSubjects.click('showHidePrivilege'); + await a11y.testAppSnapshot(); + await testSubjects.click('showHidePrivilege'); + }); + + it('a11y test for cluster privileges drop down', async () => { + await testSubjects.click('cluster-privileges-combobox'); + await a11y.testAppSnapshot(); + }); + + it('a11y test for grant access to fields toggle switch', async () => { + await testSubjects.click('restrictFieldsQuery0'); + await a11y.testAppSnapshot(); + }); + + it('a11y test for grant read privilege access box', async () => { + await testSubjects.click('restrictFieldsQuery0'); + await a11y.testAppSnapshot(); + }); + + it('a11y test for Kibana privileges panel-space privilege panel', async () => { + await testSubjects.click('addSpacePrivilegeButton'); + await a11y.testAppSnapshot(); + }); + + it('a11y test for customize feature privilege', async () => { + await testSubjects.click('featureCategory_kibana'); + await a11y.testAppSnapshot(); + await testSubjects.click('cancelSpacePrivilegeButton'); + }); + + it('a11y test for view privilege summary panel', async () => { + await PageObjects.security.clickElasticsearchRoles(); + await testSubjects.click('edit-role-action-global_canvas_all'); + await testSubjects.click('viewPrivilegeSummaryButton'); + + await a11y.testAppSnapshot(); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.click('roleFormCancelButton'); + }); + + it('a11y test for select and delete a role in roles listing table', async () => { + await testSubjects.click('checkboxSelectRow-antimeridian_points_reader'); + await a11y.testAppSnapshot(); + await testSubjects.click('deleteRoleButton'); + await a11y.testAppSnapshot(); + await testSubjects.click('confirmModalCancelButton'); + }); + }); +} diff --git a/x-pack/test/accessibility/apps/users.ts b/x-pack/test/accessibility/apps/users.ts index b3426410962af..efdcf4f3f022f 100644 --- a/x-pack/test/accessibility/apps/users.ts +++ b/x-pack/test/accessibility/apps/users.ts @@ -46,23 +46,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('showReservedUsersSwitch'); }); - it('a11y test for toggle button for show reserved users only', async () => { - await retry.waitFor( - 'show reserved users toggle button is visible', - async () => await testSubjects.exists('showReservedUsersSwitch') - ); - await testSubjects.click('showReservedUsersSwitch'); - await a11y.testAppSnapshot(); - await testSubjects.click('showReservedUsersSwitch'); - }); - it('a11y test for create user panel', async () => { await testSubjects.click('createUserButton'); await a11y.testAppSnapshot(); }); - // https://github.com/elastic/eui/issues/2841 - it.skip('a11y test for roles drop down', async () => { + it('a11y test for roles drop down', async () => { await testSubjects.setValue('userFormUserNameInput', 'a11y'); await testSubjects.setValue('passwordInput', 'password'); await testSubjects.setValue('passwordConfirmationInput', 'password'); @@ -96,8 +85,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - // https://github.com/elastic/eui/issues/2841 - it.skip('a11y test for Change password screen', async () => { + it('a11y test for Change password screen', async () => { await PageObjects.settings.clickLinkText('deleteA11y'); await testSubjects.click('changePassword'); await a11y.testAppSnapshot(); diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 5ea5c03696479..6b3a2a9add89f 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -24,6 +24,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/advanced_settings'), require.resolve('./apps/dashboard_edit_panel'), require.resolve('./apps/users'), + require.resolve('./apps/roles'), ], pageObjects, diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js index af9ff4bf1bd9a..87a67d0b6f6e6 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js @@ -13,7 +13,7 @@ import { getPolicyPayload } from './fixtures'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const { getIndex, createIndex, cleanUp: cleanUpEsResources } = initElasticsearchHelpers(es); @@ -89,7 +89,7 @@ export default function ({ getService }) { // As there is no easy way to set the index in the ERROR state to be able to retry // we validate that the error returned *is* coming from the ES "_ilm/retry" endpoint const { body } = await retryPolicyOnIndex(indexName); - const expected = `[illegal_argument_exception] cannot retry an action for an index [${indexName}] that has not encountered an error when running a Lifecycle Policy`; + const expected = `cannot retry an action for an index [${indexName}] that has not encountered an error when running a Lifecycle Policy`; expect(body.message).to.be(expected); }); }); diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/lib/elasticsearch.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/lib/elasticsearch.js index dcfc86d1d03b9..358e54d8738f6 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/lib/elasticsearch.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/lib/elasticsearch.js @@ -15,7 +15,7 @@ export const initElasticsearchHelpers = (es) => { let templatesCreated = []; // Indices - const getIndex = (index) => es.indices.get({ index }); + const getIndex = (index) => es.indices.get({ index }).then(({ body }) => body); const createIndex = (index = getRandomString()) => { indicesCreated.push(index); @@ -54,7 +54,7 @@ export const initElasticsearchHelpers = (es) => { const cleanUp = () => Promise.all([deleteAllIndices(), deleteAllTemplates()]); - const getNodesStats = () => es.nodes.stats(); + const getNodesStats = () => es.nodes.stats().then(({ body }) => body); return { getIndex, 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 bb35f6fd96429..c859037597821 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 @@ -13,7 +13,7 @@ import { initElasticsearchHelpers } from './lib'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const { getNodesStats } = initElasticsearchHelpers(es); const { loadNodes, getNodeDetails } = registerHelpers({ supertest }); diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js index fad7fb848122d..1589baabb1ded 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js @@ -15,7 +15,7 @@ import { DEFAULT_POLICY_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const { createIndex, cleanUp: cleanUpEsResources } = initElasticsearchHelpers(es); diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js index 7fb9b35b8475e..9e0d32b96af98 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js @@ -12,7 +12,7 @@ import { registerHelpers as registerPoliciesHelpers } from './policies.helpers'; export default function ({ getService }) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const { createIndexTemplate, cleanUp: cleanUpEsResources } = initElasticsearchHelpers(es); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts index 594bd727d910f..e53013348c66b 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts @@ -35,7 +35,7 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title']`, async () => { + it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector']`, async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -48,7 +48,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); expect(body.length).to.eql(1); - expect(body[0].action_field).to.eql(['description', 'status', 'tags', 'title']); + expect(body[0].action_field).to.eql(['description', 'status', 'tags', 'title', 'connector']); expect(body[0].action).to.eql('create'); expect(body[0].old_value).to.eql(null); expect(body[0].new_value).to.eql(JSON.stringify(postCaseReq)); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index 96663f1b3fb12..16a500ddf85ee 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -131,6 +131,24 @@ export default function (providerContext: FtrProviderContext) { }); expect(resSearch.id).equal('sample_search'); }); + it('should create an index pattern with the package fields', async () => { + const resIndexPatternLogs = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'logs-*', + }); + const fields = JSON.parse(resIndexPatternLogs.attributes.fields); + const exists = fields.find((field: { name: string }) => field.name === 'logs_test_name'); + expect(exists).not.to.be(undefined); + const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); + const metricsExists = fieldsMetrics.find( + (field: { name: string }) => field.name === 'metrics_test_name' + ); + expect(metricsExists).not.to.be(undefined); + }); it('should have created the correct saved object', async function () { const res = await kibanaServer.savedObjects.get({ type: 'epm-packages', diff --git a/x-pack/test_utils/jest/config.integration.js b/x-pack/test_utils/jest/config.integration.js index 03917d34ab09c..16e05ea46e308 100644 --- a/x-pack/test_utils/jest/config.integration.js +++ b/x-pack/test_utils/jest/config.integration.js @@ -19,7 +19,10 @@ export default { ), reporters: [ 'default', - ['/../src/dev/jest/junit_reporter.js', { reportName: 'Jest Integration Tests' }], + [ + '/../packages/kbn-test/target/jest/junit_reporter', + { reportName: 'Jest Integration Tests' }, + ], ], setupFilesAfterEnv: ['/../src/dev/jest/setup/after_env.integration.js'], }; diff --git a/x-pack/test_utils/jest/config.js b/x-pack/test_utils/jest/config.js index c94fe02d2f4bd..fcd50717d3441 100644 --- a/x-pack/test_utils/jest/config.js +++ b/x-pack/test_utils/jest/config.js @@ -45,5 +45,5 @@ export default { }, transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.js$', 'packages/kbn-pm/dist/index.js'], snapshotSerializers: ['/../node_modules/enzyme-to-json/serializer'], - reporters: ['default', '/../src/dev/jest/junit_reporter.js'], + reporters: ['default', '/../packages/kbn-test/target/jest/junit_reporter'], }; diff --git a/yarn.lock b/yarn.lock index 74e0bf8eb81e2..eb12df7b50d72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1567,16 +1567,16 @@ chalk "^2.0.1" slash "^2.0.0" -"@jest/console@^26.3.0": - version "26.3.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.3.0.tgz#ed04063efb280c88ba87388b6f16427c0a85c856" - integrity sha512-/5Pn6sJev0nPUcAdpJHMVIsA8sKizL2ZkcKPE5+dJrCccks7tcM7c9wbgHudBJbxXLoTbqsHkG1Dofoem4F09w== +"@jest/console@^26.3.0", "@jest/console@^26.5.2": + version "26.5.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.5.2.tgz#94fc4865b1abed7c352b5e21e6c57be4b95604a6" + integrity sha512-lJELzKINpF1v74DXHbCRIkQ/+nUV1M+ntj+X1J8LxCgpmJZjfLmhFejiMSbjjD66fayxl5Z06tbs3HMyuik6rw== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.5.2" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^26.3.0" - jest-util "^26.3.0" + jest-message-util "^26.5.2" + jest-util "^26.5.2" slash "^3.0.0" "@jest/core@^26.4.2": @@ -1653,16 +1653,16 @@ "@jest/types" "^26.3.0" expect "^26.4.2" -"@jest/reporters@^26.4.1": - version "26.4.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.4.1.tgz#3b4d6faf28650f3965f8b97bc3d114077fb71795" - integrity sha512-aROTkCLU8++yiRGVxLsuDmZsQEKO6LprlrxtAuzvtpbIFl3eIjgIf3EUxDKgomkS25R9ZzwGEdB5weCcBZlrpQ== +"@jest/reporters@^26.4.1", "@jest/reporters@^26.5.2": + version "26.5.2" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.5.2.tgz#0f1c900c6af712b46853d9d486c9c0382e4050f6" + integrity sha512-zvq6Wvy6MmJq/0QY0YfOPb49CXKSf42wkJbrBPkeypVa8I+XDxijvFuywo6TJBX/ILPrdrlE/FW9vJZh6Rf9vA== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^26.3.0" - "@jest/test-result" "^26.3.0" - "@jest/transform" "^26.3.0" - "@jest/types" "^26.3.0" + "@jest/console" "^26.5.2" + "@jest/test-result" "^26.5.2" + "@jest/transform" "^26.5.2" + "@jest/types" "^26.5.2" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" @@ -1673,10 +1673,10 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.0.2" - jest-haste-map "^26.3.0" - jest-resolve "^26.4.0" - jest-util "^26.3.0" - jest-worker "^26.3.0" + jest-haste-map "^26.5.2" + jest-resolve "^26.5.2" + jest-util "^26.5.2" + jest-worker "^26.5.0" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" @@ -1722,6 +1722,16 @@ "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" +"@jest/test-result@^26.5.2": + version "26.5.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.5.2.tgz#cc1a44cfd4db2ecee3fb0bc4e9fe087aa54b5230" + integrity sha512-E/Zp6LURJEGSCWpoMGmCFuuEI1OWuI3hmZwmULV0GsgJBh7u0rwqioxhRU95euUuviqBDN8ruX/vP/4bwYolXw== + dependencies: + "@jest/console" "^26.5.2" + "@jest/types" "^26.5.2" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + "@jest/test-sequencer@^26.4.2": version "26.4.2" resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.4.2.tgz#58a3760a61eec758a2ce6080201424580d97cbba" @@ -1733,21 +1743,21 @@ jest-runner "^26.4.2" jest-runtime "^26.4.2" -"@jest/transform@^26.0.0", "@jest/transform@^26.3.0": - version "26.3.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.3.0.tgz#c393e0e01459da8a8bfc6d2a7c2ece1a13e8ba55" - integrity sha512-Isj6NB68QorGoFWvcOjlUhpkT56PqNIsXKR7XfvoDlCANn/IANlh8DrKAA2l2JKC3yWSMH5wS0GwuQM20w3b2A== +"@jest/transform@^26.0.0", "@jest/transform@^26.3.0", "@jest/transform@^26.5.2": + version "26.5.2" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.5.2.tgz#6a0033a1d24316a1c75184d010d864f2c681bef5" + integrity sha512-AUNjvexh+APhhmS8S+KboPz+D3pCxPvEAGduffaAJYxIFxGi/ytZQkrqcKDUU0ERBAo5R7087fyOYr2oms1seg== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^26.3.0" + "@jest/types" "^26.5.2" babel-plugin-istanbul "^6.0.0" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.4" - jest-haste-map "^26.3.0" + jest-haste-map "^26.5.2" jest-regex-util "^26.0.0" - jest-util "^26.3.0" + jest-util "^26.5.2" micromatch "^4.0.2" pirates "^4.0.1" slash "^3.0.0" @@ -1773,10 +1783,10 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@jest/types@^26.3.0": - version "26.3.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.3.0.tgz#97627bf4bdb72c55346eef98e3b3f7ddc4941f71" - integrity sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ== +"@jest/types@^26.3.0", "@jest/types@^26.5.2": + version "26.5.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.5.2.tgz#44c24f30c8ee6c7f492ead9ec3f3c62a5289756d" + integrity sha512-QDs5d0gYiyetI8q+2xWdkixVQMklReZr4ltw7GFDtb4fuJIBCE6mzj2LnitGqCuAlLap6wPyb8fpoHgwZz5fdg== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" @@ -4871,6 +4881,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/stack-utils@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" + integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== + "@types/stats-lite@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@types/stats-lite/-/stats-lite-2.2.0.tgz#bc8190bf9dfa1e16b89eaa2b433c99dff0804de9" @@ -8449,16 +8464,16 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^84.0.0: - version "84.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-84.0.0.tgz#980d72bf0990bbfbce282074d15448296c55d89d" - integrity sha512-fNX9eT1C38D1W8r5ss9ty42eDK+GIkCZVKukfeDs0XSBeKfyT0o/vbMdPr9MUkWQ+vIcFAS5hFGp9E3+xoaMeQ== +chromedriver@^86.0.0: + version "86.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-86.0.0.tgz#4b9504d5bbbcd4c6bd6d6fd1dd8247ab8cdeca67" + integrity sha512-byLJWhAfuYOmzRYPDf4asJgGDbI4gJGHa+i8dnQZGuv+6WW1nW1Fg+8zbBMOfLvGn7sKL41kVdmCEVpQHn9oyg== dependencies: "@testim/chrome-version" "^1.0.7" axios "^0.19.2" del "^5.1.0" - extract-zip "^2.0.0" - https-proxy-agent "^2.2.4" + extract-zip "^2.0.1" + https-proxy-agent "^5.0.0" mkdirp "^1.0.4" tcp-port-used "^1.0.1" @@ -15310,7 +15325,7 @@ https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: agent-base "6" debug "4" -https-proxy-agent@^2.2.1, https-proxy-agent@^2.2.4: +https-proxy-agent@^2.2.1: version "2.2.4" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== @@ -16982,21 +16997,21 @@ jest-get-type@^26.3.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== -jest-haste-map@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.3.0.tgz#c51a3b40100d53ab777bfdad382d2e7a00e5c726" - integrity sha512-DHWBpTJgJhLLGwE5Z1ZaqLTYqeODQIZpby0zMBsCU9iRFHYyhklYqP4EiG73j5dkbaAdSZhgB938mL51Q5LeZA== +jest-haste-map@^26.3.0, jest-haste-map@^26.5.2: + version "26.5.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.5.2.tgz#a15008abfc502c18aa56e4919ed8c96304ceb23d" + integrity sha512-lJIAVJN3gtO3k4xy+7i2Xjtwh8CfPcH08WYjZpe9xzveDaqGw9fVNCpkYu6M525wKFVkLmyi7ku+DxCAP1lyMA== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.5.2" "@types/graceful-fs" "^4.1.2" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.4" jest-regex-util "^26.0.0" - jest-serializer "^26.3.0" - jest-util "^26.3.0" - jest-worker "^26.3.0" + jest-serializer "^26.5.0" + jest-util "^26.5.2" + jest-worker "^26.5.0" micromatch "^4.0.2" sane "^4.0.3" walker "^1.0.7" @@ -17069,14 +17084,14 @@ jest-message-util@^24.9.0: slash "^2.0.0" stack-utils "^1.0.1" -jest-message-util@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.3.0.tgz#3bdb538af27bb417f2d4d16557606fd082d5841a" - integrity sha512-xIavRYqr4/otGOiLxLZGj3ieMmjcNE73Ui+LdSW/Y790j5acqCsAdDiLIbzHCZMpN07JOENRWX5DcU+OQ+TjTA== +jest-message-util@^26.3.0, jest-message-util@^26.5.2: + version "26.5.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.5.2.tgz#6c4c4c46dcfbabb47cd1ba2f6351559729bc11bb" + integrity sha512-Ocp9UYZ5Jl15C5PNsoDiGEk14A4NG0zZKknpWdZGoMzJuGAkVt10e97tnEVMYpk7LnQHZOfuK2j/izLBMcuCZw== dependencies: "@babel/code-frame" "^7.0.0" - "@jest/types" "^26.3.0" - "@types/stack-utils" "^1.0.1" + "@jest/types" "^26.5.2" + "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.4" micromatch "^4.0.2" @@ -17138,16 +17153,16 @@ jest-resolve@^24.9.0: jest-pnp-resolver "^1.2.1" realpath-native "^1.1.0" -jest-resolve@^26.4.0: - version "26.4.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.4.0.tgz#6dc0af7fb93e65b73fec0368ca2b76f3eb59a6d7" - integrity sha512-bn/JoZTEXRSlEx3+SfgZcJAVuTMOksYq9xe9O6s4Ekg84aKBObEaVXKOEilULRqviSLAYJldnoWV9c07kwtiCg== +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" + integrity sha512-XsPxojXGRA0CoDD7Vis59ucz2p3cQFU5C+19tz3tLEAlhYKkK77IL0cjYjikY9wXnOaBeEdm1rOgSJjbZWpcZg== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.5.2" chalk "^4.0.0" graceful-fs "^4.2.4" jest-pnp-resolver "^1.2.2" - jest-util "^26.3.0" + jest-util "^26.5.2" read-pkg-up "^7.0.1" resolve "^1.17.0" slash "^3.0.0" @@ -17210,10 +17225,10 @@ jest-runtime@^26.4.2: strip-bom "^4.0.0" yargs "^15.3.1" -jest-serializer@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.3.0.tgz#1c9d5e1b74d6e5f7e7f9627080fa205d976c33ef" - integrity sha512-IDRBQBLPlKa4flg77fqg0n/pH87tcRKwe8zxOVTWISxGpPHYkRZ1dXKyh04JOja7gppc60+soKVZ791mruVdow== +jest-serializer@^26.5.0: + version "26.5.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.5.0.tgz#f5425cc4c5f6b4b355f854b5f0f23ec6b962bc13" + integrity sha512-+h3Gf5CDRlSLdgTv7y0vPIAoLgX/SI7T4v6hy+TEXMgYbv+ztzbg5PSN6mUXAT/hXYHvZRWm+MaObVfqkhCGxA== dependencies: "@types/node" "*" graceful-fs "^4.2.4" @@ -17297,12 +17312,12 @@ jest-util@^24.0.0: slash "^2.0.0" source-map "^0.6.0" -jest-util@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.3.0.tgz#a8974b191df30e2bf523ebbfdbaeb8efca535b3e" - integrity sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw== +jest-util@^26.3.0, jest-util@^26.5.2: + version "26.5.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.5.2.tgz#8403f75677902cc52a1b2140f568e91f8ed4f4d7" + integrity sha512-WTL675bK+GSSAYgS8z9FWdCT2nccO1yTIplNLPlP0OD8tUk/H5IrWKMMRudIQQ0qp8bb4k+1Qa8CxGKq9qnYdg== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.5.2" "@types/node" "*" chalk "^4.0.0" graceful-fs "^4.2.4" @@ -17350,10 +17365,10 @@ jest-worker@^25.4.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^26.2.1, jest-worker@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" - integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw== +jest-worker@^26.2.1, jest-worker@^26.3.0, jest-worker@^26.5.0: + version "26.5.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.5.0.tgz#87deee86dbbc5f98d9919e0dadf2c40e3152fa30" + integrity sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug== dependencies: "@types/node" "*" merge-stream "^2.0.0"