From 4b9570f209c8510c50b1cd01f78006de35404baf Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 4 Nov 2020 18:01:59 +0000 Subject: [PATCH 01/57] [ML] Fixes formatting of fields in index data visualizer (#82593) --- .../ml/public/application/datavisualizer/index_based/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index bad1488166e23..301ee0366325e 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -507,7 +507,7 @@ export const Page: FC = () => { if (fieldData !== undefined) { const metricConfig: FieldVisConfig = { ...fieldData, - fieldFormat: field.format, + fieldFormat: currentIndexPattern.getFormatterForField(field), type: ML_JOB_FIELD_TYPES.NUMBER, loading: true, aggregatable: true, @@ -617,7 +617,7 @@ export const Page: FC = () => { const nonMetricConfig = { ...fieldData, - fieldFormat: field.format, + fieldFormat: currentIndexPattern.getFormatterForField(field), aggregatable: field.aggregatable, scripted: field.scripted, loading: fieldData.existsInDocs, From 44368b0b6646484ab7b82692a5f46d6cabb9f8d9 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 4 Nov 2020 11:58:15 -0700 Subject: [PATCH 02/57] [data.search] Add request handler context and asScoped pattern (#80775) * [Search] Add request context and asScoped pattern * Update docs * Unify interface for getting search client * Update examples/search_examples/server/my_strategy.ts Co-authored-by: Anton Dosov * Review feedback * Fix checks * Fix CI * Fix security search * Fix test * Fix test for reals * Fix types Co-authored-by: Anton Dosov --- ...ugins-data-public.ikibanasearchresponse.md | 2 +- ...ublic.ikibanasearchresponse.rawresponse.md | 2 + ...bana-plugin-plugins-data-public.isearch.md | 11 --- .../kibana-plugin-plugins-data-public.md | 1 - ...ns-data-public.searchinterceptor.search.md | 2 +- ...plugin-plugins-data-public.searchsource.md | 2 +- ...gins-data-public.searchsource.serialize.md | 4 +- ...ugins-data-public.searchsource.setfield.md | 2 +- ...gins-data-server.isearchstart.asscoped.md} | 6 +- ...a-server.isearchstart.getsearchstrategy.md | 4 +- ...plugin-plugins-data-server.isearchstart.md | 4 +- ...gins-data-server.isearchstrategy.cancel.md | 2 +- ...gin-plugins-data-server.isearchstrategy.md | 4 +- ...gins-data-server.isearchstrategy.search.md | 2 +- .../kibana-plugin-plugins-data-server.md | 1 + ...plugin-plugins-data-server.plugin.start.md | 8 +- ...ibana-plugin-plugins-data-server.search.md | 2 +- ...ver.searchstrategydependencies.esclient.md | 11 +++ ...-data-server.searchstrategydependencies.md | 20 ++++ ...strategydependencies.savedobjectsclient.md | 11 +++ ...chstrategydependencies.uisettingsclient.md | 11 +++ .../search_examples/server/my_strategy.ts | 10 +- .../server/routes/server_search_route.ts | 7 +- .../data/common/search/aggs/agg_config.ts | 3 +- .../data/common/search/es_search/types.ts | 16 ---- .../search_source/search_source.test.ts | 4 +- .../search/search_source/search_source.ts | 22 ++--- src/plugins/data/common/search/types.ts | 33 +++++-- src/plugins/data/public/index.ts | 1 - src/plugins/data/public/public.api.md | 40 ++++---- src/plugins/data/public/search/index.ts | 1 - .../data/public/search/search_interceptor.ts | 2 +- .../data/public/search/search_service.ts | 15 +-- src/plugins/data/server/index.ts | 1 + .../es_search/es_search_strategy.test.ts | 22 ++--- .../search/es_search/es_search_strategy.ts | 6 +- src/plugins/data/server/search/index.ts | 6 +- src/plugins/data/server/search/mocks.ts | 5 +- .../data/server/search/routes/search.test.ts | 53 +++++------ .../data/server/search/routes/search.ts | 21 +---- .../data/server/search/search_service.ts | 93 ++++++++++--------- src/plugins/data/server/search/types.ts | 27 ++++-- src/plugins/data/server/server.api.md | 65 ++++++++----- .../server/routes/validate_es.ts | 4 +- .../server/series_functions/es/es.test.js | 17 +--- .../server/series_functions/es/index.js | 3 +- .../series_functions/fixtures/tl_config.js | 14 +-- .../abstract_search_strategy.test.js | 19 +--- .../strategies/abstract_search_strategy.ts | 8 +- .../public/search/search_interceptor.ts | 2 +- .../server/search/eql_search_strategy.test.ts | 48 +++++----- .../server/search/eql_search_strategy.ts | 10 +- .../server/search/es_search_strategy.test.ts | 44 ++++----- .../server/search/es_search_strategy.ts | 48 ++++------ .../search_strategy/index_fields/index.ts | 5 +- .../security_solution/index.ts | 8 +- .../server/search_strategy/timeline/index.ts | 8 +- 57 files changed, 386 insertions(+), 417 deletions(-) delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearch.md rename docs/development/plugins/data/server/{kibana-plugin-plugins-data-server.isearchstart.search.md => kibana-plugin-plugins-data-server.isearchstart.asscoped.md} (54%) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.esclient.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.savedobjectsclient.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.uisettingsclient.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md index 159dc8f4ada18..1d3e0c08dfc18 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md @@ -18,6 +18,6 @@ export interface IKibanaSearchResponse | [isPartial](./kibana-plugin-plugins-data-public.ikibanasearchresponse.ispartial.md) | boolean | Indicates whether the results returned are complete or partial | | [isRunning](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrunning.md) | boolean | Indicates whether search is still in flight | | [loaded](./kibana-plugin-plugins-data-public.ikibanasearchresponse.loaded.md) | number | If relevant to the search strategy, return a loaded number that represents how progress is indicated. | -| [rawResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md) | RawResponse | | +| [rawResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md) | RawResponse | The raw response returned by the internal search method (usually the raw ES response) | | [total](./kibana-plugin-plugins-data-public.ikibanasearchresponse.total.md) | number | If relevant to the search strategy, return a total number that represents how progress is indicated. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md index 865c7d795801b..5857911259e12 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md @@ -4,6 +4,8 @@ ## IKibanaSearchResponse.rawResponse property +The raw response returned by the internal search method (usually the raw ES response) + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearch.md deleted file mode 100644 index 79f667a70571a..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearch.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearch](./kibana-plugin-plugins-data-public.isearch.md) - -## ISearch type - -Signature: - -```typescript -export declare type ISearch = (request: IKibanaSearchRequest, options?: ISearchOptions) => Observable; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index ac6923fd12f96..255a9947858f6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -159,7 +159,6 @@ | [IndexPatternsContract](./kibana-plugin-plugins-data-public.indexpatternscontract.md) | | | [IndexPatternSelectProps](./kibana-plugin-plugins-data-public.indexpatternselectprops.md) | | | [InputTimeRange](./kibana-plugin-plugins-data-public.inputtimerange.md) | | -| [ISearch](./kibana-plugin-plugins-data-public.isearch.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | | [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | search source interface | | [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md index 672ff5065c456..61f8eeb973f4c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md @@ -23,5 +23,5 @@ search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable` -`Observalbe` emitting the search response or an error. +`Observable` emitting the search response or an error. diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md index 87346f81b13e2..548fa66e6e518 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md @@ -42,7 +42,7 @@ export declare class SearchSource | [getSerializedFields()](./kibana-plugin-plugins-data-public.searchsource.getserializedfields.md) | | serializes search source fields (which can later be passed to [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md)) | | [onRequestStart(handler)](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) | | Add a handler that will be notified whenever requests start | | [serialize()](./kibana-plugin-plugins-data-public.searchsource.serialize.md) | | Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object.The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named kibanaSavedObjectMeta.searchSourceJSON.index and kibanaSavedObjectMeta.searchSourceJSON.filter[<number>].meta.index.Using createSearchSource, the instance can be re-created. | -| [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | sets value to a single search source feild | +| [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | sets value to a single search source field | | [setFields(newFields)](./kibana-plugin-plugins-data-public.searchsource.setfields.md) | | Internal, do not use. Overrides all search source fields with the new field array. | | [setParent(parent, options)](./kibana-plugin-plugins-data-public.searchsource.setparent.md) | | Set a searchSource that this source should inherit from | | [setPreferredSearchStrategyId(searchStrategyId)](./kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md) | | internal, dont use | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md index 496e1ae9677d8..3bc2a20541777 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md @@ -15,13 +15,13 @@ Using `createSearchSource`, the instance can be re-created. ```typescript serialize(): { searchSourceJSON: string; - references: import("../../../../../core/types").SavedObjectReference[]; + references: import("src/core/server").SavedObjectReference[]; }; ``` Returns: `{ searchSourceJSON: string; - references: import("../../../../../core/types").SavedObjectReference[]; + references: import("src/core/server").SavedObjectReference[]; }` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md index 22619940f1589..e96a35d8deee9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md @@ -4,7 +4,7 @@ ## SearchSource.setField() method -sets value to a single search source feild +sets value to a single search source field Signature: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.asscoped.md similarity index 54% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.asscoped.md index 98ea175aaaea7..f97cc22a53001 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.asscoped.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) > [search](./kibana-plugin-plugins-data-server.isearchstart.search.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) > [asScoped](./kibana-plugin-plugins-data-server.isearchstart.asscoped.md) -## ISearchStart.search property +## ISearchStart.asScoped property Signature: ```typescript -search: ISearchStrategy['search']; +asScoped: (request: KibanaRequest) => ISearchClient; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md index 398ea21641942..9820e281c3f93 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md @@ -4,10 +4,10 @@ ## ISearchStart.getSearchStrategy property -Get other registered search strategies. For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. +Get other registered search strategies by name (or, by default, the Elasticsearch strategy). For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. Signature: ```typescript -getSearchStrategy: (name: string) => ISearchStrategy; +getSearchStrategy: (name?: string) => ISearchStrategy; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md index b99c5f0f10a9e..771b529f23824 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md @@ -15,7 +15,7 @@ export interface ISearchStartAggsStart | | -| [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | (name: string) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse> | Get other registered search strategies. For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. | -| [search](./kibana-plugin-plugins-data-server.isearchstart.search.md) | ISearchStrategy['search'] | | +| [asScoped](./kibana-plugin-plugins-data-server.isearchstart.asscoped.md) | (request: KibanaRequest) => ISearchClient | | +| [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | (name?: string) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse> | Get other registered search strategies by name (or, by default, the Elasticsearch strategy). For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. | | [searchSource](./kibana-plugin-plugins-data-server.isearchstart.searchsource.md) | {
asScoped: (request: KibanaRequest) => Promise<ISearchStartSearchSource>;
} | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.cancel.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.cancel.md index 34903697090ea..709d9bb7be9e5 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.cancel.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.cancel.md @@ -7,5 +7,5 @@ Signature: ```typescript -cancel?: (context: RequestHandlerContext, id: string) => Promise; +cancel?: (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md index 6dd95da2be3c1..c9f4c886735a7 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md @@ -16,6 +16,6 @@ export interface ISearchStrategy(context: RequestHandlerContext, id: string) => Promise<void> | | -| [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable<SearchStrategyResponse> | | +| [cancel](./kibana-plugin-plugins-data-server.isearchstrategy.cancel.md) | (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise<void> | | +| [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies) => Observable<SearchStrategyResponse> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md index 84b90ae23f916..266995f2ec82c 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md @@ -7,5 +7,5 @@ Signature: ```typescript -search: (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable; +search: (request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies) => Observable; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 653adda6f2ac8..82d0a5a3182b9 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -58,6 +58,7 @@ | [PluginSetup](./kibana-plugin-plugins-data-server.pluginsetup.md) | | | [PluginStart](./kibana-plugin-plugins-data-server.pluginstart.md) | | | [RefreshInterval](./kibana-plugin-plugins-data-server.refreshinterval.md) | | +| [SearchStrategyDependencies](./kibana-plugin-plugins-data-server.searchstrategydependencies.md) | | | [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) | | | [TabbedAggColumn](./kibana-plugin-plugins-data-server.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-server.tabbedtable.md) | \* | 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 660644ae73255..03d3485fce9ee 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 @@ -9,10 +9,10 @@ ```typescript start(core: CoreStart): { fieldFormats: { - fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; + fieldFormatServiceFactory: (uiSettings: import("src/core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }; @@ -28,10 +28,10 @@ start(core: CoreStart): { `{ fieldFormats: { - fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; + fieldFormatServiceFactory: (uiSettings: import("src/core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md index dcb71f01f350e..e2a71a7badd4d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md @@ -12,7 +12,7 @@ search: { utils: { doSearch: (searchMethod: () => Promise, abortSignal?: AbortSignal | undefined) => import("rxjs").Observable; shimAbortSignal: >(promise: T, signal: AbortSignal | undefined) => T; - trackSearchStatus: = import("./search").IEsSearchResponse>>(logger: import("@kbn/logging/target/logger").Logger, usage?: import("./search").SearchUsage | undefined) => import("rxjs").UnaryFunction, import("rxjs").Observable>; + trackSearchStatus: = import("./search").IEsSearchResponse>>(logger: import("src/core/server").Logger, usage?: import("./search").SearchUsage | undefined) => import("rxjs").UnaryFunction, import("rxjs").Observable>; includeTotalLoaded: () => import("rxjs").OperatorFunction>, { total: number; loaded: number; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.esclient.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.esclient.md new file mode 100644 index 0000000000000..d205021e10954 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.esclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchStrategyDependencies](./kibana-plugin-plugins-data-server.searchstrategydependencies.md) > [esClient](./kibana-plugin-plugins-data-server.searchstrategydependencies.esclient.md) + +## SearchStrategyDependencies.esClient property + +Signature: + +```typescript +esClient: IScopedClusterClient; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.md new file mode 100644 index 0000000000000..be95fb04a2c4f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchStrategyDependencies](./kibana-plugin-plugins-data-server.searchstrategydependencies.md) + +## SearchStrategyDependencies interface + +Signature: + +```typescript +export interface SearchStrategyDependencies +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [esClient](./kibana-plugin-plugins-data-server.searchstrategydependencies.esclient.md) | IScopedClusterClient | | +| [savedObjectsClient](./kibana-plugin-plugins-data-server.searchstrategydependencies.savedobjectsclient.md) | SavedObjectsClientContract | | +| [uiSettingsClient](./kibana-plugin-plugins-data-server.searchstrategydependencies.uisettingsclient.md) | IUiSettingsClient | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.savedobjectsclient.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.savedobjectsclient.md new file mode 100644 index 0000000000000..f159a863312a4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.savedobjectsclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchStrategyDependencies](./kibana-plugin-plugins-data-server.searchstrategydependencies.md) > [savedObjectsClient](./kibana-plugin-plugins-data-server.searchstrategydependencies.savedobjectsclient.md) + +## SearchStrategyDependencies.savedObjectsClient property + +Signature: + +```typescript +savedObjectsClient: SavedObjectsClientContract; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.uisettingsclient.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.uisettingsclient.md new file mode 100644 index 0000000000000..38a33e41c396f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.uisettingsclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchStrategyDependencies](./kibana-plugin-plugins-data-server.searchstrategydependencies.md) > [uiSettingsClient](./kibana-plugin-plugins-data-server.searchstrategydependencies.uisettingsclient.md) + +## SearchStrategyDependencies.uiSettingsClient property + +Signature: + +```typescript +uiSettingsClient: IUiSettingsClient; +``` diff --git a/examples/search_examples/server/my_strategy.ts b/examples/search_examples/server/my_strategy.ts index 26e7056cdd787..d5b19f0619bbc 100644 --- a/examples/search_examples/server/my_strategy.ts +++ b/examples/search_examples/server/my_strategy.ts @@ -24,18 +24,18 @@ import { IMyStrategyResponse, IMyStrategyRequest } from '../common'; export const mySearchStrategyProvider = ( data: PluginStart ): ISearchStrategy => { - const es = data.search.getSearchStrategy('es'); + const es = data.search.getSearchStrategy(); return { - search: (request, options, context) => - es.search(request, options, context).pipe( + search: (request, options, deps) => + es.search(request, options, deps).pipe( map((esSearchRes) => ({ ...esSearchRes, cool: request.get_cool ? 'YES' : 'NOPE', })) ), - cancel: async (context, id) => { + cancel: async (id, options, deps) => { if (es.cancel) { - es.cancel(context, id); + await es.cancel(id, options, deps); } }, }; diff --git a/examples/search_examples/server/routes/server_search_route.ts b/examples/search_examples/server/routes/server_search_route.ts index 21ae38b99f3d2..dae423aeecc8c 100644 --- a/examples/search_examples/server/routes/server_search_route.ts +++ b/examples/search_examples/server/routes/server_search_route.ts @@ -39,8 +39,8 @@ export function registerServerSearchRoute(router: IRouter, data: DataPluginStart // Run a synchronous search server side, by enforcing a high keepalive and waiting for completion. // If you wish to run the search with polling (in basic+), you'd have to poll on the search API. // Please reach out to the @app-arch-team if you need this to be implemented. - const res = await data.search - .search( + const res = await context + .search!.search( { params: { index, @@ -57,8 +57,7 @@ export function registerServerSearchRoute(router: IRouter, data: DataPluginStart keepAlive: '5m', }, } as IEsSearchRequest, - {}, - context + {} ) .toPromise(); diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 910c79f5dd0d7..8ca27755e3dda 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -21,13 +21,12 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { Assign, Ensure } from '@kbn/utility-types'; -import { ISearchSource } from 'src/plugins/data/public'; +import { ISearchOptions, ISearchSource } from 'src/plugins/data/public'; import { ExpressionAstFunction, ExpressionAstArgument, SerializedFieldFormat, } from 'src/plugins/expressions/common'; -import { ISearchOptions } from '../es_search'; import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; diff --git a/src/plugins/data/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts index 32a140db81b8b..7d81cf42e1866 100644 --- a/src/plugins/data/common/search/es_search/types.ts +++ b/src/plugins/data/common/search/es_search/types.ts @@ -22,22 +22,6 @@ import { IKibanaSearchRequest, IKibanaSearchResponse } from '../types'; export const ES_SEARCH_STRATEGY = 'es'; -export interface ISearchOptions { - /** - * An `AbortSignal` that allows the caller of `search` to abort a search request. - */ - abortSignal?: AbortSignal; - /** - * Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. - */ - strategy?: string; - - /** - * A session ID, grouping multiple search requests into a single session. - */ - sessionId?: string; -} - export type ISearchRequestParams> = { trackTotalHits?: boolean; } & Search; diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 00e06663e998e..98d66310c040e 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, of } from 'rxjs'; import { IndexPattern } from '../../index_patterns'; import { GetConfigFn } from '../../types'; import { fetchSoon } from './legacy'; @@ -53,7 +53,7 @@ describe('SearchSource', () => { let searchSourceDependencies: SearchSourceDependencies; beforeEach(() => { - mockSearchMethod = jest.fn().mockResolvedValue({ rawResponse: '' }); + mockSearchMethod = jest.fn().mockReturnValue(of({ rawResponse: '' })); searchSourceDependencies = { getConfig: jest.fn(), diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index c5765278ee639..9bc65ca341980 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -71,12 +71,12 @@ import { setWith } from '@elastic/safer-lodash-set'; import { uniqueId, uniq, extend, pick, difference, omit, isObject, keys, isFunction } from 'lodash'; +import { map } from 'rxjs/operators'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; import { IIndexPattern } from '../../index_patterns'; -import { IEsSearchRequest, IEsSearchResponse, ISearchOptions } from '../../search'; -import type { IKibanaSearchRequest, IKibanaSearchResponse } from '../types'; +import { ISearchGeneric, ISearchOptions } from '../..'; import type { ISearchSource, SearchSourceOptions, SearchSourceFields } from './types'; import { FetchHandlers, RequestFailure, getSearchParamsFromRequest, SearchRequest } from './fetch'; @@ -102,15 +102,7 @@ export const searchSourceRequiredUiSettings = [ ]; export interface SearchSourceDependencies extends FetchHandlers { - // Types are nearly identical to ISearchGeneric, except we are making - // search options required here and returning a promise instead of observable. - search: < - SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, - SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse - >( - request: SearchStrategyRequest, - options: ISearchOptions - ) => Promise; + search: ISearchGeneric; } /** @public **/ @@ -144,7 +136,7 @@ export class SearchSource { } /** - * sets value to a single search source feild + * sets value to a single search source field * @param field: field name * @param value: value for the field */ @@ -319,9 +311,9 @@ export class SearchSource { getConfig, }); - return search({ params, indexType: searchRequest.indexType }, options).then(({ rawResponse }) => - onResponse(searchRequest, rawResponse) - ); + return search({ params, indexType: searchRequest.indexType }, options) + .pipe(map(({ rawResponse }) => onResponse(searchRequest, rawResponse))) + .toPromise(); } /** diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index c3943af5c6ff7..7451edf5e2fa3 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -18,12 +18,7 @@ */ import { Observable } from 'rxjs'; -import { IEsSearchRequest, IEsSearchResponse, ISearchOptions } from '../../common/search'; - -export type ISearch = ( - request: IKibanaSearchRequest, - options?: ISearchOptions -) => Observable; +import { IEsSearchRequest, IEsSearchResponse } from './es_search'; export type ISearchGeneric = < SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, @@ -33,6 +28,13 @@ export type ISearchGeneric = < options?: ISearchOptions ) => Observable; +export type ISearchCancelGeneric = (id: string, options?: ISearchOptions) => Promise; + +export interface ISearchClient { + search: ISearchGeneric; + cancel: ISearchCancelGeneric; +} + export interface IKibanaSearchResponse { /** * Some responses may contain a unique id to identify the request this response came from. @@ -61,6 +63,9 @@ export interface IKibanaSearchResponse { */ isPartial?: boolean; + /** + * The raw response returned by the internal search method (usually the raw ES response) + */ rawResponse: RawResponse; } @@ -72,3 +77,19 @@ export interface IKibanaSearchRequest { params?: Params; } + +export interface ISearchOptions { + /** + * An `AbortSignal` that allows the caller of `search` to abort a search request. + */ + abortSignal?: AbortSignal; + /** + * Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. + */ + strategy?: string; + + /** + * A session ID, grouping multiple search requests into a single session. + */ + sessionId?: string; +} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index c54cb36142cbd..ce020a9742399 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -358,7 +358,6 @@ export { IKibanaSearchRequest, IKibanaSearchResponse, injectSearchSourceReferences, - ISearch, ISearchSetup, ISearchStart, ISearchStartSearchSource, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index ac8c9bec30d17..d52edbe5b11dd 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -137,7 +137,7 @@ export class AggConfig { // (undocumented) makeLabel(percentageMode?: boolean): any; static nextId(list: IAggConfig[]): number; - onSearchRequestStart(searchSource: ISearchSource_2, options?: ISearchOptions): Promise | Promise; + onSearchRequestStart(searchSource: ISearchSource_2, options?: ISearchOptions_2): Promise | Promise; // (undocumented) params: any; // Warning: (ae-incompatible-release-tags) The symbol "parent" is marked as @public, but its signature references "IAggConfigs" which is marked as @internal @@ -1047,7 +1047,6 @@ export interface IKibanaSearchResponse { isPartial?: boolean; isRunning?: boolean; loaded?: number; - // (undocumented) rawResponse: RawResponse; total?: number; } @@ -1384,11 +1383,6 @@ export type InputTimeRange = TimeRange | { // @public (undocumented) export const isCompleteResponse: (response?: IKibanaSearchResponse | undefined) => boolean; -// Warning: (ae-missing-release-tag) "ISearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ISearch = (request: IKibanaSearchRequest, options?: ISearchOptions) => Observable; - // Warning: (ae-missing-release-tag) "ISearchGeneric" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2092,7 +2086,7 @@ export class SearchSource { onRequestStart(handler: (searchSource: SearchSource, options?: ISearchOptions) => Promise): void; serialize(): { searchSourceJSON: string; - references: import("../../../../../core/types").SavedObjectReference[]; + references: import("src/core/server").SavedObjectReference[]; }; setField(field: K, value: SearchSourceFields[K]): this; setFields(newFields: SearchSourceFields): this; @@ -2327,21 +2321,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 1abf3192a4846..f6bd46c17192c 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -31,7 +31,6 @@ export { IKibanaSearchRequest, IKibanaSearchResponse, injectReferences as injectSearchSourceReferences, - ISearch, ISearchGeneric, ISearchSource, parseSearchSourceJSON, diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index e41eca1a2aa3a..3584d75ab86bb 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -218,7 +218,7 @@ export class SearchInterceptor { * * @param request * @options - * @returns `Observalbe` emitting the search response or an error. + * @returns `Observable` emitting the search response or an error. */ public search( request: IKibanaSearchRequest, diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 3dbabfc68fdbc..e5a50077518af 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -23,12 +23,7 @@ import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; import { handleResponse } from './fetch'; import { - IEsSearchRequest, - IEsSearchResponse, - IKibanaSearchRequest, - IKibanaSearchResponse, ISearchGeneric, - ISearchOptions, SearchSourceService, SearchSourceDependencies, ISessionService, @@ -126,15 +121,7 @@ export class SearchService implements Plugin { const searchSourceDependencies: SearchSourceDependencies = { getConfig: uiSettings.get.bind(uiSettings), - search: < - SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, - SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse - >( - request: SearchStrategyRequest, - options: ISearchOptions - ) => { - return search(request, options).toPromise(); - }, + search, onResponse: handleResponse, legacy: { callMsearch: getCallMsearch({ http }), diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 713fe6dc95014..9a9b8b67730cc 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -221,6 +221,7 @@ export { ISearchStrategy, ISearchSetup, ISearchStart, + SearchStrategyDependencies, getDefaultSearchParams, getShardTimeout, shimHitsTotal, diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 2dbcc3196aa75..4556bee94603f 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { RequestHandlerContext } from '../../../../../core/server'; import { pluginInitializerContextConfigMock } from '../../../../../core/server/mocks'; import { esSearchStrategyProvider } from './es_search_strategy'; +import { SearchStrategyDependencies } from '../types'; describe('ES search strategy', () => { const mockLogger: any = { @@ -36,16 +36,12 @@ describe('ES search strategy', () => { }, }); - const mockContext = ({ - core: { - uiSettings: { - client: { - get: () => {}, - }, - }, - elasticsearch: { client: { asCurrentUser: { search: mockApiCaller } } }, + const mockDeps = ({ + uiSettingsClient: { + get: () => {}, }, - } as unknown) as RequestHandlerContext; + esClient: { asCurrentUser: { search: mockApiCaller } }, + } as unknown) as SearchStrategyDependencies; const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; @@ -63,7 +59,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*' }; await esSearchStrategyProvider(mockConfig$, mockLogger) - .search({ params }, {}, mockContext) + .search({ params }, {}, mockDeps) .subscribe(() => { expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toEqual({ @@ -79,7 +75,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; await esSearchStrategyProvider(mockConfig$, mockLogger) - .search({ params }, {}, mockContext) + .search({ params }, {}, mockDeps) .subscribe(() => { expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toEqual({ @@ -97,7 +93,7 @@ describe('ES search strategy', () => { params: { index: 'logstash-*' }, }, {}, - mockContext + mockDeps ) .subscribe((data) => { expect(data.isRunning).toBe(false); diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index d9cba4baf1fad..3e2d415eac16f 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -36,7 +36,7 @@ export const esSearchStrategyProvider = ( logger: Logger, usage?: SearchUsage ): ISearchStrategy => ({ - search: (request, { abortSignal }, context) => { + search: (request, { abortSignal }, { esClient, uiSettingsClient }) => { // Only default index pattern type is supported here. // See data_enhanced for other type support. if (request.indexType) { @@ -46,12 +46,12 @@ export const esSearchStrategyProvider = ( return doSearch>(async () => { const config = await config$.pipe(first()).toPromise(); const params = toSnakeCase({ - ...(await getDefaultSearchParams(context.core.uiSettings.client)), + ...(await getDefaultSearchParams(uiSettingsClient)), ...getShardTimeout(config), ...request.params, }); - return context.core.elasticsearch.client.asCurrentUser.search(params); + return esClient.asCurrentUser.search(params); }, abortSignal).pipe( toKibanaSearchResponse(), trackSearchStatus(logger, usage), diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index b671ed806510b..1be641401b29c 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -17,12 +17,8 @@ * under the License. */ -export { ISearchStrategy, ISearchSetup, ISearchStart, SearchEnhancements } from './types'; - +export * from './types'; export * from './es_search'; - export { usageProvider, SearchUsage } from './collectors'; - export * from './aggs'; - export { shimHitsTotal } from './routes'; diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 0d4ba0cba24a3..4914726c85ef8 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -33,7 +33,10 @@ export function createSearchStartMock(): jest.Mocked { return { aggs: searchAggsStartMock(), getSearchStrategy: jest.fn(), - search: jest.fn(), + asScoped: jest.fn().mockReturnValue({ + search: jest.fn(), + cancel: jest.fn(), + }), searchSource: searchSourceMock.createStartContract(), }; } diff --git a/src/plugins/data/server/search/routes/search.test.ts b/src/plugins/data/server/search/routes/search.test.ts index 845ab3bbe4eb1..495cb1c9ea770 100644 --- a/src/plugins/data/server/search/routes/search.test.ts +++ b/src/plugins/data/server/search/routes/search.test.ts @@ -16,35 +16,19 @@ * specific language governing permissions and limitations * under the License. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; -import { Observable, from } from 'rxjs'; -import { - CoreSetup, - RequestHandlerContext, - SharedGlobalConfig, - StartServicesAccessor, -} from 'src/core/server'; -import { - coreMock, - httpServerMock, - pluginInitializerContextConfigMock, -} from '../../../../../../src/core/server/mocks'; +import type { MockedKeys } from '@kbn/utility-types/jest'; +import { from } from 'rxjs'; +import { CoreSetup, RequestHandlerContext } from 'src/core/server'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { registerSearchRoute } from './search'; import { DataPluginStart } from '../../plugin'; -import { dataPluginMock } from '../../mocks'; describe('Search service', () => { - let mockDataStart: MockedKeys; let mockCoreSetup: MockedKeys>; - let getStartServices: jest.Mocked>; - let globalConfig$: Observable; beforeEach(() => { - mockDataStart = dataPluginMock.createStartContract(); - mockCoreSetup = coreMock.createSetup({ pluginStartContract: mockDataStart }); - getStartServices = mockCoreSetup.getStartServices; - globalConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; + mockCoreSetup = coreMock.createSetup(); }); it('handler calls context.search.search with the given request and strategy', async () => { @@ -67,8 +51,12 @@ describe('Search service', () => { }, }; - mockDataStart.search.search.mockReturnValue(from(Promise.resolve(response))); - const mockContext = {}; + const mockContext = { + search: { + search: jest.fn().mockReturnValue(from(Promise.resolve(response))), + }, + }; + const mockBody = { id: undefined, params: {} }; const mockParams = { strategy: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ @@ -77,14 +65,14 @@ describe('Search service', () => { }); const mockResponse = httpServerMock.createResponseFactory(); - registerSearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ }); + registerSearchRoute(mockCoreSetup.http.createRouter()); const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; const handler = mockRouter.post.mock.calls[0][1]; await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); - expect(mockDataStart.search.search).toBeCalled(); - expect(mockDataStart.search.search.mock.calls[0][0]).toStrictEqual(mockBody); + expect(mockContext.search.search).toBeCalled(); + expect(mockContext.search.search.mock.calls[0][0]).toStrictEqual(mockBody); expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: response, @@ -101,9 +89,12 @@ describe('Search service', () => { }) ); - mockDataStart.search.search.mockReturnValue(rejectedValue); + const mockContext = { + search: { + search: jest.fn().mockReturnValue(rejectedValue), + }, + }; - const mockContext = {}; const mockBody = { id: undefined, params: {} }; const mockParams = { strategy: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ @@ -112,14 +103,14 @@ describe('Search service', () => { }); const mockResponse = httpServerMock.createResponseFactory(); - registerSearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ }); + registerSearchRoute(mockCoreSetup.http.createRouter()); const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; const handler = mockRouter.post.mock.calls[0][1]; await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); - expect(mockDataStart.search.search).toBeCalled(); - expect(mockDataStart.search.search.mock.calls[0][0]).toStrictEqual(mockBody); + expect(mockContext.search.search).toBeCalled(); + expect(mockContext.search.search.mock.calls[0][0]).toStrictEqual(mockBody); expect(mockResponse.customError).toBeCalled(); const error: any = mockResponse.customError.mock.calls[0][0]; expect(error.body.message).toBe('oh no'); diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts index f737a305a0ec7..a4161fe47b388 100644 --- a/src/plugins/data/server/search/routes/search.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -21,13 +21,9 @@ import { first } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; import type { IRouter } from 'src/core/server'; import { getRequestAbortedSignal } from '../../lib'; -import type { SearchRouteDependencies } from '../search_service'; import { shimHitsTotal } from './shim_hits_total'; -export function registerSearchRoute( - router: IRouter, - { getStartServices }: SearchRouteDependencies -): void { +export function registerSearchRoute(router: IRouter): void { router.post( { path: '/internal/search/{strategy}/{id?}', @@ -47,17 +43,14 @@ export function registerSearchRoute( const { strategy, id } = request.params; const abortSignal = getRequestAbortedSignal(request.events.aborted$); - const [, , selfStart] = await getStartServices(); - try { - const response = await selfStart.search - .search( + const response = await context + .search!.search( { ...searchRequest, id }, { abortSignal, strategy, - }, - context + } ) .pipe(first()) .toPromise(); @@ -99,12 +92,8 @@ export function registerSearchRoute( async (context, request, res) => { const { strategy, id } = request.params; - const [, , selfStart] = await getStartServices(); - const searchStrategy = selfStart.search.getSearchStrategy(strategy); - if (!searchStrategy.cancel) return res.ok(); - try { - await searchStrategy.cancel(context, id); + await context.search!.cancel(id, { strategy }); return res.ok(); } catch (err) { return res.customError({ diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 04ee0e95c7f08..c500c62914c0b 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -26,12 +26,17 @@ import { Logger, Plugin, PluginInitializerContext, - RequestHandlerContext, SharedGlobalConfig, StartServicesAccessor, } from 'src/core/server'; import { first } from 'rxjs/operators'; -import { ISearchSetup, ISearchStart, ISearchStrategy, SearchEnhancements } from './types'; +import { + ISearchSetup, + ISearchStart, + ISearchStrategy, + SearchEnhancements, + SearchStrategyDependencies, +} from './types'; import { AggsService, AggsSetupDependencies } from './aggs'; @@ -53,6 +58,7 @@ import { SearchSourceService, searchSourceRequiredUiSettings, ISearchOptions, + ISearchClient, } from '../../common/search'; import { getShardDelayBucketAgg, @@ -61,6 +67,12 @@ import { import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { ConfigSchema } from '../../config'; +declare module 'src/core/server' { + interface RequestHandlerContext { + search?: ISearchClient; + } +} + type StrategyMap = Record>; /** @internal */ @@ -103,9 +115,14 @@ export class SearchService implements Plugin { getStartServices: core.getStartServices, globalConfig$: this.initializerContext.config.legacy.globalConfig$, }; - registerSearchRoute(router, routeDependencies); + registerSearchRoute(router); registerMsearchRoute(router, routeDependencies); + core.http.registerRouteHandlerContext('search', async (context, request) => { + const [coreStart] = await core.getStartServices(); + return this.asScopedProvider(coreStart)(request); + }); + this.registerSearchStrategy( ES_SEARCH_STRATEGY, esSearchStrategyProvider( @@ -144,14 +161,17 @@ export class SearchService implements Plugin { usage, }; } + public start( - { elasticsearch, savedObjects, uiSettings }: CoreStart, + core: CoreStart, { fieldFormats, indexPatterns }: SearchServiceStartDependencies ): ISearchStart { + const { elasticsearch, savedObjects, uiSettings } = core; + const asScoped = this.asScopedProvider(core); return { aggs: this.aggsService.start({ fieldFormats, uiSettings, indexPatterns }), getSearchStrategy: this.getSearchStrategy, - search: this.search.bind(this), + asScoped, searchSource: { asScoped: async (request: KibanaRequest) => { const esClient = elasticsearch.client.asScoped(request); @@ -169,39 +189,7 @@ export class SearchService implements Plugin { const searchSourceDependencies: SearchSourceDependencies = { getConfig: (key: string): T => uiSettingsCache[key], - search: < - SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, - SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse - >( - searchStrategyRequest: SearchStrategyRequest, - options: ISearchOptions - ) => { - /** - * Unless we want all SearchSource users to provide both a KibanaRequest - * (needed for index patterns) AND the RequestHandlerContext (needed for - * low-level search), we need to fake the context as it can be derived - * from the request object anyway. This will pose problems for folks who - * are registering custom search strategies as they are only getting a - * subset of the entire context. Ideally low-level search should be - * refactored to only require the needed dependencies: esClient & uiSettings. - */ - const fakeRequestHandlerContext = { - core: { - elasticsearch: { - client: esClient, - }, - uiSettings: { - client: uiSettingsClient, - }, - }, - } as RequestHandlerContext; - - return this.search( - searchStrategyRequest, - options, - fakeRequestHandlerContext - ).toPromise(); - }, + search: asScoped(request).search, // onResponse isn't used on the server, so we just return the original value onResponse: (req, res) => res, legacy: { @@ -241,20 +229,26 @@ export class SearchService implements Plugin { >( searchRequest: SearchStrategyRequest, options: ISearchOptions, - context: RequestHandlerContext + deps: SearchStrategyDependencies ) => { const strategy = this.getSearchStrategy( - options.strategy || this.defaultSearchStrategyName + options.strategy ); - return strategy.search(searchRequest, options, context); + return strategy.search(searchRequest, options, deps); + }; + + private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => { + const strategy = this.getSearchStrategy(options.strategy); + + return strategy.cancel ? strategy.cancel(id, options, deps) : Promise.resolve(); }; private getSearchStrategy = < SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse >( - name: string + name: string = this.defaultSearchStrategyName ): ISearchStrategy => { this.logger.debug(`Get strategy ${name}`); const strategy = this.searchStrategies[name]; @@ -263,4 +257,19 @@ export class SearchService implements Plugin { } return strategy; }; + + private asScopedProvider = ({ elasticsearch, savedObjects, uiSettings }: CoreStart) => { + return (request: KibanaRequest): ISearchClient => { + const savedObjectsClient = savedObjects.getScopedClient(request); + const deps = { + savedObjectsClient, + esClient: elasticsearch.client.asScoped(request), + uiSettingsClient: uiSettings.asScopedToClient(savedObjectsClient), + }; + return { + search: (searchRequest, options = {}) => this.search(searchRequest, options, deps), + cancel: (id, options = {}) => this.cancel(id, options, deps), + }; + }; + }; } diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 9ba06d88dc4b3..ebce02014c2a4 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -18,12 +18,18 @@ */ import { Observable } from 'rxjs'; -import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { + IScopedClusterClient, + IUiSettingsClient, + SavedObjectsClientContract, + KibanaRequest, +} from 'src/core/server'; import { ISearchOptions, ISearchStartSearchSource, IKibanaSearchRequest, IKibanaSearchResponse, + ISearchClient, } from '../../common/search'; import { AggsSetup, AggsStart } from './aggs'; import { SearchUsage } from './collectors'; @@ -33,6 +39,12 @@ export interface SearchEnhancements { defaultStrategy: string; } +export interface SearchStrategyDependencies { + savedObjectsClient: SavedObjectsClientContract; + esClient: IScopedClusterClient; + uiSettingsClient: IUiSettingsClient; +} + export interface ISearchSetup { aggs: AggsSetup; /** @@ -69,9 +81,9 @@ export interface ISearchStrategy< search: ( request: SearchStrategyRequest, options: ISearchOptions, - context: RequestHandlerContext + deps: SearchStrategyDependencies ) => Observable; - cancel?: (context: RequestHandlerContext, id: string) => Promise; + cancel?: (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise; } export interface ISearchStart< @@ -80,13 +92,14 @@ export interface ISearchStart< > { aggs: AggsStart; /** - * Get other registered search strategies. For example, if a new strategy needs to use the - * already-registered ES search strategy, it can use this function to accomplish that. + * Get other registered search strategies by name (or, by default, the Elasticsearch strategy). + * For example, if a new strategy needs to use the already-registered ES search strategy, it can + * use this function to accomplish that. */ getSearchStrategy: ( - name: string + name?: string // Name of the search strategy (defaults to the Elasticsearch strategy) ) => ISearchStrategy; - search: ISearchStrategy['search']; + asScoped: (request: KibanaRequest) => ISearchClient; searchSource: { asScoped: (request: KibanaRequest) => Promise; }; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index fba86098a76fa..f62a70c9e4ce1 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -22,8 +22,10 @@ import { ErrorToastOptions } from 'src/core/public/notifications'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ISavedObjectsRepository } from 'kibana/server'; +import { IScopedClusterClient } from 'src/core/server'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; +import { IUiSettingsClient } from 'src/core/server'; import { KibanaRequest } from 'src/core/server'; import { LegacyAPICaller } from 'kibana/server'; import { Logger } from 'kibana/server'; @@ -41,7 +43,6 @@ import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core import { PublicMethodsOf } from '@kbn/utility-types'; import { RecursiveReadonly } from '@kbn/utility-types'; import { RequestAdapter } from 'src/plugins/inspector/common'; -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'; @@ -364,7 +365,7 @@ export type Filter = { // Warning: (ae-missing-release-tag) "getDefaultSearchParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient): Promise<{ +export function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient_2): Promise<{ maxConcurrentShardRequests: number | undefined; ignoreUnavailable: boolean; trackTotalHits: boolean; @@ -723,9 +724,11 @@ export interface ISearchStart ISearchStrategy; + // Warning: (ae-forgotten-export) The symbol "ISearchClient" needs to be exported by the entry point index.d.ts + // // (undocumented) - search: ISearchStrategy['search']; + asScoped: (request: KibanaRequest) => ISearchClient; + getSearchStrategy: (name?: string) => ISearchStrategy; // (undocumented) searchSource: { asScoped: (request: KibanaRequest) => Promise; @@ -737,9 +740,9 @@ export interface ISearchStart { // (undocumented) - cancel?: (context: RequestHandlerContext, id: string) => Promise; + cancel?: (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => Promise; // (undocumented) - search: (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable; + search: (request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies) => Observable; } // @public (undocumented) @@ -888,10 +891,10 @@ export class Plugin implements Plugin_2 Promise; + fieldFormatServiceFactory: (uiSettings: import("src/core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }; @@ -960,7 +963,7 @@ export const search: { utils: { doSearch: (searchMethod: () => Promise, abortSignal?: AbortSignal | undefined) => import("rxjs").Observable; shimAbortSignal: >(promise: T, signal: AbortSignal | undefined) => T; - trackSearchStatus: = import("./search").IEsSearchResponse>>(logger: import("@kbn/logging/target/logger").Logger, usage?: import("./search").SearchUsage | undefined) => import("rxjs").UnaryFunction, import("rxjs").Observable>; + trackSearchStatus: = import("./search").IEsSearchResponse>>(logger: import("src/core/server").Logger, usage?: import("./search").SearchUsage | undefined) => import("rxjs").UnaryFunction, import("rxjs").Observable>; includeTotalLoaded: () => import("rxjs").OperatorFunction>, { total: number; loaded: number; @@ -1007,6 +1010,18 @@ export const search: { tabifyGetColumns: typeof tabifyGetColumns; }; +// Warning: (ae-missing-release-tag) "SearchStrategyDependencies" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface SearchStrategyDependencies { + // (undocumented) + esClient: IScopedClusterClient; + // (undocumented) + savedObjectsClient: SavedObjectsClientContract; + // (undocumented) + uiSettingsClient: IUiSettingsClient; +} + // Warning: (ae-missing-release-tag) "SearchUsage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1147,24 +1162,24 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:234:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:234:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:234:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:234:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:249:5 - (ae-forgotten-export) The symbol "getTotalLoaded" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:250:5 - (ae-forgotten-export) The symbol "toSnakeCase" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:255:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:265:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:275:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:278:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:235:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:235:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:235:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:235:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:5 - (ae-forgotten-export) The symbol "getTotalLoaded" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:251:5 - (ae-forgotten-export) The symbol "toSnakeCase" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:255:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:265:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:276:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:279:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:50:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:91:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:104:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/vis_type_timelion/server/routes/validate_es.ts b/src/plugins/vis_type_timelion/server/routes/validate_es.ts index eeada4766b9d6..bea1b62028eee 100644 --- a/src/plugins/vis_type_timelion/server/routes/validate_es.ts +++ b/src/plugins/vis_type_timelion/server/routes/validate_es.ts @@ -19,7 +19,6 @@ import _ from 'lodash'; import { IRouter, CoreSetup } from 'kibana/server'; -import { TimelionPluginStartDeps } from '../plugin'; export function validateEsRoute(router: IRouter, core: CoreSetup) { router.get( @@ -29,7 +28,6 @@ export function validateEsRoute(router: IRouter, core: CoreSetup) { }, async function (context, request, response) { const uiSettings = await context.core.uiSettings.client.getAll(); - const deps = (await core.getStartServices())[1] as TimelionPluginStartDeps; const timefield = uiSettings['timelion:es.timefield']; @@ -56,7 +54,7 @@ export function validateEsRoute(router: IRouter, core: CoreSetup) { let resp; try { - resp = (await deps.data.search.search(body, {}, context).toPromise()).rawResponse; + resp = (await context.search!.search(body, {}).toPromise()).rawResponse; } catch (errResp) { resp = errResp; } diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js index e10b3f7e438db..f4ba36e4fdd67 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { from } from 'rxjs'; +import { of } from 'rxjs'; import es from './index'; import tlConfigFn from '../fixtures/tl_config'; import * as aggResponse from './lib/agg_response_to_series_list'; @@ -32,21 +32,10 @@ import { UI_SETTINGS } from '../../../../data/server'; describe('es', () => { let tlConfig; - let dataSearchStub; - let mockResponse; - - beforeEach(() => { - dataSearchStub = { - data: { - search: { search: jest.fn(() => from(Promise.resolve(mockResponse))) }, - }, - }; - }); function stubRequestAndServer(response, indexPatternSavedObjects = []) { - mockResponse = response; return { - getStartServices: sinon.stub().returns(Promise.resolve([{}, dataSearchStub])), + context: { search: { search: jest.fn().mockReturnValue(of(response)) } }, savedObjectsClient: { find: function () { return Promise.resolve({ @@ -83,7 +72,7 @@ describe('es', () => { await invoke(es, [5], tlConfig); - expect(dataSearchStub.data.search.search.mock.calls[0][1]).toHaveProperty('sessionId', 1); + expect(tlConfig.context.search.search.mock.calls[0][1]).toHaveProperty('sessionId', 1); }); test('returns a seriesList', () => { diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/index.js b/src/plugins/vis_type_timelion/server/series_functions/es/index.js index 71a080d4a9b95..24b3668b5cd3c 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/index.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/index.js @@ -128,9 +128,8 @@ export default new Datasource('es', { const esShardTimeout = tlConfig.esShardTimeout; const body = buildRequest(config, tlConfig, scriptedFields, esShardTimeout); - const deps = (await tlConfig.getStartServices())[1]; - const resp = await deps.data.search + const resp = await tlConfig.context.search .search( body, { diff --git a/src/plugins/vis_type_timelion/server/series_functions/fixtures/tl_config.js b/src/plugins/vis_type_timelion/server/series_functions/fixtures/tl_config.js index 38d70278fbf00..2f51cf38c0180 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/fixtures/tl_config.js +++ b/src/plugins/vis_type_timelion/server/series_functions/fixtures/tl_config.js @@ -18,7 +18,7 @@ */ import moment from 'moment'; -import sinon from 'sinon'; +import { of } from 'rxjs'; import timelionDefaults from '../../lib/get_namespaced_settings'; import esResponse from './es_response'; @@ -30,14 +30,6 @@ export default function () { if (!functions[name]) throw new Error('No such function: ' + name); return functions[name]; }, - getStartServices: sinon - .stub() - .returns( - Promise.resolve([ - {}, - { data: { search: { search: () => Promise.resolve({ rawResponse: esResponse }) } } }, - ]) - ), esShardTimeout: moment.duration(30000), allowedGraphiteUrls: ['https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite'], @@ -54,5 +46,9 @@ export default function () { tlConfig.setTargetSeries(); + tlConfig.context = { + search: { search: () => of({ rawResponse: esResponse }) }, + }; + return tlConfig; } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index 77080fe9083c1..9710f7daf69b6 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -60,22 +60,8 @@ describe('AbstractSearchStrategy', () => { const responses = await abstractSearchStrategy.search( { - requestContext: {}, - framework: { - core: { - getStartServices: jest.fn().mockReturnValue( - Promise.resolve([ - {}, - { - data: { - search: { - search: searchFn, - }, - }, - }, - ]) - ), - }, + requestContext: { + search: { search: searchFn }, }, }, searches @@ -90,7 +76,6 @@ describe('AbstractSearchStrategy', () => { }, indexType: undefined, }, - {}, {} ); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 79e037d9152ca..eb22fcb1dd689 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -54,12 +54,11 @@ export class AbstractSearchStrategy { } async search(req: ReqFacade, bodies: any[], options = {}) { - const [, deps] = await req.framework.core.getStartServices(); const requests: any[] = []; bodies.forEach((body) => { requests.push( - deps.data.search - .search( + req.requestContext + .search!.search( { params: { ...body, @@ -69,8 +68,7 @@ export class AbstractSearchStrategy { }, { ...options, - }, - req.requestContext + } ) .toPromise() ); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index eb296ead747ff..4cafcdb29ae8d 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -71,7 +71,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { timeout: this.searchTimeout, }); const abortedPromise = toPromise(combinedSignal); - const strategy = options?.strategy || ENHANCED_ES_SEARCH_STRATEGY; + const strategy = options?.strategy ?? ENHANCED_ES_SEARCH_STRATEGY; this.pendingCount$.next(this.pendingCount$.getValue() + 1); diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts index 79f8b80479ed8..88aaee8eb7da2 100644 --- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts @@ -3,10 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import type { RequestHandlerContext, Logger } from 'kibana/server'; - +import type { Logger } from 'kibana/server'; import { EqlSearchStrategyRequest } from '../../common/search/types'; import { eqlSearchStrategyProvider } from './eql_search_strategy'; +import { SearchStrategyDependencies } from '../../../../../src/plugins/data/server'; const getMockEqlResponse = () => ({ body: { @@ -46,32 +46,26 @@ describe('EQL search strategy', () => { describe('search()', () => { let mockEqlSearch: jest.Mock; let mockEqlGet: jest.Mock; - let mockContext: RequestHandlerContext; + let mockDeps: SearchStrategyDependencies; let params: Required['params']; let options: Required['options']; beforeEach(() => { mockEqlSearch = jest.fn().mockResolvedValueOnce(getMockEqlResponse()); mockEqlGet = jest.fn().mockResolvedValueOnce(getMockEqlResponse()); - mockContext = ({ - core: { - uiSettings: { - client: { - get: jest.fn(), - }, - }, - elasticsearch: { - client: { - asCurrentUser: { - eql: { - get: mockEqlGet, - search: mockEqlSearch, - }, - }, + mockDeps = ({ + uiSettingsClient: { + get: jest.fn(), + }, + esClient: { + asCurrentUser: { + eql: { + get: mockEqlGet, + search: mockEqlSearch, }, }, }, - } as unknown) as RequestHandlerContext; + } as unknown) as SearchStrategyDependencies; params = { index: 'logstash-*', body: { query: 'process where 1 == 1' }, @@ -82,7 +76,7 @@ describe('EQL search strategy', () => { describe('async functionality', () => { it('performs an eql client search with params when no ID is provided', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search({ options, params }, {}, mockContext).toPromise(); + await eqlSearch.search({ options, params }, {}, mockDeps).toPromise(); const [[request, requestOptions]] = mockEqlSearch.mock.calls; expect(request.index).toEqual('logstash-*'); @@ -92,7 +86,7 @@ describe('EQL search strategy', () => { it('retrieves the current request if an id is provided', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search({ id: 'my-search-id' }, {}, mockContext).toPromise(); + await eqlSearch.search({ id: 'my-search-id' }, {}, mockDeps).toPromise(); const [[requestParams]] = mockEqlGet.mock.calls; expect(mockEqlSearch).not.toHaveBeenCalled(); @@ -103,7 +97,7 @@ describe('EQL search strategy', () => { expect.assertions(1); mockEqlSearch.mockReset().mockRejectedValueOnce(new Error('client error')); const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - eqlSearch.search({ options, params }, {}, mockContext).subscribe( + eqlSearch.search({ options, params }, {}, mockDeps).subscribe( () => {}, (err) => { expect(err).toEqual(new Error('client error')); @@ -115,7 +109,7 @@ describe('EQL search strategy', () => { describe('arguments', () => { it('sends along async search options', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search({ options, params }, {}, mockContext).toPromise(); + await eqlSearch.search({ options, params }, {}, mockDeps).toPromise(); const [[request]] = mockEqlSearch.mock.calls; expect(request).toEqual( @@ -128,7 +122,7 @@ describe('EQL search strategy', () => { it('sends along default search parameters', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search({ options, params }, {}, mockContext).toPromise(); + await eqlSearch.search({ options, params }, {}, mockDeps).toPromise(); const [[request]] = mockEqlSearch.mock.calls; expect(request).toEqual( @@ -152,7 +146,7 @@ describe('EQL search strategy', () => { }, }, {}, - mockContext + mockDeps ) .toPromise(); const [[request]] = mockEqlSearch.mock.calls; @@ -175,7 +169,7 @@ describe('EQL search strategy', () => { params, }, {}, - mockContext + mockDeps ) .toPromise(); const [[, requestOptions]] = mockEqlSearch.mock.calls; @@ -191,7 +185,7 @@ describe('EQL search strategy', () => { it('passes transport options for an existing request', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); await eqlSearch - .search({ id: 'my-search-id', options: { ignore: [400] } }, {}, mockContext) + .search({ id: 'my-search-id', options: { ignore: [400] } }, {}, mockDeps) .toPromise(); const [[, requestOptions]] = mockEqlGet.mock.calls; diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts index f6afaf56bae33..a75f2617a9bf3 100644 --- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts @@ -21,25 +21,25 @@ export const eqlSearchStrategyProvider = ( logger: Logger ): ISearchStrategy => { return { - cancel: async (context, id) => { + cancel: async (id, options, { esClient }) => { logger.debug(`_eql/delete ${id}`); - await context.core.elasticsearch.client.asCurrentUser.eql.delete({ + await esClient.asCurrentUser.eql.delete({ id, }); }, - search: (request, options, context) => { + search: (request, options, { esClient, uiSettingsClient }) => { logger.debug(`_eql/search ${JSON.stringify(request.params) || request.id}`); const { utils } = search.esSearch; const asyncOptions = getAsyncOptions(); const requestOptions = utils.toSnakeCase({ ...request.options }); - const client = context.core.elasticsearch.client.asCurrentUser.eql; + const client = esClient.asCurrentUser.eql; return doPartialSearch>( async () => { const { ignoreThrottled, ignoreUnavailable } = await getDefaultSearchParams( - context.core.uiSettings.client + uiSettingsClient ); return client.search( diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index bab304b6afc9f..b9b6e25067f2f 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext } from '../../../../../src/core/server'; import { enhancedEsSearchStrategyProvider } from './es_search_strategy'; import { BehaviorSubject } from 'rxjs'; +import { SearchStrategyDependencies } from '../../../../../src/plugins/data/server/search'; const mockAsyncResponse = { body: { @@ -40,26 +40,20 @@ describe('ES search strategy', () => { const mockLogger: any = { debug: () => {}, }; - const mockContext = { - core: { - uiSettings: { - client: { - get: jest.fn(), - }, - }, - elasticsearch: { - client: { - asCurrentUser: { - asyncSearch: { - get: mockGetCaller, - submit: mockSubmitCaller, - }, - transport: { request: mockApiCaller }, - }, + const mockDeps = ({ + uiSettingsClient: { + get: jest.fn(), + }, + esClient: { + asCurrentUser: { + asyncSearch: { + get: mockGetCaller, + submit: mockSubmitCaller, }, + transport: { request: mockApiCaller }, }, }, - }; + } as unknown) as SearchStrategyDependencies; const mockConfig$ = new BehaviorSubject({ elasticsearch: { shardTimeout: { @@ -86,9 +80,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch - .search({ params }, {}, (mockContext as unknown) as RequestHandlerContext) - .toPromise(); + await esSearch.search({ params }, {}, mockDeps).toPromise(); expect(mockSubmitCaller).toBeCalled(); const request = mockSubmitCaller.mock.calls[0][0]; @@ -102,9 +94,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch - .search({ id: 'foo', params }, {}, (mockContext as unknown) as RequestHandlerContext) - .toPromise(); + await esSearch.search({ id: 'foo', params }, {}, mockDeps).toPromise(); expect(mockGetCaller).toBeCalled(); const request = mockGetCaller.mock.calls[0][0]; @@ -126,7 +116,7 @@ describe('ES search strategy', () => { params, }, {}, - (mockContext as unknown) as RequestHandlerContext + mockDeps ) .toPromise(); @@ -142,9 +132,7 @@ describe('ES search strategy', () => { const params = { index: 'foo-*', body: {} }; const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch - .search({ params }, {}, (mockContext as unknown) as RequestHandlerContext) - .toPromise(); + await esSearch.search({ params }, {}, mockDeps).toPromise(); expect(mockSubmitCaller).toBeCalled(); const request = mockSubmitCaller.mock.calls[0][0]; diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 02410efdca668..53bcac02cb01d 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -11,15 +11,16 @@ import { Observable } from 'rxjs'; import type { SearchResponse } from 'elasticsearch'; import type { ApiResponse } from '@elastic/elasticsearch'; -import { getShardTimeout, shimHitsTotal, search } from '../../../../../src/plugins/data/server'; +import { + getShardTimeout, + shimHitsTotal, + search, + SearchStrategyDependencies, +} from '../../../../../src/plugins/data/server'; import { doPartialSearch } from '../../common/search/es_search/es_search_rxjs_utils'; import { getDefaultSearchParams, getAsyncOptions } from './get_default_search_params'; -import type { - SharedGlobalConfig, - RequestHandlerContext, - Logger, -} from '../../../../../src/core/server'; +import type { SharedGlobalConfig, Logger } from '../../../../../src/core/server'; import type { ISearchStrategy, @@ -41,20 +42,20 @@ export const enhancedEsSearchStrategyProvider = ( config$: Observable, logger: Logger, usage?: SearchUsage -): ISearchStrategy => { +): ISearchStrategy => { function asyncSearch( request: IEnhancedEsSearchRequest, options: ISearchOptions, - context: RequestHandlerContext + { esClient, uiSettingsClient }: SearchStrategyDependencies ) { const asyncOptions = getAsyncOptions(); - const client = context.core.elasticsearch.client.asCurrentUser.asyncSearch; + const client = esClient.asCurrentUser.asyncSearch; return doPartialSearch>( async () => client.submit( utils.toSnakeCase({ - ...(await getDefaultSearchParams(context.core.uiSettings.client)), + ...(await getDefaultSearchParams(uiSettingsClient)), batchedReduceSize: 64, ...asyncOptions, ...request.params, @@ -80,13 +81,11 @@ export const enhancedEsSearchStrategyProvider = ( ); } - const rollupSearch = async function ( + async function rollupSearch( request: IEnhancedEsSearchRequest, options: ISearchOptions, - context: RequestHandlerContext + { esClient, uiSettingsClient }: SearchStrategyDependencies ): Promise { - const esClient = context.core.elasticsearch.client.asCurrentUser; - const uiSettingsClient = await context.core.uiSettings.client; const config = await config$.pipe(first()).toPromise(); const { body, index, ...params } = request.params!; const method = 'POST'; @@ -97,7 +96,7 @@ export const enhancedEsSearchStrategyProvider = ( ...params, }); - const promise = esClient.transport.request({ + const promise = esClient.asCurrentUser.transport.request({ method, path, body, @@ -111,26 +110,19 @@ export const enhancedEsSearchStrategyProvider = ( rawResponse: response, ...utils.getTotalLoaded(response._shards), }; - }; + } return { - search: ( - request: IEnhancedEsSearchRequest, - options: ISearchOptions, - context: RequestHandlerContext - ) => { + search: (request, options, deps) => { logger.debug(`search ${JSON.stringify(request.params) || request.id}`); return request.indexType !== 'rollup' - ? asyncSearch(request, options, context) - : from(rollupSearch(request, options, context)); + ? asyncSearch(request, options, deps) + : from(rollupSearch(request, options, deps)); }, - cancel: async (context: RequestHandlerContext, id: string) => { + cancel: async (id, options, { esClient }) => { logger.debug(`cancel ${id}`); - - await context.core.elasticsearch.client.asCurrentUser.asyncSearch.delete({ - id, - }); + await esClient.asCurrentUser.asyncSearch.delete({ id }); }, }; }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts index aef092949a47e..81ee4dc7c9ad2 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts @@ -26,11 +26,10 @@ export const securitySolutionIndexFieldsProvider = (): ISearchStrategy< const beatFields: BeatFields = require('../../utils/beat_schema/fields').fieldsBeat; return { - search: (request, options, context) => + search: (request, options, { esClient }) => from( new Promise(async (resolve) => { - const { elasticsearch } = context.core; - const indexPatternsFetcher = new IndexPatternsFetcher(elasticsearch.client.asCurrentUser); + const indexPatternsFetcher = new IndexPatternsFetcher(esClient.asCurrentUser); const dedupeIndices = dedupeIndexName(request.indices); const responsesIndexFields = await Promise.all( diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts index 962865880df5f..4abec07b3b493 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts @@ -20,7 +20,7 @@ export const securitySolutionSearchStrategyProvider = { + search: (request, options, deps) => { if (request.factoryQueryType == null) { throw new Error('factoryQueryType is required'); } @@ -28,12 +28,12 @@ export const securitySolutionSearchStrategyProvider = queryFactory.parse(request, esSearchRes))); }, - cancel: async (context, id) => { + cancel: async (id, options, deps) => { if (es.cancel) { - es.cancel(context, id); + return es.cancel(id, options, deps); } }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts index 165f0f586ebdb..0b73eed61765f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts @@ -20,7 +20,7 @@ export const securitySolutionTimelineSearchStrategyProvider = { + search: (request, options, deps) => { if (request.factoryQueryType == null) { throw new Error('factoryQueryType is required'); } @@ -29,12 +29,12 @@ export const securitySolutionTimelineSearchStrategyProvider = queryFactory.parse(request, esSearchRes))); }, - cancel: async (context, id) => { + cancel: async (id, options, deps) => { if (es.cancel) { - es.cancel(context, id); + return es.cancel(id, options, deps); } }, }; From 92100f2cb45209dd8fbc17f0df9a989d297eb704 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 4 Nov 2020 14:13:05 -0500 Subject: [PATCH 03/57] Reduce saved objects authorization checks (#82204) --- ...ecure_saved_objects_client_wrapper.test.ts | 75 ++++++++--- .../secure_saved_objects_client_wrapper.ts | 122 ++++++++++-------- 2 files changed, 131 insertions(+), 66 deletions(-) diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 6b9592815dfc5..c6f4ca6dd8afe 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -31,7 +31,9 @@ const createSecureSavedObjectsClientWrapperOptions = () => { createBadRequestError: jest.fn().mockImplementation((message) => new Error(message)), isNotFoundError: jest.fn().mockReturnValue(false), } as unknown) as jest.Mocked; - const getSpacesService = jest.fn().mockReturnValue(true); + const getSpacesService = jest.fn().mockReturnValue({ + namespaceToSpaceId: (namespace?: string) => (namespace ? namespace : 'default'), + }); return { actions, @@ -174,7 +176,9 @@ const expectObjectNamespaceFiltering = async ( ); expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenLastCalledWith( 'login:', - namespaces.filter((x) => x !== '*') // when we check what namespaces to redact, we don't check privileges for '*', only actual space IDs + ['some-other-namespace'] + // when we check what namespaces to redact, we don't check privileges for '*', only actual space IDs + // we don't check privileges for authorizedNamespace either, as that was already checked earlier in the operation ); }; @@ -206,12 +210,14 @@ const expectObjectsNamespaceFiltering = async (fn: Function, args: Record { describe('#deleteFromNamespaces', () => { const type = 'foo'; const id = `${type}-id`; - const namespace1 = 'foo-namespace'; - const namespace2 = 'bar-namespace'; + const namespace1 = 'default'; + const namespace2 = 'another-namespace'; const namespaces = [namespace1, namespace2]; const privilege = `mock-saved_object:${type}/share_to_space`; @@ -1153,4 +1161,41 @@ describe('other', () => { test(`assigns errors from constructor to .errors`, () => { expect(client.errors).toBe(clientOpts.errors); }); + + test(`namespace redaction fails safe`, async () => { + const type = 'foo'; + const id = `${type}-id`; + const namespace = 'some-ns'; + const namespaces = ['some-other-namespace', '*', namespace]; + const returnValue = { namespaces, foo: 'bar' }; + clientOpts.baseClient.get.mockReturnValue(returnValue as any); + + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( + getMockCheckPrivilegesSuccess // privilege check for authorization + ); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + // privilege check for namespace filtering + (_actions: string | string[], _namespaces?: string | string[]) => ({ + hasAllRequested: false, + username: USERNAME, + privileges: { + kibana: [ + // this is a contrived scenario as we *shouldn't* get both an unauthorized and authorized result for a given resource... + // however, in case we do, we should fail-safe (authorized + unauthorized = unauthorized) + { resource: 'some-other-namespace', privilege: 'login:', authorized: false }, + { resource: 'some-other-namespace', privilege: 'login:', authorized: true }, + ], + }, + }) + ); + + const result = await client.get(type, id, { namespace }); + // we will never redact the "All Spaces" ID + expect(result).toEqual(expect.objectContaining({ namespaces: ['*', namespace, '?'] })); + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenLastCalledWith('login:', [ + 'some-other-namespace', + ]); + }); }); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 2ef0cafcd6fdb..e6e34de4ac9ab 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -96,9 +96,9 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra attributes: T = {} as T, options: SavedObjectsCreateOptions = {} ) { + const namespaces = [options.namespace, ...(options.initialNamespaces || [])]; try { const args = { type, attributes, options }; - const namespaces = [options.namespace, ...(options.initialNamespaces || [])]; await this.ensureAuthorized(type, 'create', namespaces, { args }); } catch (error) { this.auditLogger.log( @@ -119,7 +119,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const savedObject = await this.baseClient.create(type, attributes, options); - return await this.redactSavedObjectNamespaces(savedObject); + return await this.redactSavedObjectNamespaces(savedObject, namespaces); } public async checkConflicts( @@ -141,15 +141,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: Array>, options: SavedObjectsBaseOptions = {} ) { + const namespaces = objects.reduce( + (acc, { initialNamespaces = [] }) => acc.concat(initialNamespaces), + [options.namespace] + ); try { const args = { objects, options }; - const namespaces = objects.reduce( - (acc, { initialNamespaces = [] }) => { - return acc.concat(initialNamespaces); - }, - [options.namespace] - ); - await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_create', namespaces, { args, }); @@ -176,7 +173,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const response = await this.baseClient.bulkCreate(objects, options); - return await this.redactSavedObjectsNamespaces(response); + return await this.redactSavedObjectsNamespaces(response, namespaces); } public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { @@ -255,7 +252,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) ); - return await this.redactSavedObjectsNamespaces(response); + return await this.redactSavedObjectsNamespaces(response, options.namespaces ?? [undefined]); } public async bulkGet( @@ -296,7 +293,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) ); - return await this.redactSavedObjectsNamespaces(response); + return await this.redactSavedObjectsNamespaces(response, [options.namespace]); } public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { @@ -323,7 +320,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra }) ); - return await this.redactSavedObjectNamespaces(savedObject); + return await this.redactSavedObjectNamespaces(savedObject, [options.namespace]); } public async update( @@ -354,7 +351,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const savedObject = await this.baseClient.update(type, id, attributes, options); - return await this.redactSavedObjectNamespaces(savedObject); + return await this.redactSavedObjectNamespaces(savedObject, [options.namespace]); } public async addToNamespaces( @@ -363,9 +360,9 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra namespaces: string[], options: SavedObjectsAddToNamespacesOptions = {} ) { + const { namespace } = options; try { const args = { type, id, namespaces, options }; - const { namespace } = options; // To share an object, the user must have the "share_to_space" permission in each of the destination namespaces. await this.ensureAuthorized(type, 'share_to_space', namespaces, { args, @@ -401,7 +398,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const response = await this.baseClient.addToNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(response); + return await this.redactSavedObjectNamespaces(response, [namespace, ...namespaces]); } public async deleteFromNamespaces( @@ -438,20 +435,20 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const response = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(response); + return await this.redactSavedObjectNamespaces(response, namespaces); } public async bulkUpdate( objects: Array> = [], options: SavedObjectsBaseOptions = {} ) { + const objectNamespaces = objects + // The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace; + // in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so. + .filter(({ namespace }) => namespace !== undefined) + .map(({ namespace }) => namespace!); + const namespaces = [options?.namespace, ...objectNamespaces]; try { - const objectNamespaces = objects - // The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace; - // in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so. - .filter(({ namespace }) => namespace !== undefined) - .map(({ namespace }) => namespace!); - const namespaces = [options?.namespace, ...objectNamespaces]; const args = { objects, options }; await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { args, @@ -479,7 +476,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const response = await this.baseClient.bulkUpdate(objects, options); - return await this.redactSavedObjectsNamespaces(response); + return await this.redactSavedObjectsNamespaces(response, namespaces); } public async removeReferencesTo( @@ -617,32 +614,43 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return uniq(objects.map((o) => o.type)); } - private async getNamespacesPrivilegeMap(namespaces: string[]) { + private async getNamespacesPrivilegeMap( + namespaces: string[], + previouslyAuthorizedSpaceIds: string[] + ) { + const namespacesToCheck = namespaces.filter( + (namespace) => !previouslyAuthorizedSpaceIds.includes(namespace) + ); + const initialPrivilegeMap = previouslyAuthorizedSpaceIds.reduce( + (acc, spaceId) => acc.set(spaceId, true), + new Map() + ); + if (namespacesToCheck.length === 0) { + return initialPrivilegeMap; + } const action = this.actions.login; - const checkPrivilegesResult = await this.checkPrivileges(action, namespaces); + const checkPrivilegesResult = await this.checkPrivileges(action, namespacesToCheck); // check if the user can log into each namespace - const map = checkPrivilegesResult.privileges.kibana.reduce( - (acc: Record, { resource, authorized }) => { - // there should never be a case where more than one privilege is returned for a given space - // if there is, fail-safe (authorized + unauthorized = unauthorized) - if (resource && (!authorized || !acc.hasOwnProperty(resource))) { - acc[resource] = authorized; - } - return acc; - }, - {} - ); + const map = checkPrivilegesResult.privileges.kibana.reduce((acc, { resource, authorized }) => { + // there should never be a case where more than one privilege is returned for a given space + // if there is, fail-safe (authorized + unauthorized = unauthorized) + if (resource && (!authorized || !acc.has(resource))) { + acc.set(resource, authorized); + } + return acc; + }, initialPrivilegeMap); return map; } - private redactAndSortNamespaces(spaceIds: string[], privilegeMap: Record) { + private redactAndSortNamespaces(spaceIds: string[], privilegeMap: Map) { return spaceIds - .map((x) => (x === ALL_SPACES_ID || privilegeMap[x] ? x : UNKNOWN_SPACE)) + .map((x) => (x === ALL_SPACES_ID || privilegeMap.get(x) ? x : UNKNOWN_SPACE)) .sort(namespaceComparator); } private async redactSavedObjectNamespaces( - savedObject: T + savedObject: T, + previouslyAuthorizedNamespaces: Array ): Promise { if ( this.getSpacesService() === undefined || @@ -652,12 +660,18 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return savedObject; } - const namespaces = savedObject.namespaces.filter((x) => x !== ALL_SPACES_ID); // all users can see the "all spaces" ID - if (namespaces.length === 0) { - return savedObject; - } + const previouslyAuthorizedSpaceIds = previouslyAuthorizedNamespaces.map((x) => + this.getSpacesService()!.namespaceToSpaceId(x) + ); + // all users can see the "all spaces" ID, and we don't need to recheck authorization for any namespaces that we just checked earlier + const namespaces = savedObject.namespaces.filter( + (x) => x !== ALL_SPACES_ID && !previouslyAuthorizedSpaceIds.includes(x) + ); - const privilegeMap = await this.getNamespacesPrivilegeMap(namespaces); + const privilegeMap = await this.getNamespacesPrivilegeMap( + namespaces, + previouslyAuthorizedSpaceIds + ); return { ...savedObject, @@ -666,20 +680,26 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } private async redactSavedObjectsNamespaces( - response: T + response: T, + previouslyAuthorizedNamespaces: Array ): Promise { if (this.getSpacesService() === undefined) { return response; } + + const previouslyAuthorizedSpaceIds = previouslyAuthorizedNamespaces.map((x) => + this.getSpacesService()!.namespaceToSpaceId(x) + ); const { saved_objects: savedObjects } = response; + // all users can see the "all spaces" ID, and we don't need to recheck authorization for any namespaces that we just checked earlier const namespaces = uniq( savedObjects.flatMap((savedObject) => savedObject.namespaces || []) - ).filter((x) => x !== ALL_SPACES_ID); // all users can see the "all spaces" ID - if (namespaces.length === 0) { - return response; - } + ).filter((x) => x !== ALL_SPACES_ID && !previouslyAuthorizedSpaceIds.includes(x)); - const privilegeMap = await this.getNamespacesPrivilegeMap(namespaces); + const privilegeMap = await this.getNamespacesPrivilegeMap( + namespaces, + previouslyAuthorizedSpaceIds + ); return { ...response, From 1cd477a793ac51033ebcbda4f4435ebc6ed8b93e Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Wed, 4 Nov 2020 14:44:46 -0500 Subject: [PATCH 04/57] [Fleet] Allow snake cased Kibana assets (#77515) * Properly handle kibana assets with underscores in their path * Recomment test * Fix type check * Don't install index patterns that are reserved * Introduce SavedObjectType to use on AssetReference * Fix Test * Update install.ts Use new `dataTypes` const which replaced `DataType` enum * Update install.ts Remove unused `indexPatternTypes` from outer scope * Update install.ts fix (?) bad updates from before where new/correct value was used but result wasn't exported * Update install.ts * Update install.ts Co-authored-by: Elastic Machine Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: John Schulz --- .../package_to_package_policy.test.ts | 2 +- .../ingest_manager/common/types/models/epm.ts | 16 ++- .../ingest_manager/sections/epm/constants.tsx | 4 +- .../server/routes/data_streams/handlers.ts | 4 +- .../services/epm/kibana/assets/install.ts | 114 +++++++++++++++--- .../epm/kibana/index_pattern/install.ts | 2 +- .../ensure_installed_default_packages.test.ts | 4 +- .../epm/packages/get_install_type.test.ts | 6 +- .../server/services/epm/packages/install.ts | 5 +- .../server/services/epm/packages/remove.ts | 42 +++++-- .../server/services/epm/registry/index.ts | 4 +- .../ingest_manager/server/types/index.tsx | 1 + .../apis/epm/install_remove_assets.ts | 33 +++++ .../apis/epm/update_assets.ts | 8 +- .../0.1.0/kibana/index_pattern/invalid.json | 11 ++ .../0.1.0/kibana/index_pattern/test-*.json | 11 ++ 16 files changed, 219 insertions(+), 48 deletions(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test-*.json diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts index 8927b5ab3ca4b..91396bce359b0 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts @@ -25,7 +25,7 @@ describe('Ingest Manager - packageToPackagePolicy', () => { dashboard: [], visualization: [], search: [], - 'index-pattern': [], + index_pattern: [], map: [], }, }, diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index a32322ecff62a..c5fc208bfb2dc 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -35,7 +35,21 @@ export type ServiceName = 'kibana' | 'elasticsearch'; export type AgentAssetType = typeof agentAssetTypes; export type AssetType = KibanaAssetType | ElasticsearchAssetType | ValueOf; +/* + Enum mapping of a saved object asset type to how it would appear in a package file path (snake cased) +*/ export enum KibanaAssetType { + dashboard = 'dashboard', + visualization = 'visualization', + search = 'search', + indexPattern = 'index_pattern', + map = 'map', +} + +/* + Enum of saved object types that are allowed to be installed +*/ +export enum KibanaSavedObjectType { dashboard = 'dashboard', visualization = 'visualization', search = 'search', @@ -271,7 +285,7 @@ export type NotInstalled = T & { export type AssetReference = KibanaAssetReference | EsAssetReference; export type KibanaAssetReference = Pick & { - type: KibanaAssetType; + type: KibanaSavedObjectType; }; export type EsAssetReference = Pick & { type: ElasticsearchAssetType; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx index da3cab1a4b8a3..1dad25e9cf059 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx @@ -20,7 +20,7 @@ export const AssetTitleMap: Record = { ilm_policy: 'ILM Policy', ingest_pipeline: 'Ingest Pipeline', transform: 'Transform', - 'index-pattern': 'Index Pattern', + index_pattern: 'Index Pattern', index_template: 'Index Template', component_template: 'Component Template', search: 'Saved Search', @@ -36,7 +36,7 @@ export const ServiceTitleMap: Record = { export const AssetIcons: Record = { dashboard: 'dashboardApp', - 'index-pattern': 'indexPatternApp', + index_pattern: 'indexPatternApp', search: 'searchProfilerApp', visualization: 'visualizeApp', map: 'mapApp', diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts index 652a7789f65a3..f42f5da2695d0 100644 --- a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -5,7 +5,7 @@ */ import { RequestHandler, SavedObjectsClientContract } from 'src/core/server'; import { DataStream } from '../../types'; -import { GetDataStreamsResponse, KibanaAssetType } from '../../../common'; +import { GetDataStreamsResponse, KibanaAssetType, KibanaSavedObjectType } from '../../../common'; import { getPackageSavedObjects, getKibanaSavedObject } from '../../services/epm/packages/get'; import { defaultIngestErrorHandler } from '../../errors'; @@ -124,7 +124,7 @@ export const getListHandler: RequestHandler = async (context, request, response) // then pick the dashboards from the package saved object const dashboards = pkgSavedObject[0].attributes?.installed_kibana?.filter( - (o) => o.type === KibanaAssetType.dashboard + (o) => o.type === KibanaSavedObjectType.dashboard ) || []; // and then pick the human-readable titles from the dashboard saved objects const enhancedDashboards = await getEnhancedDashboards( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index 201003629e5ea..e7b251ef133c5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -11,17 +11,49 @@ import { } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import * as Registry from '../../registry'; -import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; +import { + AssetType, + KibanaAssetType, + AssetReference, + AssetParts, + KibanaSavedObjectType, +} from '../../../../types'; import { savedObjectTypes } from '../../packages'; +import { indexPatternTypes } from '../index_pattern/install'; type SavedObjectToBe = Required> & { - type: AssetType; + type: KibanaSavedObjectType; }; export type ArchiveAsset = Pick< SavedObject, 'id' | 'attributes' | 'migrationVersion' | 'references' > & { - type: AssetType; + type: KibanaSavedObjectType; +}; + +// KibanaSavedObjectTypes are used to ensure saved objects being created for a given +// KibanaAssetType have the correct type +const KibanaSavedObjectTypeMapping: Record = { + [KibanaAssetType.dashboard]: KibanaSavedObjectType.dashboard, + [KibanaAssetType.indexPattern]: KibanaSavedObjectType.indexPattern, + [KibanaAssetType.map]: KibanaSavedObjectType.map, + [KibanaAssetType.search]: KibanaSavedObjectType.search, + [KibanaAssetType.visualization]: KibanaSavedObjectType.visualization, +}; + +// Define how each asset type will be installed +const AssetInstallers: Record< + KibanaAssetType, + (args: { + savedObjectsClient: SavedObjectsClientContract; + kibanaAssets: ArchiveAsset[]; + }) => Promise>> +> = { + [KibanaAssetType.dashboard]: installKibanaSavedObjects, + [KibanaAssetType.indexPattern]: installKibanaIndexPatterns, + [KibanaAssetType.map]: installKibanaSavedObjects, + [KibanaAssetType.search]: installKibanaSavedObjects, + [KibanaAssetType.visualization]: installKibanaSavedObjects, }; export async function getKibanaAsset(key: string): Promise { @@ -47,16 +79,22 @@ export function createSavedObjectKibanaAsset(asset: ArchiveAsset): SavedObjectTo export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; - kibanaAssets: ArchiveAsset[]; + kibanaAssets: Record; }): Promise { const { savedObjectsClient, kibanaAssets } = options; // install the assets const kibanaAssetTypes = Object.values(KibanaAssetType); const installedAssets = await Promise.all( - kibanaAssetTypes.map((assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, kibanaAssets }) - ) + kibanaAssetTypes.map((assetType) => { + if (kibanaAssets[assetType]) { + return AssetInstallers[assetType]({ + savedObjectsClient, + kibanaAssets: kibanaAssets[assetType], + }); + } + return []; + }) ); return installedAssets.flat(); } @@ -74,25 +112,50 @@ export const deleteKibanaInstalledRefs = async ( installed_kibana: installedAssetsToSave, }); }; -export async function getKibanaAssets(paths: string[]) { - const isKibanaAssetType = (path: string) => Registry.pathParts(path).type in KibanaAssetType; - const filteredPaths = paths.filter(isKibanaAssetType); - const kibanaAssets = await Promise.all(filteredPaths.map((path) => getKibanaAsset(path))); - return kibanaAssets; +export async function getKibanaAssets( + paths: string[] +): Promise> { + const kibanaAssetTypes = Object.values(KibanaAssetType); + const isKibanaAssetType = (path: string) => { + const parts = Registry.pathParts(path); + + return parts.service === 'kibana' && (kibanaAssetTypes as string[]).includes(parts.type); + }; + + const filteredPaths = paths + .filter(isKibanaAssetType) + .map<[string, AssetParts]>((path) => [path, Registry.pathParts(path)]); + + const assetArrays: Array> = []; + for (const assetType of kibanaAssetTypes) { + const matching = filteredPaths.filter(([path, parts]) => parts.type === assetType); + + assetArrays.push(Promise.all(matching.map(([path]) => path).map(getKibanaAsset))); + } + + const resolvedAssets = await Promise.all(assetArrays); + + const result = {} as Record; + + for (const [index, assetType] of kibanaAssetTypes.entries()) { + const expectedType = KibanaSavedObjectTypeMapping[assetType]; + const properlyTypedAssets = resolvedAssets[index].filter(({ type }) => type === expectedType); + + result[assetType] = properlyTypedAssets; + } + + return result; } + async function installKibanaSavedObjects({ savedObjectsClient, - assetType, kibanaAssets, }: { savedObjectsClient: SavedObjectsClientContract; - assetType: KibanaAssetType; kibanaAssets: ArchiveAsset[]; }) { - const isSameType = (asset: ArchiveAsset) => assetType === asset.type; - const filteredKibanaAssets = kibanaAssets.filter((asset) => isSameType(asset)); const toBeSavedObjects = await Promise.all( - filteredKibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) + kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) ); if (toBeSavedObjects.length === 0) { @@ -105,8 +168,23 @@ async function installKibanaSavedObjects({ } } +async function installKibanaIndexPatterns({ + savedObjectsClient, + kibanaAssets, +}: { + savedObjectsClient: SavedObjectsClientContract; + kibanaAssets: ArchiveAsset[]; +}) { + // Filter out any reserved index patterns + const reservedPatterns = indexPatternTypes.map((pattern) => `${pattern}-*`); + + const nonReservedPatterns = kibanaAssets.filter((asset) => !reservedPatterns.includes(asset.id)); + + return installKibanaSavedObjects({ savedObjectsClient, kibanaAssets: nonReservedPatterns }); +} + export function toAssetReference({ id, type }: SavedObject) { - const reference: AssetReference = { id, type: type as KibanaAssetType }; + const reference: AssetReference = { id, type: type as KibanaSavedObjectType }; return reference; } 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 4ca8e9d52c337..d18f43d62436a 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 @@ -72,6 +72,7 @@ export interface IndexPatternField { readFromDocValues: boolean; } +export const indexPatternTypes = Object.values(dataTypes); // TODO: use a function overload and make pkgName and pkgVersion required for install/update // and not for an update removal. or separate out the functions export async function installIndexPatterns( @@ -116,7 +117,6 @@ export async function installIndexPatterns( const packageVersionsInfo = await Promise.all(packageVersionsFetchInfoPromise); // for each index pattern type, create an index pattern - const indexPatternTypes = Object.values(dataTypes); indexPatternTypes.forEach(async (indexPatternType) => { // 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) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts index aaff5df39bac3..4ad6fc96218de 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; +import { ElasticsearchAssetType, Installation, KibanaSavedObjectType } from '../../../types'; import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; jest.mock('./install'); @@ -41,7 +41,7 @@ const mockInstallation: SavedObject = { type: 'epm-packages', attributes: { id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], es_index_patterns: { pattern: 'pattern-name' }, name: 'test package', diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts index a04bfaafe7570..a41511260c6e7 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObject } from 'src/core/server'; -import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; +import { ElasticsearchAssetType, Installation, KibanaSavedObjectType } from '../../../types'; import { getInstallType } from './install'; const mockInstallation: SavedObject = { @@ -13,7 +13,7 @@ const mockInstallation: SavedObject = { type: 'epm-packages', attributes: { id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], es_index_patterns: { pattern: 'pattern-name' }, name: 'test packagek', @@ -30,7 +30,7 @@ const mockInstallationUpdateFail: SavedObject = { type: 'epm-packages', attributes: { id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], es_index_patterns: { pattern: 'pattern-name' }, name: 'test packagek', diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 23666162e91ef..0496a6e9aeef1 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -18,6 +18,7 @@ import { KibanaAssetReference, EsAssetReference, InstallType, + KibanaAssetType, } from '../../../types'; import * as Registry from '../registry'; import { @@ -364,9 +365,9 @@ export async function createInstallation(options: { export const saveKibanaAssetsRefs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - kibanaAssets: ArchiveAsset[] + kibanaAssets: Record ) => { - const assetRefs = kibanaAssets.map(toAssetReference); + const assetRefs = Object.values(kibanaAssets).flat().map(toAssetReference); await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { installed_kibana: assetRefs, }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 4b4fe9540dd95..5db47adc983c2 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -12,6 +12,9 @@ import { AssetType, CallESAsCurrentUser, ElasticsearchAssetType, + EsAssetReference, + KibanaAssetReference, + Installation, } from '../../../types'; import { getInstallation, savedObjectTypes } from './index'; import { deletePipeline } from '../elasticsearch/ingest_pipeline/'; @@ -46,7 +49,7 @@ export async function removeInstallation(options: { // Delete the installed assets const installedAssets = [...installation.installed_kibana, ...installation.installed_es]; - await deleteAssets(installedAssets, savedObjectsClient, callCluster); + await deleteAssets(installation, savedObjectsClient, callCluster); // Delete the manager saved object with references to the asset objects // could also update with [] or some other state @@ -64,17 +67,20 @@ export async function removeInstallation(options: { // successful delete's in SO client return {}. return something more useful return installedAssets; } -async function deleteAssets( - installedObjects: AssetReference[], - savedObjectsClient: SavedObjectsClientContract, - callCluster: CallESAsCurrentUser + +function deleteKibanaAssets( + installedObjects: KibanaAssetReference[], + savedObjectsClient: SavedObjectsClientContract ) { - const logger = appContextService.getLogger(); - const deletePromises = installedObjects.map(async ({ id, type }) => { + return installedObjects.map(async ({ id, type }) => { + return savedObjectsClient.delete(type, id); + }); +} + +function deleteESAssets(installedObjects: EsAssetReference[], callCluster: CallESAsCurrentUser) { + return installedObjects.map(async ({ id, type }) => { const assetType = type as AssetType; - if (savedObjectTypes.includes(assetType)) { - return savedObjectsClient.delete(assetType, id); - } else if (assetType === ElasticsearchAssetType.ingestPipeline) { + if (assetType === ElasticsearchAssetType.ingestPipeline) { return deletePipeline(callCluster, id); } else if (assetType === ElasticsearchAssetType.indexTemplate) { return deleteTemplate(callCluster, id); @@ -82,8 +88,22 @@ async function deleteAssets( return deleteTransforms(callCluster, [id]); } }); +} + +async function deleteAssets( + { installed_es: installedEs, installed_kibana: installedKibana }: Installation, + savedObjectsClient: SavedObjectsClientContract, + callCluster: CallESAsCurrentUser +) { + const logger = appContextService.getLogger(); + + const deletePromises: Array> = [ + ...deleteESAssets(installedEs, callCluster), + ...deleteKibanaAssets(installedKibana, savedObjectsClient), + ]; + try { - await Promise.all([...deletePromises]); + await Promise.all(deletePromises); } catch (err) { logger.error(err); } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 66f28fe58599a..0172f3bb38f51 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -242,10 +242,12 @@ export function getAsset(key: string) { } export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByType { + const kibanaAssetTypes = Object.values(KibanaAssetType); + // ASK: best way, if any, to avoid `any`? const assets = paths.reduce((map: any, path) => { const parts = pathParts(path.replace(/^\/package\//, '')); - if (parts.type in KibanaAssetType) { + if (parts.service === 'kibana' && kibanaAssetTypes.includes(parts.type)) { if (!map[parts.service]) map[parts.service] = {}; if (!map[parts.service][parts.type]) map[parts.service][parts.type] = []; map[parts.service][parts.type].push(parts); diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 3518daa1aba63..5cf43d2830489 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -56,6 +56,7 @@ export { AssetType, Installable, KibanaAssetType, + KibanaSavedObjectType, AssetParts, AssetsGroupedByServiceByType, CategoryId, 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 72ea9cb4e7ef3..8e8e4f010bcb5 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 @@ -184,6 +184,16 @@ export default function (providerContext: FtrProviderContext) { resSearch = err; } expect(resSearch.response.data.statusCode).equal(404); + let resIndexPattern; + try { + resIndexPattern = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'test-*', + }); + } catch (err) { + resIndexPattern = err; + } + expect(resIndexPattern.response.data.statusCode).equal(404); }); it('should have removed the fields from the index patterns', async () => { // The reason there is an expect inside the try and inside the catch in this test case is to guard against two @@ -345,6 +355,7 @@ const expectAssetsInstalled = ({ expect(res.statusCode).equal(200); }); it('should have installed the kibana assets', async function () { + // These are installed from Fleet along with every package const resIndexPatternLogs = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'logs-*', @@ -355,6 +366,8 @@ const expectAssetsInstalled = ({ id: 'metrics-*', }); expect(resIndexPatternMetrics.id).equal('metrics-*'); + + // These are the assets from the package const resDashboard = await kibanaServer.savedObjects.get({ type: 'dashboard', id: 'sample_dashboard', @@ -375,6 +388,22 @@ const expectAssetsInstalled = ({ id: 'sample_search', }); expect(resSearch.id).equal('sample_search'); + const resIndexPattern = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'test-*', + }); + expect(resIndexPattern.id).equal('test-*'); + + let resInvalidTypeIndexPattern; + try { + resInvalidTypeIndexPattern = await kibanaServer.savedObjects.get({ + type: 'invalid-type', + id: 'invalid', + }); + } catch (err) { + resInvalidTypeIndexPattern = err; + } + expect(resInvalidTypeIndexPattern.response.data.statusCode).equal(404); }); it('should create an index pattern with the package fields', async () => { const resIndexPatternLogs = await kibanaServer.savedObjects.get({ @@ -415,6 +444,10 @@ const expectAssetsInstalled = ({ id: 'sample_dashboard2', type: 'dashboard', }, + { + id: 'test-*', + type: 'index-pattern', + }, { id: 'sample_search', type: 'search', diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts index 90dce92a2c6b5..b16cf039f0dad 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts @@ -283,14 +283,14 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_dashboard', type: 'dashboard', }, - { - id: 'sample_search2', - type: 'search', - }, { id: 'sample_visualization', type: 'visualization', }, + { + id: 'sample_search2', + type: 'search', + }, ], installed_es: [ { diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json new file mode 100644 index 0000000000000..bffc52ded73d6 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json @@ -0,0 +1,11 @@ +{ + "attributes": { + "fieldFormatMap": "{}", + "fields": "[]", + "timeFieldName": "@timestamp", + "title": "invalid" + }, + "id": "invalid", + "references": [], + "type": "invalid-type" +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test-*.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test-*.json new file mode 100644 index 0000000000000..48ba36a116709 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test-*.json @@ -0,0 +1,11 @@ +{ + "attributes": { + "fieldFormatMap": "{}", + "fields": "[]", + "timeFieldName": "@timestamp", + "title": "test-*" + }, + "id": "test-*", + "references": [], + "type": "index-pattern" +} From 2cd846befab39c4cb8700032abde57f1d89b9da1 Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Wed, 4 Nov 2020 14:31:50 -0600 Subject: [PATCH 05/57] Master backport #81233 (#82642) * fixes https://github.com/elastic/kibana/issues/74449 * :wqfixes https://github.com/elastic/kibana/issues/74449 Co-authored-by: Rashmi Kulkarni --- .../apps/management/_create_index_pattern_wizard.js | 5 +++++ test/functional/config.js | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.js index 8b11a02099f61..90838ecc1f6fb 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.js @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const es = getService('legacyEs'); const PageObjects = getPageObjects(['settings', 'common']); + const security = getService('security'); describe('"Create Index Pattern" wizard', function () { before(async function () { @@ -51,6 +52,9 @@ export default function ({ getService, getPageObjects }) { }); describe('index alias', () => { + before(async function () { + await security.testUser.setRoles(['kibana_admin', 'test_alias1_reader']); + }); it('can be an index pattern', async () => { await es.transport.request({ path: '/blogs/_doc', @@ -77,6 +81,7 @@ export default function ({ getService, getPageObjects }) { path: '/blogs', method: 'DELETE', }); + await security.testUser.restoreDefaults(); }); }); }); diff --git a/test/functional/config.js b/test/functional/config.js index c9ef6375e04a3..5bef9896d17cc 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -289,6 +289,18 @@ export default async function ({ readConfigFile }) { }, kibana: [], }, + + test_alias1_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['alias1'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }, }, defaultRoles: ['test_logstash_reader', 'kibana_admin'], }, From 0cfcf9fd7c47440415ac35ce495eef718322fe76 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 4 Nov 2020 16:00:57 -0500 Subject: [PATCH 06/57] Load choropleth layer correctly (#82628) --- .../region_map/public/choropleth_layer.js | 6 ++++-- .../public/region_map_visualization.js | 16 +++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/plugins/region_map/public/choropleth_layer.js b/src/plugins/region_map/public/choropleth_layer.js index 14a91e189e0d5..62f4ffe9be677 100644 --- a/src/plugins/region_map/public/choropleth_layer.js +++ b/src/plugins/region_map/public/choropleth_layer.js @@ -286,7 +286,8 @@ CORS configuration of the server permits requests from the Kibana application on showAllData, meta, layerConfig, - serviceSettings + serviceSettings, + leaflet ) { const clonedLayer = new ChoroplethLayer( name, @@ -295,7 +296,8 @@ CORS configuration of the server permits requests from the Kibana application on showAllData, meta, layerConfig, - serviceSettings + serviceSettings, + leaflet ); clonedLayer.setJoinField(this._joinField); clonedLayer.setColorRamp(this._colorRamp); diff --git a/src/plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js index 9b20a35630c86..ecb3541827cb3 100644 --- a/src/plugins/region_map/public/region_map_visualization.js +++ b/src/plugins/region_map/public/region_map_visualization.js @@ -170,27 +170,29 @@ export function createRegionMapVisualization({ } async _recreateChoroplethLayer(name, attribution, showAllData) { + const selectedLayer = await this._loadConfig(this._params.selectedLayer); this._kibanaMap.removeLayer(this._choroplethLayer); if (this._choroplethLayer) { this._choroplethLayer = this._choroplethLayer.cloneChoroplethLayerForNewData( name, attribution, - this._params.selectedLayer.format, + selectedLayer.format, showAllData, - this._params.selectedLayer.meta, - this._params.selectedLayer, - await getServiceSettings() + selectedLayer.meta, + selectedLayer, + await getServiceSettings(), + (await lazyLoadMapsLegacyModules()).L ); } else { const { ChoroplethLayer } = await import('./choropleth_layer'); this._choroplethLayer = new ChoroplethLayer( name, attribution, - this._params.selectedLayer.format, + selectedLayer.format, showAllData, - this._params.selectedLayer.meta, - this._params.selectedLayer, + selectedLayer.meta, + selectedLayer, await getServiceSettings(), (await lazyLoadMapsLegacyModules()).L ); From f8f163c1b734e22c9df728f6f5fb62e2a275922c Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 4 Nov 2020 20:24:40 -0500 Subject: [PATCH 07/57] Moving reinstall function outside of promise.all (#82672) --- x-pack/plugins/ingest_manager/server/services/setup.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 741a23824f010..ffdaaecd9eb04 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -58,7 +58,6 @@ async function createSetupSideEffects( ensureInstalledDefaultPackages(soClient, callCluster), outputService.ensureDefaultOutput(soClient), agentPolicyService.ensureDefaultAgentPolicy(soClient), - ensurePackagesCompletedInstall(soClient, callCluster), ensureDefaultIndices(callCluster), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { @@ -70,6 +69,14 @@ async function createSetupSideEffects( }), ]); + // Keeping this outside of the Promise.all because it introduces a race condition. + // If one of the required packages fails to install/upgrade it might get stuck in the installing state. + // On the next call to the /setup API, if there is a upgrade available for one of the required packages a race condition + // will occur between upgrading the package and reinstalling the previously failed package. + // By moving this outside of the Promise.all, the upgrade will occur first, and then we'll attempt to reinstall any + // packages that are stuck in the installing state. + await ensurePackagesCompletedInstall(soClient, callCluster); + // If we just created the default policy, ensure default packages are added to it if (defaultAgentPolicyCreated) { const agentPolicyWithPackagePolicies = await agentPolicyService.get( From b264498c1e7e4382c618e5827145bdb7f0b0a543 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 4 Nov 2020 17:43:09 -0800 Subject: [PATCH 08/57] [Enterprise Search] Refactor product server route registrations to their own files/folders (#82663) * Refactor all AS & WS route registrations to their own file/folders * Remove WS in route registrations since the main fn is already specific * Cover index.ts files - super basic, leaves out assertions to avoid brittleness * Move WS group routes to index.ts --- .../enterprise_search/server/plugin.ts | 14 +++------ .../server/routes/app_search/index.test.ts | 16 ++++++++++ .../server/routes/app_search/index.ts | 17 +++++++++++ .../server/routes/workplace_search/groups.ts | 10 ------- .../routes/workplace_search/index.test.ts | 16 ++++++++++ .../server/routes/workplace_search/index.ts | 29 +++++++++++++++++++ .../routes/workplace_search/overview.test.ts | 4 +-- .../routes/workplace_search/overview.ts | 2 +- 8 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/index.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/index.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/workplace_search/index.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index c9ffde49eb00a..292767c8fcdb9 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -39,14 +39,11 @@ import { registerConfigDataRoute } from './routes/enterprise_search/config_data' import { appSearchTelemetryType } from './saved_objects/app_search/telemetry'; import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; -import { registerEnginesRoute } from './routes/app_search/engines'; -import { registerCredentialsRoutes } from './routes/app_search/credentials'; -import { registerSettingsRoutes } from './routes/app_search/settings'; +import { registerAppSearchRoutes } from './routes/app_search'; import { workplaceSearchTelemetryType } from './saved_objects/workplace_search/telemetry'; import { registerTelemetryUsageCollector as registerWSTelemetryUsageCollector } from './collectors/workplace_search/telemetry'; -import { registerWSOverviewRoute } from './routes/workplace_search/overview'; -import { registerWSGroupRoutes } from './routes/workplace_search/groups'; +import { registerWorkplaceSearchRoutes } from './routes/workplace_search'; export interface PluginsSetup { usageCollection?: UsageCollectionSetup; @@ -127,11 +124,8 @@ export class EnterpriseSearchPlugin implements Plugin { const dependencies = { router, config, log, enterpriseSearchRequestHandler }; registerConfigDataRoute(dependencies); - registerEnginesRoute(dependencies); - registerCredentialsRoutes(dependencies); - registerSettingsRoutes(dependencies); - registerWSOverviewRoute(dependencies); - registerWSGroupRoutes(dependencies); + registerAppSearchRoutes(dependencies); + registerWorkplaceSearchRoutes(dependencies); /** * Bootstrap the routes, saved objects, and collector for telemetry diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.test.ts new file mode 100644 index 0000000000000..ba1d90b4968fc --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.test.ts @@ -0,0 +1,16 @@ +/* + * 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 { mockDependencies, MockRouter } from '../../__mocks__'; + +import { registerAppSearchRoutes } from './'; + +describe('registerAppSearchRoutes', () => { + it('runs without errors', () => { + const mockRouter = new MockRouter({} as any); + registerAppSearchRoutes({ ...mockDependencies, router: mockRouter.router }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts new file mode 100644 index 0000000000000..b10b8190d780c --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.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 { IRouteDependencies } from '../../plugin'; + +import { registerEnginesRoute } from './engines'; +import { registerCredentialsRoutes } from './credentials'; +import { registerSettingsRoutes } from './settings'; + +export const registerAppSearchRoutes = (dependencies: IRouteDependencies) => { + registerEnginesRoute(dependencies); + registerCredentialsRoutes(dependencies); + registerSettingsRoutes(dependencies); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts index 21d08e5c8756b..cbb78cef5b66c 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts @@ -244,13 +244,3 @@ export function registerBoostsGroupRoute({ } ); } - -export function registerWSGroupRoutes(dependencies: IRouteDependencies) { - registerGroupsRoute(dependencies); - registerSearchGroupsRoute(dependencies); - registerGroupRoute(dependencies); - registerGroupUsersRoute(dependencies); - registerShareGroupRoute(dependencies); - registerAssignGroupRoute(dependencies); - registerBoostsGroupRoute(dependencies); -} diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.test.ts new file mode 100644 index 0000000000000..c4c50ded0099f --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.test.ts @@ -0,0 +1,16 @@ +/* + * 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 { mockDependencies, MockRouter } from '../../__mocks__'; + +import { registerWorkplaceSearchRoutes } from './'; + +describe('registerWorkplaceSearchRoutes', () => { + it('runs without errors', () => { + const mockRouter = new MockRouter({} as any); + registerWorkplaceSearchRoutes({ ...mockDependencies, router: mockRouter.router }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts new file mode 100644 index 0000000000000..a5ebcc0d05298 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { IRouteDependencies } from '../../plugin'; + +import { registerOverviewRoute } from './overview'; +import { + registerGroupsRoute, + registerSearchGroupsRoute, + registerGroupRoute, + registerGroupUsersRoute, + registerShareGroupRoute, + registerAssignGroupRoute, + registerBoostsGroupRoute, +} from './groups'; + +export const registerWorkplaceSearchRoutes = (dependencies: IRouteDependencies) => { + registerOverviewRoute(dependencies); + registerGroupsRoute(dependencies); + registerSearchGroupsRoute(dependencies); + registerGroupRoute(dependencies); + registerGroupUsersRoute(dependencies); + registerShareGroupRoute(dependencies); + registerAssignGroupRoute(dependencies); + registerBoostsGroupRoute(dependencies); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts index a9bd4020e74b7..a387cab31c17a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts @@ -6,7 +6,7 @@ import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; -import { registerWSOverviewRoute } from './overview'; +import { registerOverviewRoute } from './overview'; describe('Overview route', () => { describe('GET /api/workplace_search/overview', () => { @@ -16,7 +16,7 @@ describe('Overview route', () => { jest.clearAllMocks(); mockRouter = new MockRouter({ method: 'get', payload: 'query' }); - registerWSOverviewRoute({ + registerOverviewRoute({ ...mockDependencies, router: mockRouter.router, }); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.ts index 8cfd65725a23a..e5f9fcc746b5b 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.ts @@ -6,7 +6,7 @@ import { IRouteDependencies } from '../../plugin'; -export function registerWSOverviewRoute({ +export function registerOverviewRoute({ router, enterpriseSearchRequestHandler, }: IRouteDependencies) { From 7f70fc84788e39badc0c7d6ca5c2b560cb22b7f9 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Thu, 5 Nov 2020 07:03:23 +0100 Subject: [PATCH 09/57] Embeddables/migrations (#82296) --- ...ins-embeddable-public.embeddablefactory.md | 2 +- ...able-public.embeddablefactorydefinition.md | 2 +- ...ugins-embeddable-public.embeddablestart.md | 2 +- ...ble-server.embeddableregistrydefinition.md | 2 +- ...gin-plugins-expressions-public.executor.md | 3 +- ...essions-public.executor.migratetolatest.md | 23 ---- ...s-expressions-public.expressionsservice.md | 5 +- ...sions-public.expressionsservice.migrate.md | 2 +- ...blic.expressionsservice.migratetolatest.md | 13 --- ...gin-plugins-expressions-server.executor.md | 3 +- ...essions-server.executor.migratetolatest.md | 23 ---- src/plugins/embeddable/common/lib/extract.ts | 54 +++++++++ src/plugins/embeddable/common/lib/index.ts | 24 ++++ src/plugins/embeddable/common/lib/inject.ts | 46 ++++++++ src/plugins/embeddable/common/lib/migrate.ts | 47 ++++++++ .../common/lib/migrate_base_input.ts | 11 +- .../embeddable/common/lib/telemetry.ts | 41 +++++++ src/plugins/embeddable/common/types.ts | 7 ++ .../default_embeddable_factory_provider.ts | 6 +- .../lib/embeddables/embeddable_factory.ts | 3 +- .../embeddable_factory_definition.ts | 1 + src/plugins/embeddable/public/mocks.tsx | 1 + src/plugins/embeddable/public/plugin.test.ts | 15 +++ src/plugins/embeddable/public/plugin.tsx | 99 ++++------------- src/plugins/embeddable/public/public.api.md | 8 +- src/plugins/embeddable/server/plugin.ts | 103 ++++-------------- src/plugins/embeddable/server/server.api.md | 5 +- src/plugins/embeddable/server/types.ts | 9 +- .../common/executor/executor.test.ts | 11 -- .../expressions/common/executor/executor.ts | 30 +---- .../common/service/expressions_services.ts | 14 +-- src/plugins/expressions/public/public.api.md | 10 +- src/plugins/expressions/server/server.api.md | 7 +- src/plugins/kibana_utils/.eslintrc.json | 5 + .../common/persistable_state/index.ts | 43 ++++++-- .../public/dynamic_actions/action_factory.ts | 1 + .../ui_actions_enhanced/server/plugin.ts | 1 + 37 files changed, 364 insertions(+), 318 deletions(-) delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.migratetolatest.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.migratetolatest.md create mode 100644 src/plugins/embeddable/common/lib/extract.ts create mode 100644 src/plugins/embeddable/common/lib/index.ts create mode 100644 src/plugins/embeddable/common/lib/inject.ts create mode 100644 src/plugins/embeddable/common/lib/migrate.ts create mode 100644 src/plugins/embeddable/common/lib/telemetry.ts create mode 100644 src/plugins/kibana_utils/.eslintrc.json diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.md index d543cf3d096df..b355acd0567a8 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.md @@ -9,7 +9,7 @@ EmbeddableFactories create and initialize an embeddable instance Signature: ```typescript -export interface EmbeddableFactory = IEmbeddable, TSavedObjectAttributes extends SavedObjectAttributes = SavedObjectAttributes> extends PersistableState +export interface EmbeddableFactory = IEmbeddable, TSavedObjectAttributes extends SavedObjectAttributes = SavedObjectAttributes> extends PersistableState ``` ## Properties diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactorydefinition.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactorydefinition.md index 4e342d3cf73a1..6ecb88e7c017e 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactorydefinition.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactorydefinition.md @@ -7,5 +7,5 @@ Signature: ```typescript -export declare type EmbeddableFactoryDefinition = IEmbeddable, T extends SavedObjectAttributes = SavedObjectAttributes> = Pick, 'create' | 'type' | 'isEditable' | 'getDisplayName'> & Partial, 'createFromSavedObject' | 'isContainerType' | 'getExplicitInput' | 'savedObjectMetaData' | 'canCreateNew' | 'getDefaultInput' | 'telemetry' | 'extract' | 'inject'>>; +export declare type EmbeddableFactoryDefinition = IEmbeddable, T extends SavedObjectAttributes = SavedObjectAttributes> = Pick, 'create' | 'type' | 'isEditable' | 'getDisplayName'> & Partial, 'createFromSavedObject' | 'isContainerType' | 'getExplicitInput' | 'savedObjectMetaData' | 'canCreateNew' | 'getDefaultInput' | 'telemetry' | 'extract' | 'inject' | 'migrations'>>; ``` 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 541575566d3f7..f500196d850a2 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 @@ -7,7 +7,7 @@ Signature: ```typescript -export interface EmbeddableStart extends PersistableState +export interface EmbeddableStart extends PersistableStateService ``` ## Properties diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddableregistrydefinition.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddableregistrydefinition.md index de46d91d90c65..681c671403315 100644 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddableregistrydefinition.md +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddableregistrydefinition.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface EmbeddableRegistryDefinition

extends PersistableStateDefinition

+export interface EmbeddableRegistryDefinition

extends PersistableStateDefinition

``` ## Properties diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md index 3cc38a0cbdc0f..6835188c2fb04 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class Executor = Record> implements PersistableState +export declare class Executor = Record> implements PersistableStateService ``` ## Constructors @@ -40,7 +40,6 @@ export declare class Executor = Record - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Executor](./kibana-plugin-plugins-expressions-public.executor.md) > [migrateToLatest](./kibana-plugin-plugins-expressions-public.executor.migratetolatest.md) - -## Executor.migrateToLatest() method - -Signature: - -```typescript -migrateToLatest(ast: unknown, version: string): ExpressionAstExpression; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| ast | unknown | | -| version | string | | - -Returns: - -`ExpressionAstExpression` - diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md index 307fc73ec6e9c..6ba0f0feb82b3 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md @@ -15,7 +15,7 @@ so that JSDoc appears in developers IDE when they use those `plugins.expressions Signature: ```typescript -export declare class ExpressionsService implements PersistableState +export declare class ExpressionsService implements PersistableStateService ``` ## Constructors @@ -39,8 +39,7 @@ export declare class ExpressionsService implements PersistableStateExpressionsServiceStart['getType'] | | | [getTypes](./kibana-plugin-plugins-expressions-public.expressionsservice.gettypes.md) | | () => ReturnType<Executor['getTypes']> | Returns POJO map of all registered expression types, where keys are names of the types and values are ExpressionType instances. | | [inject](./kibana-plugin-plugins-expressions-public.expressionsservice.inject.md) | | (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression | Injects saved object references into expression AST | -| [migrate](./kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md) | | (state: SerializableState, version: string) => ExpressionAstExpression | Injects saved object references into expression AST | -| [migrateToLatest](./kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md) | | (state: unknown, version: string) => ExpressionAstExpression | Injects saved object references into expression AST | +| [migrate](./kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md) | | (state: SerializableState, version: string) => ExpressionAstExpression | Runs the migration (if it exists) for specified version. This will run a single migration step (ie from 7.10.0 to 7.10.1) | | [registerFunction](./kibana-plugin-plugins-expressions-public.expressionsservice.registerfunction.md) | | (functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)) => void | Register an expression function, which will be possible to execute as part of the expression pipeline.Below we register a function which simply sleeps for given number of milliseconds to delay the execution and outputs its input as-is. ```ts expressions.registerFunction({ diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md index 88a6bda4ee3f5..d1f24bfcfc0bb 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md @@ -4,7 +4,7 @@ ## ExpressionsService.migrate property -Injects saved object references into expression AST +Runs the migration (if it exists) for specified version. This will run a single migration step (ie from 7.10.0 to 7.10.1) Signature: diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md deleted file mode 100644 index e6860df19fd3f..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) > [migrateToLatest](./kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md) - -## ExpressionsService.migrateToLatest property - -Injects saved object references into expression AST - -Signature: - -```typescript -readonly migrateToLatest: (state: unknown, version: string) => ExpressionAstExpression; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md index da20ae4aa892e..48002a9f986df 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class Executor = Record> implements PersistableState +export declare class Executor = Record> implements PersistableStateService ``` ## Constructors @@ -40,7 +40,6 @@ export declare class Executor = Record - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Executor](./kibana-plugin-plugins-expressions-server.executor.md) > [migrateToLatest](./kibana-plugin-plugins-expressions-server.executor.migratetolatest.md) - -## Executor.migrateToLatest() method - -Signature: - -```typescript -migrateToLatest(ast: unknown, version: string): ExpressionAstExpression; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| ast | unknown | | -| version | string | | - -Returns: - -`ExpressionAstExpression` - diff --git a/src/plugins/embeddable/common/lib/extract.ts b/src/plugins/embeddable/common/lib/extract.ts new file mode 100644 index 0000000000000..966d3f81a36c3 --- /dev/null +++ b/src/plugins/embeddable/common/lib/extract.ts @@ -0,0 +1,54 @@ +/* + * 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 { CommonEmbeddableStartContract, EmbeddableStateWithType } from '../types'; +import { extractBaseEmbeddableInput } from './migrate_base_input'; +import { SerializableState } from '../../../kibana_utils/common/persistable_state'; + +export const getExtractFunction = (embeddables: CommonEmbeddableStartContract) => { + return (state: EmbeddableStateWithType) => { + const enhancements = state.enhancements || {}; + const factory = embeddables.getEmbeddableFactory(state.type); + + const baseResponse = extractBaseEmbeddableInput(state); + let updatedInput = baseResponse.state; + const refs = baseResponse.references; + + if (factory) { + const factoryResponse = factory.extract(state); + updatedInput = factoryResponse.state; + refs.push(...factoryResponse.references); + } + + updatedInput.enhancements = {}; + Object.keys(enhancements).forEach((key) => { + if (!enhancements[key]) return; + const enhancementResult = embeddables + .getEnhancement(key) + .extract(enhancements[key] as SerializableState); + refs.push(...enhancementResult.references); + updatedInput.enhancements![key] = enhancementResult.state; + }); + + return { + state: updatedInput, + references: refs, + }; + }; +}; diff --git a/src/plugins/embeddable/common/lib/index.ts b/src/plugins/embeddable/common/lib/index.ts new file mode 100644 index 0000000000000..e180ca9489df0 --- /dev/null +++ b/src/plugins/embeddable/common/lib/index.ts @@ -0,0 +1,24 @@ +/* + * 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 './extract'; +export * from './inject'; +export * from './migrate'; +export * from './migrate_base_input'; +export * from './telemetry'; diff --git a/src/plugins/embeddable/common/lib/inject.ts b/src/plugins/embeddable/common/lib/inject.ts new file mode 100644 index 0000000000000..8cba3e9da7355 --- /dev/null +++ b/src/plugins/embeddable/common/lib/inject.ts @@ -0,0 +1,46 @@ +/* + * 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 { CommonEmbeddableStartContract, EmbeddableStateWithType } from '../types'; +import { SavedObjectReference } from '../../../../core/types'; +import { injectBaseEmbeddableInput } from './migrate_base_input'; +import { SerializableState } from '../../../kibana_utils/common/persistable_state'; + +export const getInjectFunction = (embeddables: CommonEmbeddableStartContract) => { + return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => { + const enhancements = state.enhancements || {}; + const factory = embeddables.getEmbeddableFactory(state.type); + + let updatedInput = injectBaseEmbeddableInput(state, references); + + if (factory) { + updatedInput = factory.inject(updatedInput, references); + } + + updatedInput.enhancements = {}; + Object.keys(enhancements).forEach((key) => { + if (!enhancements[key]) return; + updatedInput.enhancements![key] = embeddables + .getEnhancement(key) + .inject(enhancements[key] as SerializableState, references); + }); + + return updatedInput; + }; +}; diff --git a/src/plugins/embeddable/common/lib/migrate.ts b/src/plugins/embeddable/common/lib/migrate.ts new file mode 100644 index 0000000000000..e22e591212251 --- /dev/null +++ b/src/plugins/embeddable/common/lib/migrate.ts @@ -0,0 +1,47 @@ +/* + * 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 { CommonEmbeddableStartContract } from '../types'; +import { baseEmbeddableMigrations } from './migrate_base_input'; +import { SerializableState } from '../../../kibana_utils/common/persistable_state'; + +export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) => { + return (state: SerializableState, version: string) => { + const enhancements = (state.enhancements as SerializableState) || {}; + const factory = embeddables.getEmbeddableFactory(state.type as string); + + let updatedInput = baseEmbeddableMigrations[version] + ? baseEmbeddableMigrations[version](state) + : state; + + if (factory && factory.migrations[version]) { + updatedInput = factory.migrations[version](updatedInput); + } + + updatedInput.enhancements = {}; + Object.keys(enhancements).forEach((key) => { + if (!enhancements[key]) return; + (updatedInput.enhancements! as Record)[key] = embeddables + .getEnhancement(key) + .migrations[version](enhancements[key] as SerializableState); + }); + + return updatedInput; + }; +}; diff --git a/src/plugins/embeddable/common/lib/migrate_base_input.ts b/src/plugins/embeddable/common/lib/migrate_base_input.ts index 0d5dc508e20ad..82dc30210c355 100644 --- a/src/plugins/embeddable/common/lib/migrate_base_input.ts +++ b/src/plugins/embeddable/common/lib/migrate_base_input.ts @@ -18,22 +18,25 @@ */ import { SavedObjectReference } from '../../../../core/types'; -import { EmbeddableInput } from '../types'; +import { EmbeddableStateWithType } from '../types'; +import { MigrateFunctionsObject } from '../../../kibana_utils/common'; export const telemetryBaseEmbeddableInput = ( - state: EmbeddableInput, + state: EmbeddableStateWithType, telemetryData: Record ) => { return telemetryData; }; -export const extractBaseEmbeddableInput = (state: EmbeddableInput) => { +export const extractBaseEmbeddableInput = (state: EmbeddableStateWithType) => { return { state, references: [] as SavedObjectReference[] }; }; export const injectBaseEmbeddableInput = ( - state: EmbeddableInput, + state: EmbeddableStateWithType, references: SavedObjectReference[] ) => { return state; }; + +export const baseEmbeddableMigrations: MigrateFunctionsObject = {}; diff --git a/src/plugins/embeddable/common/lib/telemetry.ts b/src/plugins/embeddable/common/lib/telemetry.ts new file mode 100644 index 0000000000000..e2f5918b8080b --- /dev/null +++ b/src/plugins/embeddable/common/lib/telemetry.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CommonEmbeddableStartContract, EmbeddableStateWithType } from '../types'; +import { telemetryBaseEmbeddableInput } from './migrate_base_input'; + +export const getTelemetryFunction = (embeddables: CommonEmbeddableStartContract) => { + return (state: EmbeddableStateWithType, telemetryData: Record = {}) => { + const enhancements: Record = state.enhancements || {}; + const factory = embeddables.getEmbeddableFactory(state.type); + + let outputTelemetryData = telemetryBaseEmbeddableInput(state, telemetryData); + if (factory) { + outputTelemetryData = factory.telemetry(state, outputTelemetryData); + } + Object.keys(enhancements).map((key) => { + if (!enhancements[key]) return; + outputTelemetryData = embeddables + .getEnhancement(key) + .telemetry(enhancements[key], outputTelemetryData); + }); + + return outputTelemetryData; + }; +}; diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index 2737f2678ff32..7e024eda9b793 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -73,3 +73,10 @@ export type EmbeddableInput = { */ searchSessionId?: string; }; + +export type EmbeddableStateWithType = EmbeddableInput & { type: string }; + +export interface CommonEmbeddableStartContract { + getEmbeddableFactory: (embeddableFactoryId: string) => any; + getEnhancement: (enhancementId: string) => any; +} diff --git a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts index e2047dca1f770..6d5e7ef26b8cb 100644 --- a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts +++ b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts @@ -21,6 +21,7 @@ import { SavedObjectAttributes } from 'kibana/public'; import { EmbeddableFactoryDefinition } from './embeddable_factory_definition'; import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable'; import { EmbeddableFactory } from './embeddable_factory'; +import { EmbeddableStateWithType } from '../../../common/types'; import { IContainer } from '..'; export const defaultEmbeddableFactoryProvider = < @@ -49,8 +50,9 @@ export const defaultEmbeddableFactoryProvider = < getDisplayName: def.getDisplayName.bind(def), savedObjectMetaData: def.savedObjectMetaData, telemetry: def.telemetry || (() => ({})), - inject: def.inject || ((state: EmbeddableInput) => state), - extract: def.extract || ((state: EmbeddableInput) => ({ state, references: [] })), + inject: def.inject || ((state: EmbeddableStateWithType) => state), + extract: def.extract || ((state: EmbeddableStateWithType) => ({ state, references: [] })), + migrations: def.migrations || {}, }; return factory; }; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index a6fa46fbc4e3e..5e718cd25f584 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -24,6 +24,7 @@ import { ErrorEmbeddable } from './error_embeddable'; import { IContainer } from '../containers/i_container'; import { PropertySpec } from '../types'; import { PersistableState } from '../../../../kibana_utils/common'; +import { EmbeddableStateWithType } from '../../../common/types'; export interface EmbeddableInstanceConfiguration { id: string; @@ -45,7 +46,7 @@ export interface EmbeddableFactory< TEmbeddableOutput >, TSavedObjectAttributes extends SavedObjectAttributes = SavedObjectAttributes -> extends PersistableState { +> extends PersistableState { // A unique identified for this factory, which will be used to map an embeddable spec to // a factory that can generate an instance of it. readonly type: string; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts index 224a11a201b88..03b03f1ac6e91 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts @@ -43,5 +43,6 @@ export type EmbeddableFactoryDefinition< | 'telemetry' | 'extract' | 'inject' + | 'migrations' > >; diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 62063cb480338..c5a9860498117 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -123,6 +123,7 @@ const createStartContract = (): Start => { telemetry: jest.fn(), extract: jest.fn(), inject: jest.fn(), + migrate: jest.fn(), EmbeddablePanel: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), diff --git a/src/plugins/embeddable/public/plugin.test.ts b/src/plugins/embeddable/public/plugin.test.ts index 5fd3bcdd61318..5d47463344434 100644 --- a/src/plugins/embeddable/public/plugin.test.ts +++ b/src/plugins/embeddable/public/plugin.test.ts @@ -108,9 +108,11 @@ describe('embeddable factory', () => { extract: jest.fn().mockImplementation((state) => ({ state, references: [] })), inject: jest.fn().mockImplementation((state) => state), telemetry: jest.fn().mockResolvedValue({}), + migrations: { '7.11.0': jest.fn().mockImplementation((state) => state) }, } as any; const embeddableState = { id: embeddableFactoryId, + type: embeddableFactoryId, my: 'state', } as any; @@ -137,6 +139,11 @@ describe('embeddable factory', () => { start.telemetry(embeddableState, {}); expect(embeddableFactory.telemetry).toBeCalledWith(embeddableState, {}); }); + + test('embeddableFactory migrate function gets called when calling embeddable migrate', () => { + start.migrate(embeddableState, '7.11.0'); + expect(embeddableFactory.migrations['7.11.0']).toBeCalledWith(embeddableState); + }); }); describe('embeddable enhancements', () => { @@ -149,6 +156,7 @@ describe('embeddable enhancements', () => { extract: jest.fn().mockImplementation((state) => ({ state, references: [] })), inject: jest.fn().mockImplementation((state) => state), telemetry: jest.fn().mockResolvedValue({}), + migrations: { '7.11.0': jest.fn().mockImplementation((state) => state) }, } as any; const embeddableState = { enhancements: { @@ -179,4 +187,11 @@ describe('embeddable enhancements', () => { start.telemetry(embeddableState, {}); expect(embeddableEnhancement.telemetry).toBeCalledWith(embeddableState.enhancements.test, {}); }); + + test('enhancement migrate function gets called when calling embeddable migrate', () => { + start.migrate(embeddableState, '7.11.0'); + expect(embeddableEnhancement.migrations['7.11.0']).toBeCalledWith( + embeddableState.enhancements.test + ); + }); }); diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index aa4d66c43c9db..4f3de0425579c 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -30,7 +30,6 @@ import { Plugin, ScopedHistory, PublicAppInfo, - SavedObjectReference, } from '../../../core/public'; import { EmbeddableFactoryRegistry, @@ -51,14 +50,16 @@ import { } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { EmbeddableStateTransfer } from './lib/state_transfer'; -import { - extractBaseEmbeddableInput, - injectBaseEmbeddableInput, - telemetryBaseEmbeddableInput, -} from '../common/lib/migrate_base_input'; -import { PersistableState, SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; import { ATTRIBUTE_SERVICE_KEY, AttributeService } from './lib/attribute_service'; import { AttributeServiceOptions } from './lib/attribute_service/attribute_service'; +import { EmbeddableStateWithType } from '../common/types'; +import { + getExtractFunction, + getInjectFunction, + getMigrateFunction, + getTelemetryFunction, +} from '../common/lib'; export interface EmbeddableSetupDependencies { data: DataPublicPluginSetup; @@ -84,7 +85,7 @@ export interface EmbeddableSetup { setCustomEmbeddableFactoryProvider: (customProvider: EmbeddableFactoryProvider) => void; } -export interface EmbeddableStart extends PersistableState { +export interface EmbeddableStart extends PersistableStateService { getEmbeddableFactory: < I extends EmbeddableInput = EmbeddableInput, O extends EmbeddableOutput = EmbeddableOutput, @@ -188,6 +189,11 @@ export class EmbeddablePublicPlugin implements Plugin ); + const commonContract = { + getEmbeddableFactory: this.getEmbeddableFactory, + getEnhancement: this.getEnhancement, + }; + return { getEmbeddableFactory: this.getEmbeddableFactory, getEmbeddableFactories: this.getEmbeddableFactories, @@ -207,9 +213,10 @@ export class EmbeddablePublicPlugin implements Plugin = {}) => { - const enhancements: Record = state.enhancements || {}; - const factory = this.getEmbeddableFactory(state.id); - - let telemetry = telemetryBaseEmbeddableInput(state, telemetryData); - if (factory) { - telemetry = factory.telemetry(state, telemetry); - } - Object.keys(enhancements).map((key) => { - if (!enhancements[key]) return; - telemetry = this.getEnhancement(key).telemetry(enhancements[key], telemetry); - }); - - return telemetry; - }; - - private extract = (state: EmbeddableInput) => { - const enhancements = state.enhancements || {}; - const factory = this.getEmbeddableFactory(state.id); - - const baseResponse = extractBaseEmbeddableInput(state); - let updatedInput = baseResponse.state; - const refs = baseResponse.references; - - if (factory) { - const factoryResponse = factory.extract(state); - updatedInput = factoryResponse.state; - refs.push(...factoryResponse.references); - } - - updatedInput.enhancements = {}; - Object.keys(enhancements).forEach((key) => { - if (!enhancements[key]) return; - const enhancementResult = this.getEnhancement(key).extract( - enhancements[key] as SerializableState - ); - refs.push(...enhancementResult.references); - updatedInput.enhancements![key] = enhancementResult.state; - }); - - return { - state: updatedInput, - references: refs, - }; - }; - - private inject = (state: EmbeddableInput, references: SavedObjectReference[]) => { - const enhancements = state.enhancements || {}; - const factory = this.getEmbeddableFactory(state.id); - - let updatedInput = injectBaseEmbeddableInput(state, references); - - if (factory) { - updatedInput = factory.inject(updatedInput, references); - } - - updatedInput.enhancements = {}; - Object.keys(enhancements).forEach((key) => { - if (!enhancements[key]) return; - updatedInput.enhancements![key] = this.getEnhancement(key).inject( - enhancements[key] as SerializableState, - references - ); - }); - - return updatedInput; - }; - private registerEnhancement = (enhancement: EnhancementRegistryDefinition) => { if (this.enhancements.has(enhancement.id)) { throw new Error(`enhancement with id ${enhancement.id} already exists in the registry`); @@ -300,6 +239,7 @@ export class EmbeddablePublicPlugin implements Plugin { return { state, references: [] }; }), + migrations: enhancement.migrations || {}, }); }; @@ -312,6 +252,7 @@ export class EmbeddablePublicPlugin implements Plugin { return { state, references: [] }; }, + migrations: {}, } ); }; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 983daa4d5af73..8f0f56c4e1a16 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -377,10 +377,11 @@ export interface EmbeddableEditorState { } // Warning: (ae-forgotten-export) The symbol "PersistableState" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "EmbeddableStateWithType" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EmbeddableFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface EmbeddableFactory = IEmbeddable, TSavedObjectAttributes extends SavedObjectAttributes_2 = SavedObjectAttributes_2> extends PersistableState { +export interface EmbeddableFactory = IEmbeddable, TSavedObjectAttributes extends SavedObjectAttributes_2 = SavedObjectAttributes_2> extends PersistableState { canCreateNew(): boolean; create(initialInput: TEmbeddableInput, parent?: IContainer): Promise; createFromSavedObject(savedObjectId: string, input: Partial, parent?: IContainer): Promise; @@ -400,7 +401,7 @@ export interface EmbeddableFactory = IEmbeddable, T extends SavedObjectAttributes = SavedObjectAttributes> = Pick, 'create' | 'type' | 'isEditable' | 'getDisplayName'> & Partial, 'createFromSavedObject' | 'isContainerType' | 'getExplicitInput' | 'savedObjectMetaData' | 'canCreateNew' | 'getDefaultInput' | 'telemetry' | 'extract' | 'inject'>>; +export type EmbeddableFactoryDefinition = IEmbeddable, T extends SavedObjectAttributes = SavedObjectAttributes> = Pick, 'create' | 'type' | 'isEditable' | 'getDisplayName'> & Partial, 'createFromSavedObject' | 'isContainerType' | 'getExplicitInput' | 'savedObjectMetaData' | 'canCreateNew' | 'getDefaultInput' | 'telemetry' | 'extract' | 'inject' | 'migrations'>>; // Warning: (ae-missing-release-tag) "EmbeddableFactoryNotFoundError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -560,10 +561,11 @@ export interface EmbeddableSetupDependencies { uiActions: UiActionsSetup; } +// Warning: (ae-forgotten-export) The symbol "PersistableStateService" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EmbeddableStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface EmbeddableStart extends PersistableState { +export interface EmbeddableStart extends PersistableStateService { // (undocumented) EmbeddablePanel: EmbeddablePanelHOC; // (undocumented) diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts index 1e93561e4d063..6e9186e286491 100644 --- a/src/plugins/embeddable/server/plugin.ts +++ b/src/plugins/embeddable/server/plugin.ts @@ -17,7 +17,7 @@ * under the License. */ -import { CoreSetup, CoreStart, Plugin, SavedObjectReference } from 'kibana/server'; +import { CoreSetup, CoreStart, Plugin } from 'kibana/server'; import { identity } from 'lodash'; import { EmbeddableFactoryRegistry, @@ -27,12 +27,13 @@ import { EmbeddableRegistryDefinition, } from './types'; import { - extractBaseEmbeddableInput, - injectBaseEmbeddableInput, - telemetryBaseEmbeddableInput, -} from '../common/lib/migrate_base_input'; + getExtractFunction, + getInjectFunction, + getMigrateFunction, + getTelemetryFunction, +} from '../common/lib'; import { SerializableState } from '../../kibana_utils/common'; -import { EmbeddableInput } from '../common/types'; +import { EmbeddableStateWithType } from '../common/types'; export interface EmbeddableSetup { getAttributeService: any; @@ -52,82 +53,20 @@ export class EmbeddableServerPlugin implements Plugin { } public start(core: CoreStart) { - return { - telemetry: this.telemetry, - extract: this.extract, - inject: this.inject, + const commonContract = { + getEmbeddableFactory: this.getEmbeddableFactory, + getEnhancement: this.getEnhancement, }; - } - - public stop() {} - - private telemetry = (state: EmbeddableInput, telemetryData: Record = {}) => { - const enhancements: Record = state.enhancements || {}; - const factory = this.getEmbeddableFactory(state.id); - - let telemetry = telemetryBaseEmbeddableInput(state, telemetryData); - if (factory) { - telemetry = factory.telemetry(state, telemetry); - } - Object.keys(enhancements).map((key) => { - if (!enhancements[key]) return; - telemetry = this.getEnhancement(key).telemetry(enhancements[key], telemetry); - }); - - return telemetry; - }; - - private extract = (state: EmbeddableInput) => { - const enhancements = state.enhancements || {}; - const factory = this.getEmbeddableFactory(state.id); - - const baseResponse = extractBaseEmbeddableInput(state); - let updatedInput = baseResponse.state; - const refs = baseResponse.references; - - if (factory) { - const factoryResponse = factory.extract(state); - updatedInput = factoryResponse.state; - refs.push(...factoryResponse.references); - } - - updatedInput.enhancements = {}; - Object.keys(enhancements).forEach((key) => { - if (!enhancements[key]) return; - const enhancementResult = this.getEnhancement(key).extract( - enhancements[key] as SerializableState - ); - refs.push(...enhancementResult.references); - updatedInput.enhancements![key] = enhancementResult.state; - }); return { - state: updatedInput, - references: refs, + telemetry: getTelemetryFunction(commonContract), + extract: getExtractFunction(commonContract), + inject: getInjectFunction(commonContract), + migrate: getMigrateFunction(commonContract), }; - }; - - private inject = (state: EmbeddableInput, references: SavedObjectReference[]) => { - const enhancements = state.enhancements || {}; - const factory = this.getEmbeddableFactory(state.id); - - let updatedInput = injectBaseEmbeddableInput(state, references); - - if (factory) { - updatedInput = factory.inject(updatedInput, references); - } - - updatedInput.enhancements = {}; - Object.keys(enhancements).forEach((key) => { - if (!enhancements[key]) return; - updatedInput.enhancements![key] = this.getEnhancement(key).inject( - enhancements[key] as SerializableState, - references - ); - }); + } - return updatedInput; - }; + public stop() {} private registerEnhancement = (enhancement: EnhancementRegistryDefinition) => { if (this.enhancements.has(enhancement.id)) { @@ -142,6 +81,7 @@ export class EmbeddableServerPlugin implements Plugin { ((state: SerializableState) => { return { state, references: [] }; }), + migrations: enhancement.migrations || {}, }); }; @@ -154,6 +94,7 @@ export class EmbeddableServerPlugin implements Plugin { extract: (state: SerializableState) => { return { state, references: [] }; }, + migrations: {}, } ); }; @@ -168,7 +109,8 @@ export class EmbeddableServerPlugin implements Plugin { id: factory.id, telemetry: factory.telemetry || (() => ({})), inject: factory.inject || identity, - extract: factory.extract || ((state: EmbeddableInput) => ({ state, references: [] })), + extract: factory.extract || ((state: EmbeddableStateWithType) => ({ state, references: [] })), + migrations: factory.migrations || {}, }); }; @@ -177,10 +119,11 @@ export class EmbeddableServerPlugin implements Plugin { this.embeddableFactories.get(embeddableFactoryId) || { id: 'unknown', telemetry: () => ({}), - inject: (state: EmbeddableInput) => state, - extract: (state: EmbeddableInput) => { + inject: (state: EmbeddableStateWithType) => state, + extract: (state: EmbeddableStateWithType) => { return { state, references: [] }; }, + migrations: {}, } ); }; diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index d051793382ab7..87f7d76cffaa8 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -7,14 +7,13 @@ import { CoreSetup } from 'kibana/server'; import { CoreStart } from 'kibana/server'; import { Plugin } from 'kibana/server'; -import { SavedObjectReference as SavedObjectReference_2 } from 'kibana/server'; -// Warning: (ae-forgotten-export) The symbol "EmbeddableInput" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "EmbeddableStateWithType" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PersistableStateDefinition" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EmbeddableRegistryDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface EmbeddableRegistryDefinition

extends PersistableStateDefinition

{ +export interface EmbeddableRegistryDefinition

extends PersistableStateDefinition

{ // (undocumented) id: string; } diff --git a/src/plugins/embeddable/server/types.ts b/src/plugins/embeddable/server/types.ts index 64f9325dad3cb..388d4feb66104 100644 --- a/src/plugins/embeddable/server/types.ts +++ b/src/plugins/embeddable/server/types.ts @@ -22,7 +22,7 @@ import { PersistableStateDefinition, SerializableState, } from '../../kibana_utils/common'; -import { EmbeddableInput } from '../common/types'; +import { EmbeddableStateWithType } from '../common/types'; export type EmbeddableFactoryRegistry = Map; export type EnhancementsRegistry = Map; @@ -37,12 +37,13 @@ export interface EnhancementRegistryItem

- extends PersistableStateDefinition

{ +export interface EmbeddableRegistryDefinition< + P extends EmbeddableStateWithType = EmbeddableStateWithType +> extends PersistableStateDefinition

{ id: string; } -export interface EmbeddableRegistryItem

+export interface EmbeddableRegistryItem

extends PersistableState

{ id: string; } diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts index 308d6f7e71814..0c1fdf9891fca 100644 --- a/src/plugins/expressions/common/executor/executor.test.ts +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -214,16 +214,5 @@ describe('Executor', () => { expect(migrateFn).toBeCalledTimes(5); }); }); - - describe('.migrateToLatest', () => { - test('calls extract function for every expression function in expression', () => { - migrateFn.mockClear(); - executor.migrateToLatest( - parseExpression('foo bar="baz" | foo bar={foo bar="baz" | foo bar={foo bar="baz"}}'), - '7.10.0' - ); - expect(migrateFn).toBeCalledTimes(10); - }); - }); }); }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 19fc4cf5a14a2..ca631600b1083 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -32,7 +32,7 @@ import { typeSpecs } from '../expression_types/specs'; import { functionSpecs } from '../expression_functions/specs'; import { getByAlias } from '../util'; import { SavedObjectReference } from '../../../../core/types'; -import { PersistableState, SerializableState } from '../../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../../kibana_utils/common'; import { ExpressionExecutionParams } from '../service'; export interface ExpressionExecOptions { @@ -88,22 +88,8 @@ export class FunctionsRegistry implements IRegistry { } } -const semverGte = (semver1: string, semver2: string) => { - const regex = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/; - const matches1 = regex.exec(semver1) as RegExpMatchArray; - const matches2 = regex.exec(semver2) as RegExpMatchArray; - - const [, major1, minor1, patch1] = matches1; - const [, major2, minor2, patch2] = matches2; - - return ( - major1 > major2 || - (major1 === major2 && (minor1 > minor2 || (minor1 === minor2 && patch1 >= patch2))) - ); -}; - export class Executor = Record> - implements PersistableState { + implements PersistableStateService { static createWithDefaults = Record>( state?: ExecutorState ): Executor { @@ -272,18 +258,6 @@ export class Executor = Record { - for (const key of Object.keys(fn.migrations)) { - if (semverGte(key, version)) { - const updatedAst = fn.migrations[key](link) as ExpressionAstFunction; - link.arguments = updatedAst.arguments; - link.type = updatedAst.type; - } - } - }); - } - public fork(): Executor { const initialState = this.state.get(); const fork = new Executor(initialState); diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index 0f898563c3d0e..01289ca1ae57a 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -24,7 +24,7 @@ import { ExecutionContract } from '../execution/execution_contract'; import { AnyExpressionTypeDefinition } from '../expression_types'; import { AnyExpressionFunctionDefinition } from '../expression_functions'; import { SavedObjectReference } from '../../../../core/types'; -import { PersistableState, SerializableState } from '../../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../../kibana_utils/common'; import { Adapters } from '../../../inspector/common/adapters'; import { ExecutionContextSearch } from '../execution'; @@ -171,7 +171,7 @@ export interface ExpressionServiceParams { * * so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`. */ -export class ExpressionsService implements PersistableState { +export class ExpressionsService implements PersistableStateService { public readonly executor: Executor; public readonly renderers: ExpressionRendererRegistry; @@ -313,16 +313,6 @@ export class ExpressionsService implements PersistableState { - return this.executor.migrateToLatest(state, version); - }; - /** * Returns Kibana Platform *setup* life-cycle contract. Useful to return the * same contract on server-side and browser-side. diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 454c3030aa072..e6bff703aadcd 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -187,11 +187,11 @@ export interface ExecutionState extends ExecutorState state: 'not-started' | 'pending' | 'result' | 'error'; } -// Warning: (ae-forgotten-export) The symbol "PersistableState" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "PersistableStateService" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Executor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class Executor = Record> implements PersistableState { +export class Executor = Record> implements PersistableStateService { constructor(state?: ExecutorState); // (undocumented) get context(): Record; @@ -227,8 +227,6 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; @@ -327,6 +325,7 @@ export interface ExpressionExecutor { interpreter: ExpressionInterpreter; } +// Warning: (ae-forgotten-export) The symbol "PersistableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExpressionFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -581,7 +580,7 @@ export { ExpressionsPublicPlugin as Plugin } // Warning: (ae-missing-release-tag) "ExpressionsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export class ExpressionsService implements PersistableState { +export class ExpressionsService implements PersistableStateService { // Warning: (ae-forgotten-export) The symbol "ExpressionServiceParams" needs to be exported by the entry point index.d.ts constructor({ executor, renderers, }?: ExpressionServiceParams); // (undocumented) @@ -605,7 +604,6 @@ export class ExpressionsService implements PersistableState ReturnType; readonly inject: (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression; readonly migrate: (state: SerializableState, version: string) => ExpressionAstExpression; - readonly migrateToLatest: (state: unknown, version: string) => ExpressionAstExpression; readonly registerFunction: (functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)) => void; // (undocumented) readonly registerRenderer: (definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)) => void; diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 742322b1b5a48..27a3193bf7894 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -169,11 +169,11 @@ export interface ExecutionState extends ExecutorState state: 'not-started' | 'pending' | 'result' | 'error'; } -// Warning: (ae-forgotten-export) The symbol "PersistableState" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "PersistableStateService" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Executor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class Executor = Record> implements PersistableState { +export class Executor = Record> implements PersistableStateService { constructor(state?: ExecutorState); // (undocumented) get context(): Record; @@ -209,8 +209,6 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; @@ -299,6 +297,7 @@ export interface ExpressionAstFunctionBuilder = (state: FromVersion) => ToVersion; -export interface PersistableState

{ +export type MigrateFunctionsObject = { + [key: string]: MigrateFunction; +}; + +export interface PersistableStateService

{ /** * function to extract telemetry information * @param state @@ -65,16 +68,36 @@ export interface PersistableState

SerializableState; + migrate: (state: SerializableState, version: string) => SerializableState; } -export type PersistableStateDefinition

= Partial< - Omit, 'migrate'> -> & { +export interface PersistableState

{ + /** + * function to extract telemetry information + * @param state + * @param collector + */ + telemetry: (state: P, collector: Record) => Record; + /** + * inject function receives state and a list of references and should return state with references injected + * default is identity function + * @param state + * @param references + */ + inject: (state: P, references: SavedObjectReference[]) => P; + /** + * extract function receives state and should return state with references extracted and array of references + * default returns same state with empty reference array + * @param state + */ + extract: (state: P) => { state: P; references: SavedObjectReference[] }; + /** * list of all migrations per semver */ - migrations?: { - [key: string]: MigrateFunction; - }; -}; + migrations: MigrateFunctionsObject; +} + +export type PersistableStateDefinition

= Partial< + PersistableState

+>; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 57c8733ed44fc..a9c498c0210db 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts @@ -67,6 +67,7 @@ export class ActionFactory< public readonly ReactCollectConfig = uiToReactComponent(this.CollectConfig); public readonly createConfig = this.def.createConfig; public readonly isConfigValid = this.def.isConfigValid; + public readonly migrations = this.def.migrations || {}; public getIconType(context: FactoryContext): string | undefined { if (!this.def.getIconType) return undefined; diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts index 0a61c917a2c5c..d6d18848be4de 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts @@ -61,6 +61,7 @@ export class AdvancedUiActionsPublicPlugin ((state: SerializedEvent) => { return { state, references: [] }; }), + migrations: definition.migrations || {}, }); }; From 706be6b10aa772ca50e05bcfcdeaa719a66731cf Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Thu, 5 Nov 2020 07:14:43 +0100 Subject: [PATCH 10/57] [Security Solution] Unskips Overview tests (#82459) * unskips Overview tests * fixes overview tests * unskips 'with no data' test * fixes 'with no data' test * stubs empty instace * fixes typecheck issue * fixes hooks issue Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../cypress/fixtures/empty_instance.json | 1 + .../cypress/integration/overview.spec.ts | 21 +++++++------------ 2 files changed, 9 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/fixtures/empty_instance.json diff --git a/x-pack/plugins/security_solution/cypress/fixtures/empty_instance.json b/x-pack/plugins/security_solution/cypress/fixtures/empty_instance.json new file mode 100644 index 0000000000000..1668203c33729 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/fixtures/empty_instance.json @@ -0,0 +1 @@ +{"indexFields":[],"indicesExist":[],"rawResponse":{"timed_out":false,"took":-1,"_shards":{"total":-1,"successful":-1,"failed":-1,"skipped":-1},"hits":{"total":-1,"max_score":-1,"hits":[{"_index":"","_type":"","_id":"","_score":-1,"_source":null}]}}} diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index 542cf4ad8178f..9e46a53703041 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts @@ -10,16 +10,11 @@ import { expandHostStats, expandNetworkStats } from '../tasks/overview'; import { loginAndWaitForPage } from '../tasks/login'; import { OVERVIEW_URL } from '../urls/navigation'; -import { esArchiverUnload, esArchiverLoad } from '../tasks/es_archiver'; -describe.skip('Overview Page', () => { - before(() => { +describe('Overview Page', () => { + it('Host stats render with correct values', () => { cy.stubSearchStrategyApi('overviewHostQuery', 'overview_search_strategy'); - cy.stubSearchStrategyApi('overviewNetworkQuery', 'overview_search_strategy'); loginAndWaitForPage(OVERVIEW_URL); - }); - - it('Host stats render with correct values', () => { expandHostStats(); HOST_STATS.forEach((stat) => { @@ -28,6 +23,8 @@ describe.skip('Overview Page', () => { }); it('Network stats render with correct values', () => { + cy.stubSearchStrategyApi('overviewNetworkQuery', 'overview_search_strategy'); + loginAndWaitForPage(OVERVIEW_URL); expandNetworkStats(); NETWORK_STATS.forEach((stat) => { @@ -35,14 +32,12 @@ describe.skip('Overview Page', () => { }); }); - describe.skip('with no data', () => { + describe('with no data', () => { before(() => { - esArchiverUnload('auditbeat'); + cy.server(); + cy.fixture('empty_instance').as('emptyInstance'); loginAndWaitForPage(OVERVIEW_URL); - }); - - after(() => { - esArchiverLoad('auditbeat'); + cy.route('POST', '**/internal/search/securitySolutionIndexFields', '@emptyInstance'); }); it('Splash screen should be here', () => { From ed47da8e87ae45d15151f58211cf34f52bf3b403 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 5 Nov 2020 08:15:15 +0100 Subject: [PATCH 11/57] improve client-side SO client get pooling (#82603) --- .../saved_objects_client.test.ts | 51 ++++++++++++ .../saved_objects/saved_objects_client.ts | 79 ++++++++++++------- 2 files changed, 102 insertions(+), 28 deletions(-) diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts index d5d97aded7bd0..8aa70d89d6c2a 100644 --- a/src/core/public/saved_objects/saved_objects_client.test.ts +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -97,6 +97,57 @@ describe('SavedObjectsClient', () => { `); }); + test('removes duplicates when calling `_bulk_get`', async () => { + // Await #get call to ensure batchQueue is empty and throttle has reset + await savedObjectsClient.get('type2', doc.id); + http.fetch.mockClear(); + + savedObjectsClient.get(doc.type, doc.id); + savedObjectsClient.get('some-type', 'some-id'); + await savedObjectsClient.get(doc.type, doc.id); + + expect(http.fetch).toHaveBeenCalledTimes(1); + expect(http.fetch.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/saved_objects/_bulk_get", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\"},{\\"id\\":\\"some-id\\",\\"type\\":\\"some-type\\"}]", + "method": "POST", + "query": undefined, + }, + ] + `); + }); + + test('resolves with correct object when there are duplicates present', async () => { + // Await #get call to ensure batchQueue is empty and throttle has reset + await savedObjectsClient.get('type2', doc.id); + http.fetch.mockClear(); + + const call1 = savedObjectsClient.get(doc.type, doc.id); + const objFromCall2 = await savedObjectsClient.get(doc.type, doc.id); + const objFromCall1 = await call1; + + expect(objFromCall1.type).toBe(doc.type); + expect(objFromCall1.id).toBe(doc.id); + + expect(objFromCall2.type).toBe(doc.type); + expect(objFromCall2.id).toBe(doc.id); + }); + + test('do not share instances or references between duplicate callers', async () => { + // Await #get call to ensure batchQueue is empty and throttle has reset + await savedObjectsClient.get('type2', doc.id); + http.fetch.mockClear(); + + const call1 = savedObjectsClient.get(doc.type, doc.id); + const objFromCall2 = await savedObjectsClient.get(doc.type, doc.id); + const objFromCall1 = await call1; + + objFromCall1.set('title', 'new title'); + expect(objFromCall2.get('title')).toEqual('Example title'); + }); + test('resolves with SimpleSavedObject instance', async () => { const response = savedObjectsClient.get(doc.type, doc.id); await expect(response).resolves.toBeInstanceOf(SimpleSavedObject); diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 3169dad31e2a8..d8b65dbc2330e 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -17,7 +17,7 @@ * under the License. */ -import { cloneDeep, pick, throttle } from 'lodash'; +import { pick, throttle, cloneDeep } from 'lodash'; import { resolve as resolveUrl } from 'url'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -144,6 +144,23 @@ const API_BASE_URL = '/api/saved_objects/'; */ export type SavedObjectsClientContract = PublicMethodsOf; +interface ObjectTypeAndId { + id: string; + type: string; +} + +const getObjectsToFetch = (queue: BatchQueueEntry[]): ObjectTypeAndId[] => { + const objects: ObjectTypeAndId[] = []; + const inserted = new Set(); + queue.forEach(({ id, type }) => { + if (!inserted.has(`${type}|${id}`)) { + objects.push({ id, type }); + inserted.add(`${type}|${id}`); + } + }); + return objects; +}; + /** * Saved Objects is Kibana's data persisentence mechanism allowing plugins to * use Elasticsearch for storing plugin state. The client-side @@ -160,31 +177,34 @@ export class SavedObjectsClient { * Throttled processing of get requests into bulk requests at 100ms interval */ private processBatchQueue = throttle( - () => { - const queue = cloneDeep(this.batchQueue); + async () => { + const queue = [...this.batchQueue]; this.batchQueue = []; - this.bulkGet(queue) - .then(({ savedObjects }) => { - queue.forEach((queueItem) => { - const foundObject = savedObjects.find((savedObject) => { - return savedObject.id === queueItem.id && savedObject.type === queueItem.type; - }); + try { + const objectsToFetch = getObjectsToFetch(queue); + const { saved_objects: savedObjects } = await this.performBulkGet(objectsToFetch); - if (!foundObject) { - return queueItem.resolve( - this.createSavedObject(pick(queueItem, ['id', 'type']) as SavedObject) - ); - } - - queueItem.resolve(foundObject); - }); - }) - .catch((err) => { - queue.forEach((queueItem) => { - queueItem.reject(err); + queue.forEach((queueItem) => { + const foundObject = savedObjects.find((savedObject) => { + return savedObject.id === queueItem.id && savedObject.type === queueItem.type; }); + + if (foundObject) { + // multiple calls may have been requested the same object. + // we need to clone to avoid sharing references between the instances + queueItem.resolve(this.createSavedObject(cloneDeep(foundObject))); + } else { + queueItem.resolve( + this.createSavedObject(pick(queueItem, ['id', 'type']) as SavedObject) + ); + } }); + } catch (err) { + queue.forEach((queueItem) => { + queueItem.reject(err); + }); + } }, BATCH_INTERVAL, { leading: false } @@ -383,14 +403,8 @@ export class SavedObjectsClient { * ]) */ public bulkGet = (objects: Array<{ id: string; type: string }> = []) => { - const path = this.getPath(['_bulk_get']); const filteredObjects = objects.map((obj) => pick(obj, ['id', 'type'])); - - const request: ReturnType = this.savedObjectsFetch(path, { - method: 'POST', - body: JSON.stringify(filteredObjects), - }); - return request.then((resp) => { + return this.performBulkGet(filteredObjects).then((resp) => { resp.saved_objects = resp.saved_objects.map((d) => this.createSavedObject(d)); return renameKeys< PromiseType>, @@ -399,6 +413,15 @@ export class SavedObjectsClient { }); }; + private async performBulkGet(objects: ObjectTypeAndId[]) { + const path = this.getPath(['_bulk_get']); + const request: ReturnType = this.savedObjectsFetch(path, { + method: 'POST', + body: JSON.stringify(objects), + }); + return request; + } + /** * Updates an object * From abc6abc95f78dbd15222665fd5cf189497628b23 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 5 Nov 2020 08:22:08 +0100 Subject: [PATCH 12/57] [Search] Add used index pattern name to the search agg error field (#82604) --- src/plugins/data/common/search/aggs/param_types/field.ts | 3 ++- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/plugins/data/common/search/aggs/param_types/field.ts b/src/plugins/data/common/search/aggs/param_types/field.ts index a0bc71ac8e156..f00e2d6786a00 100644 --- a/src/plugins/data/common/search/aggs/param_types/field.ts +++ b/src/plugins/data/common/search/aggs/param_types/field.ts @@ -90,10 +90,11 @@ export class FieldParamType extends BaseParamType { 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage', { defaultMessage: - 'Saved field "{fieldParameter}" is invalid for use with the "{aggType}" aggregation. Please select a new field.', + 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with the "{aggType}" aggregation. Please select a new field.', values: { fieldParameter: fieldName, aggType: aggConfig?.type?.title, + indexPatternTitle: aggConfig.getIndexPattern().title, }, } ) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c1e2753d66d06..52ead76c5bdc1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1279,7 +1279,6 @@ "data.search.aggs.metrics.uniqueCountTitle": "ユニークカウント", "data.search.aggs.otherBucket.labelForMissingValuesLabel": "欠測値のラベル", "data.search.aggs.otherBucket.labelForOtherBucketLabel": "他のバケットのラベル", - "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "「{aggType}」アグリゲーションで使用するには、保存されたフィールド「{fieldParameter}」が無効です。新しいフィールドを選択してください。", "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} は必須パラメーターです", "data.search.aggs.percentageOfLabel": "{label} の割合", "data.search.aggs.string.customLabel": "カスタムラベル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a2e9187258a25..4eaa9929105cc 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1280,7 +1280,6 @@ "data.search.aggs.metrics.uniqueCountTitle": "唯一计数", "data.search.aggs.otherBucket.labelForMissingValuesLabel": "缺失值的标签", "data.search.aggs.otherBucket.labelForOtherBucketLabel": "其他存储桶的标签", - "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "已保存的字段“{fieldParameter}”无效,无法用于“{aggType}”聚合。请选择新字段。", "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} 是必需字段", "data.search.aggs.percentageOfLabel": "{label} 的百分比", "data.search.aggs.string.customLabel": "定制标签", From ef650f4be0dd6e10b610b41a702b36a6767df035 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Thu, 5 Nov 2020 09:30:01 +0000 Subject: [PATCH 13/57] [Discover] Adding uiMetric to track Visualize link click (#82344) * [Discover] Adding uiMetric around Visualize link click * Change metric name * Fixing wrong merge * Applying PR fixes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/discover/kibana.json | 2 +- .../application/components/discover_legacy.tsx | 2 ++ .../components/sidebar/discover_field.tsx | 9 +++++++++ .../components/sidebar/discover_field_details.tsx | 6 ++++++ .../components/sidebar/discover_sidebar.test.tsx | 1 + .../components/sidebar/discover_sidebar.tsx | 12 +++++++++++- src/plugins/discover/public/build_services.ts | 4 ++++ src/plugins/discover/public/plugin.ts | 2 ++ 8 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index f65740d0d4e7d..7db03f726e6f5 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -14,6 +14,6 @@ "uiActions", "savedObjects" ], - "optionalPlugins": ["home", "share"], + "optionalPlugins": ["home", "share", "usageCollection"], "requiredBundles": ["kibanaUtils", "home", "kibanaReact"] } diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index 5d419977113a8..e9de4c08a177b 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -122,6 +122,7 @@ export function DiscoverLegacy({ }: DiscoverLegacyProps) { const [isSidebarClosed, setIsSidebarClosed] = useState(false); const { TopNavMenu } = getServices().navigation.ui; + const { trackUiMetric } = getServices(); const { savedSearch, indexPatternList } = opts; const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; const bucketInterval = @@ -189,6 +190,7 @@ export function DiscoverLegacy({ onRemoveField={onRemoveColumn} selectedIndexPattern={searchSource && searchSource.getField('index')} setIndexPattern={setIndexPattern} + trackUiMetric={trackUiMetric} /> )} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index 8ff603884239e..0329b3a34580c 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -19,6 +19,7 @@ import React, { useState } from 'react'; import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { UiStatsMetricType } from '@kbn/analytics'; import { DiscoverFieldDetails } from './discover_field_details'; import { FieldIcon, FieldButton } from '../../../../../kibana_react/public'; import { FieldDetails } from './types'; @@ -61,6 +62,12 @@ export interface DiscoverFieldProps { * Determines whether the field name is shortened test.sub1.sub2 = t.s.sub2 */ useShortDots?: boolean; + /** + * Metric tracking function + * @param metricType + * @param eventName + */ + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } export function DiscoverField({ @@ -72,6 +79,7 @@ export function DiscoverField({ getDetails, selected, useShortDots, + trackUiMetric, }: DiscoverFieldProps) { const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', { defaultMessage: 'Add {field} to table', @@ -220,6 +228,7 @@ export function DiscoverField({ field={field} details={getDetails(field)} onAddFilter={onAddFilter} + trackUiMetric={trackUiMetric} /> )} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx index 3061839bf3ef0..dab08a17efcae 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx @@ -19,6 +19,7 @@ import React, { useState, useEffect } from 'react'; import { EuiLink, EuiIconTip, EuiText, EuiPopoverFooter, EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; import { DiscoverFieldBucket } from './discover_field_bucket'; import { getWarnings } from './lib/get_warnings'; import { @@ -35,6 +36,7 @@ interface DiscoverFieldDetailsProps { indexPattern: IndexPattern; details: FieldDetails; onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } export function DiscoverFieldDetails({ @@ -42,6 +44,7 @@ export function DiscoverFieldDetails({ indexPattern, details, onAddFilter, + trackUiMetric, }: DiscoverFieldDetailsProps) { const warnings = getWarnings(field); const [showVisualizeLink, setShowVisualizeLink] = useState(false); @@ -70,6 +73,9 @@ export function DiscoverFieldDetails({ const handleVisualizeLinkClick = (event: React.MouseEvent) => { // regular link click. let the uiActions code handle the navigation and show popup if needed event.preventDefault(); + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'visualize_link_click'); + } triggerVisualizeActions(field, indexPattern.id, details.columns); }; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 6177b60a0a7ad..3d2c20109a264 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -101,6 +101,7 @@ function getCompProps() { selectedIndexPattern: indexPattern, setIndexPattern: jest.fn(), state: {}, + trackUiMetric: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 2407cff181901..dfd09ccee9337 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiTitle, EuiSpacer } from '@elastic/eui'; import { sortBy } from 'lodash'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { UiStatsMetricType } from '@kbn/analytics'; import { DiscoverField } from './discover_field'; import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; @@ -73,6 +74,12 @@ export interface DiscoverSidebarProps { * Callback function to select another index pattern */ setIndexPattern: (id: string) => void; + /** + * Metric tracking function + * @param metricType + * @param eventName + */ + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } export function DiscoverSidebar({ @@ -85,12 +92,12 @@ export function DiscoverSidebar({ onRemoveField, selectedIndexPattern, setIndexPattern, + trackUiMetric, }: DiscoverSidebarProps) { const [showFields, setShowFields] = useState(false); const [fields, setFields] = useState(null); const [fieldFilterState, setFieldFilterState] = useState(getDefaultFieldFilter()); const services = useMemo(() => getServices(), []); - useEffect(() => { const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts); setFields(newFields); @@ -195,6 +202,7 @@ export function DiscoverSidebar({ getDetails={getDetailsByField} selected={true} useShortDots={useShortDots} + trackUiMetric={trackUiMetric} /> ); @@ -269,6 +277,7 @@ export function DiscoverSidebar({ onAddFilter={onAddFilter} getDetails={getDetailsByField} useShortDots={useShortDots} + trackUiMetric={trackUiMetric} /> ); @@ -299,6 +308,7 @@ export function DiscoverSidebar({ onAddFilter={onAddFilter} getDetails={getDetailsByField} useShortDots={useShortDots} + trackUiMetric={trackUiMetric} /> ); diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index b2fdacc629d4f..b8e8bb314dd55 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -37,6 +37,7 @@ import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/publi import { SharePluginStart } from 'src/plugins/share/public'; import { ChartsPluginStart } from 'src/plugins/charts/public'; +import { UiStatsMetricType } from '@kbn/analytics'; import { DiscoverStartPlugins } from './plugin'; import { createSavedSearchesLoader, SavedSearch } from './saved_searches'; import { getHistory } from './kibana_services'; @@ -67,6 +68,7 @@ export interface DiscoverServices { getSavedSearchUrlById: (id: string) => Promise; getEmbeddableInjector: any; uiSettings: IUiSettingsClient; + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } export async function buildServices( @@ -80,6 +82,7 @@ export async function buildServices( savedObjects: plugins.savedObjects, }; const savedObjectService = createSavedSearchesLoader(services); + const { usageCollection } = plugins; return { addBasePath: core.http.basePath.prepend, @@ -106,5 +109,6 @@ export async function buildServices( timefilter: plugins.data.query.timefilter.timefilter, toastNotifications: core.notifications.toasts, uiSettings: core.uiSettings, + trackUiMetric: usageCollection?.reportUiStats.bind(usageCollection, 'discover'), }; } diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 10bde12f8768d..647746b98cbd1 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -69,6 +69,7 @@ import { DiscoverUrlGenerator, } from './url_generator'; import { SearchEmbeddableFactory } from './application/embeddable'; +import { UsageCollectionSetup } from '../../usage_collection/public'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -139,6 +140,7 @@ export interface DiscoverStartPlugins { urlForwarding: UrlForwardingStart; inspector: InspectorPublicPluginStart; savedObjects: SavedObjectsStart; + usageCollection?: UsageCollectionSetup; } const innerAngularName = 'app/discover'; From 926fe8915dcb1e1bcfb1519f3ef6dd86c344978a Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 5 Nov 2020 10:40:15 +0100 Subject: [PATCH 14/57] Add platform's missing READMEs (#82268) * add missing readme * update ascidoc --- docs/developer/plugin-list.asciidoc | 24 +++++++++---------- src/plugins/legacy_export/README.md | 3 +++ src/plugins/saved_objects/README.md | 3 +++ .../saved_objects_management/README.md | 3 +++ x-pack/plugins/cloud/README.md | 3 +++ x-pack/plugins/features/README.md | 3 +++ .../plugins/global_search_providers/README.md | 15 ++++++++++++ 7 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 src/plugins/legacy_export/README.md create mode 100644 src/plugins/saved_objects/README.md create mode 100644 src/plugins/saved_objects_management/README.md create mode 100644 x-pack/plugins/cloud/README.md create mode 100644 x-pack/plugins/features/README.md create mode 100644 x-pack/plugins/global_search_providers/README.md diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 9a32f3b3adb3c..9235fc1198b12 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -128,8 +128,8 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel. |Utilities for building Kibana plugins. -|{kib-repo}blob/{branch}/src/plugins/legacy_export[legacyExport] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/legacy_export/README.md[legacyExport] +|The legacyExport plugin adds support for the legacy saved objects export format. |{kib-repo}blob/{branch}/src/plugins/management/README.md[management] @@ -156,12 +156,12 @@ Content is fetched from the remote (https://feeds.elastic.co and https://feeds-s |Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states. -|{kib-repo}blob/{branch}/src/plugins/saved_objects[savedObjects] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/saved_objects/README.md[savedObjects] +|The savedObjects plugin exposes utilities to manipulate saved objects on the client side. -|{kib-repo}blob/{branch}/src/plugins/saved_objects_management[savedObjectsManagement] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/saved_objects_management/README.md[savedObjectsManagement] +|The savedObjectsManagement plugin manages the Saved Objects management section. |{kib-repo}blob/{branch}/src/plugins/saved_objects_tagging_oss/README.md[savedObjectsTaggingOss] @@ -309,8 +309,8 @@ Failure to have auth enabled in Kibana will make for a broken UI. UI-based error |Experimental Feature -|{kib-repo}blob/{branch}/x-pack/plugins/cloud[cloud] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/cloud/README.md[cloud] +|The cloud plugin adds cloud specific features to Kibana. |{kib-repo}blob/{branch}/x-pack/plugins/code[code] @@ -361,8 +361,8 @@ occuring in Kibana, initially just for the Make It Action project - alerts and actions. -|{kib-repo}blob/{branch}/x-pack/plugins/features[features] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/features/README.md[features] +|The features plugin enhance Kibana with a per-feature privilege system. |{kib-repo}blob/{branch}/x-pack/plugins/file_upload/README.md[fileUpload] @@ -378,8 +378,8 @@ or dashboards from the Kibana instance, from both server and client-side plugins |The GlobalSearchBar plugin provides a search interface for navigating Kibana. (It is the UI to the GlobalSearch plugin.) -|{kib-repo}blob/{branch}/x-pack/plugins/global_search_providers[globalSearchProviders] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/global_search_providers/README.md[globalSearchProviders] +|The globalSearchProviders plugin provides Kibana default search providers for the GlobalSearch plugin. |{kib-repo}blob/{branch}/x-pack/plugins/graph/README.md[graph] diff --git a/src/plugins/legacy_export/README.md b/src/plugins/legacy_export/README.md new file mode 100644 index 0000000000000..050e39b8f19e4 --- /dev/null +++ b/src/plugins/legacy_export/README.md @@ -0,0 +1,3 @@ +# `legacyExport` plugin + +The `legacyExport` plugin adds support for the legacy saved objects export format. diff --git a/src/plugins/saved_objects/README.md b/src/plugins/saved_objects/README.md new file mode 100644 index 0000000000000..2f8dd44a2c5fa --- /dev/null +++ b/src/plugins/saved_objects/README.md @@ -0,0 +1,3 @@ +# `savedObjects` plugin + +The `savedObjects` plugin exposes utilities to manipulate saved objects on the client side. \ No newline at end of file diff --git a/src/plugins/saved_objects_management/README.md b/src/plugins/saved_objects_management/README.md new file mode 100644 index 0000000000000..cdaf027e7d2de --- /dev/null +++ b/src/plugins/saved_objects_management/README.md @@ -0,0 +1,3 @@ +# `savedObjectsManagement` plugin + +The `savedObjectsManagement` plugin manages the `Saved Objects` management section. diff --git a/x-pack/plugins/cloud/README.md b/x-pack/plugins/cloud/README.md new file mode 100644 index 0000000000000..13172e0a6ddc0 --- /dev/null +++ b/x-pack/plugins/cloud/README.md @@ -0,0 +1,3 @@ +# `cloud` plugin + +The `cloud` plugin adds cloud specific features to Kibana. diff --git a/x-pack/plugins/features/README.md b/x-pack/plugins/features/README.md new file mode 100644 index 0000000000000..0951b0c13c314 --- /dev/null +++ b/x-pack/plugins/features/README.md @@ -0,0 +1,3 @@ +# `features` plugin + +The `features` plugin enhance Kibana with a per-feature privilege system. \ No newline at end of file diff --git a/x-pack/plugins/global_search_providers/README.md b/x-pack/plugins/global_search_providers/README.md new file mode 100644 index 0000000000000..da78f5adbfb09 --- /dev/null +++ b/x-pack/plugins/global_search_providers/README.md @@ -0,0 +1,15 @@ +# Kibana `GlobalSearchProviders` plugin + +The globalSearchProviders plugin provides Kibana default search providers for the `GlobalSearch` plugin. + +## Server-side providers + +### SavedObjects + +This provider returns results for all saved object types that are searchable. + +## Client-side providers + +### Applications + +This provider returns results for the applications that are currently registered on the client-side. From 7558fe14096250ea6156398936e6e7bdcd42873b Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 5 Nov 2020 11:22:15 +0100 Subject: [PATCH 15/57] [UX]Swap env filter with percentile (#82246) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/app/RumDashboard/Panels/MainFilters.tsx | 6 +++--- .../app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx | 2 +- .../public/components/shared/EnvironmentFilter/index.tsx | 2 +- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx index efc52e7cb426a..7c21079885334 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx @@ -44,12 +44,12 @@ export function MainFilters() { serviceNames={data ?? []} /> - - - + + + ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx index cf419f6edffc0..b70621b1e4cbc 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx @@ -65,7 +65,7 @@ function ServiceNameFilter({ loading, serviceNames }: Props) { prepend={i18n.translate( 'xpack.apm.ux.localFilters.titles.webApplication', { - defaultMessage: 'Web Application', + defaultMessage: 'Web application', } )} isLoading={loading} diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx index c3ecaa1b053b6..e6e40e44bad38 100644 --- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx @@ -76,7 +76,7 @@ export function EnvironmentFilter() { return ( Date: Thu, 5 Nov 2020 11:24:54 +0100 Subject: [PATCH 16/57] [ILM] Fix breadcrumbs (#82594) * added breadcrumb service and call on ILM pages * add notices to legacy pattern services * fix jest tests and create mock --- .../edit_policy/edit_policy.helpers.tsx | 15 ++++- .../public/application/index.tsx | 5 +- .../edit_policy/edit_policy.container.tsx | 12 +++- .../policy_table/policy_table.container.tsx | 10 ++- .../application/services/breadcrumbs.mock.ts | 13 ++++ .../application/services/breadcrumbs.ts | 67 +++++++++++++++++++ .../application/services/documentation.ts | 6 ++ .../public/application/services/http.ts | 6 ++ .../application/services/notification.ts | 6 ++ .../public/application/services/ui_metric.ts | 6 ++ .../public/plugin.tsx | 7 +- .../public/types.ts | 3 + 12 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/breadcrumbs.mock.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/breadcrumbs.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index ad61641ea1e36..0b9f47e188d15 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -14,6 +14,9 @@ import { DataTierAllocationType } from '../../../public/application/sections/edi import { Phases as PolicyPhases } from '../../../common/types'; +import { KibanaContextProvider } from '../../../public/shared_imports'; +import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; + type Phases = keyof PolicyPhases; import { POLICY_NAME } from './constants'; @@ -48,7 +51,17 @@ const testBedConfig: TestBedConfig = { }, }; -const initTestBed = registerTestBed(EditPolicy, testBedConfig); +const breadcrumbService = createBreadcrumbsMock(); + +const MyComponent = (props: any) => { + return ( + + + + ); +}; + +const initTestBed = registerTestBed(MyComponent, testBedConfig); type SetupReturn = ReturnType; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx index 7a7fd20e96c63..3d4cc7dbbd1d4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -14,17 +14,20 @@ import { KibanaContextProvider } from '../shared_imports'; import { App } from './app'; +import { BreadcrumbService } from './services/breadcrumbs'; + export const renderApp = ( element: Element, I18nContext: I18nStart['Context'], history: ScopedHistory, navigateToApp: ApplicationStart['navigateToApp'], getUrlForApp: ApplicationStart['getUrlForApp'], + breadcrumbService: BreadcrumbService, cloud?: CloudSetup ): UnmountCallback => { render( - + , diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx index dfc3e7194da06..c82a420b74857 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; + +import { useKibana } from '../../../shared_imports'; + import { useLoadPoliciesList } from '../../services/api'; import { EditPolicy as PresentationComponent } from './edit_policy'; @@ -33,7 +36,14 @@ export const EditPolicy: React.FunctionComponent { + const { + services: { breadcrumbService }, + } = useKibana(); const { error, isLoading, data: policies, resendRequest } = useLoadPoliciesList(false); + + useEffect(() => { + breadcrumbService.setBreadcrumbs('editPolicy'); + }, [breadcrumbService]); if (isLoading) { return ( = navigateToApp, history, }) => { + const { + services: { breadcrumbService }, + } = useKibana(); const { data: policies, isLoading, error, resendRequest } = useLoadPoliciesList(true); + useEffect(() => { + breadcrumbService.setBreadcrumbs('policies'); + }, [breadcrumbService]); + if (isLoading) { return ( { + const breadcrumbService = new BreadcrumbService(); + breadcrumbService.setup(jest.fn()); + return breadcrumbService; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/breadcrumbs.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/breadcrumbs.ts new file mode 100644 index 0000000000000..7f9a5b8a3dab1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/breadcrumbs.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { ChromeBreadcrumb } from 'kibana/public'; +import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; + +type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; + +// Build the breadcrumbs for this app +const breadcrumbs = (function () { + const policies: ChromeBreadcrumb[] = [ + { + text: i18n.translate('xpack.indexLifecycleMgmt.breadcrumb.homeLabel', { + defaultMessage: 'Index Lifecycle Management', + }), + href: `/policies`, + }, + ]; + + const editPolicy: ChromeBreadcrumb[] = [ + ...policies, + { + text: i18n.translate('xpack.indexLifecycleMgmt.breadcrumb.editPolicyLabel', { + defaultMessage: 'Edit policy', + }), + href: undefined, + }, + ]; + + return { + policies, + editPolicy, + }; +})(); + +export class BreadcrumbService { + private setBreadcrumbsHandler?: SetBreadcrumbs; + + public setup(setBreadcrumbsHandler: SetBreadcrumbs): void { + this.setBreadcrumbsHandler = setBreadcrumbsHandler; + } + + public setBreadcrumbs(type: keyof typeof breadcrumbs): void { + if (!this.setBreadcrumbsHandler) { + throw new Error(`BreadcrumbService#setup() must be called first!`); + } + + const newBreadcrumbs = breadcrumbs[type] ? [...breadcrumbs[type]] : [...breadcrumbs.policies]; + + // Pop off last breadcrumb + const lastBreadcrumb = newBreadcrumbs.pop() as { + text: string; + href?: string; + }; + + // Put last breadcrumb back without href + newBreadcrumbs.push({ + ...lastBreadcrumb, + href: undefined, + }); + + this.setBreadcrumbsHandler(newBreadcrumbs); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts index d459d304d5c71..b80de8a1477a0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * TODO: + * IMPORTANT: Please see how {@link BreadcrumbService} is set up for an example of how these services should be set up + * in future. The pattern in this file is legacy and should be updated to conform to the plugin lifecycle. + */ + export let skippingDisconnectedClustersUrl: string; export let remoteClustersUrl: string; export let transportPortUrl: string; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts index d61ed1ad25dde..b7761aec3fb8e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * TODO: + * IMPORTANT: Please see how {@link BreadcrumbService} is set up for an example of how these services should be set up + * in future. The pattern in this file is legacy and should be updated to conform to the plugin lifecycle. + */ + import { HttpSetup } from 'src/core/public'; import { UseRequestConfig, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts index aa3ac9ea75c22..e8ef2d9b22443 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * TODO: + * IMPORTANT: Please see how {@link BreadcrumbService} is set up for an example of how these services should be set up + * in future. The pattern in this file is legacy and should be updated to conform to the plugin lifecycle. + */ + import { IToasts, FatalErrorsSetup } from 'src/core/public'; export let toasts: IToasts; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts index 274d3d1ca97f3..a94c0a8b8ef59 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * TODO: + * IMPORTANT: Please see how {@link BreadcrumbService} is set up for an example of how these services should be set up + * in future. The pattern in this file is legacy and should be updated to conform to the plugin lifecycle. + */ + import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { UiStatsMetricType } from '@kbn/analytics'; diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index 24ce036c0e058..6300c6bfc7eb1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -12,12 +12,15 @@ import { init as initHttp } from './application/services/http'; import { init as initDocumentation } from './application/services/documentation'; import { init as initUiMetric } from './application/services/ui_metric'; import { init as initNotification } from './application/services/notification'; +import { BreadcrumbService } from './application/services/breadcrumbs'; import { addAllExtensions } from './extend_index_management'; import { PluginsDependencies, ClientConfigType } from './types'; export class IndexLifecycleManagementPlugin { constructor(private readonly initializerContext: PluginInitializerContext) {} + private breadcrumbService = new BreadcrumbService(); + public setup(coreSetup: CoreSetup, plugins: PluginsDependencies) { const { ui: { enabled: isIndexLifecycleManagementUiEnabled }, @@ -42,7 +45,7 @@ export class IndexLifecycleManagementPlugin { id: PLUGIN.ID, title: PLUGIN.TITLE, order: 2, - mount: async ({ element, history }) => { + mount: async ({ element, history, setBreadcrumbs }) => { const [coreStart] = await getStartServices(); const { chrome: { docTitle }, @@ -52,6 +55,7 @@ export class IndexLifecycleManagementPlugin { } = coreStart; docTitle.change(PLUGIN.TITLE); + this.breadcrumbService.setup(setBreadcrumbs); // Initialize additional services. initDocumentation( @@ -66,6 +70,7 @@ export class IndexLifecycleManagementPlugin { history, navigateToApp, getUrlForApp, + this.breadcrumbService, cloud ); diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts index c9b9b063cd45f..6b11830b424af 100644 --- a/x-pack/plugins/index_lifecycle_management/public/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/types.ts @@ -10,6 +10,8 @@ import { ManagementSetup } from '../../../../src/plugins/management/public'; import { IndexManagementPluginSetup } from '../../index_management/public'; import { CloudSetup } from '../../cloud/public'; +import { BreadcrumbService } from './application/services/breadcrumbs'; + export interface PluginsDependencies { usageCollection?: UsageCollectionSetup; management: ManagementSetup; @@ -25,5 +27,6 @@ export interface ClientConfigType { } export interface AppServicesContext { + breadcrumbService: BreadcrumbService; cloud?: CloudSetup; } From bc05e79b850615ae42cd9eb9e542a8d85c845799 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Thu, 5 Nov 2020 07:19:36 -0500 Subject: [PATCH 17/57] Revert "[Fleet] Allow snake cased Kibana assets (#77515)" (#82706) This reverts commit 1cd477a793ac51033ebcbda4f4435ebc6ed8b93e. --- .../package_to_package_policy.test.ts | 2 +- .../ingest_manager/common/types/models/epm.ts | 16 +-- .../ingest_manager/sections/epm/constants.tsx | 4 +- .../server/routes/data_streams/handlers.ts | 4 +- .../services/epm/kibana/assets/install.ts | 114 +++--------------- .../epm/kibana/index_pattern/install.ts | 2 +- .../ensure_installed_default_packages.test.ts | 4 +- .../epm/packages/get_install_type.test.ts | 6 +- .../server/services/epm/packages/install.ts | 5 +- .../server/services/epm/packages/remove.ts | 42 ++----- .../server/services/epm/registry/index.ts | 4 +- .../ingest_manager/server/types/index.tsx | 1 - .../apis/epm/install_remove_assets.ts | 33 ----- .../apis/epm/update_assets.ts | 8 +- .../0.1.0/kibana/index_pattern/invalid.json | 11 -- .../0.1.0/kibana/index_pattern/test-*.json | 11 -- 16 files changed, 48 insertions(+), 219 deletions(-) delete mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json delete mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test-*.json diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts index 91396bce359b0..8927b5ab3ca4b 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts @@ -25,7 +25,7 @@ describe('Ingest Manager - packageToPackagePolicy', () => { dashboard: [], visualization: [], search: [], - index_pattern: [], + 'index-pattern': [], map: [], }, }, diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index c5fc208bfb2dc..a32322ecff62a 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -35,21 +35,7 @@ export type ServiceName = 'kibana' | 'elasticsearch'; export type AgentAssetType = typeof agentAssetTypes; export type AssetType = KibanaAssetType | ElasticsearchAssetType | ValueOf; -/* - Enum mapping of a saved object asset type to how it would appear in a package file path (snake cased) -*/ export enum KibanaAssetType { - dashboard = 'dashboard', - visualization = 'visualization', - search = 'search', - indexPattern = 'index_pattern', - map = 'map', -} - -/* - Enum of saved object types that are allowed to be installed -*/ -export enum KibanaSavedObjectType { dashboard = 'dashboard', visualization = 'visualization', search = 'search', @@ -285,7 +271,7 @@ export type NotInstalled = T & { export type AssetReference = KibanaAssetReference | EsAssetReference; export type KibanaAssetReference = Pick & { - type: KibanaSavedObjectType; + type: KibanaAssetType; }; export type EsAssetReference = Pick & { type: ElasticsearchAssetType; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx index 1dad25e9cf059..da3cab1a4b8a3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx @@ -20,7 +20,7 @@ export const AssetTitleMap: Record = { ilm_policy: 'ILM Policy', ingest_pipeline: 'Ingest Pipeline', transform: 'Transform', - index_pattern: 'Index Pattern', + 'index-pattern': 'Index Pattern', index_template: 'Index Template', component_template: 'Component Template', search: 'Saved Search', @@ -36,7 +36,7 @@ export const ServiceTitleMap: Record = { export const AssetIcons: Record = { dashboard: 'dashboardApp', - index_pattern: 'indexPatternApp', + 'index-pattern': 'indexPatternApp', search: 'searchProfilerApp', visualization: 'visualizeApp', map: 'mapApp', diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts index f42f5da2695d0..652a7789f65a3 100644 --- a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -5,7 +5,7 @@ */ import { RequestHandler, SavedObjectsClientContract } from 'src/core/server'; import { DataStream } from '../../types'; -import { GetDataStreamsResponse, KibanaAssetType, KibanaSavedObjectType } from '../../../common'; +import { GetDataStreamsResponse, KibanaAssetType } from '../../../common'; import { getPackageSavedObjects, getKibanaSavedObject } from '../../services/epm/packages/get'; import { defaultIngestErrorHandler } from '../../errors'; @@ -124,7 +124,7 @@ export const getListHandler: RequestHandler = async (context, request, response) // then pick the dashboards from the package saved object const dashboards = pkgSavedObject[0].attributes?.installed_kibana?.filter( - (o) => o.type === KibanaSavedObjectType.dashboard + (o) => o.type === KibanaAssetType.dashboard ) || []; // and then pick the human-readable titles from the dashboard saved objects const enhancedDashboards = await getEnhancedDashboards( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index e7b251ef133c5..201003629e5ea 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -11,49 +11,17 @@ import { } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import * as Registry from '../../registry'; -import { - AssetType, - KibanaAssetType, - AssetReference, - AssetParts, - KibanaSavedObjectType, -} from '../../../../types'; +import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; import { savedObjectTypes } from '../../packages'; -import { indexPatternTypes } from '../index_pattern/install'; type SavedObjectToBe = Required> & { - type: KibanaSavedObjectType; + type: AssetType; }; export type ArchiveAsset = Pick< SavedObject, 'id' | 'attributes' | 'migrationVersion' | 'references' > & { - type: KibanaSavedObjectType; -}; - -// KibanaSavedObjectTypes are used to ensure saved objects being created for a given -// KibanaAssetType have the correct type -const KibanaSavedObjectTypeMapping: Record = { - [KibanaAssetType.dashboard]: KibanaSavedObjectType.dashboard, - [KibanaAssetType.indexPattern]: KibanaSavedObjectType.indexPattern, - [KibanaAssetType.map]: KibanaSavedObjectType.map, - [KibanaAssetType.search]: KibanaSavedObjectType.search, - [KibanaAssetType.visualization]: KibanaSavedObjectType.visualization, -}; - -// Define how each asset type will be installed -const AssetInstallers: Record< - KibanaAssetType, - (args: { - savedObjectsClient: SavedObjectsClientContract; - kibanaAssets: ArchiveAsset[]; - }) => Promise>> -> = { - [KibanaAssetType.dashboard]: installKibanaSavedObjects, - [KibanaAssetType.indexPattern]: installKibanaIndexPatterns, - [KibanaAssetType.map]: installKibanaSavedObjects, - [KibanaAssetType.search]: installKibanaSavedObjects, - [KibanaAssetType.visualization]: installKibanaSavedObjects, + type: AssetType; }; export async function getKibanaAsset(key: string): Promise { @@ -79,22 +47,16 @@ export function createSavedObjectKibanaAsset(asset: ArchiveAsset): SavedObjectTo export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; - kibanaAssets: Record; + kibanaAssets: ArchiveAsset[]; }): Promise { const { savedObjectsClient, kibanaAssets } = options; // install the assets const kibanaAssetTypes = Object.values(KibanaAssetType); const installedAssets = await Promise.all( - kibanaAssetTypes.map((assetType) => { - if (kibanaAssets[assetType]) { - return AssetInstallers[assetType]({ - savedObjectsClient, - kibanaAssets: kibanaAssets[assetType], - }); - } - return []; - }) + kibanaAssetTypes.map((assetType) => + installKibanaSavedObjects({ savedObjectsClient, assetType, kibanaAssets }) + ) ); return installedAssets.flat(); } @@ -112,50 +74,25 @@ export const deleteKibanaInstalledRefs = async ( installed_kibana: installedAssetsToSave, }); }; -export async function getKibanaAssets( - paths: string[] -): Promise> { - const kibanaAssetTypes = Object.values(KibanaAssetType); - const isKibanaAssetType = (path: string) => { - const parts = Registry.pathParts(path); - - return parts.service === 'kibana' && (kibanaAssetTypes as string[]).includes(parts.type); - }; - - const filteredPaths = paths - .filter(isKibanaAssetType) - .map<[string, AssetParts]>((path) => [path, Registry.pathParts(path)]); - - const assetArrays: Array> = []; - for (const assetType of kibanaAssetTypes) { - const matching = filteredPaths.filter(([path, parts]) => parts.type === assetType); - - assetArrays.push(Promise.all(matching.map(([path]) => path).map(getKibanaAsset))); - } - - const resolvedAssets = await Promise.all(assetArrays); - - const result = {} as Record; - - for (const [index, assetType] of kibanaAssetTypes.entries()) { - const expectedType = KibanaSavedObjectTypeMapping[assetType]; - const properlyTypedAssets = resolvedAssets[index].filter(({ type }) => type === expectedType); - - result[assetType] = properlyTypedAssets; - } - - return result; +export async function getKibanaAssets(paths: string[]) { + const isKibanaAssetType = (path: string) => Registry.pathParts(path).type in KibanaAssetType; + const filteredPaths = paths.filter(isKibanaAssetType); + const kibanaAssets = await Promise.all(filteredPaths.map((path) => getKibanaAsset(path))); + return kibanaAssets; } - async function installKibanaSavedObjects({ savedObjectsClient, + assetType, kibanaAssets, }: { savedObjectsClient: SavedObjectsClientContract; + assetType: KibanaAssetType; kibanaAssets: ArchiveAsset[]; }) { + const isSameType = (asset: ArchiveAsset) => assetType === asset.type; + const filteredKibanaAssets = kibanaAssets.filter((asset) => isSameType(asset)); const toBeSavedObjects = await Promise.all( - kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) + filteredKibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) ); if (toBeSavedObjects.length === 0) { @@ -168,23 +105,8 @@ async function installKibanaSavedObjects({ } } -async function installKibanaIndexPatterns({ - savedObjectsClient, - kibanaAssets, -}: { - savedObjectsClient: SavedObjectsClientContract; - kibanaAssets: ArchiveAsset[]; -}) { - // Filter out any reserved index patterns - const reservedPatterns = indexPatternTypes.map((pattern) => `${pattern}-*`); - - const nonReservedPatterns = kibanaAssets.filter((asset) => !reservedPatterns.includes(asset.id)); - - return installKibanaSavedObjects({ savedObjectsClient, kibanaAssets: nonReservedPatterns }); -} - export function toAssetReference({ id, type }: SavedObject) { - const reference: AssetReference = { id, type: type as KibanaSavedObjectType }; + const reference: AssetReference = { id, type: type as KibanaAssetType }; return reference; } 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 d18f43d62436a..4ca8e9d52c337 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 @@ -72,7 +72,6 @@ export interface IndexPatternField { readFromDocValues: boolean; } -export const indexPatternTypes = Object.values(dataTypes); // TODO: use a function overload and make pkgName and pkgVersion required for install/update // and not for an update removal. or separate out the functions export async function installIndexPatterns( @@ -117,6 +116,7 @@ export async function installIndexPatterns( const packageVersionsInfo = await Promise.all(packageVersionsFetchInfoPromise); // for each index pattern type, create an index pattern + const indexPatternTypes = Object.values(dataTypes); indexPatternTypes.forEach(async (indexPatternType) => { // 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) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts index 4ad6fc96218de..aaff5df39bac3 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchAssetType, Installation, KibanaSavedObjectType } from '../../../types'; +import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; jest.mock('./install'); @@ -41,7 +41,7 @@ const mockInstallation: SavedObject = { type: 'epm-packages', attributes: { id: 'test-pkg', - installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], es_index_patterns: { pattern: 'pattern-name' }, name: 'test package', diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts index a41511260c6e7..a04bfaafe7570 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObject } from 'src/core/server'; -import { ElasticsearchAssetType, Installation, KibanaSavedObjectType } from '../../../types'; +import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; import { getInstallType } from './install'; const mockInstallation: SavedObject = { @@ -13,7 +13,7 @@ const mockInstallation: SavedObject = { type: 'epm-packages', attributes: { id: 'test-pkg', - installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], es_index_patterns: { pattern: 'pattern-name' }, name: 'test packagek', @@ -30,7 +30,7 @@ const mockInstallationUpdateFail: SavedObject = { type: 'epm-packages', attributes: { id: 'test-pkg', - installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], es_index_patterns: { pattern: 'pattern-name' }, name: 'test packagek', diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 0496a6e9aeef1..23666162e91ef 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -18,7 +18,6 @@ import { KibanaAssetReference, EsAssetReference, InstallType, - KibanaAssetType, } from '../../../types'; import * as Registry from '../registry'; import { @@ -365,9 +364,9 @@ export async function createInstallation(options: { export const saveKibanaAssetsRefs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - kibanaAssets: Record + kibanaAssets: ArchiveAsset[] ) => { - const assetRefs = Object.values(kibanaAssets).flat().map(toAssetReference); + const assetRefs = kibanaAssets.map(toAssetReference); await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { installed_kibana: assetRefs, }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 5db47adc983c2..4b4fe9540dd95 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -12,9 +12,6 @@ import { AssetType, CallESAsCurrentUser, ElasticsearchAssetType, - EsAssetReference, - KibanaAssetReference, - Installation, } from '../../../types'; import { getInstallation, savedObjectTypes } from './index'; import { deletePipeline } from '../elasticsearch/ingest_pipeline/'; @@ -49,7 +46,7 @@ export async function removeInstallation(options: { // Delete the installed assets const installedAssets = [...installation.installed_kibana, ...installation.installed_es]; - await deleteAssets(installation, savedObjectsClient, callCluster); + await deleteAssets(installedAssets, savedObjectsClient, callCluster); // Delete the manager saved object with references to the asset objects // could also update with [] or some other state @@ -67,20 +64,17 @@ export async function removeInstallation(options: { // successful delete's in SO client return {}. return something more useful return installedAssets; } - -function deleteKibanaAssets( - installedObjects: KibanaAssetReference[], - savedObjectsClient: SavedObjectsClientContract +async function deleteAssets( + installedObjects: AssetReference[], + savedObjectsClient: SavedObjectsClientContract, + callCluster: CallESAsCurrentUser ) { - return installedObjects.map(async ({ id, type }) => { - return savedObjectsClient.delete(type, id); - }); -} - -function deleteESAssets(installedObjects: EsAssetReference[], callCluster: CallESAsCurrentUser) { - return installedObjects.map(async ({ id, type }) => { + const logger = appContextService.getLogger(); + const deletePromises = installedObjects.map(async ({ id, type }) => { const assetType = type as AssetType; - if (assetType === ElasticsearchAssetType.ingestPipeline) { + if (savedObjectTypes.includes(assetType)) { + return savedObjectsClient.delete(assetType, id); + } else if (assetType === ElasticsearchAssetType.ingestPipeline) { return deletePipeline(callCluster, id); } else if (assetType === ElasticsearchAssetType.indexTemplate) { return deleteTemplate(callCluster, id); @@ -88,22 +82,8 @@ function deleteESAssets(installedObjects: EsAssetReference[], callCluster: CallE return deleteTransforms(callCluster, [id]); } }); -} - -async function deleteAssets( - { installed_es: installedEs, installed_kibana: installedKibana }: Installation, - savedObjectsClient: SavedObjectsClientContract, - callCluster: CallESAsCurrentUser -) { - const logger = appContextService.getLogger(); - - const deletePromises: Array> = [ - ...deleteESAssets(installedEs, callCluster), - ...deleteKibanaAssets(installedKibana, savedObjectsClient), - ]; - try { - await Promise.all(deletePromises); + await Promise.all([...deletePromises]); } catch (err) { logger.error(err); } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 0172f3bb38f51..66f28fe58599a 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -242,12 +242,10 @@ export function getAsset(key: string) { } export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByType { - const kibanaAssetTypes = Object.values(KibanaAssetType); - // ASK: best way, if any, to avoid `any`? const assets = paths.reduce((map: any, path) => { const parts = pathParts(path.replace(/^\/package\//, '')); - if (parts.service === 'kibana' && kibanaAssetTypes.includes(parts.type)) { + if (parts.type in KibanaAssetType) { if (!map[parts.service]) map[parts.service] = {}; if (!map[parts.service][parts.type]) map[parts.service][parts.type] = []; map[parts.service][parts.type].push(parts); diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 5cf43d2830489..3518daa1aba63 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -56,7 +56,6 @@ export { AssetType, Installable, KibanaAssetType, - KibanaSavedObjectType, AssetParts, AssetsGroupedByServiceByType, CategoryId, 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 8e8e4f010bcb5..72ea9cb4e7ef3 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 @@ -184,16 +184,6 @@ export default function (providerContext: FtrProviderContext) { resSearch = err; } expect(resSearch.response.data.statusCode).equal(404); - let resIndexPattern; - try { - resIndexPattern = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'test-*', - }); - } catch (err) { - resIndexPattern = err; - } - expect(resIndexPattern.response.data.statusCode).equal(404); }); it('should have removed the fields from the index patterns', async () => { // The reason there is an expect inside the try and inside the catch in this test case is to guard against two @@ -355,7 +345,6 @@ const expectAssetsInstalled = ({ expect(res.statusCode).equal(200); }); it('should have installed the kibana assets', async function () { - // These are installed from Fleet along with every package const resIndexPatternLogs = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'logs-*', @@ -366,8 +355,6 @@ const expectAssetsInstalled = ({ id: 'metrics-*', }); expect(resIndexPatternMetrics.id).equal('metrics-*'); - - // These are the assets from the package const resDashboard = await kibanaServer.savedObjects.get({ type: 'dashboard', id: 'sample_dashboard', @@ -388,22 +375,6 @@ const expectAssetsInstalled = ({ id: 'sample_search', }); expect(resSearch.id).equal('sample_search'); - const resIndexPattern = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'test-*', - }); - expect(resIndexPattern.id).equal('test-*'); - - let resInvalidTypeIndexPattern; - try { - resInvalidTypeIndexPattern = await kibanaServer.savedObjects.get({ - type: 'invalid-type', - id: 'invalid', - }); - } catch (err) { - resInvalidTypeIndexPattern = err; - } - expect(resInvalidTypeIndexPattern.response.data.statusCode).equal(404); }); it('should create an index pattern with the package fields', async () => { const resIndexPatternLogs = await kibanaServer.savedObjects.get({ @@ -444,10 +415,6 @@ const expectAssetsInstalled = ({ id: 'sample_dashboard2', type: 'dashboard', }, - { - id: 'test-*', - type: 'index-pattern', - }, { id: 'sample_search', type: 'search', diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts index b16cf039f0dad..90dce92a2c6b5 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts @@ -283,14 +283,14 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_dashboard', type: 'dashboard', }, - { - id: 'sample_visualization', - type: 'visualization', - }, { id: 'sample_search2', type: 'search', }, + { + id: 'sample_visualization', + type: 'visualization', + }, ], installed_es: [ { diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json deleted file mode 100644 index bffc52ded73d6..0000000000000 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "attributes": { - "fieldFormatMap": "{}", - "fields": "[]", - "timeFieldName": "@timestamp", - "title": "invalid" - }, - "id": "invalid", - "references": [], - "type": "invalid-type" -} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test-*.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test-*.json deleted file mode 100644 index 48ba36a116709..0000000000000 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test-*.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "attributes": { - "fieldFormatMap": "{}", - "fields": "[]", - "timeFieldName": "@timestamp", - "title": "test-*" - }, - "id": "test-*", - "references": [], - "type": "index-pattern" -} From 52e8d1459cebd75b0caddfbff1700efcb21d102c Mon Sep 17 00:00:00 2001 From: "Kim S. Ly" <1557482+lykims@users.noreply.github.com> Date: Thu, 5 Nov 2020 08:33:56 -0500 Subject: [PATCH 18/57] [Lens] Remove visible title in workspace panel (#82234) --- .../workspace_panel/workspace_panel.tsx | 1 - .../workspace_panel_wrapper.scss | 11 ------- .../workspace_panel_wrapper.test.tsx | 2 -- .../workspace_panel_wrapper.tsx | 30 ++++--------------- .../test/functional/page_objects/lens_page.ts | 2 +- 5 files changed, 7 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index e79060fb77329..5a6e9af5d6ff2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -309,7 +309,6 @@ export function WorkspacePanel({ title={title} framePublicAPI={framePublicAPI} dispatch={dispatch} - emptyExpression={expression === null} visualizationState={visualizationState} visualizationId={activeVisualizationId} datasourceStates={datasourceStates} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss index 33b9b2fe1dbf0..ae9294c474b42 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss @@ -11,17 +11,6 @@ position: relative; // For positioning the dnd overlay min-height: $euiSizeXXL * 10; - .lnsWorkspacePanelWrapper__pageContentHeader { - @include euiTitle('xs'); - padding: $euiSizeM; - // override EuiPage - margin-bottom: 0 !important; // sass-lint:disable-line no-important - } - - .lnsWorkspacePanelWrapper__pageContentHeader--unsaved { - color: $euiTextSubduedColor; - } - .lnsWorkspacePanelWrapper__pageContentBody { @include euiScrollBar; flex-grow: 1; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx index f7ae7753698bb..6cca42dc1cb93 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx @@ -36,7 +36,6 @@ describe('workspace_panel_wrapper', () => { visualizationMap={{ myVis: mockVisualization }} datasourceMap={{}} datasourceStates={{}} - emptyExpression={false} > @@ -58,7 +57,6 @@ describe('workspace_panel_wrapper', () => { visualizationMap={{ myVis: { ...mockVisualization, renderToolbar: renderToolbarMock } }} datasourceMap={{}} datasourceStates={{}} - emptyExpression={false} /> ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 5cfc269dbb97b..33ddc23312a96 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -8,11 +8,9 @@ import './workspace_panel_wrapper.scss'; import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import classNames from 'classnames'; import { EuiPageContent, EuiPageContentBody, - EuiPageContentHeader, EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly, @@ -27,7 +25,6 @@ export interface WorkspacePanelWrapperProps { framePublicAPI: FramePublicAPI; visualizationState: unknown; dispatch: (action: Action) => void; - emptyExpression: boolean; title?: string; visualizationMap: Record; visualizationId: string | null; @@ -47,7 +44,6 @@ export function WorkspacePanelWrapper({ visualizationState, dispatch, title, - emptyExpression, visualizationId, visualizationMap, datasourceMap, @@ -105,26 +101,12 @@ export function WorkspacePanelWrapper({ - {!emptyExpression || title ? ( - -

- {title || - i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} -

- - ) : ( - -

- {title || - i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} -

-
- )} + +

+ {title || + i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} +

+
{children} diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index f33fbcf296786..ffb74837e9fdd 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -231,7 +231,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, getTitle() { - return testSubjects.getVisibleText('lns_ChartTitle'); + return testSubjects.getAttribute('lns_ChartTitle', 'innerText'); }, async getFiltersAggLabels() { From f4386fc5b02fb74133bc0c1c6673f7765662ff04 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 5 Nov 2020 13:41:16 +0000 Subject: [PATCH 19/57] [ML] Updating analysis config schema (#82703) * [ML] Updating analyss config schema * better schema order --- .../ml/server/routes/schemas/anomaly_detectors_schema.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 5aa7dd326af50..6e203ae18f30f 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -82,6 +82,10 @@ export const analysisConfigSchema = schema.object({ detectors: schema.arrayOf(detectorSchema), influencers: schema.arrayOf(schema.maybe(schema.string())), categorization_field_name: schema.maybe(schema.string()), + categorization_analyzer: schema.maybe(schema.any()), + categorization_filters: schema.maybe(schema.arrayOf(schema.string())), + latency: schema.maybe(schema.number()), + multivariate_by_fields: schema.maybe(schema.boolean()), per_partition_categorization: schema.maybe( schema.object({ enabled: schema.boolean(), From 051ed138586e9a81618269d0ab725ea210f8e87b Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 5 Nov 2020 08:56:11 -0500 Subject: [PATCH 20/57] [Maps] Convert ES-sources to typescript (#81951) --- src/plugins/region_map/config.ts | 39 +-- x-pack/plugins/maps/common/constants.ts | 10 +- .../data_request_descriptor_types.ts | 8 +- .../source_descriptor_types.ts | 27 +- .../elasticsearch_geo_utils.d.ts | 32 +- .../common/elasticsearch_util/es_agg_utils.ts | 12 +- .../migrations/top_hits_time_to_sort.js | 5 +- .../classes/fields/kibana_region_field.ts | 6 +- .../maps/public/classes/joins/inner_join.d.ts | 7 +- .../plugins/maps/public/classes/joins/join.ts | 8 +- .../blended_vector_layer.ts | 15 +- .../maps/public/classes/layers/layer.tsx | 2 +- .../create_layer_descriptor.test.ts | 2 + .../security/create_layer_descriptors.test.ts | 8 +- .../layers/vector_layer/vector_layer.tsx | 18 +- .../ems_file_source/ems_file_source.tsx | 13 +- .../sources/ems_tms_source/ems_tms_source.js | 22 +- .../sources/es_agg_source/es_agg_source.ts | 20 +- .../es_geo_grid_source.d.ts | 46 --- ..._grid_source.js => es_geo_grid_source.tsx} | 220 +++++++++----- .../es_pew_pew_source/es_pew_pew_source.js | 15 +- .../{constants.js => constants.ts} | 2 +- .../es_documents_layer_wizard.tsx | 11 +- .../es_search_source/es_search_source.d.ts | 26 -- .../es_search_source/es_search_source.test.ts | 37 ++- ..._search_source.js => es_search_source.tsx} | 283 ++++++++++++------ ...dex_settings.js => load_index_settings.ts} | 16 +- .../es_search_source/update_source_editor.js | 8 +- .../classes/sources/es_source/es_source.d.ts | 86 ------ .../es_source/{es_source.js => es_source.ts} | 260 +++++++++++----- .../es_term_source/es_term_source.d.ts | 22 -- .../es_term_source/es_term_source.test.js | 2 + .../{es_term_source.js => es_term_source.ts} | 61 ++-- .../geojson_file_source.ts | 13 +- .../kibana_regionmap_layer_wizard.tsx | 2 +- .../kibana_regionmap_source.d.ts | 15 - ...p_source.js => kibana_regionmap_source.ts} | 53 ++-- .../mvt_single_layer_vector_source.tsx | 13 +- .../maps/public/classes/sources/source.ts | 13 +- .../public/classes/sources/source_registry.ts | 3 +- .../sources/vector_source/vector_source.d.ts | 108 ------- .../sources/vector_source/vector_source.js | 140 --------- .../sources/vector_source/vector_source.tsx | 209 +++++++++++++ .../classes/util/valid_string_config.ts | 13 + x-pack/plugins/maps/public/meta.ts | 3 +- .../maps/public/selectors/map_selectors.ts | 9 +- 46 files changed, 1065 insertions(+), 878 deletions(-) delete mode 100644 x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.d.ts rename x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/{es_geo_grid_source.js => es_geo_grid_source.tsx} (63%) rename x-pack/plugins/maps/public/classes/sources/es_search_source/{constants.js => constants.ts} (80%) delete mode 100644 x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts rename x-pack/plugins/maps/public/classes/sources/es_search_source/{es_search_source.js => es_search_source.tsx} (68%) rename x-pack/plugins/maps/public/classes/sources/es_search_source/{load_index_settings.js => load_index_settings.ts} (79%) delete mode 100644 x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts rename x-pack/plugins/maps/public/classes/sources/es_source/{es_source.js => es_source.ts} (51%) delete mode 100644 x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.d.ts rename x-pack/plugins/maps/public/classes/sources/es_term_source/{es_term_source.js => es_term_source.ts} (65%) delete mode 100644 x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.d.ts rename x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/{kibana_regionmap_source.js => kibana_regionmap_source.ts} (55%) delete mode 100644 x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts delete mode 100644 x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js create mode 100644 x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx create mode 100644 x-pack/plugins/maps/public/classes/util/valid_string_config.ts diff --git a/src/plugins/region_map/config.ts b/src/plugins/region_map/config.ts index a721a76ca0a82..d60831563ed2b 100644 --- a/src/plugins/region_map/config.ts +++ b/src/plugins/region_map/config.ts @@ -19,28 +19,29 @@ import { schema, TypeOf } from '@kbn/config-schema'; -export const configSchema = schema.object({ - includeElasticMapsService: schema.boolean({ defaultValue: true }), - layers: schema.arrayOf( +const layerConfigSchema = schema.object({ + url: schema.string(), + format: schema.object({ + type: schema.string({ defaultValue: 'geojson' }), + }), + meta: schema.object({ + feature_collection_path: schema.string({ defaultValue: 'data' }), + }), + attribution: schema.string(), + name: schema.string(), + fields: schema.arrayOf( schema.object({ - url: schema.string(), - format: schema.object({ - type: schema.string({ defaultValue: 'geojson' }), - }), - meta: schema.object({ - feature_collection_path: schema.string({ defaultValue: 'data' }), - }), - attribution: schema.string(), name: schema.string(), - fields: schema.arrayOf( - schema.object({ - name: schema.string(), - description: schema.string(), - }) - ), - }), - { defaultValue: [] } + description: schema.string(), + }) ), }); +export const configSchema = schema.object({ + includeElasticMapsService: schema.boolean({ defaultValue: true }), + layers: schema.arrayOf(layerConfigSchema, { defaultValue: [] }), +}); + +export type LayerConfig = TypeOf; + export type ConfigSchema = TypeOf; diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 469a4023434a8..bcfe11851d1ea 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -60,11 +60,6 @@ export enum LAYER_TYPE { TILED_VECTOR = 'TILED_VECTOR', // similar to a regular vector-layer, but it consumes the data as .mvt tilea iso GeoJson. It supports similar ad-hoc configurations like a regular vector layer (E.g. using IVectorStyle), although there is some loss of functionality e.g. does not support term joining } -export enum SORT_ORDER { - ASC = 'asc', - DESC = 'desc', -} - export enum SOURCE_TYPES { EMS_TMS = 'EMS_TMS', EMS_FILE = 'EMS_FILE', @@ -237,6 +232,11 @@ export enum SCALING_TYPES { MVT = 'MVT', } +export enum FORMAT_TYPE { + GEOJSON = 'geojson', + TOPOJSON = 'topojson', +} + export enum MVT_FIELD_TYPE { STRING = 'String', NUMBER = 'Number', diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index 16b60492c9b78..fc691f339f34a 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -5,7 +5,9 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants'; +import { Query } from 'src/plugins/data/public'; +import { SortDirection } from 'src/plugins/data/common/search'; +import { RENDER_AS, SCALING_TYPES } from '../constants'; import { MapExtent, MapQuery } from './map_descriptor'; import { Filter, TimeRange } from '../../../../../src/plugins/data/common'; @@ -22,7 +24,7 @@ export type MapFilters = { type ESSearchSourceSyncMeta = { sortField: string; - sortOrder: SORT_ORDER; + sortOrder: SortDirection; scalingType: SCALING_TYPES; topHitsSplitField: string; topHitsSize: number; @@ -45,7 +47,7 @@ export type VectorSourceRequestMeta = MapFilters & { export type VectorJoinSourceRequestMeta = MapFilters & { applyGlobalQuery: boolean; fieldNames: string[]; - sourceQuery: MapQuery; + sourceQuery?: Query; }; export type VectorStyleRequestMeta = MapFilters & { diff --git a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts index 400b6a41ead71..3dc90a12513fd 100644 --- a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts @@ -7,14 +7,8 @@ import { FeatureCollection } from 'geojson'; import { Query } from 'src/plugins/data/public'; -import { - AGG_TYPE, - GRID_RESOLUTION, - RENDER_AS, - SORT_ORDER, - SCALING_TYPES, - MVT_FIELD_TYPE, -} from '../constants'; +import { SortDirection } from 'src/plugins/data/common/search'; +import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SCALING_TYPES, MVT_FIELD_TYPE } from '../constants'; export type AttributionDescriptor = { attributionText?: string; @@ -40,6 +34,7 @@ export type EMSFileSourceDescriptor = AbstractSourceDescriptor & { export type AbstractESSourceDescriptor = AbstractSourceDescriptor & { // id: UUID + id: string; indexPatternId: string; geoField?: string; }; @@ -55,18 +50,20 @@ export type AbstractESAggSourceDescriptor = AbstractESSourceDescriptor & { }; export type ESGeoGridSourceDescriptor = AbstractESAggSourceDescriptor & { - requestType?: RENDER_AS; - resolution?: GRID_RESOLUTION; + geoField: string; + requestType: RENDER_AS; + resolution: GRID_RESOLUTION; }; export type ESSearchSourceDescriptor = AbstractESSourceDescriptor & { + geoField: string; filterByMapBounds?: boolean; tooltipProperties?: string[]; - sortField?: string; - sortOrder?: SORT_ORDER; + sortField: string; + sortOrder: SortDirection; scalingType: SCALING_TYPES; - topHitsSplitField?: string; - topHitsSize?: number; + topHitsSplitField: string; + topHitsSize: number; }; export type ESPewPewSourceDescriptor = AbstractESAggSourceDescriptor & { @@ -76,7 +73,7 @@ export type ESPewPewSourceDescriptor = AbstractESAggSourceDescriptor & { export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & { indexPatternTitle?: string; - term?: string; // term field name + term: string; // term field name whereQuery?: Query; }; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts index cff8ba119e1de..a757a78cd210b 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FeatureCollection, GeoJsonProperties } from 'geojson'; +import { FeatureCollection, GeoJsonProperties, Polygon } from 'geojson'; import { MapExtent } from '../descriptor_types'; -import { ES_GEO_FIELD_TYPE } from '../constants'; +import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent; @@ -23,3 +23,31 @@ export function hitsToGeoJson( geoFieldType: ES_GEO_FIELD_TYPE, epochMillisFields: string[] ): FeatureCollection; + +export interface ESBBox { + top_left: number[]; + bottom_right: number[]; +} + +export interface ESGeoBoundingBoxFilter { + geo_bounding_box: { + [geoFieldName: string]: ESBBox; + }; +} + +export interface ESPolygonFilter { + geo_shape: { + [geoFieldName: string]: { + shape: Polygon; + relation: ES_SPATIAL_RELATIONS.INTERSECTS; + }; + }; +} + +export function createExtentFilter( + mapExtent: MapExtent, + geoFieldName: string, + geoFieldType: ES_GEO_FIELD_TYPE +): ESPolygonFilter | ESGeoBoundingBoxFilter; + +export function makeESBbox({ maxLat, maxLon, minLat, minLon }: MapExtent): ESBBox; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts index f157ffe9f1c80..99c1fa3070fb9 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts @@ -8,7 +8,10 @@ import _ from 'lodash'; import { IndexPattern, IFieldType } from '../../../../../src/plugins/data/common'; import { TOP_TERM_PERCENTAGE_SUFFIX } from '../constants'; -export function getField(indexPattern: IndexPattern, fieldName: string) { +export type BucketProperties = Record; +export type PropertiesMap = Map; + +export function getField(indexPattern: IndexPattern, fieldName: string): IFieldType { const field = indexPattern.fields.getByName(fieldName); if (!field) { throw new Error( @@ -33,9 +36,10 @@ export function addFieldToDSL(dsl: object, field: IFieldType) { }; } -export type BucketProperties = Record; - -export function extractPropertiesFromBucket(bucket: any, ignoreKeys: string[] = []) { +export function extractPropertiesFromBucket( + bucket: any, + ignoreKeys: string[] = [] +): BucketProperties { const properties: BucketProperties = {}; for (const key in bucket) { if (ignoreKeys.includes(key) || !bucket.hasOwnProperty(key)) { diff --git a/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js b/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js index 6b7a5931255c5..9b63db63b967d 100644 --- a/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js +++ b/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js @@ -5,7 +5,8 @@ */ import _ from 'lodash'; -import { SOURCE_TYPES, SORT_ORDER } from '../constants'; +import { SOURCE_TYPES } from '../constants'; +import { SortDirection } from '../../../../../src/plugins/data/common/search'; function isEsDocumentSource(layerDescriptor) { const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); @@ -23,7 +24,7 @@ export function topHitsTimeToSort({ attributes }) { if (_.has(layerDescriptor, 'sourceDescriptor.topHitsTimeField')) { layerDescriptor.sourceDescriptor.sortField = layerDescriptor.sourceDescriptor.topHitsTimeField; - layerDescriptor.sourceDescriptor.sortOrder = SORT_ORDER.DESC; + layerDescriptor.sourceDescriptor.sortOrder = SortDirection.desc; delete layerDescriptor.sourceDescriptor.topHitsTimeField; } } diff --git a/x-pack/plugins/maps/public/classes/fields/kibana_region_field.ts b/x-pack/plugins/maps/public/classes/fields/kibana_region_field.ts index ce72f01adb5f8..622cf68948f99 100644 --- a/x-pack/plugins/maps/public/classes/fields/kibana_region_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/kibana_region_field.ts @@ -5,12 +5,12 @@ */ import { IField, AbstractField } from './field'; -import { IKibanaRegionSource } from '../sources/kibana_regionmap_source/kibana_regionmap_source'; +import { KibanaRegionmapSource } from '../sources/kibana_regionmap_source/kibana_regionmap_source'; import { FIELD_ORIGIN } from '../../../common/constants'; import { IVectorSource } from '../sources/vector_source'; export class KibanaRegionField extends AbstractField implements IField { - private readonly _source: IKibanaRegionSource; + private readonly _source: KibanaRegionmapSource; constructor({ fieldName, @@ -18,7 +18,7 @@ export class KibanaRegionField extends AbstractField implements IField { origin, }: { fieldName: string; - source: IKibanaRegionSource; + source: KibanaRegionmapSource; origin: FIELD_ORIGIN; }) { super({ fieldName, origin }); diff --git a/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts b/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts index 3e2ceac4971c4..987e7bc93c2f6 100644 --- a/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts +++ b/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts @@ -5,19 +5,20 @@ */ import { Feature, GeoJsonProperties } from 'geojson'; -import { IESTermSource } from '../sources/es_term_source'; -import { IJoin, PropertiesMap } from './join'; +import { ESTermSource } from '../sources/es_term_source'; +import { IJoin } from './join'; import { JoinDescriptor } from '../../../common/descriptor_types'; import { ISource } from '../sources/source'; import { ITooltipProperty } from '../tooltips/tooltip_property'; import { IField } from '../fields/field'; +import { PropertiesMap } from '../../../common/elasticsearch_util'; export class InnerJoin implements IJoin { constructor(joinDescriptor: JoinDescriptor, leftSource: ISource); destroy: () => void; - getRightJoinSource(): IESTermSource; + getRightJoinSource(): ESTermSource; toDescriptor(): JoinDescriptor; diff --git a/x-pack/plugins/maps/public/classes/joins/join.ts b/x-pack/plugins/maps/public/classes/joins/join.ts index df6f6f684f4d2..465ffbda27303 100644 --- a/x-pack/plugins/maps/public/classes/joins/join.ts +++ b/x-pack/plugins/maps/public/classes/joins/join.ts @@ -5,18 +5,16 @@ */ import { Feature, GeoJsonProperties } from 'geojson'; -import { IESTermSource } from '../sources/es_term_source'; +import { ESTermSource } from '../sources/es_term_source'; import { JoinDescriptor } from '../../../common/descriptor_types'; import { ITooltipProperty } from '../tooltips/tooltip_property'; import { IField } from '../fields/field'; -import { BucketProperties } from '../../../common/elasticsearch_util'; - -export type PropertiesMap = Map; +import { PropertiesMap } from '../../../common/elasticsearch_util'; export interface IJoin { destroy: () => void; - getRightJoinSource: () => IESTermSource; + getRightJoinSource: () => ESTermSource; toDescriptor: () => JoinDescriptor; diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index 65a76f0c54ffb..2ab8a70f2e4df 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -25,7 +25,6 @@ import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_so import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { IVectorLayer } from '../vector_layer/vector_layer'; import { IESSource } from '../../sources/es_source'; -import { IESAggSource } from '../../sources/es_agg_source'; import { ISource } from '../../sources/source'; import { DataRequestContext } from '../../../actions'; import { DataRequestAbortError } from '../../util/data_request'; @@ -36,9 +35,11 @@ import { StylePropertyOptions, LayerDescriptor, VectorLayerDescriptor, + VectorSourceRequestMeta, } from '../../../../common/descriptor_types'; import { IVectorSource } from '../../sources/vector_source'; import { LICENSED_FEATURES } from '../../../licensed_features'; +import { ESSearchSource } from '../../sources/es_search_source/es_search_source'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; @@ -50,7 +51,7 @@ function getAggType(dynamicProperty: IDynamicStyleProperty { const source = this.getSource(); return await source.getImmutableProperties(); } diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts index e6349fbe9ab9d..66eba3a539801 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts @@ -175,6 +175,7 @@ describe('createLayerDescriptor', () => { query: 'processor.event:"transaction"', }, sourceDescriptor: { + applyGlobalQuery: true, geoField: 'client.geo.location', id: '12345', indexPatternId: 'apm_static_index_pattern_id', @@ -216,6 +217,7 @@ describe('createLayerDescriptor', () => { query: 'processor.event:"transaction"', }, sourceDescriptor: { + applyGlobalQuery: true, geoField: 'client.geo.location', id: '12345', indexPatternId: 'apm_static_index_pattern_id', diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts index d02f07923c682..22456527491eb 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts @@ -21,7 +21,7 @@ jest.mock('uuid/v4', () => { import { createSecurityLayerDescriptors } from './create_layer_descriptors'; describe('createLayerDescriptor', () => { - test('amp index', () => { + test('apm index', () => { expect(createSecurityLayerDescriptors('id', 'apm-*-transaction*')).toEqual([ { __dataRequests: [], @@ -32,6 +32,7 @@ describe('createLayerDescriptor', () => { maxZoom: 24, minZoom: 0, sourceDescriptor: { + applyGlobalQuery: true, filterByMapBounds: true, geoField: 'client.geo.location', id: '12345', @@ -138,6 +139,7 @@ describe('createLayerDescriptor', () => { maxZoom: 24, minZoom: 0, sourceDescriptor: { + applyGlobalQuery: true, filterByMapBounds: true, geoField: 'server.geo.location', id: '12345', @@ -244,6 +246,7 @@ describe('createLayerDescriptor', () => { maxZoom: 24, minZoom: 0, sourceDescriptor: { + applyGlobalQuery: true, destGeoField: 'server.geo.location', id: '12345', indexPatternId: 'id', @@ -362,6 +365,7 @@ describe('createLayerDescriptor', () => { maxZoom: 24, minZoom: 0, sourceDescriptor: { + applyGlobalQuery: true, filterByMapBounds: true, geoField: 'source.geo.location', id: '12345', @@ -468,6 +472,7 @@ describe('createLayerDescriptor', () => { maxZoom: 24, minZoom: 0, sourceDescriptor: { + applyGlobalQuery: true, filterByMapBounds: true, geoField: 'destination.geo.location', id: '12345', @@ -574,6 +579,7 @@ describe('createLayerDescriptor', () => { maxZoom: 24, minZoom: 0, sourceDescriptor: { + applyGlobalQuery: true, destGeoField: 'destination.geo.location', id: '12345', indexPatternId: 'id', diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index c44ebcf969f7c..b9d7834896245 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -46,18 +46,20 @@ import { DynamicStylePropertyOptions, MapFilters, MapQuery, + VectorJoinSourceRequestMeta, VectorLayerDescriptor, VectorSourceRequestMeta, VectorStyleRequestMeta, } from '../../../../common/descriptor_types'; import { IVectorSource } from '../../sources/vector_source'; import { CustomIconAndTooltipContent, ILayer } from '../layer'; -import { IJoin, PropertiesMap } from '../../joins/join'; +import { IJoin } from '../../joins/join'; import { IField } from '../../fields/field'; import { DataRequestContext } from '../../../actions'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; import { IESSource } from '../../sources/es_source'; +import { PropertiesMap } from '../../../../common/elasticsearch_util'; interface SourceResult { refreshed: boolean; @@ -239,7 +241,7 @@ export class VectorLayer extends AbstractLayer { } const requestToken = Symbol(`${SOURCE_BOUNDS_DATA_REQUEST_ID}-${this.getId()}`); - const searchFilters = this._getSearchFilters( + const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( dataFilters, this.getSource(), this.getCurrentStyle() @@ -324,7 +326,7 @@ export class VectorLayer extends AbstractLayer { const joinSource = join.getRightJoinSource(); const sourceDataId = join.getSourceDataRequestId(); const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); - const searchFilters = { + const searchFilters: VectorJoinSourceRequestMeta = { ...dataFilters, fieldNames: joinSource.getFieldNames(), sourceQuery: joinSource.getWhereQuery(), @@ -386,9 +388,11 @@ export class VectorLayer extends AbstractLayer { source: IVectorSource, style: IVectorStyle ): VectorSourceRequestMeta { + const styleFieldNames = + style.getType() === LAYER_STYLE_TYPE.VECTOR ? style.getSourceFieldNames() : []; const fieldNames = [ ...source.getFieldNames(), - ...(style.getType() === LAYER_STYLE_TYPE.VECTOR ? style.getSourceFieldNames() : []), + ...styleFieldNames, ...this.getValidJoins().map((join) => join.getLeftField().getName()), ]; @@ -464,7 +468,11 @@ export class VectorLayer extends AbstractLayer { } = syncContext; const dataRequestId = SOURCE_DATA_REQUEST_ID; const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); - const searchFilters = this._getSearchFilters(dataFilters, source, style); + const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( + dataFilters, + source, + style + ); const prevDataRequest = this.getSourceDataRequest(); const canSkipFetch = await canSkipSourceUpdate({ source, diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx index 38e13a68437c7..780b771336b34 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx @@ -11,7 +11,12 @@ import { Adapters } from 'src/plugins/inspector/public'; import { FileLayer } from '@elastic/ems-client'; import { Attribution, ImmutableSourceProperty, SourceEditorArgs } from '../source'; import { AbstractVectorSource, GeoJsonWithMeta, IVectorSource } from '../vector_source'; -import { SOURCE_TYPES, FIELD_ORIGIN, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; +import { + SOURCE_TYPES, + FIELD_ORIGIN, + VECTOR_SHAPE_TYPE, + FORMAT_TYPE, +} from '../../../../common/constants'; import { getEmsFileLayers } from '../../../meta'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { UpdateSourceEditor } from './update_source_editor'; @@ -30,11 +35,9 @@ export const sourceTitle = i18n.translate('xpack.maps.source.emsFileTitle', { }); export class EMSFileSource extends AbstractVectorSource implements IEmsFileSource { - static type = SOURCE_TYPES.EMS_FILE; - static createDescriptor({ id, tooltipProperties = [] }: Partial) { return { - type: EMSFileSource.type, + type: SOURCE_TYPES.EMS_FILE, id: id!, tooltipProperties, }; @@ -99,7 +102,7 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc const emsFileLayer = await this.getEMSFileLayer(); // @ts-ignore const featureCollection = await AbstractVectorSource.getGeoJson({ - format: emsFileLayer.getDefaultFormatType(), + format: emsFileLayer.getDefaultFormatType() as FORMAT_TYPE, featureCollectionPath: 'data', fetchUrl: emsFileLayer.getDefaultFormatUrl(), }); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js index b364dd32860f3..b294f201def4c 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import React from 'react'; import { AbstractTMSSource } from '../tms_source'; import { getEmsTmsServices } from '../../../meta'; @@ -20,25 +19,18 @@ export const sourceTitle = i18n.translate('xpack.maps.source.emsTileTitle', { }); export class EMSTMSSource extends AbstractTMSSource { - static type = SOURCE_TYPES.EMS_TMS; - - static createDescriptor(sourceConfig) { + static createDescriptor(descriptor) { return { - type: EMSTMSSource.type, - id: sourceConfig.id, - isAutoSelect: sourceConfig.isAutoSelect, + type: SOURCE_TYPES.EMS_TMS, + id: descriptor.id, + isAutoSelect: + typeof descriptor.isAutoSelect !== 'undefined' ? !!descriptor.isAutoSelect : false, }; } constructor(descriptor, inspectorAdapters) { - super( - { - id: descriptor.id, - type: EMSTMSSource.type, - isAutoSelect: _.get(descriptor, 'isAutoSelect', false), - }, - inspectorAdapters - ); + descriptor = EMSTMSSource.createDescriptor(descriptor); + super(descriptor, inspectorAdapters); } renderSourceSettingsEditor({ onChange }) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts index 5c062f3419e28..dc95632032fa9 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts @@ -33,9 +33,21 @@ export abstract class AbstractESAggSource extends AbstractESSource { private readonly _metricFields: IESAggField[]; private readonly _canReadFromGeoJson: boolean; + static createDescriptor( + descriptor: Partial + ): AbstractESAggSourceDescriptor { + const normalizedDescriptor = AbstractESSource.createDescriptor(descriptor); + return { + ...normalizedDescriptor, + type: descriptor.type ? descriptor.type : '', + metrics: + descriptor.metrics && descriptor.metrics.length > 0 ? descriptor.metrics : [DEFAULT_METRIC], + }; + } + constructor( descriptor: AbstractESAggSourceDescriptor, - inspectorAdapters: Adapters, + inspectorAdapters?: Adapters, canReadFromGeoJson = true ) { super(descriptor, inspectorAdapters); @@ -55,7 +67,7 @@ export abstract class AbstractESAggSource extends AbstractESSource { } } - getFieldByName(fieldName: string) { + getFieldByName(fieldName: string): IField | null { return this.getMetricFieldForName(fieldName); } @@ -113,7 +125,7 @@ export abstract class AbstractESAggSource extends AbstractESSource { } } - async getFields() { + async getFields(): Promise { return this.getMetricFields(); } @@ -128,7 +140,7 @@ export abstract class AbstractESAggSource extends AbstractESSource { return valueAggsDsl; } - async getTooltipProperties(properties: GeoJsonProperties) { + async getTooltipProperties(properties: GeoJsonProperties): Promise { const metricFields = await this.getFields(); const promises: Array> = []; metricFields.forEach((metricField) => { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.d.ts deleted file mode 100644 index b221d13bb0f8a..0000000000000 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.d.ts +++ /dev/null @@ -1,46 +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 { AbstractESAggSource } from '../es_agg_source'; -import { - ESGeoGridSourceDescriptor, - MapFilters, - MapQuery, - VectorSourceSyncMeta, - VectorSourceRequestMeta, -} from '../../../../common/descriptor_types'; -import { GRID_RESOLUTION } from '../../../../common/constants'; -import { IField } from '../../fields/field'; -import { ITiledSingleLayerVectorSource } from '../vector_source'; - -export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingleLayerVectorSource { - static createDescriptor({ - indexPatternId, - geoField, - requestType, - resolution, - }: Partial): ESGeoGridSourceDescriptor; - - constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown); - - readonly _descriptor: ESGeoGridSourceDescriptor; - - getFieldNames(): string[]; - getGridResolution(): GRID_RESOLUTION; - getGeoGridPrecision(zoom: number): number; - createField({ fieldName }: { fieldName: string }): IField; - - getLayerName(): string; - - getUrlTemplateWithMeta( - searchFilters: VectorSourceRequestMeta - ): Promise<{ - layerName: string; - urlTemplate: string; - minSourceZoom: number; - maxSourceZoom: number; - }>; -} diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx similarity index 63% rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx index 181af6b17b7dd..6ec51b8e118cb 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx @@ -4,37 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import uuid from 'uuid/v4'; +import React, { ReactElement } from 'react'; +import { i18n } from '@kbn/i18n'; +import rison from 'rison-node'; +import { Feature } from 'geojson'; +import { SearchResponse } from 'elasticsearch'; import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson, makeESBbox, } from '../../../../common/elasticsearch_util'; +// @ts-expect-error import { UpdateSourceEditor } from './update_source_editor'; import { - SOURCE_TYPES, DEFAULT_MAX_BUCKETS_LIMIT, - RENDER_AS, - GRID_RESOLUTION, - VECTOR_SHAPE_TYPE, - MVT_SOURCE_LAYER_NAME, + ES_GEO_FIELD_TYPE, + GEOCENTROID_AGG_NAME, + GEOTILE_GRID_AGG_NAME, GIS_API_PATH, + GRID_RESOLUTION, MVT_GETGRIDTILE_API_PATH, - GEOTILE_GRID_AGG_NAME, - GEOCENTROID_AGG_NAME, - ES_GEO_FIELD_TYPE, + MVT_SOURCE_LAYER_NAME, + RENDER_AS, + SOURCE_TYPES, + VECTOR_SHAPE_TYPE, } from '../../../../common/constants'; -import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; -import { AbstractESAggSource, DEFAULT_METRIC } from '../es_agg_source'; +import { AbstractESAggSource } from '../es_agg_source'; import { DataRequestAbortError } from '../../util/data_request'; import { registerSource } from '../source_registry'; import { LICENSED_FEATURES } from '../../../licensed_features'; -import rison from 'rison-node'; import { getHttp } from '../../../kibana_services'; +import { GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; +import { + ESGeoGridSourceDescriptor, + MapExtent, + VectorSourceRequestMeta, + VectorSourceSyncMeta, +} from '../../../../common/descriptor_types'; +import { ImmutableSourceProperty, SourceEditorArgs } from '../source'; +import { ISearchSource } from '../../../../../../../src/plugins/data/common/search/search_source'; +import { IndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; +import { isValidStringConfig } from '../../util/valid_string_config'; export const MAX_GEOTILE_LEVEL = 29; @@ -46,31 +60,41 @@ export const heatmapTitle = i18n.translate('xpack.maps.source.esGridHeatmapTitle defaultMessage: 'Heat map', }); -export class ESGeoGridSource extends AbstractESAggSource { - static type = SOURCE_TYPES.ES_GEO_GRID; - - static createDescriptor({ indexPatternId, geoField, metrics, requestType, resolution }) { +export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingleLayerVectorSource { + static createDescriptor( + descriptor: Partial + ): ESGeoGridSourceDescriptor { + const normalizedDescriptor = AbstractESAggSource.createDescriptor(descriptor); + if (!isValidStringConfig(normalizedDescriptor.geoField)) { + throw new Error('Cannot create an ESGeoGridSourceDescriptor without a geoField'); + } return { - type: ESGeoGridSource.type, - id: uuid(), - indexPatternId, - geoField, - metrics: metrics ? metrics : [DEFAULT_METRIC], - requestType, - resolution: resolution ? resolution : GRID_RESOLUTION.COARSE, + ...normalizedDescriptor, + type: SOURCE_TYPES.ES_GEO_GRID, + geoField: normalizedDescriptor.geoField!, + requestType: descriptor.requestType || RENDER_AS.POINT, + resolution: descriptor.resolution ? descriptor.resolution : GRID_RESOLUTION.COARSE, }; } - constructor(descriptor, inspectorAdapters) { - super(descriptor, inspectorAdapters, descriptor.resolution !== GRID_RESOLUTION.SUPER_FINE); + readonly _descriptor: ESGeoGridSourceDescriptor; + + constructor(descriptor: Partial, inspectorAdapters?: Adapters) { + const sourceDescriptor = ESGeoGridSource.createDescriptor(descriptor); + super( + sourceDescriptor, + inspectorAdapters, + descriptor.resolution !== GRID_RESOLUTION.SUPER_FINE + ); + this._descriptor = sourceDescriptor; } - renderSourceSettingsEditor({ onChange, currentLayerType }) { + renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement { return ( { + let indexPatternName = this.getIndexPatternId(); try { const indexPattern = await this.getIndexPattern(); - indexPatternTitle = indexPattern.title; + indexPatternName = indexPattern.title; } catch (error) { // ignore error, title will just default to id } @@ -102,7 +126,7 @@ export class ESGeoGridSource extends AbstractESAggSource { label: i18n.translate('xpack.maps.source.esGrid.indexPatternLabel', { defaultMessage: 'Index pattern', }), - value: indexPatternTitle, + value: indexPatternName, }, { label: i18n.translate('xpack.maps.source.esGrid.geospatialFieldLabel', { @@ -117,7 +141,7 @@ export class ESGeoGridSource extends AbstractESAggSource { return this.getMetricFields().map((esAggMetricField) => esAggMetricField.getName()); } - isGeoGridPrecisionAware() { + isGeoGridPrecisionAware(): boolean { if (this._descriptor.resolution === GRID_RESOLUTION.SUPER_FINE) { // MVT gridded data should not bootstrap each time the precision changes // mapbox-gl needs to handle this @@ -128,15 +152,15 @@ export class ESGeoGridSource extends AbstractESAggSource { } } - showJoinEditor() { + showJoinEditor(): boolean { return false; } - getGridResolution() { + getGridResolution(): GRID_RESOLUTION { return this._descriptor.resolution; } - getGeoGridPrecision(zoom) { + getGeoGridPrecision(zoom: number): number { if (this._descriptor.resolution === GRID_RESOLUTION.SUPER_FINE) { // The target-precision needs to be determined server side. return NaN; @@ -178,9 +202,18 @@ export class ESGeoGridSource extends AbstractESAggSource { bucketsPerGrid, isRequestStillActive, bufferedExtent, + }: { + searchSource: ISearchSource; + indexPattern: IndexPattern; + precision: number; + layerName: string; + registerCancelCallback: (callback: () => void) => void; + bucketsPerGrid: number; + isRequestStillActive: () => boolean; + bufferedExtent: MapExtent; }) { - const gridsPerRequest = Math.floor(DEFAULT_MAX_BUCKETS_LIMIT / bucketsPerGrid); - const aggs = { + const gridsPerRequest: number = Math.floor(DEFAULT_MAX_BUCKETS_LIMIT / bucketsPerGrid); + const aggs: any = { compositeSplit: { composite: { size: gridsPerRequest, @@ -232,8 +265,10 @@ export class ESGeoGridSource extends AbstractESAggSource { aggs.compositeSplit.composite.after = afterKey; } searchSource.setField('aggs', aggs); - const requestId = afterKey ? `${this.getId()} afterKey ${afterKey.geoSplit}` : this.getId(); - const esResponse = await this._runEsQuery({ + const requestId: string = afterKey + ? `${this.getId()} afterKey ${afterKey.geoSplit}` + : this.getId(); + const esResponse: SearchResponse = await this._runEsQuery({ requestId, requestName: `${layerName} (${requestCount})`, searchSource, @@ -259,7 +294,12 @@ export class ESGeoGridSource extends AbstractESAggSource { return features; } - _addNonCompositeAggsToSearchSource(searchSource, indexPattern, precision, bufferedExtent) { + _addNonCompositeAggsToSearchSource( + searchSource: ISearchSource, + indexPattern: IndexPattern, + precision: number | null, + bufferedExtent?: MapExtent | null + ) { searchSource.setField('aggs', { [GEOTILE_GRID_AGG_NAME]: { geotile_grid: { @@ -290,7 +330,14 @@ export class ESGeoGridSource extends AbstractESAggSource { layerName, registerCancelCallback, bufferedExtent, - }) { + }: { + searchSource: ISearchSource; + indexPattern: IndexPattern; + precision: number; + layerName: string; + registerCancelCallback: (callback: () => void) => void; + bufferedExtent?: MapExtent; + }): Promise { this._addNonCompositeAggsToSearchSource(searchSource, indexPattern, precision, bufferedExtent); const esResponse = await this._runEsQuery({ @@ -306,52 +353,69 @@ export class ESGeoGridSource extends AbstractESAggSource { return convertRegularRespToGeoJson(esResponse, this._descriptor.requestType); } - async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback, isRequestStillActive) { - const indexPattern = await this.getIndexPattern(); - const searchSource = await this.makeSearchSource(searchFilters, 0); + async getGeoJsonWithMeta( + layerName: string, + searchFilters: VectorSourceRequestMeta, + registerCancelCallback: (callback: () => void) => void, + isRequestStillActive: () => boolean + ): Promise { + const indexPattern: IndexPattern = await this.getIndexPattern(); + const searchSource: ISearchSource = await this.makeSearchSource(searchFilters, 0); let bucketsPerGrid = 1; this.getMetricFields().forEach((metricField) => { bucketsPerGrid += metricField.getBucketCount(); }); - const features = - bucketsPerGrid === 1 - ? await this._nonCompositeAggRequest({ - searchSource, - indexPattern, - precision: searchFilters.geogridPrecision, - layerName, - registerCancelCallback, - bufferedExtent: searchFilters.buffer, - }) - : await this._compositeAggRequest({ - searchSource, - indexPattern, - precision: searchFilters.geogridPrecision, - layerName, - registerCancelCallback, - bucketsPerGrid, - isRequestStillActive, - bufferedExtent: searchFilters.buffer, - }); + let features: Feature[]; + if (searchFilters.buffer) { + features = + bucketsPerGrid === 1 + ? await this._nonCompositeAggRequest({ + searchSource, + indexPattern, + precision: searchFilters.geogridPrecision || 0, + layerName, + registerCancelCallback, + bufferedExtent: searchFilters.buffer, + }) + : await this._compositeAggRequest({ + searchSource, + indexPattern, + precision: searchFilters.geogridPrecision || 0, + layerName, + registerCancelCallback, + bucketsPerGrid, + isRequestStillActive, + bufferedExtent: searchFilters.buffer, + }); + } else { + throw new Error('Cannot get GeoJson without searchFilter.buffer'); + } return { data: { type: 'FeatureCollection', - features: features, + features, }, meta: { areResultsTrimmed: false, }, - }; + } as GeoJsonWithMeta; } - getLayerName() { + getLayerName(): string { return MVT_SOURCE_LAYER_NAME; } - async getUrlTemplateWithMeta(searchFilters) { + async getUrlTemplateWithMeta( + searchFilters: VectorSourceRequestMeta + ): Promise<{ + layerName: string; + urlTemplate: string; + minSourceZoom: number; + maxSourceZoom: number; + }> { const indexPattern = await this.getIndexPattern(); const searchSource = await this.makeSearchSource(searchFilters, 0); @@ -376,25 +440,25 @@ export class ESGeoGridSource extends AbstractESAggSource { layerName: this.getLayerName(), minSourceZoom: this.getMinZoom(), maxSourceZoom: this.getMaxZoom(), - urlTemplate: urlTemplate, + urlTemplate, }; } - isFilterByMapBounds() { + isFilterByMapBounds(): boolean { if (this._descriptor.resolution === GRID_RESOLUTION.SUPER_FINE) { - //MVT gridded data. Should exclude bounds-filter from ES-DSL + // MVT gridded data. Should exclude bounds-filter from ES-DSL return false; } else { - //Should include bounds-filter from ES-DSL + // Should include bounds-filter from ES-DSL return true; } } - canFormatFeatureProperties() { + canFormatFeatureProperties(): boolean { return true; } - async getSupportedShapeTypes() { + async getSupportedShapeTypes(): Promise { if (this._descriptor.requestType === RENDER_AS.GRID) { return [VECTOR_SHAPE_TYPE.POLYGON]; } @@ -402,7 +466,7 @@ export class ESGeoGridSource extends AbstractESAggSource { return [VECTOR_SHAPE_TYPE.POINT]; } - async getLicensedFeatures() { + async getLicensedFeatures(): Promise { const geoField = await this._getGeoField(); return geoField.type === ES_GEO_FIELD_TYPE.GEO_SHAPE ? [LICENSED_FEATURES.GEO_SHAPE_AGGS_GEO_TILE] diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index 0360208ef8370..504212ea1ea84 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -5,7 +5,6 @@ */ import React from 'react'; -import uuid from 'uuid/v4'; import turfBbox from '@turf/bbox'; import { multiPoint } from '@turf/helpers'; @@ -14,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { convertToLines } from './convert_to_lines'; -import { AbstractESAggSource, DEFAULT_METRIC } from '../es_agg_source'; +import { AbstractESAggSource } from '../es_agg_source'; import { registerSource } from '../source_registry'; import { turfBboxToBounds } from '../../../../common/elasticsearch_util'; import { DataRequestAbortError } from '../../util/data_request'; @@ -28,14 +27,14 @@ export const sourceTitle = i18n.translate('xpack.maps.source.pewPewTitle', { export class ESPewPewSource extends AbstractESAggSource { static type = SOURCE_TYPES.ES_PEW_PEW; - static createDescriptor({ indexPatternId, sourceGeoField, destGeoField, metrics }) { + static createDescriptor(descriptor) { + const normalizedDescriptor = AbstractESAggSource.createDescriptor(descriptor); return { + ...normalizedDescriptor, type: ESPewPewSource.type, - id: uuid(), - indexPatternId: indexPatternId, - sourceGeoField, - destGeoField, - metrics: metrics ? metrics : [DEFAULT_METRIC], + indexPatternId: descriptor.indexPatternId, + sourceGeoField: descriptor.sourceGeoField, + destGeoField: descriptor.destGeoField, }; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/constants.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/constants.ts similarity index 80% rename from x-pack/plugins/maps/public/classes/sources/es_search_source/constants.js rename to x-pack/plugins/maps/public/classes/sources/es_search_source/constants.ts index d7d11440c360b..8ddf536cabecd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/constants.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/constants.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const DEFAULT_FILTER_BY_MAP_BOUNDS = true; +export const DEFAULT_FILTER_BY_MAP_BOUNDS: boolean = true; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index af2061d6c541f..80cc88f432cad 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -16,8 +16,15 @@ import { VectorLayer } from '../../layers/vector_layer/vector_layer'; import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants'; import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; import { EsDocumentsLayerIcon } from './es_documents_layer_icon'; +import { + ESSearchSourceDescriptor, + VectorLayerDescriptor, +} from '../../../../common/descriptor_types'; -export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: string[]) { +export function createDefaultLayerDescriptor( + sourceConfig: Partial, + mapColors: string[] +): VectorLayerDescriptor { const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig); if (sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS) { @@ -36,7 +43,7 @@ export const esDocumentsLayerWizardConfig: LayerWizard = { }), icon: EsDocumentsLayerIcon, renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { - const onSourceConfigChange = (sourceConfig: unknown) => { + const onSourceConfigChange = (sourceConfig: Partial) => { if (!sourceConfig) { previewLayers([]); return; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts deleted file mode 100644 index 67d68dc065b00..0000000000000 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts +++ /dev/null @@ -1,26 +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 { AbstractESSource } from '../es_source'; -import { ESSearchSourceDescriptor, MapFilters } from '../../../../common/descriptor_types'; -import { ITiledSingleLayerVectorSource } from '../vector_source'; - -export class ESSearchSource extends AbstractESSource implements ITiledSingleLayerVectorSource { - static createDescriptor(sourceConfig: unknown): ESSearchSourceDescriptor; - - constructor(sourceDescriptor: Partial, inspectorAdapters: unknown); - getFieldNames(): string[]; - - getUrlTemplateWithMeta( - searchFilters: MapFilters - ): Promise<{ - layerName: string; - urlTemplate: string; - minSourceZoom: number; - maxSourceZoom: number; - }>; - getLayerName(): string; -} diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts index 9c1cda4088dcd..e7099115ffe5e 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts @@ -11,21 +11,22 @@ jest.mock('./load_index_settings'); import { getIndexPatternService, getSearchService, getHttp } from '../../../kibana_services'; import { SearchSource } from 'src/plugins/data/public'; -// @ts-expect-error import { loadIndexSettings } from './load_index_settings'; import { ESSearchSource } from './es_search_source'; import { VectorSourceRequestMeta } from '../../../../common/descriptor_types'; +const mockDescriptor = { indexPatternId: 'foo', geoField: 'bar' }; + describe('ESSearchSource', () => { it('constructor', () => { - const esSearchSource = new ESSearchSource({}, null); + const esSearchSource = new ESSearchSource(mockDescriptor); expect(esSearchSource instanceof ESSearchSource).toBe(true); }); describe('ITiledSingleLayerVectorSource', () => { it('mb-source params', () => { - const esSearchSource = new ESSearchSource({}, null); + const esSearchSource = new ESSearchSource(mockDescriptor); expect(esSearchSource.getMinZoom()).toBe(0); expect(esSearchSource.getMaxZoom()).toBe(24); expect(esSearchSource.getLayerName()).toBe('source_layer'); @@ -72,6 +73,7 @@ describe('ESSearchSource', () => { getIndexPatternService.mockReturnValue(mockIndexPatternService); // @ts-expect-error getSearchService.mockReturnValue(mockSearchService); + // @ts-expect-error loadIndexSettings.mockReturnValue({ maxResultWindow: 1000, }); @@ -104,10 +106,10 @@ describe('ESSearchSource', () => { }; it('Should only include required props', async () => { - const esSearchSource = new ESSearchSource( - { geoField: geoFieldName, indexPatternId: 'ipId' }, - null - ); + const esSearchSource = new ESSearchSource({ + geoField: geoFieldName, + indexPatternId: 'ipId', + }); const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta(searchFilters); expect(urlTemplateWithMeta.urlTemplate).toBe( `rootdir/api/maps/mvt/getTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':fields,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))&geoFieldType=geo_shape` @@ -118,22 +120,28 @@ describe('ESSearchSource', () => { describe('isFilterByMapBounds', () => { it('default', () => { - const esSearchSource = new ESSearchSource({}, null); + const esSearchSource = new ESSearchSource(mockDescriptor); expect(esSearchSource.isFilterByMapBounds()).toBe(true); }); it('mvt', () => { - const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null); + const esSearchSource = new ESSearchSource({ + ...mockDescriptor, + scalingType: SCALING_TYPES.MVT, + }); expect(esSearchSource.isFilterByMapBounds()).toBe(false); }); }); describe('getJoinsDisabledReason', () => { it('default', () => { - const esSearchSource = new ESSearchSource({}, null); + const esSearchSource = new ESSearchSource(mockDescriptor); expect(esSearchSource.getJoinsDisabledReason()).toBe(null); }); it('mvt', () => { - const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null); + const esSearchSource = new ESSearchSource({ + ...mockDescriptor, + scalingType: SCALING_TYPES.MVT, + }); expect(esSearchSource.getJoinsDisabledReason()).toBe( 'Joins are not supported when scaling by mvt vector tiles' ); @@ -142,12 +150,15 @@ describe('ESSearchSource', () => { describe('getFields', () => { it('default', () => { - const esSearchSource = new ESSearchSource({}, null); + const esSearchSource = new ESSearchSource(mockDescriptor); const docField = esSearchSource.createField({ fieldName: 'prop1' }); expect(docField.canReadFromGeoJson()).toBe(true); }); it('mvt', () => { - const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null); + const esSearchSource = new ESSearchSource({ + ...mockDescriptor, + scalingType: SCALING_TYPES.MVT, + }); const docField = esSearchSource.createField({ fieldName: 'prop1' }); expect(docField.canReadFromGeoJson()).toBe(false); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx similarity index 68% rename from x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js rename to x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index da75dfd83d809..d31f6ee626245 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -5,50 +5,82 @@ */ import _ from 'lodash'; -import React from 'react'; +import React, { ReactElement } from 'react'; import rison from 'rison-node'; +import { i18n } from '@kbn/i18n'; +import { IFieldType, IndexPattern } from 'src/plugins/data/public'; +import { FeatureCollection, GeoJsonProperties } from 'geojson'; import { AbstractESSource } from '../es_source'; -import { getSearchService, getHttp } from '../../../kibana_services'; -import { hitsToGeoJson, getField, addFieldToDSL } from '../../../../common/elasticsearch_util'; +import { getHttp, getSearchService } from '../../../kibana_services'; +import { addFieldToDSL, getField, hitsToGeoJson } from '../../../../common/elasticsearch_util'; +// @ts-expect-error import { UpdateSourceEditor } from './update_source_editor'; + import { - SOURCE_TYPES, - ES_GEO_FIELD_TYPE, DEFAULT_MAX_BUCKETS_LIMIT, - SORT_ORDER, - SCALING_TYPES, - VECTOR_SHAPE_TYPE, - MVT_SOURCE_LAYER_NAME, + ES_GEO_FIELD_TYPE, + FIELD_ORIGIN, GIS_API_PATH, MVT_GETTILE_API_PATH, + MVT_SOURCE_LAYER_NAME, + SCALING_TYPES, + SOURCE_TYPES, + VECTOR_SHAPE_TYPE, } from '../../../../common/constants'; -import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { getSourceFields } from '../../../index_pattern_util'; import { loadIndexSettings } from './load_index_settings'; -import uuid from 'uuid/v4'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; import { ESDocField } from '../../fields/es_doc_field'; import { registerSource } from '../source_registry'; +import { + ESSearchSourceDescriptor, + VectorSourceRequestMeta, + VectorSourceSyncMeta, +} from '../../../../common/descriptor_types'; +import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; +import { ImmutableSourceProperty, PreIndexedShape, SourceEditorArgs } from '../source'; +import { IField } from '../../fields/field'; +import { + GeoJsonWithMeta, + ITiledSingleLayerVectorSource, + SourceTooltipConfig, +} from '../vector_source'; +import { ITooltipProperty } from '../../tooltips/tooltip_property'; +import { DataRequest } from '../../util/data_request'; +import { SortDirection, SortDirectionNumeric } from '../../../../../../../src/plugins/data/common'; +import { isValidStringConfig } from '../../util/valid_string_config'; export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { defaultMessage: 'Documents', }); -function getDocValueAndSourceFields(indexPattern, fieldNames) { - const docValueFields = []; - const sourceOnlyFields = []; - const scriptFields = {}; +export interface ScriptField { + source: string; + lang: string; +} + +function getDocValueAndSourceFields( + indexPattern: IndexPattern, + fieldNames: string[] +): { + docValueFields: Array; + sourceOnlyFields: string[]; + scriptFields: Record; +} { + const docValueFields: Array = []; + const sourceOnlyFields: string[] = []; + const scriptFields: Record = {}; fieldNames.forEach((fieldName) => { const field = getField(indexPattern, fieldName); if (field.scripted) { scriptFields[field.name] = { script: { - source: field.script, - lang: field.lang, + source: field.script || '', + lang: field.lang || '', }, }; } else if (field.readFromDocValues) { @@ -68,43 +100,64 @@ function getDocValueAndSourceFields(indexPattern, fieldNames) { return { docValueFields, sourceOnlyFields, scriptFields }; } -export class ESSearchSource extends AbstractESSource { - static type = SOURCE_TYPES.ES_SEARCH; +export class ESSearchSource extends AbstractESSource implements ITiledSingleLayerVectorSource { + readonly _descriptor: ESSearchSourceDescriptor; + protected readonly _tooltipFields: ESDocField[]; - static createDescriptor(descriptor) { + static createDescriptor(descriptor: Partial): ESSearchSourceDescriptor { + const normalizedDescriptor = AbstractESSource.createDescriptor(descriptor); + if (!isValidStringConfig(normalizedDescriptor.geoField)) { + throw new Error('Cannot create an ESSearchSourceDescriptor without a geoField'); + } return { - ...descriptor, - id: descriptor.id ? descriptor.id : uuid(), - type: ESSearchSource.type, - indexPatternId: descriptor.indexPatternId, - geoField: descriptor.geoField, - filterByMapBounds: _.get(descriptor, 'filterByMapBounds', DEFAULT_FILTER_BY_MAP_BOUNDS), - tooltipProperties: _.get(descriptor, 'tooltipProperties', []), - sortField: _.get(descriptor, 'sortField', ''), - sortOrder: _.get(descriptor, 'sortOrder', SORT_ORDER.DESC), - scalingType: _.get(descriptor, 'scalingType', SCALING_TYPES.LIMIT), - topHitsSplitField: descriptor.topHitsSplitField, - topHitsSize: _.get(descriptor, 'topHitsSize', 1), + ...normalizedDescriptor, + type: SOURCE_TYPES.ES_SEARCH, + geoField: normalizedDescriptor.geoField!, + filterByMapBounds: + typeof descriptor.filterByMapBounds === 'boolean' + ? descriptor.filterByMapBounds + : DEFAULT_FILTER_BY_MAP_BOUNDS, + tooltipProperties: Array.isArray(descriptor.tooltipProperties) + ? descriptor.tooltipProperties + : [], + sortField: isValidStringConfig(descriptor.sortField) ? (descriptor.sortField as string) : '', + sortOrder: isValidStringConfig(descriptor.sortOrder) + ? descriptor.sortOrder! + : SortDirection.desc, + scalingType: isValidStringConfig(descriptor.scalingType) + ? descriptor.scalingType! + : SCALING_TYPES.LIMIT, + topHitsSplitField: isValidStringConfig(descriptor.topHitsSplitField) + ? descriptor.topHitsSplitField! + : '', + topHitsSize: + typeof descriptor.topHitsSize === 'number' && descriptor.topHitsSize > 0 + ? descriptor.topHitsSize + : 1, }; } - constructor(descriptor, inspectorAdapters) { - super(ESSearchSource.createDescriptor(descriptor), inspectorAdapters); - - this._tooltipFields = this._descriptor.tooltipProperties.map((property) => - this.createField({ fieldName: property }) - ); + constructor(descriptor: Partial, inspectorAdapters?: Adapters) { + const sourceDescriptor = ESSearchSource.createDescriptor(descriptor); + super(sourceDescriptor, inspectorAdapters); + this._descriptor = sourceDescriptor; + this._tooltipFields = this._descriptor.tooltipProperties + ? this._descriptor.tooltipProperties.map((property) => { + return this.createField({ fieldName: property }); + }) + : []; } - createField({ fieldName }) { + createField({ fieldName }: { fieldName: string }): ESDocField { return new ESDocField({ fieldName, source: this, + origin: FIELD_ORIGIN.SOURCE, canReadFromGeoJson: this._descriptor.scalingType !== SCALING_TYPES.MVT, }); } - renderSourceSettingsEditor({ onChange }) { + renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement | null { const getGeoField = () => { return this._getGeoField(); }; @@ -113,7 +166,7 @@ export class ESSearchSource extends AbstractESSource { source={this} indexPatternId={this.getIndexPatternId()} getGeoField={getGeoField} - onChange={onChange} + onChange={sourceEditorArgs.onChange} tooltipFields={this._tooltipFields} sortField={this._descriptor.sortField} sortOrder={this._descriptor.sortOrder} @@ -125,34 +178,36 @@ export class ESSearchSource extends AbstractESSource { ); } - async getFields() { + async getFields(): Promise { try { const indexPattern = await this.getIndexPattern(); - return indexPattern.fields - .filter((field) => { - // Ensure fielddata is enabled for field. - // Search does not request _source - return field.aggregatable; - }) - .map((field) => { + const fields: IFieldType[] = indexPattern.fields.filter((field) => { + // Ensure fielddata is enabled for field. + // Search does not request _source + return field.aggregatable; + }); + + return fields.map( + (field): IField => { return this.createField({ fieldName: field.name }); - }); + } + ); } catch (error) { // failed index-pattern retrieval will show up as error-message in the layer-toc-entry return []; } } - getFieldNames() { + getFieldNames(): string[] { return [this._descriptor.geoField]; } - async getImmutableProperties() { - let indexPatternTitle = this.getIndexPatternId(); + async getImmutableProperties(): Promise { + let indexPatternName = this.getIndexPatternId(); let geoFieldType = ''; try { const indexPattern = await this.getIndexPattern(); - indexPatternTitle = indexPattern.title; + indexPatternName = indexPattern.title; const geoField = await this._getGeoField(); geoFieldType = geoField.type; } catch (error) { @@ -168,7 +223,7 @@ export class ESSearchSource extends AbstractESSource { label: i18n.translate('xpack.maps.source.esSearch.indexPatternLabel', { defaultMessage: `Index pattern`, }), - value: indexPatternTitle, + value: indexPatternName, }, { label: i18n.translate('xpack.maps.source.esSearch.geoFieldLabel', { @@ -186,8 +241,12 @@ export class ESSearchSource extends AbstractESSource { } // Returns sort content for an Elasticsearch search body - _buildEsSort() { + _buildEsSort(): Array> { const { sortField, sortOrder } = this._descriptor; + + if (!sortField) { + throw new Error('Cannot build sort'); + } return [ { [sortField]: { @@ -197,16 +256,30 @@ export class ESSearchSource extends AbstractESSource { ]; } - async _getTopHits(layerName, searchFilters, registerCancelCallback) { + async _getTopHits( + layerName: string, + searchFilters: VectorSourceRequestMeta, + registerCancelCallback: (callback: () => void) => void + ) { const { topHitsSplitField: topHitsSplitFieldName, topHitsSize } = this._descriptor; - const indexPattern = await this.getIndexPattern(); + if (!topHitsSplitFieldName) { + throw new Error('Cannot _getTopHits without topHitsSplitField'); + } + + const indexPattern: IndexPattern = await this.getIndexPattern(); const { docValueFields, sourceOnlyFields, scriptFields } = getDocValueAndSourceFields( indexPattern, searchFilters.fieldNames ); - const topHits = { + const topHits: { + size: number; + script_fields: Record; + docvalue_fields: Array; + _source?: boolean | { includes: string[] }; + sort?: Array>; + } = { size: topHitsSize, script_fields: scriptFields, docvalue_fields: docValueFields, @@ -215,6 +288,7 @@ export class ESSearchSource extends AbstractESSource { if (this._hasSort()) { topHits.sort = this._buildEsSort(); } + if (sourceOnlyFields.length === 0) { topHits._source = false; } else { @@ -223,7 +297,7 @@ export class ESSearchSource extends AbstractESSource { }; } - const topHitsSplitField = getField(indexPattern, topHitsSplitFieldName); + const topHitsSplitField: IFieldType = getField(indexPattern, topHitsSplitFieldName); const cardinalityAgg = { precision_threshold: 1 }; const termsAgg = { size: DEFAULT_MAX_BUCKETS_LIMIT, @@ -253,13 +327,13 @@ export class ESSearchSource extends AbstractESSource { requestDescription: 'Elasticsearch document top hits request', }); - const allHits = []; + const allHits: any[] = []; const entityBuckets = _.get(resp, 'aggregations.entitySplit.buckets', []); const totalEntities = _.get(resp, 'aggregations.totalEntities.value', 0); // can not compare entityBuckets.length to totalEntities because totalEntities is an approximate const areEntitiesTrimmed = entityBuckets.length >= DEFAULT_MAX_BUCKETS_LIMIT; let areTopHitsTrimmed = false; - entityBuckets.forEach((entityBucket) => { + entityBuckets.forEach((entityBucket: any) => { const total = _.get(entityBucket, 'entityHits.hits.total', 0); const hits = _.get(entityBucket, 'entityHits.hits.hits', []); // Reverse hits list so top documents by sort are drawn on top @@ -282,7 +356,12 @@ export class ESSearchSource extends AbstractESSource { // searchFilters.fieldNames contains geo field and any fields needed for styling features // Performs Elasticsearch search request being careful to pull back only required fields to minimize response size - async _getSearchHits(layerName, searchFilters, maxResultWindow, registerCancelCallback) { + async _getSearchHits( + layerName: string, + searchFilters: VectorSourceRequestMeta, + maxResultWindow: number, + registerCancelCallback: (callback: () => void) => void + ) { const indexPattern = await this.getIndexPattern(); const { docValueFields, sourceOnlyFields } = getDocValueAndSourceFields( @@ -322,23 +401,28 @@ export class ESSearchSource extends AbstractESSource { }; } - _isTopHits() { + _isTopHits(): boolean { const { scalingType, topHitsSplitField } = this._descriptor; return !!(scalingType === SCALING_TYPES.TOP_HITS && topHitsSplitField); } - _hasSort() { + _hasSort(): boolean { const { sortField, sortOrder } = this._descriptor; return !!sortField && !!sortOrder; } - async getMaxResultWindow() { + async getMaxResultWindow(): Promise { const indexPattern = await this.getIndexPattern(); const indexSettings = await loadIndexSettings(indexPattern.title); return indexSettings.maxResultWindow; } - async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { + async getGeoJsonWithMeta( + layerName: string, + searchFilters: VectorSourceRequestMeta, + registerCancelCallback: (callback: () => void) => void, + isRequestStillActive: () => boolean + ): Promise { const indexPattern = await this.getIndexPattern(); const indexSettings = await loadIndexSettings(indexPattern.title); @@ -355,7 +439,7 @@ export class ESSearchSource extends AbstractESSource { const unusedMetaFields = indexPattern.metaFields.filter((metaField) => { return !['_id', '_index'].includes(metaField); }); - const flattenHit = (hit) => { + const flattenHit = (hit: Record) => { const properties = indexPattern.flattenHit(hit); // remove metaFields unusedMetaFields.forEach((metaField) => { @@ -375,7 +459,7 @@ export class ESSearchSource extends AbstractESSource { hits, flattenHit, geoField.name, - geoField.type, + geoField.type as ES_GEO_FIELD_TYPE, epochMillisFields ); } catch (error) { @@ -394,11 +478,11 @@ export class ESSearchSource extends AbstractESSource { }; } - canFormatFeatureProperties() { + canFormatFeatureProperties(): boolean { return this._tooltipFields.length > 0; } - async _loadTooltipProperties(docId, index, indexPattern) { + async _loadTooltipProperties(docId: string | number, index: string, indexPattern: IndexPattern) { if (this._tooltipFields.length === 0) { return {}; } @@ -430,7 +514,7 @@ export class ESSearchSource extends AbstractESSource { } const properties = indexPattern.flattenHit(hit); - indexPattern.metaFields.forEach((metaField) => { + indexPattern.metaFields.forEach((metaField: string) => { if (!this._getTooltipPropertyNames().includes(metaField)) { delete properties[metaField]; } @@ -438,7 +522,14 @@ export class ESSearchSource extends AbstractESSource { return properties; } - async getTooltipProperties(properties) { + _getTooltipPropertyNames(): string[] { + return this._tooltipFields.map((field: IField) => field.getName()); + } + + async getTooltipProperties(properties: GeoJsonProperties): Promise { + if (properties === null) { + throw new Error('properties cannot be null'); + } const indexPattern = await this.getIndexPattern(); const propertyValues = await this._loadTooltipProperties( properties._id, @@ -452,25 +543,27 @@ export class ESSearchSource extends AbstractESSource { return Promise.all(tooltipProperties); } - isFilterByMapBounds() { - if (this._descriptor.scalingType === SCALING_TYPES.CLUSTER) { + isFilterByMapBounds(): boolean { + if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) { return true; } else if (this._descriptor.scalingType === SCALING_TYPES.MVT) { return false; } else { - return this._descriptor.filterByMapBounds; + return !!this._descriptor.filterByMapBounds; } } - async getLeftJoinFields() { + async getLeftJoinFields(): Promise { const indexPattern = await this.getIndexPattern(); // Left fields are retrieved from _source. - return getSourceFields(indexPattern.fields).map((field) => - this.createField({ fieldName: field.name }) + return getSourceFields(indexPattern.fields).map( + (field): IField => { + return this.createField({ fieldName: field.name }); + } ); } - async getSupportedShapeTypes() { + async getSupportedShapeTypes(): Promise { let geoFieldType; try { const geoField = await this._getGeoField(); @@ -486,8 +579,10 @@ export class ESSearchSource extends AbstractESSource { return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; } - getSourceTooltipContent(sourceDataRequest) { - const featureCollection = sourceDataRequest ? sourceDataRequest.getData() : null; + getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig { + const featureCollection: FeatureCollection | null = sourceDataRequest + ? (sourceDataRequest.getData() as FeatureCollection) + : null; const meta = sourceDataRequest ? sourceDataRequest.getMeta() : null; if (!featureCollection || !meta) { // no tooltip content needed when there is no feature collection or meta @@ -519,7 +614,7 @@ export class ESSearchSource extends AbstractESSource { tooltipContent: `${entitiesFoundMsg} ${docsPerEntityMsg}`, // Used to show trimmed icon in legend // user only needs to be notified of trimmed results when entities are trimmed - areResultsTrimmed: meta.areEntitiesTrimmed, + areResultsTrimmed: !!meta.areEntitiesTrimmed, }; } @@ -542,7 +637,7 @@ export class ESSearchSource extends AbstractESSource { }; } - getSyncMeta() { + getSyncMeta(): VectorSourceSyncMeta | null { return { sortField: this._descriptor.sortField, sortOrder: this._descriptor.sortOrder, @@ -552,7 +647,10 @@ export class ESSearchSource extends AbstractESSource { }; } - async getPreIndexedShape(properties) { + async getPreIndexedShape(properties: GeoJsonProperties): Promise { + if (properties === null) { + return null; + } const geoField = await this._getGeoField(); return { index: properties._index, // Can not use index pattern title because it may reference many indices @@ -561,7 +659,7 @@ export class ESSearchSource extends AbstractESSource { }; } - getJoinsDisabledReason() { + getJoinsDisabledReason(): string | null { let reason; if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) { reason = i18n.translate('xpack.maps.source.esSearch.joinsDisabledReason', { @@ -577,11 +675,18 @@ export class ESSearchSource extends AbstractESSource { return reason; } - getLayerName() { + getLayerName(): string { return MVT_SOURCE_LAYER_NAME; } - async getUrlTemplateWithMeta(searchFilters) { + async getUrlTemplateWithMeta( + searchFilters: VectorSourceRequestMeta + ): Promise<{ + layerName: string; + urlTemplate: string; + minSourceZoom: number; + maxSourceZoom: number; + }> { const indexPattern = await this.getIndexPattern(); const indexSettings = await loadIndexSettings(indexPattern.title); @@ -621,7 +726,7 @@ export class ESSearchSource extends AbstractESSource { layerName: this.getLayerName(), minSourceZoom: this.getMinZoom(), maxSourceZoom: this.getMaxZoom(), - urlTemplate: urlTemplate, + urlTemplate, }; } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.ts similarity index 79% rename from x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.js rename to x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.ts index d5d24da225232..5f69fa2eeadb3 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.ts @@ -4,20 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { DEFAULT_MAX_RESULT_WINDOW, DEFAULT_MAX_INNER_RESULT_WINDOW, INDEX_SETTINGS_API_PATH, } from '../../../../common/constants'; import { getHttp, getToasts } from '../../../kibana_services'; -import { i18n } from '@kbn/i18n'; let toastDisplayed = false; -const indexSettings = new Map(); +const indexSettings = new Map>(); + +export interface INDEX_SETTINGS { + maxResultWindow: number; + maxInnerResultWindow: number; +} -export async function loadIndexSettings(indexPatternTitle) { +export async function loadIndexSettings(indexPatternTitle: string): Promise { if (indexSettings.has(indexPatternTitle)) { - return indexSettings.get(indexPatternTitle); + return indexSettings.get(indexPatternTitle)!; } const fetchPromise = fetchIndexSettings(indexPatternTitle); @@ -25,7 +30,7 @@ export async function loadIndexSettings(indexPatternTitle) { return fetchPromise; } -async function fetchIndexSettings(indexPatternTitle) { +async function fetchIndexSettings(indexPatternTitle: string): Promise { const http = getHttp(); const toasts = getToasts(); try { @@ -50,6 +55,7 @@ async function fetchIndexSettings(indexPatternTitle) { toastDisplayed = true; toasts.addWarning(warningMsg); } + // eslint-disable-next-line no-console console.warn(warningMsg); return { maxResultWindow: DEFAULT_MAX_RESULT_WINDOW, diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js index 735510102e25e..eef802f1e19ce 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js @@ -18,10 +18,10 @@ import { getSourceFields, supportsGeoTileAgg, } from '../../../index_pattern_util'; -import { SORT_ORDER } from '../../../../common/constants'; +import { SortDirection, indexPatterns } from '../../../../../../../src/plugins/data/public'; import { ESDocField } from '../../fields/es_doc_field'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indexPatterns } from '../../../../../../../src/plugins/data/public'; + import { ScalingForm } from './scaling_form'; export class UpdateSourceEditor extends Component { @@ -183,13 +183,13 @@ export class UpdateSourceEditor extends Component { text: i18n.translate('xpack.maps.source.esSearch.ascendingLabel', { defaultMessage: 'ascending', }), - value: SORT_ORDER.ASC, + value: SortDirection.asc, }, { text: i18n.translate('xpack.maps.source.esSearch.descendingLabel', { defaultMessage: 'descending', }), - value: SORT_ORDER.DESC, + value: SortDirection.desc, }, ]} value={this.props.sortOrder} diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts deleted file mode 100644 index c11b6f0853cc7..0000000000000 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts +++ /dev/null @@ -1,86 +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 { AbstractVectorSource } from '../vector_source'; -import { IVectorSource } from '../vector_source'; -import { TimeRange } from '../../../../../../../src/plugins/data/common'; -import { IndexPattern, ISearchSource } from '../../../../../../../src/plugins/data/public'; -import { - DynamicStylePropertyOptions, - MapQuery, - VectorSourceRequestMeta, -} from '../../../../common/descriptor_types'; -import { IVectorStyle } from '../../styles/vector/vector_style'; -import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; - -export interface IESSource extends IVectorSource { - getId(): string; - getIndexPattern(): Promise; - getIndexPatternId(): string; - getGeoFieldName(): string; - getMaxResultWindow(): Promise; - makeSearchSource( - searchFilters: VectorSourceRequestMeta, - limit: number, - initialSearchContext?: object - ): Promise; - loadStylePropsMeta({ - layerName, - style, - dynamicStyleProps, - registerCancelCallback, - sourceQuery, - timeFilters, - }: { - layerName: string; - style: IVectorStyle; - dynamicStyleProps: Array>; - registerCancelCallback: (callback: () => void) => void; - sourceQuery?: MapQuery; - timeFilters: TimeRange; - }): Promise; -} - -export class AbstractESSource extends AbstractVectorSource implements IESSource { - getId(): string; - getIndexPattern(): Promise; - getIndexPatternId(): string; - getGeoFieldName(): string; - getMaxResultWindow(): Promise; - makeSearchSource( - searchFilters: VectorSourceRequestMeta, - limit: number, - initialSearchContext?: object - ): Promise; - loadStylePropsMeta({ - layerName, - style, - dynamicStyleProps, - registerCancelCallback, - sourceQuery, - timeFilters, - }: { - layerName: string; - style: IVectorStyle; - dynamicStyleProps: Array>; - registerCancelCallback: (callback: () => void) => void; - sourceQuery?: MapQuery; - timeFilters: TimeRange; - }): Promise; - _runEsQuery: ({ - requestId, - requestName, - requestDescription, - searchSource, - registerCancelCallback, - }: { - requestId: string; - requestName: string; - requestDescription: string; - searchSource: ISearchSource; - registerCancelCallback: () => void; - }) => Promise; -} diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts similarity index 51% rename from x-pack/plugins/maps/public/classes/sources/es_source/es_source.js rename to x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 0c8cb5f514247..68b6b131978ea 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractVectorSource } from '../vector_source'; +import { i18n } from '@kbn/i18n'; +import uuid from 'uuid/v4'; +import { Filter, IFieldType, IndexPattern, ISearchSource } from 'src/plugins/data/public'; +import { AbstractVectorSource, BoundsFilters } from '../vector_source'; import { getAutocompleteService, getIndexPatternService, @@ -12,62 +15,122 @@ import { getSearchService, } from '../../../kibana_services'; import { createExtentFilter } from '../../../../common/elasticsearch_util'; -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import uuid from 'uuid/v4'; - import { copyPersistentState } from '../../../reducers/util'; import { DataRequestAbortError } from '../../util/data_request'; import { expandToTileBoundaries } from '../../../../common/geo_tile_utils'; import { search } from '../../../../../../../src/plugins/data/public'; +import { IVectorSource } from '../vector_source'; +import { TimeRange } from '../../../../../../../src/plugins/data/common'; +import { + AbstractESSourceDescriptor, + AbstractSourceDescriptor, + DynamicStylePropertyOptions, + MapExtent, + MapQuery, + VectorJoinSourceRequestMeta, + VectorSourceRequestMeta, +} from '../../../../common/descriptor_types'; +import { IVectorStyle } from '../../styles/vector/vector_style'; +import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; +import { IField } from '../../fields/field'; +import { ES_GEO_FIELD_TYPE, FieldFormatter } from '../../../../common/constants'; +import { + Adapters, + RequestResponder, +} from '../../../../../../../src/plugins/inspector/common/adapters'; +import { isValidStringConfig } from '../../util/valid_string_config'; + +export interface IESSource extends IVectorSource { + isESSource(): true; + getId(): string; + getIndexPattern(): Promise; + getIndexPatternId(): string; + getGeoFieldName(): string; + loadStylePropsMeta({ + layerName, + style, + dynamicStyleProps, + registerCancelCallback, + sourceQuery, + timeFilters, + }: { + layerName: string; + style: IVectorStyle; + dynamicStyleProps: Array>; + registerCancelCallback: (callback: () => void) => void; + sourceQuery?: MapQuery; + timeFilters: TimeRange; + }): Promise; +} -export class AbstractESSource extends AbstractVectorSource { - constructor(descriptor, inspectorAdapters) { - super( - { - ...descriptor, - applyGlobalQuery: _.get(descriptor, 'applyGlobalQuery', true), - }, - inspectorAdapters - ); +export class AbstractESSource extends AbstractVectorSource implements IESSource { + indexPattern?: IndexPattern; + + readonly _descriptor: AbstractESSourceDescriptor; + + static createDescriptor( + descriptor: Partial + ): AbstractESSourceDescriptor { + if (!isValidStringConfig(descriptor.indexPatternId)) { + throw new Error( + 'Cannot create AbstractESSourceDescriptor when indexPatternId is not provided' + ); + } + return { + ...descriptor, + id: isValidStringConfig(descriptor.id) ? descriptor.id! : uuid(), + type: isValidStringConfig(descriptor.type) ? descriptor.type! : '', + indexPatternId: descriptor.indexPatternId!, + applyGlobalQuery: + // backfill old _.get usage + typeof descriptor.applyGlobalQuery !== 'undefined' ? !!descriptor.applyGlobalQuery : true, + }; + } + + constructor(descriptor: AbstractESSourceDescriptor, inspectorAdapters?: Adapters) { + super(AbstractESSource.createDescriptor(descriptor), inspectorAdapters); + this._descriptor = descriptor; } - getId() { + getId(): string { return this._descriptor.id; } - isFieldAware() { + isFieldAware(): boolean { return true; } - isRefreshTimerAware() { + isRefreshTimerAware(): boolean { return true; } - isQueryAware() { + isQueryAware(): boolean { return true; } - getIndexPatternIds() { + getIndexPatternIds(): string[] { return [this.getIndexPatternId()]; } - getQueryableIndexPatternIds() { + getQueryableIndexPatternIds(): string[] { if (this.getApplyGlobalQuery()) { return [this.getIndexPatternId()]; } return []; } - isESSource() { + isESSource(): true { return true; } destroy() { - this._inspectorAdapters.requests.resetRequest(this.getId()); + const inspectorAdapters = this.getInspectorAdapters(); + if (inspectorAdapters) { + inspectorAdapters.requests.resetRequest(this.getId()); + } } - cloneDescriptor() { + cloneDescriptor(): AbstractSourceDescriptor { const clonedDescriptor = copyPersistentState(this._descriptor); // id used as uuid to track requests in inspector clonedDescriptor.id = uuid(); @@ -80,26 +143,45 @@ export class AbstractESSource extends AbstractVectorSource { requestDescription, searchSource, registerCancelCallback, - }) { + }: { + requestId: string; + requestName: string; + requestDescription: string; + searchSource: ISearchSource; + registerCancelCallback: (callback: () => void) => void; + }): Promise { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); - const inspectorRequest = this._inspectorAdapters.requests.start(requestName, { - id: requestId, - description: requestDescription, - }); + const inspectorAdapters = this.getInspectorAdapters(); + let inspectorRequest: RequestResponder | undefined; + if (inspectorAdapters) { + inspectorRequest = inspectorAdapters.requests.start(requestName, { + id: requestId, + description: requestDescription, + }); + } + let resp; try { - inspectorRequest.stats(search.getRequestInspectorStats(searchSource)); - searchSource.getSearchRequestBody().then((body) => { - inspectorRequest.json(body); - }); + if (inspectorRequest) { + const requestStats = search.getRequestInspectorStats(searchSource); + inspectorRequest.stats(requestStats); + searchSource.getSearchRequestBody().then((body) => { + if (inspectorRequest) { + inspectorRequest.json(body); + } + }); + } resp = await searchSource.fetch({ abortSignal: abortController.signal }); - inspectorRequest - .stats(search.getResponseInspectorStats(resp, searchSource)) - .ok({ json: resp }); + if (inspectorRequest) { + const responseStats = search.getResponseInspectorStats(resp, searchSource); + inspectorRequest.stats(responseStats).ok({ json: resp }); + } } catch (error) { - inspectorRequest.error({ error }); + if (inspectorRequest) { + inspectorRequest.error(error); + } if (error.name === 'AbortError') { throw new DataRequestAbortError(); } @@ -115,22 +197,40 @@ export class AbstractESSource extends AbstractVectorSource { return resp; } - async makeSearchSource(searchFilters, limit, initialSearchContext) { + async makeSearchSource( + searchFilters: VectorSourceRequestMeta | VectorJoinSourceRequestMeta | BoundsFilters, + limit: number, + initialSearchContext?: object + ): Promise { const indexPattern = await this.getIndexPattern(); const isTimeAware = await this.isTimeAware(); - const applyGlobalQuery = _.get(searchFilters, 'applyGlobalQuery', true); - const globalFilters = applyGlobalQuery ? searchFilters.filters : []; - const allFilters = [...globalFilters]; - if (this.isFilterByMapBounds() && searchFilters.buffer) { - //buffer can be empty + const applyGlobalQuery = + typeof searchFilters.applyGlobalQuery === 'boolean' ? searchFilters.applyGlobalQuery : true; + const globalFilters: Filter[] = applyGlobalQuery ? searchFilters.filters : []; + const allFilters: Filter[] = [...globalFilters]; + if (this.isFilterByMapBounds() && 'buffer' in searchFilters && searchFilters.buffer) { + // buffer can be empty const geoField = await this._getGeoField(); - const buffer = this.isGeoGridPrecisionAware() - ? expandToTileBoundaries(searchFilters.buffer, searchFilters.geogridPrecision) - : searchFilters.buffer; - allFilters.push(createExtentFilter(buffer, geoField.name, geoField.type)); + const buffer: MapExtent = + this.isGeoGridPrecisionAware() && + 'geogridPrecision' in searchFilters && + typeof searchFilters.geogridPrecision === 'number' + ? expandToTileBoundaries(searchFilters.buffer, searchFilters.geogridPrecision) + : searchFilters.buffer; + const extentFilter = createExtentFilter( + buffer, + geoField.name, + geoField.type as ES_GEO_FIELD_TYPE + ); + + // @ts-expect-error + allFilters.push(extentFilter); } if (isTimeAware) { - allFilters.push(getTimeFilter().createFilter(indexPattern, searchFilters.timeFilters)); + const filter = getTimeFilter().createFilter(indexPattern, searchFilters.timeFilters); + if (filter) { + allFilters.push(filter); + } } const searchService = getSearchService(); const searchSource = await searchService.searchSource.create(initialSearchContext); @@ -153,7 +253,10 @@ export class AbstractESSource extends AbstractVectorSource { return searchSource; } - async getBoundsForFilters(boundsFilters, registerCancelCallback) { + async getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (callback: () => void) => void + ): Promise { const searchSource = await this.makeSearchSource(boundsFilters, 0); searchSource.setField('aggs', { fitToBounds: { @@ -184,14 +287,14 @@ export class AbstractESSource extends AbstractVectorSource { const minLon = esBounds.top_left.lon; const maxLon = esBounds.bottom_right.lon; return { - minLon: minLon > maxLon ? minLon - 360 : minLon, //fixes an ES bbox to straddle dateline + minLon: minLon > maxLon ? minLon - 360 : minLon, // fixes an ES bbox to straddle dateline maxLon, minLat: esBounds.bottom_right.lat, maxLat: esBounds.top_left.lat, }; } - async isTimeAware() { + async isTimeAware(): Promise { try { const indexPattern = await this.getIndexPattern(); const timeField = indexPattern.timeFieldName; @@ -201,15 +304,19 @@ export class AbstractESSource extends AbstractVectorSource { } } - getIndexPatternId() { + getIndexPatternId(): string { return this._descriptor.indexPatternId; } - getGeoFieldName() { + getGeoFieldName(): string { + if (!this._descriptor.geoField) { + throw new Error('Should not call'); + } return this._descriptor.geoField; } - async getIndexPattern() { + async getIndexPattern(): Promise { + // Do we need this cache? Doesn't the IndexPatternService take care of this? if (this.indexPattern) { return this.indexPattern; } @@ -227,16 +334,16 @@ export class AbstractESSource extends AbstractVectorSource { } } - async supportsFitToBounds() { + async supportsFitToBounds(): Promise { try { const geoField = await this._getGeoField(); - return geoField.aggregatable; + return !!geoField.aggregatable; } catch (error) { return false; } } - async _getGeoField() { + async _getGeoField(): Promise { const indexPattern = await this.getIndexPattern(); const geoField = indexPattern.fields.getByName(this.getGeoFieldName()); if (!geoField) { @@ -250,7 +357,7 @@ export class AbstractESSource extends AbstractVectorSource { return geoField; } - async getDisplayName() { + async getDisplayName(): Promise { try { const indexPattern = await this.getIndexPattern(); return indexPattern.title; @@ -260,15 +367,11 @@ export class AbstractESSource extends AbstractVectorSource { } } - isBoundsAware() { + isBoundsAware(): boolean { return true; } - getId() { - return this._descriptor.id; - } - - async createFieldFormatter(field) { + async createFieldFormatter(field: IField): Promise { let indexPattern; try { indexPattern = await this.getIndexPattern(); @@ -291,15 +394,25 @@ export class AbstractESSource extends AbstractVectorSource { registerCancelCallback, sourceQuery, timeFilters, - }) { + }: { + layerName: string; + style: IVectorStyle; + dynamicStyleProps: Array>; + registerCancelCallback: (callback: () => void) => void; + sourceQuery?: MapQuery; + timeFilters: TimeRange; + }): Promise { const promises = dynamicStyleProps.map((dynamicStyleProp) => { return dynamicStyleProp.getFieldMetaRequest(); }); const fieldAggRequests = await Promise.all(promises); - const aggs = fieldAggRequests.reduce((aggs, fieldAggRequest) => { - return fieldAggRequest ? { ...aggs, ...fieldAggRequest } : aggs; - }, {}); + const allAggs: Record = fieldAggRequests.reduce( + (aggs: Record, fieldAggRequest: unknown | null) => { + return fieldAggRequest ? { ...aggs, ...(fieldAggRequest as Record) } : aggs; + }, + {} + ); const indexPattern = await this.getIndexPattern(); const searchService = getSearchService(); @@ -307,12 +420,15 @@ export class AbstractESSource extends AbstractVectorSource { searchSource.setField('index', indexPattern); searchSource.setField('size', 0); - searchSource.setField('aggs', aggs); + searchSource.setField('aggs', allAggs); if (sourceQuery) { searchSource.setField('query', sourceQuery); } if (style.isTimeAware() && (await this.isTimeAware())) { - searchSource.setField('filter', [getTimeFilter().createFilter(indexPattern, timeFilters)]); + const timeFilter = getTimeFilter().createFilter(indexPattern, timeFilters); + if (timeFilter) { + searchSource.setField('filter', [timeFilter]); + } } const resp = await this._runEsQuery({ @@ -335,15 +451,17 @@ export class AbstractESSource extends AbstractVectorSource { return resp.aggregations; } - getValueSuggestions = async (field, query) => { + getValueSuggestions = async (field: IField, query: string): Promise => { try { const indexPattern = await this.getIndexPattern(); + const indexPatternField = indexPattern.fields.getByName(field.getRootName())!; return await getAutocompleteService().getValueSuggestions({ indexPattern, - field: indexPattern.fields.getByName(field.getRootName()), + field: indexPatternField, query, }); } catch (error) { + // eslint-disable-next-line no-console console.warn( `Unable to fetch suggestions for field: ${field.getRootName()}, query: ${query}, error: ${ error.message diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.d.ts deleted file mode 100644 index ef1ada8da8289..0000000000000 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { MapQuery, VectorJoinSourceRequestMeta } from '../../../../common/descriptor_types'; -import { IField } from '../../fields/field'; -import { IESAggSource } from '../es_agg_source'; -import { PropertiesMap } from '../../joins/join'; - -export interface IESTermSource extends IESAggSource { - getTermField: () => IField; - hasCompleteConfig: () => boolean; - getWhereQuery: () => MapQuery; - getPropertiesMap: ( - searchFilters: VectorJoinSourceRequestMeta, - leftSourceName: string, - leftFieldName: string, - registerCancelCallback: (callback: () => void) => void - ) => PropertiesMap; -} diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js index 060096157f578..22ef4cc8b373a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js @@ -34,6 +34,7 @@ describe('getMetricFields', () => { id: '1234', indexPatternTitle: indexPatternTitle, term: termFieldName, + indexPatternId: 'foobar', }); const metrics = source.getMetricFields(); expect(metrics[0].getName()).toEqual('__kbnjoin__count__1234'); @@ -46,6 +47,7 @@ describe('getMetricFields', () => { indexPatternTitle: indexPatternTitle, term: termFieldName, metrics: metricExamples, + indexPatternId: 'foobar', }); const metrics = source.getMetricFields(); expect(metrics[0].getName()).toEqual('__kbnjoin__sum_of_myFieldGettingSummed__1234'); diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.js b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts similarity index 65% rename from x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.js rename to x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts index ff52dccdd2ef4..3220253436168 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts @@ -5,8 +5,8 @@ */ import _ from 'lodash'; - import { i18n } from '@kbn/i18n'; +import { ISearchSource, Query } from 'src/plugins/data/public'; import { AGG_TYPE, DEFAULT_MAX_BUCKETS_LIMIT, @@ -20,15 +20,22 @@ import { getField, addFieldToDSL, extractPropertiesFromBucket, + BucketProperties, } from '../../../../common/elasticsearch_util'; +import { + ESTermSourceDescriptor, + VectorJoinSourceRequestMeta, +} from '../../../../common/descriptor_types'; +import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; +import { PropertiesMap } from '../../../../common/elasticsearch_util'; const TERMS_AGG_NAME = 'join'; - const TERMS_BUCKET_KEYS_TO_IGNORE = ['key', 'doc_count']; -export function extractPropertiesMap(rawEsData, countPropertyName) { - const propertiesMap = new Map(); - _.get(rawEsData, ['aggregations', TERMS_AGG_NAME, 'buckets'], []).forEach((termBucket) => { +export function extractPropertiesMap(rawEsData: any, countPropertyName: string): PropertiesMap { + const propertiesMap: PropertiesMap = new Map(); + const buckets: any[] = _.get(rawEsData, ['aggregations', TERMS_AGG_NAME, 'buckets'], []); + buckets.forEach((termBucket: any) => { const properties = extractPropertiesFromBucket(termBucket, TERMS_BUCKET_KEYS_TO_IGNORE); if (countPropertyName) { properties[countPropertyName] = termBucket.doc_count; @@ -41,37 +48,36 @@ export function extractPropertiesMap(rawEsData, countPropertyName) { export class ESTermSource extends AbstractESAggSource { static type = SOURCE_TYPES.ES_TERM_SOURCE; - constructor(descriptor, inspectorAdapters) { - super(descriptor, inspectorAdapters); + private readonly _termField: ESDocField; + readonly _descriptor: ESTermSourceDescriptor; + + constructor(descriptor: ESTermSourceDescriptor, inspectorAdapters: Adapters) { + super(AbstractESAggSource.createDescriptor(descriptor), inspectorAdapters); + this._descriptor = descriptor; this._termField = new ESDocField({ - fieldName: descriptor.term, + fieldName: this._descriptor.term, source: this, origin: this.getOriginForField(), }); } - static renderEditor({}) { - //no need to localize. this editor is never rendered. - return `
editor details
`; - } - hasCompleteConfig() { return _.has(this._descriptor, 'indexPatternId') && _.has(this._descriptor, 'term'); } - getTermField() { + getTermField(): ESDocField { return this._termField; } - getOriginForField() { + getOriginForField(): FIELD_ORIGIN { return FIELD_ORIGIN.JOIN; } - getWhereQuery() { + getWhereQuery(): Query | undefined { return this._descriptor.whereQuery; } - getAggKey(aggType, fieldName) { + getAggKey(aggType: AGG_TYPE, fieldName?: string): string { return getJoinAggKey({ aggType, aggFieldName: fieldName, @@ -79,7 +85,7 @@ export class ESTermSource extends AbstractESAggSource { }); } - getAggLabel(aggType, fieldName) { + getAggLabel(aggType: AGG_TYPE, fieldName: string) { return aggType === AGG_TYPE.COUNT ? i18n.translate('xpack.maps.source.esJoin.countLabel', { defaultMessage: `Count of {indexPatternTitle}`, @@ -88,13 +94,18 @@ export class ESTermSource extends AbstractESAggSource { : super.getAggLabel(aggType, fieldName); } - async getPropertiesMap(searchFilters, leftSourceName, leftFieldName, registerCancelCallback) { + async getPropertiesMap( + searchFilters: VectorJoinSourceRequestMeta, + leftSourceName: string, + leftFieldName: string, + registerCancelCallback: (callback: () => void) => void + ): Promise { if (!this.hasCompleteConfig()) { - return []; + return new Map(); } const indexPattern = await this.getIndexPattern(); - const searchSource = await this.makeSearchSource(searchFilters, 0); + const searchSource: ISearchSource = await this.makeSearchSource(searchFilters, 0); const termsField = getField(indexPattern, this._termField.getName()); const termsAgg = { size: DEFAULT_MAX_BUCKETS_LIMIT }; searchSource.setField('aggs', { @@ -122,16 +133,16 @@ export class ESTermSource extends AbstractESAggSource { return extractPropertiesMap(rawEsData, countPropertyName); } - isFilterByMapBounds() { + isFilterByMapBounds(): boolean { return false; } - async getDisplayName() { - //no need to localize. this is never rendered. + async getDisplayName(): Promise { + // no need to localize. this is never rendered. return `es_table ${this.getIndexPatternId()}`; } - getFieldNames() { + getFieldNames(): string[] { return this.getMetricFields().map((esAggMetricField) => esAggMetricField.getName()); } } diff --git a/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts b/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts index 336a947e9fe4f..6172405152739 100644 --- a/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts @@ -5,10 +5,11 @@ */ import { Feature, FeatureCollection } from 'geojson'; -import { AbstractVectorSource } from '../vector_source'; +import { AbstractVectorSource, GeoJsonWithMeta } from '../vector_source'; import { EMPTY_FEATURE_COLLECTION, SOURCE_TYPES } from '../../../../common/constants'; import { GeojsonFileSourceDescriptor } from '../../../../common/descriptor_types'; import { registerSource } from '../source_registry'; +import { IField } from '../../fields/field'; function getFeatureCollection(geoJson: Feature | FeatureCollection | null): FeatureCollection { if (!geoJson) { @@ -30,26 +31,28 @@ function getFeatureCollection(geoJson: Feature | FeatureCollection | null): Feat } export class GeojsonFileSource extends AbstractVectorSource { - static type = SOURCE_TYPES.GEOJSON_FILE; - static createDescriptor( geoJson: Feature | FeatureCollection | null, name: string ): GeojsonFileSourceDescriptor { return { - type: GeojsonFileSource.type, + type: SOURCE_TYPES.GEOJSON_FILE, __featureCollection: getFeatureCollection(geoJson), name, }; } - async getGeoJsonWithMeta() { + async getGeoJsonWithMeta(): Promise { return { data: (this._descriptor as GeojsonFileSourceDescriptor).__featureCollection, meta: {}, }; } + createField({ fieldName }: { fieldName: string }): IField { + throw new Error('Not implemented'); + } + async getDisplayName() { return (this._descriptor as GeojsonFileSourceDescriptor).name; } diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx index c8a1c346646e0..f8a311429d3dc 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -26,7 +26,7 @@ export const kibanaRegionMapLayerWizardConfig: LayerWizard = { }), icon: 'logoKibana', renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { - const onSourceConfigChange = (sourceConfig: unknown) => { + const onSourceConfigChange = (sourceConfig: { name: string }) => { const sourceDescriptor = KibanaRegionmapSource.createDescriptor(sourceConfig); const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); previewLayers([layerDescriptor]); diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.d.ts b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.d.ts deleted file mode 100644 index db67001dcd85a..0000000000000 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.d.ts +++ /dev/null @@ -1,15 +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 { AbstractVectorSource, IVectorSource } from '../vector_source'; - -export interface IKibanaRegionSource extends IVectorSource { - getVectorFileMeta(): Promise; -} - -export class KibanaRegionSource extends AbstractVectorSource implements IKibanaRegionSource { - getVectorFileMeta(): Promise; -} diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts similarity index 55% rename from x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.js rename to x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts index d937edb4ed362..bf39d78a4784f 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.js +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts @@ -4,29 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractVectorSource } from '../vector_source'; -import { getKibanaRegionList } from '../../../meta'; import { i18n } from '@kbn/i18n'; +import { AbstractVectorSource, GeoJsonWithMeta } from '../vector_source'; +import { getKibanaRegionList } from '../../../meta'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; -import { FIELD_ORIGIN, SOURCE_TYPES } from '../../../../common/constants'; +import { FIELD_ORIGIN, FORMAT_TYPE, SOURCE_TYPES } from '../../../../common/constants'; import { KibanaRegionField } from '../../fields/kibana_region_field'; import { registerSource } from '../source_registry'; +import { KibanaRegionmapSourceDescriptor } from '../../../../common/descriptor_types/source_descriptor_types'; +import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; +import { IField } from '../../fields/field'; +import { LayerConfig } from '../../../../../../../src/plugins/region_map/config'; export const sourceTitle = i18n.translate('xpack.maps.source.kbnRegionMapTitle', { defaultMessage: 'Configured GeoJSON', }); export class KibanaRegionmapSource extends AbstractVectorSource { - static type = SOURCE_TYPES.REGIONMAP_FILE; + readonly _descriptor: KibanaRegionmapSourceDescriptor; - static createDescriptor({ name }) { + static createDescriptor({ name }: { name: string }): KibanaRegionmapSourceDescriptor { return { - type: KibanaRegionmapSource.type, - name: name, + type: SOURCE_TYPES.REGIONMAP_FILE, + name, }; } - createField({ fieldName }) { + constructor(descriptor: KibanaRegionmapSourceDescriptor, inspectorAdapters?: Adapters) { + super(descriptor, inspectorAdapters); + this._descriptor = descriptor; + } + + createField({ fieldName }: { fieldName: string }): KibanaRegionField { return new KibanaRegionField({ fieldName, source: this, @@ -49,10 +58,12 @@ export class KibanaRegionmapSource extends AbstractVectorSource { ]; } - async getVectorFileMeta() { - const regionList = getKibanaRegionList(); - const meta = regionList.find((source) => source.name === this._descriptor.name); - if (!meta) { + async getVectorFileMeta(): Promise { + const regionList: LayerConfig[] = getKibanaRegionList(); + const layerConfig: LayerConfig | undefined = regionList.find( + (regionConfig: LayerConfig) => regionConfig.name === this._descriptor.name + ); + if (!layerConfig) { throw new Error( i18n.translate('xpack.maps.source.kbnRegionMap.noConfigErrorMessage', { defaultMessage: `Unable to find map.regionmap configuration for {name}`, @@ -62,13 +73,13 @@ export class KibanaRegionmapSource extends AbstractVectorSource { }) ); } - return meta; + return layerConfig; } - async getGeoJsonWithMeta() { + async getGeoJsonWithMeta(): Promise { const vectorFileMeta = await this.getVectorFileMeta(); const featureCollection = await AbstractVectorSource.getGeoJson({ - format: vectorFileMeta.format.type, + format: vectorFileMeta.format.type as FORMAT_TYPE, featureCollectionPath: vectorFileMeta.meta.feature_collection_path, fetchUrl: vectorFileMeta.url, }); @@ -78,12 +89,16 @@ export class KibanaRegionmapSource extends AbstractVectorSource { }; } - async getLeftJoinFields() { - const vectorFileMeta = await this.getVectorFileMeta(); - return vectorFileMeta.fields.map((f) => this.createField({ fieldName: f.name })); + async getLeftJoinFields(): Promise { + const vectorFileMeta: LayerConfig = await this.getVectorFileMeta(); + return vectorFileMeta.fields.map( + (field): KibanaRegionField => { + return this.createField({ fieldName: field.name }); + } + ); } - async getDisplayName() { + async getDisplayName(): Promise { return this._descriptor.name; } diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index 440f0cb4457e8..6390626b006b4 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -28,6 +28,7 @@ import { import { MVTField } from '../../fields/mvt_field'; import { UpdateSourceEditor } from './update_source_editor'; import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property'; +import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; export const sourceTitle = i18n.translate( 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle', @@ -66,7 +67,7 @@ export class MVTSingleLayerVectorSource constructor( sourceDescriptor: TiledSingleLayerVectorSourceDescriptor, - inspectorAdapters?: object + inspectorAdapters?: Adapters ) { super(sourceDescriptor, inspectorAdapters); this._descriptor = MVTSingleLayerVectorSource.createDescriptor(sourceDescriptor); @@ -165,22 +166,22 @@ export class MVTSingleLayerVectorSource return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; } - canFormatFeatureProperties() { + canFormatFeatureProperties(): boolean { return !!this._tooltipFields.length; } - getMinZoom() { + getMinZoom(): number { return this._descriptor.minSourceZoom; } - getMaxZoom() { + getMaxZoom(): number { return this._descriptor.maxSourceZoom; } - getBoundsForFilters( + async getBoundsForFilters( boundsFilters: BoundsFilters, registerCancelCallback: (callback: () => void) => void - ): MapExtent | null { + ): Promise { return null; } diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index c4fb5178c0b56..f24ec012836b6 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -9,6 +9,7 @@ import { ReactElement } from 'react'; import { Adapters } from 'src/plugins/inspector/public'; +import { GeoJsonProperties } from 'geojson'; import { copyPersistentState } from '../../reducers/util'; import { IField } from '../fields/field'; @@ -62,7 +63,7 @@ export interface ISource { getIndexPatternIds(): string[]; getQueryableIndexPatternIds(): string[]; getGeoGridPrecision(zoom: number): number; - getPreIndexedShape(): Promise; + getPreIndexedShape(properties: GeoJsonProperties): Promise; createFieldFormatter(field: IField): Promise; getValueSuggestions(field: IField, query: string): Promise; getMinZoom(): number; @@ -72,7 +73,7 @@ export interface ISource { export class AbstractSource implements ISource { readonly _descriptor: AbstractSourceDescriptor; - readonly _inspectorAdapters?: Adapters | undefined; + private readonly _inspectorAdapters?: Adapters; constructor(descriptor: AbstractSourceDescriptor, inspectorAdapters?: Adapters) { this._descriptor = descriptor; @@ -153,7 +154,7 @@ export class AbstractSource implements ISource { return false; } - getJoinsDisabledReason() { + getJoinsDisabledReason(): string | null { return null; } @@ -162,7 +163,7 @@ export class AbstractSource implements ISource { } // Returns geo_shape indexed_shape context for spatial quering by pre-indexed shapes - async getPreIndexedShape(/* properties */): Promise { + async getPreIndexedShape(properties: GeoJsonProperties): Promise { return null; } @@ -183,11 +184,11 @@ export class AbstractSource implements ISource { return false; } - getMinZoom() { + getMinZoom(): number { return MIN_ZOOM; } - getMaxZoom() { + getMaxZoom(): number { return MAX_ZOOM; } diff --git a/x-pack/plugins/maps/public/classes/sources/source_registry.ts b/x-pack/plugins/maps/public/classes/sources/source_registry.ts index 462624dfa6ec9..2bf7e84850693 100644 --- a/x-pack/plugins/maps/public/classes/sources/source_registry.ts +++ b/x-pack/plugins/maps/public/classes/sources/source_registry.ts @@ -6,11 +6,12 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { ISource } from './source'; +import { Adapters } from '../../../../../../src/plugins/inspector/common/adapters'; export type SourceRegistryEntry = { ConstructorFunction: new ( sourceDescriptor: any, // this is the source-descriptor that corresponds specifically to the particular ISource instance - inspectorAdapters?: object + inspectorAdapters?: Adapters ) => ISource; type: string; }; diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts deleted file mode 100644 index 7bf1db43c2871..0000000000000 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; -import { Filter, TimeRange } from 'src/plugins/data/public'; -import { AbstractSource, ISource } from '../source'; -import { IField } from '../../fields/field'; -import { - ESSearchSourceResponseMeta, - MapExtent, - MapFilters, - MapQuery, - VectorSourceRequestMeta, - VectorSourceSyncMeta, -} from '../../../../common/descriptor_types'; -import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; -import { ITooltipProperty } from '../../tooltips/tooltip_property'; -import { DataRequest } from '../../util/data_request'; - -export interface SourceTooltipConfig { - tooltipContent: string | null; - areResultsTrimmed: boolean; -} - -export type GeoJsonFetchMeta = ESSearchSourceResponseMeta; - -export type GeoJsonWithMeta = { - data: FeatureCollection; - meta?: GeoJsonFetchMeta; -}; - -export type BoundsFilters = { - applyGlobalQuery: boolean; - filters: Filter[]; - query?: MapQuery; - sourceQuery?: MapQuery; - timeFilters: TimeRange; -}; - -export interface IVectorSource extends ISource { - getTooltipProperties(properties: GeoJsonProperties): Promise; - getBoundsForFilters( - boundsFilters: BoundsFilters, - registerCancelCallback: (callback: () => void) => void - ): MapExtent | null; - getGeoJsonWithMeta( - layerName: string, - searchFilters: MapFilters, - registerCancelCallback: (callback: () => void) => void, - isRequestStillActive: () => boolean - ): Promise; - - getFields(): Promise; - getFieldByName(fieldName: string): IField | null; - getLeftJoinFields(): Promise; - getSyncMeta(): VectorSourceSyncMeta; - getFieldNames(): string[]; - getApplyGlobalQuery(): boolean; - createField({ fieldName }: { fieldName: string }): IField; - canFormatFeatureProperties(): boolean; - getSupportedShapeTypes(): Promise; - isBoundsAware(): boolean; - getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig; -} - -export class AbstractVectorSource extends AbstractSource implements IVectorSource { - getTooltipProperties(properties: GeoJsonProperties): Promise; - getBoundsForFilters( - boundsFilters: BoundsFilters, - registerCancelCallback: (callback: () => void) => void - ): MapExtent | null; - getGeoJsonWithMeta( - layerName: string, - searchFilters: VectorSourceRequestMeta, - registerCancelCallback: (callback: () => void) => void, - isRequestStillActive: () => boolean - ): Promise; - - getFields(): Promise; - getFieldByName(fieldName: string): IField | null; - getLeftJoinFields(): Promise; - getSyncMeta(): VectorSourceSyncMeta; - getSupportedShapeTypes(): Promise; - canFormatFeatureProperties(): boolean; - getApplyGlobalQuery(): boolean; - getFieldNames(): string[]; - createField({ fieldName }: { fieldName: string }): IField; - isBoundsAware(): boolean; - getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig; -} - -export interface ITiledSingleLayerVectorSource extends IVectorSource { - getUrlTemplateWithMeta( - searchFilters: VectorSourceRequestMeta - ): Promise<{ - layerName: string; - urlTemplate: string; - minSourceZoom: number; - maxSourceZoom: number; - }>; - getMinZoom(): number; - getMaxZoom(): number; - getLayerName(): string; -} diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js deleted file mode 100644 index 9569b8626aabf..0000000000000 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js +++ /dev/null @@ -1,140 +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 { TooltipProperty } from '../../tooltips/tooltip_property'; -import { AbstractSource } from './../source'; -import * as topojson from 'topojson-client'; -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; - -export class AbstractVectorSource extends AbstractSource { - static async getGeoJson({ format, featureCollectionPath, fetchUrl }) { - let fetchedJson; - try { - // TODO proxy map.regionmap url requests through kibana server and then use kfetch - // Can not use kfetch because fetchUrl may point to external URL. (map.regionmap) - const response = await fetch(fetchUrl); - if (!response.ok) { - throw new Error('Request failed'); - } - fetchedJson = await response.json(); - } catch (e) { - throw new Error( - i18n.translate('xpack.maps.source.vetorSource.requestFailedErrorMessage', { - defaultMessage: `Unable to fetch vector shapes from url: {fetchUrl}`, - values: { fetchUrl }, - }) - ); - } - - if (format === 'geojson') { - return fetchedJson; - } - - if (format === 'topojson') { - const features = _.get(fetchedJson, `objects.${featureCollectionPath}`); - return topojson.feature(fetchedJson, features); - } - - throw new Error( - i18n.translate('xpack.maps.source.vetorSource.formatErrorMessage', { - defaultMessage: `Unable to fetch vector shapes from url: {format}`, - values: { format }, - }) - ); - } - - /** - * factory function creating a new field-instance - * @param fieldName - * @param label - * @returns {IField} - */ - createField() { - throw new Error(`Should implemement ${this.constructor.type} ${this}`); - } - - getFieldNames() { - return []; - } - - /** - * Retrieves a field. This may be an existing instance. - * @param fieldName - * @param label - * @returns {IField} - */ - getFieldByName(name) { - return this.createField({ fieldName: name }); - } - - _getTooltipPropertyNames() { - return this._tooltipFields.map((field) => field.getName()); - } - - isFilterByMapBounds() { - return false; - } - - isBoundsAware() { - return false; - } - - async getBoundsForFilters() { - console.warn('Should implement AbstractVectorSource#getBoundsForFilters'); - return null; - } - - async getFields() { - return []; - } - - async getLeftJoinFields() { - return []; - } - - async getGeoJsonWithMeta() { - throw new Error('Should implement VectorSource#getGeoJson'); - } - - canFormatFeatureProperties() { - return false; - } - - // Allow source to filter and format feature properties before displaying to user - async getTooltipProperties(properties) { - const tooltipProperties = []; - for (const key in properties) { - if (key.startsWith('__kbn')) { - //these are system properties and should be ignored - continue; - } - tooltipProperties.push(new TooltipProperty(key, key, properties[key])); - } - return tooltipProperties; - } - - async isTimeAware() { - return false; - } - - showJoinEditor() { - return true; - } - - async getSupportedShapeTypes() { - return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; - } - - getSourceTooltipContent(/* sourceDataRequest */) { - return { tooltipContent: null, areResultsTrimmed: false }; - } - - getSyncMeta() { - return {}; - } -} diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx new file mode 100644 index 0000000000000..38ff3b49a87f4 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -0,0 +1,209 @@ +/* + * 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. + */ + +// @ts-expect-error +import * as topojson from 'topojson-client'; +import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { FeatureCollection, GeoJsonProperties } from 'geojson'; +import { Filter, TimeRange } from 'src/plugins/data/public'; +import { FORMAT_TYPE, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; +import { TooltipProperty, ITooltipProperty } from '../../tooltips/tooltip_property'; +import { AbstractSource, ISource } from '../source'; +import { IField } from '../../fields/field'; +import { + ESSearchSourceResponseMeta, + MapExtent, + MapQuery, + VectorSourceRequestMeta, + VectorSourceSyncMeta, +} from '../../../../common/descriptor_types'; +import { DataRequest } from '../../util/data_request'; + +export interface SourceTooltipConfig { + tooltipContent: string | null; + areResultsTrimmed: boolean; +} + +export type GeoJsonFetchMeta = ESSearchSourceResponseMeta; + +export interface GeoJsonWithMeta { + data: FeatureCollection; + meta?: GeoJsonFetchMeta; +} + +export interface BoundsFilters { + applyGlobalQuery: boolean; + filters: Filter[]; + query?: MapQuery; + sourceQuery?: MapQuery; + timeFilters: TimeRange; +} + +export interface IVectorSource extends ISource { + getTooltipProperties(properties: GeoJsonProperties): Promise; + getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (callback: () => void) => void + ): Promise; + getGeoJsonWithMeta( + layerName: string, + searchFilters: VectorSourceRequestMeta, + registerCancelCallback: (callback: () => void) => void, + isRequestStillActive: () => boolean + ): Promise; + + getFields(): Promise; + getFieldByName(fieldName: string): IField | null; + getLeftJoinFields(): Promise; + getSyncMeta(): VectorSourceSyncMeta | null; + getFieldNames(): string[]; + getApplyGlobalQuery(): boolean; + createField({ fieldName }: { fieldName: string }): IField; + canFormatFeatureProperties(): boolean; + getSupportedShapeTypes(): Promise; + isBoundsAware(): boolean; + getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig; +} + +export interface ITiledSingleLayerVectorSource extends IVectorSource { + getUrlTemplateWithMeta( + searchFilters: VectorSourceRequestMeta + ): Promise<{ + layerName: string; + urlTemplate: string; + minSourceZoom: number; + maxSourceZoom: number; + }>; + getMinZoom(): number; + getMaxZoom(): number; + getLayerName(): string; +} + +export class AbstractVectorSource extends AbstractSource implements IVectorSource { + static async getGeoJson({ + format, + featureCollectionPath, + fetchUrl, + }: { + format: FORMAT_TYPE; + featureCollectionPath: string; + fetchUrl: string; + }) { + let fetchedJson; + try { + const response = await fetch(fetchUrl); + if (!response.ok) { + throw new Error('Request failed'); + } + fetchedJson = await response.json(); + } catch (e) { + throw new Error( + i18n.translate('xpack.maps.source.vetorSource.requestFailedErrorMessage', { + defaultMessage: `Unable to fetch vector shapes from url: {fetchUrl}`, + values: { fetchUrl }, + }) + ); + } + + if (format === FORMAT_TYPE.GEOJSON) { + return fetchedJson; + } + + if (format === FORMAT_TYPE.TOPOJSON) { + const features = _.get(fetchedJson, `objects.${featureCollectionPath}`); + return topojson.feature(fetchedJson, features); + } + + throw new Error( + i18n.translate('xpack.maps.source.vetorSource.formatErrorMessage', { + defaultMessage: `Unable to fetch vector shapes from url: {format}`, + values: { format }, + }) + ); + } + + getFieldNames(): string[] { + return []; + } + + createField({ fieldName }: { fieldName: string }): IField { + throw new Error('Not implemented'); + } + + getFieldByName(fieldName: string): IField | null { + return this.createField({ fieldName }); + } + + isFilterByMapBounds() { + return false; + } + + isBoundsAware(): boolean { + return false; + } + + async getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (callback: () => void) => void + ): Promise { + return null; + } + + async getFields(): Promise { + return []; + } + + async getLeftJoinFields(): Promise { + return []; + } + + async getGeoJsonWithMeta( + layerName: string, + searchFilters: VectorSourceRequestMeta, + registerCancelCallback: (callback: () => void) => void, + isRequestStillActive: () => boolean + ): Promise { + throw new Error('Should implement VectorSource#getGeoJson'); + } + + canFormatFeatureProperties() { + return false; + } + + // Allow source to filter and format feature properties before displaying to user + async getTooltipProperties(properties: GeoJsonProperties): Promise { + const tooltipProperties: ITooltipProperty[] = []; + for (const key in properties) { + if (key.startsWith('__kbn')) { + // these are system properties and should be ignored + continue; + } + tooltipProperties.push(new TooltipProperty(key, key, properties[key])); + } + return tooltipProperties; + } + + async isTimeAware() { + return false; + } + + showJoinEditor() { + return true; + } + + async getSupportedShapeTypes() { + return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; + } + + getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig { + return { tooltipContent: null, areResultsTrimmed: false }; + } + + getSyncMeta(): VectorSourceSyncMeta | null { + return null; + } +} diff --git a/x-pack/plugins/maps/public/classes/util/valid_string_config.ts b/x-pack/plugins/maps/public/classes/util/valid_string_config.ts new file mode 100644 index 0000000000000..29080f7988bea --- /dev/null +++ b/x-pack/plugins/maps/public/classes/util/valid_string_config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Validate user-generated data (e.g. descriptors). Possibly dirty or of wrong type. + * @param value + */ +export function isValidStringConfig(value: any): boolean { + return typeof value === 'string' && value !== ''; +} diff --git a/x-pack/plugins/maps/public/meta.ts b/x-pack/plugins/maps/public/meta.ts index 4eca6c3e671b7..929050338de72 100644 --- a/x-pack/plugins/maps/public/meta.ts +++ b/x-pack/plugins/maps/public/meta.ts @@ -29,8 +29,9 @@ import { getKibanaVersion, } from './kibana_services'; import { getLicenseId } from './licensed_features'; +import { LayerConfig } from '../../../../src/plugins/region_map/config'; -export function getKibanaRegionList(): unknown[] { +export function getKibanaRegionList(): LayerConfig[] { return getRegionmapLayers(); } diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 4b5122050eb71..eac71e627fd7d 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -34,6 +34,7 @@ import { import { extractFeaturesFromFilters } from '../../common/elasticsearch_util'; import { MapStoreState } from '../reducers/store'; import { + AbstractSourceDescriptor, DataRequestDescriptor, DrawState, Goto, @@ -94,7 +95,13 @@ export function createLayerInstance( } } -function createSourceInstance(sourceDescriptor: any, inspectorAdapters?: Adapters): ISource { +function createSourceInstance( + sourceDescriptor: AbstractSourceDescriptor | null, + inspectorAdapters?: Adapters +): ISource { + if (sourceDescriptor === null) { + throw new Error('Source-descriptor should be initialized'); + } const source = getSourceByType(sourceDescriptor.type); if (!source) { throw new Error(`Unrecognized sourceType ${sourceDescriptor.type}`); From ea0736e74a32a9bb88fb840b3e2eaa51106b74db Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 5 Nov 2020 09:01:42 -0500 Subject: [PATCH 21/57] [Remote clusters] Refactor tests (#82517) --- .../add/remote_clusters_add.helpers.js | 10 ++- .../add/remote_clusters_add.test.js | 83 +++++++++++++------ .../edit/remote_clusters_edit.helpers.js | 1 - .../edit/remote_clusters_edit.test.js | 18 ++-- .../helpers/setup_environment.js | 17 ++-- .../list/remote_clusters_list.helpers.js | 55 +++++++++--- .../list/remote_clusters_list.test.js | 36 ++++---- x-pack/test_utils/README.md | 2 +- 8 files changed, 142 insertions(+), 80 deletions(-) diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.js index c8badc92ab11c..f420e83adc031 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.helpers.js @@ -3,10 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { act } from 'react-dom/test-utils'; import { registerTestBed } from '../../../../../test_utils'; -/* eslint-disable @kbn/eslint/no-restricted-paths */ import { RemoteClusterAdd } from '../../../public/application/sections/remote_cluster_add'; import { createRemoteClustersStore } from '../../../public/application/store'; import { registerRouter } from '../../../public/application/services/routing'; @@ -24,8 +24,12 @@ export const setup = (props) => { const testBed = initTestBed(props); // User actions - const clickSaveForm = () => { - testBed.find('remoteClusterFormSaveButton').simulate('click'); + const clickSaveForm = async () => { + await act(async () => { + testBed.find('remoteClusterFormSaveButton').simulate('click'); + }); + + testBed.component.update(); }; return { diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.js index 05a4a2e330325..545e3dd0ba969 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.js @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { act } from 'react-dom/test-utils'; -import { nextTick, setupEnvironment } from '../helpers'; +import { setupEnvironment } from '../helpers'; import { NON_ALPHA_NUMERIC_CHARS, ACCENTED_CHARS } from './special_characters'; import { setup } from './remote_clusters_add.helpers'; @@ -15,6 +16,7 @@ describe('Create Remote cluster', () => { let actions; let form; let server; + let component; beforeAll(() => { ({ server } = setupEnvironment()); @@ -24,8 +26,11 @@ describe('Create Remote cluster', () => { server.restore(); }); - beforeEach(() => { - ({ form, exists, find, actions } = setup()); + beforeEach(async () => { + await act(async () => { + ({ form, exists, find, actions, component } = setup()); + }); + component.update(); }); test('should have the title of the page set correctly', () => { @@ -45,7 +50,11 @@ describe('Create Remote cluster', () => { false ); - form.toggleEuiSwitch('remoteClusterFormSkipUnavailableFormToggle'); + act(() => { + form.toggleEuiSwitch('remoteClusterFormSkipUnavailableFormToggle'); + }); + + component.update(); expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe(true); }); @@ -56,16 +65,20 @@ describe('Create Remote cluster', () => { // By default it should be set to "false" expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(false); - form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); + act(() => { + form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); + }); + + component.update(); expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(true); }); - test('should display errors and disable the save button when clicking "save" without filling the form', () => { + test('should display errors and disable the save button when clicking "save" without filling the form', async () => { expect(exists('remoteClusterFormGlobalError')).toBe(false); expect(find('remoteClusterFormSaveButton').props().disabled).toBe(false); - actions.clickSaveForm(); + await actions.clickSaveForm(); expect(exists('remoteClusterFormGlobalError')).toBe(true); expect(form.getErrorsMessages()).toEqual([ @@ -83,19 +96,22 @@ describe('Create Remote cluster', () => { let form; beforeEach(async () => { - ({ component, form, actions } = setup()); + await act(async () => { + ({ component, form, actions } = setup()); + }); - await nextTick(); component.update(); }); - test('should not allow spaces', () => { + test('should not allow spaces', async () => { form.setInputValue('remoteClusterFormNameInput', 'with space'); - actions.clickSaveForm(); + + await actions.clickSaveForm(); + expect(form.getErrorsMessages()).toContain('Spaces are not allowed in the name.'); }); - test('should only allow alpha-numeric characters, "-" (dash) and "_" (underscore)', () => { + test('should only allow alpha-numeric characters, "-" (dash) and "_" (underscore)', async () => { const expectInvalidChar = (char) => { if (char === '-' || char === '_') { return; @@ -103,6 +119,7 @@ describe('Create Remote cluster', () => { try { form.setInputValue('remoteClusterFormNameInput', `with${char}`); + expect(form.getErrorsMessages()).toContain( `Remove the character ${char} from the name.` ); @@ -111,7 +128,7 @@ describe('Create Remote cluster', () => { } }; - actions.clickSaveForm(); // display form errors + await actions.clickSaveForm(); // display form errors [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS].forEach(expectInvalidChar); }); @@ -120,13 +137,20 @@ describe('Create Remote cluster', () => { describe('seeds', () => { let actions; let form; + let component; beforeEach(async () => { - ({ form, actions } = setup()); + await act(async () => { + ({ form, actions, component } = setup()); + }); + + component.update(); + + form.setInputValue('remoteClusterFormNameInput', 'remote_cluster_test'); }); - test('should only allow alpha-numeric characters and "-" (dash) in the node "host" part', () => { - actions.clickSaveForm(); // display form errors + test('should only allow alpha-numeric characters and "-" (dash) in the node "host" part', async () => { + await actions.clickSaveForm(); // display form errors const notInArray = (array) => (value) => array.indexOf(value) < 0; @@ -142,8 +166,8 @@ describe('Create Remote cluster', () => { .forEach(expectInvalidChar); }); - test('should require a numeric "port" to be set', () => { - actions.clickSaveForm(); + test('should require a numeric "port" to be set', async () => { + await actions.clickSaveForm(); form.setComboBoxValue('remoteClusterFormSeedsInput', '192.168.1.1'); expect(form.getErrorsMessages()).toContain('A port is required.'); @@ -156,16 +180,25 @@ describe('Create Remote cluster', () => { describe('proxy address', () => { let actions; let form; + let component; beforeEach(async () => { - ({ form, actions } = setup()); + await act(async () => { + ({ form, actions, component } = setup()); + }); - // Enable "proxy" mode - form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); + component.update(); + + act(() => { + // Enable "proxy" mode + form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); + }); + + component.update(); }); - test('should only allow alpha-numeric characters and "-" (dash) in the proxy address "host" part', () => { - actions.clickSaveForm(); // display form errors + test('should only allow alpha-numeric characters and "-" (dash) in the proxy address "host" part', async () => { + await actions.clickSaveForm(); // display form errors const notInArray = (array) => (value) => array.indexOf(value) < 0; @@ -181,8 +214,8 @@ describe('Create Remote cluster', () => { .forEach(expectInvalidChar); }); - test('should require a numeric "port" to be set', () => { - actions.clickSaveForm(); + test('should require a numeric "port" to be set', async () => { + await actions.clickSaveForm(); form.setInputValue('remoteClusterFormProxyAddressInput', '192.168.1.1'); expect(form.getErrorsMessages()).toContain('A port is required.'); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.js index b5402f3b017f0..331ef24d1d8a1 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.helpers.js @@ -6,7 +6,6 @@ import { registerTestBed } from '../../../../../test_utils'; -/* eslint-disable @kbn/eslint/no-restricted-paths */ import { RemoteClusterEdit } from '../../../public/application/sections/remote_cluster_edit'; import { createRemoteClustersStore } from '../../../public/application/store'; import { registerRouter } from '../../../public/application/services/routing'; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.js index b0e0832cb0831..d3dee936c68dc 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/edit/remote_clusters_edit.test.js @@ -16,28 +16,23 @@ import { } from './remote_clusters_edit.helpers'; describe('Edit Remote cluster', () => { - let server; - let httpRequestsMockHelpers; let component; let find; let exists; - let waitFor; - beforeAll(() => { - ({ server, httpRequestsMockHelpers } = setupEnvironment()); - }); + const { server, httpRequestsMockHelpers } = setupEnvironment(); afterAll(() => { server.restore(); }); - beforeEach(async () => { - httpRequestsMockHelpers.setLoadRemoteClustersResponse([REMOTE_CLUSTER_EDIT]); + httpRequestsMockHelpers.setLoadRemoteClustersResponse([REMOTE_CLUSTER_EDIT]); + beforeEach(async () => { await act(async () => { - ({ component, find, exists, waitFor } = setup()); - await waitFor('remoteClusterForm'); + ({ component, find, exists } = setup()); }); + component.update(); }); test('should have the title of the page set correctly', () => { @@ -59,9 +54,10 @@ describe('Edit Remote cluster', () => { await act(async () => { addRemoteClusterTestBed = setupRemoteClustersAdd(); - addRemoteClusterTestBed.waitFor('remoteClusterAddPage'); }); + addRemoteClusterTestBed.component.update(); + const formEdit = component.find(RemoteClusterForm); const formAdd = addRemoteClusterTestBed.component.find(RemoteClusterForm); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js index c912a4ddabc9d..de5c1e5290540 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js @@ -3,20 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import axios from 'axios'; +import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { notificationServiceMock, fatalErrorsServiceMock, docLinksServiceMock, - injectedMetadataServiceMock, } from '../../../../../../src/core/public/mocks'; import { usageCollectionPluginMock } from '../../../../../../src/plugins/usage_collection/public/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { HttpService } from '../../../../../../src/core/public/http'; - -/* eslint-disable @kbn/eslint/no-restricted-paths */ import { init as initBreadcrumb } from '../../../public/application/services/breadcrumb'; import { init as initHttp } from '../../../public/application/services/http'; import { init as initNotification } from '../../../public/application/services/notification'; @@ -25,10 +22,10 @@ import { init as initDocumentation } from '../../../public/application/services/ import { init as initHttpRequests } from './http_requests'; export const setupEnvironment = () => { - const httpServiceSetupMock = new HttpService().setup({ - injectedMetadata: injectedMetadataServiceMock.createSetupContract(), - fatalErrors: fatalErrorsServiceMock.createSetupContract(), - }); + // axios has a similar interface to HttpSetup, but we + // flatten out the response. + const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); + mockHttpClient.interceptors.response.use(({ data }) => data); initBreadcrumb(() => {}); initDocumentation(docLinksServiceMock.createStartContract()); @@ -37,7 +34,7 @@ export const setupEnvironment = () => { notificationServiceMock.createSetupContract().toasts, fatalErrorsServiceMock.createSetupContract() ); - initHttp(httpServiceSetupMock); + initHttp(mockHttpClient); const { server, httpRequestsMockHelpers } = initHttpRequests(); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.helpers.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.helpers.js index ce9638d95bd28..5f34728def3d3 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.helpers.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.helpers.js @@ -3,10 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { act } from 'react-dom/test-utils'; import { registerTestBed, findTestSubject } from '../../../../../test_utils'; -/* eslint-disable @kbn/eslint/no-restricted-paths */ import { RemoteClusterList } from '../../../public/application/sections/remote_cluster_list'; import { createRemoteClustersStore } from '../../../public/application/store'; import { registerRouter } from '../../../public/application/services/routing'; @@ -29,15 +29,26 @@ export const setup = (props) => { const { rows } = testBed.table.getMetaData(EUI_TABLE); const row = rows[index]; const checkBox = row.reactWrapper.find('input').hostNodes(); - checkBox.simulate('change', { target: { checked: true } }); + + act(() => { + checkBox.simulate('change', { target: { checked: true } }); + }); + + testBed.component.update(); }; const clickBulkDeleteButton = () => { - testBed.find('remoteClusterBulkDeleteButton').simulate('click'); + const { find, component } = testBed; + act(() => { + find('remoteClusterBulkDeleteButton').simulate('click'); + }); + + component.update(); }; const clickRowActionButtonAt = (index = 0, action = 'delete') => { - const { rows } = testBed.table.getMetaData(EUI_TABLE); + const { table, component } = testBed; + const { rows } = table.getMetaData(EUI_TABLE); const indexLastColumn = rows[index].columns.length - 1; const tableCellActions = rows[index].columns[indexLastColumn].reactWrapper; @@ -45,32 +56,54 @@ export const setup = (props) => { if (action === 'delete') { button = findTestSubject(tableCellActions, 'remoteClusterTableRowRemoveButton'); } else if (action === 'edit') { - findTestSubject(tableCellActions, 'remoteClusterTableRowEditButton'); + button = findTestSubject(tableCellActions, 'remoteClusterTableRowEditButton'); } if (!button) { throw new Error(`Button for action "${action}" not found.`); } - button.simulate('click'); + act(() => { + button.simulate('click'); + }); + + component.update(); }; const clickConfirmModalDeleteRemoteCluster = () => { - const modal = testBed.find('remoteClustersDeleteConfirmModal'); - findTestSubject(modal, 'confirmModalConfirmButton').simulate('click'); + const { find, component } = testBed; + const modal = find('remoteClustersDeleteConfirmModal'); + + act(() => { + findTestSubject(modal, 'confirmModalConfirmButton').simulate('click'); + }); + + component.update(); }; const clickRemoteClusterAt = (index = 0) => { - const { rows } = testBed.table.getMetaData(EUI_TABLE); + const { table, component } = testBed; + const { rows } = table.getMetaData(EUI_TABLE); const remoteClusterLink = findTestSubject( rows[index].reactWrapper, 'remoteClustersTableListClusterLink' ); - remoteClusterLink.simulate('click'); + + act(() => { + remoteClusterLink.simulate('click'); + }); + + component.update(); }; const clickPaginationNextButton = () => { - testBed.find('remoteClusterListTable.pagination-button-next').simulate('click'); + const { find, component } = testBed; + + act(() => { + find('remoteClusterListTable.pagination-button-next').simulate('click'); + }); + + component.update(); }; return { diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js index d75921c5f49f2..765da32260eb7 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js @@ -10,25 +10,23 @@ import { getRemoteClusterMock } from '../../../fixtures/remote_cluster'; import { PROXY_MODE } from '../../../common/constants'; -import { setupEnvironment, nextTick, getRandomString, findTestSubject } from '../helpers'; +import { setupEnvironment, getRandomString, findTestSubject } from '../helpers'; import { setup } from './remote_clusters_list.helpers'; describe('', () => { - let server; - let httpRequestsMockHelpers; + const { server, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { - ({ server, httpRequestsMockHelpers } = setupEnvironment()); + jest.useFakeTimers(); }); afterAll(() => { + jest.useRealTimers(); server.restore(); }); - beforeEach(() => { - httpRequestsMockHelpers.setLoadRemoteClustersResponse([]); - }); + httpRequestsMockHelpers.setLoadRemoteClustersResponse([]); describe('on component mount', () => { let exists; @@ -47,9 +45,10 @@ describe('', () => { let component; beforeEach(async () => { - ({ exists, component } = setup()); + await act(async () => { + ({ exists, component } = setup()); + }); - await nextTick(100); // We need to wait next tick for the mock server response to kick in component.update(); }); @@ -66,7 +65,7 @@ describe('', () => { let find; let table; let actions; - let waitFor; + let component; let form; const remoteClusters = [ @@ -87,9 +86,10 @@ describe('', () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); await act(async () => { - ({ find, table, actions, waitFor, form } = setup()); - await waitFor('remoteClusterListTable'); + ({ find, table, actions, form, component } = setup()); }); + + component.update(); }); test('pagination works', () => { @@ -117,7 +117,6 @@ describe('', () => { let actions; let tableCellsValues; let rows; - let waitFor; // For deterministic tests, we need to make sure that remoteCluster1 comes before remoteCluster2 // in the table list that is rendered. As the table orders alphabetically by index name @@ -151,11 +150,11 @@ describe('', () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); await act(async () => { - ({ component, find, exists, table, actions, waitFor } = setup()); - - await waitFor('remoteClusterListTable'); + ({ component, find, exists, table, actions } = setup()); }); + component.update(); + // Read the remote clusters list table ({ rows, tableCellsValues } = table.getMetaData('remoteClusterListTable')); }); @@ -282,10 +281,11 @@ describe('', () => { actions.clickConfirmModalDeleteRemoteCluster(); await act(async () => { - await nextTick(600); // there is a 500ms timeout in the api action - component.update(); + jest.advanceTimersByTime(600); // there is a 500ms timeout in the api action }); + component.update(); + ({ rows } = table.getMetaData('remoteClusterListTable')); expect(rows.length).toBe(2); diff --git a/x-pack/test_utils/README.md b/x-pack/test_utils/README.md index 04c920c4ae834..a6ca1a5d86866 100644 --- a/x-pack/test_utils/README.md +++ b/x-pack/test_utils/README.md @@ -122,7 +122,7 @@ In order to prevent flakiness in component integration tests, please consider th - Be **synchronous** as much as possible. ​ - Hooks are delicate when it comes to state updates. Sometimes calling `act()` synchronously works, sometimes it doesn't. The reasoning behind this isn't clear yet. The best approach is to try synchrounsly first and if it fails, because of an `act()` error, then use the async version. + Hooks are delicate when it comes to state updates. Sometimes calling `act()` synchronously works, sometimes it doesn't. The reasoning behind this isn't clear yet. The best approach is to try synchronously first and if it fails, because of an `act()` error, then use the async version. ```js // First try this From 3c99839cf7a624e7a8201f8636ae8c9b10cd8a9c Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 5 Nov 2020 08:37:16 -0600 Subject: [PATCH 22/57] disable test 'allows to assign tags to the new visualization' --- .../functional/tests/visualize_integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts index 2ccdd4ecf9690..d8bd39ac7dc53 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts @@ -83,7 +83,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('creating', () => { - it('allows to assign tags to the new visualization', async () => { + it.skip('allows to assign tags to the new visualization', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickMarkdownWidget(); From ef5287f9012a9abd698b6498559523cf1102bedb Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 5 Nov 2020 10:30:47 -0500 Subject: [PATCH 23/57] [SECURITY SOLUTIONS] Bring Async Search Back (#82650) * bring async search back to the security solutions * remove id for timeline details searchStrategy * fix events count * fix total count * update units * update integration tests Co-authored-by: Angela Chuang Co-authored-by: Patryk Kopycinski --- .../events/last_event_time/index.ts | 3 --- .../containers/matrix_histogram/index.ts | 3 --- .../containers/authentications/index.tsx | 2 -- .../hosts/containers/hosts/details/_index.tsx | 4 +--- .../public/hosts/containers/hosts/index.tsx | 2 -- .../kpi_hosts/authentications/index.tsx | 2 -- .../containers/kpi_hosts/hosts/index.tsx | 2 -- .../containers/kpi_hosts/unique_ips/index.tsx | 2 -- .../containers/uncommon_processes/index.tsx | 2 -- .../network/containers/details/index.tsx | 2 -- .../containers/kpi_network/dns/index.tsx | 2 -- .../kpi_network/network_events/index.tsx | 2 -- .../kpi_network/tls_handshakes/index.tsx | 2 -- .../kpi_network/unique_flows/index.tsx | 2 -- .../kpi_network/unique_private_ips/index.tsx | 2 -- .../network/containers/network_dns/index.tsx | 2 -- .../network/containers/network_http/index.tsx | 2 -- .../network_top_countries/index.tsx | 16 +------------- .../containers/network_top_n_flow/index.tsx | 2 -- .../public/network/containers/tls/index.tsx | 2 -- .../public/network/containers/users/index.tsx | 3 --- .../containers/overview_host/index.tsx | 2 -- .../containers/overview_network/index.tsx | 2 -- .../timelines/containers/details/index.tsx | 3 --- .../public/timelines/containers/index.tsx | 8 +++---- .../uncommon_processes/__mocks__/index.ts | 20 ++++++++--------- .../hosts/uncommon_processes/helpers.ts | 5 +++-- .../factory/matrix_histogram/index.ts | 2 +- .../factory/network/kpi/dns/index.ts | 4 +--- .../network/kpi/network_events/index.ts | 4 +--- .../network/kpi/tls_handshakes/index.ts | 4 +--- .../security_solution/index.ts | 3 ++- .../timeline/factory/events/all/index.ts | 4 ++-- .../server/search_strategy/timeline/index.ts | 3 ++- .../apis/security_solution/host_details.ts | 2 +- .../apis/security_solution/kpi_hosts.ts | 12 +++++----- .../apis/security_solution/kpi_network.ts | 22 +++++++++---------- 37 files changed, 46 insertions(+), 115 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts index ab44cbd65516e..2d85c1d60a7b0 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts @@ -25,8 +25,6 @@ import { import * as i18n from './translations'; import { DocValueFields } from '../../../../../common/search_strategy'; -const ID = 'timelineEventsLastEventTimeQuery'; - export interface UseTimelineLastEventTimeArgs { lastSeen: string | null; refetch: inputsModel.Refetch; @@ -56,7 +54,6 @@ export const useTimelineLastEventTime = ({ defaultIndex: indexNames, docValueFields, factoryQueryType: TimelineEventsQueries.lastEventTime, - id: ID, indexKey, details, }); diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 6250a4fd959b6..8db513da1f3a1 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -45,8 +45,6 @@ export interface UseMatrixHistogramArgs { }>; } -const ID = 'matrixHistogramQuery'; - export const useMatrixHistogram = ({ endDate, errorMessage, @@ -73,7 +71,6 @@ export const useMatrixHistogram = ({ factoryQueryType: MatrixHistogramQuery, filterQuery: createFilter(filterQuery), histogramType, - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index 6418ea83d97f9..3cc87bdd275df 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -88,7 +88,6 @@ export const useAuthentications = ({ docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.authentications, filterQuery: createFilter(filterQuery), - id: ID, pagination: generateTablePaginationOptions(activePage, limit), timerange: { interval: '12h', @@ -203,7 +202,6 @@ export const useAuthentications = ({ docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.authentications, filterQuery: createFilter(filterQuery), - id: ID, pagination: generateTablePaginationOptions(activePage, limit), timerange: { interval: '12h', diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx index 2dec01dc4d9e3..61c3f63bdb4aa 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx @@ -65,7 +65,6 @@ export const useHostDetails = ({ ? { defaultIndex: indexNames, hostName, - id, factoryQueryType: HostsQueries.details, timerange: { interval: '12h', @@ -79,7 +78,7 @@ export const useHostDetails = ({ const [hostDetailsResponse, setHostDetailsResponse] = useState({ endDate, hostDetails: {}, - id: ID, + id, inspect: { dsl: [], response: [], @@ -154,7 +153,6 @@ export const useHostDetails = ({ defaultIndex: indexNames, factoryQueryType: HostsQueries.details, hostName, - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 9dd4881b3c9ff..1228f94c7a39a 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -83,7 +83,6 @@ export const useAllHost = ({ docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.hosts, filterQuery: createFilter(filterQuery), - id: ID, pagination: generateTablePaginationOptions(activePage, limit), timerange: { interval: '12h', @@ -200,7 +199,6 @@ export const useAllHost = ({ docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.hosts, filterQuery: createFilter(filterQuery), - id: ID, pagination: generateTablePaginationOptions(activePage, limit), timerange: { interval: '12h', diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx index 90be23b48786c..d433fef98bece 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx @@ -61,7 +61,6 @@ export const useHostsKpiAuthentications = ({ defaultIndex: indexNames, factoryQueryType: HostsKpiQueries.kpiAuthentications, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, @@ -159,7 +158,6 @@ export const useHostsKpiAuthentications = ({ defaultIndex: indexNames, factoryQueryType: HostsKpiQueries.kpiAuthentications, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx index 2bb08dec78e8f..13d9bf368fc3d 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx @@ -60,7 +60,6 @@ export const useHostsKpiHosts = ({ defaultIndex: indexNames, factoryQueryType: HostsKpiQueries.kpiHosts, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, @@ -149,7 +148,6 @@ export const useHostsKpiHosts = ({ defaultIndex: indexNames, factoryQueryType: HostsKpiQueries.kpiHosts, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx index e5ef53643ff53..fb1d0e05dd2a0 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx @@ -61,7 +61,6 @@ export const useHostsKpiUniqueIps = ({ defaultIndex: indexNames, factoryQueryType: HostsKpiQueries.kpiUniqueIps, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, @@ -156,7 +155,6 @@ export const useHostsKpiUniqueIps = ({ defaultIndex: indexNames, factoryQueryType: HostsKpiQueries.kpiUniqueIps, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx index 2bf97c896f5e5..ec792a6cad075 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx @@ -85,7 +85,6 @@ export const useUncommonProcesses = ({ docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.uncommonProcesses, filterQuery: createFilter(filterQuery), - id: ID, pagination: generateTablePaginationOptions(activePage, limit), timerange: { interval: '12h', @@ -204,7 +203,6 @@ export const useUncommonProcesses = ({ docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.uncommonProcesses, filterQuery: createFilter(filterQuery), - id: ID, pagination: generateTablePaginationOptions(activePage, limit), timerange: { interval: '12h', diff --git a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx index 238270107b071..2d5ed093ca1e4 100644 --- a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx @@ -69,7 +69,6 @@ export const useNetworkDetails = ({ docValueFields: docValueFields ?? [], factoryQueryType: NetworkQueries.details, filterQuery: createFilter(filterQuery), - id, ip, } : null @@ -153,7 +152,6 @@ export const useNetworkDetails = ({ docValueFields: docValueFields ?? [], factoryQueryType: NetworkQueries.details, filterQuery: createFilter(filterQuery), - id, ip, }; if (!skip && !deepEqual(prevRequest, myRequest)) { diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx index aa0e607fc3c05..65f10a03cf13d 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx @@ -65,7 +65,6 @@ export const useNetworkKpiDns = ({ defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.dns, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, @@ -152,7 +151,6 @@ export const useNetworkKpiDns = ({ defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.dns, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx index 9ab14602140f7..a8f1a7abe2d44 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx @@ -65,7 +65,6 @@ export const useNetworkKpiNetworkEvents = ({ defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.networkEvents, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, @@ -157,7 +156,6 @@ export const useNetworkKpiNetworkEvents = ({ defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.networkEvents, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx index bc32395c100f2..61861de9c0033 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx @@ -65,7 +65,6 @@ export const useNetworkKpiTlsHandshakes = ({ defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.tlsHandshakes, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, @@ -156,7 +155,6 @@ export const useNetworkKpiTlsHandshakes = ({ defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.tlsHandshakes, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx index 256953efac146..594fc2a2adcfa 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx @@ -65,7 +65,6 @@ export const useNetworkKpiUniqueFlows = ({ defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.uniqueFlows, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, @@ -157,7 +156,6 @@ export const useNetworkKpiUniqueFlows = ({ defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.uniqueFlows, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx index 54307eb7c4c1d..8484f1388caac 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx @@ -69,7 +69,6 @@ export const useNetworkKpiUniquePrivateIps = ({ defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.uniquePrivateIps, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, @@ -168,7 +167,6 @@ export const useNetworkKpiUniquePrivateIps = ({ defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.uniquePrivateIps, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 576fc810e9c5f..92a8f8c49dfc6 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -79,7 +79,6 @@ export const useNetworkDns = ({ defaultIndex: indexNames, factoryQueryType: NetworkQueries.dns, filterQuery: createFilter(filterQuery), - id: ID, isPtrIncluded, pagination: generateTablePaginationOptions(activePage, limit), sort, @@ -197,7 +196,6 @@ export const useNetworkDns = ({ isPtrIncluded, factoryQueryType: NetworkQueries.dns, filterQuery: createFilter(filterQuery), - id: ID, pagination: generateTablePaginationOptions(activePage, limit), sort, timerange: { diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index 356173fa2ac71..e4fa68b75999f 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -82,7 +82,6 @@ export const useNetworkHttp = ({ defaultIndex: indexNames, factoryQueryType: NetworkQueries.http, filterQuery: createFilter(filterQuery), - id: ID, ip, pagination: generateTablePaginationOptions(activePage, limit), sort: sort as SortField, @@ -197,7 +196,6 @@ export const useNetworkHttp = ({ defaultIndex: indexNames, factoryQueryType: NetworkQueries.http, filterQuery: createFilter(filterQuery), - id: ID, ip, pagination: generateTablePaginationOptions(activePage, limit), sort: sort as SortField, diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index c2dc638fa719f..4b0ea2b82855b 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -86,7 +86,6 @@ export const useNetworkTopCountries = ({ factoryQueryType: NetworkQueries.topCountries, filterQuery: createFilter(filterQuery), flowTarget, - id: queryId, ip, pagination: generateTablePaginationOptions(activePage, limit), sort, @@ -204,7 +203,6 @@ export const useNetworkTopCountries = ({ factoryQueryType: NetworkQueries.topCountries, filterQuery: createFilter(filterQuery), flowTarget, - id: queryId, ip, pagination: generateTablePaginationOptions(activePage, limit), sort, @@ -219,19 +217,7 @@ export const useNetworkTopCountries = ({ } return prevRequest; }); - }, [ - activePage, - indexNames, - endDate, - filterQuery, - ip, - limit, - startDate, - sort, - skip, - flowTarget, - queryId, - ]); + }, [activePage, indexNames, endDate, filterQuery, ip, limit, startDate, sort, skip, flowTarget]); useEffect(() => { networkTopCountriesSearch(networkTopCountriesRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx index 87968e7a03522..342fbbf67be4d 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx @@ -85,7 +85,6 @@ export const useNetworkTopNFlow = ({ factoryQueryType: NetworkQueries.topNFlow, filterQuery: createFilter(filterQuery), flowTarget, - id: `${ID}-${flowTarget}`, ip, pagination: generateTablePaginationOptions(activePage, limit), sort, @@ -201,7 +200,6 @@ export const useNetworkTopNFlow = ({ factoryQueryType: NetworkQueries.topNFlow, filterQuery: createFilter(filterQuery), flowTarget, - id: `${ID}-${flowTarget}`, ip, pagination: generateTablePaginationOptions(activePage, limit), timerange: { diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index 09ade9c1bd885..336aaec1d4bc9 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -82,7 +82,6 @@ export const useNetworkTls = ({ factoryQueryType: NetworkQueries.tls, filterQuery: createFilter(filterQuery), flowTarget, - id, ip, pagination: generateTablePaginationOptions(activePage, limit), sort, @@ -195,7 +194,6 @@ export const useNetworkTls = ({ factoryQueryType: NetworkQueries.tls, filterQuery: createFilter(filterQuery), flowTarget, - id, ip, pagination: generateTablePaginationOptions(activePage, limit), timerange: { diff --git a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx index 2e83c9866c59a..8f21a23417aae 100644 --- a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx @@ -80,7 +80,6 @@ export const useNetworkUsers = ({ factoryQueryType: NetworkQueries.users, filterQuery: createFilter(filterQuery), flowTarget, - id, ip, pagination: generateTablePaginationOptions(activePage, limit), sort, @@ -192,7 +191,6 @@ export const useNetworkUsers = ({ setNetworkUsersRequest((prevRequest) => { const myRequest = { ...(prevRequest ?? {}), - id, ip, defaultIndex, factoryQueryType: NetworkQueries.users, @@ -222,7 +220,6 @@ export const useNetworkUsers = ({ skip, ip, flowTarget, - id, ]); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx index e53915bc05fdf..3c59ff7ba36c2 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx @@ -61,7 +61,6 @@ export const useHostOverview = ({ defaultIndex: indexNames, factoryQueryType: HostsQueries.overview, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, @@ -148,7 +147,6 @@ export const useHostOverview = ({ defaultIndex: indexNames, factoryQueryType: HostsQueries.overview, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx index 96711917ca393..7f659db70277f 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx @@ -64,7 +64,6 @@ export const useNetworkOverview = ({ defaultIndex: indexNames, factoryQueryType: NetworkQueries.overview, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, @@ -151,7 +150,6 @@ export const useNetworkOverview = ({ defaultIndex: indexNames, factoryQueryType: NetworkQueries.overview, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index 2b3d615fe9b32..a431b86047d59 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -29,8 +29,6 @@ export interface UseTimelineEventsDetailsProps { skip: boolean; } -const ID = 'timelineEventsDetails'; - export const useTimelineEventsDetails = ({ docValueFields, indexName, @@ -108,7 +106,6 @@ export const useTimelineEventsDetails = ({ ...(prevRequest ?? {}), docValueFields, indexName, - id: ID, eventId, factoryQueryType: TimelineEventsQueries.details, }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 0ae60e6ad0131..65f8a3dc78e4d 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -90,7 +90,6 @@ export const useTimelineEvents = ({ fields: [], fieldRequested: fields, filterQuery: createFilter(filterQuery), - id: ID, timerange: { interval: '12h', from: startDate, @@ -238,7 +237,6 @@ export const useTimelineEvents = ({ fieldRequested: fields, fields: [], filterQuery: createFilter(filterQuery), - id, pagination: { activePage: newActivePage, querySize: limit, @@ -265,14 +263,14 @@ export const useTimelineEvents = ({ return prevRequest; }); }, [ - indexNames, + activePage, docValueFields, endDate, filterQuery, - startDate, id, - activePage, + indexNames, limit, + startDate, sort, skip, fields, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/__mocks__/index.ts index 1c0b44bdfc4f6..ec0b4bd48887a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/__mocks__/index.ts @@ -4045,7 +4045,7 @@ export const formattedSearchStrategyResponse = { { node: { _id: 'ayrMZnQBB-gskcly0w7l', - instances: 0, + instances: 1, process: { args: [ 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe', @@ -4073,7 +4073,7 @@ export const formattedSearchStrategyResponse = { { node: { _id: 'M-GvaHQBA6bGZw2uBoYz', - instances: 0, + instances: 1, process: { args: [ 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe', @@ -4101,7 +4101,7 @@ export const formattedSearchStrategyResponse = { { node: { _id: 'cinEZnQBB-gskclyvNmU', - instances: 0, + instances: 1, process: { args: ['C:\\Windows\\system32\\devicecensus.exe'], name: ['DeviceCensus.exe'], @@ -4125,7 +4125,7 @@ export const formattedSearchStrategyResponse = { { node: { _id: 'HNKSZHQBA6bGZw2uCtRk', - instances: 0, + instances: 1, process: { args: ['C:\\Windows\\system32\\disksnapshot.exe', '-z'], name: ['DiskSnapshot.exe'], @@ -4149,7 +4149,7 @@ export const formattedSearchStrategyResponse = { { node: { _id: '2zncaHQBB-gskcly1QaD', - instances: 0, + instances: 1, process: { args: [ 'C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe', @@ -4176,7 +4176,7 @@ export const formattedSearchStrategyResponse = { { node: { _id: 'gdVuZXQBA6bGZw2uFsPP', - instances: 0, + instances: 1, process: { args: ['C:\\Windows\\System32\\sihclient.exe', '/cv', '33nfV21X50ie84HvATAt1w.0.1'], name: ['SIHClient.exe'], @@ -4200,7 +4200,7 @@ export const formattedSearchStrategyResponse = { { node: { _id: '6NmKZnQBA6bGZw2uma12', - instances: 0, + instances: 1, process: { args: ['C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe'], name: ['SpeechModelDownload.exe'], @@ -4224,7 +4224,7 @@ export const formattedSearchStrategyResponse = { { node: { _id: 'Pi68Z3QBc39KFIJb3txa', - instances: 0, + instances: 1, process: { args: ['C:\\Windows\\system32\\usoclient.exe', 'StartScan'], name: ['UsoClient.exe'], @@ -4248,7 +4248,7 @@ export const formattedSearchStrategyResponse = { { node: { _id: 'Ziw-Z3QBB-gskcly0vqU', - instances: 0, + instances: 1, process: { args: ['/etc/cron.daily/apt-compat'], name: ['apt-compat'], @@ -4272,7 +4272,7 @@ export const formattedSearchStrategyResponse = { { node: { _id: 'aSw-Z3QBB-gskcly0vqU', - instances: 0, + instances: 1, process: { args: ['/etc/cron.daily/bsdmainutils'], name: ['bsdmainutils'], diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts index 7d9351993bc85..8fd5d7654ac99 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { get, getOr } from 'lodash/fp'; +import { get } from 'lodash/fp'; import { set } from '@elastic/safer-lodash-set/fp'; import { mergeFieldsWithHit } from '../../../../../utils/build_query'; @@ -64,8 +64,9 @@ export const formatUncommonProcessesData = ( ): HostsUncommonProcessesEdges => fields.reduce( (flattenedFields, fieldName) => { + const instancesCount = typeof hit.total === 'number' ? hit.total : hit.total.value; flattenedFields.node._id = hit._id; - flattenedFields.node.instances = getOr(0, 'total.value', hit); + flattenedFields.node.instances = instancesCount; flattenedFields.node.hosts = hit.host; if (hit.cursor) { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts index 9cee2c0f1dc43..476c7caf5cec3 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts @@ -48,7 +48,7 @@ export const matrixHistogram: SecuritySolutionFactory = { return { ...response, inspect, - dnsQueries: getOr(null, 'hits.total.value', response.rawResponse), + dnsQueries: response.rawResponse.hits.total, }; }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/network_events/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/network_events/index.ts index 913b1d566a4ea..4d9ab9d656f3e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/network_events/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/network_events/index.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; - import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; import { NetworkKpiQueries, @@ -29,7 +27,7 @@ export const networkKpiNetworkEvents: SecuritySolutionFactory( data: PluginStart ): ISearchStrategy, StrategyResponseType> => { - const es = data.search.getSearchStrategy('es'); + const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); return { search: (request, options, deps) => { diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts index 182197432da81..19535fa3dc8a8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep, getOr, uniq } from 'lodash/fp'; +import { cloneDeep, uniq } from 'lodash/fp'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; @@ -36,7 +36,7 @@ export const timelineEventsAll: SecuritySolutionTimelineFactory // @ts-expect-error diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts index 0b73eed61765f..29ad37e76264f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts @@ -6,6 +6,7 @@ import { mergeMap } from 'rxjs/operators'; import { ISearchStrategy, PluginStart } from '../../../../../../src/plugins/data/server'; +import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../data_enhanced/common'; import { TimelineFactoryQueryTypes, TimelineStrategyResponseType, @@ -17,7 +18,7 @@ import { SecuritySolutionTimelineFactory } from './factory/types'; export const securitySolutionTimelineSearchStrategyProvider = ( data: PluginStart ): ISearchStrategy, TimelineStrategyResponseType> => { - const es = data.search.getSearchStrategy('es'); + const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); return { search: (request, options, deps) => { diff --git a/x-pack/test/api_integration/apis/security_solution/host_details.ts b/x-pack/test/api_integration/apis/security_solution/host_details.ts index 9fe9df1fae506..2dbe736dd6301 100644 --- a/x-pack/test/api_integration/apis/security_solution/host_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/host_details.ts @@ -215,7 +215,7 @@ export default function ({ getService }: FtrProviderContext) { const { body: { hostDetails }, } = await supertest - .post('/internal/search/securitySolutionSearchStrategy') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: HostsQueries.details, diff --git a/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts b/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts index b141087c4e3ba..7ec8945408303 100644 --- a/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts +++ b/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts @@ -85,7 +85,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiHosts data', async () => { const { body: kpiHosts } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/hostsKpiHostsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: HostsKpiQueries.kpiHosts, @@ -106,7 +106,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiAuthentications data', async () => { const { body } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/hostsKpiAuthenticationsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: HostsKpiQueries.kpiAuthentications, @@ -128,7 +128,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiUniqueIps data', async () => { const { body } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/hostsKpiUniqueIpsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: HostsKpiQueries.kpiUniqueIps, @@ -197,7 +197,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiHosts data', async () => { const { body: kpiHosts } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/hostsKpiHostsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: HostsKpiQueries.kpiHosts, @@ -218,7 +218,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiAuthentications data', async () => { const { body } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/hostsKpiAuthenticationsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: HostsKpiQueries.kpiAuthentications, @@ -240,7 +240,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiUniqueIps data', async () => { const { body } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/hostsKpiUniqueIpsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: HostsKpiQueries.kpiUniqueIps, diff --git a/x-pack/test/api_integration/apis/security_solution/kpi_network.ts b/x-pack/test/api_integration/apis/security_solution/kpi_network.ts index 641e6658d28cc..b1802a012179d 100644 --- a/x-pack/test/api_integration/apis/security_solution/kpi_network.ts +++ b/x-pack/test/api_integration/apis/security_solution/kpi_network.ts @@ -66,7 +66,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiNetwork uniqueFlows data', async () => { const { body: kpiNetwork } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/networkKpiUniqueFlowsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: NetworkKpiQueries.uniqueFlows, @@ -86,7 +86,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiNetwork networkEvents data', async () => { const { body: kpiNetwork } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/networkKpiNetworkEventsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: NetworkKpiQueries.networkEvents, @@ -106,7 +106,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiNetwork DNS data', async () => { const { body: kpiNetwork } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/networkKpiDnsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: NetworkKpiQueries.dns, @@ -126,7 +126,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiNetwork networkEvents data', async () => { const { body: kpiNetwork } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/networkKpiNetworkEventsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: NetworkKpiQueries.networkEvents, @@ -146,7 +146,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiNetwork tlsHandshakes data', async () => { const { body: kpiNetwork } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/networkKpiTlsHandshakesQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: NetworkKpiQueries.tlsHandshakes, @@ -166,7 +166,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiNetwork uniquePrivateIps data', async () => { const { body: kpiNetwork } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/networkKpiUniquePrivateIpsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: NetworkKpiQueries.uniquePrivateIps, @@ -213,7 +213,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiNetwork uniqueFlows data', async () => { const { body: kpiNetwork } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/networkKpiUniqueFlowsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: NetworkKpiQueries.uniqueFlows, @@ -233,7 +233,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiNetwork DNS data', async () => { const { body: kpiNetwork } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/networkKpiDnsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: NetworkKpiQueries.dns, @@ -253,7 +253,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiNetwork networkEvents data', async () => { const { body: kpiNetwork } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/networkKpiNetworkEventsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: NetworkKpiQueries.networkEvents, @@ -273,7 +273,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiNetwork tlsHandshakes data', async () => { const { body: kpiNetwork } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/networkKpiTlsHandshakesQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: NetworkKpiQueries.tlsHandshakes, @@ -293,7 +293,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get KpiNetwork uniquePrivateIps data', async () => { const { body: kpiNetwork } = await supertest - .post('/internal/search/securitySolutionSearchStrategy/networkKpiUniquePrivateIpsQuery') + .post('/internal/search/securitySolutionSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: NetworkKpiQueries.uniquePrivateIps, From 6cf53a075ca92b5c4980237b85995d5714b12ced Mon Sep 17 00:00:00 2001 From: Bill McConaghy Date: Thu, 5 Nov 2020 10:58:29 -0500 Subject: [PATCH 24/57] [DOCS] Adding better documentation for required RBAC settings for alerting (#82375) * Adding better documentation for required RBAC settings for alerting * Bolding feature name and spelling out ampersand * clarifying explanation of action privileges needed --- docs/user/alerting/alerting-getting-started.asciidoc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index f8656b87cbe04..2b22b49375676 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -169,12 +169,15 @@ If you are using an *on-premises* Elastic Stack deployment with <> +* <> * <> * <> * <> -See <> for more information on configuring roles that provide access to these features. +See <> for more information on configuring roles that provide access to these features. +Also note that a user will need +read+ privileges for the *Actions and Connectors* feature to attach actions to an alert or to edit an alert that has an action attached to it. [float] [[alerting-spaces]] From 7e5ca2944e5799c6f00ef220c186e0214f0cc312 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 5 Nov 2020 10:00:43 -0600 Subject: [PATCH 25/57] [Enterprise Search] Migrate shared SourceConfigFields component (#82608) --- .../shared/source_config_fields/index.ts | 7 +++ .../source_config_fields.test.tsx | 45 ++++++++++++++ .../source_config_fields.tsx | 61 +++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/index.ts new file mode 100644 index 0000000000000..d857ed5775d81 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/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 { SourceConfigFields } from './source_config_fields'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx new file mode 100644 index 0000000000000..c9565d75e176b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ApiKey } from '../api_key'; +import { CredentialItem } from '../credential_item'; + +import { SourceConfigFields } from './'; + +describe('SourceConfigFields', () => { + it('renders empty with no items', () => { + const wrapper = shallow(); + + expect(wrapper.find(ApiKey)).toHaveLength(0); + expect(wrapper.find(CredentialItem)).toHaveLength(0); + }); + + it('renders with all items, hiding API Keys', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(ApiKey)).toHaveLength(0); + expect(wrapper.find(CredentialItem)).toHaveLength(3); + }); + + it('shows API keys', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(ApiKey)).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx new file mode 100644 index 0000000000000..c0249afaeb3cc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiSpacer } from '@elastic/eui'; + +import { ApiKey } from '../api_key'; +import { CredentialItem } from '../credential_item'; + +interface ISourceConfigFieldsProps { + clientId?: string; + clientSecret?: string; + publicKey?: string; + consumerKey?: string; + baseUrl?: string; +} + +export const SourceConfigFields: React.FC = ({ + clientId, + clientSecret, + publicKey, + consumerKey, + baseUrl, +}) => { + const showApiKey = (publicKey || consumerKey) && !clientId; + + const credentialItem = (label: string, item?: string) => + item && ; + + const keyElement = ( + <> + {publicKey && ( + <> + + + + )} + {consumerKey && ( + <> + + + + )} + + ); + + return ( + <> + {showApiKey && keyElement} + {credentialItem('Client id', clientId)} + + {credentialItem('Client secret', clientSecret)} + + {credentialItem('Base URL', baseUrl)} + + ); +}; From 894e76f79eaa5621f1c41c26d2cb8ce57eff5670 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 5 Nov 2020 10:01:00 -0600 Subject: [PATCH 26/57] [Enterprise Search] Write tests groups routes (#82669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove validation from groups route responses Since this is a migration of known good endpoints, these aren’t necessary. * Add tests for groups routes * Remove unused types * Remove registerWSGroupRoutes from groups test This was removed in another commit and is no longer needed Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../routes/workplace_search/groups.test.ts | 355 ++++++++++++++++++ .../server/routes/workplace_search/groups.ts | 21 -- 2 files changed, 355 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts new file mode 100644 index 0000000000000..31e055565ead1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts @@ -0,0 +1,355 @@ +/* + * 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 { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { + registerGroupsRoute, + registerSearchGroupsRoute, + registerGroupRoute, + registerGroupUsersRoute, + registerShareGroupRoute, + registerAssignGroupRoute, + registerBoostsGroupRoute, +} from './groups'; + +describe('groups routes', () => { + describe('GET /api/workplace_search/groups', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ method: 'get', payload: 'query' }); + + registerGroupsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/groups', + }); + }); + }); + + describe('POST /api/workplace_search/groups', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'post', payload: 'body' }); + + registerGroupsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + const mockRequest = { + body: { + group_name: 'group', + }, + }; + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/groups', + ...mockRequest, + }); + }); + }); + + describe('POST /api/workplace_search/groups/search', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'post', payload: 'body' }); + + registerSearchGroupsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + const mockRequest = { + body: { + page: { + current: 1, + size: 1, + }, + search: { + query: 'foo', + content_source_ids: ['123', '234'], + user_ids: ['345', '456'], + }, + }, + }; + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/groups/search', + ...mockRequest, + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + page: { + current: 1, + size: 1, + }, + search: { + query: 'foo', + content_source_ids: ['123', '234'], + user_ids: ['345', '456'], + }, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('throws on unnecessary properties', () => { + const request = { + body: { + page: null, + search: { + kites: 'bar', + }, + }, + }; + mockRouter.shouldThrow(request); + }); + }); + }); + + describe('GET /api/workplace_search/groups/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ method: 'get', payload: 'params' }); + + registerGroupRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/groups/123', + }); + }); + }); + + describe('PUT /api/workplace_search/groups/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockPayload = { + group: { + name: 'group', + }, + }; + + it('creates a request handler', () => { + mockRouter = new MockRouter({ method: 'put', payload: 'body' }); + + registerGroupRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + body: mockPayload, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/groups/123', + body: mockPayload, + }); + }); + }); + + describe('DELETE /api/workplace_search/groups/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'delete', payload: 'params' }); + + registerGroupRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + const mockRequest = { + params: { + id: '123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/groups/123', + }); + }); + }); + + describe('GET /api/workplace_search/groups/{id}/users', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ method: 'get', payload: 'params' }); + + registerGroupUsersRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/groups/123/group_users', + }); + }); + }); + + describe('POST /api/workplace_search/groups/{id}/share', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'post', payload: 'body' }); + + registerShareGroupRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + const mockRequest = { + params: { id: '123' }, + body: { + content_source_ids: ['123', '234'], + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/groups/123/share', + body: mockRequest.body, + }); + }); + }); + + describe('POST /api/workplace_search/groups/{id}/assign', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'post', payload: 'body' }); + + registerAssignGroupRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + const mockRequest = { + params: { id: '123' }, + body: { + user_ids: ['123', '234'], + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/groups/123/assign', + body: mockRequest.body, + }); + }); + }); + + describe('PUT /api/workplace_search/groups/{id}/boosts', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockPayload = { + group: { + content_source_boosts: [['boost'], ['boost2', 'boost3']], + }, + }; + + it('creates a request handler', () => { + mockRouter = new MockRouter({ method: 'put', payload: 'body' }); + + registerBoostsGroupRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + body: mockPayload, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/groups/123/update_source_boosts', + body: mockPayload, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts index cbb78cef5b66c..35c585eb9f781 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts @@ -8,9 +8,6 @@ import { schema } from '@kbn/config-schema'; import { IRouteDependencies } from '../../plugin'; -import { IMeta } from '../../../common/types'; -import { IUser, IContentSource, IGroup } from '../../../common/types/workplace_search'; - export function registerGroupsRoute({ router, enterpriseSearchRequestHandler, @@ -22,8 +19,6 @@ export function registerGroupsRoute({ }, enterpriseSearchRequestHandler.createRequest({ path: '/ws/org/groups', - hasValidData: (body: { users: IUser[]; contentSources: IContentSource[] }) => - typeof Array.isArray(body?.users) && typeof Array.isArray(body?.contentSources), }) ); @@ -40,7 +35,6 @@ export function registerGroupsRoute({ return enterpriseSearchRequestHandler.createRequest({ path: '/ws/org/groups', body: request.body, - hasValidData: (body: { created_at: string }) => typeof body?.created_at === 'string', })(context, request, response); } ); @@ -71,9 +65,6 @@ export function registerSearchGroupsRoute({ return enterpriseSearchRequestHandler.createRequest({ path: '/ws/org/groups/search', body: request.body, - hasValidData: (body: { results: IGroup[]; meta: IMeta }) => - typeof Array.isArray(body?.results) && - typeof body?.meta?.page?.total_results === 'number', })(context, request, response); } ); @@ -92,8 +83,6 @@ export function registerGroupRoute({ router, enterpriseSearchRequestHandler }: I async (context, request, response) => { return enterpriseSearchRequestHandler.createRequest({ path: `/ws/org/groups/${request.params.id}`, - hasValidData: (body: IGroup) => - typeof body?.createdAt === 'string' && typeof body?.usersCount === 'number', })(context, request, response); } ); @@ -116,8 +105,6 @@ export function registerGroupRoute({ router, enterpriseSearchRequestHandler }: I return enterpriseSearchRequestHandler.createRequest({ path: `/ws/org/groups/${request.params.id}`, body: request.body, - hasValidData: (body: IGroup) => - typeof body?.createdAt === 'string' && typeof body?.usersCount === 'number', })(context, request, response); } ); @@ -134,7 +121,6 @@ export function registerGroupRoute({ router, enterpriseSearchRequestHandler }: I async (context, request, response) => { return enterpriseSearchRequestHandler.createRequest({ path: `/ws/org/groups/${request.params.id}`, - hasValidData: (body: { deleted: boolean }) => body?.deleted === true, })(context, request, response); } ); @@ -156,7 +142,6 @@ export function registerGroupUsersRoute({ async (context, request, response) => { return enterpriseSearchRequestHandler.createRequest({ path: `/ws/org/groups/${request.params.id}/group_users`, - hasValidData: (body: IUser[]) => typeof Array.isArray(body), })(context, request, response); } ); @@ -182,8 +167,6 @@ export function registerShareGroupRoute({ return enterpriseSearchRequestHandler.createRequest({ path: `/ws/org/groups/${request.params.id}/share`, body: request.body, - hasValidData: (body: IGroup) => - typeof body?.createdAt === 'string' && typeof body?.usersCount === 'number', })(context, request, response); } ); @@ -209,8 +192,6 @@ export function registerAssignGroupRoute({ return enterpriseSearchRequestHandler.createRequest({ path: `/ws/org/groups/${request.params.id}/assign`, body: request.body, - hasValidData: (body: IGroup) => - typeof body?.createdAt === 'string' && typeof body?.usersCount === 'number', })(context, request, response); } ); @@ -238,8 +219,6 @@ export function registerBoostsGroupRoute({ return enterpriseSearchRequestHandler.createRequest({ path: `/ws/org/groups/${request.params.id}/update_source_boosts`, body: request.body, - hasValidData: (body: IGroup) => - typeof body?.createdAt === 'string' && typeof body?.usersCount === 'number', })(context, request, response); } ); From a89176e265fbdeedf9d514636addec51baa51533 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 5 Nov 2020 08:01:20 -0800 Subject: [PATCH 27/57] [build] Use 8.2 tag of ubi-minimal (#82688) In kibana#82475 we prevented the update of crypto-policies as it's currently not compatible with libnss. This recent latest tag includes the crypto-policies which we need to avoid for now. Signed-off-by: Tyler Smalley Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/build/tasks/os_packages/docker_generator/run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 19487efe1366c..8679cce9b11fc 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -40,7 +40,7 @@ export async function runDockerGenerator( ubi: boolean = false ) { // UBI var config - const baseOSImage = ubi ? 'docker.elastic.co/ubi8/ubi-minimal:latest' : 'centos:8'; + const baseOSImage = ubi ? 'docker.elastic.co/ubi8/ubi-minimal:8.2' : 'centos:8'; const ubiVersionTag = 'ubi8'; const ubiImageFlavor = ubi ? `-${ubiVersionTag}` : ''; From 7c66880a11d33adcaed65cb38e203d80a15bd7e8 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 5 Nov 2020 11:11:29 -0500 Subject: [PATCH 28/57] [Time to Visualize] Embeddable Error Handling Without ReplacePanel (#82201) Fixed embeddable error handling so that fatal errors are caught and displayed with an errorEmbeddable no matter when they occur. --- ...embeddable-public.embeddable.fatalerror.md | 11 ++++++++ ...in-plugins-embeddable-public.embeddable.md | 2 ++ ...beddable-public.embeddable.onfatalerror.md | 22 +++++++++++++++ ...mbeddable-public.iembeddable.fatalerror.md | 13 +++++++++ ...n-plugins-embeddable-public.iembeddable.md | 1 + .../actions/add_to_library_action.test.tsx | 12 ++++++++- .../actions/add_to_library_action.tsx | 4 ++- .../actions/clone_panel_action.test.tsx | 12 ++++++++- .../actions/clone_panel_action.tsx | 4 ++- .../library_notification_action.test.tsx | 12 ++++++++- .../actions/library_notification_action.tsx | 8 +++++- .../unlink_from_library_action.test.tsx | 16 ++++++++++- .../actions/unlink_from_library_action.tsx | 4 ++- .../application/dashboard_app_controller.tsx | 8 ++++-- .../public/lib/embeddables/embeddable.tsx | 15 ++++++++--- .../lib/embeddables/error_embeddable.tsx | 2 +- .../public/lib/embeddables/i_embeddable.ts | 5 ++++ .../public/lib/panel/embeddable_panel.tsx | 27 ++++++++++++++----- src/plugins/embeddable/public/public.api.md | 5 ++++ .../embeddable/embeddable.tsx | 12 +++++++-- .../lens/public/lens_attribute_service.ts | 18 ++++++------- 21 files changed, 181 insertions(+), 32 deletions(-) create mode 100644 docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.fatalerror.md create mode 100644 docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.onfatalerror.md create mode 100644 docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.fatalerror.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.fatalerror.md new file mode 100644 index 0000000000000..e937fa8fd80e7 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.fatalerror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [Embeddable](./kibana-plugin-plugins-embeddable-public.embeddable.md) > [fatalError](./kibana-plugin-plugins-embeddable-public.embeddable.fatalerror.md) + +## Embeddable.fatalError property + +Signature: + +```typescript +fatalError?: Error; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md index 295cc10b1bb19..b1f1bed7541c3 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md @@ -20,6 +20,7 @@ export declare abstract class EmbeddableError | | | [id](./kibana-plugin-plugins-embeddable-public.embeddable.id.md) | | string | | | [input](./kibana-plugin-plugins-embeddable-public.embeddable.input.md) | | TEmbeddableInput | | | [isContainer](./kibana-plugin-plugins-embeddable-public.embeddable.iscontainer.md) | | boolean | | @@ -43,6 +44,7 @@ export declare abstract class Embeddable + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [Embeddable](./kibana-plugin-plugins-embeddable-public.embeddable.md) > [onFatalError](./kibana-plugin-plugins-embeddable-public.embeddable.onfatalerror.md) + +## Embeddable.onFatalError() method + +Signature: + +```typescript +protected onFatalError(e: Error): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| e | Error | | + +Returns: + +`void` + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md new file mode 100644 index 0000000000000..4b764a6ede079 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [IEmbeddable](./kibana-plugin-plugins-embeddable-public.iembeddable.md) > [fatalError](./kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md) + +## IEmbeddable.fatalError property + +If this embeddable has encountered a fatal error, that error will be stored here + +Signature: + +```typescript +fatalError?: Error; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.md index b3b6f961e56d1..f96477ed65a04 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.md @@ -15,6 +15,7 @@ export interface IEmbeddableobject | Extra abilities added to Embeddable by *_enhanced plugins. | +| [fatalError](./kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md) | Error | If this embeddable has encountered a fatal error, that error will be stored here | | [id](./kibana-plugin-plugins-embeddable-public.iembeddable.id.md) | string | A unique identifier for this embeddable. Mainly only used by containers to map their Panel States to a child embeddable instance. | | [isContainer](./kibana-plugin-plugins-embeddable-public.iembeddable.iscontainer.md) | boolean | Is this embeddable an instance of a Container class, can it contain nested embeddables? | | [parent](./kibana-plugin-plugins-embeddable-public.iembeddable.parent.md) | IContainer | If this embeddable is nested inside a container, this will contain a reference to its parent. | diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx index 3f7d05e8692c2..650a273314412 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx @@ -35,7 +35,7 @@ import { coreMock } from '../../../../../core/public/mocks'; import { CoreStart } from 'kibana/public'; import { AddToLibraryAction } from '.'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; -import { ViewMode } from '../../../../embeddable/public'; +import { ErrorEmbeddable, ViewMode } from '../../../../embeddable/public'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -86,6 +86,16 @@ beforeEach(async () => { } }); +test('Add to library is incompatible with Error Embeddables', async () => { + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); +}); + test('Add to library is compatible when embeddable on dashboard has value type input', async () => { const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsValueType()); diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index d89c38f297e8f..179e5d522a2b3 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -26,6 +26,7 @@ import { PanelNotFoundError, EmbeddableInput, isReferenceOrValueEmbeddable, + isErrorEmbeddable, } from '../../../../embeddable/public'; import { NotificationsStart } from '../../../../../core/public'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; @@ -61,7 +62,8 @@ export class AddToLibraryAction implements ActionByType { } }); +test('Clone is incompatible with Error Embeddables', async () => { + const action = new ClonePanelAction(coreStart); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); +}); + test('Clone adds a new embeddable', async () => { const dashboard = embeddable.getRoot() as IContainer; const originalPanelCount = Object.keys(dashboard.getInput().panels).length; diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index dc5887ee0e644..2d98d419689c1 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -28,6 +28,7 @@ import { PanelNotFoundError, EmbeddableInput, SavedObjectEmbeddableInput, + isErrorEmbeddable, } from '../../../../embeddable/public'; import { placePanelBeside, @@ -66,7 +67,8 @@ export class ClonePanelAction implements ActionByType public async isCompatible({ embeddable }: ClonePanelActionContext) { return Boolean( - embeddable.getInput()?.viewMode !== ViewMode.VIEW && + !isErrorEmbeddable(embeddable) && + embeddable.getInput()?.viewMode !== ViewMode.VIEW && embeddable.getRoot() && embeddable.getRoot().isContainer && embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx index 996649677e6c9..f45d64cdc0ab8 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx @@ -30,7 +30,7 @@ import { coreMock } from '../../../../../core/public/mocks'; import { CoreStart } from 'kibana/public'; import { LibraryNotificationAction, UnlinkFromLibraryAction } from '.'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; -import { ViewMode } from '../../../../embeddable/public'; +import { ErrorEmbeddable, IContainer, ViewMode } from '../../../../embeddable/public'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -87,6 +87,16 @@ beforeEach(async () => { embeddable.updateInput({ viewMode: ViewMode.EDIT }); }); +test('Notification is incompatible with Error Embeddables', async () => { + const action = new LibraryNotificationAction(unlinkAction); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); +}); + test('Notification is shown when embeddable on dashboard has reference type input', async () => { const action = new LibraryNotificationAction(unlinkAction); embeddable.updateInput(await embeddable.getInputAsRefType()); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx index 6a0b71d8250be..d6e75a3bb132b 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx @@ -19,7 +19,12 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { IEmbeddable, ViewMode, isReferenceOrValueEmbeddable } from '../../embeddable_plugin'; +import { + IEmbeddable, + ViewMode, + isReferenceOrValueEmbeddable, + isErrorEmbeddable, +} from '../../embeddable_plugin'; import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; import { reactToUiComponent } from '../../../../kibana_react/public'; import { UnlinkFromLibraryAction } from '.'; @@ -79,6 +84,7 @@ export class LibraryNotificationAction implements ActionByType { return ( + !isErrorEmbeddable(embeddable) && embeddable.getRoot().isContainer && embeddable.getInput()?.viewMode !== ViewMode.VIEW && isReferenceOrValueEmbeddable(embeddable) && diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx index 0f61a74cd7036..4f668ec9ea04c 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx @@ -30,7 +30,11 @@ import { coreMock } from '../../../../../core/public/mocks'; import { CoreStart } from 'kibana/public'; import { UnlinkFromLibraryAction } from '.'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; -import { ViewMode, SavedObjectEmbeddableInput } from '../../../../embeddable/public'; +import { + ViewMode, + SavedObjectEmbeddableInput, + ErrorEmbeddable, +} from '../../../../embeddable/public'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -80,6 +84,16 @@ beforeEach(async () => { embeddable.updateInput({ viewMode: ViewMode.EDIT }); }); +test('Unlink is incompatible with Error Embeddables', async () => { + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); +}); + test('Unlink is compatible when embeddable on dashboard has reference type input', async () => { const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsRefType()); diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx index f5cf8b4e866a8..5e16145364712 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx @@ -26,6 +26,7 @@ import { PanelNotFoundError, EmbeddableInput, isReferenceOrValueEmbeddable, + isErrorEmbeddable, } from '../../../../embeddable/public'; import { NotificationsStart } from '../../../../../core/public'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; @@ -61,7 +62,8 @@ export class UnlinkFromLibraryAction implements ActionByType merge( ...newChildIds.map((childId) => - dashboardContainer!.getChild(childId).getOutput$() + dashboardContainer! + .getChild(childId) + .getOutput$() + .pipe(catchError(() => EMPTY)) ) ) ) diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index c7afc157c1452..31df5c5085f8b 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -43,6 +43,7 @@ export abstract class Embeddable< public readonly isContainer: boolean = false; public abstract readonly type: string; public readonly id: string; + public fatalError?: Error; protected output: TEmbeddableOutput; protected input: TEmbeddableInput; @@ -88,9 +89,12 @@ export abstract class Embeddable< map(({ title }) => title || ''), distinctUntilChanged() ) - .subscribe((title) => { - this.renderComplete.setTitle(title); - }); + .subscribe( + (title) => { + this.renderComplete.setTitle(title); + }, + () => {} + ); } public getIsContainer(): this is IContainer { @@ -193,6 +197,11 @@ export abstract class Embeddable< } } + protected onFatalError(e: Error) { + this.fatalError = e; + this.output$.error(e); + } + private onResetInput(newInput: TEmbeddableInput) { if (!isEqual(this.input, newInput)) { const oldLastReloadRequestTime = this.input.lastReloadRequestTime; diff --git a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx index cdbe7af98a4f4..34d971cbb717a 100644 --- a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx @@ -30,7 +30,7 @@ export const ERROR_EMBEDDABLE_TYPE = 'error'; export function isErrorEmbeddable( embeddable: TEmbeddable | ErrorEmbeddable ): embeddable is ErrorEmbeddable { - return (embeddable as ErrorEmbeddable).error !== undefined; + return Boolean(embeddable.fatalError || (embeddable as ErrorEmbeddable).error !== undefined); } export class ErrorEmbeddable extends Embeddable { diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 3843950c164c9..5a73df2e13861 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -85,6 +85,11 @@ export interface IEmbeddable< */ enhancements?: object; + /** + * If this embeddable has encountered a fatal error, that error will be stored here + **/ + fatalError?: Error; + /** * A functional representation of the isContainer variable, but helpful for typescript to * know the shape if this returns true diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 1cd48e85469fd..4a1fd22894e7e 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -50,7 +50,7 @@ import { EditPanelAction } from '../actions'; import { CustomizePanelModal } from './panel_header/panel_actions/customize_title/customize_panel_modal'; import { EmbeddableStart } from '../../plugin'; import { EmbeddableErrorLabel } from './embeddable_error_label'; -import { EmbeddableStateTransfer } from '..'; +import { EmbeddableStateTransfer, ErrorEmbeddable } from '..'; const sortByOrderField = ( { order: orderA }: { order?: number }, @@ -85,6 +85,7 @@ interface State { notifications: Array>; loading?: boolean; error?: EmbeddableError; + errorEmbeddable?: ErrorEmbeddable; } interface PanelUniversalActions { @@ -199,6 +200,9 @@ export class EmbeddablePanel extends React.Component { if (this.parentSubscription) { this.parentSubscription.unsubscribe(); } + if (this.state.errorEmbeddable) { + this.state.errorEmbeddable.destroy(); + } this.props.embeddable.destroy(); } @@ -257,12 +261,21 @@ export class EmbeddablePanel extends React.Component { public componentDidMount() { if (this.embeddableRoot.current) { this.subscription.add( - this.props.embeddable.getOutput$().subscribe((output: EmbeddableOutput) => { - this.setState({ - error: output.error, - loading: output.loading, - }); - }) + this.props.embeddable.getOutput$().subscribe( + (output: EmbeddableOutput) => { + this.setState({ + error: output.error, + loading: output.loading, + }); + }, + (error) => { + if (this.embeddableRoot.current) { + const errorEmbeddable = new ErrorEmbeddable(error, { id: this.props.embeddable.id }); + errorEmbeddable.render(this.embeddableRoot.current); + this.setState({ errorEmbeddable }); + } + } + ) ); this.props.embeddable.render(this.embeddableRoot.current); } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 8f0f56c4e1a16..4406dded98547 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -278,6 +278,8 @@ export abstract class Embeddable>; // (undocumented) getInput(): Readonly; @@ -298,6 +300,8 @@ export abstract class Embeddable { destroy(): void; enhancements?: object; + fatalError?: Error; getInput$(): Readonly>; getInput(): Readonly; getInspectorAdapters(): Adapters | undefined; diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 02ac58328b4e0..fdb267835f44c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -44,7 +44,7 @@ import { getEditPath, DOC_TYPE } from '../../../common'; import { IBasePath } from '../../../../../../src/core/public'; import { LensAttributeService } from '../../lens_attribute_service'; -export type LensSavedObjectAttributes = Omit; +export type LensSavedObjectAttributes = Omit; export type LensByValueInput = { attributes: LensSavedObjectAttributes; @@ -130,7 +130,15 @@ export class Embeddable } async initializeSavedVis(input: LensEmbeddableInput) { - const attributes = await this.deps.attributeService.unwrapAttributes(input); + const attributes: + | LensSavedObjectAttributes + | false = await this.deps.attributeService.unwrapAttributes(input).catch((e: Error) => { + this.onFatalError(e); + return false; + }); + if (!attributes) { + return; + } this.savedVis = { ...attributes, type: this.type, diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts index fac8f445abb9e..e8bb031a2b027 100644 --- a/x-pack/plugins/lens/public/lens_attribute_service.ts +++ b/x-pack/plugins/lens/public/lens_attribute_service.ts @@ -12,7 +12,7 @@ import { LensByValueInput, LensByReferenceInput, } from './editor_frame_service/embeddable/embeddable'; -import { SavedObjectIndexStore } from './persistence'; +import { SavedObjectIndexStore, Document } from './persistence'; import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; import { DOC_TYPE } from '../common'; @@ -22,6 +22,12 @@ export type LensAttributeService = AttributeService< LensByReferenceInput >; +function documentToAttributes(doc: Document): LensSavedObjectAttributes { + delete doc.savedObjectId; + delete doc.type; + return { ...doc }; +} + export function getLensAttributeService( core: CoreStart, startDependencies: LensPluginStartDependencies @@ -41,14 +47,8 @@ export function getLensAttributeService( return { id: savedDoc.savedObjectId }; }, unwrapMethod: async (savedObjectId: string): Promise => { - const savedObject = await core.savedObjects.client.get( - DOC_TYPE, - savedObjectId - ); - return { - ...savedObject.attributes, - references: savedObject.references, - }; + const attributes = documentToAttributes(await savedObjectStore.load(savedObjectId)); + return attributes; }, checkForDuplicateTitle: (props: OnSaveProps) => { const savedObjectsClient = core.savedObjects.client; From 340f85cdf74333d569e9991182e5f3e4c2f94c75 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 5 Nov 2020 11:58:29 -0500 Subject: [PATCH 29/57] [Watcher] Retain search and pagination values when watch list refreshes (#82651) --- .../helpers/watch_list.helpers.ts | 30 ++++++- .../client_integration/watch_list.test.ts | 78 +++++++++++++------ .../watch_list/components/watch_list.tsx | 73 +++++++++++------ 3 files changed, 131 insertions(+), 50 deletions(-) diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts index e511dcdc58606..ca5d962e4dd21 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts @@ -14,7 +14,7 @@ import { nextTick, } from '../../../../../test_utils'; import { WatchList } from '../../../public/application/sections/watch_list/components/watch_list'; -import { ROUTES } from '../../../common/constants'; +import { ROUTES, REFRESH_INTERVALS } from '../../../common/constants'; import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { @@ -31,6 +31,8 @@ export interface WatchListTestBed extends TestBed { selectWatchAt: (index: number) => void; clickWatchAt: (index: number) => void; clickWatchActionAt: (index: number, action: 'delete' | 'edit') => void; + searchWatches: (term: string) => void; + advanceTimeToTableRefresh: () => Promise; }; } @@ -73,12 +75,35 @@ export const setup = async (): Promise => { }); }; + const searchWatches = (term: string) => { + const { find, component } = testBed; + const searchInput = find('watchesTableContainer').find('.euiFieldSearch'); + + // Enter input into the search box + // @ts-ignore + searchInput.instance().value = term; + searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 }); + + component.update(); + }; + + const advanceTimeToTableRefresh = async () => { + const { component } = testBed; + await act(async () => { + // Advance timers to simulate another request + jest.advanceTimersByTime(REFRESH_INTERVALS.WATCH_LIST); + }); + component.update(); + }; + return { ...testBed, actions: { selectWatchAt, clickWatchAt, clickWatchActionAt, + searchWatches, + advanceTimeToTableRefresh, }, }; }; @@ -95,4 +120,5 @@ export type TestSubjects = | 'createWatchButton' | 'emptyPrompt' | 'emptyPrompt.createWatchButton' - | 'editWatchButton'; + | 'editWatchButton' + | 'watchesTableContainer'; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts index e436971edbb69..844493ea35261 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts @@ -6,13 +6,7 @@ import { act } from 'react-dom/test-utils'; import * as fixtures from '../../test/fixtures'; -import { - setupEnvironment, - pageHelpers, - nextTick, - getRandomString, - findTestSubject, -} from './helpers'; +import { setupEnvironment, pageHelpers, getRandomString, findTestSubject } from './helpers'; import { WatchListTestBed } from './helpers/watch_list.helpers'; import { ROUTES } from '../../common/constants'; @@ -24,28 +18,29 @@ describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchListTestBed; + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); server.restore(); }); describe('on component mount', () => { - beforeEach(async () => { - testBed = await setup(); - }); - describe('watches', () => { describe('when there are no watches', () => { - beforeEach(() => { + beforeEach(async () => { httpRequestsMockHelpers.setLoadWatchesResponse({ watches: [] }); - }); - - test('should display an empty prompt', async () => { - const { component, exists } = await setup(); await act(async () => { - await nextTick(); - component.update(); + testBed = await setup(); }); + testBed.component.update(); + }); + + test('should display an empty prompt', async () => { + const { exists } = testBed; expect(exists('emptyPrompt')).toBe(true); expect(exists('emptyPrompt.createWatchButton')).toBe(true); @@ -76,12 +71,47 @@ describe('', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadWatchesResponse({ watches }); - testBed = await setup(); - await act(async () => { - await nextTick(); - testBed.component.update(); + testBed = await setup(); }); + + testBed.component.update(); + }); + + test('should retain the search query', async () => { + const { table, actions } = testBed; + + actions.searchWatches(watch1.name); + + const { tableCellsValues } = table.getMetaData('watchesTable'); + + // Expect "watch1" is only visible in the table + expect(tableCellsValues.length).toEqual(1); + const row = tableCellsValues[0]; + const { name, id, watchStatus } = watch1; + + const expectedRow = [ + '', // checkbox + id, + name, + watchStatus.state, + '', // comment + '', // lastMetCondition + '', // lastChecked + '', // actions + ]; + + expect(row).toEqual(expectedRow); + + await actions.advanceTimeToTableRefresh(); + + const { tableCellsValues: updatedTableCellsValues } = table.getMetaData('watchesTable'); + + // Verify "watch1" is still the only watch visible in the table + expect(updatedTableCellsValues.length).toEqual(1); + const updatedRow = updatedTableCellsValues[0]; + + expect(updatedRow).toEqual(expectedRow); }); test('should set the correct app title', () => { @@ -185,7 +215,7 @@ describe('', () => { }); test('should send the correct HTTP request to delete watch', async () => { - const { component, actions, table } = testBed; + const { actions, table } = testBed; const { rows } = table.getMetaData('watchesTable'); const watchId = rows[0].columns[2].value; @@ -208,8 +238,6 @@ describe('', () => { await act(async () => { confirmButton!.click(); - await nextTick(); - component.update(); }); const latestRequest = server.requests[server.requests.length - 1]; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx b/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx index 729e37475fc99..a42704f37dc56 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx @@ -7,6 +7,7 @@ import React, { useState, useMemo, useEffect, Fragment } from 'react'; import { + CriteriaWithPagination, EuiButton, EuiButtonEmpty, EuiFlexGroup, @@ -57,6 +58,11 @@ export const WatchList = () => { // Filter out deleted watches on the client, because the API will return 200 even though some watches // may not really be deleted until after they're done firing and this could take some time. const [deletedWatches, setDeletedWatches] = useState([]); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGINATION.initialPageSize, + }); + const [query, setQuery] = useState(''); useEffect(() => { setBreadcrumbs([listBreadcrumb]); @@ -379,7 +385,14 @@ export const WatchList = () => { : '', }; + const handleOnChange = (search: { queryText: string }) => { + setQuery(search.queryText); + return true; + }; + const searchConfig = { + query, + onChange: handleOnChange, box: { incremental: true, }, @@ -409,29 +422,43 @@ export const WatchList = () => { }; content = ( - - } - rowProps={() => ({ - 'data-test-subj': 'row', - })} - cellProps={() => ({ - 'data-test-subj': 'cell', - })} - data-test-subj="watchesTable" - /> +
+ ) => + setPagination({ pageIndex: index, pageSize: size }) + } + items={availableWatches} + itemId="id" + columns={columns} + search={searchConfig} + pagination={{ + ...PAGINATION, + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + }} + sorting={{ + sort: { + field: 'name', + direction: 'asc', + }, + }} + selection={selectionConfig} + isSelectable={true} + message={ + + } + rowProps={() => ({ + 'data-test-subj': 'row', + })} + cellProps={() => ({ + 'data-test-subj': 'cell', + })} + data-test-subj="watchesTable" + /> +
); } From 1f37816d35d73ddaed1eee9cd75c708ced6e6ddf Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Thu, 5 Nov 2020 17:39:54 +0000 Subject: [PATCH 30/57] Filter out read access to config and telemetry obj (#82314) * Filter out read access to config and telemetry obj * Fix eslint errors --- .../server/audit/audit_events.test.ts | 57 +++++++++++++++++++ .../security/server/audit/audit_events.ts | 10 +++- .../server/audit/audit_service.test.ts | 20 +++++++ .../security/server/audit/audit_service.ts | 7 ++- 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security/server/audit/audit_events.test.ts b/x-pack/plugins/security/server/audit/audit_events.test.ts index 1713badede2f7..c826bb1d33f99 100644 --- a/x-pack/plugins/security/server/audit/audit_events.test.ts +++ b/x-pack/plugins/security/server/audit/audit_events.test.ts @@ -106,6 +106,63 @@ describe('#savedObjectEvent', () => { `); }); + test('does create event for read access of saved objects', () => { + expect( + savedObjectEvent({ + action: SavedObjectAction.GET, + savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' }, + }) + ).not.toBeUndefined(); + expect( + savedObjectEvent({ + action: SavedObjectAction.FIND, + savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' }, + }) + ).not.toBeUndefined(); + }); + + test('does not create event for read access of config or telemetry objects', () => { + expect( + savedObjectEvent({ + action: SavedObjectAction.GET, + savedObject: { type: 'config', id: 'SAVED_OBJECT_ID' }, + }) + ).toBeUndefined(); + expect( + savedObjectEvent({ + action: SavedObjectAction.GET, + savedObject: { type: 'telemetry', id: 'SAVED_OBJECT_ID' }, + }) + ).toBeUndefined(); + expect( + savedObjectEvent({ + action: SavedObjectAction.FIND, + savedObject: { type: 'config', id: 'SAVED_OBJECT_ID' }, + }) + ).toBeUndefined(); + expect( + savedObjectEvent({ + action: SavedObjectAction.FIND, + savedObject: { type: 'telemetry', id: 'SAVED_OBJECT_ID' }, + }) + ).toBeUndefined(); + }); + + test('does create event for write access of config or telemetry objects', () => { + expect( + savedObjectEvent({ + action: SavedObjectAction.UPDATE, + savedObject: { type: 'config', id: 'SAVED_OBJECT_ID' }, + }) + ).not.toBeUndefined(); + expect( + savedObjectEvent({ + action: SavedObjectAction.UPDATE, + savedObject: { type: 'telemetry', id: 'SAVED_OBJECT_ID' }, + }) + ).not.toBeUndefined(); + }); + test('creates event with `success` outcome for `REMOVE_REFERENCES` action', () => { expect( savedObjectEvent({ diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts index e3c1f95349c92..6aba78c936071 100644 --- a/x-pack/plugins/security/server/audit/audit_events.ts +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -220,7 +220,7 @@ export function savedObjectEvent({ deleteFromSpaces, outcome, error, -}: SavedObjectParams): AuditEvent { +}: SavedObjectParams): AuditEvent | undefined { const doc = savedObject ? `${savedObject.type} [id=${savedObject.id}]` : 'saved objects'; const [present, progressive, past] = eventVerbs[action]; const message = error @@ -230,6 +230,14 @@ export function savedObjectEvent({ : `User has ${past} ${doc}`; const type = eventTypes[action]; + if ( + type === EventType.ACCESS && + savedObject && + (savedObject.type === 'config' || savedObject.type === 'telemetry') + ) { + return; + } + return { message, event: { diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts index e0dd98c7de639..9b30d4dbba456 100644 --- a/x-pack/plugins/security/server/audit/audit_service.test.ts +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -130,6 +130,26 @@ describe('#asScoped', () => { audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); expect(logger.info).not.toHaveBeenCalled(); }); + + it('does not log to audit logger if no event was generated', async () => { + const audit = new AuditService(logger).setup({ + license, + config: { + enabled: true, + ignore_filters: [{ actions: ['ACTION'] }], + }, + logging, + http, + getCurrentUser, + getSpaceId, + }); + const request = httpServerMock.createKibanaRequest({ + kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, + }); + + audit.asScoped(request).log(undefined); + expect(logger.info).not.toHaveBeenCalled(); + }); }); describe('#createLoggingConfig', () => { diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts index 31c7e28be3b8c..744e4af56c861 100644 --- a/x-pack/plugins/security/server/audit/audit_service.ts +++ b/x-pack/plugins/security/server/audit/audit_service.ts @@ -27,7 +27,7 @@ export interface LegacyAuditLogger { } export interface AuditLogger { - log: (event: AuditEvent) => void; + log: (event: AuditEvent | undefined) => void; } interface AuditLogMeta extends AuditEvent { @@ -127,7 +127,10 @@ export class AuditService { * }); * ``` */ - const log = (event: AuditEvent) => { + const log: AuditLogger['log'] = (event) => { + if (!event) { + return; + } const user = getCurrentUser(request); const spaceId = getSpaceId(request); const meta: AuditLogMeta = { From ccc3e236206c9f9bd228239c18fada83f045886d Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 5 Nov 2020 11:48:43 -0600 Subject: [PATCH 31/57] [Workplace Search] Refactor nav constants (#82646) * Move NAV constants to top level This was only needed locally in Groups but we can to store all nav constants in the global constants file * Extract remaining nav constants * Fix links to NAV in routers * Use constant for path Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/layout/nav.tsx | 27 ++++--------- .../workplace_search/constants.ts | 38 +++++++++++++++++++ .../applications/workplace_search/index.tsx | 4 +- .../views/groups/components/group_sub_nav.tsx | 4 +- .../views/groups/constants.ts | 20 ---------- .../views/groups/group_router.tsx | 2 +- .../views/groups/groups_router.tsx | 2 +- 7 files changed, 51 insertions(+), 46 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/constants.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index a454e3146f4d9..6fa6698e6b6ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; @@ -13,7 +12,7 @@ import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { SideNav, SideNavLink } from '../../../shared/layout'; import { GroupSubNav } from '../../views/groups/components/group_sub_nav'; -import { NAV } from '../../views/groups/constants'; +import { NAV } from '../../constants'; import { ORG_SOURCES_PATH, @@ -29,38 +28,26 @@ export const WorkplaceSearchNav: React.FC = () => { return ( - {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.overview', { - defaultMessage: 'Overview', - })} + {NAV.OVERVIEW} - {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.sources', { - defaultMessage: 'Sources', - })} + {NAV.SOURCES} }> {NAV.GROUPS} - {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.roleMappings', { - defaultMessage: 'Role Mappings', - })} + {NAV.ROLE_MAPPINGS} - {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.security', { - defaultMessage: 'Security', - })} + {NAV.SECURITY} - {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.settings', { - defaultMessage: 'Settings', - })} + {NAV.SETTINGS} - {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.personalDashboard', { - defaultMessage: 'View my personal dashboard', - })} + {NAV.PERSONAL_DASHBOARD} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 9f313a6995ad5..3b911b87dea12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -3,6 +3,44 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; + +export const NAV = { + OVERVIEW: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.overview', { + defaultMessage: 'Overview', + }), + SOURCES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.sources', { + defaultMessage: 'Sources', + }), + GROUPS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.groups', { + defaultMessage: 'Groups', + }), + GROUP_OVERVIEW: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.nav.groups.groupOverview', + { + defaultMessage: 'Overview', + } + ), + SOURCE_PRIORITIZATION: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.nav.groups.sourcePrioritization', + { defaultMessage: 'Source Prioritization' } + ), + ROLE_MAPPINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.roleMappings', { + defaultMessage: 'Role Mappings', + }), + SECURITY: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.security', { + defaultMessage: 'Security', + }), + SETTINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.settings', { + defaultMessage: 'Settings', + }), + PERSONAL_DASHBOARD: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.nav.personalDashboard', + { + defaultMessage: 'View my personal dashboard', + } + ), +}; export const MAX_TABLE_ROW_ICONS = 3; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index e22b9c6282f95..9875af7889447 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -16,7 +16,7 @@ import { AppLogic } from './app_logic'; import { Layout } from '../shared/layout'; import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout'; -import { SETUP_GUIDE_PATH } from './routes'; +import { GROUPS_PATH, SETUP_GUIDE_PATH } from './routes'; import { SetupGuide } from './views/setup_guide'; import { ErrorState } from './views/error_state'; @@ -56,7 +56,7 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { ) : ( - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx index a41cf6191eb64..ad19ad059fb40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { useValues } from 'kea'; import { GroupLogic } from '../group_logic'; -import { NAV } from '../constants'; +import { NAV } from '../../../constants'; import { SideNavLink } from '../../../../shared/layout'; @@ -23,7 +23,7 @@ export const GroupSubNav: React.FC = () => { return ( <> - {NAV.OVERVIEW} + {NAV.GROUP_OVERVIEW} {NAV.SOURCE_PRIORITIZATION} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/constants.ts deleted file mode 100644 index 7c3d160017138..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const NAV = { - GROUPS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.groups', { - defaultMessage: 'Groups', - }), - OVERVIEW: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.groups.groupOverview', { - defaultMessage: 'Overview', - }), - SOURCE_PRIORITIZATION: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.nav.groups.sourcePrioritization', - { defaultMessage: 'Source Prioritization' } - ), -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx index 1b6f0c4c49a05..822d966bfb8d2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx @@ -14,7 +14,7 @@ import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kiban import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { GROUP_SOURCE_PRIORITIZATION_PATH, GROUP_PATH } from '../../routes'; -import { NAV } from './constants'; +import { NAV } from '../../constants'; import { GroupLogic } from './group_logic'; import { ManageUsersModal } from './components/manage_users_modal'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx index a4fe472065d90..326362a0ddfbc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx @@ -14,7 +14,7 @@ import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kiban import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { GROUP_PATH, GROUPS_PATH } from '../../routes'; -import { NAV } from './constants'; +import { NAV } from '../../constants'; import { GroupsLogic } from './groups_logic'; From 074ef6f4d8e296e1a1759d0a652acd94e212ea04 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 5 Nov 2020 09:51:41 -0800 Subject: [PATCH 32/57] [App Search] Implement initial Engine routing/navigation (#82549) * [Setup] Update routes + role privileges Routes: use generatePath to better match ent-search Roles: We're using "Search UI" in the nav copy now, so we should update our vars accordingly * Add new EngineNav and EngineRouter components * Update existing components to use new EngineRouter/EngineNav * Add App Search engine label & new SideNavItem component * Add EngineRouter breadcrumbs * [Refactor] DRY out i18n constants --- .../app_search/components/engine/constants.ts | 58 +++++ .../components/engine/engine_nav.scss | 17 ++ .../components/engine/engine_nav.test.tsx | 138 +++++++++++ .../components/engine/engine_nav.tsx | 225 ++++++++++++++++++ .../app_search/components/engine/index.ts | 7 + .../engine_overview/engine_table.test.tsx | 5 +- .../engine_overview/engine_table.tsx | 7 +- .../applications/app_search/index.test.tsx | 15 +- .../public/applications/app_search/index.tsx | 15 +- .../public/applications/app_search/routes.ts | 28 ++- .../app_search/utils/role/index.test.ts | 2 +- .../app_search/utils/role/index.ts | 4 +- .../applications/shared/layout/index.ts | 2 +- .../shared/layout/side_nav.test.tsx | 32 ++- .../applications/shared/layout/side_nav.tsx | 17 ++ 15 files changed, 555 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts new file mode 100644 index 0000000000000..ee5b47eda490e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts @@ -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 { i18n } from '@kbn/i18n'; + +// TODO: It's very likely that we'll move these i18n constants to their respective component +// folders once those are migrated over. This is a temporary way of DRYing them out for now. + +export const ENGINES_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.engines.title', { + defaultMessage: 'Engines', +}); + +export const OVERVIEW_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.overview.title', + { defaultMessage: 'Overview' } +); +export const ANALYTICS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.title', + { defaultMessage: 'Analytics' } +); +export const DOCUMENTS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.documents.title', + { defaultMessage: 'Documents' } +); +export const SCHEMA_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.engine.schema.title', { + defaultMessage: 'Schema', +}); +export const CRAWLER_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.crawler.title', + { defaultMessage: 'Crawler' } +); +export const RELEVANCE_TUNING_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.title', + { defaultMessage: 'Relevance Tuning' } +); +export const SYNONYMS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.synonyms.title', + { defaultMessage: 'Synonyms' } +); +export const CURATIONS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.title', + { defaultMessage: 'Curations' } +); +export const RESULT_SETTINGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.title', + { defaultMessage: 'Result Settings' } +); +export const SEARCH_UI_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.searchUI.title', + { defaultMessage: 'Search UI' } +); +export const API_LOGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.apiLogs.title', + { defaultMessage: 'API Logs' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss new file mode 100644 index 0000000000000..d7740724204a7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss @@ -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. + */ + +.appSearchNavEngineLabel { + padding-top: $euiSizeS; + padding-bottom: $euiSizeS; + + .euiText { + font-weight: $euiFontWeightMedium; + } + .euiBadge { + margin-top: $euiSizeXS; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx new file mode 100644 index 0000000000000..7bdc3c86a50d6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/react_router_history.mock'; +import { setMockValues } from '../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { Switch, useParams } from 'react-router-dom'; +import { EuiBadge } from '@elastic/eui'; + +import { EngineRouter, EngineNav } from './'; + +describe('EngineRouter', () => { + it('renders a default engine overview', () => { + setMockValues({ myRole: {} }); + const wrapper = shallow(); + + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="EngineOverviewTODO"]')).toHaveLength(1); + }); + + it('renders an analytics view', () => { + setMockValues({ myRole: { canViewEngineAnalytics: true } }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="AnalyticsTODO"]')).toHaveLength(1); + }); +}); + +describe('EngineNav', () => { + beforeEach(() => { + (useParams as jest.Mock).mockReturnValue({ engineName: 'some-engine' }); + }); + + it('does not render without an engine name', () => { + setMockValues({ myRole: {} }); + (useParams as jest.Mock).mockReturnValue({ engineName: '' }); + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders an engine label', () => { + setMockValues({ myRole: {} }); + const wrapper = mount(); + + const label = wrapper.find('[data-test-subj="EngineLabel"]').last(); + expect(label.text()).toEqual(expect.stringContaining('SOME-ENGINE')); + + // TODO: Test sample & meta engine conditional rendering + expect(label.find(EuiBadge).text()).toEqual('SAMPLE ENGINE'); + }); + + it('renders a default engine overview link', () => { + setMockValues({ myRole: {} }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineOverviewLink"]')).toHaveLength(1); + }); + + it('renders an analytics link', () => { + setMockValues({ myRole: { canViewEngineAnalytics: true } }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineAnalyticsLink"]')).toHaveLength(1); + }); + + it('renders a documents link', () => { + setMockValues({ myRole: { canViewEngineDocuments: true } }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineDocumentsLink"]')).toHaveLength(1); + }); + + it('renders a schema link', () => { + setMockValues({ myRole: { canViewEngineSchema: true } }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineSchemaLink"]')).toHaveLength(1); + + // TODO: Schema warning icon + }); + + // TODO: Unskip when EngineLogic is migrated + it.skip('renders a crawler link', () => { + setMockValues({ myRole: { canViewEngineCrawler: true } }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(1); + + // TODO: Test that the crawler link does NOT show up for meta/sample engines + }); + + // TODO: Unskip when EngineLogic is migrated + it.skip('renders a meta engine source engines link', () => { + setMockValues({ myRole: { canViewMetaEngineSourceEngines: true } }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="MetaEngineEnginesLink"]')).toHaveLength(1); + + // TODO: Test that the crawler link does NOT show up for non-meta engines + }); + + it('renders a relevance tuning link', () => { + setMockValues({ myRole: { canManageEngineRelevanceTuning: true } }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineRelevanceTuningLink"]')).toHaveLength(1); + + // TODO: Boost error icon + }); + + it('renders a synonyms link', () => { + setMockValues({ myRole: { canManageEngineSynonyms: true } }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineSynonymsLink"]')).toHaveLength(1); + }); + + it('renders a curations link', () => { + setMockValues({ myRole: { canManageEngineCurations: true } }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineCurationsLink"]')).toHaveLength(1); + }); + + it('renders a results settings link', () => { + setMockValues({ myRole: { canManageEngineResultSettings: true } }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineResultSettingsLink"]')).toHaveLength(1); + }); + + it('renders a Search UI link', () => { + setMockValues({ myRole: { canManageEngineSearchUi: true } }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineSearchUILink"]')).toHaveLength(1); + }); + + it('renders an API logs link', () => { + setMockValues({ myRole: { canViewEngineApiLogs: true } }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineAPILogsLink"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx new file mode 100644 index 0000000000000..e5ee392b34c01 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Route, Switch, useParams } from 'react-router-dom'; +import { useValues } from 'kea'; + +import { EuiText, EuiBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { SideNavLink, SideNavItem } from '../../../shared/layout'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { AppLogic } from '../../app_logic'; +import { + getEngineRoute, + ENGINE_PATH, + ENGINE_ANALYTICS_PATH, + ENGINE_DOCUMENTS_PATH, + ENGINE_SCHEMA_PATH, + ENGINE_CRAWLER_PATH, + META_ENGINE_SOURCE_ENGINES_PATH, + ENGINE_RELEVANCE_TUNING_PATH, + ENGINE_SYNONYMS_PATH, + ENGINE_CURATIONS_PATH, + ENGINE_RESULT_SETTINGS_PATH, + ENGINE_SEARCH_UI_PATH, + ENGINE_API_LOGS_PATH, +} from '../../routes'; +import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; +import { + ENGINES_TITLE, + OVERVIEW_TITLE, + ANALYTICS_TITLE, + DOCUMENTS_TITLE, + SCHEMA_TITLE, + CRAWLER_TITLE, + RELEVANCE_TUNING_TITLE, + SYNONYMS_TITLE, + CURATIONS_TITLE, + RESULT_SETTINGS_TITLE, + SEARCH_UI_TITLE, + API_LOGS_TITLE, +} from './constants'; + +import './engine_nav.scss'; + +export const EngineRouter: React.FC = () => { + const { + myRole: { canViewEngineAnalytics }, + } = useValues(AppLogic); + + // TODO: EngineLogic + + const { engineName } = useParams() as { engineName: string }; + const engineBreadcrumb = [ENGINES_TITLE, engineName]; + + return ( + // TODO: Add more routes as we migrate them + + {canViewEngineAnalytics && ( + + +
Just testing right now
+
+ )} + + +
Overview
+
+
+ ); +}; + +export const EngineNav: React.FC = () => { + const { + myRole: { + canViewEngineAnalytics, + canViewEngineDocuments, + canViewEngineSchema, + canViewEngineCrawler, + canViewMetaEngineSourceEngines, + canManageEngineSynonyms, + canManageEngineCurations, + canManageEngineRelevanceTuning, + canManageEngineResultSettings, + canManageEngineSearchUi, + canViewEngineApiLogs, + }, + } = useValues(AppLogic); + + // TODO: Use EngineLogic + const isSampleEngine = true; + const isMetaEngine = false; + const { engineName } = useParams() as { engineName: string }; + const engineRoute = engineName && getEngineRoute(engineName); + + if (!engineName) return null; + + return ( + <> + + +
{engineName.toUpperCase()}
+ {isSampleEngine && ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.sampleEngineBadge', { + defaultMessage: 'SAMPLE ENGINE', + })} + + )} + {isMetaEngine && ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.metaEngineBadge', { + defaultMessage: 'META ENGINE', + })} + + )} +
+
+ + {OVERVIEW_TITLE} + + {canViewEngineAnalytics && ( + + {ANALYTICS_TITLE} + + )} + {canViewEngineDocuments && ( + + {DOCUMENTS_TITLE} + + )} + {canViewEngineSchema && ( + + {SCHEMA_TITLE} + {/* TODO: Engine schema warning icon */} + + )} + {canViewEngineCrawler && !isMetaEngine && !isSampleEngine && ( + + {CRAWLER_TITLE} + + )} + {canViewMetaEngineSourceEngines && isMetaEngine && ( + + {ENGINES_TITLE} + + )} + {canManageEngineRelevanceTuning && ( + + {RELEVANCE_TUNING_TITLE} + {/* TODO: invalid boosts error icon */} + + )} + {canManageEngineSynonyms && ( + + {SYNONYMS_TITLE} + + )} + {canManageEngineCurations && ( + + {CURATIONS_TITLE} + + )} + {canManageEngineResultSettings && ( + + {RESULT_SETTINGS_TITLE} + + )} + {canManageEngineSearchUi && ( + + {SEARCH_UI_TITLE} + + )} + {canViewEngineApiLogs && ( + + {API_LOGS_TITLE} + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts new file mode 100644 index 0000000000000..a3320ba5024ca --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/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 { EngineRouter, EngineNav } from './engine_nav'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx index 37ed45a379c0e..a4f248f87a81c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx @@ -9,7 +9,8 @@ import '../../../__mocks__/enterprise_search_url.mock'; import { mockTelemetryActions, mountWithIntl } from '../../../__mocks__/'; import React from 'react'; -import { EuiBasicTable, EuiPagination, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { EuiBasicTable, EuiPagination, EuiButtonEmpty } from '@elastic/eui'; +import { EuiLink } from '../../../shared/react_router_helpers'; import { EngineTable } from './engine_table'; @@ -52,7 +53,7 @@ describe('EngineTable', () => { const engineLinks = wrapper.find(EuiLink); engineLinks.forEach((link) => { - expect(link.prop('href')).toEqual('http://localhost:3002/as/engines/test-engine'); + expect(link.prop('to')).toEqual('/engines/test-engine'); link.simulate('click'); expect(mockTelemetryActions.sendAppSearchTelemetry).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx index ffa5b8e9a1622..abeaf45e6aee8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx @@ -6,12 +6,12 @@ import React from 'react'; import { useActions } from 'kea'; -import { EuiBasicTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { TelemetryLogic } from '../../../shared/telemetry'; -import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; +import { EuiLink } from '../../../shared/react_router_helpers'; import { getEngineRoute } from '../../routes'; import { ENGINES_PAGE_SIZE } from '../../../../../common/constants'; @@ -44,8 +44,7 @@ export const EngineTable: React.FC = ({ const { sendAppSearchTelemetry } = useActions(TelemetryLogic); const engineLinkProps = (name: string) => ({ - href: getAppSearchUrl(getEngineRoute(name)), - target: '_blank', + to: getEngineRoute(name), onClick: () => sendAppSearchTelemetry({ action: 'clicked', 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 49e74582f5f15..700b903efe59b 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 @@ -17,6 +17,7 @@ import { Layout, SideNav, SideNavLink } from '../shared/layout'; import { SetupGuide } from './components/setup_guide'; import { ErrorConnecting } from './components/error_connecting'; import { EngineOverview } from './components/engine_overview'; +import { EngineRouter } from './components/engine'; import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; describe('AppSearch', () => { @@ -54,9 +55,10 @@ describe('AppSearchConfigured', () => { it('renders with layout', () => { const wrapper = shallow(); - expect(wrapper.find(Layout)).toHaveLength(1); - expect(wrapper.find(Layout).prop('readOnlyMode')).toBeFalsy(); + expect(wrapper.find(Layout)).toHaveLength(2); + expect(wrapper.find(Layout).last().prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(EngineOverview)).toHaveLength(1); + expect(wrapper.find(EngineRouter)).toHaveLength(1); }); it('initializes app data with passed props', () => { @@ -91,7 +93,7 @@ describe('AppSearchConfigured', () => { const wrapper = shallow(); - expect(wrapper.find(Layout).prop('readOnlyMode')).toEqual(true); + expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true); }); describe('ability checks', () => { @@ -108,6 +110,13 @@ describe('AppSearchNav', () => { expect(wrapper.find(SideNavLink).prop('to')).toEqual('/engines'); }); + it('renders an Engine subnav if passed', () => { + const wrapper = shallow(Testing} />); + const link = wrapper.find(SideNavLink).dive(); + + expect(link.find('[data-test-subj="subnav"]')).toHaveLength(1); + }); + it('renders the Settings link', () => { setMockValues({ myRole: { canViewSettings: true } }); const wrapper = shallow(); 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 cf67aa3ec7d9d..f32b0b256b898 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 @@ -18,6 +18,7 @@ import { IInitialAppData } from '../../../common/types'; import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; +import { EngineNav, EngineRouter } from './components/engine'; import { ROOT_PATH, @@ -26,6 +27,7 @@ import { CREDENTIALS_PATH, ROLE_MAPPINGS_PATH, ENGINES_PATH, + ENGINE_PATH, } from './routes'; import { SetupGuide } from './components/setup_guide'; @@ -65,6 +67,11 @@ export const AppSearchConfigured: React.FC = (props) => { + + } />} readOnlyMode={readOnlyMode}> + + + } readOnlyMode={readOnlyMode}> {errorConnecting ? ( @@ -94,14 +101,18 @@ export const AppSearchConfigured: React.FC = (props) => { ); }; -export const AppSearchNav: React.FC = () => { +interface IAppSearchNavProps { + subNav?: React.ReactNode; +} + +export const AppSearchNav: React.FC = ({ subNav }) => { const { myRole: { canViewSettings, canViewAccountCredentials, canViewRoleMappings }, } = useValues(AppLogic); return ( - + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.engines', { defaultMessage: 'Engines', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index 51e2497365dd7..3f2d5a7f8ab84 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { generatePath } from 'react-router-dom'; + export const ROOT_PATH = '/'; export const SETUP_GUIDE_PATH = '/setup_guide'; export const SETTINGS_PATH = '/settings/account'; @@ -14,4 +16,28 @@ export const ENGINES_PATH = '/engines'; export const CREATE_ENGINES_PATH = `${ENGINES_PATH}/new`; export const ENGINE_PATH = '/engines/:engineName'; -export const getEngineRoute = (engineName: string) => `${ENGINES_PATH}/${engineName}`; +export const SAMPLE_ENGINE_PATH = '/engines/national-parks-demo'; +export const getEngineRoute = (engineName: string) => generatePath(ENGINE_PATH, { engineName }); + +export const ENGINE_ANALYTICS_PATH = '/analytics'; +// TODO: Analytics sub-pages + +export const ENGINE_DOCUMENTS_PATH = '/documents'; +export const ENGINE_DOCUMENT_DETAIL_PATH = `${ENGINE_DOCUMENTS_PATH}/:documentId`; + +export const ENGINE_SCHEMA_PATH = '/schema/edit'; +export const ENGINE_REINDEX_JOB_PATH = '/reindex-job/:activeReindexJobId'; + +export const ENGINE_CRAWLER_PATH = '/crawler'; +// TODO: Crawler sub-pages + +export const META_ENGINE_SOURCE_ENGINES_PATH = '/engines'; + +export const ENGINE_RELEVANCE_TUNING_PATH = '/search-settings'; +export const ENGINE_SYNONYMS_PATH = '/synonyms'; +export const ENGINE_CURATIONS_PATH = '/curations'; +// TODO: Curations sub-pages +export const ENGINE_RESULT_SETTINGS_PATH = '/result-settings'; + +export const ENGINE_SEARCH_UI_PATH = '/reference_application/new'; +export const ENGINE_API_LOGS_PATH = '/api-logs'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.test.ts index a2eb3d8fbc90d..71b8d90d46e43 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.test.ts @@ -43,7 +43,7 @@ describe('getRoleAbilities', () => { canManageEngineCredentials: false, canManageEngineCurations: false, canManageEngineRelevanceTuning: false, - canManageEngineReferenceUi: false, + canManageEngineSearchUi: false, canManageEngineResultSettings: false, canManageEngineSchema: false, canManageMetaEngineSourceEngines: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.ts index 409aef3cd42ec..a935fa657738c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.ts @@ -38,9 +38,9 @@ export interface IRole { canManageEngineCredentials: boolean; canManageEngineCurations: boolean; canManageEngineRelevanceTuning: boolean; - canManageEngineReferenceUi: boolean; canManageEngineResultSettings: boolean; canManageEngineSchema: boolean; + canManageEngineSearchUi: boolean; canManageMetaEngineSourceEngines: boolean; } @@ -94,9 +94,9 @@ export const getRoleAbilities = (role: IAccount['role']): IRole => { canManageEngineCredentials: myRole.can('manage', 'engine_credentials'), canManageEngineCurations: myRole.can('manage', 'engine_curations'), canManageEngineRelevanceTuning: myRole.can('manage', 'engine_relevance_tuning'), - canManageEngineReferenceUi: myRole.can('manage', 'engine_reference_ui'), canManageEngineResultSettings: myRole.can('manage', 'engine_result_settings'), canManageEngineSchema: myRole.can('manage', 'engine_schema'), + canManageEngineSearchUi: myRole.can('manage', 'engine_reference_ui'), canManageMetaEngineSourceEngines: myRole.can('manage', 'meta_engine_source_engines'), }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts index 2211cdee6c730..d522bd56c46a0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts @@ -5,4 +5,4 @@ */ export { Layout } from './layout'; -export { SideNav, SideNavLink } from './side_nav'; +export { SideNav, SideNavLink, SideNavItem } from './side_nav'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx index b006068ac0d9e..e3e9872f892a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx @@ -14,7 +14,7 @@ import { EuiLink as EuiLinkExternal } from '@elastic/eui'; import { EuiLink } from '../react_router_helpers'; import { ENTERPRISE_SEARCH_PLUGIN, APP_SEARCH_PLUGIN } from '../../../../common/constants'; -import { SideNav, SideNavLink } from './'; +import { SideNav, SideNavLink, SideNavItem } from './'; describe('SideNav', () => { it('renders link children', () => { @@ -106,3 +106,33 @@ describe('SideNavLink', () => { expect(wrapper.find('[data-test-subj="subNav"]')).toHaveLength(1); }); }); + +describe('SideNavItem', () => { + it('renders', () => { + const wrapper = shallow(Test); + + expect(wrapper.type()).toEqual('li'); + expect(wrapper.find('.enterpriseSearchNavLinks__item')).toHaveLength(1); + }); + + it('renders children', () => { + const wrapper = shallow( + + World + + ); + + expect(wrapper.find('[data-test-subj="hello"]').text()).toEqual('World'); + }); + + it('passes down custom classes and props', () => { + const wrapper = shallow( + + Test + + ); + + expect(wrapper.find('.testing')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="testing"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx index 837a565d5525d..facfd0bfcb16d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx @@ -109,3 +109,20 @@ export const SideNavLink: React.FC = ({ ); }; + +/** + * Side navigation non-link item + */ + +interface ISideNavItemProps { + className?: string; +} + +export const SideNavItem: React.FC = ({ children, className, ...rest }) => { + const classes = classNames('enterpriseSearchNavLinks__item', className); + return ( +
  • + {children} +
  • + ); +}; From 250fe67828bb1af012b5f8f6a00ef0c4e5ca984a Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 5 Nov 2020 12:23:57 -0600 Subject: [PATCH 33/57] [Metrics UI] Add full custom metric UI to inventory alerts (#81929) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../inventory/components/expression.test.tsx | 4 +- .../inventory/components/expression.tsx | 85 ++---- .../alerting/inventory/components/metric.tsx | 273 +++++++++++++++--- .../inventory/components/validation.tsx | 14 +- 4 files changed, 282 insertions(+), 94 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx index 60a00371e5ade..54d3b783d22f6 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx @@ -12,7 +12,7 @@ import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/ap // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; import React from 'react'; -import { Expressions, AlertContextMeta, ExpressionRow } from './expression'; +import { Expressions, AlertContextMeta, ExpressionRow, defaultExpression } from './expression'; import { act } from 'react-dom/test-utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; @@ -105,6 +105,7 @@ describe('Expression', () => { threshold: [], timeSize: 1, timeUnit: 'm', + customMetric: defaultExpression.customMetric, }, ]); }); @@ -155,6 +156,7 @@ describe('ExpressionRow', () => { alertsContextMetadata={{ customMetrics: [], }} + fields={[{ name: 'some.system.field', type: 'bzzz' }]} /> ); diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index 66d547eb50d9c..097e0f1f1690b 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from '@elastic/safer-lodash-set'; -import { debounce, pick, uniqBy, isEqual } from 'lodash'; +import { debounce, pick } from 'lodash'; import { Unit } from '@elastic/datemath'; import React, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react'; +import { IFieldType } from 'src/plugins/data/public'; import { EuiFlexGroup, EuiFlexItem, @@ -23,7 +23,6 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertPreview } from '../../common'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; @@ -95,13 +94,18 @@ interface Props { setAlertProperty(key: string, value: any): void; } -const defaultExpression = { +export const defaultExpression = { metric: 'cpu' as SnapshotMetricType, comparator: Comparator.GT, threshold: [], timeSize: 1, timeUnit: 'm', - customMetric: undefined, + customMetric: { + type: 'custom', + id: 'alert-custom-metric', + field: '', + aggregation: 'avg', + }, } as InventoryMetricConditions; export const Expressions: React.FC = (props) => { @@ -226,7 +230,7 @@ export const Expressions: React.FC = (props) => { metric: md.options.metric!.type, customMetric: SnapshotCustomMetricInputRT.is(md.options.metric) ? md.options.metric - : undefined, + : defaultExpression.customMetric, } as InventoryMetricConditions, ]); } else { @@ -306,6 +310,7 @@ export const Expressions: React.FC = (props) => { errors={errors[idx] || emptyError} expression={e || {}} alertsContextMetadata={alertsContext.metadata} + fields={derivedIndexPattern.fields} /> ); })} @@ -415,6 +420,7 @@ interface ExpressionRowProps { remove(id: number): void; setAlertParams(id: number, params: Partial): void; alertsContextMetadata: AlertsContextValue['metadata']; + fields: IFieldType[]; } const StyledExpressionRow = euiStyled(EuiFlexGroup)` @@ -428,48 +434,25 @@ const StyledExpression = euiStyled.div` `; export const ExpressionRow: React.FC = (props) => { - const { - setAlertParams, - expression, - errors, - expressionId, - remove, - canDelete, - alertsContextMetadata, - } = props; + const { setAlertParams, expression, errors, expressionId, remove, canDelete, fields } = props; const { metric, comparator = Comparator.GT, threshold = [], customMetric } = expression; - const [customMetrics, updateCustomMetrics] = useState([]); - - // Create and uniquify a list of custom metrics including: - // - The alert metadata context (which only gives us custom metrics on the inventory page) - // - The custom metric stored in the expression (necessary when editing this alert without having - // access to the metadata context) - // - Whatever custom metrics were previously stored in this list (to preserve the custom metric in the dropdown - // if the user edits the alert and switches away from the custom metric) - useEffect(() => { - const ctxCustomMetrics = alertsContextMetadata?.customMetrics ?? []; - const expressionCustomMetrics = customMetric ? [customMetric] : []; - const newCustomMetrics = uniqBy( - [...customMetrics, ...ctxCustomMetrics, ...expressionCustomMetrics], - (cm: SnapshotCustomMetricInput) => cm.id - ); - if (!isEqual(customMetrics, newCustomMetrics)) updateCustomMetrics(newCustomMetrics); - }, [alertsContextMetadata, customMetric, customMetrics, updateCustomMetrics]); const updateMetric = useCallback( (m?: SnapshotMetricType | string) => { - const newMetric = SnapshotMetricTypeRT.is(m) ? m : 'custom'; + const newMetric = SnapshotMetricTypeRT.is(m) ? m : Boolean(m) ? 'custom' : undefined; const newAlertParams = { ...expression, metric: newMetric }; - if (newMetric === 'custom' && customMetrics) { - set( - newAlertParams, - 'customMetric', - customMetrics.find((cm) => cm.id === m) - ); - } setAlertParams(expressionId, newAlertParams); }, - [expressionId, expression, setAlertParams, customMetrics] + [expressionId, expression, setAlertParams] + ); + + const updateCustomMetric = useCallback( + (cm?: SnapshotCustomMetricInput) => { + if (SnapshotCustomMetricInputRT.is(cm)) { + setAlertParams(expressionId, { ...expression, customMetric: cm }); + } + }, + [expressionId, expression, setAlertParams] ); const updateComparator = useCallback( @@ -515,17 +498,8 @@ export const ExpressionRow: React.FC = (props) => { myMetrics = containerMetricTypes; break; } - const baseMetricOpts = myMetrics.map(toMetricOpt); - const customMetricOpts = customMetrics - ? customMetrics.map((m, i) => ({ - text: getCustomMetricLabel(m), - value: m.id, - })) - : []; - return [...baseMetricOpts, ...customMetricOpts]; - }, [props.nodeType, customMetrics]); - - const selectedMetricValue = metric === 'custom' && customMetric ? customMetric.id : metric!; + return myMetrics.map(toMetricOpt); + }, [props.nodeType]); return ( <> @@ -535,8 +509,8 @@ export const ExpressionRow: React.FC = (props) => { v?.value === selectedMetricValue)?.text || '', + value: metric!, + text: ofFields.find((v) => v?.value === metric)?.text || '', }} metrics={ ofFields.filter((m) => m !== undefined && m.value !== undefined) as Array<{ @@ -545,7 +519,10 @@ export const ExpressionRow: React.FC = (props) => { }> } onChange={updateMetric} + onChangeCustom={updateCustomMetric} errors={errors} + customMetric={customMetric} + fields={fields} /> diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx index 5418eab3c5fc2..2dd2938dfd55a 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { debounce } from 'lodash'; import { EuiExpression, EuiPopover, @@ -14,16 +15,33 @@ import { EuiFlexItem, EuiFormRow, EuiComboBox, + EuiButtonGroup, + EuiSpacer, + EuiSelect, + EuiText, + EuiFieldText, } from '@elastic/eui'; +import { IFieldType } from 'src/plugins/data/public'; import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { + SnapshotCustomMetricInput, + SnapshotCustomMetricInputRT, + SnapshotCustomAggregation, + SNAPSHOT_CUSTOM_AGGREGATIONS, + SnapshotCustomAggregationRT, +} from '../../../../common/http_api/snapshot_api'; interface Props { metric?: { value: string; text: string }; metrics: Array<{ value: string; text: string }>; errors: IErrorObject; onChange: (metric?: string) => void; + onChangeCustom: (customMetric?: SnapshotCustomMetricInput) => void; + customMetric?: SnapshotCustomMetricInput; + fields: IFieldType[]; popupPosition?: | 'upCenter' | 'upLeft' @@ -39,8 +57,40 @@ interface Props { | 'rightDown'; } -export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosition }: Props) => { - const [aggFieldPopoverOpen, setAggFieldPopoverOpen] = useState(false); +const AGGREGATION_LABELS = { + ['avg']: i18n.translate('xpack.infra.waffle.customMetrics.aggregationLables.avg', { + defaultMessage: 'Average', + }), + ['max']: i18n.translate('xpack.infra.waffle.customMetrics.aggregationLables.max', { + defaultMessage: 'Max', + }), + ['min']: i18n.translate('xpack.infra.waffle.customMetrics.aggregationLables.min', { + defaultMessage: 'Min', + }), + ['rate']: i18n.translate('xpack.infra.waffle.customMetrics.aggregationLables.rate', { + defaultMessage: 'Rate', + }), +}; +const aggregationOptions = SNAPSHOT_CUSTOM_AGGREGATIONS.map((k) => ({ + text: AGGREGATION_LABELS[k as SnapshotCustomAggregation], + value: k, +})); + +export const MetricExpression = ({ + metric, + metrics, + customMetric, + fields, + errors, + onChange, + onChangeCustom, + popupPosition, +}: Props) => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [customMetricTabOpen, setCustomMetricTabOpen] = useState(metric?.value === 'custom'); + const [selectedOption, setSelectedOption] = useState(metric?.value); + const [fieldDisplayedCustomLabel, setFieldDisplayedCustomLabel] = useState(customMetric?.label); + const firstFieldOption = { text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.metric.selectFieldLabel', { defaultMessage: 'Select a metric', @@ -48,13 +98,84 @@ export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosit value: '', }; + const fieldOptions = useMemo( + () => + fields + .filter((f) => f.aggregatable && f.type === 'number' && !(customMetric?.field === f.name)) + .map((f) => ({ label: f.name })), + [fields, customMetric?.field] + ); + + const expressionDisplayValue = useMemo( + () => { + return customMetricTabOpen + ? customMetric?.field && getCustomMetricLabel(customMetric) + : metric?.text || firstFieldOption.text; + }, + // The ?s are confusing eslint here, so... + // eslint-disable-next-line react-hooks/exhaustive-deps + [customMetricTabOpen, metric, customMetric, firstFieldOption] + ); + + const onChangeTab = useCallback( + (id) => { + if (id === 'metric-popover-custom') { + setCustomMetricTabOpen(true); + onChange('custom'); + } else { + setCustomMetricTabOpen(false); + onChange(selectedOption); + } + }, + [setCustomMetricTabOpen, onChange, selectedOption] + ); + + const onAggregationChange = useCallback( + (e) => { + const value = e.target.value; + const aggValue: SnapshotCustomAggregation = SnapshotCustomAggregationRT.is(value) + ? value + : 'avg'; + const newCustomMetric = { + ...customMetric, + aggregation: aggValue, + }; + if (SnapshotCustomMetricInputRT.is(newCustomMetric)) onChangeCustom(newCustomMetric); + }, + [customMetric, onChangeCustom] + ); + + const onFieldChange = useCallback( + (selectedOptions: Array<{ label: string }>) => { + const newCustomMetric = { + ...customMetric, + field: selectedOptions[0].label, + }; + if (SnapshotCustomMetricInputRT.is(newCustomMetric)) onChangeCustom(newCustomMetric); + }, + [customMetric, onChangeCustom] + ); + + const debouncedOnChangeCustom = debounce(onChangeCustom, 500); + const onLabelChange = useCallback( + (e) => { + setFieldDisplayedCustomLabel(e.target.value); + const newCustomMetric = { + ...customMetric, + label: e.target.value, + }; + if (SnapshotCustomMetricInputRT.is(newCustomMetric)) debouncedOnChangeCustom(newCustomMetric); + }, + [customMetric, debouncedOnChangeCustom] + ); + const availablefieldsOptions = metrics.map((m) => { return { label: m.text, value: m.value }; }, []); return ( 0))} + value={expressionDisplayValue} + isActive={Boolean(popoverOpen || (errors.metric && errors.metric.length > 0))} onClick={() => { - setAggFieldPopoverOpen(true); + setPopoverOpen(true); }} color={errors.metric?.length ? 'danger' : 'secondary'} /> } - isOpen={aggFieldPopoverOpen} + isOpen={popoverOpen} closePopover={() => { - setAggFieldPopoverOpen(false); + setPopoverOpen(false); }} anchorPosition={popupPosition ?? 'downRight'} zIndex={8000} > -
    - setAggFieldPopoverOpen(false)}> +
    + setPopoverOpen(false)}> - - - 0} error={errors.metric}> - + + {customMetricTabOpen ? ( + <> + + + + + + + + + {i18n.translate('xpack.infra.waffle.customMetrics.of', { + defaultMessage: 'of', + })} + + + + + 0} + /> + + + + of ".', + })} + > + 0} - placeholder={firstFieldOption.text} - options={availablefieldsOptions} - noSuggestions={!availablefieldsOptions.length} - selectedOptions={ - metric ? availablefieldsOptions.filter((a) => a.value === metric.value) : [] - } - renderOption={(o: any) => o.label} - onChange={(selectedOptions) => { - if (selectedOptions.length > 0) { - onChange(selectedOptions[0].value); - setAggFieldPopoverOpen(false); - } else { - onChange(); - } - }} + onChange={onLabelChange} /> - - + + ) : ( + + + + 0} + placeholder={firstFieldOption.text} + options={availablefieldsOptions} + noSuggestions={!availablefieldsOptions.length} + selectedOptions={ + metric ? availablefieldsOptions.filter((a) => a.value === metric.value) : [] + } + renderOption={(o: any) => o.label} + onChange={(selectedOptions) => { + if (selectedOptions.length > 0) { + onChange(selectedOptions[0].value); + setSelectedOption(selectedOptions[0].value); + } else { + onChange(); + } + }} + /> + + + + )}
    ); diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/validation.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/validation.tsx index 47ecd3c527fad..4b522d7d97730 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/validation.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/validation.tsx @@ -6,14 +6,14 @@ import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetricExpressionParams } from '../../../../server/lib/alerting/metric_threshold/types'; +import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; export function validateMetricThreshold({ criteria, }: { - criteria: MetricExpressionParams[]; + criteria: InventoryMetricConditions[]; }): ValidationResult { const validationResult = { errors: {} }; const errors: { @@ -81,14 +81,20 @@ export function validateMetricThreshold({ }) ); } - - if (!c.metric && c.aggType !== 'count') { + if (!c.metric) { errors[id].metric.push( i18n.translate('xpack.infra.metrics.alertFlyout.error.metricRequired', { defaultMessage: 'Metric is required.', }) ); } + if (c.metric === 'custom' && !c.customMetric?.field) { + errors[id].metric.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.customMetricFieldRequired', { + defaultMessage: 'Field is required.', + }) + ); + } }); return validationResult; From 9bff56df7d28b7471830be798262d30579e537a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Thu, 5 Nov 2020 19:37:21 +0100 Subject: [PATCH 34/57] [Security Solution] Fix Overview cypress tests (#82761) --- .../security_solution/cypress/integration/overview.spec.ts | 4 ++-- .../plugins/security_solution/cypress/support/commands.js | 3 +-- x-pack/plugins/security_solution/cypress/support/index.d.ts | 6 +----- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index 9e46a53703041..69094cad7456e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts @@ -13,7 +13,7 @@ import { OVERVIEW_URL } from '../urls/navigation'; describe('Overview Page', () => { it('Host stats render with correct values', () => { - cy.stubSearchStrategyApi('overviewHostQuery', 'overview_search_strategy'); + cy.stubSearchStrategyApi('overview_search_strategy'); loginAndWaitForPage(OVERVIEW_URL); expandHostStats(); @@ -23,7 +23,7 @@ describe('Overview Page', () => { }); it('Network stats render with correct values', () => { - cy.stubSearchStrategyApi('overviewNetworkQuery', 'overview_search_strategy'); + cy.stubSearchStrategyApi('overview_search_strategy'); loginAndWaitForPage(OVERVIEW_URL); expandNetworkStats(); diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index dbd60cdd31a5a..e13a76736205c 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -40,7 +40,6 @@ Cypress.Commands.add('stubSecurityApi', function (dataFileName) { }); Cypress.Commands.add('stubSearchStrategyApi', function ( - queryId, dataFileName, searchStrategyName = 'securitySolutionSearchStrategy' ) { @@ -49,7 +48,7 @@ Cypress.Commands.add('stubSearchStrategyApi', function ( }); cy.server(); cy.fixture(dataFileName).as(`${dataFileName}JSON`); - cy.route('POST', `internal/search/${searchStrategyName}/${queryId}`, `@${dataFileName}JSON`); + cy.route('POST', `internal/search/${searchStrategyName}`, `@${dataFileName}JSON`); }); Cypress.Commands.add( diff --git a/x-pack/plugins/security_solution/cypress/support/index.d.ts b/x-pack/plugins/security_solution/cypress/support/index.d.ts index 0cf3cf614cdb9..fb55a2890c8b7 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.d.ts +++ b/x-pack/plugins/security_solution/cypress/support/index.d.ts @@ -8,11 +8,7 @@ declare namespace Cypress { interface Chainable { promisify(): Promise; stubSecurityApi(dataFileName: string): Chainable; - stubSearchStrategyApi( - queryId: string, - dataFileName: string, - searchStrategyName?: string - ): Chainable; + stubSearchStrategyApi(dataFileName: string, searchStrategyName?: string): Chainable; attachFile(fileName: string, fileType?: string): Chainable; waitUntil( fn: (subject: Subject) => boolean | Chainable, From 62443a6a0574343f0ee7fd448328369e748aee32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 5 Nov 2020 16:29:27 -0300 Subject: [PATCH 35/57] [APM] Filtering by "Type" on error overview sometimes causes an error --- .../__test__/__snapshots__/List.test.tsx.snap | 24 +++++++++---------- .../app/ErrorGroupOverview/List/index.tsx | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index b5a558621e9ca..1f34a0cef1ccf 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -826,7 +826,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` query={ Object { "end": "2018-01-10T10:06:41.050Z", - "kuery": "error.exception.type:AssertionError", + "kuery": "error.exception.type:\\"AssertionError\\"", "page": 0, "serviceName": "opbeans-python", "start": "2018-01-10T09:51:41.050Z", @@ -838,12 +838,12 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1065,7 +1065,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` query={ Object { "end": "2018-01-10T10:06:41.050Z", - "kuery": "error.exception.type:AssertionError", + "kuery": "error.exception.type:\\"AssertionError\\"", "page": 0, "serviceName": "opbeans-python", "start": "2018-01-10T09:51:41.050Z", @@ -1077,12 +1077,12 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1304,7 +1304,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` query={ Object { "end": "2018-01-10T10:06:41.050Z", - "kuery": "error.exception.type:AssertionError", + "kuery": "error.exception.type:\\"AssertionError\\"", "page": 0, "serviceName": "opbeans-python", "start": "2018-01-10T09:51:41.050Z", @@ -1316,12 +1316,12 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > @@ -1543,7 +1543,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` query={ Object { "end": "2018-01-10T10:06:41.050Z", - "kuery": "error.exception.type:AssertionError", + "kuery": "error.exception.type:\\"AssertionError\\"", "page": 0, "serviceName": "opbeans-python", "start": "2018-01-10T09:51:41.050Z", @@ -1555,12 +1555,12 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index 33105189f9c3e..e1f6239112555 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -107,7 +107,7 @@ function ErrorGroupList({ items, serviceName }: Props) { query={ { ...urlParams, - kuery: `error.exception.type:${type}`, + kuery: `error.exception.type:"${type}"`, } as APMQueryParams } > From eeebe580e31f08b04953bf13d95c7f35945a37f5 Mon Sep 17 00:00:00 2001 From: Stuart Tettemer Date: Thu, 5 Nov 2020 13:30:15 -0600 Subject: [PATCH 36/57] Docs: Remove references to Goovy, JS and Py scripted fields (#82662) --- docs/management/managing-fields.asciidoc | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index 3734655edd91b..e69b147615669 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -121,12 +121,8 @@ WARNING: Computing data on the fly with scripted fields can be very resource int {kib} performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get exceptions whenever you try to view the dynamically generated data. -When you define a scripted field in {kib}, you have a choice of scripting languages. In 5.0 and later, the default -options are {ref}/modules-scripting-expression.html[Lucene expressions] and {ref}/modules-scripting-painless.html[Painless]. -While you can use other scripting languages if you enable dynamic scripting for them in {es}, this is not recommended -because they cannot be sufficiently {ref}/modules-scripting-security.html[sandboxed]. - -WARNING: In 5.0 and later, Groovy, JavaScript, and Python scripting are deprecated and unsupported. +When you define a scripted field in {kib}, you have a choice of the {ref}/modules-scripting-expression.html[Lucene expressions] or the +{ref}/modules-scripting-painless.html[Painless] scripting language. You can reference any single value numeric field in your expressions, for example: From c584376ef75e7e138719d1a108bb05ae09753310 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 5 Nov 2020 14:58:31 -0500 Subject: [PATCH 37/57] Skip failing suite (#81848) --- .../security_solution/cypress/integration/overview.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index 69094cad7456e..dafcabb8e1e8d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts @@ -11,7 +11,8 @@ import { loginAndWaitForPage } from '../tasks/login'; import { OVERVIEW_URL } from '../urls/navigation'; -describe('Overview Page', () => { +// Failing: See https://github.com/elastic/kibana/issues/81848 +describe.skip('Overview Page', () => { it('Host stats render with correct values', () => { cy.stubSearchStrategyApi('overview_search_strategy'); loginAndWaitForPage(OVERVIEW_URL); From 64371392b0d795f63736f5cfbb4557a369a026af Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Thu, 5 Nov 2020 15:06:31 -0500 Subject: [PATCH 38/57] [Fleet] Remove asterix from test file name (#82721) * Revert "Revert "[Fleet] Allow snake cased Kibana assets (#77515)" (#82706)" This reverts commit bc05e79b850615ae42cd9eb9e542a8d85c845799. * Rename test index pattern --- .../package_to_package_policy.test.ts | 2 +- .../ingest_manager/common/types/models/epm.ts | 16 ++- .../ingest_manager/sections/epm/constants.tsx | 4 +- .../server/routes/data_streams/handlers.ts | 4 +- .../services/epm/kibana/assets/install.ts | 114 +++++++++++++++--- .../epm/kibana/index_pattern/install.ts | 2 +- .../ensure_installed_default_packages.test.ts | 4 +- .../epm/packages/get_install_type.test.ts | 6 +- .../server/services/epm/packages/install.ts | 5 +- .../server/services/epm/packages/remove.ts | 42 +++++-- .../server/services/epm/registry/index.ts | 4 +- .../ingest_manager/server/types/index.tsx | 1 + .../apis/epm/install_remove_assets.ts | 33 +++++ .../apis/epm/update_assets.ts | 8 +- .../0.1.0/kibana/index_pattern/invalid.json | 11 ++ .../index_pattern/test_index_pattern.json | 11 ++ 16 files changed, 219 insertions(+), 48 deletions(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test_index_pattern.json diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts index 8927b5ab3ca4b..91396bce359b0 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts @@ -25,7 +25,7 @@ describe('Ingest Manager - packageToPackagePolicy', () => { dashboard: [], visualization: [], search: [], - 'index-pattern': [], + index_pattern: [], map: [], }, }, diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index a32322ecff62a..c5fc208bfb2dc 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -35,7 +35,21 @@ export type ServiceName = 'kibana' | 'elasticsearch'; export type AgentAssetType = typeof agentAssetTypes; export type AssetType = KibanaAssetType | ElasticsearchAssetType | ValueOf; +/* + Enum mapping of a saved object asset type to how it would appear in a package file path (snake cased) +*/ export enum KibanaAssetType { + dashboard = 'dashboard', + visualization = 'visualization', + search = 'search', + indexPattern = 'index_pattern', + map = 'map', +} + +/* + Enum of saved object types that are allowed to be installed +*/ +export enum KibanaSavedObjectType { dashboard = 'dashboard', visualization = 'visualization', search = 'search', @@ -271,7 +285,7 @@ export type NotInstalled = T & { export type AssetReference = KibanaAssetReference | EsAssetReference; export type KibanaAssetReference = Pick & { - type: KibanaAssetType; + type: KibanaSavedObjectType; }; export type EsAssetReference = Pick & { type: ElasticsearchAssetType; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx index da3cab1a4b8a3..1dad25e9cf059 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx @@ -20,7 +20,7 @@ export const AssetTitleMap: Record = { ilm_policy: 'ILM Policy', ingest_pipeline: 'Ingest Pipeline', transform: 'Transform', - 'index-pattern': 'Index Pattern', + index_pattern: 'Index Pattern', index_template: 'Index Template', component_template: 'Component Template', search: 'Saved Search', @@ -36,7 +36,7 @@ export const ServiceTitleMap: Record = { export const AssetIcons: Record = { dashboard: 'dashboardApp', - 'index-pattern': 'indexPatternApp', + index_pattern: 'indexPatternApp', search: 'searchProfilerApp', visualization: 'visualizeApp', map: 'mapApp', diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts index 652a7789f65a3..f42f5da2695d0 100644 --- a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -5,7 +5,7 @@ */ import { RequestHandler, SavedObjectsClientContract } from 'src/core/server'; import { DataStream } from '../../types'; -import { GetDataStreamsResponse, KibanaAssetType } from '../../../common'; +import { GetDataStreamsResponse, KibanaAssetType, KibanaSavedObjectType } from '../../../common'; import { getPackageSavedObjects, getKibanaSavedObject } from '../../services/epm/packages/get'; import { defaultIngestErrorHandler } from '../../errors'; @@ -124,7 +124,7 @@ export const getListHandler: RequestHandler = async (context, request, response) // then pick the dashboards from the package saved object const dashboards = pkgSavedObject[0].attributes?.installed_kibana?.filter( - (o) => o.type === KibanaAssetType.dashboard + (o) => o.type === KibanaSavedObjectType.dashboard ) || []; // and then pick the human-readable titles from the dashboard saved objects const enhancedDashboards = await getEnhancedDashboards( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index 201003629e5ea..e7b251ef133c5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -11,17 +11,49 @@ import { } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import * as Registry from '../../registry'; -import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; +import { + AssetType, + KibanaAssetType, + AssetReference, + AssetParts, + KibanaSavedObjectType, +} from '../../../../types'; import { savedObjectTypes } from '../../packages'; +import { indexPatternTypes } from '../index_pattern/install'; type SavedObjectToBe = Required> & { - type: AssetType; + type: KibanaSavedObjectType; }; export type ArchiveAsset = Pick< SavedObject, 'id' | 'attributes' | 'migrationVersion' | 'references' > & { - type: AssetType; + type: KibanaSavedObjectType; +}; + +// KibanaSavedObjectTypes are used to ensure saved objects being created for a given +// KibanaAssetType have the correct type +const KibanaSavedObjectTypeMapping: Record = { + [KibanaAssetType.dashboard]: KibanaSavedObjectType.dashboard, + [KibanaAssetType.indexPattern]: KibanaSavedObjectType.indexPattern, + [KibanaAssetType.map]: KibanaSavedObjectType.map, + [KibanaAssetType.search]: KibanaSavedObjectType.search, + [KibanaAssetType.visualization]: KibanaSavedObjectType.visualization, +}; + +// Define how each asset type will be installed +const AssetInstallers: Record< + KibanaAssetType, + (args: { + savedObjectsClient: SavedObjectsClientContract; + kibanaAssets: ArchiveAsset[]; + }) => Promise>> +> = { + [KibanaAssetType.dashboard]: installKibanaSavedObjects, + [KibanaAssetType.indexPattern]: installKibanaIndexPatterns, + [KibanaAssetType.map]: installKibanaSavedObjects, + [KibanaAssetType.search]: installKibanaSavedObjects, + [KibanaAssetType.visualization]: installKibanaSavedObjects, }; export async function getKibanaAsset(key: string): Promise { @@ -47,16 +79,22 @@ export function createSavedObjectKibanaAsset(asset: ArchiveAsset): SavedObjectTo export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; - kibanaAssets: ArchiveAsset[]; + kibanaAssets: Record; }): Promise { const { savedObjectsClient, kibanaAssets } = options; // install the assets const kibanaAssetTypes = Object.values(KibanaAssetType); const installedAssets = await Promise.all( - kibanaAssetTypes.map((assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, kibanaAssets }) - ) + kibanaAssetTypes.map((assetType) => { + if (kibanaAssets[assetType]) { + return AssetInstallers[assetType]({ + savedObjectsClient, + kibanaAssets: kibanaAssets[assetType], + }); + } + return []; + }) ); return installedAssets.flat(); } @@ -74,25 +112,50 @@ export const deleteKibanaInstalledRefs = async ( installed_kibana: installedAssetsToSave, }); }; -export async function getKibanaAssets(paths: string[]) { - const isKibanaAssetType = (path: string) => Registry.pathParts(path).type in KibanaAssetType; - const filteredPaths = paths.filter(isKibanaAssetType); - const kibanaAssets = await Promise.all(filteredPaths.map((path) => getKibanaAsset(path))); - return kibanaAssets; +export async function getKibanaAssets( + paths: string[] +): Promise> { + const kibanaAssetTypes = Object.values(KibanaAssetType); + const isKibanaAssetType = (path: string) => { + const parts = Registry.pathParts(path); + + return parts.service === 'kibana' && (kibanaAssetTypes as string[]).includes(parts.type); + }; + + const filteredPaths = paths + .filter(isKibanaAssetType) + .map<[string, AssetParts]>((path) => [path, Registry.pathParts(path)]); + + const assetArrays: Array> = []; + for (const assetType of kibanaAssetTypes) { + const matching = filteredPaths.filter(([path, parts]) => parts.type === assetType); + + assetArrays.push(Promise.all(matching.map(([path]) => path).map(getKibanaAsset))); + } + + const resolvedAssets = await Promise.all(assetArrays); + + const result = {} as Record; + + for (const [index, assetType] of kibanaAssetTypes.entries()) { + const expectedType = KibanaSavedObjectTypeMapping[assetType]; + const properlyTypedAssets = resolvedAssets[index].filter(({ type }) => type === expectedType); + + result[assetType] = properlyTypedAssets; + } + + return result; } + async function installKibanaSavedObjects({ savedObjectsClient, - assetType, kibanaAssets, }: { savedObjectsClient: SavedObjectsClientContract; - assetType: KibanaAssetType; kibanaAssets: ArchiveAsset[]; }) { - const isSameType = (asset: ArchiveAsset) => assetType === asset.type; - const filteredKibanaAssets = kibanaAssets.filter((asset) => isSameType(asset)); const toBeSavedObjects = await Promise.all( - filteredKibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) + kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) ); if (toBeSavedObjects.length === 0) { @@ -105,8 +168,23 @@ async function installKibanaSavedObjects({ } } +async function installKibanaIndexPatterns({ + savedObjectsClient, + kibanaAssets, +}: { + savedObjectsClient: SavedObjectsClientContract; + kibanaAssets: ArchiveAsset[]; +}) { + // Filter out any reserved index patterns + const reservedPatterns = indexPatternTypes.map((pattern) => `${pattern}-*`); + + const nonReservedPatterns = kibanaAssets.filter((asset) => !reservedPatterns.includes(asset.id)); + + return installKibanaSavedObjects({ savedObjectsClient, kibanaAssets: nonReservedPatterns }); +} + export function toAssetReference({ id, type }: SavedObject) { - const reference: AssetReference = { id, type: type as KibanaAssetType }; + const reference: AssetReference = { id, type: type as KibanaSavedObjectType }; return reference; } 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 4ca8e9d52c337..d18f43d62436a 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 @@ -72,6 +72,7 @@ export interface IndexPatternField { readFromDocValues: boolean; } +export const indexPatternTypes = Object.values(dataTypes); // TODO: use a function overload and make pkgName and pkgVersion required for install/update // and not for an update removal. or separate out the functions export async function installIndexPatterns( @@ -116,7 +117,6 @@ export async function installIndexPatterns( const packageVersionsInfo = await Promise.all(packageVersionsFetchInfoPromise); // for each index pattern type, create an index pattern - const indexPatternTypes = Object.values(dataTypes); indexPatternTypes.forEach(async (indexPatternType) => { // 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) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts index aaff5df39bac3..4ad6fc96218de 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; +import { ElasticsearchAssetType, Installation, KibanaSavedObjectType } from '../../../types'; import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; jest.mock('./install'); @@ -41,7 +41,7 @@ const mockInstallation: SavedObject = { type: 'epm-packages', attributes: { id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], es_index_patterns: { pattern: 'pattern-name' }, name: 'test package', diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts index a04bfaafe7570..a41511260c6e7 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObject } from 'src/core/server'; -import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; +import { ElasticsearchAssetType, Installation, KibanaSavedObjectType } from '../../../types'; import { getInstallType } from './install'; const mockInstallation: SavedObject = { @@ -13,7 +13,7 @@ const mockInstallation: SavedObject = { type: 'epm-packages', attributes: { id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], es_index_patterns: { pattern: 'pattern-name' }, name: 'test packagek', @@ -30,7 +30,7 @@ const mockInstallationUpdateFail: SavedObject = { type: 'epm-packages', attributes: { id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], es_index_patterns: { pattern: 'pattern-name' }, name: 'test packagek', diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 23666162e91ef..0496a6e9aeef1 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -18,6 +18,7 @@ import { KibanaAssetReference, EsAssetReference, InstallType, + KibanaAssetType, } from '../../../types'; import * as Registry from '../registry'; import { @@ -364,9 +365,9 @@ export async function createInstallation(options: { export const saveKibanaAssetsRefs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - kibanaAssets: ArchiveAsset[] + kibanaAssets: Record ) => { - const assetRefs = kibanaAssets.map(toAssetReference); + const assetRefs = Object.values(kibanaAssets).flat().map(toAssetReference); await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { installed_kibana: assetRefs, }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 4b4fe9540dd95..5db47adc983c2 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -12,6 +12,9 @@ import { AssetType, CallESAsCurrentUser, ElasticsearchAssetType, + EsAssetReference, + KibanaAssetReference, + Installation, } from '../../../types'; import { getInstallation, savedObjectTypes } from './index'; import { deletePipeline } from '../elasticsearch/ingest_pipeline/'; @@ -46,7 +49,7 @@ export async function removeInstallation(options: { // Delete the installed assets const installedAssets = [...installation.installed_kibana, ...installation.installed_es]; - await deleteAssets(installedAssets, savedObjectsClient, callCluster); + await deleteAssets(installation, savedObjectsClient, callCluster); // Delete the manager saved object with references to the asset objects // could also update with [] or some other state @@ -64,17 +67,20 @@ export async function removeInstallation(options: { // successful delete's in SO client return {}. return something more useful return installedAssets; } -async function deleteAssets( - installedObjects: AssetReference[], - savedObjectsClient: SavedObjectsClientContract, - callCluster: CallESAsCurrentUser + +function deleteKibanaAssets( + installedObjects: KibanaAssetReference[], + savedObjectsClient: SavedObjectsClientContract ) { - const logger = appContextService.getLogger(); - const deletePromises = installedObjects.map(async ({ id, type }) => { + return installedObjects.map(async ({ id, type }) => { + return savedObjectsClient.delete(type, id); + }); +} + +function deleteESAssets(installedObjects: EsAssetReference[], callCluster: CallESAsCurrentUser) { + return installedObjects.map(async ({ id, type }) => { const assetType = type as AssetType; - if (savedObjectTypes.includes(assetType)) { - return savedObjectsClient.delete(assetType, id); - } else if (assetType === ElasticsearchAssetType.ingestPipeline) { + if (assetType === ElasticsearchAssetType.ingestPipeline) { return deletePipeline(callCluster, id); } else if (assetType === ElasticsearchAssetType.indexTemplate) { return deleteTemplate(callCluster, id); @@ -82,8 +88,22 @@ async function deleteAssets( return deleteTransforms(callCluster, [id]); } }); +} + +async function deleteAssets( + { installed_es: installedEs, installed_kibana: installedKibana }: Installation, + savedObjectsClient: SavedObjectsClientContract, + callCluster: CallESAsCurrentUser +) { + const logger = appContextService.getLogger(); + + const deletePromises: Array> = [ + ...deleteESAssets(installedEs, callCluster), + ...deleteKibanaAssets(installedKibana, savedObjectsClient), + ]; + try { - await Promise.all([...deletePromises]); + await Promise.all(deletePromises); } catch (err) { logger.error(err); } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 66f28fe58599a..0172f3bb38f51 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -242,10 +242,12 @@ export function getAsset(key: string) { } export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByType { + const kibanaAssetTypes = Object.values(KibanaAssetType); + // ASK: best way, if any, to avoid `any`? const assets = paths.reduce((map: any, path) => { const parts = pathParts(path.replace(/^\/package\//, '')); - if (parts.type in KibanaAssetType) { + if (parts.service === 'kibana' && kibanaAssetTypes.includes(parts.type)) { if (!map[parts.service]) map[parts.service] = {}; if (!map[parts.service][parts.type]) map[parts.service][parts.type] = []; map[parts.service][parts.type].push(parts); diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 3518daa1aba63..5cf43d2830489 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -56,6 +56,7 @@ export { AssetType, Installable, KibanaAssetType, + KibanaSavedObjectType, AssetParts, AssetsGroupedByServiceByType, CategoryId, 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 72ea9cb4e7ef3..8e8e4f010bcb5 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 @@ -184,6 +184,16 @@ export default function (providerContext: FtrProviderContext) { resSearch = err; } expect(resSearch.response.data.statusCode).equal(404); + let resIndexPattern; + try { + resIndexPattern = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'test-*', + }); + } catch (err) { + resIndexPattern = err; + } + expect(resIndexPattern.response.data.statusCode).equal(404); }); it('should have removed the fields from the index patterns', async () => { // The reason there is an expect inside the try and inside the catch in this test case is to guard against two @@ -345,6 +355,7 @@ const expectAssetsInstalled = ({ expect(res.statusCode).equal(200); }); it('should have installed the kibana assets', async function () { + // These are installed from Fleet along with every package const resIndexPatternLogs = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'logs-*', @@ -355,6 +366,8 @@ const expectAssetsInstalled = ({ id: 'metrics-*', }); expect(resIndexPatternMetrics.id).equal('metrics-*'); + + // These are the assets from the package const resDashboard = await kibanaServer.savedObjects.get({ type: 'dashboard', id: 'sample_dashboard', @@ -375,6 +388,22 @@ const expectAssetsInstalled = ({ id: 'sample_search', }); expect(resSearch.id).equal('sample_search'); + const resIndexPattern = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'test-*', + }); + expect(resIndexPattern.id).equal('test-*'); + + let resInvalidTypeIndexPattern; + try { + resInvalidTypeIndexPattern = await kibanaServer.savedObjects.get({ + type: 'invalid-type', + id: 'invalid', + }); + } catch (err) { + resInvalidTypeIndexPattern = err; + } + expect(resInvalidTypeIndexPattern.response.data.statusCode).equal(404); }); it('should create an index pattern with the package fields', async () => { const resIndexPatternLogs = await kibanaServer.savedObjects.get({ @@ -415,6 +444,10 @@ const expectAssetsInstalled = ({ id: 'sample_dashboard2', type: 'dashboard', }, + { + id: 'test-*', + type: 'index-pattern', + }, { id: 'sample_search', type: 'search', diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts index 90dce92a2c6b5..b16cf039f0dad 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts @@ -283,14 +283,14 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_dashboard', type: 'dashboard', }, - { - id: 'sample_search2', - type: 'search', - }, { id: 'sample_visualization', type: 'visualization', }, + { + id: 'sample_search2', + type: 'search', + }, ], installed_es: [ { diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json new file mode 100644 index 0000000000000..bffc52ded73d6 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/invalid.json @@ -0,0 +1,11 @@ +{ + "attributes": { + "fieldFormatMap": "{}", + "fields": "[]", + "timeFieldName": "@timestamp", + "title": "invalid" + }, + "id": "invalid", + "references": [], + "type": "invalid-type" +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test_index_pattern.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test_index_pattern.json new file mode 100644 index 0000000000000..48ba36a116709 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/index_pattern/test_index_pattern.json @@ -0,0 +1,11 @@ +{ + "attributes": { + "fieldFormatMap": "{}", + "fields": "[]", + "timeFieldName": "@timestamp", + "title": "test-*" + }, + "id": "test-*", + "references": [], + "type": "index-pattern" +} From 4a8f42603b42b731ef43e0e89eea0a33bb6aa7f8 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 5 Nov 2020 12:18:06 -0800 Subject: [PATCH 39/57] [Enterprise Search] Fix/update MockRouter helper to return specific routes/paths (#82682) * Fix tests failing for route files that have more than 2 router registrations of the same method - This fix allows us to specify the route call we're testing via a path param * Update all existing uses of MockRouter to pass path param * Add helpful error messaging - e.g., in case a path gets typoed --- .../server/__mocks__/router.mock.ts | 18 +++++- .../routes/app_search/credentials.test.ts | 30 +++++++-- .../server/routes/app_search/engines.test.ts | 6 +- .../server/routes/app_search/settings.test.ts | 11 +++- .../enterprise_search/config_data.test.ts | 5 +- .../enterprise_search/telemetry.test.ts | 6 +- .../routes/workplace_search/groups.test.ts | 62 +++++++++++++++---- .../routes/workplace_search/overview.test.ts | 6 +- 8 files changed, 119 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts index e3471d7268cb1..f00e0f2807e8d 100644 --- a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts +++ b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts @@ -21,6 +21,7 @@ type PayloadType = 'params' | 'query' | 'body'; interface IMockRouterProps { method: MethodType; + path: string; payload?: PayloadType; } interface IMockRouterRequest { @@ -33,12 +34,14 @@ type TMockRouterRequest = KibanaRequest | IMockRouterRequest; export class MockRouter { public router!: jest.Mocked; public method: MethodType; + public path: string; public payload?: PayloadType; public response = httpServerMock.createResponseFactory(); - constructor({ method, payload }: IMockRouterProps) { + constructor({ method, path, payload }: IMockRouterProps) { this.createRouter(); this.method = method; + this.path = path; this.payload = payload; } @@ -47,8 +50,13 @@ export class MockRouter { }; public callRoute = async (request: TMockRouterRequest) => { - const [, handler] = this.router[this.method].mock.calls[0]; + const routerCalls = this.router[this.method].mock.calls as any[]; + if (!routerCalls.length) throw new Error('No routes registered.'); + const route = routerCalls.find(([router]: any) => router.path === this.path); + if (!route) throw new Error('No matching registered routes found - check method/path keys'); + + const [, handler] = route; const context = {} as jest.Mocked; await handler(context, httpServerMock.createKibanaRequest(request as any), this.response); }; @@ -81,7 +89,11 @@ export class MockRouter { /** * Example usage: */ -// const mockRouter = new MockRouter({ method: 'get', payload: 'body' }); +// const mockRouter = new MockRouter({ +// method: 'get', +// path: '/api/app_search/test', +// payload: 'body' +// }); // // beforeEach(() => { // jest.clearAllMocks(); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts index 357b49de93412..af498e346529a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts @@ -14,7 +14,11 @@ describe('credentials routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'get', payload: 'query' }); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/credentials', + payload: 'query', + }); registerCredentialsRoutes({ ...mockDependencies, @@ -46,7 +50,11 @@ describe('credentials routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'post', payload: 'body' }); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/credentials', + payload: 'body', + }); registerCredentialsRoutes({ ...mockDependencies, @@ -155,7 +163,11 @@ describe('credentials routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'get', payload: 'query' }); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/credentials/details', + payload: 'query', + }); registerCredentialsRoutes({ ...mockDependencies, @@ -175,7 +187,11 @@ describe('credentials routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'put', payload: 'body' }); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/app_search/credentials/{name}', + payload: 'body', + }); registerCredentialsRoutes({ ...mockDependencies, @@ -292,7 +308,11 @@ describe('credentials routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'delete', payload: 'params' }); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/app_search/credentials/{name}', + payload: 'params', + }); registerCredentialsRoutes({ ...mockDependencies, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index cd22ff98b01ce..3bfe8abf8a2df 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -25,7 +25,11 @@ describe('engine routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'get', payload: 'query' }); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines', + payload: 'query', + }); registerEnginesRoute({ ...mockDependencies, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts index 095c0ac2b6ab1..be3b2632eb67d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts @@ -14,7 +14,10 @@ describe('log settings routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'get' }); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/log_settings', + }); registerSettingsRoutes({ ...mockDependencies, @@ -36,7 +39,11 @@ describe('log settings routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'put', payload: 'body' }); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/app_search/log_settings', + payload: 'body', + }); registerSettingsRoutes({ ...mockDependencies, diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts index 253c9a418d60b..b6f449ced2599 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts @@ -18,7 +18,10 @@ describe('Enterprise Search Config Data API', () => { let mockRouter: MockRouter; beforeEach(() => { - mockRouter = new MockRouter({ method: 'get' }); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/enterprise_search/config_data', + }); registerConfigDataRoute({ ...mockDependencies, diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts index bd6f4b9da91fd..2229860d87a00 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts @@ -25,7 +25,11 @@ describe('Enterprise Search Telemetry API', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'put', payload: 'body' }); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/enterprise_search/stats', + payload: 'body', + }); registerTelemetryRoute({ ...mockDependencies, diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts index 31e055565ead1..2f244022be037 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts @@ -25,7 +25,11 @@ describe('groups routes', () => { }); it('creates a request handler', () => { - mockRouter = new MockRouter({ method: 'get', payload: 'query' }); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/groups', + payload: 'query', + }); registerGroupsRoute({ ...mockDependencies, @@ -43,7 +47,11 @@ describe('groups routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'post', payload: 'body' }); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/groups', + payload: 'body', + }); registerGroupsRoute({ ...mockDependencies, @@ -71,7 +79,11 @@ describe('groups routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'post', payload: 'body' }); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/groups/search', + payload: 'body', + }); registerSearchGroupsRoute({ ...mockDependencies, @@ -141,7 +153,11 @@ describe('groups routes', () => { }); it('creates a request handler', () => { - mockRouter = new MockRouter({ method: 'get', payload: 'params' }); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/groups/{id}', + payload: 'params', + }); registerGroupRoute({ ...mockDependencies, @@ -176,7 +192,11 @@ describe('groups routes', () => { }; it('creates a request handler', () => { - mockRouter = new MockRouter({ method: 'put', payload: 'body' }); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/workplace_search/groups/{id}', + payload: 'body', + }); registerGroupRoute({ ...mockDependencies, @@ -204,7 +224,11 @@ describe('groups routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'delete', payload: 'params' }); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/workplace_search/groups/{id}', + payload: 'params', + }); registerGroupRoute({ ...mockDependencies, @@ -227,7 +251,7 @@ describe('groups routes', () => { }); }); - describe('GET /api/workplace_search/groups/{id}/users', () => { + describe('GET /api/workplace_search/groups/{id}/group_users', () => { let mockRouter: MockRouter; beforeEach(() => { @@ -235,7 +259,11 @@ describe('groups routes', () => { }); it('creates a request handler', () => { - mockRouter = new MockRouter({ method: 'get', payload: 'params' }); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/groups/{id}/group_users', + payload: 'params', + }); registerGroupUsersRoute({ ...mockDependencies, @@ -261,7 +289,11 @@ describe('groups routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'post', payload: 'body' }); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/groups/{id}/share', + payload: 'body', + }); registerShareGroupRoute({ ...mockDependencies, @@ -291,7 +323,11 @@ describe('groups routes', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'post', payload: 'body' }); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/groups/{id}/assign', + payload: 'body', + }); registerAssignGroupRoute({ ...mockDependencies, @@ -330,7 +366,11 @@ describe('groups routes', () => { }; it('creates a request handler', () => { - mockRouter = new MockRouter({ method: 'put', payload: 'body' }); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/workplace_search/groups/{id}/boosts', + payload: 'body', + }); registerBoostsGroupRoute({ ...mockDependencies, diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts index a387cab31c17a..9317b1ada85af 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts @@ -14,7 +14,11 @@ describe('Overview route', () => { beforeEach(() => { jest.clearAllMocks(); - mockRouter = new MockRouter({ method: 'get', payload: 'query' }); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/overview', + payload: 'query', + }); registerOverviewRoute({ ...mockDependencies, From ca04175ae9cb8305a153612e85b4403bf9d95a8c Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 5 Nov 2020 15:29:33 -0500 Subject: [PATCH 40/57] Combine related getBuffer* functions. Add tests (#82766) ## Summary Move logic from `getBufferExtractorForContentType` into `getBufferExtractor` & change the interface so one function can be used. ### Diff showing old vs new call ```diff - getBufferExtractorForContentType(contentType); + getBufferExtractor({ contentType }); ``` ```diff - getBufferExtractor(archivePath); + getBufferExtractor({ archivePath }); ``` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../server/services/epm/archive/index.ts | 21 ++++-------- .../server/services/epm/registry/extract.ts | 24 +++++++++++-- .../services/epm/registry/index.test.ts | 34 ++++++++++++++++--- .../server/services/epm/registry/index.ts | 16 ++++----- 4 files changed, 64 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/archive/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/archive/index.ts index 91ed489b3a5bb..395f9c15b3b87 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/archive/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/archive/index.ts @@ -18,7 +18,7 @@ import { import { PackageInvalidArchiveError, PackageUnsupportedMediaTypeError } from '../../../errors'; import { pkgToPkgKey } from '../registry'; import { cacheGet, cacheSet, setArchiveFilelist } from '../registry/cache'; -import { unzipBuffer, untarBuffer, ArchiveEntry } from '../registry/extract'; +import { ArchiveEntry, getBufferExtractor } from '../registry/extract'; export async function loadArchivePackage({ archiveBuffer, @@ -37,24 +37,17 @@ export async function loadArchivePackage({ }; } -function getBufferExtractorForContentType(contentType: string) { - if (contentType === 'application/gzip') { - return untarBuffer; - } else if (contentType === 'application/zip') { - return unzipBuffer; - } else { - throw new PackageUnsupportedMediaTypeError( - `Unsupported media type ${contentType}. Please use 'application/gzip' or 'application/zip'` - ); - } -} - export async function unpackArchiveToCache( archiveBuffer: Buffer, contentType: string, filter = (entry: ArchiveEntry): boolean => true ): Promise { - const bufferExtractor = getBufferExtractorForContentType(contentType); + const bufferExtractor = getBufferExtractor({ contentType }); + if (!bufferExtractor) { + throw new PackageUnsupportedMediaTypeError( + `Unsupported media type ${contentType}. Please use 'application/gzip' or 'application/zip'` + ); + } const paths: string[] = []; try { await bufferExtractor(archiveBuffer, filter, (entry: ArchiveEntry) => { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts index 6d029b54a6317..b79218638ce24 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts @@ -17,7 +17,7 @@ export async function untarBuffer( buffer: Buffer, filter = (entry: ArchiveEntry): boolean => true, onEntry = (entry: ArchiveEntry): void => {} -): Promise { +): Promise { const deflatedStream = bufferToStream(buffer); // use tar.list vs .extract to avoid writing to disk const inflateStream = tar.list().on('entry', (entry: tar.FileStat) => { @@ -36,7 +36,7 @@ export async function unzipBuffer( buffer: Buffer, filter = (entry: ArchiveEntry): boolean => true, onEntry = (entry: ArchiveEntry): void => {} -): Promise { +): Promise { const zipfile = await yauzlFromBuffer(buffer, { lazyEntries: true }); zipfile.readEntry(); zipfile.on('entry', async (entry: yauzl.Entry) => { @@ -50,6 +50,26 @@ export async function unzipBuffer( return new Promise((resolve, reject) => zipfile.on('end', resolve).on('error', reject)); } +type BufferExtractor = typeof unzipBuffer | typeof untarBuffer; +export function getBufferExtractor( + args: { contentType: string } | { archivePath: string } +): BufferExtractor | undefined { + if ('contentType' in args) { + if (args.contentType === 'application/gzip') { + return untarBuffer; + } else if (args.contentType === 'application/zip') { + return unzipBuffer; + } + } else if ('archivePath' in args) { + if (args.archivePath.endsWith('.zip')) { + return unzipBuffer; + } + if (args.archivePath.endsWith('.gz')) { + return untarBuffer; + } + } +} + function yauzlFromBuffer(buffer: Buffer, opts: yauzl.Options): Promise { return new Promise((resolve, reject) => yauzl.fromBuffer(buffer, opts, (err?: Error, handle?: yauzl.ZipFile) => diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts index ba51636c13f36..a2d5c8147002d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts @@ -82,14 +82,38 @@ describe('splitPkgKey tests', () => { }); }); -describe('getBufferExtractor', () => { - it('returns unzipBuffer if the archive key ends in .zip', () => { - const extractor = getBufferExtractor('.zip'); +describe('getBufferExtractor called with { archivePath }', () => { + it('returns unzipBuffer if `archivePath` ends in .zip', () => { + const extractor = getBufferExtractor({ archivePath: '.zip' }); expect(extractor).toBe(unzipBuffer); }); - it('returns untarBuffer if the key ends in anything else', () => { - const extractor = getBufferExtractor('.xyz'); + it('returns untarBuffer if `archivePath` ends in .gz', () => { + const extractor = getBufferExtractor({ archivePath: '.gz' }); expect(extractor).toBe(untarBuffer); + const extractor2 = getBufferExtractor({ archivePath: '.tar.gz' }); + expect(extractor2).toBe(untarBuffer); + }); + + it('returns `undefined` if `archivePath` ends in anything else', () => { + const extractor = getBufferExtractor({ archivePath: '.xyz' }); + expect(extractor).toEqual(undefined); + }); +}); + +describe('getBufferExtractor called with { contentType }', () => { + it('returns unzipBuffer if `contentType` is `application/zip`', () => { + const extractor = getBufferExtractor({ contentType: 'application/zip' }); + expect(extractor).toBe(unzipBuffer); + }); + + it('returns untarBuffer if `contentType` is `application/gzip`', () => { + const extractor = getBufferExtractor({ contentType: 'application/gzip' }); + expect(extractor).toBe(untarBuffer); + }); + + it('returns `undefined` if `contentType` ends in anything else', () => { + const extractor = getBufferExtractor({ contentType: '.xyz' }); + expect(extractor).toEqual(undefined); }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 0172f3bb38f51..e6d14a7846c22 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -26,14 +26,14 @@ import { setArchiveFilelist, deleteArchiveFilelist, } from './cache'; -import { ArchiveEntry, untarBuffer, unzipBuffer } from './extract'; +import { ArchiveEntry, getBufferExtractor } from './extract'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; import { getRegistryUrl } from './registry_url'; import { appContextService } from '../..'; import { PackageNotFoundError, PackageCacheError } from '../../../errors'; -export { ArchiveEntry } from './extract'; +export { ArchiveEntry, getBufferExtractor } from './extract'; export interface SearchParams { category?: CategoryId; @@ -139,7 +139,10 @@ export async function unpackRegistryPackageToCache( ): Promise { const paths: string[] = []; const { archiveBuffer, archivePath } = await fetchArchiveBuffer(pkgName, pkgVersion); - const bufferExtractor = getBufferExtractor(archivePath); + const bufferExtractor = getBufferExtractor({ archivePath }); + if (!bufferExtractor) { + throw new Error('Unknown compression format. Please use .zip or .gz'); + } await bufferExtractor(archiveBuffer, filter, (entry: ArchiveEntry) => { const { path, buffer } = entry; const { file } = pathParts(path); @@ -199,13 +202,6 @@ export function pathParts(path: string): AssetParts { } as AssetParts; } -export function getBufferExtractor(archivePath: string) { - const isZip = archivePath.endsWith('.zip'); - const bufferExtractor = isZip ? unzipBuffer : untarBuffer; - - return bufferExtractor; -} - export async function ensureCachedArchiveInfo( name: string, version: string, From 2287376aebb58ffb127b19193bbb5255a2614dd1 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 5 Nov 2020 12:37:58 -0800 Subject: [PATCH 41/57] [DOCS] Updates field formatters (#82667) * [DOCS] Updates field formatters * Update docs/management/managing-fields.asciidoc Co-authored-by: Kaarina Tungseth * [DOCS] Minor edits Co-authored-by: Kaarina Tungseth --- .../images/management-index-patterns.png | Bin 53215 -> 0 bytes docs/management/managing-fields.asciidoc | 75 ++++-------------- 2 files changed, 17 insertions(+), 58 deletions(-) delete mode 100644 docs/management/images/management-index-patterns.png diff --git a/docs/management/images/management-index-patterns.png b/docs/management/images/management-index-patterns.png deleted file mode 100644 index 232d32893b96d3e6a0ae7775fc0cdde40a1a4bc3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53215 zcmeFZQ*@=xx&|5>Gq%;SZJQn2c6V&swma;oW83c7w(Xp(z0Ufv*MD~|&dna<8)Me^ z=B!zGQC08rycMP(Ck_vT4Fd!O1TQHeq67p41^@yA&W3{cd_#M1{qgw${7p$*2&i%b z_Xr3`07z0qP}vpuGz(H6wI5?Z&pkju;*j@Nw~&OG5KmN8G_Vs~K?HynOB}8bT%#|D zBuc3yQdBOHwC-`y9uyl31bsC8;gO)-rj3lO>Da}dzPfgPdOBv8GTLFc?y&j*d(S{9ljXOR8^cM#S#j6|y`cyKpxV7daad$8EYcbOeCFPX6lB z9xdbr6A&T?LMrgTp9gN>JC@MU(BxR^g+A_(klo=u%89Lo{76@YirAQ@)|S`24r%s?#?y?9KJari^FRvOoE0(w((2&g8lbX0bQb+n=S~b0~H^4+k{r} z3;Ks2eO9xEE-}qr_A59;@FxH7vHt6g76>Z9I_#1e`g8vN_ai{{`K1!k7Q{bx`TuG{ z$Nt|8{eRV=i3Xgg=-!=dB*Tpc9%P zRtONof$(N~0fQrw8~a7Ix4Wo-T2yv!01G=9)20k*}x#p^tT5>^+8OWBy z71Qoyz)W>w%=-W(Fl%sez(8q)XHfjDmY+w6z-7HTryHjd*Z>^tr+%TM__B7M2)QO_CxQqiZ|@KP z%@=I069G~Pk$o=ULP&`GTB8NDLEe!}4f1?rgR0%A{${mJDNv-wHCK-GH9P7On+<>= z%{PHBgsaKr17d&s%YS>D&2@4C;H~wJ4qUvyd4*L@5DlIew*!ctWXVli@>r)2043q- z_yhFIKlwuq5yBO8D_+;bSQ?EsWf(L%(wIgc8C<+y;-gKTPqkxN+GUv#qpP_Qc8Aev^rx-8W4|@_#wiE;=aV z4ByS&T~d7f^X>iP&_d?(US`x$LUZ=5GhP2RS~mFngm#OMNv) zgGDYE?EU?7q5~fssxYWuVoiB%+c-fEgo%w$jz%4yl>)(Z!;lH+)rJqw$XO%YrI%hg z!NSMKS;q?goOMEK=)-V?n#XBzjGM>&&dVj_^|MS~{85cC-veS`4~keD5dZc7gAXQh z0166q2qHlo3Wc16h=@oFq==Z<$U7+HX3-|yG$Z5D{7y6uN%f9#iD&z36U`#d7rzve zD22_lBVsk;E)F(b$6fmbE;2{SjUVqb4N~4P(Con}^?%GoZM=X?+vBc%99z=04Ayn};7hiyCe;+#j>5oRc84lN%s0SyNY z!lTFKmp;GZa*RYKa7PJS7qez$G>QYFFEoqa1Wj%E>CZaWzD&;1Qt9!2mAu{`(~ZgT zI2N*J_+tTXloM!2&>wOsQ&CvAP!SU%HZhQsTD*dXm~5|z)Z@6y;4oz8fmka>(zU6XG3XTC1qge z08VkI+-?IB7EY&WtHzI_HM+dP1-ADyKaSQGFUPhO1H5QOWa%6XEUGw$@z*Rg1BZ^o z8FH1Sx_WA0VDPrCQ*4{tO?Y|v2|N*0e+>~)9)oLEAL5|Ls__K)Z-iyXAi>j<49G1+ ze2aVGK|lyS1%)Sc>6K?YG7e5ei^D$OynXmsVrr`SSG|=6(?SFA=d+!i1o5`^Z>B#= zcexfSbrLo=^*uv&9#@+k+FY*rPS|T|9FPeJQZhdT9z?~(GfsXyA<*~>T3f>%LmzL! zV)gIJghs|=l2$~@E3nCl`7&_-ek^)x{(cmH_~hy7C|#-DJX3PFL4FpH-K#k@b%GYi z$jQwq-R%!9QwafUcXD&$BB5X(8yFHKEoa|4ZJdrl4VN%1v=1P_^>g~w@OE0N*;QI1>wX*zMk}0QA?*H)+uO*-tZS&cavKP8GdX?abXg`i0T`9U}IL(H0cf68a?ikk|S8 zAEzAOG+N}>oLs`81Tsd)q1znRdW}eD9IFf}ju(}H47zHX}^YSbDplQ3(c7$4Q{FIHqHH=# zGTn!V+(;r_R(@Jg1p4SuTuxYVak1z7l0>O)yJq-XGNO1ieggZ(DTSb5_YZ!0mkZ0K zr1O&oo0YvgK0C=hsU+jmMLQ8;u#Wdis5d2LE!^pTzX-xBjy~wYb+B zW+igiFXYphUGN>Tz(TQCEdxCc#we4@mE%90j%cE=S!K{E>xlL)dl7e{fO7oGFy{B+B)Y zst38lvn9T12QeynM`8Qm3@Y|#u~3mcor@dTb~g#ywbc9S`E@_DN}&>w-4UBfaT|+M z$k>@8_fwPB5p2xoR_6abefVh+167SIWF-1N2pU+tt&uMwB7%-RhcT{n>1ejF(~xI*FA-7S`>|p|i)0wO z@fsglIT`D1vbx>A$Sxg386N1L$q6V3E9~sev3jqy+%XyMepza^iea3GMWg!Cof@Yq zY&RIKQ)}2Sh4t27d_6=W`_(`d(N{6Yn{LDNhF2uiY9N+gvkuC%Z^z`}oVV8EH+o&o z*LD0Uj1BIi2&fjW-dLB75I@e|vFew{$IyFWqxXk%Zo-v@i^Q~F`y3tg|k7Ett;8`itIwV z!sGQEdD`vz9!NO-wpYpII<>O+u|d-rj-MX0wb|WU>-`3i#^P3H9W)hB0=fE3#G%vY zW;I)4s_F;T1_<@AY@GGX4fcE+ciCiK&E%aH?nWc-Q~ZW#yWG^Tje}-UFKqCoAH70MAw*JW=$Cno@jex;>8Q--T9?@dE2_t<+_H_hcQhq|#b(y|+IF9KYw5_l zbvFMzT=2*CLxNBkBvqeEjUnXLr!eumACXyKK%p6qY~f-_U`*txg5o=F15XX5Q`#W& zIEL#0rCyh`*d}N3P2pKuqY;tnt>wO5UjPsf%H6w73{6lTJ~|%#dj~#%TuL$~BzzPT z3QkK-Cpkev$p$8y5k>C`=YYLGJ^tu`Uy~ru zw6-dFV$Tenx<#t^_0yfJ4OP26Y~}~tz@q-GJC6(ZP=EO)#C*###5;R`oxvQzTFxz_ zTc^8_`m--zKdGCTx^9A|l(pIeSq%3P&pbZyzPfee`8Oqx{kGvj^2YSXfm_Z}f(E!- z%$1$QEOhBk+MPOWX4f0b9)~;g=}QEZiF`x&WmWkH!;WlZ+#t629&M$%DjOWJXKIVc z`PJ%ShWQ(?kPU@|#h1~+gU@2;vyAmQ$ zHG?Kv-8Pv4Pwsilq}~u8IJ-b9@`~>CE49=(0$z8xNM{N+m|(9M{I&j0V`%N)V3=tk zyDN5`?99{I+4u~;<5~J%O>R}hA{*YIdk-6CSQyWhI(hYI-wx<*cRVy& zowII_elLv0H(F1x;$}b!^;SQG41LSeOY}*pV)->Y>FmkX&#BCj3Vubz-UA-o@qiE* zCPaB({2&PLY_jVM^cYhEwx{RwMv}$ATxCR~n4g+f!(aW8TXHZEj#h4bIE5HVwD#0& zmXI;Kbj`kgN-v%A9#Qi&baR=1xOjy{soj)&CBWfvpZUu1ZkERR4WSJeSxxxlFm+73c66Tl4we>uZa%$Mog-bOHPGDU4n0m1!nb z_HLkqosRdDNjh90Wl%Ts&}Y6k9mH0Ub{XSUkPM*n!N*a2Jkz= z=BDx~FJm}(#?2fZ*f1gc=yMCo(*LAp%)PbKmGTbI%56C~y654vrGXn_>CyXRFGgz5 z4b_F|@l(-31CGRdt~<9+L%-a4?p@lXJ5Eo>i7}W z&Y*g4{LC~vD4&7(1G$X};~dUc+Fx#fgK&&lTQ{p?6bgELtA8m^3>Yg)`>WSVVqEgx z%}Ae9aiLBa-}>!MhlkJVjDA_M|1fR$%FB7 zeMDo+x6rCBRl5=WeG5*-2|9F~lo|>#27tBZV0Ft12x||&*2m5)62gWA||Ak*BqK&HR1 z)ZU4%rQo2au&G?9wTRON4nNix?`;S-Rhrk*4)B+#SZFwLsIR}B#FCbr1)XfbtUxw= zA;V#mWP{qGri^srg+6Zk-4e-i{Sb%981}=h6QAzFtfdJx8+1gVSDvP#egiS(F3I*T zLaJGaQ?~t1oZjzFk`os|?!EbFlTtPVwV*s|rvHWzSio+2kpe?ST>phOdQ*$8Go|8i zLBW~G%`Aui9-9$FDS$__-S_)vTm@$TDs~i#H4R=q2^yzcwwKU8i<;=5&*c*E?(`dr z63j1OXAY!^e!Gymhc`X(KCYPH3+cs&m&CFWuIv^iF8E~Z9;E+n`Q9MeA+`|tOpT7<)T0!Nm+L^-=ClCqq{4>#=%fuLf->G`~)g*>0T%?0klBn_8`2 z-_-QHf~!OB=UbyFguI%{PLPARnBTi0Neam$$J@NUe`HVa@PBxk1!8<2mqX?wf zNL2`*ZhuXSeK}ffPH;GopLT^H$TXn2*zf_$hSCD?O?~Biw#Nhu>Z7OPRW|E8nJXvZ z)jJ7b91BUku4l1bl?X?rv)0sYb#AZFtrz2AjNsqiK`fL`k)xbAJUO8@>UdJB)QO1Y ze@`Xr6ckY;65}kr`pK`Gtt&P(we-;w1S?sSFZ#7E4Hc0)@|_UElk7_i5JS!BI! z6&pujOvHKu-F)P6^uzeJeGi_lea=3A^nX2^E|PNeaO_1Z+xL=>22Jo1CHb5?*l;dH|lgzx`m#p<2i35 zpflBxRZ9+(G*Xs%K6sYsLhF@lOx|eRJ74(}1N=U)) z@=bWJ~jqwY_((o{v^#xIJ*+yGz!xJ}MZ($5lF)^_Z+37PG<;Pb?5IH*ZAlPZkF zqu{Rk90l!~vNhNmWSFNL72;nYU5PJ5Z6oC)V3^%%{524?6WW>xB6i*tJVrprSojB2 zLg~2F#RKPk7ArH`Kqqx+i2$LFXcJ*C#8oWdlXNI95~N_Hdi3lT+7pNi3*tP4a-~D4 zj_?Mn0GQ*%F`MI*fKX*HkidI+;V7KV4PWxs)5jatus{TZbBAx}FK;H_8kgk7=ZbXA z`hWI=o~+kftSK!w*w~nsH(o3nv`hbx&mlVgQRxN=fruX~#8a(2i}i}g@2MA#N@=Zq zO}j!{l`dlk5~+;F=-xh0lH{siT6}&*BJK&*+mw1Z_G{u%D}|lm*H3)pWACb8%gIaz z+r~z(DO4&_!EMY8L05LA-`dsYU}m`sq>@IM%=JH5Vio}5F$`kxR#Uiun}fHsRpYm3 zCn6Y`gh@WUIH}JL%=idoaq*H&0N^LGd5w^eicNi_Kf>On72C8gai3NR1~Zw9s>JhpSp0Gf)9U=| z=zWHlidswgW4sTX&lj7~TsD(E70*(w5wTEE=7y}4@W*`Gj9oANr3ZT~euv(aeW9qI zMnr!eQ%?zf2gN&aZ7)l{Ck6m-4Vd5cy?`)m-%=2GJND)}`vaRV{iw1$#lk^BAFE_m z1GGTXOUuBRs9&zC!PdNJnv$3$hJ%;F@Ic(Um4DC#j-B^^LzXkX^5sB5ru;cz9ZN-` zDtQ3CQq+`_;`yX(!i=+l#)(qqRX6i0Gid#tHWWda(W-L5hYvHf9XloVOW&GHdbKP* zX0udT!TE*OgcKT2u~Q(FJ+>|sJf9A=PjAWm^)rFTu3wDWu+-RmImI26Yj&9{gGsMN z>Gc*&>HC$`5jY|gYxEot`*4kc`3m|PaulD))?lq#JN+!l7R$g_k6p{su$%8$ZEKiK zyx3?7`3%2+vi^VD5ga&iT*aO2S^wou5 z0=(%1AcgL>n|Isi8IBq0^k{&eO%sqcRovxP(vqyt4TW}7kawX)Xd-)g>ku#7)?9e=%*vcq5sps;hR-mE-YOv=Ro>ZX0&9j!K?LjrBP2RiHhjzV9pBA$8)ACj(PzWTp5!`Z&cwsPNKn6Q`C;M#Ki5EjdeQhvUPTrbuJh8`Ou9L zRf1@&TrS@8t^(GJ)ziDa)w8l)3kBzP(>yi>>q_!^D!F9}d7q~PI8lzQ{B?wu?GSg? zOg~>k%44ss?PEiAe4&Y;Td7|^WD9S;Wo>4~9p1(1wvSma>;>JfbQ-Vs*$3W6V};CN zX>LYL_YNy|2w(TDJ_=juTnk%_`_Xio<<8dr3AqY10DSX`nCqaAk;0*oS!X(YYnq)D zNS2tYzUV=rYtpH^nf<`# zhSN$=kk|V;@&b!6Obmq>5~ocMfY|s|4-<}PI>oQyDnd@jPd*q>IZV{l+6jIo_|O<4 zcor?*nRaZqg?z+sPmkQJ&B zfEI#uobh`N3JTxV*%SFqt@q7lcVgM=IXttUHNEvNGg9+AX$|lj{B%A5EX2$TE&i*7 z=@ap_`l%n#H*8&W=J}p^zpZmvh#0~`flR!qjhk!8z=W2|%s14Y!3^2AJ>0;^&lr19 z#g$&`QQM1cze5^AuvynW5ZtTxp%w=Z*s|&b_0uLN0M6FNdy!a$i0<&F8hgJufY`V) zxGYzhCe<7TqBnyWjyNot(Dj=+lnBc?1-vj65Te)m{AA0T>$PDfJrSu;P{KL@{C)j9 z`c72r&(vyhU^XA&|-XFuNAh%y{ zn$WYm1D57aKfP?d7|`G&?>t{^UJ1ld9<)RRCt@*H#%8IoGrVzf)FWu#BQqnSgRW>c z$4rIYk_(OPlXca)mFuM{{4AHaGZ>TRi7pz;aFr*9XzB{1<0s53{#`V(t1A~3q9vG| zbRA>CPAnyE_7%KRor2*5ObeD1qG_f($Z}1f6$5!KhWni6KJ&xZ4(0{cR5fUL>1xgQs#%f({)Zb!9~bfW~N zt11RlJX!7$trfXsuLNRXBO;k$$`Lyb)&GL{wnAwKPnQl<3=~3=U~Nu!*>jd{`=ZNy zNA3S552h-y7@GQoDw^|gg%9#Kz$-7(J8S#!F<4W3|12|q7?TEgF)vz3ogjF7)4XtaiWeI0b*V{%e0Ma~Ji zU9rj_C;Hp(VFe4AMpss$Dz)owE9HpBkc8P@Fh$31Xd|N9lIe=kEQ)f(VYC^af`r&q zacM=GmHZTj@ zGixY!cUN*ZvrJjG**lSz!fp8-ic~b>XgqFP)#ND^C}qIa-6%=X+gp+4+z7c^K=y@P z(p2myd89~+ZzhwId&{=p-s^~bKVH@oB5XjoO9C0Qk(AwHJrdu|NmXa6NGi5ejn@mp zueu-Mn&}?`DSNZKsWH6EY~#4N?8|#nL#ZeJ{KTV(;L_c6zs%hL!#00aG%ctupiZawp2i`mq@;uJDjwnU{R# zPyz0nuK~ksMJ6ukRpu^e+V^uOd9276r6+IA(y2j>wm1WaQ}=tjcjROysH$$5Fc?oP zu%{xda`kPjR#mT{F5d?WvA(m?u_Ewck19Y22bwD~Rv{rqciVg8onJuKaQS67VjDgz zAo?!+>_?Wp5g{&~E5bLydo<7ID=vILzOk^5H?vpZXoIX6jFZz!TdJ5`Twq|kJN*I% zJcx)7Fm(cI2LSM1Y4U0$O$965t#VJrLPc9IVoAEo2pA<_getp-7S1kzc%hg3vi8G3 zwJ`Y+v{3~U`E*M|c|A^&%9p2vQQXW8(|;01njy6dzvIiJ9OqSxl1T2CM@gshfVZ2T ziZP9qJK`JA6~zWR9>j*sh}f`%vB+6Wlyov!%VuJQLvzsTklQ5S6<3A|&v2;pDb5k% zJ|uH>87qJyx(Drj-?Z#*qvoEQ*#I0{DbL8k#RX-d zP~%}|P<|+azMY2K`P9ELFrq|I8_|e$3s@9Zx})tW5;DY{sLu8cb5_}Ncs(zlIGX7y zz?{*16Y5t*(Z0)2$(IXA{E{@qdhP~2QGY%154f19f?9`Oa*amY_N(>9UmiI$nsp+o zmD;pc*%#KTfz|gC&1ncKB0tok)@Js;1vf;=eS;pzSNQg=`%N-otWb@dL^5gs_jrbp z=FjkOtPU9RCCy9k@mHoTpE={6=ilVf>+WotYoiG|uRpn=jn3+SygPzlU2xUJQ|@3H z24Y|Rieu0Hx<-60JVKG(zm-;^6k0b=_FRpcCkH_*=-Boh_PHLe@|B*DuEQzF zlgWFWGNDhNS2V&f*d0{}jRkmHeo+^87_nYP*F_55nlGZGM}@#pEC!-qi9N@hA|p(U zwE+pzkHa%-EjnH*sBJ4~ljcR9xKLFT{r0MNa6}3rP}=kP!htg;^1XDGR4Iq3m-bl0 z0ZA8cMW2+te*|T6fs(iY1Hb!;a9>}~j+9S^?$Hyb^7(}bKe1ONgjK>p6S*mU{wM8rAl6ka6eJscXc$&tu+o_-6=geci*`-(=c7?C~0>TZOh-S6TI9hIGxOAd@Bb2T>b^YO*^n9tenZbN%AOYVS=YyIU z3r#F2!+@+3!S){ODI$F@8IiThV!r`7yu|bZE_?&ShQU{ar&AcIV@4O{R7~ba5r+l=IC28M z=D~eoUg=3pI_VvEJUxOvyKihoaphgo4zx6v}TZ-84oO6C2cEUE#LXHLf>Gw_p-Qit#doXFd% zy5S806j}dtNTXJ=(6*#JCnAyMdUD|_9_6I7ym!k1SfD@n4UX{B(6mvl-Niec(X`9? zs!2S)yBit#Vzu^OJX!e8kMHrni{(h{1X9CB8&6xO7y2Voh6tF=GLk@p5pgUZzxaRvK!9ny+)g?+Z z1P@m75E1l9$V*txZ^vhEH$xN5_IHjJ;xPgDgz&{^@F%=Q?1Ag`Gpm90h3)}}`7~VB zA%svdf0u^uDc71$f?I?sz_9wt3a(i&2;@{KDjbub)^~KqXb<#Fp+Z3s&~sTLi{1Ch zJj7hiSEK3#ZAbCnkE}CGNA#}}*A-E68140)J>r2CxDTzq)jsRTvh=Ha zuuvJOPGB}{R$o;<;*9bgBS-6O4l~xtVjaD$WV3*SN!xu4Si;C}X*^=N;(r3|Vbjvl z`cI;vGMANWpz?t(2Y9gtG06#&*_Fn}hgDI7Lef~e=?}>FYW1keH<$5cl$g|yYvpX?0 z+tiN(J@N&=In$d4-S!q!xBm82*=%+@KGT$(Uq9owZ~Ouv`+L`P!ZE0`g@BalL*q#J zSqF><&9dY7$1|{ml~UwiO~+iWm&^)^1OuMQZTUn4(q0+9?c=(g)-;$ipV2Ga5vGEY zs75Vl)?4=c?)kkdsBi&uXIP?K^XfEN=YGfat0#!=#kD>E%tVI}L{TQ+2E~;pCM{Rn%iStJz1VUq%~s< zRQ|&CjrE5d>RSdFK#Xw`k{AdObTcI_<6Dk~41qFYhBQr%amC~V(e7ZxWSJ>nV2z!I#V zn+>vju%=)1Fp1{soBAzxrG;k)r3uoK1Va?d6;LrCW!f@ZsCo6ZT4sk!W?-(Lb5$`aQ(olh;Kk09>p!s+=zFe3GH)_gZ}f^+o?$@rzLA2itE=+O$rPKuaBJG z>+xe0$G&IXp8K3F#Toz+a|@4I`T|ok(u>=-NoXj`FZ?&R3ANvkI>`@wTjKbic6Th0 zb0SU9*zMNSe;BBt4jBA0MI0@Z{AxVbxFNkW5JmU}RrXg_La|)8XnRIrVBo)KWKstQ z*|L@Freo?$HCBzN)T+v#*pfb++uGCfN!4)Y2VDy9W&G4-FntGRQXBo#Cfqs)H}`0K z(WvqJYiE(yi}>$~x8IW}vL*iR_&mwV;-X;iO9}-(IPo9#LXLxV;Vs+p8Em{dBswT` zWl(u;m=i|BgSX+8nz{U+d~*?Dv2YCf?EJE3Q-B&ey#{#%27SNldpt$9c}{-xUO@0y zIGlST9`^m=9eEz|4Nq2V8?!T;3TZ6_oO`6c+#gIX6EeS^gNuCTt5zE9dc`4!{Zz=l zzcv<>T=BPx(;kH{)~Eqmi)~fPBu?U4CwuBglvqZq$YnBNVuA>+AO|trki0{zG)}0V zXjLAi{v_ou$pZzObHLT}`JAFSxm?WWmUtp8xQXOtNXB#UnCbd4zG7Spv@ zIFn>UHjv0L>9SUOr3AwtqB5L9-WaXXH5@s5Qh?SF45q%#+jI-G!T1>szem)98DRb} z=hjs7sXK&iIO}tBwtNSP>Qn2*;a_nI>nnWFRN2rI(+w#~#bXD4Sq?s5@W=SAcQ-VZ z<&M9WlI*IFFvS84U5luT`=f+0!YEo0h+&5{CxOcF1v>I35UY4zdO7+AhaSK>Q(wXx zdGMYEpc{MoIT(6>->a|@(-&lMcL68FK2hRV@g~P7H;9uk)a~AyO&yib34QOsqsvT= z$4_;{K_mJS<$Ryr3Q`4k_6?aLbF#XD4{~|N!xlU_SUV0VPwd6mAnL(X6mS$!lS4b)6C}NhN7G&MtCNaYJ+dh@)QahCVZ-?G5-~2YDwxrdRovG} zv7o`Hv(b1w-Dyb2UZACO^(!AHAF3qUiMm<>tyaTGZubS&g2$cr)IE(!?|^KmUylDv z-!v4&`#TwJt4yX(=eymoRaLuR$15GJ7N36(`tS?0d%=KDo)GvJ?-tG5I=(ToSXpRp zPQ5wJdm`m;li3pbvBw?~@J=xz6MtM|IMDW}k}C|s%5Sd!$+Atw!)UWaEqY#13R z2*}=vU599nn8YV^6vggu6kjZu!S<67n+KxXFc=P(MWoV@H3Ff@6dPHe(rhxDdGO%Q$@M1z+2O2llwnoaZij1sIAL%`w)r#lB=W-ogh%b2=|Q_Z zdHl-D{-pyzMMP&Mne%L4!$oC;lu*Cf3PkBk7I@w3Rl%}dGtpym%F_`kw;dt1R>Vcb zck$M+S12>%-aU-Bhw~pIFEDa7B*=5-?lvI~14`T0BH3`z=?7n^tV@8P4;u3zuxn*l zBMqgw&F+Tv#$COI!;-oL29arOTtnG8bB+u}g2h!#_f1}XW+5GNMX-#fCs`qKXBF8IVN7Q7&2xvAv%Hl8WDE&5CVHcaQx9YC$ z{w5tG=)OX0v%Gb}OG#K#{y2n0Afcd8eZ_EiBgUV`afDH-*v=KX!m(8AgcI3mqtsxH zg~jzf6b|zep+VTImhzbOaxNujG{f#LM6g{q`)E|_xl{GR+a$^Vhe`E7rANS;qTv>TKh!&jDl z=C$P4ySKOZwOEeswdLf(UJ`zLBxbJdTg!7Foh$F@YW-j+XhE<`5YGNUbAam~`~q*XwGW*B~rD z9)AN+KRMX1PrI#lola)0Gfqy|LAwI18rkk67_9cyGMI?8%Sev>Xb8&g_oRFskqLQ zAg=`9+xNQ20Q*|YIqcGy*iQiSMrt!Cq89pz?EZAgDt?Lw-Y(T_Q^%n-f<1W}E7~}0 z4NzGPH}U||x02P(Rrivc>~267?0Bhs80GbI(?J2tAM`Ki$0DyEX@H8^HhuYM^MoFj z7DDGH@q}XzlCOF06Qr|2JPqno$IJ}hIWw^{s>#M<>R0W7E8@*QNfGao$Ay4;&5FC<^E(y%74rC-G3)9fwTpPlO zyk9;;@yYajADYQk>V8E}s>7eD05UEt7Xork#f?6vikMqf*d#NjT~(A{i?MoV7SvM6 zPwgBL)$cWQ=a{#$Tp)(X`+k>~T&*oSFx|EjC9h$1SR#akFh*ag4{!A%@+a4;i`n0$ z*K^QK@0APqftafLbgjI}T#KAQI3X3!1a_N zC5=geJg;S;=OEx^#{x(-a6u>inaw6fO1~D%wi^w~b>Z||(7yK)sHfb{POdl^HvA>w z%_STtyV#s&owCVAvGtP84RN@a|8-EX$1pBJ0GvG8nqss1WOE7ZBj0;_Z!5hh3mO`l z5u$itCxu3XcnNcM>84;XS`uX2d3Nu^J?x-T7d~N!LM}8#7N-GpTK!$v{ujRwoq+b5 zc2Qy{Khqw+{^}0Oq{}K4RuL0NPRlU?pChX68^$sLlY`|?7@FUF4emDpha)wuqtTN^DHuS+BM}^XSGOzvCt|05VK> zapSSi*W1N~wmL3AWBlCjj=Q?#cyGM6`y{{7-A!EvG<_0f+F4R!lm|TS&y##_fXQ)% z+W|1d#syUIwC)EoYnAs0P?b^SQ8XI0BH>ezYi2tB0Rc7Jy_`EldChc{YH~SzM#&?y zA~9~)uN0x`(Te9>=QqofR$Gs{--Lw5dVb_kg;xK8J;rvm%jgprPEf~aM};MNxFzCh zH6SrOpm~>;^u?zG1tvk(SnnTq+|^u6Fu}z5>bP=A)XEA-@VeU$)A-ea1&-m5y8js> z{Y)9RZ-r72Xwo5FLV#Wc^Ros34j~;;7Q#aQZ1F#^XKdG-m@i1rn{z*>?w@PSA35ty z(4X1awjkJue+Ih#Br%0c)eYYBNjY&xDzk=01ZxDb7X0*sGdBoE3 zF770qFReglXZ^KG2gT;z=@-6AIb>ukmtwWJ)(Z_3Am~D3pu1dOv-$K}I0FeMv?jm5 zRPhBvIB~xH&s_i#gv?+t`_3uPWR4N~oSdKe`hFD@Q!R$NnX^Qrax%a~qY5`7f1lh7 zfP{nufZdg#ZLO|p_r8b598M&#;=Z`U#om#WG}aB|>M;ZzjY4+xAdaH^0OPGORfWvz z4X@TxnFsMt!1-UY=*lA7+$406IA8l@+{t|Ckv(}XNiI2(%T)FeX8tSRelr^BOZ;!q zW!2jxVA|LImC?CFXN|pp&%D`a9aT1&KX=TZJ$#@8W>v=}Bn~Y|VEGvYq78FzD3*n+RiZ|&W(*2*p z5H2tpAQKk}?d+78Z(UbuZB*y`@Dbc5;jaC z`iKE>V4?_t8-%U+?5PJl<)ARg5wgOSKm-f~ z`W7K|KPWHzC&tz#^tbW-OCe@|D3Xv1{=|W#f@BAegN*BfM;@}=>YCw&fkH?2{4?L_ ze|q0!0b=Sn%OvXl`{$*=r*eCccbVXxJsnru{jF#JzG6Y20_<}NhJ*Ck|AbKgNZ{?t zhF17Yz%K9Yi2FmY|0wuo0djryDKPxk{xF-r6g)nYoV&BD5MKVC_Wkc3I1BlL_-AQD zIS2erKmXl>(=;G(FpmzdQ=Gpm&HpsK2n`Ga%aOTW$7fwWt3#(Enc~+Qmuz z;;<(bC^Fa=&SAW#;YRll5^5TRA`NY7;fQE#6iT3eB^L&a2g=YsXNuCryXiD*1Om z@jnK=B)M6M0l>w@75Qr%?+=Ui1v|$DKHy%s&9YirqEvhcjSfp{YT|JC{v8zq1H&OO z8i0{Q___6QIp4MZud5?I?ekn1#3ji$SI93MOsnB6*5_S=xYhY0Nj((m*iWHC9m!(3 zu%^y@T6uL%n~aU_ov$qum+7B`iUr~l<+}-XMMz96)Pcn;r&^^$kH}_C*skj#3WGXC zuU4fUm(_YdbgTCf`{eUZashWv{Z1C~Riuge0Z!|GIlND|(}W1kamwY&*4b!w{2^@~wNf;j&Z+#|=#1^f-q7HBj?!X+Mwx%YtHJVUZ|KGsaW6 zM04{ET!xx+I&(>!hm(d|HhnNT1!wa62#Wcz5o=&hF@T>C(96qXT{;t3D0Egvz8bUe zpcC6k`-@Uw9w;g*I&MxeYP8!%Sg&;qLSeP&&}%zmjDyygZ5VL6@BxVMou~3nA$o2S z{!o+u6jq%G1uODUiYLK_X0d)v#{jt!aY*s3QfOyp70Hg0&)jZ7u!$R=iR0O+yB5kk zIW^3ds_1al*->6KGOSme@lO7?U$<`rRQmt}qa0f;*d*qEqUZHxNi%4OG+jGLTAN$v zbf07kF?3o@avM$7#?zK9HP(sV|A)PA{LgdS+HI^hwrwkfePXJ(?r5<-o(Jn&;78eNBA&m^7wL!nZq`0R*Hr2VzrV25LNiX=_^yh8NlYuqvF z#`9@uN(kTxa#UoEyEGv`qQbGTl*o40$6wP~CT505MkY3#I05*%kML87B+szeF@ymy z;;*et-yPFWS3P(mb1hIH#gXaEA|0$n!};YNc?Uv>hNM$>Zr%4re@dvmEGkq6VzmaB z{~q!D@;+S_&~s>@t??r}`J#J}l)%Eedq6fIx_MC*s>5B){f3u!`1bp|(ySvV=W5K) z?|snpHXagFJmpIcno?mW_hDzf|Jzj&_-)f%x$>w04fqOUZ_>^gK+Z+ zkur?|&YQ$!(wQ-MWRuZjNmZg(`T-=!`n|aYRC`_-9HLy#^d`XJU|}lgIa|vOkUL^I zxxAdV*e~?PJ}+(h8M|W#WRsK9*AKm31!6nkKen!vh1;ziZJB%f2WdEngOZ9sA#{2l zRffZ~GGkpE%8D*+e_Hvwi~QkKOChpWfrPZQWa;GhpX}x!lJQ~q_>e}^n#!wU3U ze#qyk3$eNH`Zs6uc(`_w8;K`<&VkqQVv$P}KrdG62)$C5wc+i6(>_~Q3q_$YRIXU` z&5w|H(;_-kXG&0}!A#@)=*7qT(|AO@UPBBTjZ&8romQuXvdZ2=T0DWAlCOv0dg39@ zaBf-Znm2-SL>r!E%y;OYudCIKUxv(KU$Kc?hqw?$RSv{qSD@Ut z#dfVwR0{G64JHJqV2Z_Wh^)%OqC)HKF2t}~pIQ+G_CoipHO1VeqkX}yQ>UI`ahc2H ziiiBKb^nBt(g=Q;jLBRv&UD5z4lT<|I(<_q8biPtKwg(ee*|HS-%>_|B83ch79lI~ zy9CP^=|V z8^Sf+6?aoHc8sHlO@fE|-n?q9_5mMpXXzXQuvQWM+Cj|@&1pgJ;HO`}T~3}v3!sgR zb`K1UkT~HKd8e@wu7b`+h_)dd- zqGxq|F$~pKgCTa6*80TH^|3t6a;jLhDNb9u#xE-w*a<+N2WN@~V(6(k-wQLXfinZ7<^?)JcTaFH5*Usrm2w>MLl zVm*s1I<;AUI!(B)z|Y75WCJ4##(VnHBn*yc3j^7@DK`iDY?A*K%x4egN+!Byq2X@C-$?hi7m{?Zqw8fk-i zd~^(f=@{+J-NYvC4rU~g6n_~ES*H^K&$Q}rBrHL1 zkwi&Amu+$8*BIdwN;bIi8W2hPaa&v!JPdvH5zdw1O4xk~` z9mbd2f>CesWr)|ASyRXjLjrLh{!d#@2J$a)-0DyH5SGzHd6Q(d#Obv)A&X5c+Clt4 zy-MMW&;)bCvBWcB^uvv+C@3V^2X~s|glvbWb~e#c#Y%EN@J=W{bSk3Nak02%1vu~S zu%5wTM<=Ht%8#9~5V&BJ--@ksgd)6{rs+VzQ)EBr=o|!3rYnSLm_rV+x1|^;JQ6tE6p^9g0(*R?ki7&hOk?%6^&`RlHwEr zdqX1~N96Dagd#4pRAh0mOa=@RA)M!o;4xMC1&a4~OArv>LUSKTlL#jRU8DUYH0EtY z@_q#8_ySJwLSHRmw|e#FFCsWrGs9=A0tJamY4%aW7NI!CLYb->0YXub6=vb(4P*mJ zYamDF0(bi~w>O|c&C-;9M}_HjuB`hDLzTLadKs3<#;v|tNBwa!vuw|)$`#OEy@iB^ zYrBwj^;v?^s%toGpKbWO#0jg#LZd~6Ioo77l*23sU)Ww9seikeWFgL<>|_V3J7^T< zeVJADl7Y2iVWsv-cOopC$20`Ol^?JetdTFy$?4o4%99yzpMkq49EBrXKau^sXb%x? zvT$2wk>Co;vD6ZIPHV@rFl(E?yY+A-3_72=Ik}o2GI=(|Nm5>!A4;Lmk&>u!FLc%= z=vDd0VFv``(?$y6ThP~7b%e2^E)#LKJrFi*oaGt3mRyHHRds5HmGdQ(Q{RCE4R=|PU?(FI;x;KhapimUKeRPzv^_ZKNM?y?I zJ3EoW5LA-YOar>o-d{`87kI84FlRnL3Lp<8)g4TZL5DLD6AJ<=c`guB!-aqP#(gN( zJNJ%_&smz8+sE7XHx;Y4E713W~OSsf$LCeqmvN0t(xRah3<+J9RQ0 zl?ruCA`Na#$;{W``!kMl(H(r+Z|m(sZ$AwZskK@q`2_?f?j@3FM@AFMA}*^94rrIE z_3}VLL5EZ4%t-*$c4vto=ygy=a%4VM2mJ13A zizQGd>H&BpJ&8QeXbhPF54|rWHV^23=iU74(8eqb0Uju8$Q8OB@DN z&;hMWW!Li>R%2OSG@qXh3=H}=Hgr5Q-ojG8zw6&F#1e_Xsx^LV(QJ2?bG>y|>wGZ_ z-|cTKSS%#$E?4Uc31qrGe@&#;tXBnObqH|#B~}_McI#wBLX(n`+Mn00gCWp}WpMEz z;oJZ^PEP$(EK%)qPTBQlYy6ECYqWfPe5^@H?(Am%oUir}5(_i#N;$bY?|krZ;9}4B zQR2=&nG|FO!b{;1Jl~EWj^;apg8hcEJY0*Gz*+v7q=v7SfCGhryaNXS+oWhb-N);M z?ZZQ25^>}tfQssHNF3Q-LQ>mEVMLkH`oN?w6K2HQ5 z#6`gV^-&SgE@h*G`#!~^(KYDRNQQ>6=XDAKoUq4r%(r#_bWp2KT86LV@f%)QA$Dxr zLp_kFD3prP7+r6Kbe+>exd!^g;53zF{qa9}!NsoXNr`5p?%7Qq#l@f7xDru@d z&sUhAzrB4quZ{ComZ(IlE%D`g%svj3o|9A479f|n?_lrX5EsRR31E~l%h+`p<%L4- zc>UCd#bN)p)c=QQ8F3hpC8okB$AEDDz}j4mbsEqD8Qfc=T$0hVtIp8B;Srtt#Zf;mRJXYE~j~0W9yUdqN zdTM?9cvv~7j?-9~u@cqU7 z1Vrg{PLi>AKm(uG8wn!e8yV{ENSuZu{?F%&0M(W)m7SQ)lLM-j>NC1;X@M4%mz0Mu znq&a^(BSsx^JmdgRuOPmY(g3hE;D6Kz%DQi0ZHsUDl5oWnyQVe`tLXY=XGyQ2iP<~ zJ!@-gyNJ^T*fW$`>`t+yQkY}SHaj(?J-igM_+9a|XI9FF(@%gdSQfUA0A0hx?|EJH zRP-i<{g!Lm{nufO_j@de=;-8JZk>c65Vu2LqTff~mpI@q%R@;%%D7%Y(Lni6yH!y_ zk~5#GC@R9Yqbed->o1fAe?pNHl;sQc4aJvFASLfukma``$me%$+4yGLD+5Vfk9<9G zL;Rj)2cTAYO!W*sI(}_@_dbx~8{qD68x?@N=cQZv>IAl?*E0#bLb#h+CNa5~o}NCA zz*S^%-Bh=Xc3;Br*$}HHzdDigPoXY}NVlaxf6wXVH1C!5Sk{4#t)IBKIHE&&!<#p% z5cROU!p!b;jzBQuCBI((hma^4UX7skWl&eC*&2h`fC>yIsUAckXm(T_56$iE?QoME z3z`}JFifrNooYc)oTNyZrjU9;0KZsJ)7F)vEXvNqtRhe@uPbiF@{CleRFPXnA@es$ z@PEZ_K8OU?U)ZDwA4DWdkm1GsA+7$x$o;Pgt-*mLc*zqeJ{pH7{yBISj|arFhyzn; ze*$ew_`E-Uc@7TvSvVwRLr2H;L{4On`iV(zF3RJw5GTU$0a> zP$~!R?a8x=jgKYu4f*l!u=}?ar1k!Qy1%BCK~i740jBA`m4sa84X^n<2>}xAt=yU9 z57+`77}G~~Rk<9PU)ovdN~X3s=i2kXcK+{HEkMvk7@#@}vE}~Lrx?GgupecY{`3u+ zrT{>sX(|(c^Cyfx#tT5&M&6^ee+t*ci2#64+$}f#Z01!h9qUiXi%C8D0 z0N1H3Bkj5WVVyR=0MZTC7}>QFe|XI`|M)eRADQF_e~M?4e7tvz4&$_bg#f%hpa9E} zP*JsY+o@dhad2?dG&Qxw77e%j72d~#0ERi*Be0Jwmdyw+DkhcWCwCQ_nka`Es8DWT zZC{-v?z6sqcBEeZus8ABU^nFf;Le)Dw3ZG4qUsP?u`|fY;(mUMGdBGJ+|ET!*u(HuT|Hej5-z7-w-umKKQ{P~AyQ`) zr0#0`Z57#pqRmi81s%wt*}{v~-gP-BFE8)0WtzMEq@E7*H;;=q1};$MRdxEz=NhF! z2caJaxnZ&x+m`!>Q+m z5IEb=IY~D~wgbYgfeu%7iZ@%8u%?*<`%IK0vPw?!9KQ2w!=UQ0H2f;sJJ4A+Jmy=F zrFv7p{Gx(wk9st(_YbBkoqqOhue3IHb}YT!Eu^^~`T6<7*=SEAy)Jh7`~#!ceNhSp z@_Up4+G$-nC^XyK`nzWfI`QTlP6_7^T;w%1HHdaE$)HNb#FLY3@1I7!aNd8TU-(c( z@Rp#!Lj!Zc=v^o%rXl4MP6&@?fZlAoFVJYWo(iH?sY$a+JekZ>9Zt_rHV zwZtMkGuzJ;q0$meZ@CJ7sXY(wCn=C{_HN_!lu|0u8X3E-f!unAfuEbsGcv+x+Kv{) z<|1&QbRC_+>C4Z(D1kl>g2$kL%JQg1h=G%ab+nc6gSPcLi6&&*U=I|df*U@q_-^}! zL~B|3VJN*cw5Ti&!>`hQQq@b*Q_D+mJ8q^xI6DZ#Kq_3o;_l7)(dH58Xogvlw49yX zEjKRTE`!%)H`T1x)~VdR?be$(S9Q(70i;0Xx%8{#5R!t?@M=b~N^m(3I$d28Hj*(U z<64?ZqNNmNiszjQpYMcZKDMN`n_#RwL%v*flFc?qyXJKz+d>lPXWRhAcq6x+{)rJ> z?6NSJ^E6YtnEe2mp_`u;eR5*m?&eCD(PWjCq;ppoVe#=!U(cfR(7pIw3pua**j=)f zs_iS$3|&=y9wWhEq)ef1`)@3f2+z#H7H>Vzt6Vp9=J67DXC+M$xBXg_ka%tlQY_h6 zfnKM8s;IF~W&31Y8(~nKk`&(U8}Zl(h!Rf|Je*-Yoeyx$aHw&vIH{kpFYxMR$Z+v=ICBrO1C@}E)@qZ;v~$c6FHTL|V%f0Db-uV& z-S%L6s@aJ)ZeIW`1dPo!0LQ$=QRbmtFkB=RmF2AldTBUqdNwj!NyJNPdMw^*;k|=r zHMf>3PM)nat_^tVblA3>F4icuu1`-0!hTz6RQo!SOnUQ{PJ-vb&d=G=UX5dz81lhd z(B4-V#E@VapNOard%+~YLW6l^Rc)r=Ax{P%o9rsVs~gRBu)7-`4_-gymgG|m8X+$` zvtxF2#Ad2Msd-p4K29wrA(bF>b0BUq_xbAbGF}MgK&*?*b!0I_p+IPCJ~lqyuyPM@ zRQ@Fi)f@}OdVBV;s^Q6fzJ0nYTe{x@QUZyjf*adu?MeC6oT?77)}?0T?-#WBQtbCK zy?FP7aZy9P8({_Pz~Uto#M%?la3Yhn1a*^ijL<-G(anQZj*``|cgxjdnfZvrCeOKt z586-Q0U~|Ah6;A{%sO%Q@fl>h7pqHT_$7G9dijeW+Nj8TosM!Z&>LL3g|Ud+68QJ* zouc>A4Gx2R8x*<_zr1^(-s>C~Q8KKd8vJZZT+WBFuNeMrpj)1AKOgnCESDgSuV7h& zTW#47n<}nvqp${s*5vr<2RF7pM}mZLY>_{;asWhv9$H4y_Aa#04QvC6ds|*UdVK3? z?$l`2;ru)pVf|60&!%ekjIg5gdF1;~MVF5rlR><>+kC7~N`rWPEqC^g(f2KcBqH}Y*zuhPk*LtdJfu~cPALHQyg2?5>f#H>@EYj{UffZ<^Mm5BezHv}meVw26i z>08%$pr`}cc^NPt>EQit6xJ>iXBo8ikkie^nhoLy0=@M{la%`VYHC00>JJww5a&#$6M88% zY~rnbdJn$I*$8L?fcH!Qd|mn#;&~`Hg$~^k8G`fRj`bwoDcaWhh5&Ud<`fIHQ>V@j zm$S(@yO`f%?vy8`>YMjHCHUcU2W)s|X1&<{MQ|d>P#l3e+i1imfMV4{W0mqKa(z`u zXC!g{a5P)nreT(AQI~qPhU@l*rmN*fmmVz7W=DI64On|pksunw5wIst6o+Xt@B^FF z`zOh?FKRC!zyL8P`n+X!?$OvxOlUei-vC096Tx6(NwkS6Gex8KXDfiKLOeWP!>cc+ zw!vzhE-YuH2mE%1@{G<;dHTmzR%T_B8EvTheH#JSo+*)rKTFS7X_1`6Bb^dxgF*yl z+DW_0AE0cW5WpU${VgO3;dK=*>?b82Y2oUY|4hWf+H}*18;5Hxh}VC7eff+Tf`r`y z3JH16dJsJK&N()WXjOjxw%*hL+T#)`CK-nnM3flrkF1W)z~<}=Mb7Iy=KmB zvN|qDiRwSFCrj0NSW^wETRE%k9Ikxw9GN*1qs`Js-WCE!g5FJaQ`ex3H8XZ^KRTOR zhyJM9encI7*so}ivK({_2Abee)$flin(h+j6m?D1)amkdw{`N+B>G`Du8#3O#hr4k zh}D9`AH+Vl;MJ+EedNev zv556~qiM6h6BPDhJ+aSxEC;mZ>!G}h)ui-eyIbGP^1-_SXMbRe^{|&~eZVSACcCtQ z^)r98Nz4i>n89?yrju0_Rn>a=4a(BTn(VnC3B$=jau-kppscl(>0qWDKGmL!HR=oI zA^42!veztK>{j2b*hGp??@gkezSch|SojI5Y&z*Ef!i6}irNF+I_GGDZomZ{?SI;r zxc?+GV_a3z`D(#VxuM(eX1Cgf*IWkjlS=vic~eWZ9i*2p5BiJsD5^~md9^jK8__+n z!N&)$Hev1bR;fxWRlZ($fRy1kt6Z>7kL;Ry|GF-)8}r92p4vUtSn5HCPI&2ke$GR1 z2v0Ub%Jcrv&QKJxxG(uU>m%GMN&`2NNz&*m$bx<^H=1sy@^dPR!KR$fH7}4?Tk7LZ zETB0__>P=`#ojXKrn+w#ryQBYDqGqYgrPetnNky7Kt`)z7gxp)jL2v&Sn_VLgp;W4 z#&%B)F^<8T(}#gRtoJzUC1eO5aj}U6bUPa{!Y1ziPLl{bD=>0-uS4&ny4ElIJ*1D2 zIxkt6A$blk*x0q!JY+}T>)`x-O8_G1-&}4(j0e`%1h^j$EifKlS%PVc@hL{c5YZL+ zgd3Bl5#RBP^46mm~e&#m703LXrpq5uogCe4|(HxC6Eb+!~Fu z@QZNHem#VK8rx(5+a9@Iqc?_xyumkPC0N_6Bt_Zdbq`gGm{&SAM;NT4fI2qJEKBTB zyj^Kc?6M!KV+f?b4YYfHdl5avV~3)r+RWN%DIFl>-%`~NEnj%l@BRMhI6K_YUq7b< zvZYyOnlHLr6Gs}rn<(lVDe=&xi*!q6Lp~WSecl!}c1)o5DjDq21fe1eJ$}-HJ0&x|I|)0n48_2=5UuVUVw047QWm0J63# zN?QcR5t)XZ>GoMz>XG+D07AcIIUeyCf=f1kL?^_u$i=L<6R=eBup@JtV+AVb1dswSOl8o{?qxr>B0w^v@`Z##%O{w-Ew$0Yoftm zBelC1k6%zXHas7Qj@I+g1pP9ubssE?vo1S7zUs$^Fgq7oS-Bj%$`tf9XzdQ?_=)md z#x88&5SYu5coR%SYVA*Jflvd8%Vc5>@TeqiZ`^}g4& z8edboR`rT|v|J4CSG%Heu4M8f>+0DEseUuAdT4{E1J#-b$Cf<49$5eFXZP45oRj+_ni~V$HDpy<^twSm17H zV+Z(CFLuGC<^-aaC&6CHOR(!K6!duc&$Ze@XrNDXEKY*H7-Ru><(pc`PnFbFCjXv%hfrlpl0jKVOQ1S3S`1KhPk>QiPOS-bM@ZC}h}AJQ)S6bpSHZ;m>L&-1bSAq2 z=5@u^UPH{zD_d_9RyWjr&y{%)jmWaMD?FIS8^guqswFgQr@Meu9WYkl^gD$p%1^xB z*D!q+{CW?qGjk)9-Z`}MaC*IKi7SuCF%=>vqQ3ANAZSup5n^A|o*CCqI6OLE<*~j! zPw`Z~c&#+fUAS_i-eG=|Qn|Pr>U$hl;p?0AYu7T8zbF(YpFWuz`q2tXSzL^6&8$!~ zk0fn|{WNK10}t_H=W$*EZ+rf38Nmp5l}LBd=T2|s1j(vGq{)K=TUN33M9;Ie^ukoZq$bDc_REVy+f?>z^;6ja zAIf5u&7&id5U3FN52r>;U11Tbg$kvJ*!rI1%+W0uQH3bQ!Opr<+Af{%NVP=zWr}cB z6na}ss3Y;29S%~;2)nPbWE~TKA(r5Z}FJ`aaVlOT{7G)cOzo#pbb5*;IZe=HHh7P zAk&cG@GmX15V-0k5*3hzC@W@03pit(efAn&X_8dwgHf-jpVY|uu1fL-Xe@O&@H2;Z z$>x#T=Ixwn#`T6v#cJtiCs|vS^R59Z5LO?Iq|lckt+bEDa&!z%S*$ z`uG+(_YUBcu=Jx}YbcyT!Y6q1yIvNi5`k{iX3=|UrBtek2J;rB0)2rEfO5*76r_O! zF-!eo`{4*&T~W@0VUk4G!ELc4B46$teV!lyA9F}LjSe@e1VOJ7F@(QgkT~>%XSi{; zDT3K?v(<+_*GJ_ac64lK0$@Qh2SS938*lY3Qv5Z2nDEKBYK^cDNi2mH0?=aZjAI_C ztuwtE41~K~DoP1~O6^k4?}DfPdTnt7jGyOyVYjj)aX15ct*Wz5nFyv zkmOWQPp~3l6L1aL!R|>!20TeN2Wz=j2QU*NshCJULI1#Dc480qjC59$F!kYlxGNpr ztneai4gnESN4M8PqILFzQ!g#NdfDHyr7G+`KLB+=m-58IZoTioE*p6tG>wrj+;)TR zzMPfJ54Oz418Ws1G2p3^;6NBpuFUaS3LCks+3<#K-gVJxQtCFeFe?{=AjDQ4L3nI^ zAjNms>T_w^1x@D~Yxwb2t{9M>Y3I4Jej<#VjrBOi{RrLi;;@B2B{hF#`^p1LDG3V5xOg`faIl($Ph7NXIs@uOR)jYU7Pb!-3PM)2S`R@U-F}kh^MM0VofDD^eQ+6bU4{P6F3 z<11`s>cm}uqiGbvWcY@@^sKvTK9zW_sPio>;+}kvJ3&QuJdMaeT<3cd0)eFP%9-~= zWjlrVen^?VfIHFlZKkGEg)Td@ze1-X+Kpai?a>e}*FoFQIrk2;Ub&yC`MwYwWmoU) zIngj5*(G;hxc*~GgF@yRqNlco@$%|Zxek*Q{ukRI_qzdE1#Fvz$+PQ&bPV}2X)c%Y z3kFdu@i-034h7E?PVIGPqwL$$*XGkz1cEHj+sswR1Fq|yX*#Km#Js9Ic6$|w1_yIx z%ox2NQ2>cCs0jUP27W=mz=*vxgsicZsxERM6Ap*b{C4i!uz9`dw_4dl#?Sa8?9^6<zC~Oh1 zh8&lhsCK4WmZ%^gz%(L2J#_$M3!Z3CIaE+Q!#7-B-$KrQsTWA~N#b0imwa1w9RDWK zj63(@Ao2n0ec`quXT5dwZH8!KcW;V2v1 zwGnzy*#?qt3MX&gc>2;Uqs}N3Ug|nZ1LpPU-aoOV?vYeE8BJtxluLAP8X1U&@F$d` zUv=*y2$xnrTx3lus($)(QOc6?b$}_tS9bNqYlmP6z_}y=i=VoKpG$H*rO2ni8x_?0!@f zPVGL{Sh@D?f;97yhsC+X)rw7YMV%6uP82jeOXKR6OOZx2ZYD|b5+hBMC82<_hUiY=#NG^@@EC;OM!e4yC%6c=LaTj;*b_I` zH0vEEMs}_bpw>y@Su6+P5kicL0Q=f7;E~#tIkbs9^0Q8z#j&KG0 zkAv?;hA)e9&XLmlT?mwvCdY!Vn8)8{t%$jvIcdA3Q+3xf3MVv@ZH{k>{#tX3?oJT@ zOLd)N&m=K$`n;Yq>F;^uxkWB3eDL7)cn;?cCYs=81o?VGav2U|atc|zeNA(ZwKQf$ zZC?(RgL|wQ9n8gWR-jlbS=?JR?+M%A7%$*Fn%R~|Uf#~~Y16-=px0aHu%ELFYxM9B znMcT(vw-g75$&g%Ewb*e@tdpQ-RPWoZ-_ZC-(k9IXUBV z=4dBdz&^DwCUBL(_m<^lxo{-~P{wyHm6thge&QfuabApSi%?BJ$)g!u4JiF*0j3HS z_<(p&58UGqC(WO!iBMJ1jvM3D>%U`hD4H~PxR9a;k>HzhWQ^I&a>^2jJj>5?!VZw6 zIUZk9fvolD(uNvwhYv%ge?GhR%<*W$d|N!OODM6+9=c;Q`7X?}y1=5sIbt3~=c==2 zDv;nC8l1>#aWcIc*jgq+?RYH$c^^hY3&T(Scs>H)(0R#5P~@@cqO=!;3*OeN+y%eyF6t>gba%Rg?GlT zKH|-UCv`HLFz2mU2rukQ7LX5LZ1@`XZ5N$7@^>H#Bn%9U;)-Z|+)P|iDQjkr6SfX! z5)zDya0Tf#P|ds{eS^p}8JQ{u1@CdGZ=R<40%z6uFKrxmk`~f8RKHg`)q%<4W{7RQ z>H&ZxXEv6~Q1>`ns8>y8h`Fm|2RU-p%Up=%{c-HE2^w;OTa{WT*Vz!RJX!~O#ufHh zwamcKycN0(<#8^Dj$%d;p+d!aa1q{gGjcn44&g55T~jQgp?&qi9UxJfIkBYt&``mC zMToUkm9M3396~m&J0A!5)s`NF{Tz$FbZz* zs4o%gp5&u3gIJhjCFriag?mmb2Y`%8dyKK=o>^3}tvH>yR)q2X#xe1^<_9o{*<5}o z$r4{)0G$zcm)RGT#v{F?lm51R0H$!f%dfKe3la4)D0Ym74vG-5DY2|P;5J3}*x1sx z5d5|5d~iA7hq+|D2v=(dTfwLPqXr>yUDo#BDF~XM0XzV-@JKX3!9E?%XOwSU6plAX z7zJVSp!dtV@&FZ8Di~bTj%x)MXMJ+ZXLo2;$x8pZz!@tPDo1sLy z#>Qlkume~~vH)_+mH||3d^#n&-0!r3|K*K$KO#*BxHz$(K~?O!y`ky!qYO~u`zzk^ zfdUIlylxWWV$l7w6`p(nbWT zGE(JFtkM<&08wg{(@Z-4H`d0#){-_IFd!J?A*#Q6;Quaj_A0=2Wi4@;$o|CNLi^R; z5>U63@Kc0axAd_0`yXG7Da-*+Sx7beEAU=Dd{ZfF)eSB)sq5Fa)2SN^Zc^l^xF3+lq-dH z>ZMsOwX;t4{HIw!x};drkvZHLUW1l!MkXC&vk+EO~w0EZ5t=RMN=b}j5!Ce_uINt=(k}mJ=kGBIeC5=teXe!-$q-B z5HWXYK{$f4ri>20wb^+2)_;76(|ceUH8+lW7@~Fh_Rs*Gh=m0jQq^X+{@B~OxFclW z?{=a(?DlhPndk@pqQ;Y!Mn^TR%RhR)#KC?si;xA4p?k-?v>argbW{Y6m9OJj-WF+{ zSjvcT%pmp|gKkix5;mNSEt3k`^tXPpjS!s)6xelSx1WEht2G);6}V=={=w>B9R$%j z7Kc+NwgZnhvtLy)v@7$%Ia?d*-gN3MD3}q?j>2ign{N!%jvG9WaE3InrvkFb!^e=VT5{>jDksEIq-3WA)_p;N+dEb)rbC=(@V`u5i$0& zFzcj@6J#7X!Fu5d3SOJhD0#JS1b*(TB!j!Ux?-;c5z#&84Ml7S*y=ESB zarr9n;ao6xD#P3c(nS;aW(ap>`9u4C=Q{F~C9)g|Yz6v43xk9ZzF%|wpoJYO5ce*s zVB+8E1~i{r9{RAb92AC!-bYs>iIB0>}KBJp|`0E^(4-9;&^-tAaP*+dNwMNvl|O zQ;QAWw&~Ieo`;nN>8(@;6i|DC&e*@Te49EW1D4(~_7WQP*r-6MsN1~QC+|Z$i*d8D za@e1<-)lV!lGue<<-vZiv8zoQijjC~+* zTak4I(YXydx6qTGWFOkD{|H%b6;v6l*i?bMBbbq~De@(fI)PO6-gM4^$0>@ zW!A!--FezE<1ige-HmNb@G*+=4k7Fw5p&bBc`R$&5z`8FYP73F?$2)phqL=Q8Kp>S zy2zID@rD43@hdi6Q3t2-N}^h_Gu#Y6YIgzH*Knr?wB+Mgq@E6}%n7~QgIyCcpyViM z^2(puKo5=s?Y9js9>>ZqhA-e^lPGe+G)q)Yo*h4e`IcFJXCne$UgEL5B~iTAc8<{) zSdrp@0=f7Gtlsq%*Ii7ZE?rG_A=PqGK&Y`H2APxk#=Po;(-*?Ni9VHKs$j)t1giN- z>sij}jp(-HF$tErFRKvt;<3Vh?}-2^V~kINSt_%s^(JmdVJYWIL7?eO%Q^Rv-*eSE z9$m&UsD1+HuL~fEr}CnnR7q4|?w5#pdgZWZ8w!8+s&S{8#te-F=c8L&B!6sa^D%XF z+Yzs|cB?OTj*et=Zebq=Fb44HR%K_>y}--vBj^WHu%g6y%0@ipwcd>Ok>0DT`y4swZ-;2?8UT1VowP@NOsGVCbZxaS?S zz)IAZm50X2*#u>pfdE!sN<1@jya*>tJl_@{l);VB|hzUQJLk6@DGV8c+c6Ygn zctFCA)Z%S-tH3ceu(=(V6*919>%1x|5D-%mNbSR!kWKWtPt>pwV0JJ#q#>}}dN)c+ zNF}uaczR7eQWYGzoQk|ZVeFhC<%sZBwRFDXk7jfGSL;0-jEjKP#BAQlrom&Z)rGX2 z^Bfwm6B_-X{~Mk37fN9Ck*C3DCCJ?rLB4%Pr`nIw)KcOORPdO@gt)AGa)4&o_VK;5 zwe!8frhWIhX~g&r31CBxakRiTRRT_!YxeR$^FwD%OM4)9v#b8ns|+byZ0)lmgNYAU z_qH*4ssSo(rOMuq-ywa@R1>(d<9r!?4R;Pn2*!Da8W^Rfe(r@-0Q*fZ82WcKmF|<_ zz_V{O=z-lBFnL}~KJA|ngwsRkJRc968wzMz>Tx6Oc^3ygZzikHlP^k85@O~$R2rNB z+Z2HD@4v5qDgi??iGa+|$YSmR-1>QfUH$Zi4H}XKzgD9!GmZh=3ml9|+dS2fYz4}H z?=zCmRbRF5#~AF~H(^^@VFR!`>{YJCnK$PZ?-bWGv*3ClktN%Jdh?~9CV|j_8}f}- zz#80VlQ@0-yCT|pVXMVu1$gDzNFp3vGXDrJ^6V11k7PeW8$o;>i|i=J>nnWIlF4un9X#aRZCK^i6x(HIHXPwg&qZEA$CiVhC9Phv(; zEH%LFn+3RZiunZHMh8Wz{>@vdh*v}}iqG`0&4|5zCYRotUkF5FhAj{x6W2% z|IWOtGMkSc$QW4xD1XuGppeL4jo(CV9QQqGHgHIv;UDJm&v^t6Y$W6@$-#+HuNp{s1Db%&WDo-Q27O<_y zgIXj=uqLL7n<@Xklbv%D+g-O@OI|zVBCM4<3TS%QU>5ABdx50=ir^J(bC0HUt&WGYOS)OZIHkcT~==q@vPr`rt)`4_i@M zg1WSfUPLu~*1$f#JC)GOs-n;+VuGI_EG(T8EtDp?Dp!i*+KZ_5&H-(0>KC2^x~_TNdBgaj<`aX z4iTv0Fk3WMfc{-o14%4?zUp?QLwmN2k#R~&)EQ8^2~3afQMUP|cK_H$4E-1Am`_e# zb?R>}ogHFM)s=f>f5&$5pgsx`izGG=cB>2;N!2yEPvbj^|AVb8;SeeO1cMFdI2uuJc{f*8Ro0RL-z2mi(m>a(3*g3ySw{Y>b;*i>>v zN4KUbk*cTZV;!g>IEXS5G8^wtK(3A+68)Sw_i6~p5c0G&`Czl!{>X7LKPi(5d=t#ASPIfqkljaF(4L)&LV=@jekDR_LN#z zl?5ZWfd7TPME2_f!JXdTBswyij(9wZ1(*!UpeGZ z-sMS^JhC5OX3r++*Cup%s(;zb+cpW)t%)wN{l&z#vo^tgC)e{Xhgeme#`X! z>cbF2Vzpap$rp)t^NKrft-i#1OMgYN zh5Ca)uMG?(TE1ZtRzOB{!m~?k z5=HieALav(^md%@lE<)g%lLz(cAxaLXyeP`%mq`#wY8XD;G9qFclG3id@VIX7Ex4diPkilqq!rnQAKkKWBjm)5jJ;x8-97d8!yI?3`I|?!CZ9|~!{_YV3f#R| z1zxu(n0@yh-Ag5Q_&zo#2lG^~=8?-U%!;?K@aE{#G)C#cqSFfa=mXUio0~__Qusyy z8xdS^z?`XJE)VYB=&Noe(F>-a=)R^!y#-zUHI2n4ZKSJYe4ptrHP2;TK?}5Qv8ufL zeqjj=036kZB30sEbZ8#yRS|FG`Z4i}5qDpC{5FaujlMEYGLh%7xE5_yG`V2xcZ|y7 zYkF>%1q(a(XfH?IF-W6S)9&Uf@0@s9e@7TN4So(Bb`k?_*l8B@8-?ny`-n6oy!`fo zUR7odyvxMHR{ndNi8Y)4sWojuMp*tpn{V_6Bv+oVUAV^=U@KO2bzs&d=*hn8wI5Gt z>IZ=+dW%%WQ{UuE2=ROwHI``NHuaBp?0@U#;w%q z+Xl_;1;g_$!&^TXyJ=cx)hh9Pjy3coB=F3TB-{o+(62Mu#`^#~lqzh4$`B?t0e;INVS)4O z+5S`^u7gd~;r?44g$xj;B)h8rjEPZMw9vlQHN?a7(3@_A3JUR)x#oZqWY>y6x`Gtgg3XiUZ*-p zjvc#34j*Djj^1p^&w8gl&w-upbpZGqaf8$`-d2LKG^>GNqf$97&vIX3sr`sSun6lH=d z<0|IM@&L=IcpG|B8U7%`!Ph?t;(miGvSr{A^*~%%mTot$;!tMJWVKrP__4z z4&gRM$ZOzRJtfI;tJBOVLSOtGPY_(6FOx29F;P>Hwdf2wzn%IQMamd~f`If8=}BeZ zHp_%XP+i=psHaFGwK+&$08YF#{Hj8EVnVy4Y%8B6lib$l$}Se@>+*1O@LBPYv}9;EL6g_ZuTOp#Ulx)Lf#g?S_2InUTix4BCfllUlqgf%CN(3>2EN} z7yY02wvFvnM-7V-0Wefs={Tq<(?fCq`OoA3dKF$Y&bCP%_Y|((KLNmGiXTCEbwpGI z(x0&7M-cB&(I=D7RQ?YVJz^j&>+FFdK-}-3E$9&j89`X~HV~v7@h4#U|Ewz4G`A9y zvW!5)g^$L@1_2oW=-(UU=latC;^xK$B%h>M3KpRz$cd9qB}HwXoTXPK|C!?-p|8K_ z)wH34^^GmuGLq!0@6(k*K?&|AD;Oe-L&%+fazFp$ljXb+F65OA2Bi=I{MDb6w-SKG zcH)1sY%7$eXqB7$rOlr%Qh?-lsN!Rz1OGu9{Ofr(K$1$?|L@NK{}uwpca1crrS9zd zT*9R#sZozk_Q;yNZyml?9{F)~VEOv8k_yN4VEs>|V)eOA!lX>Vq3>MAeXY>=VS6_L zmeDwAcX{r4)6KxdCP~EJ%U~3jfbqXUzrT-ENl?;dewEv1IjyYDsd(Ws^k3A^aQKq&4(LIxzXX8>Z`7ZD0J*br0 zH5+x<3HiZ8b@FtcA|@MDk{h|VLBT^L#M{p)8RQ0x3-mShVloaEA+_IjC>foQl89_I zrnoUbq*Y=VJxh{H%8tezfS*iuI40M^4ZYp$_*C`IVS-@a(^>6pu!|>J%xaT-7Vm0Z z0Uf|awq3uPa4vu78pLdUabDjDU=|~;+syx_ldrH?T?`k*cX@tzU|;c3rWp5389fOP8UuFt&r!p)U-Ff;k1HCd#@Sf zQQfPpv@JR|?P949W2H&lrs15Dz|*_)&D99Q2@eJ^d(8@4YDs^^O zhk;^fXglckS3ywjT$^o0)B@%FWFrA>c_7Me!Pf!_FK$NVSpr#AB-s;lbuEIX<^`>{ zI^LK6p2M;Dt7IrqkJp zraUkjVl~NEA8QnF)Nel1*fl;pH!7(g$Dr@(7V5qg;P(mSWw!~uZRV2R)dt^rL^3Ex zEep&Jh~kHo@bo;lcmC41*N?I^-JVQH&EZ?68$+2l2;y3kVG8$nuaM-3gFn}S9vZNVD5-Y zjkYXa`i1&12D?wqdwKYAMT8IKU(x+9#t`}Z5JDPbzMTlCn|ALF8rwr!T_u)VYauz( z9BcKsLRqwNjZQlFCZu`Sz$m)SPi|%q+&}CfH{;K(-90gVjfAOmJl_+`8&?bJjY-))m%mAd_+1ycV5`%N#TAHa;#hcCxx}4Wt zXz5vR*4F9J)$uro?*7r>6;of%O%F0auw zL5Q$>8{cv`1SX1K)E0&bOeAFZ^fIzSoJ(l+J|_KmP7@B3rha&DPTR&zbAmNBXb;8t zg5F3HI{r+M<8635!J+0z?S3FtfcWVXz3=-Y1;0*O!+To13sg~xSACjvQ&j#Mwx!gz zo9T9b-?P+DkD)K<5iB!oF}f^Kl~}dc7%RVJ;Od=I#I%LF?sm?qY5^b=@vUX;KOODM=sE$kbe^I5=P^x_Twlv#;ASlFvk#w5}iBBk=;ea67GD2rr4 zG7>E055Zo?E1c-&aeilRY|bFMtlrj627czOGN@fo2i%k{x?~SCCItM>9GHlI-`B zm~QyF-0S<;n!?JX*dqa)J#jqN$}H1&6+uo>6iqQ@v)E?Jy)tizmxXpq>d40Pugvk& zND1~v?2f)#b~7agR=+k!x`Pf9LuH$mvJLW4{}8NC`kFPZL#(o)R3Mp1>+AgcSW_qV z2gm@4#RYKp4gdNUTx#@nz!MSvTyawi=U{5XYyw@_J08c~7K9J8)8SBgwweWw^(5e> zA6~?00vEu~1pJmUZq~jYut|jtQWbwIdL6pk=w#c<>#jBfiUd^nB0x+gMFM{qNCM3w zcloeB{RQZorAiRFcm+s?MJ?T_1~P5>akWK<1@00xvm#xylZ4gh|F&!aB{Na{q-F8A zweG`75rR;gwXcyJqTqb$$!cevgU!*wpwu=~ZuYsmj368Yk*d$F=<AdZIPL1W zVP=!B4vL>oW;1tK28P5Q6?uqoKB3(ma~`0WVMxR>A4UPdSh+Hj9U;RIxOZ#HN)LIF z$qS?qImhdF!t5VfxW1NOHNL}-4f8j;bop_TjA5^IBOeCQ$%o*%B(pZ&5nRSIM<|KG zQ+WoagQ+DSDP7g(4!(Ll-?xqv9aI#1k6ix-jQ!rqb&V@e=!>FK?k73?SJsDtP6(K# zS8Ym`(O(kRmvIMBr==S2O159GozOK9b!|oKSV+x$&zXs4!ax6)0MM6rKOrxsv^7zm zmA3YySh>W8LJ3K}((2XaoIx%$3bJicc+Kb>r3NB&h#>ACEmgu#tM-T6+CjO#x0x`? zh}j1OVajIL69S*h3Eaxl?@jQzo>VlFD9gC%2$$6qxIYB*LD9ZwQ?9c`RZ$gWAHTFZ zMO%rlJa)jj&T&HnqqOM{^^b|psg7>0J&D&Py7mVfX3;x3`4?5fDl1yrBE~R#^%%s(ukK@tNBgm{^d;7?f~V^yS&#Q2T0N9 zA|Yd9_UC)xaMUXAeJKx6K)^lLj4DXn?>q7(SCAQs4o|qJnO?9o!KnDOqS1Sl6)_!^ zPT@tZjTj$(duwNRIBMcUwDcS{OnyJ?JZ}Z>YG?i8Kx@Lse=T#3{RP)$cF0)~omtTs z898Pvh+bOC%ANdjS8zvPC@?U70{8H6{S9@@1KlWs7{YuQ8;h9fbd;4e&ot@}JY7N+ z5dFUW6Y&mUYL|#QZ4JkcZ@cbvX3pXZ;|P>c)*laa8M`bR=cOm>jlfsgJl5HcURR+} zgQZBkP<5RN2+{}T_J1CZL3vOlVumNau5d1i=K2t27txUImQx^=HinJZSjWX`PVb^3 zxaDWu9O(DPcDn1!T^V@XiXPGSy^_gE3k1tiC7WjcogY{?a~CV&8^_}sNEL-c?wau_Ed!ca`$gc9$N ze|kwO{#z#vI5`n5=)xy25>(b=K~f}(-1aW0FhLqw%^TbZW^p6DxsBzTP`7btG?IA5 zMn}>UqgSVUl6jnX5?eveWBHN~ZnoS9)P_FC6VGzPhwJRb)yQJp)(`b3o&OFK(n3jF zLj_1-32HXkoEOrr=PG`zxGa2;9s_aa@i3ZIO!c~#%4#9d1^p5Z-xkHK!nV*Y1z{e$ zYd$@h_kzcNI6d7bxu!Lig3H0q=)Z}5>Amr4zJJEFpd+&^Z$o&)bgb|&a!0$%eBiq1 zxg6Xc3QDiAHPq>8hBMD>yH0+D=;vJnVfv09uT^Y~$XxVpox9OC9}Dt~e@KSm&7koU z609-AXqr^KGW8}>#=HN1K0O^-0SdLNa@IGAuznbZz&*4*Uc_l|CVW!e1q_bNb?B`J z+X_zyp2$w1*P%MKbh(-y#}NRktEFs;{YOTAJ34}qEuuaA)*LYKPk#B zF~;7R)AiksbK4x1&1!cJUWwG;u}uQTf9R=9glNf4$~h0})z=n8G+0T(&T<^mDwbYm zIC^D2?9aY#3k`2lrj3d?&t^yJw%A1(og~Nxy0QE9uP#TU@F$_!Xs#rV6L2kmtfInk z`!uVThZLbAf{T#qwpO(ST@##)VTP#-G3~9?ezxji&lC_!9`8zo7IWVs#QLXZ(a*HV zN$IR+7tY(@~SI9-iH7KqdXq=~Zzx4s$6i{FaKg=;BcOipuiHNlF3mY?blzTkM?b5H_z5c!ls@8GMbeIA4#_C^IG*R{1J~etIJ?lu zK#85t0F_v52MUKa-VqG11`aUc2n%v6Q?)9Y8Umi8(FzDn>e-_F;Ih|W>IQ(+2oUNw1`gI^TtN*KuxMX9?J zTm5cR5m5+HQhO19Cyn!3hxGM(W(xe+H}$-!=1LM0RLsq|LB<~G&(=Ii`v98D4p0Uf zm5_Q(nt~{Pt=Pn%0ik8MkNmX}+oWVbWBh)OGr@%bmT^6-Ig?yTx-@VcSURhNXiNl^ zFh?}1S#TO$ph{IyGcs>6^zS}$1WKNwP-^Shj5-CVIcTDs?3UF%(|S|$^-VJm$Xa?y zFO6m07U_s*E{Ot)bYt3|>SGQI%bQ^D(r}W>&I}hvYuiLW)ZWhofn(~kiV2NyaHMoS zDg_xFJRZiD@y!y;HaEcW(@zSIb;EMdKDt{opLd-UnnQ`XQ$SZX?7eI8z*cJAuP1J#0p!|7C|TW zHFhQ=^LM;baREN<1TFDqBGLc+8>IF`f8BKSH2egL5f#pU0&cZ0HB1qNv=GVQe*t)> z@VF(_sL11?W_?l)7emTeTSZGFz($)%$(VE)|J_~Ho5}Wqj+v-`Rbwainkt_9mBvD~eNZXg#TXMQbIse1|(5Jr2Ug4@)x`>m<`IB#)t?*`W?dm4+IR- zi^u;P1pM`c>ZiV@Fj^J(A5iI^dhzXl){77R3GGvnu>V@r-{&B`c-Oy!A-|qvDT1N} z2x2`jzqi+a^dT`&Z;^kCRs8oShBPSeJv$8WYf=Au4gs1Z%9Pse<2EHQ`gp(-pGknBE#70(Xa;I%(=>STj7v}N> z=k)#^IRzB~x)T9TEW3yO4>nUA{fWu%GJ+NUn~5L@A@(!rpXv)B3XM~08XN2Vu zdhO~@fg7ryt`z)w@ei#b$OX#W&ra(AOXL58$6y45yeUWV{V!Y0|7``T-JfawmsoyN zztV;Ny6m6xlV^Y-ZS_aS#7}~P!!v~4{hw@r@iPJ?9B%ywzt2khNrV|S0F(dsUPyjW z5yBv|D^XP;{ozHvID$$YyhHrH|0jNp(obr`|M`unTLm>aHAT+D(*oE{`t*rfOG~TQ zXBDw6dox(yjd_kb>xaM#MJO`i4^PQ*28P!J1QZ%|wzMlN`dCwZ74*aq@bKa9*84N_ z^%m79Yo&6h#H&n3Jnk+lpkF)FX=^vw5H~DMtMT&j$y;^{{N6fQjqrGUIOMR%1c@+6 z2NC!AyB|O#pLbsRd7z-gC*(TF?VW;z(On17W^!Sm4UeRjx!HQRc~8CdIhsYIWn=}D z-(923H=Ozf1*@^CNXf{=pZ3Ydao1T6N)Wb^c6O>UOkm89Y*S=eET8Z_d<0;T8lPld zlCPy}D4?SjJn^eM16L%J%e0H2U~Y%T6+(vsf+M#BJ8eP2O2BObf`{K-)E@%A$gR@5 zp8>vtfP!Ik6uO>ku@n8r{>63|-^Io0qNp+pIW^y5D|8TokkD&@8(%RQ$UJ@0qxXb+Nd-qqamtlrEzKmNur zYOIFG^LMMHb%>oVS3^exB=nA+d?8c;49^kD#+3-%Q%ZocNz?FpCbIYDbe>uoIc2(G zu6yQxXw?gfO3KOA$tMhsy$%#Kgoh8*@33Nw2)DsWd&5>*RN{9m_T7G>8vKh=dNc0hfq5oQ~he z952-Agm~S4z4Fe9Wwo6Z1?jN|n1!BK)4vb>{vpw_+=h~mu-RZ~Nw+L}+q)nyZ}34R zM7qVLYzw>Vboa68yY5uGH}3x3ns;IF`YE7Erk#)lt>oM@8;@(~dd~Ut%aXHBM#R<1 znnEQ4A;~@}w#n!{I-|6nyYu*Ej$=a|8fgb6CKAq?SPIj*DVO_2-gKAD6TsI~T2j(Z ze@e3TXp`cRY_Z|=U4IIjCEd&MC2E@6YAbu>8~RUDl2}{>=(ID%Ap=jqCkdZtDvaA{Z2tqZQ-yoM6Vexvb3cb9wNQsD$s4*Rje`sD!cU@}U(al`-5s+6_ncVtD z`J%HYa7O+@mmpv={E0)~Q)y{=d2EJ$>^D^I3n|0{knqbBEV6W23L6;})mp2m4=b_S z>}hP$Lg$LZ(Z`gYK7+>1)2%J40~PB?fDK4c;o2fe`z3#i1obQoLD_9`YI4M$FDcng zIK%VY`ObWjakA9>LlaKS?yBz#se-^|0G{|0l(QZ1yH+Nh zl<|s*;7eMD?|h|rKkFBXf^*1H?SD)pO&QvkIBFP9>_)lw7=7(IEZh1wmIa35B8$B#%>K%;r2 z*ZDTpR~}dJG+@DGo_#J$4`kbE4F0Eg1YMn!nLgBPmg=FElj75FPv%~XCs1+OFi{A+ zAGId#irprW9Vb-%T~{ACZDk#IMp=-(9ULN@+S<`$Oa~Ad+lXs+AIYoMvI-zZ{QN5h zZ6&GA0hJj=aOaD)Z!b2oIEZw~zOr}7-|5}+Ked83ngBeezi1#$m+t}AY2XoF35xbp zp9?OEove_~lxVQ*Bi7HEr$dbZL48pJpY9&e#aeraF6!lyv7O<}T?_y5^3;Jhlh|-c zQCDvaBc#;j_b-{s0E#ui=d}F^i;jS*%!GKQ`nI>kc%sHS-vwHs(*h_ap`hyA**i;kbC3WqhX%yn2x<)RZb*-1m@q86#m-d{3gT=pRb3r!<( z+(geZDGiHykY*coQj6xBNk^vV{kFMmsHsK9ej(6XO@Z%{XKCyZc-8;Z)&T_GaKnvs zZynHOD$bEzWB}?j?&Xb85|*<4k3qYQJlgiIpH{TqH@Dp{;2N12u&m>N(O$02rdHIH z>v0FKI3GAgU3VB3VI8JgkutOmaJwFuF}7bhz_D*_Z_ne}wr?5)72NDU1{Yl2E!0`g z4-q(-&DGbjUCnz!Bdd;|K6}1cNZ>oWHSP#y%w&~5yoIvxRi^hckKa_hZLa%1@?Az`|$t5J4BweR*dz`#(Pr9_2Pk=wr) ze^jkHSyrgk=ToSnh?com)l@X(SawLI*MBTCryt=1@_&RS^om>dS(GE_9E8tQsDmT$ zwyLIX5wR{F8y`y*SUqWX6dpJk_grpvsrL^I2{jy%qQu8~M_tM}f_!RM*seuY-S^$Qo!%mMTUpZ{srUx=L++3ToR$MuEjE&s;2}rP|2O zz#_x7@*N)(bvrzE?RLlKA=o#6jPH_lZEJQuR;kFg(|_^FjtQuSc*ml% z(CUs@ud5O`6A-YF0dH4Wrdv0;P|G#Y?0kr)*FHRlgv;UfMlh7V6B6I48l{xbZG7r^ zHPt*6Z*tb;VKTg)Uo&&pAJLe+2w6~GUfxa3UkDoY%gA`2z}QUyY;*mn27*j2oyaLQ zWUhD1?9B}id1i}!ZV3D1MyI~rhi`3%-iqUbooMMTD>a;N?vC1@Kj?BfxWN(Z;UL6Z z?fHC`JC_ZnTHIKz7_J13_?}?9p{QOH_7*r-9UPaCDqupE0aq=&a@Rx{uW0a-rGVE$ z#R6#8q+cyd#&_sBqAz7XE3Mb|5D8=OHpZ<9z01Rk1cQJh^83%>yFPIi{BW^ajVrZV z99*3}2Q*5KB~6&3rsmq_5EpS^>y-m=uW`_$tQIT)W@^M+(YtTIt&8a5tEoX{6QS=$ z&54L?&6UNFSBS@|HkCexlTTn*`9H|s&+bkRok$uzYWYCx8{IIW$ zP~}mFzpevYY+nDIBJk8{x=UDV*}r1A&yVg~1Pd1OihEVN-ijcG`bi?ipjLi;wr0z{ zsVVXFElP@*M)3>93onblCktId7y^sMcV^!NzUO5b6Ds2m52~9^4@{ZMwcz`_bo#4p87nqk9umfeg+%|B@*2Z+ zQ+A8t#>_-Qw~olRm7ql4BMVx(rLiO$FZ2K$Edthvl*Ga<&p2T%(rxpRRF2E2IQxun zra^SHh;E=sh-pR8aG?KFH?Wv@ZX54!KL>gI?KVP@=Ef1qZb(((0U_BNzg2YpItaYt z8I8TS+Xzla=4F!*aH+gAx!)?%H4xgh9W&MV3ttR9ohIX5dbhi~JhfkEf!plg++js_ z3G$MXP8qP2c`veOzxi&8D9`|TV~YcUDnYQ)c?i>LQ_`-3d6MgVt2E^RXUtu+vD0!o zdgTFw-pwB9`eYrjeZSW1GzE5@+ACA<@!)CjzH(tMfBH%fw%CTD-A()SWVXU!RS`@H zmR372A0ixbH~p*qT$0=M!Knzl(0f!NF@6lcaW+c6=Z{6uOb$JjifdKlNusUieT(c) zfPpyS_{>bKL?pTkx}ct$TFY+$;-x!b*(SdYuV+*4KwKHPk8v z&8r)gL1fJ<52qstz0$=RW3emJ_jSRrXtinUg97>=YFgWaB0eCupYifM)n-T;M56ys z|M9aYSxaJ!J4ElKQRcL~!9yW=9Ahm2s06?v5Sa1wFYsrVpIh$Jo$#&iTk+LpZV-^$ zR26mw@yYW|Pzc-s6}^@9?h;O8vi#Q%^{JRFw2`yE`$cSuK{`T1s+zq&xMU1Dc;>Db z3BAc?>YEyCHI#7o?=;)--5J(mUvd4Ycnm!wy|>xIc6Hz^UG)7Mth)M8(Py;#EO3kI zf{xn?Cc~N6VY(G(2CCC!P|UTiMGF_y!{!Ui@A01*_Tsv`4p4cnJxr3QKYv3j$;UTi{OK>TWu<21^Xjtrh^ zps*Y?PTf<^JL()COZ+i!bV7LGIP>6@B8Kb`>1XR=mTU(Sk8&lQwK2tDJRT3d&xM&;X+@P zd^!YU-YP2{q(Y~4e7jh_F%cD?PoS+@GN3kL!;w+8f#)R6jANCFy|lyE;{efRTWxOP z)Mt@0&yJVhJ)#h}t1a;2I;~2j_!V+NNwD|z3~ObnQg_4jtxGt*^GB&F(Y6Zfw}cl$ zSD}tO3sRI$ENW}h49GO7MFEkqSk8@5WSy9-dA_mRedjWA z4f79VJimuq?qXhGU>_fi{v9ZA-U;Aliuvrq+6YRhlNn|mS-Q57Qi_42pdu#4MEq*S4WMY`rLaUfqMMmMh z;d7?TdYnvYEF7vBei_)@?!EQZ+jXq&a}#4f$$vZM|Mbp+ zywJ5Ho`>qN_fanrMzb%12BZ2b9*~FL5VP!77X~TZa_#hb&Ad?W4(3WWTjmll1{9uY zzH#LauB{rlIFB66(9t^1P0hI058;^6)MP1b!noWTg<5gR1xn|S`Cd)*>nCNUn;W`C zjVUwg)zcm2Wofx#lH##=O*|(Lf5zJj)7>Kq>a=nrctj^kSSY zvPP@(B3ez-Dw?_Gesyq}#znXo_15;DJreP_vU_1MS&38*sQO?ydt?_a;*HpUT(_H% zNieBC%6(Z|>6}*|qKaWLTtq8O6FI6T9oc?GBl?Xb;?Di9U^A&g6U{@1Fv_dDvY}GgW?8kT`8=0=33z=A|k)c zWWb@6$+P_YUEbdwlvKzZoZg9+Abh8sV)gNU8B$Xp>7UZmo~`eW_`Eo8v|qh_^C^}X z*UdVFkIU^WH}*QQLi>X`%dU^sQmv8o(8#>KeaA;4S76nlujXmZPAV@bI&RZaTQ5B= z55K6Rlk+~PW8c-^lmPEk3uZT@D`@b?*x*WiQhlK;kWm?HbY{N!wYj#XB>u zfJD44tMVGfd+qzC2v(Y8#b)}m6p*m*M`3$O`bccM^m;iofJ0udh9K8%PFh${N`mMRAy4Jpr8RsmKX9P-QVI#~|dz`R4| zDld~1BN;e&x)<00g{4!2UJ7q7Q^Lx0Tn}@ej0tqZZGAdi+D2Zy!qpaDhkO)xxPdwP zp-;=*UMl2zRnwz^{9%(^M_S|~4+Q0^2^j@leF7p%vPJIqc1MNx>y0^P{kB4$w z?QdErUqc)?hO($;F*e0>+PoSm{l>j24${ZvA^pxufQR}A2OI$zeBb)7ub`L>G0)GX zJN#Ks_%vlG3LZv}eqo$L4 z9LhT;Q<*uqn?y2}II_Uy{wNq0#>c)-?YXz!PrujzZtf!ZiE*xR;E?aAr6g(B>({Yg z(69o1VCY9JB@ABkoVShl8i3BIXt*$!9G*rxxiSB}*o}nD>*?wm*Rl4@1tTFD=GwYk z3)1mlN4cM_2GH^c&>I2fQPk1;j}JZveSD~GfqH=+t=wL<(zNTxg0 z<3#!23HM)T@971>h-YJCM@fI_YkszI91b`H+&B9jhF`7r^M87@{lJ7v2?^6B{^9hf zf<$Z(P^ikA!+O6zZCD)`c#x(5KX>eZdb0mL2wFhF5#{xLZTV{k`u7!7OHk!iK3+}) zzdf-3G#sj+WHgJb(C?Mcf+~Oibi2p<_k8xR(!QY)`NiRwo&@||`hVY4uoN1OsOSEC z!u=l&1S(gV6l4>&(Ix2LE7wFs6%4!H%)R{GCVAo@n^>eZA^u)@)f*x|)Wd#>-CxJl zpN}pX4YJ8Jrz6(ymH*%0Aegj@t#=puT3_GRyDZ0IdUBM<{MIyJ5LFnOyUdWi$di^^ zri;}HtBWAW=KY<}9_#N0hEt95dk-D)mdYir-r_oC! Roles*. - -[float] -=== Create an index pattern - -An index pattern is the glue that connects {kib} to your {es} data. Create an -index pattern whenever you load your own data into {kib}. To get started, -click *Create index pattern*, and then follow the guided steps. Refer to -<> for the types of index patterns -that you can create. - -[float] -=== Manage your index pattern - -To view the fields and associated data types in an index pattern, click its name in -the *Index patterns* overview. - -[role="screenshot"] -image::management/index-patterns/images/new-index-pattern.png["Index files and data types"] +Kibana provides these field formatters: -Use the icons to perform the following actions: - -* [[set-default-pattern]]*Set the default index pattern.* {kib} uses a badge to make users -aware of which index pattern is the default. The first pattern -you create is automatically designated as the default pattern. The default -index pattern is loaded when you open *Discover*. +* <> +* <> +* <> +* <> -* *Refresh the index fields list.* You can refresh the index fields list to -pick up any newly-added fields. Doing so also resets the {kib} popularity counters -for the fields. The popularity counters are used in *Discover* to sort fields in lists. +To format a field: -* [[delete-pattern]]*Delete the index pattern.* This action removes the pattern from the list of -Saved Objects in {kib}. You will not be able to recover field formatters, -scripted fields, source filters, and field popularity data associated with the index pattern. -Deleting an index pattern does -not remove any indices or data documents from {es}. +. Open the main menu, and click *Stack Management > Index Patterns*. +. Click the index pattern that contains the field you want to format. +. Find the field you want to format and click the edit icon (image:management/index-patterns/images/edit_icon.png[]). +. Select a format and fill in the details. + -WARNING: Deleting an index pattern breaks all visualizations, saved searches, and -other saved objects that reference the pattern. - -[float] -=== Edit a field - -To edit a field's properties, click the edit icon -image:management/index-patterns/images/edit_icon.png[] in the detail view. -You can set the field's format and popularity value. +[role="screenshot"] +image:management/index-patterns/images/edit-field-format.png["Edit field format"] -Kibana has field formatters for the following field types: -* <> -* <> -* <> -* <> [[field-formatters-string]] === String field formatters From 26f79a6a2971f95f1fc1b24691c457a9aebe09aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Fri, 6 Nov 2020 00:30:47 +0100 Subject: [PATCH 42/57] [Security Solution] Unskip Overview cypress tests (#82782) --- .../security_solution/cypress/integration/overview.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index dafcabb8e1e8d..69094cad7456e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts @@ -11,8 +11,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { OVERVIEW_URL } from '../urls/navigation'; -// Failing: See https://github.com/elastic/kibana/issues/81848 -describe.skip('Overview Page', () => { +describe('Overview Page', () => { it('Host stats render with correct values', () => { cy.stubSearchStrategyApi('overview_search_strategy'); loginAndWaitForPage(OVERVIEW_URL); From 8cdf56636aa5fd7453922714cd0ce01040d103d4 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Thu, 5 Nov 2020 17:41:07 -0700 Subject: [PATCH 43/57] Adds cloud links to user popover (#66825) Co-authored-by: Ryan Keairns --- x-pack/plugins/cloud/kibana.json | 2 +- x-pack/plugins/cloud/public/index.ts | 2 +- x-pack/plugins/cloud/public/mocks.ts | 18 +++ x-pack/plugins/cloud/public/plugin.ts | 28 +++- .../plugins/cloud/public/user_menu_links.ts | 38 +++++ x-pack/plugins/cloud/server/config.ts | 2 + x-pack/plugins/security/public/index.ts | 1 + x-pack/plugins/security/public/mocks.ts | 7 + .../security/public/nav_control/index.mock.ts | 14 ++ .../security/public/nav_control/index.ts | 3 +- .../nav_control/nav_control_component.scss | 11 ++ .../nav_control_component.test.tsx | 38 +++++ .../nav_control/nav_control_component.tsx | 139 ++++++++++++------ .../nav_control/nav_control_service.tsx | 39 ++++- .../plugins/security/public/plugin.test.tsx | 7 +- x-pack/plugins/security/public/plugin.tsx | 4 +- 16 files changed, 292 insertions(+), 61 deletions(-) create mode 100644 x-pack/plugins/cloud/public/mocks.ts create mode 100644 x-pack/plugins/cloud/public/user_menu_links.ts create mode 100644 x-pack/plugins/security/public/nav_control/index.mock.ts create mode 100644 x-pack/plugins/security/public/nav_control/nav_control_component.scss diff --git a/x-pack/plugins/cloud/kibana.json b/x-pack/plugins/cloud/kibana.json index 27b35bcbdd88b..9bca2f30bd23c 100644 --- a/x-pack/plugins/cloud/kibana.json +++ b/x-pack/plugins/cloud/kibana.json @@ -3,7 +3,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "cloud"], - "optionalPlugins": ["usageCollection", "home"], + "optionalPlugins": ["usageCollection", "home", "security"], "server": true, "ui": true } diff --git a/x-pack/plugins/cloud/public/index.ts b/x-pack/plugins/cloud/public/index.ts index 39ef5f452c18b..680b2f1ad2bd6 100644 --- a/x-pack/plugins/cloud/public/index.ts +++ b/x-pack/plugins/cloud/public/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext } from '../../../../src/core/public'; import { CloudPlugin } from './plugin'; -export { CloudSetup } from './plugin'; +export { CloudSetup, CloudConfigType } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new CloudPlugin(initializerContext); } diff --git a/x-pack/plugins/cloud/public/mocks.ts b/x-pack/plugins/cloud/public/mocks.ts new file mode 100644 index 0000000000000..bafebbca4ecdd --- /dev/null +++ b/x-pack/plugins/cloud/public/mocks.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +function createSetupMock() { + return { + cloudId: 'mock-cloud-id', + isCloudEnabled: true, + resetPasswordUrl: 'reset-password-url', + accountUrl: 'account-url', + }; +} + +export const cloudMock = { + createSetup: createSetupMock, +}; diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 45005f3f5e422..bc410b89c30e7 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -6,40 +6,51 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { i18n } from '@kbn/i18n'; +import { SecurityPluginStart } from '../../security/public'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { ELASTIC_SUPPORT_LINK } from '../common/constants'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { createUserMenuLinks } from './user_menu_links'; -interface CloudConfigType { +export interface CloudConfigType { id?: string; resetPasswordUrl?: string; deploymentUrl?: string; + accountUrl?: string; } interface CloudSetupDependencies { home?: HomePublicPluginSetup; } +interface CloudStartDependencies { + security?: SecurityPluginStart; +} + export interface CloudSetup { cloudId?: string; cloudDeploymentUrl?: string; isCloudEnabled: boolean; + resetPasswordUrl?: string; + accountUrl?: string; } export class CloudPlugin implements Plugin { private config!: CloudConfigType; + private isCloudEnabled: boolean; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); + this.isCloudEnabled = false; } public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { const { id, resetPasswordUrl, deploymentUrl } = this.config; - const isCloudEnabled = getIsCloudEnabled(id); + this.isCloudEnabled = getIsCloudEnabled(id); if (home) { - home.environment.update({ cloud: isCloudEnabled }); - if (isCloudEnabled) { + home.environment.update({ cloud: this.isCloudEnabled }); + if (this.isCloudEnabled) { home.tutorials.setVariable('cloud', { id, resetPasswordUrl }); } } @@ -47,11 +58,11 @@ export class CloudPlugin implements Plugin { return { cloudId: id, cloudDeploymentUrl: deploymentUrl, - isCloudEnabled, + isCloudEnabled: this.isCloudEnabled, }; } - public start(coreStart: CoreStart) { + public start(coreStart: CoreStart, { security }: CloudStartDependencies) { const { deploymentUrl } = this.config; coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK); if (deploymentUrl) { @@ -63,5 +74,10 @@ export class CloudPlugin implements Plugin { href: deploymentUrl, }); } + + if (security && this.isCloudEnabled) { + const userMenuLinks = createUserMenuLinks(this.config); + security.navControlService.addUserMenuLinks(userMenuLinks); + } } } diff --git a/x-pack/plugins/cloud/public/user_menu_links.ts b/x-pack/plugins/cloud/public/user_menu_links.ts new file mode 100644 index 0000000000000..15e2f14e885ba --- /dev/null +++ b/x-pack/plugins/cloud/public/user_menu_links.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { UserMenuLink } from '../../security/public'; +import { CloudConfigType } from '.'; + +export const createUserMenuLinks = (config: CloudConfigType): UserMenuLink[] => { + const { resetPasswordUrl, accountUrl } = config; + const userMenuLinks = [] as UserMenuLink[]; + + if (resetPasswordUrl) { + userMenuLinks.push({ + label: i18n.translate('xpack.cloud.userMenuLinks.profileLinkText', { + defaultMessage: 'Cloud profile', + }), + iconType: 'logoCloud', + href: resetPasswordUrl, + order: 100, + }); + } + + if (accountUrl) { + userMenuLinks.push({ + label: i18n.translate('xpack.cloud.userMenuLinks.accountLinkText', { + defaultMessage: 'Account & Billing', + }), + iconType: 'gear', + href: accountUrl, + order: 200, + }); + } + + return userMenuLinks; +}; diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index ff8a2c5acdf9a..eaa4ab7a482dd 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -23,6 +23,7 @@ const configSchema = schema.object({ apm: schema.maybe(apmConfigSchema), resetPasswordUrl: schema.maybe(schema.string()), deploymentUrl: schema.maybe(schema.string()), + accountUrl: schema.maybe(schema.string()), }); export type CloudConfigType = TypeOf; @@ -32,6 +33,7 @@ export const config: PluginConfigDescriptor = { id: true, resetPasswordUrl: true, deploymentUrl: true, + accountUrl: true, }, schema: configSchema, }; diff --git a/x-pack/plugins/security/public/index.ts b/x-pack/plugins/security/public/index.ts index 8016c94224060..d0382c22ed3c6 100644 --- a/x-pack/plugins/security/public/index.ts +++ b/x-pack/plugins/security/public/index.ts @@ -16,6 +16,7 @@ import { export { SecurityPluginSetup, SecurityPluginStart }; export { AuthenticatedUser } from '../common/model'; export { SecurityLicense, SecurityLicenseFeatures } from '../common/licensing'; +export { UserMenuLink } from '../public/nav_control'; export const plugin: PluginInitializer< SecurityPluginSetup, diff --git a/x-pack/plugins/security/public/mocks.ts b/x-pack/plugins/security/public/mocks.ts index 33c1d1446afba..26a759ca52267 100644 --- a/x-pack/plugins/security/public/mocks.ts +++ b/x-pack/plugins/security/public/mocks.ts @@ -7,6 +7,7 @@ import { authenticationMock } from './authentication/index.mock'; import { createSessionTimeoutMock } from './session/session_timeout.mock'; import { licenseMock } from '../common/licensing/index.mock'; +import { navControlServiceMock } from './nav_control/index.mock'; function createSetupMock() { return { @@ -15,7 +16,13 @@ function createSetupMock() { license: licenseMock.create(), }; } +function createStartMock() { + return { + navControlService: navControlServiceMock.createStart(), + }; +} export const securityMock = { createSetup: createSetupMock, + createStart: createStartMock, }; diff --git a/x-pack/plugins/security/public/nav_control/index.mock.ts b/x-pack/plugins/security/public/nav_control/index.mock.ts new file mode 100644 index 0000000000000..1cd10810d7c8f --- /dev/null +++ b/x-pack/plugins/security/public/nav_control/index.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SecurityNavControlServiceStart } from '.'; + +export const navControlServiceMock = { + createStart: (): jest.Mocked => ({ + getUserMenuLinks$: jest.fn(), + addUserMenuLinks: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/nav_control/index.ts b/x-pack/plugins/security/public/nav_control/index.ts index 2b0af1a45d05a..737ae50054698 100644 --- a/x-pack/plugins/security/public/nav_control/index.ts +++ b/x-pack/plugins/security/public/nav_control/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SecurityNavControlService } from './nav_control_service'; +export { SecurityNavControlService, SecurityNavControlServiceStart } from './nav_control_service'; +export { UserMenuLink } from './nav_control_component'; diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.scss b/x-pack/plugins/security/public/nav_control/nav_control_component.scss new file mode 100644 index 0000000000000..a3e04b08cfac2 --- /dev/null +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.scss @@ -0,0 +1,11 @@ +.chrNavControl__userMenu { + .euiContextMenuPanelTitle { + // Uppercased by default, override to match actual username + text-transform: none; + } + + .euiContextMenuItem { + // Temp fix for EUI issue https://github.com/elastic/eui/issues/3092 + line-height: normal; + } +} \ No newline at end of file diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx index c1c6a9f69b6ec..1da91e80d062d 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { BehaviorSubject } from 'rxjs'; import { shallowWithIntl, nextTick, mountWithIntl } from 'test_utils/enzyme_helpers'; import { SecurityNavControl } from './nav_control_component'; import { AuthenticatedUser } from '../../common/model'; @@ -17,6 +18,7 @@ describe('SecurityNavControl', () => { user: new Promise(() => {}) as Promise, editProfileUrl: '', logoutUrl: '', + userMenuLinks$: new BehaviorSubject([]), }; const wrapper = shallowWithIntl(); @@ -42,6 +44,7 @@ describe('SecurityNavControl', () => { user: Promise.resolve({ full_name: 'foo' }) as Promise, editProfileUrl: '', logoutUrl: '', + userMenuLinks$: new BehaviorSubject([]), }; const wrapper = shallowWithIntl(); @@ -70,6 +73,7 @@ describe('SecurityNavControl', () => { user: Promise.resolve({ full_name: 'foo' }) as Promise, editProfileUrl: '', logoutUrl: '', + userMenuLinks$: new BehaviorSubject([]), }; const wrapper = mountWithIntl(); @@ -91,6 +95,7 @@ describe('SecurityNavControl', () => { user: Promise.resolve({ full_name: 'foo' }) as Promise, editProfileUrl: '', logoutUrl: '', + userMenuLinks$: new BehaviorSubject([]), }; const wrapper = mountWithIntl(); @@ -107,4 +112,37 @@ describe('SecurityNavControl', () => { expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(1); expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(1); }); + + it('renders a popover with additional user menu links registered by other plugins', async () => { + const props = { + user: Promise.resolve({ full_name: 'foo' }) as Promise, + editProfileUrl: '', + logoutUrl: '', + userMenuLinks$: new BehaviorSubject([ + { label: 'link1', href: 'path-to-link-1', iconType: 'empty', order: 1 }, + { label: 'link2', href: 'path-to-link-2', iconType: 'empty', order: 2 }, + { label: 'link3', href: 'path-to-link-3', iconType: 'empty', order: 3 }, + ]), + }; + + const wrapper = mountWithIntl(); + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'userMenu')).toHaveLength(0); + expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(0); + expect(findTestSubject(wrapper, 'userMenuLink__link1')).toHaveLength(0); + expect(findTestSubject(wrapper, 'userMenuLink__link2')).toHaveLength(0); + expect(findTestSubject(wrapper, 'userMenuLink__link3')).toHaveLength(0); + expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(0); + + wrapper.find(EuiHeaderSectionItemButton).simulate('click'); + + expect(findTestSubject(wrapper, 'userMenu')).toHaveLength(1); + expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(1); + expect(findTestSubject(wrapper, 'userMenuLink__link1')).toHaveLength(1); + expect(findTestSubject(wrapper, 'userMenuLink__link2')).toHaveLength(1); + expect(findTestSubject(wrapper, 'userMenuLink__link3')).toHaveLength(1); + expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx index 3ddabb0dc55f8..c22308fa8a43e 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx @@ -7,38 +7,52 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; - +import { Observable, Subscription } from 'rxjs'; import { EuiAvatar, - EuiFlexGroup, - EuiFlexItem, EuiHeaderSectionItemButton, - EuiLink, - EuiText, - EuiSpacer, EuiPopover, EuiLoadingSpinner, + EuiIcon, + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, + IconType, + EuiText, } from '@elastic/eui'; import { AuthenticatedUser } from '../../common/model'; +import './nav_control_component.scss'; + +export interface UserMenuLink { + label: string; + iconType: IconType; + href: string; + order?: number; +} + interface Props { user: Promise; editProfileUrl: string; logoutUrl: string; + userMenuLinks$: Observable; } interface State { isOpen: boolean; authenticatedUser: AuthenticatedUser | null; + userMenuLinks: UserMenuLink[]; } export class SecurityNavControl extends Component { + private subscription?: Subscription; + constructor(props: Props) { super(props); this.state = { isOpen: false, authenticatedUser: null, + userMenuLinks: [], }; props.user.then((authenticatedUser) => { @@ -48,6 +62,18 @@ export class SecurityNavControl extends Component { }); } + componentDidMount() { + this.subscription = this.props.userMenuLinks$.subscribe(async (userMenuLinks) => { + this.setState({ userMenuLinks }); + }); + } + + componentWillUnmount() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + onMenuButtonClick = () => { if (!this.state.authenticatedUser) { return; @@ -66,13 +92,13 @@ export class SecurityNavControl extends Component { render() { const { editProfileUrl, logoutUrl } = this.props; - const { authenticatedUser } = this.state; + const { authenticatedUser, userMenuLinks } = this.state; - const name = + const username = (authenticatedUser && (authenticatedUser.full_name || authenticatedUser.username)) || ''; const buttonContents = authenticatedUser ? ( - + ) : ( ); @@ -92,6 +118,60 @@ export class SecurityNavControl extends Component { ); + const profileMenuItem = { + name: ( + + ), + icon: , + href: editProfileUrl, + 'data-test-subj': 'profileLink', + }; + + const logoutMenuItem = { + name: ( + + ), + icon: , + href: logoutUrl, + 'data-test-subj': 'logoutLink', + }; + + const items: EuiContextMenuPanelItemDescriptor[] = []; + + items.push(profileMenuItem); + + if (userMenuLinks.length) { + const userMenuLinkMenuItems = userMenuLinks + .sort(({ order: orderA = Infinity }, { order: orderB = Infinity }) => orderA - orderB) + .map(({ label, iconType, href }: UserMenuLink) => ({ + name: {label}, + icon: , + href, + 'data-test-subj': `userMenuLink__${label}`, + })); + + items.push(...userMenuLinkMenuItems, { + isSeparator: true, + key: 'securityNavControlComponent__userMenuLinksSeparator', + }); + } + + items.push(logoutMenuItem); + + const panels = [ + { + id: 0, + title: username, + items, + }, + ]; + return ( { repositionOnScroll closePopover={this.closeMenu} panelPaddingSize="none" + buffer={0} > -
    - - - - - - - -

    {name}

    -
    - - - - - - - - - - - - - - - - - - - - -
    -
    +
    +
    ); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx index aa3ec2e47469d..4ae64d667ce29 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx @@ -4,12 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Subscription } from 'rxjs'; +import { sortBy } from 'lodash'; +import { Observable, Subscription, BehaviorSubject, ReplaySubject } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; import { CoreStart } from 'src/core/public'; + import ReactDOM from 'react-dom'; import React from 'react'; + import { SecurityLicense } from '../../common/licensing'; -import { SecurityNavControl } from './nav_control_component'; +import { SecurityNavControl, UserMenuLink } from './nav_control_component'; import { AuthenticationServiceSetup } from '../authentication'; interface SetupDeps { @@ -22,6 +26,18 @@ interface StartDeps { core: CoreStart; } +export interface SecurityNavControlServiceStart { + /** + * Returns an Observable of the array of user menu links registered by other plugins + */ + getUserMenuLinks$: () => Observable; + + /** + * Registers the provided user menu links to be displayed in the user menu in the global nav + */ + addUserMenuLinks: (newUserMenuLink: UserMenuLink[]) => void; +} + export class SecurityNavControlService { private securityLicense!: SecurityLicense; private authc!: AuthenticationServiceSetup; @@ -31,13 +47,16 @@ export class SecurityNavControlService { private securityFeaturesSubscription?: Subscription; + private readonly stop$ = new ReplaySubject(1); + private userMenuLinks$ = new BehaviorSubject([]); + public setup({ securityLicense, authc, logoutUrl }: SetupDeps) { this.securityLicense = securityLicense; this.authc = authc; this.logoutUrl = logoutUrl; } - public start({ core }: StartDeps) { + public start({ core }: StartDeps): SecurityNavControlServiceStart { this.securityFeaturesSubscription = this.securityLicense.features$.subscribe( ({ showLinks }) => { const isAnonymousPath = core.http.anonymousPaths.isAnonymous(window.location.pathname); @@ -49,6 +68,14 @@ export class SecurityNavControlService { } } ); + + return { + getUserMenuLinks$: () => + this.userMenuLinks$.pipe(map(this.sortUserMenuLinks), takeUntil(this.stop$)), + addUserMenuLinks: (userMenuLink: UserMenuLink[]) => { + this.userMenuLinks$.next(userMenuLink); + }, + }; } public stop() { @@ -57,6 +84,7 @@ export class SecurityNavControlService { this.securityFeaturesSubscription = undefined; } this.navControlRegistered = false; + this.stop$.next(); } private registerSecurityNavControl( @@ -72,6 +100,7 @@ export class SecurityNavControlService { user: currentUserPromise, editProfileUrl: core.http.basePath.prepend('/security/account'), logoutUrl: this.logoutUrl, + userMenuLinks$: this.userMenuLinks$, }; ReactDOM.render( @@ -86,4 +115,8 @@ export class SecurityNavControlService { this.navControlRegistered = true; } + + private sortUserMenuLinks(userMenuLinks: UserMenuLink[]) { + return sortBy(userMenuLinks, 'order'); + } } diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index d86d4812af5e3..6f5a2a031a7b2 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -97,7 +97,12 @@ describe('Security Plugin', () => { data: {} as DataPublicPluginStart, features: {} as FeaturesPluginStart, }) - ).toBeUndefined(); + ).toEqual({ + navControlService: { + getUserMenuLinks$: expect.any(Function), + addUserMenuLinks: expect.any(Function), + }, + }); }); it('starts Management Service if `management` plugin is available', () => { diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index 700653c4cecb8..f94772c43dd89 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -146,11 +146,13 @@ export class SecurityPlugin public start(core: CoreStart, { management, securityOss }: PluginStartDependencies) { this.sessionTimeout.start(); - this.navControlService.start({ core }); this.securityCheckupService.start({ securityOssStart: securityOss, docLinks: core.docLinks }); + if (management) { this.managementService.start({ capabilities: core.application.capabilities }); } + + return { navControlService: this.navControlService.start({ core }) }; } public stop() { From f3599fec4c2edd97fa555679a154cb5a8b48096b Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 5 Nov 2020 19:45:10 -0500 Subject: [PATCH 44/57] [SECURITY SOLUTIONS] Keep context of timeline when switching tabs in security solutions (#82237) * try to keep timeline context when switching tabs * fix popover * simpler solution to keep timelien context between tabs * fix timeline context with relative date * allow update on the kql bar when opening new timeline * keep detail view in context when savedObjectId of the timeline does not chnage * remove redux solution and just KISS it * add unit test for the popover * add test on timeline context cache * final commit -> to fix context of timeline between tabs * keep timerange kind to absolute when refreshing * fix bug today/thiw week to be absolute and not relative * add unit test for absolute date for today and this week * fix absolute today/this week on timeline * fix refresh between page and timeline when link * clean up * remove nit Co-authored-by: Patryk Kopycinski --- .../common/components/query_bar/index.tsx | 5 +- .../common/components/search_bar/index.tsx | 34 ++- .../super_date_picker/index.test.tsx | 20 +- .../components/super_date_picker/index.tsx | 63 ++-- .../super_date_picker/selectors.test.ts | 69 +++-- .../components/super_date_picker/selectors.ts | 15 +- .../public/common/store/inputs/actions.ts | 2 + .../public/common/store/inputs/model.ts | 4 +- .../public/common/store/inputs/reducer.ts | 23 +- .../common/store/sourcerer/selectors.ts | 21 +- .../field_renderers/field_renderers.test.tsx | 42 +++ .../field_renderers/field_renderers.tsx | 8 +- .../__snapshots__/timeline.test.tsx.snap | 1 + .../components/timeline/body/events/index.tsx | 1 - .../timeline/body/events/stateful_event.tsx | 276 ++++++------------ .../timelines/components/timeline/index.tsx | 11 +- .../components/timeline/timeline.test.tsx | 1 + .../components/timeline/timeline.tsx | 3 + .../containers/active_timeline_context.ts | 75 +++++ .../timelines/containers/index.test.tsx | 210 +++++++++++++ .../public/timelines/containers/index.tsx | 141 +++++++-- .../timeline/epic_local_storage.test.tsx | 1 + .../timelines/store/timeline/helpers.ts | 33 ++- 23 files changed, 748 insertions(+), 311 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx index d68ab3a171151..7555f6e734214 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx @@ -62,7 +62,10 @@ export const QueryBar = memo( const [draftQuery, setDraftQuery] = useState(filterQuery); useEffect(() => { - // Reset draftQuery when `Create new timeline` is clicked + setDraftQuery(filterQuery); + }, [filterQuery]); + + useEffect(() => { if (filterQueryDraft == null) { setDraftQuery(filterQuery); } diff --git a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx index 2dc44fd48e66d..acc01ac4f76aa 100644 --- a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx @@ -132,7 +132,7 @@ export const SearchBarComponent = memo( if (!isStateUpdated) { // That mean we are doing a refresh! - if (isQuickSelection) { + if (isQuickSelection && payload.dateRange.to !== payload.dateRange.from) { updateSearchBar.updateTime = true; updateSearchBar.end = payload.dateRange.to; updateSearchBar.start = payload.dateRange.from; @@ -313,7 +313,7 @@ const makeMapStateToProps = () => { fromStr: getFromStrSelector(inputsRange), filterQuery: getFilterQuerySelector(inputsRange), isLoading: getIsLoadingSelector(inputsRange), - queries: getQueriesSelector(inputsRange), + queries: getQueriesSelector(state, id), savedQuery: getSavedQuerySelector(inputsRange), start: getStartSelector(inputsRange), toStr: getToStrSelector(inputsRange), @@ -351,15 +351,27 @@ export const dispatchUpdateSearch = (dispatch: Dispatch) => ({ const fromDate = formatDate(start); let toDate = formatDate(end, { roundUp: true }); if (isQuickSelection) { - dispatch( - inputsActions.setRelativeRangeDatePicker({ - id, - fromStr: start, - toStr: end, - from: fromDate, - to: toDate, - }) - ); + if (end === start) { + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + id, + fromStr: start, + toStr: end, + from: fromDate, + to: toDate, + }) + ); + } else { + dispatch( + inputsActions.setRelativeRangeDatePicker({ + id, + fromStr: start, + toStr: end, + from: fromDate, + to: toDate, + }) + ); + } } else { toDate = formatDate(end); dispatch( diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx index 956ee4b05f9d6..bcb10f8fd26c3 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx @@ -139,7 +139,7 @@ describe('SIEM Super Date Picker', () => { expect(store.getState().inputs.global.timerange.toStr).toBe('now'); }); - test('Make Sure it is Today date', () => { + test('Make Sure it is Today date is an absolute date', () => { wrapper .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') .first() @@ -151,8 +151,22 @@ describe('SIEM Super Date Picker', () => { .first() .simulate('click'); wrapper.update(); - expect(store.getState().inputs.global.timerange.fromStr).toBe('now/d'); - expect(store.getState().inputs.global.timerange.toStr).toBe('now/d'); + expect(store.getState().inputs.global.timerange.kind).toBe('absolute'); + }); + + test('Make Sure it is This Week date is an absolute date', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerCommonlyUsed_This_week"]') + .first() + .simulate('click'); + wrapper.update(); + expect(store.getState().inputs.global.timerange.kind).toBe('absolute'); }); test('Make Sure to (end date) is superior than from (start date)', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx index 4443d24531b22..97e023176647f 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx @@ -91,12 +91,12 @@ export const SuperDatePickerComponent = React.memo( toStr, updateReduxTime, }) => { - const [isQuickSelection, setIsQuickSelection] = useState(true); const [recentlyUsedRanges, setRecentlyUsedRanges] = useState( [] ); const onRefresh = useCallback( ({ start: newStart, end: newEnd }: OnRefreshProps): void => { + const isQuickSelection = newStart.includes('now') || newEnd.includes('now'); const { kqlHaveBeenUpdated } = updateReduxTime({ end: newEnd, id, @@ -117,12 +117,13 @@ export const SuperDatePickerComponent = React.memo( refetchQuery(queries); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [end, id, isQuickSelection, kqlQuery, start, timelineId] + [end, id, kqlQuery, queries, start, timelineId, updateReduxTime] ); const onRefreshChange = useCallback( ({ isPaused, refreshInterval }: OnRefreshChangeProps): void => { + const isQuickSelection = + (fromStr != null && fromStr.includes('now')) || (toStr != null && toStr.includes('now')); if (duration !== refreshInterval) { setDuration({ id, duration: refreshInterval }); } @@ -137,27 +138,22 @@ export const SuperDatePickerComponent = React.memo( refetchQuery(queries); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [id, isQuickSelection, duration, policy, toStr] + [fromStr, toStr, duration, policy, setDuration, id, stopAutoReload, startAutoReload, queries] ); - const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => { + const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); }; const onTimeChange = useCallback( - ({ - start: newStart, - end: newEnd, - isQuickSelection: newIsQuickSelection, - isInvalid, - }: OnTimeChangeProps) => { + ({ start: newStart, end: newEnd, isInvalid }: OnTimeChangeProps) => { + const isQuickSelection = newStart.includes('now') || newEnd.includes('now'); if (!isInvalid) { updateReduxTime({ end: newEnd, id, isInvalid, - isQuickSelection: newIsQuickSelection, + isQuickSelection, kql: kqlQuery, start: newStart, timelineId, @@ -174,15 +170,13 @@ export const SuperDatePickerComponent = React.memo( ]; setRecentlyUsedRanges(newRecentlyUsedRanges); - setIsQuickSelection(newIsQuickSelection); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [recentlyUsedRanges, kqlQuery] + [updateReduxTime, id, kqlQuery, timelineId, recentlyUsedRanges] ); - const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); - const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); + const endDate = toStr != null ? toStr : new Date(end).toISOString(); + const startDate = fromStr != null ? fromStr : new Date(start).toISOString(); const [quickRanges] = useUiSetting$(DEFAULT_TIMEPICKER_QUICK_RANGES); const commonlyUsedRanges = isEmpty(quickRanges) @@ -232,15 +226,27 @@ export const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({ const fromDate = formatDate(start); let toDate = formatDate(end, { roundUp: true }); if (isQuickSelection) { - dispatch( - inputsActions.setRelativeRangeDatePicker({ - id, - fromStr: start, - toStr: end, - from: fromDate, - to: toDate, - }) - ); + if (end === start) { + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + id, + fromStr: start, + toStr: end, + from: fromDate, + to: toDate, + }) + ); + } else { + dispatch( + inputsActions.setRelativeRangeDatePicker({ + id, + fromStr: start, + toStr: end, + from: fromDate, + to: toDate, + }) + ); + } } else { toDate = formatDate(end); dispatch( @@ -284,6 +290,7 @@ export const makeMapStateToProps = () => { const getToStrSelector = toStrSelector(); return (state: State, { id }: OwnProps) => { const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state); + return { duration: getDurationSelector(inputsRange), end: getEndSelector(inputsRange), @@ -292,7 +299,7 @@ export const makeMapStateToProps = () => { kind: getKindSelector(inputsRange), kqlQuery: getKqlQuerySelector(inputsRange) as inputsModel.GlobalKqlQuery, policy: getPolicySelector(inputsRange), - queries: getQueriesSelector(inputsRange) as inputsModel.GlobalGraphqlQuery[], + queries: getQueriesSelector(state, id), start: getStartSelector(inputsRange), toStr: getToStrSelector(inputsRange), }; diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts b/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts index 7cb4ea9ada93f..ee19aef717f4f 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts @@ -17,6 +17,8 @@ import { } from './selectors'; import { InputsRange, AbsoluteTimeRange, RelativeTimeRange } from '../../store/inputs/model'; import { cloneDeep } from 'lodash/fp'; +import { mockGlobalState } from '../../mock'; +import { State } from '../../store'; describe('selectors', () => { let absoluteTime: AbsoluteTimeRange = { @@ -42,6 +44,8 @@ describe('selectors', () => { filters: [], }; + let mockState: State = mockGlobalState; + const getPolicySelector = policySelector(); const getDurationSelector = durationSelector(); const getKindSelector = kindSelector(); @@ -75,6 +79,8 @@ describe('selectors', () => { }, filters: [], }; + + mockState = mockGlobalState; }); describe('#policySelector', () => { @@ -375,34 +381,61 @@ describe('selectors', () => { describe('#queriesSelector', () => { test('returns the same reference given the same identical input twice', () => { - const result1 = getQueriesSelector(inputState); - const result2 = getQueriesSelector(inputState); + const myMock = { + ...mockState, + inputs: { + ...mockState.inputs, + global: inputState, + }, + }; + const result1 = getQueriesSelector(myMock, 'global'); + const result2 = getQueriesSelector(myMock, 'global'); expect(result1).toBe(result2); }); test('DOES NOT return the same reference given different input twice but with different deep copies since the query is not a primitive', () => { - const clone = cloneDeep(inputState); - const result1 = getQueriesSelector(inputState); - const result2 = getQueriesSelector(clone); + const myMock = { + ...mockState, + inputs: { + ...mockState.inputs, + global: inputState, + }, + }; + const clone = cloneDeep(myMock); + const result1 = getQueriesSelector(myMock, 'global'); + const result2 = getQueriesSelector(clone, 'global'); expect(result1).not.toBe(result2); }); test('returns a different reference even if the contents are the same since query is an array and not a primitive', () => { - const result1 = getQueriesSelector(inputState); - const change: InputsRange = { - ...inputState, - queries: [ - { - loading: false, - id: '1', - inspect: { dsl: [], response: [] }, - isInspected: false, - refetch: null, - selectedInspectIndex: 0, + const myMock = { + ...mockState, + inputs: { + ...mockState.inputs, + global: inputState, + }, + }; + const result1 = getQueriesSelector(myMock, 'global'); + const myMockChange: State = { + ...myMock, + inputs: { + ...mockState.inputs, + global: { + ...mockState.inputs.global, + queries: [ + { + loading: false, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ], }, - ], + }, }; - const result2 = getQueriesSelector(change); + const result2 = getQueriesSelector(myMockChange, 'global'); expect(result1).not.toBe(result2); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.ts b/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.ts index d4b990890ebba..840dd1f4a6b9f 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash'; import { createSelector } from 'reselect'; +import { State } from '../../store'; +import { InputsModelId } from '../../store/inputs/constants'; import { Policy, InputsRange, TimeRange, GlobalQuery } from '../../store/inputs/model'; export const getPolicy = (inputState: InputsRange): Policy => inputState.policy; @@ -13,6 +16,16 @@ export const getTimerange = (inputState: InputsRange): TimeRange => inputState.t export const getQueries = (inputState: InputsRange): GlobalQuery[] => inputState.queries; +export const getGlobalQueries = (state: State, id: InputsModelId): GlobalQuery[] => { + const inputsRange = state.inputs[id]; + return !isEmpty(inputsRange.linkTo) + ? inputsRange.linkTo.reduce((acc, linkToId) => { + const linkToIdInputsRange: InputsRange = state.inputs[linkToId]; + return [...acc, ...linkToIdInputsRange.queries]; + }, inputsRange.queries) + : inputsRange.queries; +}; + export const policySelector = () => createSelector(getPolicy, (policy) => policy.kind); export const durationSelector = () => createSelector(getPolicy, (policy) => policy.duration); @@ -31,7 +44,7 @@ export const isLoadingSelector = () => createSelector(getQueries, (queries) => queries.some((i) => i.loading === true)); export const queriesSelector = () => - createSelector(getQueries, (queries) => queries.filter((q) => q.id !== 'kql')); + createSelector(getGlobalQueries, (queries) => queries.filter((q) => q.id !== 'kql')); export const kqlQuerySelector = () => createSelector(getQueries, (queries) => queries.find((q) => q.id === 'kql')); diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts b/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts index 5d00882f778c0..db91136597215 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts @@ -16,6 +16,8 @@ export const setAbsoluteRangeDatePicker = actionCreator<{ id: InputsModelId; from: string; to: string; + fromStr?: string; + toStr?: string; }>('SET_ABSOLUTE_RANGE_DATE_PICKER'); export const setTimelineRangeDatePicker = actionCreator<{ diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts index a8db48c7b31bb..f4e2c2f28f477 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts @@ -11,8 +11,8 @@ import { Query, Filter, SavedQuery } from '../../../../../../../src/plugins/data export interface AbsoluteTimeRange { kind: 'absolute'; - fromStr: undefined; - toStr: undefined; + fromStr?: string; + toStr?: string; from: string; to: string; } diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/reducer.ts b/x-pack/plugins/security_solution/public/common/store/inputs/reducer.ts index a94f0f6ca24ee..59ae029a9207e 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/reducer.ts @@ -149,16 +149,19 @@ export const inputsReducer = reducerWithInitialState(initialInputsState) }, }; }) - .case(setAbsoluteRangeDatePicker, (state, { id, from, to }) => { - const timerange: TimeRange = { - kind: 'absolute', - fromStr: undefined, - toStr: undefined, - from, - to, - }; - return updateInputTimerange(id, timerange, state); - }) + .case( + setAbsoluteRangeDatePicker, + (state, { id, from, to, fromStr = undefined, toStr = undefined }) => { + const timerange: TimeRange = { + kind: 'absolute', + fromStr, + toStr, + from, + to, + }; + return updateInputTimerange(id, timerange, state); + } + ) .case(setRelativeRangeDatePicker, (state, { id, fromStr, from, to, toStr }) => { const timerange: TimeRange = { kind: 'relative', diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts index e7bd6234cb207..6ebc00133c0cd 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts @@ -86,18 +86,25 @@ export const defaultIndexNamesSelector = () => { return mapStateToProps; }; -const EXLCUDE_ELASTIC_CLOUD_INDEX = '-*elastic-cloud-logs-*'; +const EXCLUDE_ELASTIC_CLOUD_INDEX = '-*elastic-cloud-logs-*'; export const getSourcererScopeSelector = () => { const getScopesSelector = scopesSelector(); - const mapStateToProps = (state: State, scopeId: SourcererScopeName): ManageScope => ({ - ...getScopesSelector(state)[scopeId], - selectedPatterns: getScopesSelector(state)[scopeId].selectedPatterns.some( + const mapStateToProps = (state: State, scopeId: SourcererScopeName): ManageScope => { + const selectedPatterns = getScopesSelector(state)[scopeId].selectedPatterns.some( (index) => index === 'logs-*' ) - ? [...getScopesSelector(state)[scopeId].selectedPatterns, EXLCUDE_ELASTIC_CLOUD_INDEX] - : getScopesSelector(state)[scopeId].selectedPatterns, - }); + ? [...getScopesSelector(state)[scopeId].selectedPatterns, EXCLUDE_ELASTIC_CLOUD_INDEX] + : getScopesSelector(state)[scopeId].selectedPatterns; + return { + ...getScopesSelector(state)[scopeId], + selectedPatterns, + indexPattern: { + ...getScopesSelector(state)[scopeId].indexPattern, + title: selectedPatterns.join(), + }, + }; + }; return mapStateToProps; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx index bf89cc7fa9084..1d8d0f789d6b7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx @@ -20,6 +20,7 @@ import { reputationRenderer, DefaultFieldRenderer, DEFAULT_MORE_MAX_HEIGHT, + DefaultFieldRendererOverflow, MoreContainer, } from './field_renderers'; import { mockData } from '../../../network/components/details/mock'; @@ -330,4 +331,45 @@ describe('Field Renderers', () => { expect(render).toHaveBeenCalledTimes(2); }); }); + + describe('DefaultFieldRendererOverflow', () => { + const idPrefix = 'prefix-1'; + const rowItems = ['item1', 'item2', 'item3', 'item4', 'item5', 'item6', 'item7']; + + test('it should render the length of items after the overflowIndexStart', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.text()).toEqual(' ,+2 More'); + expect(wrapper.find('[data-test-subj="more-container"]').first().exists()).toBe(false); + }); + + test('it should render the items after overflowIndexStart in the popover', () => { + const wrapper = mount( + + + + ); + + wrapper.find('button').first().simulate('click'); + wrapper.update(); + expect(wrapper.find('.euiPopover').first().exists()).toBe(true); + expect(wrapper.find('[data-test-subj="more-container"]').first().text()).toEqual( + 'item6item7' + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx index cb913287b24d8..7f543ab859bb4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx @@ -260,12 +260,12 @@ MoreContainer.displayName = 'MoreContainer'; export const DefaultFieldRendererOverflow = React.memo( ({ idPrefix, moreMaxHeight, overflowIndexStart = 5, render, rowItems }) => { const [isOpen, setIsOpen] = useState(false); - const handleClose = useCallback(() => setIsOpen(false), []); + const togglePopover = useCallback(() => setIsOpen((currentIsOpen) => !currentIsOpen), []); const button = useMemo( () => ( <> {' ,'} - + {`+${rowItems.length - overflowIndexStart} `} ), - [handleClose, overflowIndexStart, rowItems.length] + [togglePopover, overflowIndexStart, rowItems.length] ); return ( @@ -284,7 +284,7 @@ export const DefaultFieldRendererOverflow = React.memo = ({ columnHeaders={columnHeaders} columnRenderers={columnRenderers} containerElementRef={containerElementRef} - disableSensorVisibility={data != null && data.length < 101} docValueFields={docValueFields} event={event} eventIdToNoteIds={eventIdToNoteIds} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 4f385a4656483..83e824aa2450a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -6,7 +6,6 @@ import React, { useRef, useState, useCallback } from 'react'; import uuid from 'uuid'; -import VisibilitySensor from 'react-visibility-sensor'; import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; @@ -19,7 +18,6 @@ import { import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions, TimelineModel } from '../../../../../timelines/store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { SkeletonRow } from '../../skeleton_row'; import { OnColumnResized, OnPinEvent, @@ -38,6 +36,8 @@ import { NoteCards } from '../../../notes/note_cards'; import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context'; import { EventColumnView } from './event_column_view'; import { inputsModel } from '../../../../../common/store'; +import { TimelineId } from '../../../../../../common/types/timeline'; +import { activeTimeline } from '../../../../containers/active_timeline_context'; interface Props { actionsColumnWidth: number; @@ -46,7 +46,6 @@ interface Props { browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; - disableSensorVisibility: boolean; docValueFields: DocValueFields[]; event: TimelineItem; eventIdToNoteIds: Readonly>; @@ -73,33 +72,6 @@ export const getNewNoteId = (): string => uuid.v4(); const emptyDetails: TimelineEventsDetailsItem[] = []; -/** - * This is the default row height whenever it is a plain row renderer and not a custom row height. - * We use this value when we do not know the height of a particular row. - */ -const DEFAULT_ROW_HEIGHT = '32px'; - -/** - * This is the top offset in pixels of the top part of the timeline. The UI area where you do your - * drag and drop and filtering. It is a positive number in pixels of _PART_ of the header but not - * the entire header. We leave room for some rows to render behind the drag and drop so they might be - * visible by the time the user scrolls upwards. All other DOM elements are replaced with their "blank" - * rows. - */ -const TOP_OFFSET = 50; - -/** - * This is the bottom offset in pixels of the bottom part of the timeline. The UI area right below the - * timeline which is the footer. Since the footer is so incredibly small we don't have enough room to - * render around 5 rows below the timeline to get the user the best chance of always scrolling without seeing - * "blank rows". The negative number is to give the bottom of the browser window a bit of invisible space to - * keep around 5 rows rendering below it. All other DOM elements are replaced with their "blank" - * rows. - */ -const BOTTOM_OFFSET = -500; - -const VISIBILITY_SENSOR_OFFSET = { top: TOP_OFFSET, bottom: BOTTOM_OFFSET }; - const emptyNotes: string[] = []; const EventsTrSupplementContainerWrapper = React.memo(({ children }) => { @@ -116,7 +88,6 @@ const StatefulEventComponent: React.FC = ({ containerElementRef, columnHeaders, columnRenderers, - disableSensorVisibility = true, docValueFields, event, eventIdToNoteIds, @@ -138,7 +109,9 @@ const StatefulEventComponent: React.FC = ({ toggleColumn, updateNote, }) => { - const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({}); + const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>( + timelineId === TimelineId.active ? activeTimeline.getExpandedEventIds() : {} + ); const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); const { status: timelineStatus } = useShallowEqualSelector( (state) => state.timeline.timelineById[timelineId] @@ -148,21 +121,21 @@ const StatefulEventComponent: React.FC = ({ docValueFields, indexName: event._index!, eventId: event._id, - skip: !expanded[event._id], + skip: !expanded || !expanded[event._id], }); const onToggleShowNotes = useCallback(() => { const eventId = event._id; - setShowNotes({ ...showNotes, [eventId]: !showNotes[eventId] }); - }, [event, showNotes]); + setShowNotes((prevShowNotes) => ({ ...prevShowNotes, [eventId]: !prevShowNotes[eventId] })); + }, [event]); const onToggleExpanded = useCallback(() => { const eventId = event._id; - setExpanded({ - ...expanded, - [eventId]: !expanded[eventId], - }); - }, [event, expanded]); + setExpanded((prevExpanded) => ({ ...prevExpanded, [eventId]: !prevExpanded[eventId] })); + if (timelineId === TimelineId.active) { + activeTimeline.toggleExpandedEvent(eventId); + } + }, [event._id, timelineId]); const associateNote = useCallback( (noteId: string) => { @@ -174,152 +147,87 @@ const StatefulEventComponent: React.FC = ({ [addNoteToEvent, event, isEventPinned, onPinEvent] ); - // Number of current columns plus one for actions. - const columnCount = columnHeaders.length + 1; - - const VisibilitySensorContent = useCallback( - ({ isVisible }) => { - if (isVisible || disableSensorVisibility) { - return ( - - - - - - - - - {getRowRenderer(event.ecs, rowRenderers).renderRow({ - browserFields, - data: event.ecs, - timelineId, - })} - - - - - - - ); - } else { - // Height place holder for visibility detection as well as re-rendering sections. - const height = - divElement.current != null && divElement.current!.clientHeight - ? `${divElement.current!.clientHeight}px` - : DEFAULT_ROW_HEIGHT; - - return ; - } - }, - [ - actionsColumnWidth, - associateNote, - browserFields, - columnCount, - columnHeaders, - columnRenderers, - detailsData, - disableSensorVisibility, - event._id, - event.data, - event.ecs, - eventIdToNoteIds, - expanded, - getNotesByIds, - isEventPinned, - isEventViewer, - loading, - loadingEventIds, - onColumnResized, - onPinEvent, - onRowSelected, - onToggleExpanded, - onToggleShowNotes, - onUnPinEvent, - onUpdateColumns, - refetch, - onRuleChange, - rowRenderers, - selectedEventIds, - showCheckboxes, - showNotes, - timelineId, - timelineStatus, - toggleColumn, - updateNote, - ] - ); - return ( - - {VisibilitySensorContent} - + + + + + + + + {getRowRenderer(event.ecs, rowRenderers).renderRow({ + browserFields, + data: event.ecs, + timelineId, + })} + + + + + + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 0c7b1e0cdecd5..35d31e034e7f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -27,6 +27,11 @@ export interface OwnProps { export type Props = OwnProps & PropsFromRedux; +const isTimerangeSame = (prevProps: Props, nextProps: Props) => + prevProps.end === nextProps.end && + prevProps.start === nextProps.start && + prevProps.timerangeKind === nextProps.timerangeKind; + const StatefulTimelineComponent = React.memo( ({ columns, @@ -51,6 +56,7 @@ const StatefulTimelineComponent = React.memo( start, status, timelineType, + timerangeKind, updateItemsPerPage, upsertColumn, usersViewing, @@ -125,13 +131,14 @@ const StatefulTimelineComponent = React.memo( status={status} toggleColumn={toggleColumn} timelineType={timelineType} + timerangeKind={timerangeKind} usersViewing={usersViewing} /> ); }, (prevProps, nextProps) => { return ( - prevProps.end === nextProps.end && + isTimerangeSame(prevProps, nextProps) && prevProps.graphEventId === nextProps.graphEventId && prevProps.id === nextProps.id && prevProps.isLive === nextProps.isLive && @@ -142,7 +149,6 @@ const StatefulTimelineComponent = React.memo( prevProps.kqlQueryExpression === nextProps.kqlQueryExpression && prevProps.show === nextProps.show && prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && - prevProps.start === nextProps.start && prevProps.timelineType === nextProps.timelineType && prevProps.status === nextProps.status && deepEqual(prevProps.columns, nextProps.columns) && @@ -209,6 +215,7 @@ const makeMapStateToProps = () => { start: input.timerange.from, status, timelineType, + timerangeKind: input.timerange.kind, }; }; return mapStateToProps; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index 630a71693d182..7fc269c954ac4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -116,6 +116,7 @@ describe('Timeline', () => { start: startDate, status: TimelineStatus.active, timelineType: TimelineType.default, + timerangeKind: 'absolute', toggleColumn: jest.fn(), usersViewing: ['elastic'], }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx index b860011c2ddaf..f7c76c110ac3f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx @@ -112,6 +112,7 @@ export interface Props { start: string; status: TimelineStatusLiteral; timelineType: TimelineType; + timerangeKind: 'absolute' | 'relative'; toggleColumn: (column: ColumnHeaderOptions) => void; usersViewing: string[]; } @@ -143,6 +144,7 @@ export const TimelineComponent: React.FC = ({ status, sort, timelineType, + timerangeKind, toggleColumn, usersViewing, }) => { @@ -212,6 +214,7 @@ export const TimelineComponent: React.FC = ({ startDate: start, skip: !canQueryTimeline, sort: timelineQuerySortField, + timerangeKind, }); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts b/x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts new file mode 100644 index 0000000000000..50bf8b37adf28 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/active_timeline_context.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimelineArgs } from '.'; +import { TimelineEventsAllRequestOptions } from '../../../common/search_strategy/timeline'; + +/* + * Future Engineer + * This class is just there to manage temporarily the reload of the active timeline when switching tabs + * because of the bootstrap of the security solution app, we will always trigger the query + * to avoid it we will cache its request and response so we can go back where the user was before switching tabs + * + * !!! Important !!! this is just there until, we will have a better way to bootstrap the app + * I did not want to put in the store because I was feeling it will feel less temporarily and I did not want other engineer using it + * + */ +class ActiveTimelineEvents { + private _activePage: number = 0; + private _expandedEventIds: Record = {}; + private _pageName: string = ''; + private _request: TimelineEventsAllRequestOptions | null = null; + private _response: TimelineArgs | null = null; + + getActivePage() { + return this._activePage; + } + + setActivePage(activePage: number) { + this._activePage = activePage; + } + + getExpandedEventIds() { + return this._expandedEventIds; + } + + toggleExpandedEvent(eventId: string) { + this._expandedEventIds = { + ...this._expandedEventIds, + [eventId]: !this._expandedEventIds[eventId], + }; + } + + setExpandedEventIds(expandedEventIds: Record) { + this._expandedEventIds = expandedEventIds; + } + + getPageName() { + return this._pageName; + } + + setPageName(pageName: string) { + this._pageName = pageName; + } + + getRequest() { + return this._request; + } + + setRequest(req: TimelineEventsAllRequestOptions) { + this._request = req; + } + + getResponse() { + return this._response; + } + + setResponse(resp: TimelineArgs | null) { + this._response = resp; + } +} + +export const activeTimeline = new ActiveTimelineEvents(); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx new file mode 100644 index 0000000000000..a5f8300546b5b --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -0,0 +1,210 @@ +/* + * 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 { initSortDefault, TimelineArgs, useTimelineEvents, UseTimelineEventsProps } from '.'; +import { SecurityPageName } from '../../../common/constants'; +import { TimelineId } from '../../../common/types/timeline'; +import { mockTimelineData } from '../../common/mock'; +import { useRouteSpy } from '../../common/utils/route/use_route_spy'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +const mockEvents = mockTimelineData.filter((i, index) => index <= 11); + +const mockSearch = jest.fn(); + +jest.mock('../../common/lib/kibana', () => ({ + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + capabilities: { + siem: { + crud: true, + }, + }, + }, + data: { + search: { + search: jest.fn().mockImplementation((args) => { + mockSearch(); + return { + subscribe: jest.fn().mockImplementation(({ next }) => { + next({ + isRunning: false, + isPartial: false, + inspect: { + dsl: [], + response: [], + }, + edges: mockEvents.map((item) => ({ node: item })), + pageInfo: { + activePage: 0, + totalPages: 10, + }, + rawResponse: {}, + totalCount: mockTimelineData.length, + }); + return { unsubscribe: jest.fn() }; + }), + }; + }), + }, + }, + notifications: { + toasts: { + addWarning: jest.fn(), + }, + }, + }, + }), +})); + +const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock; +jest.mock('../../common/utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn(), +})); + +mockUseRouteSpy.mockReturnValue([ + { + pageName: SecurityPageName.overview, + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/overview', + }, +]); + +describe('useTimelineEvents', () => { + beforeEach(() => { + mockSearch.mockReset(); + }); + + const startDate: string = '2020-07-07T08:20:18.966Z'; + const endDate: string = '3000-01-01T00:00:00.000Z'; + const props: UseTimelineEventsProps = { + docValueFields: [], + endDate: '', + id: TimelineId.active, + indexNames: ['filebeat-*'], + fields: [], + filterQuery: '', + startDate: '', + limit: 25, + sort: initSortDefault, + skip: false, + }; + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineEventsProps, + [boolean, TimelineArgs] + >((args) => useTimelineEvents(args), { + initialProps: { ...props }, + }); + + // useEffect on params request + await waitForNextUpdate(); + expect(result.current).toEqual([ + false, + { + events: [], + id: TimelineId.active, + inspect: result.current[1].inspect, + loadPage: result.current[1].loadPage, + pageInfo: result.current[1].pageInfo, + refetch: result.current[1].refetch, + totalCount: -1, + updatedAt: 0, + }, + ]); + }); + }); + + test('happy path query', async () => { + await act(async () => { + const { result, waitForNextUpdate, rerender } = renderHook< + UseTimelineEventsProps, + [boolean, TimelineArgs] + >((args) => useTimelineEvents(args), { + initialProps: { ...props }, + }); + + // useEffect on params request + await waitForNextUpdate(); + rerender({ ...props, startDate, endDate }); + // useEffect on params request + await waitForNextUpdate(); + + expect(mockSearch).toHaveBeenCalledTimes(1); + expect(result.current).toEqual([ + false, + { + events: mockEvents, + id: TimelineId.active, + inspect: result.current[1].inspect, + loadPage: result.current[1].loadPage, + pageInfo: result.current[1].pageInfo, + refetch: result.current[1].refetch, + totalCount: 31, + updatedAt: result.current[1].updatedAt, + }, + ]); + }); + }); + + test('Mock cache for active timeline when switching page', async () => { + await act(async () => { + const { result, waitForNextUpdate, rerender } = renderHook< + UseTimelineEventsProps, + [boolean, TimelineArgs] + >((args) => useTimelineEvents(args), { + initialProps: { ...props }, + }); + + // useEffect on params request + await waitForNextUpdate(); + rerender({ ...props, startDate, endDate }); + // useEffect on params request + await waitForNextUpdate(); + + mockUseRouteSpy.mockReturnValue([ + { + pageName: SecurityPageName.timelines, + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/timelines', + }, + ]); + + expect(mockSearch).toHaveBeenCalledTimes(1); + + expect(result.current).toEqual([ + false, + { + events: mockEvents, + id: TimelineId.active, + inspect: result.current[1].inspect, + loadPage: result.current[1].loadPage, + pageInfo: result.current[1].pageInfo, + refetch: result.current[1].refetch, + totalCount: 31, + updatedAt: result.current[1].updatedAt, + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 65f8a3dc78e4d..5f92596f03311 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -30,6 +30,9 @@ import { } from '../../../common/search_strategy'; import { InspectResponse } from '../../types'; import * as i18n from './translations'; +import { TimelineId } from '../../../common/types/timeline'; +import { useRouteSpy } from '../../common/utils/route/use_route_spy'; +import { activeTimeline } from './active_timeline_context'; export interface TimelineArgs { events: TimelineItem[]; @@ -44,7 +47,7 @@ export interface TimelineArgs { type LoadPage = (newActivePage: number) => void; -interface UseTimelineEventsProps { +export interface UseTimelineEventsProps { docValueFields?: DocValueFields[]; filterQuery?: ESQuery | string; skip?: boolean; @@ -55,17 +58,26 @@ interface UseTimelineEventsProps { limit: number; sort: SortField; startDate: string; + timerangeKind?: 'absolute' | 'relative'; } const getTimelineEvents = (timelineEdges: TimelineEdges[]): TimelineItem[] => timelineEdges.map((e: TimelineEdges) => e.node); const ID = 'timelineEventsQuery'; -const initSortDefault = { +export const initSortDefault = { field: '@timestamp', direction: Direction.asc, }; +function usePreviousRequest(value: TimelineEventsAllRequestOptions | null) { + const ref = useRef(value); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} + export const useTimelineEvents = ({ docValueFields, endDate, @@ -77,13 +89,17 @@ export const useTimelineEvents = ({ limit, sort = initSortDefault, skip = false, + timerangeKind, }: UseTimelineEventsProps): [boolean, TimelineArgs] => { + const [{ pageName }] = useRouteSpy(); const dispatch = useDispatch(); const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); - const [activePage, setActivePage] = useState(0); + const [activePage, setActivePage] = useState( + id === TimelineId.active ? activeTimeline.getActivePage() : 0 + ); const [timelineRequest, setTimelineRequest] = useState( !skip ? { @@ -106,6 +122,7 @@ export const useTimelineEvents = ({ } : null ); + const prevTimelineRequest = usePreviousRequest(timelineRequest); const clearSignalsState = useCallback(() => { if (id != null && detectionsTimelineIds.some((timelineId) => timelineId === id)) { @@ -117,18 +134,31 @@ export const useTimelineEvents = ({ const wrappedLoadPage = useCallback( (newActivePage: number) => { clearSignalsState(); + + if (id === TimelineId.active) { + activeTimeline.setExpandedEventIds({}); + activeTimeline.setActivePage(newActivePage); + } + setActivePage(newActivePage); }, - [clearSignalsState] + [clearSignalsState, id] ); + const refetchGrid = useCallback(() => { + if (refetch.current != null) { + refetch.current(); + } + wrappedLoadPage(0); + }, [wrappedLoadPage]); + const [timelineResponse, setTimelineResponse] = useState({ - id: ID, + id, inspect: { dsl: [], response: [], }, - refetch: refetch.current, + refetch: refetchGrid, totalCount: -1, pageInfo: { activePage: 0, @@ -141,15 +171,13 @@ export const useTimelineEvents = ({ const timelineSearch = useCallback( (request: TimelineEventsAllRequestOptions | null) => { - if (request == null) { + if (request == null || pageName === '') { return; } - let didCancel = false; const asyncSearch = async () => { abortCtrl.current = new AbortController(); setLoading(true); - const searchSubscription$ = data.search .search(request, { strategy: 'securitySolutionTimelineSearchStrategy', @@ -157,26 +185,39 @@ export const useTimelineEvents = ({ }) .subscribe({ next: (response) => { - if (isCompleteResponse(response)) { - if (!didCancel) { - setLoading(false); - setTimelineResponse((prevResponse) => ({ - ...prevResponse, - events: getTimelineEvents(response.edges), - inspect: getInspectResponse(response, prevResponse.inspect), - pageInfo: response.pageInfo, - refetch: refetch.current, - totalCount: response.totalCount, - updatedAt: Date.now(), - })); - } - searchSubscription$.unsubscribe(); - } else if (isErrorResponse(response)) { - if (!didCancel) { - setLoading(false); + try { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + + setTimelineResponse((prevResponse) => { + const newTimelineResponse = { + ...prevResponse, + events: getTimelineEvents(response.edges), + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + totalCount: response.totalCount, + updatedAt: Date.now(), + }; + if (id === TimelineId.active) { + activeTimeline.setExpandedEventIds({}); + activeTimeline.setPageName(pageName); + activeTimeline.setRequest(request); + activeTimeline.setResponse(newTimelineResponse); + } + return newTimelineResponse; + }); + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + notifications.toasts.addWarning(i18n.ERROR_TIMELINE_EVENTS); + searchSubscription$.unsubscribe(); } + } catch { notifications.toasts.addWarning(i18n.ERROR_TIMELINE_EVENTS); - searchSubscription$.unsubscribe(); } }, error: (msg) => { @@ -189,15 +230,43 @@ export const useTimelineEvents = ({ }, }); }; + + if ( + id === TimelineId.active && + activeTimeline.getPageName() !== '' && + pageName !== activeTimeline.getPageName() + ) { + activeTimeline.setPageName(pageName); + + abortCtrl.current.abort(); + setLoading(false); + refetch.current = asyncSearch.bind(null, activeTimeline.getRequest()); + setTimelineResponse((prevResp) => { + const resp = activeTimeline.getResponse(); + if (resp != null) { + return { + ...resp, + refetch: refetchGrid, + loadPage: wrappedLoadPage, + }; + } + return prevResp; + }); + if (activeTimeline.getResponse() != null) { + return; + } + } + abortCtrl.current.abort(); asyncSearch(); refetch.current = asyncSearch; + return () => { didCancel = true; abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, id, notifications.toasts, pageName, refetchGrid, wrappedLoadPage] ); useEffect(() => { @@ -251,8 +320,10 @@ export const useTimelineEvents = ({ if (activePage !== newActivePage) { setActivePage(newActivePage); + if (id === TimelineId.active) { + activeTimeline.setActivePage(newActivePage); + } } - if ( !skip && !skipQueryForDetectionsPage(id, indexNames) && @@ -263,12 +334,13 @@ export const useTimelineEvents = ({ return prevRequest; }); }, [ + dispatch, + indexNames, activePage, docValueFields, endDate, filterQuery, id, - indexNames, limit, startDate, sort, @@ -277,8 +349,13 @@ export const useTimelineEvents = ({ ]); useEffect(() => { - timelineSearch(timelineRequest); - }, [timelineRequest, timelineSearch]); + if ( + id !== TimelineId.active || + timerangeKind === 'absolute' || + !deepEqual(prevTimelineRequest, timelineRequest) + ) + timelineSearch(timelineRequest); + }, [id, prevTimelineRequest, timelineRequest, timelineSearch, timerangeKind]); return [loading, timelineResponse]; }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 1992b1f88f064..d6597df71526f 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -102,6 +102,7 @@ describe('epicLocalStorage', () => { status: TimelineStatus.active, sort, timelineType: TimelineType.default, + timerangeKind: 'absolute', toggleColumn: jest.fn(), usersViewing: ['elastic'], }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 30d0796443ab5..d4e807b4a9a07 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -26,12 +26,14 @@ import { TimelineTypeLiteral, TimelineType, RowRendererId, + TimelineId, } from '../../../../common/types/timeline'; import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; import { timelineDefaults } from './defaults'; import { ColumnHeaderOptions, KqlMode, TimelineModel } from './model'; import { TimelineById } from './types'; +import { activeTimeline } from '../../containers/active_timeline_context'; export const isNotNull = (value: T | null): value is T => value !== null; @@ -113,6 +115,17 @@ interface AddTimelineParams { timelineById: TimelineById; } +export const shouldResetActiveTimelineContext = ( + id: string, + oldTimeline: TimelineModel, + newTimeline: TimelineModel +) => { + if (id === TimelineId.active && oldTimeline.savedObjectId !== newTimeline.savedObjectId) { + return true; + } + return false; +}; + /** * Add a saved object timeline to the store * and default the value to what need to be if values are null @@ -121,13 +134,19 @@ export const addTimelineToStore = ({ id, timeline, timelineById, -}: AddTimelineParams): TimelineById => ({ - ...timelineById, - [id]: { - ...timeline, - isLoading: timelineById[id].isLoading, - }, -}); +}: AddTimelineParams): TimelineById => { + if (shouldResetActiveTimelineContext(id, timelineById[id], timeline)) { + activeTimeline.setActivePage(0); + activeTimeline.setExpandedEventIds({}); + } + return { + ...timelineById, + [id]: { + ...timeline, + isLoading: timelineById[id].isLoading, + }, + }; +}; interface AddNewTimelineParams { columns: ColumnHeaderOptions[]; From 1ecd12cdf32ef41a370af7064467dd3047529f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 5 Nov 2020 19:50:50 -0500 Subject: [PATCH 45/57] Add description and documentation link in alert flyout (#81526) * Add description and documentation URL in alert flyout * Add unit tests * Fix type check * Add horizontal rule * Design fixes * Fix uptime alert link * Fix uptime urls * Add anchor tag * Fix jest test failures * Fix monitoring links --- .../public/alert_types/always_firing.tsx | 1 + .../public/alert_types/astros.tsx | 1 + .../alerting/register_apm_alerts.ts | 12 +++++++ .../infra/public/alerting/inventory/index.ts | 3 ++ .../log_threshold/log_threshold_alert_type.ts | 3 ++ .../public/alerting/metric_threshold/index.ts | 3 ++ .../cpu_usage_alert/cpu_usage_alert.tsx | 3 ++ .../public/alerts/disk_usage_alert/index.tsx | 3 ++ .../alerts/legacy_alert/legacy_alert.tsx | 3 ++ .../alerts/memory_usage_alert/index.tsx | 3 ++ .../missing_monitoring_data_alert.tsx | 3 ++ .../thread_pool_rejections_alert/index.tsx | 3 ++ .../geo_threshold/index.ts | 2 ++ .../threshold/expression.tsx | 1 - .../builtin_alert_types/threshold/index.ts | 3 ++ .../sections/alert_form/alert_add.test.tsx | 1 + .../sections/alert_form/alert_edit.test.tsx | 1 + .../sections/alert_form/alert_form.test.tsx | 36 ++++++++++++++++++- .../sections/alert_form/alert_form.tsx | 29 +++++++++++++++ .../components/alerts_list.test.tsx | 1 + .../public/application/type_registry.test.ts | 1 + .../triggers_actions_ui/public/types.ts | 1 + .../__tests__/monitor_status.test.ts | 1 + .../lib/alert_types/duration_anomaly.tsx | 3 ++ .../public/lib/alert_types/monitor_status.tsx | 3 ++ .../uptime/public/lib/alert_types/tls.tsx | 3 ++ .../fixtures/plugins/alerts/public/plugin.ts | 2 ++ 27 files changed, 127 insertions(+), 2 deletions(-) diff --git a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index 9c420f4425d04..a5d158fca836b 100644 --- a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -22,6 +22,7 @@ export function getAlertType(): AlertTypeModel { name: 'Always Fires', description: 'Alert when called', iconClass: 'bolt', + documentationUrl: null, alertParamsExpression: AlwaysFiringExpression, validate: (alertParams: AlwaysFiringParamsProps['alertParams']) => { const { instances } = alertParams; diff --git a/x-pack/examples/alerting_example/public/alert_types/astros.tsx b/x-pack/examples/alerting_example/public/alert_types/astros.tsx index 343f6b10ef85b..73c7dfea1263b 100644 --- a/x-pack/examples/alerting_example/public/alert_types/astros.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/astros.tsx @@ -47,6 +47,7 @@ export function getAlertType(): AlertTypeModel { name: 'People Are In Space Right Now', description: 'Alert when people are in space right now', iconClass: 'globe', + documentationUrl: null, alertParamsExpression: PeopleinSpaceExpression, validate: (alertParams: PeopleinSpaceParamsProps['alertParams']) => { const { outerSpaceCapacity, craft, op } = alertParams; diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 0eeb31927b2f5..988e335af5b7c 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -22,6 +22,9 @@ export function registerApmAlerts( 'Alert when the number of errors in a service exceeds a defined threshold.', }), iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; + }, alertParamsExpression: lazy(() => import('./ErrorCountAlertTrigger')), validate: () => ({ errors: [], @@ -53,6 +56,9 @@ export function registerApmAlerts( } ), iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; + }, alertParamsExpression: lazy( () => import('./TransactionDurationAlertTrigger') ), @@ -87,6 +93,9 @@ export function registerApmAlerts( } ), iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; + }, alertParamsExpression: lazy( () => import('./TransactionErrorRateAlertTrigger') ), @@ -121,6 +130,9 @@ export function registerApmAlerts( } ), iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; + }, alertParamsExpression: lazy( () => import('./TransactionDurationAnomalyAlertTrigger') ), diff --git a/x-pack/plugins/infra/public/alerting/inventory/index.ts b/x-pack/plugins/infra/public/alerting/inventory/index.ts index b49465db05135..d7afd73c0e3a7 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/index.ts +++ b/x-pack/plugins/infra/public/alerting/inventory/index.ts @@ -21,6 +21,9 @@ export function createInventoryMetricAlertType(): AlertTypeModel { defaultMessage: 'Alert when the inventory exceeds a defined threshold.', }), iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/infrastructure-threshold-alert.html`; + }, alertParamsExpression: React.lazy(() => import('./components/expression')), validate: validateMetricThreshold, defaultActionMessage: i18n.translate( diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts index 2e4cb2a53b6b5..60c22c42c00b6 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts @@ -19,6 +19,9 @@ export function getAlertType(): AlertTypeModel { defaultMessage: 'Alert when the log aggregation exceeds the threshold.', }), iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/logs-threshold-alert.html`; + }, alertParamsExpression: React.lazy(() => import('./components/expression_editor/editor')), validate: validateExpression, defaultActionMessage: i18n.translate( diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts index a48837792a3cc..05c69e5ccb78b 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts @@ -21,6 +21,9 @@ export function createMetricThresholdAlertType(): AlertTypeModel { defaultMessage: 'Alert when the metrics aggregation exceeds the threshold.', }), iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/metrics-threshold-alert.html`; + }, alertParamsExpression: React.lazy(() => import('./components/expression')), validate: validateMetricThreshold, defaultActionMessage: i18n.translate( diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx index 11ba8214ff81e..5054c47245f0f 100644 --- a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx @@ -16,6 +16,9 @@ export function createCpuUsageAlertType(): AlertTypeModel { name: ALERT_DETAILS[ALERT_CPU_USAGE].label, description: ALERT_DETAILS[ALERT_CPU_USAGE].description, iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cpu-threshold`; + }, alertParamsExpression: (props: Props) => ( ), diff --git a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx index 7c44e37904ec5..00b70658e4289 100644 --- a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx @@ -18,6 +18,9 @@ export function createDiskUsageAlertType(): AlertTypeModel { name: ALERT_DETAILS[ALERT_DISK_USAGE].label, description: ALERT_DETAILS[ALERT_DISK_USAGE].description, iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-disk-usage-threshold`; + }, alertParamsExpression: (props: Props) => ( ), diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx index ca7af2fe64e78..c8d0a7a5d49f2 100644 --- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -18,6 +18,9 @@ export function createLegacyAlertTypes(): AlertTypeModel[] { name: LEGACY_ALERT_DETAILS[legacyAlert].label, description: LEGACY_ALERT_DETAILS[legacyAlert].description, iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/cluster-alerts.html`; + }, alertParamsExpression: () => ( diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx index 14fb7147179c1..062c32c758794 100644 --- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx @@ -18,6 +18,9 @@ export function createMemoryUsageAlertType(): AlertTypeModel { name: ALERT_DETAILS[ALERT_MEMORY_USAGE].label, description: ALERT_DETAILS[ALERT_MEMORY_USAGE].description, iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-jvm-memory-threshold`; + }, alertParamsExpression: (props: Props) => ( ), diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx index 4c8f00f8385c2..ec97a45a8a800 100644 --- a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx @@ -16,6 +16,9 @@ export function createMissingMonitoringDataAlertType(): AlertTypeModel { name: ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].label, description: ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].description, iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-missing-monitoring-data`; + }, alertParamsExpression: (props: any) => ( ( <> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/index.ts index 9f33e2c2495c5..00d9ebbbbc066 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/index.ts @@ -20,6 +20,8 @@ export function getAlertType(): AlertTypeModel import('./query_builder')), validate: validateExpression, requiresAppContext: false, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index 7c42c43dc79a2..e309d97b57f34 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -281,7 +281,6 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent ) : null} -
    import('./expression')), validate: validateExpression, requiresAppContext: false, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 2a69580d7185c..d66c5ba5121b8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -99,6 +99,7 @@ describe('alert_add', () => { iconClass: 'test', name: 'test-alert', description: 'test', + documentationUrl: null, validate: (): ValidationResult => { return { errors: {} }; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index 34f9f29274f8f..31c61f0bba768 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -52,6 +52,7 @@ describe('alert_edit', () => { iconClass: 'test', name: 'test-alert', description: 'test', + documentationUrl: null, validate: (): ValidationResult => { return { errors: {} }; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 98eaea64797b2..4041f6f451a23 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -31,7 +31,8 @@ describe('alert_form', () => { id: 'my-alert-type', iconClass: 'test', name: 'test-alert', - description: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', validate: (): ValidationResult => { return { errors: {} }; }, @@ -59,6 +60,7 @@ describe('alert_form', () => { iconClass: 'test', name: 'non edit alert', description: 'test', + documentationUrl: null, validate: (): ValidationResult => { return { errors: {} }; }, @@ -182,6 +184,22 @@ describe('alert_form', () => { ); expect(alertTypeSelectOptions.exists()).toBeFalsy(); }); + + it('renders alert type description', async () => { + await setup(); + wrapper.find('[data-test-subj="my-alert-type-SelectOption"]').first().simulate('click'); + const alertDescription = wrapper.find('[data-test-subj="alertDescription"]'); + expect(alertDescription.exists()).toBeTruthy(); + expect(alertDescription.first().text()).toContain('Alert when testing'); + }); + + it('renders alert type documentation link', async () => { + await setup(); + wrapper.find('[data-test-subj="my-alert-type-SelectOption"]').first().simulate('click'); + const alertDocumentationLink = wrapper.find('[data-test-subj="alertDocumentationLink"]'); + expect(alertDocumentationLink.exists()).toBeTruthy(); + expect(alertDocumentationLink.first().prop('href')).toBe('https://localhost.local/docs'); + }); }); describe('alert_form create alert non alerting consumer and producer', () => { @@ -244,6 +262,7 @@ describe('alert_form', () => { iconClass: 'test', name: 'test-alert', description: 'test', + documentationUrl: null, validate: (): ValidationResult => { return { errors: {} }; }, @@ -255,6 +274,7 @@ describe('alert_form', () => { iconClass: 'test', name: 'test-alert', description: 'test', + documentationUrl: null, validate: (): ValidationResult => { return { errors: {} }; }, @@ -423,5 +443,19 @@ describe('alert_form', () => { const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]'); expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle); }); + + it('renders alert type description', async () => { + await setup(); + const alertDescription = wrapper.find('[data-test-subj="alertDescription"]'); + expect(alertDescription.exists()).toBeTruthy(); + expect(alertDescription.first().text()).toContain('Alert when testing'); + }); + + it('renders alert type documentation link', async () => { + await setup(); + const alertDocumentationLink = wrapper.find('[data-test-subj="alertDocumentationLink"]'); + expect(alertDocumentationLink.exists()).toBeTruthy(); + expect(alertDocumentationLink.first().prop('href')).toBe('https://localhost.local/docs'); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index bdc11fd543ee1..9a637ea750f81 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -25,6 +25,8 @@ import { EuiHorizontalRule, EuiLoadingSpinner, EuiEmptyPrompt, + EuiLink, + EuiText, } from '@elastic/eui'; import { some, filter, map, fold } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -247,6 +249,33 @@ export const AlertForm = ({ ) : null} + {alertTypeModel?.description && ( + + + + {alertTypeModel.description}  + {alertTypeModel?.documentationUrl && ( + + + + )} + + + + )} + {AlertParamsExpressionComponent ? ( }> { return { errors: {} }; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts index 311f366df74e0..f875bcabdcde8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts @@ -17,6 +17,7 @@ const getTestAlertType = (id?: string, name?: string, iconClass?: string) => { name: name || 'Test alert type', description: 'Test description', iconClass: iconClass || 'icon', + documentationUrl: null, validate: (): ValidationResult => { return { errors: {} }; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index bf1ff26af42e2..1a6b68080c9a4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -176,6 +176,7 @@ export interface AlertTypeModel name: string | JSX.Element; description: string; iconClass: string; + documentationUrl: string | ((docLinks: DocLinksStart) => string) | null; validate: (alertParams: AlertParamsType) => ValidationResult; alertParamsExpression: | React.FunctionComponent diff --git a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts index 5106fcbc97bcd..8da45276fa532 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts @@ -204,6 +204,7 @@ describe('monitor status alert type', () => { "alertParamsExpression": [Function], "defaultActionMessage": "Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}}", "description": "Alert when a monitor is down or an availability threshold is breached.", + "documentationUrl": [Function], "iconClass": "uptimeApp", "id": "xpack.uptime.alerts.monitorStatus", "name": ({ id: CLIENT_ALERT_TYPES.DURATION_ANOMALY, iconClass: 'uptimeApp', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/uptime/${docLinks.DOC_LINK_VERSION}/uptime-alerting.html`; + }, alertParamsExpression: (params: unknown) => ( ), diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index 4e3d9a3c6e0ac..43aaa26d86642 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -31,6 +31,9 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({ ), description, iconClass: 'uptimeApp', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/uptime/${docLinks.DOC_LINK_VERSION}/uptime-alerting.html#_monitor_status_alerts`; + }, alertParamsExpression: (params: any) => ( ), diff --git a/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx b/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx index 41ff08b0da97c..83c4792e26f59 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx @@ -15,6 +15,9 @@ const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert')); export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): AlertTypeModel => ({ id: CLIENT_ALERT_TYPES.TLS, iconClass: 'uptimeApp', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/uptime/${docLinks.DOC_LINK_VERSION}/uptime-alerting.html#_tls_alerts`; + }, alertParamsExpression: (params: any) => ( ), diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts index c738ce0697f75..af4aedda06ef7 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts @@ -31,6 +31,7 @@ export class AlertingFixturePlugin implements Plugin React.createElement('div', null, 'Test Always Firing'), validate: () => { return { errors: {} }; @@ -43,6 +44,7 @@ export class AlertingFixturePlugin implements Plugin React.createElement('div', null, 'Test Noop'), validate: () => { return { errors: {} }; From d6200462c6ecd8d80d47291a3d0a9b7b85e56f68 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 5 Nov 2020 19:05:41 -0600 Subject: [PATCH 46/57] Add APM OSS README (#82754) --- docs/developer/plugin-list.asciidoc | 4 +--- src/plugins/apm_oss/README.asciidoc | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 src/plugins/apm_oss/README.asciidoc diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 9235fc1198b12..b59545cbb85a6 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -28,9 +28,7 @@ allowing users to configure their advanced settings, also known as uiSettings within the code. -|{kib-repo}blob/{branch}/src/plugins/apm_oss[apmOss] -|WARNING: Missing README. - +|{kib-repo}blob/{branch}/src/plugins/apm_oss/README.asciidoc[apmOss] |{kib-repo}blob/{branch}/src/plugins/bfetch/README.md[bfetch] |bfetch allows to batch HTTP requests and streams responses back. diff --git a/src/plugins/apm_oss/README.asciidoc b/src/plugins/apm_oss/README.asciidoc new file mode 100644 index 0000000000000..c3c060a99ee27 --- /dev/null +++ b/src/plugins/apm_oss/README.asciidoc @@ -0,0 +1,5 @@ +# APM OSS plugin + +OSS plugin for APM. Includes index configuration and tutorial resources. + +See <<../../x-pack/plugins/apm/readme.md,the X-Pack APM plugin README>> for information about the main APM plugin. From e378555971afeac14bead8949da95389144bafe5 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Thu, 5 Nov 2020 21:25:57 -0700 Subject: [PATCH 47/57] Revert "Adds cloud links to user popover (#66825)" (#82802) This reverts commit 8cdf56636aa5fd7453922714cd0ce01040d103d4. --- x-pack/plugins/cloud/kibana.json | 2 +- x-pack/plugins/cloud/public/index.ts | 2 +- x-pack/plugins/cloud/public/mocks.ts | 18 --- x-pack/plugins/cloud/public/plugin.ts | 28 +--- .../plugins/cloud/public/user_menu_links.ts | 38 ----- x-pack/plugins/cloud/server/config.ts | 2 - x-pack/plugins/security/public/index.ts | 1 - x-pack/plugins/security/public/mocks.ts | 7 - .../security/public/nav_control/index.mock.ts | 14 -- .../security/public/nav_control/index.ts | 3 +- .../nav_control/nav_control_component.scss | 11 -- .../nav_control_component.test.tsx | 38 ----- .../nav_control/nav_control_component.tsx | 139 ++++++------------ .../nav_control/nav_control_service.tsx | 39 +---- .../plugins/security/public/plugin.test.tsx | 7 +- x-pack/plugins/security/public/plugin.tsx | 4 +- 16 files changed, 61 insertions(+), 292 deletions(-) delete mode 100644 x-pack/plugins/cloud/public/mocks.ts delete mode 100644 x-pack/plugins/cloud/public/user_menu_links.ts delete mode 100644 x-pack/plugins/security/public/nav_control/index.mock.ts delete mode 100644 x-pack/plugins/security/public/nav_control/nav_control_component.scss diff --git a/x-pack/plugins/cloud/kibana.json b/x-pack/plugins/cloud/kibana.json index 9bca2f30bd23c..27b35bcbdd88b 100644 --- a/x-pack/plugins/cloud/kibana.json +++ b/x-pack/plugins/cloud/kibana.json @@ -3,7 +3,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "cloud"], - "optionalPlugins": ["usageCollection", "home", "security"], + "optionalPlugins": ["usageCollection", "home"], "server": true, "ui": true } diff --git a/x-pack/plugins/cloud/public/index.ts b/x-pack/plugins/cloud/public/index.ts index 680b2f1ad2bd6..39ef5f452c18b 100644 --- a/x-pack/plugins/cloud/public/index.ts +++ b/x-pack/plugins/cloud/public/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext } from '../../../../src/core/public'; import { CloudPlugin } from './plugin'; -export { CloudSetup, CloudConfigType } from './plugin'; +export { CloudSetup } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new CloudPlugin(initializerContext); } diff --git a/x-pack/plugins/cloud/public/mocks.ts b/x-pack/plugins/cloud/public/mocks.ts deleted file mode 100644 index bafebbca4ecdd..0000000000000 --- a/x-pack/plugins/cloud/public/mocks.ts +++ /dev/null @@ -1,18 +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. - */ - -function createSetupMock() { - return { - cloudId: 'mock-cloud-id', - isCloudEnabled: true, - resetPasswordUrl: 'reset-password-url', - accountUrl: 'account-url', - }; -} - -export const cloudMock = { - createSetup: createSetupMock, -}; diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index bc410b89c30e7..45005f3f5e422 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -6,51 +6,40 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { i18n } from '@kbn/i18n'; -import { SecurityPluginStart } from '../../security/public'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { ELASTIC_SUPPORT_LINK } from '../common/constants'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; -import { createUserMenuLinks } from './user_menu_links'; -export interface CloudConfigType { +interface CloudConfigType { id?: string; resetPasswordUrl?: string; deploymentUrl?: string; - accountUrl?: string; } interface CloudSetupDependencies { home?: HomePublicPluginSetup; } -interface CloudStartDependencies { - security?: SecurityPluginStart; -} - export interface CloudSetup { cloudId?: string; cloudDeploymentUrl?: string; isCloudEnabled: boolean; - resetPasswordUrl?: string; - accountUrl?: string; } export class CloudPlugin implements Plugin { private config!: CloudConfigType; - private isCloudEnabled: boolean; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); - this.isCloudEnabled = false; } public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { const { id, resetPasswordUrl, deploymentUrl } = this.config; - this.isCloudEnabled = getIsCloudEnabled(id); + const isCloudEnabled = getIsCloudEnabled(id); if (home) { - home.environment.update({ cloud: this.isCloudEnabled }); - if (this.isCloudEnabled) { + home.environment.update({ cloud: isCloudEnabled }); + if (isCloudEnabled) { home.tutorials.setVariable('cloud', { id, resetPasswordUrl }); } } @@ -58,11 +47,11 @@ export class CloudPlugin implements Plugin { return { cloudId: id, cloudDeploymentUrl: deploymentUrl, - isCloudEnabled: this.isCloudEnabled, + isCloudEnabled, }; } - public start(coreStart: CoreStart, { security }: CloudStartDependencies) { + public start(coreStart: CoreStart) { const { deploymentUrl } = this.config; coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK); if (deploymentUrl) { @@ -74,10 +63,5 @@ export class CloudPlugin implements Plugin { href: deploymentUrl, }); } - - if (security && this.isCloudEnabled) { - const userMenuLinks = createUserMenuLinks(this.config); - security.navControlService.addUserMenuLinks(userMenuLinks); - } } } diff --git a/x-pack/plugins/cloud/public/user_menu_links.ts b/x-pack/plugins/cloud/public/user_menu_links.ts deleted file mode 100644 index 15e2f14e885ba..0000000000000 --- a/x-pack/plugins/cloud/public/user_menu_links.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { UserMenuLink } from '../../security/public'; -import { CloudConfigType } from '.'; - -export const createUserMenuLinks = (config: CloudConfigType): UserMenuLink[] => { - const { resetPasswordUrl, accountUrl } = config; - const userMenuLinks = [] as UserMenuLink[]; - - if (resetPasswordUrl) { - userMenuLinks.push({ - label: i18n.translate('xpack.cloud.userMenuLinks.profileLinkText', { - defaultMessage: 'Cloud profile', - }), - iconType: 'logoCloud', - href: resetPasswordUrl, - order: 100, - }); - } - - if (accountUrl) { - userMenuLinks.push({ - label: i18n.translate('xpack.cloud.userMenuLinks.accountLinkText', { - defaultMessage: 'Account & Billing', - }), - iconType: 'gear', - href: accountUrl, - order: 200, - }); - } - - return userMenuLinks; -}; diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index eaa4ab7a482dd..ff8a2c5acdf9a 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -23,7 +23,6 @@ const configSchema = schema.object({ apm: schema.maybe(apmConfigSchema), resetPasswordUrl: schema.maybe(schema.string()), deploymentUrl: schema.maybe(schema.string()), - accountUrl: schema.maybe(schema.string()), }); export type CloudConfigType = TypeOf; @@ -33,7 +32,6 @@ export const config: PluginConfigDescriptor = { id: true, resetPasswordUrl: true, deploymentUrl: true, - accountUrl: true, }, schema: configSchema, }; diff --git a/x-pack/plugins/security/public/index.ts b/x-pack/plugins/security/public/index.ts index d0382c22ed3c6..8016c94224060 100644 --- a/x-pack/plugins/security/public/index.ts +++ b/x-pack/plugins/security/public/index.ts @@ -16,7 +16,6 @@ import { export { SecurityPluginSetup, SecurityPluginStart }; export { AuthenticatedUser } from '../common/model'; export { SecurityLicense, SecurityLicenseFeatures } from '../common/licensing'; -export { UserMenuLink } from '../public/nav_control'; export const plugin: PluginInitializer< SecurityPluginSetup, diff --git a/x-pack/plugins/security/public/mocks.ts b/x-pack/plugins/security/public/mocks.ts index 26a759ca52267..33c1d1446afba 100644 --- a/x-pack/plugins/security/public/mocks.ts +++ b/x-pack/plugins/security/public/mocks.ts @@ -7,7 +7,6 @@ import { authenticationMock } from './authentication/index.mock'; import { createSessionTimeoutMock } from './session/session_timeout.mock'; import { licenseMock } from '../common/licensing/index.mock'; -import { navControlServiceMock } from './nav_control/index.mock'; function createSetupMock() { return { @@ -16,13 +15,7 @@ function createSetupMock() { license: licenseMock.create(), }; } -function createStartMock() { - return { - navControlService: navControlServiceMock.createStart(), - }; -} export const securityMock = { createSetup: createSetupMock, - createStart: createStartMock, }; diff --git a/x-pack/plugins/security/public/nav_control/index.mock.ts b/x-pack/plugins/security/public/nav_control/index.mock.ts deleted file mode 100644 index 1cd10810d7c8f..0000000000000 --- a/x-pack/plugins/security/public/nav_control/index.mock.ts +++ /dev/null @@ -1,14 +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 { SecurityNavControlServiceStart } from '.'; - -export const navControlServiceMock = { - createStart: (): jest.Mocked => ({ - getUserMenuLinks$: jest.fn(), - addUserMenuLinks: jest.fn(), - }), -}; diff --git a/x-pack/plugins/security/public/nav_control/index.ts b/x-pack/plugins/security/public/nav_control/index.ts index 737ae50054698..2b0af1a45d05a 100644 --- a/x-pack/plugins/security/public/nav_control/index.ts +++ b/x-pack/plugins/security/public/nav_control/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SecurityNavControlService, SecurityNavControlServiceStart } from './nav_control_service'; -export { UserMenuLink } from './nav_control_component'; +export { SecurityNavControlService } from './nav_control_service'; diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.scss b/x-pack/plugins/security/public/nav_control/nav_control_component.scss deleted file mode 100644 index a3e04b08cfac2..0000000000000 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.scss +++ /dev/null @@ -1,11 +0,0 @@ -.chrNavControl__userMenu { - .euiContextMenuPanelTitle { - // Uppercased by default, override to match actual username - text-transform: none; - } - - .euiContextMenuItem { - // Temp fix for EUI issue https://github.com/elastic/eui/issues/3092 - line-height: normal; - } -} \ No newline at end of file diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx index 1da91e80d062d..c1c6a9f69b6ec 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import { BehaviorSubject } from 'rxjs'; import { shallowWithIntl, nextTick, mountWithIntl } from 'test_utils/enzyme_helpers'; import { SecurityNavControl } from './nav_control_component'; import { AuthenticatedUser } from '../../common/model'; @@ -18,7 +17,6 @@ describe('SecurityNavControl', () => { user: new Promise(() => {}) as Promise, editProfileUrl: '', logoutUrl: '', - userMenuLinks$: new BehaviorSubject([]), }; const wrapper = shallowWithIntl(); @@ -44,7 +42,6 @@ describe('SecurityNavControl', () => { user: Promise.resolve({ full_name: 'foo' }) as Promise, editProfileUrl: '', logoutUrl: '', - userMenuLinks$: new BehaviorSubject([]), }; const wrapper = shallowWithIntl(); @@ -73,7 +70,6 @@ describe('SecurityNavControl', () => { user: Promise.resolve({ full_name: 'foo' }) as Promise, editProfileUrl: '', logoutUrl: '', - userMenuLinks$: new BehaviorSubject([]), }; const wrapper = mountWithIntl(); @@ -95,7 +91,6 @@ describe('SecurityNavControl', () => { user: Promise.resolve({ full_name: 'foo' }) as Promise, editProfileUrl: '', logoutUrl: '', - userMenuLinks$: new BehaviorSubject([]), }; const wrapper = mountWithIntl(); @@ -112,37 +107,4 @@ describe('SecurityNavControl', () => { expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(1); expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(1); }); - - it('renders a popover with additional user menu links registered by other plugins', async () => { - const props = { - user: Promise.resolve({ full_name: 'foo' }) as Promise, - editProfileUrl: '', - logoutUrl: '', - userMenuLinks$: new BehaviorSubject([ - { label: 'link1', href: 'path-to-link-1', iconType: 'empty', order: 1 }, - { label: 'link2', href: 'path-to-link-2', iconType: 'empty', order: 2 }, - { label: 'link3', href: 'path-to-link-3', iconType: 'empty', order: 3 }, - ]), - }; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - expect(findTestSubject(wrapper, 'userMenu')).toHaveLength(0); - expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(0); - expect(findTestSubject(wrapper, 'userMenuLink__link1')).toHaveLength(0); - expect(findTestSubject(wrapper, 'userMenuLink__link2')).toHaveLength(0); - expect(findTestSubject(wrapper, 'userMenuLink__link3')).toHaveLength(0); - expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(0); - - wrapper.find(EuiHeaderSectionItemButton).simulate('click'); - - expect(findTestSubject(wrapper, 'userMenu')).toHaveLength(1); - expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(1); - expect(findTestSubject(wrapper, 'userMenuLink__link1')).toHaveLength(1); - expect(findTestSubject(wrapper, 'userMenuLink__link2')).toHaveLength(1); - expect(findTestSubject(wrapper, 'userMenuLink__link3')).toHaveLength(1); - expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(1); - }); }); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx index c22308fa8a43e..3ddabb0dc55f8 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx @@ -7,52 +7,38 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { Observable, Subscription } from 'rxjs'; + import { EuiAvatar, + EuiFlexGroup, + EuiFlexItem, EuiHeaderSectionItemButton, + EuiLink, + EuiText, + EuiSpacer, EuiPopover, EuiLoadingSpinner, - EuiIcon, - EuiContextMenu, - EuiContextMenuPanelItemDescriptor, - IconType, - EuiText, } from '@elastic/eui'; import { AuthenticatedUser } from '../../common/model'; -import './nav_control_component.scss'; - -export interface UserMenuLink { - label: string; - iconType: IconType; - href: string; - order?: number; -} - interface Props { user: Promise; editProfileUrl: string; logoutUrl: string; - userMenuLinks$: Observable; } interface State { isOpen: boolean; authenticatedUser: AuthenticatedUser | null; - userMenuLinks: UserMenuLink[]; } export class SecurityNavControl extends Component { - private subscription?: Subscription; - constructor(props: Props) { super(props); this.state = { isOpen: false, authenticatedUser: null, - userMenuLinks: [], }; props.user.then((authenticatedUser) => { @@ -62,18 +48,6 @@ export class SecurityNavControl extends Component { }); } - componentDidMount() { - this.subscription = this.props.userMenuLinks$.subscribe(async (userMenuLinks) => { - this.setState({ userMenuLinks }); - }); - } - - componentWillUnmount() { - if (this.subscription) { - this.subscription.unsubscribe(); - } - } - onMenuButtonClick = () => { if (!this.state.authenticatedUser) { return; @@ -92,13 +66,13 @@ export class SecurityNavControl extends Component { render() { const { editProfileUrl, logoutUrl } = this.props; - const { authenticatedUser, userMenuLinks } = this.state; + const { authenticatedUser } = this.state; - const username = + const name = (authenticatedUser && (authenticatedUser.full_name || authenticatedUser.username)) || ''; const buttonContents = authenticatedUser ? ( - + ) : ( ); @@ -118,60 +92,6 @@ export class SecurityNavControl extends Component { ); - const profileMenuItem = { - name: ( - - ), - icon: , - href: editProfileUrl, - 'data-test-subj': 'profileLink', - }; - - const logoutMenuItem = { - name: ( - - ), - icon: , - href: logoutUrl, - 'data-test-subj': 'logoutLink', - }; - - const items: EuiContextMenuPanelItemDescriptor[] = []; - - items.push(profileMenuItem); - - if (userMenuLinks.length) { - const userMenuLinkMenuItems = userMenuLinks - .sort(({ order: orderA = Infinity }, { order: orderB = Infinity }) => orderA - orderB) - .map(({ label, iconType, href }: UserMenuLink) => ({ - name: {label}, - icon: , - href, - 'data-test-subj': `userMenuLink__${label}`, - })); - - items.push(...userMenuLinkMenuItems, { - isSeparator: true, - key: 'securityNavControlComponent__userMenuLinksSeparator', - }); - } - - items.push(logoutMenuItem); - - const panels = [ - { - id: 0, - title: username, - items, - }, - ]; - return ( { repositionOnScroll closePopover={this.closeMenu} panelPaddingSize="none" - buffer={0} > -
    - +
    + + + + + + + +

    {name}

    +
    + + + + + + + + + + + + + + + + + + + + +
    +
    ); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx index 4ae64d667ce29..aa3ec2e47469d 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx @@ -4,16 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sortBy } from 'lodash'; -import { Observable, Subscription, BehaviorSubject, ReplaySubject } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { Subscription } from 'rxjs'; import { CoreStart } from 'src/core/public'; - import ReactDOM from 'react-dom'; import React from 'react'; - import { SecurityLicense } from '../../common/licensing'; -import { SecurityNavControl, UserMenuLink } from './nav_control_component'; +import { SecurityNavControl } from './nav_control_component'; import { AuthenticationServiceSetup } from '../authentication'; interface SetupDeps { @@ -26,18 +22,6 @@ interface StartDeps { core: CoreStart; } -export interface SecurityNavControlServiceStart { - /** - * Returns an Observable of the array of user menu links registered by other plugins - */ - getUserMenuLinks$: () => Observable; - - /** - * Registers the provided user menu links to be displayed in the user menu in the global nav - */ - addUserMenuLinks: (newUserMenuLink: UserMenuLink[]) => void; -} - export class SecurityNavControlService { private securityLicense!: SecurityLicense; private authc!: AuthenticationServiceSetup; @@ -47,16 +31,13 @@ export class SecurityNavControlService { private securityFeaturesSubscription?: Subscription; - private readonly stop$ = new ReplaySubject(1); - private userMenuLinks$ = new BehaviorSubject([]); - public setup({ securityLicense, authc, logoutUrl }: SetupDeps) { this.securityLicense = securityLicense; this.authc = authc; this.logoutUrl = logoutUrl; } - public start({ core }: StartDeps): SecurityNavControlServiceStart { + public start({ core }: StartDeps) { this.securityFeaturesSubscription = this.securityLicense.features$.subscribe( ({ showLinks }) => { const isAnonymousPath = core.http.anonymousPaths.isAnonymous(window.location.pathname); @@ -68,14 +49,6 @@ export class SecurityNavControlService { } } ); - - return { - getUserMenuLinks$: () => - this.userMenuLinks$.pipe(map(this.sortUserMenuLinks), takeUntil(this.stop$)), - addUserMenuLinks: (userMenuLink: UserMenuLink[]) => { - this.userMenuLinks$.next(userMenuLink); - }, - }; } public stop() { @@ -84,7 +57,6 @@ export class SecurityNavControlService { this.securityFeaturesSubscription = undefined; } this.navControlRegistered = false; - this.stop$.next(); } private registerSecurityNavControl( @@ -100,7 +72,6 @@ export class SecurityNavControlService { user: currentUserPromise, editProfileUrl: core.http.basePath.prepend('/security/account'), logoutUrl: this.logoutUrl, - userMenuLinks$: this.userMenuLinks$, }; ReactDOM.render( @@ -115,8 +86,4 @@ export class SecurityNavControlService { this.navControlRegistered = true; } - - private sortUserMenuLinks(userMenuLinks: UserMenuLink[]) { - return sortBy(userMenuLinks, 'order'); - } } diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 6f5a2a031a7b2..d86d4812af5e3 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -97,12 +97,7 @@ describe('Security Plugin', () => { data: {} as DataPublicPluginStart, features: {} as FeaturesPluginStart, }) - ).toEqual({ - navControlService: { - getUserMenuLinks$: expect.any(Function), - addUserMenuLinks: expect.any(Function), - }, - }); + ).toBeUndefined(); }); it('starts Management Service if `management` plugin is available', () => { diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index f94772c43dd89..700653c4cecb8 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -146,13 +146,11 @@ export class SecurityPlugin public start(core: CoreStart, { management, securityOss }: PluginStartDependencies) { this.sessionTimeout.start(); + this.navControlService.start({ core }); this.securityCheckupService.start({ securityOssStart: securityOss, docLinks: core.docLinks }); - if (management) { this.managementService.start({ capabilities: core.application.capabilities }); } - - return { navControlService: this.navControlService.start({ core }) }; } public stop() { From 0faf8c24eec2906363c4538f6dd1060aba15b2d3 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 6 Nov 2020 11:34:57 +0300 Subject: [PATCH 48/57] Use monacco editor in the inspector request panel (#82272) * Use monacco editor in the inspector request panel Closes: #81921 * insRequestCodeViewer -> insRequestCodeViewer * remove uiSettings from props * fix functional tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/inspector/public/plugin.tsx | 11 +- .../inspector_panel.test.tsx.snap | 361 +++++++++--------- .../inspector/public/ui/inspector_panel.scss | 12 +- .../public/ui/inspector_panel.test.tsx | 10 +- .../inspector/public/ui/inspector_panel.tsx | 32 +- .../__snapshots__/data_view.test.tsx.snap | 243 ++---------- .../views/data/components/data_view.test.tsx | 14 +- .../views/data/components/data_view.tsx | 18 +- .../public/views/data/{index.tsx => index.ts} | 16 +- .../components/details/req_code_viewer.tsx | 82 ++++ .../details/req_details_request.tsx | 13 +- .../details/req_details_response.tsx | 13 +- .../requests/components/requests_view.tsx | 4 + .../inspector/public/views/requests/index.ts | 4 +- test/functional/page_objects/tile_map_page.ts | 4 +- test/functional/services/inspector.ts | 13 + .../maps/documents_source/docvalue_fields.js | 2 +- 17 files changed, 402 insertions(+), 450 deletions(-) rename src/plugins/inspector/public/views/data/{index.tsx => index.ts} (72%) create mode 100644 src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx index f906dbcab8043..07ef7c8fbab0d 100644 --- a/src/plugins/inspector/public/plugin.tsx +++ b/src/plugins/inspector/public/plugin.tsx @@ -70,7 +70,7 @@ export class InspectorPublicPlugin implements Plugin { public async setup(core: CoreSetup) { this.views = new InspectorViewRegistry(); - this.views.register(getDataViewDescription(core.uiSettings)); + this.views.register(getDataViewDescription()); this.views.register(getRequestsViewDescription()); return { @@ -101,7 +101,14 @@ export class InspectorPublicPlugin implements Plugin { } return core.overlays.openFlyout( - toMountPoint(), + toMountPoint( + + ), { 'data-test-subj': 'inspectorPanel', closeButtonAriaLabel: closeButtonLabel, diff --git a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap index 709c0bfe69f0b..7fb00fe8d40c4 100644 --- a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap +++ b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap @@ -10,6 +10,11 @@ exports[`InspectorPanel should render as expected 1`] = ` }, } } + dependencies={ + Object { + "uiSettings": Object {}, + } + } intl={ Object { "defaultFormats": Object {}, @@ -135,216 +140,228 @@ exports[`InspectorPanel should render as expected 1`] = ` ] } > - -
    - -
    - -
    - -

    - Inspector -

    -
    -
    -
    - -
    + Inspector +
    +
    +
    + + - + - - - + } + } + views={ + Array [ + Object { + "component": [Function], + "order": 200, + "title": "View 1", + }, + Object { + "component": [Function], + "order": 100, + "shouldShow": [Function], + "title": "Foo View", + }, + Object { + "component": [Function], + "order": 200, + "shouldShow": [Function], + "title": "Never", + }, + ] } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="inspectorViewChooser" - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - repositionOnScroll={true} > - + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="inspectorViewChooser" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + repositionOnScroll={true} > -
    - - - + + + +
    -
    - -
    - - - - - - - - -
    + + +
    + + + + + +
    - -

    - View 1 -

    -
    + } + > + +

    + View 1 +

    +
    +
    +
    - -
    +
    + `; diff --git a/src/plugins/inspector/public/ui/inspector_panel.scss b/src/plugins/inspector/public/ui/inspector_panel.scss index ff0b491e1222b..2a6cfed66e4ff 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.scss +++ b/src/plugins/inspector/public/ui/inspector_panel.scss @@ -1,11 +1,15 @@ .insInspectorPanel__flyoutBody { - // TODO: EUI to allow for custom classNames to inner elements - // Or supply this as default - > div { + .euiFlyoutBody__overflowContent { + height: 100%; display: flex; + flex-wrap: nowrap; flex-direction: column; - > div { + >div { + flex-grow: 0; + } + + .insRequestCodeViewer { flex-grow: 1; } } diff --git a/src/plugins/inspector/public/ui/inspector_panel.test.tsx b/src/plugins/inspector/public/ui/inspector_panel.test.tsx index 23f698c23793b..67e197abe7134 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.test.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.test.tsx @@ -22,10 +22,12 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { InspectorPanel } from './inspector_panel'; import { InspectorViewDescription } from '../types'; import { Adapters } from '../../common'; +import type { IUiSettingsClient } from 'kibana/public'; describe('InspectorPanel', () => { let adapters: Adapters; let views: InspectorViewDescription[]; + const uiSettings: IUiSettingsClient = {} as IUiSettingsClient; beforeEach(() => { adapters = { @@ -62,12 +64,16 @@ describe('InspectorPanel', () => { }); it('should render as expected', () => { - const component = mountWithIntl(); + const component = mountWithIntl( + + ); expect(component).toMatchSnapshot(); }); it('should not allow updating adapters', () => { - const component = mountWithIntl(); + const component = mountWithIntl( + + ); adapters.notAllowed = {}; expect(() => component.setProps({ adapters })).toThrow(); }); diff --git a/src/plugins/inspector/public/ui/inspector_panel.tsx b/src/plugins/inspector/public/ui/inspector_panel.tsx index 37a51257112d6..dbad202953b0b 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.tsx @@ -19,12 +19,21 @@ import './inspector_panel.scss'; import { i18n } from '@kbn/i18n'; -import React, { Component } from 'react'; +import React, { Component, Suspense } from 'react'; import PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { IUiSettingsClient } from 'kibana/public'; import { InspectorViewDescription } from '../types'; import { Adapters } from '../../common'; import { InspectorViewChooser } from './inspector_view_chooser'; +import { KibanaContextProvider } from '../../../kibana_react/public'; function hasAdaptersChanged(oldAdapters: Adapters, newAdapters: Adapters) { return ( @@ -41,6 +50,9 @@ interface InspectorPanelProps { adapters: Adapters; title?: string; views: InspectorViewDescription[]; + dependencies: { + uiSettings: IUiSettingsClient; + }; } interface InspectorPanelState { @@ -95,19 +107,21 @@ export class InspectorPanel extends Component + }> + + ); } render() { - const { views, title } = this.props; + const { views, title, dependencies } = this.props; const { selectedView } = this.state; return ( - + @@ -127,7 +141,7 @@ export class InspectorPanel extends Component {this.renderSelectedPanel()} - + ); } } diff --git a/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap index 2632afff2f63b..3bd3bb6531cc7 100644 --- a/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Inspector Data View component should render empty state 1`] = ` - - + `; exports[`Inspector Data View component should render loading state 1`] = ` - + loading + } intl={ Object { @@ -431,204 +439,9 @@ exports[`Inspector Data View component should render loading state 1`] = ` "timeZone": null, } } - title="Test Data" > - - -
    - -
    - -
    - - - - - - - - - -
    - - -
    -

    - - Gathering data - -

    -
    -
    -
    - -
    - -
    - - - +
    + loading +
    + `; diff --git a/src/plugins/inspector/public/views/data/components/data_view.test.tsx b/src/plugins/inspector/public/views/data/components/data_view.test.tsx index bd78bca42c479..6a7f878ef807e 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.test.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.test.tsx @@ -17,11 +17,10 @@ * under the License. */ -import React from 'react'; +import React, { Suspense } from 'react'; import { getDataViewDescription } from '../index'; import { DataAdapter } from '../../../../common/adapters/data'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { IUiSettingsClient } from '../../../../../../core/public'; jest.mock('../lib/export_csv', () => ({ exportAsCsv: jest.fn(), @@ -31,9 +30,7 @@ describe('Inspector Data View', () => { let DataView: any; beforeEach(() => { - const uiSettings = {} as IUiSettingsClient; - - DataView = getDataViewDescription(uiSettings); + DataView = getDataViewDescription(); }); it('should only show if data adapter is present', () => { @@ -51,7 +48,12 @@ describe('Inspector Data View', () => { }); it('should render loading state', () => { - const component = mountWithIntl(); // eslint-disable-line react/jsx-pascal-case + const DataViewComponent = DataView.component; + const component = mountWithIntl( + loading
    }> + + + ); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/inspector/public/views/data/components/data_view.tsx b/src/plugins/inspector/public/views/data/components/data_view.tsx index 1a2b6f9922d2d..100fa7787321c 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.tsx @@ -38,6 +38,7 @@ import { TabularCallback, } from '../../../../common/adapters/data/types'; import { IUiSettingsClient } from '../../../../../../core/public'; +import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public'; interface DataViewComponentState { tabularData: TabularData | null; @@ -47,20 +48,23 @@ interface DataViewComponentState { } interface DataViewComponentProps extends InspectorViewProps { - uiSettings: IUiSettingsClient; + kibana: KibanaReactContextValue<{ uiSettings: IUiSettingsClient }>; } -export class DataViewComponent extends Component { +class DataViewComponent extends Component { static propTypes = { - uiSettings: PropTypes.object.isRequired, adapters: PropTypes.object.isRequired, title: PropTypes.string.isRequired, + kibana: PropTypes.object, }; state = {} as DataViewComponentState; _isMounted = false; - static getDerivedStateFromProps(nextProps: InspectorViewProps, state: DataViewComponentState) { + static getDerivedStateFromProps( + nextProps: DataViewComponentProps, + state: DataViewComponentState + ) { if (state && nextProps.adapters === state.adapters) { return null; } @@ -172,8 +176,12 @@ export class DataViewComponent extends Component ); } } + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export default withKibana(DataViewComponent); diff --git a/src/plugins/inspector/public/views/data/index.tsx b/src/plugins/inspector/public/views/data/index.ts similarity index 72% rename from src/plugins/inspector/public/views/data/index.tsx rename to src/plugins/inspector/public/views/data/index.ts index b02e02bbe6b6b..d201ad89022be 100644 --- a/src/plugins/inspector/public/views/data/index.tsx +++ b/src/plugins/inspector/public/views/data/index.ts @@ -16,17 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { DataViewComponent } from './components/data_view'; -import { InspectorViewDescription, InspectorViewProps } from '../../types'; +import { InspectorViewDescription } from '../../types'; import { Adapters } from '../../../common'; -import { IUiSettingsClient } from '../../../../../core/public'; -export const getDataViewDescription = ( - uiSettings: IUiSettingsClient -): InspectorViewDescription => ({ +const DataViewComponent = lazy(() => import('./components/data_view')); + +export const getDataViewDescription = (): InspectorViewDescription => ({ title: i18n.translate('inspector.data.dataTitle', { defaultMessage: 'Data', }), @@ -37,7 +35,5 @@ export const getDataViewDescription = ( shouldShow(adapters: Adapters) { return Boolean(adapters.data); }, - component: (props: InspectorViewProps) => ( - - ), + component: DataViewComponent, }); diff --git a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx new file mode 100644 index 0000000000000..71499d46071c8 --- /dev/null +++ b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexItem, EuiFlexGroup, EuiCopy, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; + +import { CodeEditor } from '../../../../../../kibana_react/public'; + +interface RequestCodeViewerProps { + json: string; +} + +const copyToClipboardLabel = i18n.translate('inspector.requests.copyToClipboardLabel', { + defaultMessage: 'Copy to clipboard', +}); + +/** + * @internal + */ +export const RequestCodeViewer = ({ json }: RequestCodeViewerProps) => ( + + + +
    + + {(copy) => ( + + {copyToClipboardLabel} + + )} + +
    +
    + + {}} + options={{ + readOnly: true, + lineNumbers: 'off', + fontSize: 12, + minimap: { + enabled: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, + }} + /> + +
    +); diff --git a/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx b/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx index d7cb8f5745613..47ed226c24a5c 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx @@ -19,9 +19,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { EuiCodeBlock } from '@elastic/eui'; import { Request } from '../../../../../common/adapters/request/types'; import { RequestDetailsProps } from '../types'; +import { RequestCodeViewer } from './req_code_viewer'; export class RequestDetailsRequest extends Component { static propTypes = { @@ -37,15 +37,6 @@ export class RequestDetailsRequest extends Component { return null; } - return ( - - {JSON.stringify(json, null, 2)} - - ); + return ; } } diff --git a/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx b/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx index 933495ff47396..5ad5cc0537ada 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx @@ -19,9 +19,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { EuiCodeBlock } from '@elastic/eui'; import { Request } from '../../../../../common/adapters/request/types'; import { RequestDetailsProps } from '../types'; +import { RequestCodeViewer } from './req_code_viewer'; export class RequestDetailsResponse extends Component { static propTypes = { @@ -40,15 +40,6 @@ export class RequestDetailsResponse extends Component { return null; } - return ( - - {JSON.stringify(responseJSON, null, 2)} - - ); + return ; } } diff --git a/src/plugins/inspector/public/views/requests/components/requests_view.tsx b/src/plugins/inspector/public/views/requests/components/requests_view.tsx index 13575de0c5064..7762689daf4e6 100644 --- a/src/plugins/inspector/public/views/requests/components/requests_view.tsx +++ b/src/plugins/inspector/public/views/requests/components/requests_view.tsx @@ -175,3 +175,7 @@ export class RequestsViewComponent extends Component import('./components/requests_view')); + export const getRequestsViewDescription = (): InspectorViewDescription => ({ title: i18n.translate('inspector.requests.requestsTitle', { defaultMessage: 'Requests', diff --git a/test/functional/page_objects/tile_map_page.ts b/test/functional/page_objects/tile_map_page.ts index 609e6ebddd50a..7881c9b1f7155 100644 --- a/test/functional/page_objects/tile_map_page.ts +++ b/test/functional/page_objects/tile_map_page.ts @@ -50,12 +50,14 @@ export function TileMapPageProvider({ getService, getPageObjects }: FtrProviderC await testSubjects.click('inspectorViewChooser'); await testSubjects.click('inspectorViewChooserRequests'); await testSubjects.click('inspectorRequestDetailRequest'); - return await testSubjects.getVisibleText('inspectorRequestBody'); + + return await inspector.getCodeEditorValue(); } public async getMapBounds(): Promise { const request = await this.getVisualizationRequest(); const requestObject = JSON.parse(request); + return requestObject.aggs.filter_agg.filter.geo_bounding_box['geo.coordinates']; } diff --git a/test/functional/services/inspector.ts b/test/functional/services/inspector.ts index 1c0bf7ad46df1..e256cf14541a7 100644 --- a/test/functional/services/inspector.ts +++ b/test/functional/services/inspector.ts @@ -23,6 +23,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function InspectorProvider({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); + const browser = getService('browser'); const renderable = getService('renderable'); const flyout = getService('flyout'); const testSubjects = getService('testSubjects'); @@ -245,6 +246,18 @@ export function InspectorProvider({ getService }: FtrProviderContext) { public getOpenRequestDetailResponseButton() { return testSubjects.find('inspectorRequestDetailResponse'); } + + public async getCodeEditorValue() { + let request: string = ''; + + await retry.try(async () => { + request = await browser.execute( + () => (window as any).monaco.editor.getModels()[0].getValue() as string + ); + }); + + return request; + } } return new Inspector(); diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js index 4edee0a0b78ba..a336ebc0d57db 100644 --- a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js +++ b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js @@ -26,7 +26,7 @@ export default function ({ getPageObjects, getService }) { await inspector.open(); await inspector.openInspectorRequestsView(); await testSubjects.click('inspectorRequestDetailResponse'); - const responseBody = await testSubjects.getVisibleText('inspectorResponseBody'); + const responseBody = await inspector.getCodeEditorValue(); await inspector.close(); return JSON.parse(responseBody); } From 1b65a674d0ee0478e7502262c7e74ee72afec585 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Fri, 6 Nov 2020 11:17:01 +0100 Subject: [PATCH 49/57] [Dashboard] Fix cloning panels reactive issue (#74253) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../actions/add_to_library_action.test.tsx | 21 +++----- .../actions/clone_panel_action.test.tsx | 7 ++- .../unlink_from_library_action.test.tsx | 20 ++----- .../embeddable/dashboard_container.test.tsx | 43 +++++++++++++++ .../embeddable/dashboard_container.tsx | 54 +++++++++---------- .../embeddable/grid/dashboard_grid.tsx | 3 ++ .../public/lib/containers/container.ts | 40 ++++++++++---- 7 files changed, 120 insertions(+), 68 deletions(-) diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx index 650a273314412..feb30b248c066 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx @@ -134,19 +134,15 @@ test('Add to library is not compatible when embeddable is not in a dashboard con expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false); }); -test('Add to library replaces embeddableId but retains panel count', async () => { +test('Add to library replaces embeddableId and retains panel count', async () => { const dashboard = embeddable.getRoot() as IContainer; const originalPanelCount = Object.keys(dashboard.getInput().panels).length; - const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount); - - const newPanelId = Object.keys(container.getInput().panels).find( - (key) => !originalPanelKeySet.has(key) - ); - expect(newPanelId).toBeDefined(); - const newPanel = container.getInput().panels[newPanelId!]; + expect(Object.keys(container.getInput().panels)).toContain(embeddable.id); + const newPanel = container.getInput().panels[embeddable.id!]; expect(newPanel.type).toEqual(embeddable.type); }); @@ -162,15 +158,10 @@ test('Add to library returns reference type input', async () => { mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: embeddable.id }, mockedByValueInput: { attributes: complicatedAttributes, id: embeddable.id } as EmbeddableInput, }); - const dashboard = embeddable.getRoot() as IContainer; - const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); - const newPanelId = Object.keys(container.getInput().panels).find( - (key) => !originalPanelKeySet.has(key) - ); - expect(newPanelId).toBeDefined(); - const newPanel = container.getInput().panels[newPanelId!]; + expect(Object.keys(container.getInput().panels)).toContain(embeddable.id); + const newPanel = container.getInput().panels[embeddable.id!]; expect(newPanel.type).toEqual(embeddable.type); expect(newPanel.explicitInput.attributes).toBeUndefined(); expect(newPanel.explicitInput.savedObjectId).toBe('testSavedObjectId'); diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx index 193376ae97c0b..25179fd7ccd38 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx @@ -108,7 +108,12 @@ test('Clone adds a new embeddable', async () => { ); expect(newPanelId).toBeDefined(); const newPanel = container.getInput().panels[newPanelId!]; - expect(newPanel.type).toEqual(embeddable.type); + expect(newPanel.type).toEqual('placeholder'); + // let the placeholder load + await dashboard.untilEmbeddableLoaded(newPanelId!); + // now wait for the full embeddable to replace it + const loadedPanel = await dashboard.untilEmbeddableLoaded(newPanelId!); + expect(loadedPanel.type).toEqual(embeddable.type); }); test('Clones an embeddable without a saved object ID', async () => { diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx index 4f668ec9ea04c..f191be6f7baad 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx @@ -132,19 +132,14 @@ test('Unlink is not compatible when embeddable is not in a dashboard container', expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false); }); -test('Unlink replaces embeddableId but retains panel count', async () => { +test('Unlink replaces embeddableId and retains panel count', async () => { const dashboard = embeddable.getRoot() as IContainer; const originalPanelCount = Object.keys(dashboard.getInput().panels).length; - const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount); - - const newPanelId = Object.keys(container.getInput().panels).find( - (key) => !originalPanelKeySet.has(key) - ); - expect(newPanelId).toBeDefined(); - const newPanel = container.getInput().panels[newPanelId!]; + expect(Object.keys(container.getInput().panels)).toContain(embeddable.id); + const newPanel = container.getInput().panels[embeddable.id!]; expect(newPanel.type).toEqual(embeddable.type); }); @@ -164,15 +159,10 @@ test('Unlink unwraps all attributes from savedObject', async () => { mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: embeddable.id }, mockedByValueInput: { attributes: complicatedAttributes, id: embeddable.id }, }); - const dashboard = embeddable.getRoot() as IContainer; - const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); - const newPanelId = Object.keys(container.getInput().panels).find( - (key) => !originalPanelKeySet.has(key) - ); - expect(newPanelId).toBeDefined(); - const newPanel = container.getInput().panels[newPanelId!]; + expect(Object.keys(container.getInput().panels)).toContain(embeddable.id); + const newPanel = container.getInput().panels[embeddable.id!]; expect(newPanel.type).toEqual(embeddable.type); expect(newPanel.explicitInput.attributes).toEqual(complicatedAttributes); }); diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx index 89aacf2a84029..caa8321d7b8b2 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx @@ -27,6 +27,7 @@ import { ContactCardEmbeddableInput, ContactCardEmbeddable, ContactCardEmbeddableOutput, + EMPTY_EMBEDDABLE, } from '../../embeddable_plugin_test_samples'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; @@ -100,6 +101,48 @@ test('DashboardContainer.addNewEmbeddable', async () => { expect(embeddableInContainer.id).toBe(embeddable.id); }); +test('DashboardContainer.replacePanel', async (done) => { + const ID = '123'; + const initialInput = getSampleDashboardInput({ + panels: { + [ID]: getSampleDashboardPanel({ + explicitInput: { firstName: 'Sam', id: ID }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, + }); + + const container = new DashboardContainer(initialInput, options); + let counter = 0; + + const subscriptionHandler = jest.fn(({ panels }) => { + counter++; + expect(panels[ID]).toBeDefined(); + // It should be called exactly 2 times and exit the second time + switch (counter) { + case 1: + return expect(panels[ID].type).toBe(CONTACT_CARD_EMBEDDABLE); + + case 2: { + expect(panels[ID].type).toBe(EMPTY_EMBEDDABLE); + subscription.unsubscribe(); + done(); + } + + default: + throw Error('Called too many times!'); + } + }); + + const subscription = container.getInput$().subscribe(subscriptionHandler); + + // replace the panel now + container.replacePanel(container.getInput().panels[ID], { + type: EMPTY_EMBEDDABLE, + explicitInput: { id: ID }, + }); +}); + test('Container view mode change propagates to existing children', async () => { const initialInput = getSampleDashboardInput({ panels: { diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 757488185fe8e..051a7ef8bfb92 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -154,42 +154,43 @@ export class DashboardContainer extends Container) => - this.replacePanel(placeholderPanelState, newPanelState) - ); + + // wait until the placeholder is ready, then replace it with new panel + // this is useful as sometimes panels can load faster than the placeholder one (i.e. by value embeddables) + this.untilEmbeddableLoaded(originalPanelState.explicitInput.id) + .then(() => newStateComplete) + .then((newPanelState: Partial) => + this.replacePanel(placeholderPanelState, newPanelState) + ); } public replacePanel( previousPanelState: DashboardPanelState, newPanelState: Partial ) { - // TODO: In the current infrastructure, embeddables in a container do not react properly to - // changes. Removing the existing embeddable, and adding a new one is a temporary workaround - // until the container logic is fixed. - - const finalPanels = { ...this.input.panels }; - delete finalPanels[previousPanelState.explicitInput.id]; - const newPanelId = newPanelState.explicitInput?.id ? newPanelState.explicitInput.id : uuid.v4(); - finalPanels[newPanelId] = { - ...previousPanelState, - ...newPanelState, - gridData: { - ...previousPanelState.gridData, - i: newPanelId, - }, - explicitInput: { - ...newPanelState.explicitInput, - id: newPanelId, + // Because the embeddable type can change, we have to operate at the container level here + return this.updateInput({ + panels: { + ...this.input.panels, + [previousPanelState.explicitInput.id]: { + ...previousPanelState, + ...newPanelState, + gridData: { + ...previousPanelState.gridData, + }, + explicitInput: { + ...newPanelState.explicitInput, + id: previousPanelState.explicitInput.id, + }, + }, }, - }; - this.updateInput({ - panels: finalPanels, lastReloadRequestTime: new Date().getTime(), }); } @@ -201,16 +202,15 @@ export class DashboardContainer extends Container(type: string, explicitInput: Partial, embeddableId?: string) { const idToReplace = embeddableId || explicitInput.id; if (idToReplace && this.input.panels[idToReplace]) { - this.replacePanel(this.input.panels[idToReplace], { + return this.replacePanel(this.input.panels[idToReplace], { type, explicitInput: { ...explicitInput, - id: uuid.v4(), + id: idToReplace, }, }); - } else { - this.addNewEmbeddable(type, explicitInput); } + return this.addNewEmbeddable(type, explicitInput); } public render(dom: HTMLElement) { diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx index d4d8fd0a4374b..03c92d91a80cc 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx @@ -265,6 +265,7 @@ class DashboardGridUi extends React.Component {
    { @@ -272,6 +273,8 @@ class DashboardGridUi extends React.Component { }} > this.maybeUpdateChildren()); + this.subscription = this.getInput$() + // At each update event, get both the previous and current state + .pipe(startWith(input), pairwise()) + .subscribe(([{ panels: prevPanels }, { panels: currentPanels }]) => { + this.maybeUpdateChildren(currentPanels, prevPanels); + }); } public updateInputForChild( @@ -329,16 +335,30 @@ export abstract class Container< return embeddable; } - private maybeUpdateChildren() { - const allIds = Object.keys({ ...this.input.panels, ...this.output.embeddableLoaded }); + private panelHasChanged(currentPanel: PanelState, prevPanel: PanelState) { + if (currentPanel.type !== prevPanel.type) { + return true; + } + } + + private maybeUpdateChildren( + currentPanels: TContainerInput['panels'], + prevPanels: TContainerInput['panels'] + ) { + const allIds = Object.keys({ ...currentPanels, ...this.output.embeddableLoaded }); allIds.forEach((id) => { - if (this.input.panels[id] !== undefined && this.output.embeddableLoaded[id] === undefined) { - this.onPanelAdded(this.input.panels[id]); - } else if ( - this.input.panels[id] === undefined && - this.output.embeddableLoaded[id] !== undefined - ) { - this.onPanelRemoved(id); + if (currentPanels[id] !== undefined && this.output.embeddableLoaded[id] === undefined) { + return this.onPanelAdded(currentPanels[id]); + } + if (currentPanels[id] === undefined && this.output.embeddableLoaded[id] !== undefined) { + return this.onPanelRemoved(id); + } + // In case of type change, remove and add a panel with the same id + if (currentPanels[id] && prevPanels[id]) { + if (this.panelHasChanged(currentPanels[id], prevPanels[id])) { + this.onPanelRemoved(id); + this.onPanelAdded(currentPanels[id]); + } } }); } From 814603455937e7cf4d9c9de0aedae0c8dabbcef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Fri, 6 Nov 2020 11:18:42 +0100 Subject: [PATCH 50/57] [Security Solution] Bump why-did-you-render (#82591) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 44a0c833eae27..1c218307b35c3 100644 --- a/package.json +++ b/package.json @@ -567,7 +567,7 @@ "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^3.10.0", "@typescript-eslint/parser": "^3.10.0", - "@welldone-software/why-did-you-render": "^4.0.0", + "@welldone-software/why-did-you-render": "^5.0.0", "@yarnpkg/lockfile": "^1.1.0", "abab": "^1.0.4", "angular-aria": "^1.8.0", diff --git a/yarn.lock b/yarn.lock index 6ba53d0e4dd43..b79e246b27851 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6242,10 +6242,10 @@ text-table "^0.2.0" webpack-log "^1.1.2" -"@welldone-software/why-did-you-render@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-4.0.0.tgz#cc98c996f5a06ea55bd07dc99ba4b4d68af93332" - integrity sha512-PjqriZ8Ak9biP2+kOcIrg+NwsFwWVhGV03Hm+ns84YBCArn+hWBKM9rMBEU6e62I1qyrYF2/G9yktNpEmfWfJA== +"@welldone-software/why-did-you-render@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-5.0.0.tgz#5dd8d20ad9f00fd500de852dd06eea0c057a0bce" + integrity sha512-A6xUP/55vJQwA1+L6iZbG81cQanSQQVR15yPcjLIp6lHmybXEOXsYcuXaDZHYqiNStZRzv64YPcYJC9wdphfhw== dependencies: lodash "^4" From b0eb2779838e339cc8c1d6a3562abf4299ad09eb Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Fri, 6 Nov 2020 11:57:12 +0100 Subject: [PATCH 51/57] Add steps to migrate from a legacy kibana index (#82161) * Add steps to migrate from a legacy kibana index * Clarify data loss from legacy index to alias with same name * Use aliases api to safely add a .kibana alias to a legacy index --- rfcs/text/0013_saved_object_migrations.md | 53 ++++++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/rfcs/text/0013_saved_object_migrations.md b/rfcs/text/0013_saved_object_migrations.md index c5069625cb8a6..1a0967d110d06 100644 --- a/rfcs/text/0013_saved_object_migrations.md +++ b/rfcs/text/0013_saved_object_migrations.md @@ -212,39 +212,68 @@ Note: If none of the aliases exists, this is a new Elasticsearch cluster and no migrations are necessary. Create the `.kibana_7.10.0_001` index with the following aliases: `.kibana_current` and `.kibana_7.10.0`. -2. If `.kibana_current` and `.kibana_7.10.0` both exists and are pointing to the same index this version's migration has already been completed. +2. If the source is a < v6.5 `.kibana` index or < 7.4 `.kibana_task_manager` + index prepare the legacy index for a migration: + 1. Mark the legacy index as read-only and wait for all in-flight operations to drain (requires https://github.com/elastic/elasticsearch/pull/58094). This prevents any further writes from outdated nodes. Assuming this API is similar to the existing `//_close` API, we expect to receive `"acknowledged" : true` and `"shards_acknowledged" : true`. If all shards don’t acknowledge within the timeout, retry the operation until it succeeds. + 2. Clone the legacy index into a new index which has writes enabled. Use a fixed index name i.e `.kibana_pre6.5.0_001` or `.kibana_task_manager_pre7.4.0_001`. `POST /.kibana/_clone/.kibana_pre6.5.0_001?wait_for_active_shards=all {"settings": {"index.blocks.write": false}}`. Ignore errors if the clone already exists. Ignore errors if the legacy source doesn't exist. + 3. Wait for the cloning to complete `GET /_cluster/health/.kibana_pre6.5.0_001?wait_for_status=green&timeout=60s` If cloning doesn’t complete within the 60s timeout, log a warning for visibility and poll again. + 4. Apply the `convertToAlias` script if defined `POST /.kibana_pre6.5.0_001/_update_by_query?conflicts=proceed {"script": {...}}`. The `convertToAlias` script will have to be idempotent, preferably setting `ctx.op="noop"` on subsequent runs to avoid unecessary writes. + 5. Delete the legacy index and replace it with an alias of the same name + ``` + POST /_aliases + { + "actions" : [ + { "add": { "index": ".kibana_pre6.5.0_001", "alias": ".kibana" } }, + { "remove_index": { "index": ".kibana" } } + ] + } + ```. + Unlike the delete index API, the `remove_index` action will fail if + provided with an _alias_. Ignore "The provided expression [.kibana] + matches an alias, specify the corresponding concrete indices instead." + or "index_not_found_exception" errors. These actions are applied + atomically so that other Kibana instances will always see either a + `.kibana` index or an alias, but never neither. + 6. Use the cloned `.kibana_pre6.5.0_001` as the source for the rest of the migration algorithm. +3. If `.kibana_current` and `.kibana_7.10.0` both exists and are pointing to the same index this version's migration has already been completed. 1. Because the same version can have plugins enabled at any point in time, perform the mappings update in step (6) and migrate outdated documents with step (7). 2. Skip to step (9) to start serving traffic. -3. Fail the migration if: +4. Fail the migration if: 1. `.kibana_current` is pointing to an index that belongs to a later version of Kibana .e.g. `.kibana_7.12.0_001` 2. (Only in 8.x) The source index contains documents that belong to an unknown Saved Object type (from a disabled plugin). Log an error explaining that the plugin that created these documents needs to be enabled again or that these objects should be deleted. See section (4.2.1.4). -4. Mark the source index as read-only and wait for all in-flight operations to drain (requires https://github.com/elastic/elasticsearch/pull/58094). This prevents any further writes from outdated nodes. Assuming this API is similar to the existing `//_close` API, we expect to receive `"acknowledged" : true` and `"shards_acknowledged" : true`. If all shards don’t acknowledge within the timeout, retry the operation until it succeeds. -5. Clone the source index into a new target index which has writes enabled. All nodes on the same version will use the same fixed index name e.g. `.kibana_7.10.0_001`. The `001` postfix isn't used by Kibana, but allows for re-indexing an index should this be required by an Elasticsearch upgrade. E.g. re-index `.kibana_7.10.0_001` into `.kibana_7.10.0_002` and point the `.kibana_7.10.0` alias to `.kibana_7.10.0_002`. +5. Mark the source index as read-only and wait for all in-flight operations to drain (requires https://github.com/elastic/elasticsearch/pull/58094). This prevents any further writes from outdated nodes. Assuming this API is similar to the existing `//_close` API, we expect to receive `"acknowledged" : true` and `"shards_acknowledged" : true`. If all shards don’t acknowledge within the timeout, retry the operation until it succeeds. +6. Clone the source index into a new target index which has writes enabled. All nodes on the same version will use the same fixed index name e.g. `.kibana_7.10.0_001`. The `001` postfix isn't used by Kibana, but allows for re-indexing an index should this be required by an Elasticsearch upgrade. E.g. re-index `.kibana_7.10.0_001` into `.kibana_7.10.0_002` and point the `.kibana_7.10.0` alias to `.kibana_7.10.0_002`. 1. `POST /.kibana_n/_clone/.kibana_7.10.0_001?wait_for_active_shards=all {"settings": {"index.blocks.write": false}}`. Ignore errors if the clone already exists. 2. Wait for the cloning to complete `GET /_cluster/health/.kibana_7.10.0_001?wait_for_status=green&timeout=60s` If cloning doesn’t complete within the 60s timeout, log a warning for visibility and poll again. -6. Update the mappings of the target index +7. Update the mappings of the target index 1. Retrieve the existing mappings including the `migrationMappingPropertyHashes` metadata. 2. Update the mappings with `PUT /.kibana_7.10.0_001/_mapping`. The API deeply merges any updates so this won't remove the mappings of any plugins that were enabled in a previous version but are now disabled. 3. Ensure that fields are correctly indexed using the target index's latest mappings `POST /.kibana_7.10.0_001/_update_by_query?conflicts=proceed`. In the future we could optimize this query by only targeting documents: 1. That belong to a known saved object type. 2. Which don't have outdated migrationVersion numbers since these will be transformed anyway. 3. That belong to a type whose mappings were changed by comparing the `migrationMappingPropertyHashes`. (Metadata, unlike the mappings isn't commutative, so there is a small chance that the metadata hashes do not accurately reflect the latest mappings, however, this will just result in an less efficient query). -7. Transform documents by reading batches of outdated documents from the target index then transforming and updating them with optimistic concurrency control. +8. Transform documents by reading batches of outdated documents from the target index then transforming and updating them with optimistic concurrency control. 1. Ignore any version conflict errors. 2. If a document transform throws an exception, add the document to a failure list and continue trying to transform all other documents. If any failures occured, log the complete list of documents that failed to transform. Fail the migration. -8. Mark the migration as complete by doing a single atomic operation (requires https://github.com/elastic/elasticsearch/pull/58100) that: - 1. Checks that `.kibana-current` alias is still pointing to the source index - 2. Points the `.kibana-7.10.0` and `.kibana_current` aliases to the target index. - 3. If this fails with a "required alias [.kibana_current] does not exist" error fetch `.kibana_current` again: +9. Mark the migration as complete by doing a single atomic operation (requires https://github.com/elastic/elasticsearch/pull/58100) that: + 3. Checks that `.kibana_current` alias is still pointing to the source index + 4. Points the `.kibana_7.10.0` and `.kibana_current` aliases to the target index. + 5. If this fails with a "required alias [.kibana_current] does not exist" error fetch `.kibana_current` again: 1. If `.kibana_current` is _not_ pointing to our target index fail the migration. 2. If `.kibana_current` is pointing to our target index the migration has succeeded and we can proceed to step (9). -9. Start serving traffic. +10. Start serving traffic. + +This algorithm shares a weakness with our existing migration algorithm +(since v7.4). When the task manager index gets reindexed a reindex script is +applied. Because we delete the original task manager index there is no way to +rollback a failed task manager migration without a snapshot. Together with the limitations, this algorithm ensures that migrations are idempotent. If two nodes are started simultaneously, both of them will start -transforming documents in that version's target index, but because migrations are idempotent, it doesn’t matter which node’s writes win. +transforming documents in that version's target index, but because migrations +are idempotent, it doesn’t matter which node’s writes win.
    In the future, this algorithm could enable (2.6) "read-only functionality during the downtime window" but this is outside of the scope of this RFC. From d83167629c60a4263e1479591a9188adc25e76b0 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 6 Nov 2020 12:18:54 +0100 Subject: [PATCH 52/57] fix underlying data drilldown for Lens (#82737) --- .../embeddable/embeddable.test.tsx | 38 +++++++++++++++++++ .../embeddable/embeddable.tsx | 6 ++- x-pack/test/functional/apps/lens/dashboard.ts | 35 ++++++++++++++++- 3 files changed, 75 insertions(+), 4 deletions(-) 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 5658f029c48ab..9f9d7fef9c7b4 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 @@ -139,6 +139,44 @@ describe('embeddable', () => { | expression`); }); + it('should initialize output with deduped list of index patterns', async () => { + attributeService = attributeServiceMockFromSavedVis({ + ...savedVis, + references: [ + { type: 'index-pattern', id: '123', name: 'abc' }, + { type: 'index-pattern', id: '123', name: 'def' }, + { type: 'index-pattern', id: '456', name: 'ghi' }, + ], + }); + const embeddable = new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: ({ + get: (id: string) => Promise.resolve({ id }), + } as unknown) as IndexPatternsContract, + editable: true, + getTrigger, + documentToExpression: () => + Promise.resolve({ + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }), + }, + {} as LensEmbeddableInput + ); + await embeddable.initializeSavedVis({} as LensEmbeddableInput); + const outputIndexPatterns = embeddable.getOutput().indexPatterns!; + expect(outputIndexPatterns.length).toEqual(2); + expect(outputIndexPatterns[0].id).toEqual('123'); + expect(outputIndexPatterns[1].id).toEqual('456'); + }); + it('should re-render if new input is pushed', async () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index fdb267835f44c..33e5dee99081f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -259,8 +259,10 @@ export class Embeddable if (!this.savedVis) { return; } - const promises = this.savedVis.references - .filter(({ type }) => type === 'index-pattern') + const promises = _.uniqBy( + this.savedVis.references.filter(({ type }) => type === 'index-pattern'), + 'id' + ) .map(async ({ id }) => { try { return await this.deps.indexPatternService.get(id); diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index fa13d013ea115..c24f4ccf01bcd 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -8,7 +8,14 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['header', 'common', 'dashboard', 'timePicker', 'lens']); + const PageObjects = getPageObjects([ + 'header', + 'common', + 'dashboard', + 'timePicker', + 'lens', + 'discover', + ]); const find = getService('find'); const dashboardAddPanel = getService('dashboardAddPanel'); @@ -18,6 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); const security = getService('security'); + const panelActions = getService('dashboardPanelActions'); async function clickInChart(x: number, y: number) { const el = await elasticChart.getCanvas(); @@ -27,7 +35,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('lens dashboard tests', () => { before(async () => { await PageObjects.common.navigateToApp('dashboard'); - await security.testUser.setRoles(['global_dashboard_all', 'test_logstash_reader'], false); + await security.testUser.setRoles( + ['global_dashboard_all', 'global_discover_all', 'test_logstash_reader'], + false + ); }); after(async () => { await security.testUser.restoreDefaults(); @@ -68,6 +79,26 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const hasIpFilter = await filterBar.hasFilter('ip', '97.220.3.248'); expect(hasIpFilter).to.be(true); }); + + it('should be able to drill down to discover', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsXYvis'); + await find.clickByButtonText('lnsXYvis'); + await dashboardAddPanel.closeAddPanel(); + await PageObjects.lens.goToTimeRange(); + await PageObjects.dashboard.saveDashboard('lnsDrilldown'); + await panelActions.openContextMenu(); + await testSubjects.clickWhenNotDisabled('embeddablePanelAction-ACTION_EXPLORE_DATA'); + await PageObjects.discover.waitForDiscoverAppOnScreen(); + + const el = await testSubjects.find('indexPattern-switch-link'); + const text = await el.getVisibleText(); + + expect(text).to.be('logstash-*'); + }); + it('should be able to add filters by clicking in pie chart', async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); From 8d5ee265b455df3a090e4a49af106d87f0513771 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 6 Nov 2020 12:51:38 +0100 Subject: [PATCH 53/57] [Uptime] Display response headers for a ping (#82332) --- .../uptime/common/runtime_types/ping/ping.ts | 1 + .../__snapshots__/ping_headers.test.tsx.snap | 94 +++++++++++++++++++ .../ping_list/__tests__/ping_headers.test.tsx | 31 ++++++ .../monitor/ping_list/expanded_row.tsx | 6 ++ .../components/monitor/ping_list/headers.tsx | 48 ++++++++++ 5 files changed, 180 insertions(+) create mode 100644 x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_headers.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_headers.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/ping_list/headers.tsx diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index 775078a7e5df1..315b8f543b800 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -145,6 +145,7 @@ export const PingType = t.intersection([ bytes: t.number, redirects: t.array(t.string), status_code: t.number, + headers: t.record(t.string, t.string), }), version: t.string, }), diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_headers.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_headers.test.tsx.snap new file mode 100644 index 0000000000000..ef707dc3ea315 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_headers.test.tsx.snap @@ -0,0 +1,94 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Ping Headers shallow renders expected elements for valid props 1`] = ` + + + +

    + Response headers +

    + + } + id="responseHeaderAccord" + initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} + paddingSize="none" + > + + +
    +
    +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_headers.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_headers.test.tsx new file mode 100644 index 0000000000000..86394a0a4d841 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_headers.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { PingHeaders } from '../headers'; + +describe('Ping Headers', () => { + const headers = { + 'Content-Type': 'text/html', + 'Content-Length': '174781', + Expires: 'Mon, 02 Nov 2020 17:22:03 GMT', + 'X-Xss-Protection': '0', + 'Accept-Ranges': 'bytes', + Date: 'Mon, 02 Nov 2020 17:22:03 GMT', + 'Cache-Control': 'private, max-age=0', + 'Alt-Svc': + 'h3-Q050=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"', + Server: 'sffe', + 'Last-Modified': 'Wed, 28 Oct 2020 18:45:00 GMT', + Vary: 'Accept-Encoding', + 'X-Content-Type-Options': 'nosniff', + }; + + it('shallow renders expected elements for valid props', () => { + expect(shallowWithIntl()).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx index 6af38eca6b0e9..e6a9b1ebe9c84 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx @@ -21,6 +21,7 @@ import { Ping, HttpResponseBody } from '../../../../common/runtime_types'; import { DocLinkForBody } from './doc_link_body'; import { PingRedirects } from './ping_redirects'; import { BrowserExpandedRow } from '../synthetics/browser_expanded_row'; +import { PingHeaders } from './headers'; interface Props { ping: Ping; @@ -105,6 +106,11 @@ export const PingListExpandedRowComponent = ({ ping }: Props) => { )} + {ping?.http?.response?.headers && ( + + + + )} diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/headers.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/headers.tsx new file mode 100644 index 0000000000000..52fe26a7e08ca --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/headers.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiAccordion, EuiDescriptionList, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface Props { + headers: Record; +} + +export const PingHeaders = ({ headers }: Props) => { + const headersList = Object.keys(headers) + .sort() + .map((header) => ({ + title: header, + description: headers[header], + })); + + return ( + <> + + +

    + {i18n.translate('xpack.uptime.pingList.headers.title', { + defaultMessage: 'Response headers', + })} +

    + + } + > + + +
    + + ); +}; From 3da6efcc7320a2f7b213a1cc2dc27491e9e04b1a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 6 Nov 2020 07:06:11 -0500 Subject: [PATCH 54/57] Bump trim to 0.0.3 (#82800) --- package.json | 1 + yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 1c218307b35c3..322bdf445758a 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "**/minimist": "^1.2.5", "**/node-jose/node-forge": "^0.10.0", "**/request": "^2.88.2", + "**/trim": "0.0.3", "**/typescript": "4.0.2" }, "engines": { diff --git a/yarn.lock b/yarn.lock index b79e246b27851..6e6647016dac2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28431,10 +28431,10 @@ trim-trailing-lines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.0.tgz#7aefbb7808df9d669f6da2e438cac8c46ada7684" integrity sha1-eu+7eAjfnWafbaLkOMrIxGradoQ= -trim@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" - integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0= +trim@0.0.1, trim@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.3.tgz#05243a47a3a4113e6b49367880a9cca59697a20b" + integrity sha512-h82ywcYhHK7veeelXrCScdH7HkWfbIT1D/CgYO+nmDarz3SGNssVBMws6jU16Ga60AJCRAvPV6w6RLuNerQqjg== triple-beam@^1.2.0, triple-beam@^1.3.0: version "1.3.0" From 1046fc299cfe90c272da379725d79ef56c31f9ab Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Fri, 6 Nov 2020 13:16:03 +0100 Subject: [PATCH 55/57] [Ingest Manager] Add tests to verify field parsing behavior. (#82809) * Add tests to verify field parsing behavior. * Verify behavior for multiple field redefinitions. --- .../server/services/epm/fields/field.test.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts index abd2ba777e516..dcd8846fa96a4 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts @@ -547,4 +547,77 @@ describe('processFields', () => { ]; expect(processFields(nested)).toEqual(nestedExpected); }); + + test('ignores redefinitions of a field', () => { + const fields = [ + { + name: 'a', + type: 'text', + }, + { + name: 'a', + type: 'number', + }, + { + name: 'b.c', + type: 'number', + }, + { + name: 'b', + type: 'group', + fields: [ + { + name: 'c', + type: 'text', + }, + ], + }, + ]; + + const fieldsExpected = [ + { + name: 'a', + // should preserve the field that was parsed first which had type: text + type: 'text', + }, + { + name: 'b', + type: 'group', + fields: [ + { + name: 'c', + // should preserve the field that was parsed first which had type: number + type: 'number', + }, + ], + }, + ]; + expect(processFields(fields)).toEqual(fieldsExpected); + }); + + test('ignores multiple redefinitions of a field', () => { + const fields = [ + { + name: 'a', + type: 'text', + }, + { + name: 'a', + type: 'number', + }, + { + name: 'a', + type: 'keyword', + }, + ]; + + const fieldsExpected = [ + { + name: 'a', + // should preserve the field that was parsed first which had type: text + type: 'text', + }, + ]; + expect(processFields(fields)).toEqual(fieldsExpected); + }); }); From dae28519e654c3021b6795010a2a54289c9af2df Mon Sep 17 00:00:00 2001 From: ymao1 Date: Fri, 6 Nov 2020 07:28:08 -0500 Subject: [PATCH 56/57] [Alerting] Display Action Group in Alert Details (#82645) * Adding action group id to event log. Showing action group as part of status in alert details view * Simplifying getting action group id * Cleanup * Adding unit tests * Updating functional tests * Updating test * Fix types check * Updating test * PR fixes * PR fixes --- .../alerts/common/alert_instance_summary.ts | 1 + .../tests/get_alert_instance_summary.test.ts | 9 +- ...rt_instance_summary_from_event_log.test.ts | 117 +++++++++++++++--- .../alert_instance_summary_from_event_log.ts | 3 + .../server/task_runner/task_runner.test.ts | 11 +- .../alerts/server/task_runner/task_runner.ts | 18 +-- .../plugins/event_log/generated/mappings.json | 4 + x-pack/plugins/event_log/generated/schemas.ts | 1 + x-pack/plugins/event_log/scripts/mappings.js | 5 + .../components/alert_details.tsx | 1 + .../components/alert_instances.test.tsx | 96 ++++++++++---- .../components/alert_instances.tsx | 38 +++++- .../components/alert_instances_route.test.tsx | 22 +++- .../components/alert_instances_route.tsx | 5 +- .../spaces_only/tests/alerting/event_log.ts | 2 +- .../alerting/get_alert_instance_summary.ts | 2 + .../apps/triggers_actions_ui/details.ts | 6 +- 17 files changed, 276 insertions(+), 65 deletions(-) diff --git a/x-pack/plugins/alerts/common/alert_instance_summary.ts b/x-pack/plugins/alerts/common/alert_instance_summary.ts index 08c3b2fc2c241..1aa183a141eab 100644 --- a/x-pack/plugins/alerts/common/alert_instance_summary.ts +++ b/x-pack/plugins/alerts/common/alert_instance_summary.ts @@ -27,5 +27,6 @@ export interface AlertInstanceSummary { export interface AlertInstanceStatus { status: AlertInstanceStatusValues; muted: boolean; + actionGroupId?: string; activeStartDate?: string; } diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index a53e49337f385..9cb2a33222d23 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -118,12 +118,12 @@ describe('getAlertInstanceSummary()', () => { .addExecute() .addNewInstance('instance-currently-active') .addNewInstance('instance-previously-active') - .addActiveInstance('instance-currently-active') - .addActiveInstance('instance-previously-active') + .addActiveInstance('instance-currently-active', 'action group A') + .addActiveInstance('instance-previously-active', 'action group B') .advanceTime(10000) .addExecute() .addResolvedInstance('instance-previously-active') - .addActiveInstance('instance-currently-active') + .addActiveInstance('instance-currently-active', 'action group A') .getEvents(); const eventsResult = { ...AlertInstanceSummaryFindEventsResult, @@ -144,16 +144,19 @@ describe('getAlertInstanceSummary()', () => { "id": "1", "instances": Object { "instance-currently-active": Object { + "actionGroupId": "action group A", "activeStartDate": "2019-02-12T21:01:22.479Z", "muted": false, "status": "Active", }, "instance-muted-no-activity": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", }, "instance-previously-active": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts index 566a1770c0658..f9e4a2908d6ce 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts @@ -104,11 +104,13 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", }, "instance-2": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", @@ -184,7 +186,7 @@ describe('alertInstanceSummaryFromEventLog', () => { const events = eventsFactory .addExecute() .addNewInstance('instance-1') - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() .addResolvedInstance('instance-1') @@ -202,6 +204,7 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", @@ -218,7 +221,7 @@ describe('alertInstanceSummaryFromEventLog', () => { const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() .addResolvedInstance('instance-1') @@ -236,6 +239,7 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", @@ -253,10 +257,10 @@ describe('alertInstanceSummaryFromEventLog', () => { const events = eventsFactory .addExecute() .addNewInstance('instance-1') - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -271,6 +275,79 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": "action group A", + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": false, + "status": "Active", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + }); + + test('alert with currently active instance with no action group in event log', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-1') + .addActiveInstance('instance-1', undefined) + .advanceTime(10000) + .addExecute() + .addActiveInstance('instance-1', undefined) + .getEvents(); + + const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ + alert, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, instances } = summary; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "actionGroupId": undefined, + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": false, + "status": "Active", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + }); + + test('alert with currently active instance that switched action groups', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') + .advanceTime(10000) + .addExecute() + .addActiveInstance('instance-1', 'action group B') + .getEvents(); + + const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ + alert, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, instances } = summary; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "actionGroupId": "action group B", "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": false, "status": "Active", @@ -287,10 +364,10 @@ describe('alertInstanceSummaryFromEventLog', () => { const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -305,6 +382,7 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": "action group A", "activeStartDate": undefined, "muted": false, "status": "Active", @@ -322,12 +400,12 @@ describe('alertInstanceSummaryFromEventLog', () => { const events = eventsFactory .addExecute() .addNewInstance('instance-1') - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .addNewInstance('instance-2') - .addActiveInstance('instance-2') + .addActiveInstance('instance-2', 'action group B') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .addResolvedInstance('instance-2') .getEvents(); @@ -343,11 +421,13 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": "action group A", "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": true, "status": "Active", }, "instance-2": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", @@ -365,19 +445,19 @@ describe('alertInstanceSummaryFromEventLog', () => { const events = eventsFactory .addExecute() .addNewInstance('instance-1') - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .addNewInstance('instance-2') - .addActiveInstance('instance-2') + .addActiveInstance('instance-2', 'action group B') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .addResolvedInstance('instance-2') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group B') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group B') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -392,11 +472,13 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": "action group B", "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": false, "status": "Active", }, "instance-2": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", @@ -452,14 +534,17 @@ export class EventsFactory { return this; } - addActiveInstance(instanceId: string): EventsFactory { + addActiveInstance(instanceId: string, actionGroupId: string | undefined): EventsFactory { + const kibanaAlerting = actionGroupId + ? { instance_id: instanceId, action_group_id: actionGroupId } + : { instance_id: instanceId }; this.events.push({ '@timestamp': this.date, event: { provider: EVENT_LOG_PROVIDER, action: EVENT_LOG_ACTIONS.activeInstance, }, - kibana: { alerting: { instance_id: instanceId } }, + kibana: { alerting: kibanaAlerting }, }); return this; } diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts index 9a5e870c8199a..8fed97a74435d 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts @@ -78,10 +78,12 @@ export function alertInstanceSummaryFromEventLog( // intentionally no break here case EVENT_LOG_ACTIONS.activeInstance: status.status = 'Active'; + status.actionGroupId = event?.kibana?.alerting?.action_group_id; break; case EVENT_LOG_ACTIONS.resolvedInstance: status.status = 'OK'; status.activeStartDate = undefined; + status.actionGroupId = undefined; } } @@ -118,6 +120,7 @@ function getAlertInstanceStatus( const status: AlertInstanceStatus = { status: 'OK', muted: false, + actionGroupId: undefined, activeStartDate: undefined, }; instances.set(instanceId, status); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 86e78dea66a09..4d0d69010914e 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -292,6 +292,7 @@ describe('Task Runner', () => { kibana: { alerting: { instance_id: '1', + action_group_id: 'default', }, saved_objects: [ { @@ -302,7 +303,7 @@ describe('Task Runner', () => { }, ], }, - message: "test:1: 'alert-name' active instance: '1'", + message: "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }); expect(eventLogger.logEvent).toHaveBeenCalledWith({ event: { @@ -424,6 +425,7 @@ describe('Task Runner', () => { }, "kibana": Object { "alerting": Object { + "action_group_id": undefined, "instance_id": "1", }, "saved_objects": Array [ @@ -445,6 +447,7 @@ describe('Task Runner', () => { }, "kibana": Object { "alerting": Object { + "action_group_id": "default", "instance_id": "1", }, "saved_objects": Array [ @@ -456,7 +459,7 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' active instance: '1'", + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], Array [ @@ -565,6 +568,7 @@ describe('Task Runner', () => { }, "kibana": Object { "alerting": Object { + "action_group_id": undefined, "instance_id": "2", }, "saved_objects": Array [ @@ -586,6 +590,7 @@ describe('Task Runner', () => { }, "kibana": Object { "alerting": Object { + "action_group_id": "default", "instance_id": "1", }, "saved_objects": Array [ @@ -597,7 +602,7 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' active instance: '1'", + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], ] diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 2611ba766173b..6a49f67268d69 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { pickBy, mapValues, without } from 'lodash'; +import { Dictionary, pickBy, mapValues, without } from 'lodash'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; @@ -224,11 +224,10 @@ export class TaskRunner { const instancesWithScheduledActions = pickBy(alertInstances, (alertInstance: AlertInstance) => alertInstance.hasScheduledActions() ); - const currentAlertInstanceIds = Object.keys(instancesWithScheduledActions); generateNewAndResolvedInstanceEvents({ eventLogger, originalAlertInstanceIds, - currentAlertInstanceIds, + currentAlertInstances: instancesWithScheduledActions, alertId, alertLabel, namespace, @@ -382,7 +381,7 @@ export class TaskRunner { interface GenerateNewAndResolvedInstanceEventsParams { eventLogger: IEventLogger; originalAlertInstanceIds: string[]; - currentAlertInstanceIds: string[]; + currentAlertInstances: Dictionary; alertId: string; alertLabel: string; namespace: string | undefined; @@ -393,9 +392,10 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst eventLogger, alertId, namespace, - currentAlertInstanceIds, + currentAlertInstances, originalAlertInstanceIds, } = params; + const currentAlertInstanceIds = Object.keys(currentAlertInstances); const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds); const resolvedIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); @@ -411,11 +411,12 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst } for (const id of currentAlertInstanceIds) { - const message = `${params.alertLabel} active instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.activeInstance, message); + const actionGroup = currentAlertInstances[id].getScheduledActionOptions()?.actionGroup; + const message = `${params.alertLabel} active instance: '${id}' in actionGroup: '${actionGroup}'`; + logInstanceEvent(id, EVENT_LOG_ACTIONS.activeInstance, message, actionGroup); } - function logInstanceEvent(instanceId: string, action: string, message: string) { + function logInstanceEvent(instanceId: string, action: string, message: string, group?: string) { const event: IEvent = { event: { action, @@ -423,6 +424,7 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst kibana: { alerting: { instance_id: instanceId, + action_group_id: group, }, saved_objects: [ { diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 0a858969c4f6a..5c7eb50117d9b 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -81,6 +81,10 @@ "instance_id": { "type": "keyword", "ignore_above": 1024 + }, + "action_group_id": { + "type": "keyword", + "ignore_above": 1024 } } }, diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 57fe90a8e876e..3dbb43b15350f 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -60,6 +60,7 @@ export const EventSchema = schema.maybe( alerting: schema.maybe( schema.object({ instance_id: ecsString(), + action_group_id: ecsString(), }) ), saved_objects: schema.maybe( diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index fd149d132031e..c9af2b0aa57fb 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -18,6 +18,10 @@ exports.EcsKibanaExtensionsMappings = { type: 'keyword', ignore_above: 1024, }, + action_group_id: { + type: 'keyword', + ignore_above: 1024, + }, }, }, // array of saved object references, for "linking" via search @@ -63,6 +67,7 @@ exports.EcsEventLogProperties = [ 'user.name', 'kibana.server_uuid', 'kibana.alerting.instance_id', + 'kibana.alerting.action_group_id', 'kibana.saved_objects.rel', 'kibana.saved_objects.namespace', 'kibana.saved_objects.id', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 1272024557bb6..abd8127962561 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -301,6 +301,7 @@ export const AlertDetails: React.FunctionComponent = ({ ) : ( 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 e1287d299b6e9..25bbe977fd76a 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 @@ -7,7 +7,7 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; import { AlertInstances, AlertInstanceListItem, alertInstanceToListItem } from './alert_instances'; -import { Alert, AlertInstanceSummary, AlertInstanceStatus } from '../../../../types'; +import { Alert, AlertInstanceSummary, AlertInstanceStatus, AlertType } from '../../../../types'; import { EuiBasicTable } from '@elastic/eui'; const fakeNow = new Date('2020-02-09T23:15:41.941Z'); @@ -34,15 +34,18 @@ jest.mock('../../../app_context', () => { describe('alert_instances', () => { it('render a list of alert instances', () => { const alert = mockAlert(); + const alertType = mockAlertType(); const alertInstanceSummary = mockAlertInstanceSummary({ instances: { first_instance: { status: 'OK', muted: false, + actionGroupId: 'default', }, second_instance: { status: 'Active', muted: false, + actionGroupId: 'action group id unknown', }, }, }); @@ -51,14 +54,14 @@ describe('alert_instances', () => { // active first alertInstanceToListItem( fakeNow.getTime(), - alert, + alertType, 'second_instance', alertInstanceSummary.instances.second_instance ), // ok second alertInstanceToListItem( fakeNow.getTime(), - alert, + alertType, 'first_instance', alertInstanceSummary.instances.first_instance ), @@ -69,6 +72,7 @@ describe('alert_instances', () => { @@ -80,6 +84,7 @@ describe('alert_instances', () => { it('render a hidden field with duration epoch', () => { const alert = mockAlert(); + const alertType = mockAlertType(); const alertInstanceSummary = mockAlertInstanceSummary(); expect( @@ -88,6 +93,7 @@ describe('alert_instances', () => { durationEpoch={fake2MinutesAgo.getTime()} {...mockAPIs} alert={alert} + alertType={alertType} readOnly={false} alertInstanceSummary={alertInstanceSummary} /> @@ -99,6 +105,7 @@ describe('alert_instances', () => { it('render all active alert instances', () => { const alert = mockAlert(); + const alertType = mockAlertType(); const instances: Record = { ['us-central']: { status: 'OK', @@ -114,6 +121,7 @@ describe('alert_instances', () => { { .find(EuiBasicTable) .prop('items') ).toEqual([ - alertInstanceToListItem(fakeNow.getTime(), alert, 'us-central', instances['us-central']), - alertInstanceToListItem(fakeNow.getTime(), alert, 'us-east', instances['us-east']), + alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-central', instances['us-central']), + alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-east', instances['us-east']), ]); }); @@ -132,6 +140,7 @@ describe('alert_instances', () => { const alert = mockAlert({ mutedInstanceIds: ['us-west', 'us-east'], }); + const alertType = mockAlertType(); const instanceUsWest: AlertInstanceStatus = { status: 'OK', muted: false }; const instanceUsEast: AlertInstanceStatus = { status: 'OK', muted: false }; @@ -140,6 +149,7 @@ describe('alert_instances', () => { { .find(EuiBasicTable) .prop('items') ).toEqual([ - alertInstanceToListItem(fakeNow.getTime(), alert, 'us-west', instanceUsWest), - alertInstanceToListItem(fakeNow.getTime(), alert, 'us-east', instanceUsEast), + alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-west', instanceUsWest), + alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-east', instanceUsEast), ]); }); }); describe('alertInstanceToListItem', () => { it('handles active instances', () => { - const alert = mockAlert(); + const alertType = mockAlertType({ + actionGroups: [ + { id: 'default', name: 'Default Action Group' }, + { id: 'testing', name: 'Test Action Group' }, + ], + }); const start = fake2MinutesAgo; const instance: AlertInstanceStatus = { status: 'Active', muted: false, activeStartDate: fake2MinutesAgo.toISOString(), + actionGroupId: 'testing', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ instance: 'id', - status: { label: 'Active', healthColor: 'primary' }, + status: { label: 'Active', actionGroup: 'Test Action Group', healthColor: 'primary' }, start, sortPriority: 0, duration: fakeNow.getTime() - fake2MinutesAgo.getTime(), @@ -184,20 +200,38 @@ describe('alertInstanceToListItem', () => { }); }); - it('handles active muted instances', () => { - const alert = mockAlert({ - mutedInstanceIds: ['id'], + it('handles active instances with no action group id', () => { + const alertType = mockAlertType(); + const start = fake2MinutesAgo; + const instance: AlertInstanceStatus = { + status: 'Active', + muted: false, + activeStartDate: fake2MinutesAgo.toISOString(), + }; + + expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ + instance: 'id', + status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' }, + start, + sortPriority: 0, + duration: fakeNow.getTime() - fake2MinutesAgo.getTime(), + isMuted: false, }); + }); + + it('handles active muted instances', () => { + const alertType = mockAlertType(); const start = fake2MinutesAgo; const instance: AlertInstanceStatus = { status: 'Active', muted: true, activeStartDate: fake2MinutesAgo.toISOString(), + actionGroupId: 'default', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ instance: 'id', - status: { label: 'Active', healthColor: 'primary' }, + status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' }, start, sortPriority: 0, duration: fakeNow.getTime() - fake2MinutesAgo.getTime(), @@ -206,15 +240,16 @@ describe('alertInstanceToListItem', () => { }); it('handles active instances with start date', () => { - const alert = mockAlert(); + const alertType = mockAlertType(); const instance: AlertInstanceStatus = { status: 'Active', muted: false, + actionGroupId: 'default', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ instance: 'id', - status: { label: 'Active', healthColor: 'primary' }, + status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' }, start: undefined, duration: 0, sortPriority: 0, @@ -223,14 +258,13 @@ describe('alertInstanceToListItem', () => { }); it('handles muted inactive instances', () => { - const alert = mockAlert({ - mutedInstanceIds: ['id'], - }); + const alertType = mockAlertType(); const instance: AlertInstanceStatus = { status: 'OK', muted: true, + actionGroupId: 'default', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ instance: 'id', status: { label: 'OK', healthColor: 'subdued' }, start: undefined, @@ -268,6 +302,23 @@ function mockAlert(overloads: Partial = {}): Alert { }; } +function mockAlertType(overloads: Partial = {}): AlertType { + return { + id: 'test.testAlertType', + name: 'My Test Alert Type', + actionGroups: [{ id: 'default', name: 'Default Action Group' }], + actionVariables: { + context: [], + state: [], + params: [], + }, + defaultActionGroupId: 'default', + authorizedConsumers: {}, + producer: 'alerts', + ...overloads, + }; +} + function mockAlertInstanceSummary( overloads: Partial = {} ): AlertInstanceSummary { @@ -288,6 +339,7 @@ function mockAlertInstanceSummary( foo: { status: 'OK', muted: false, + actionGroupId: 'testActionGroup', }, }, }; 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 0648f34927db3..ed05d81646c4a 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,8 +11,14 @@ 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 { ActionGroup, AlertInstanceStatusValues } from '../../../../../../alerts/common'; +import { + Alert, + AlertInstanceSummary, + AlertInstanceStatus, + AlertType, + Pagination, +} from '../../../../types'; import { ComponentOpts as AlertApis, withBulkAlertOperations, @@ -21,6 +27,7 @@ import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; type AlertInstancesProps = { alert: Alert; + alertType: AlertType; readOnly: boolean; alertInstanceSummary: AlertInstanceSummary; requestRefresh: () => Promise; @@ -48,7 +55,12 @@ export const alertInstancesTableColumns = ( { defaultMessage: 'Status' } ), render: (value: AlertInstanceListItemStatus, instance: AlertInstanceListItem) => { - return {value.label}; + return ( + + {value.label} + {value.actionGroup ? ` (${value.actionGroup})` : ``} + + ); }, sortable: false, 'data-test-subj': 'alertInstancesTableCell-status', @@ -113,6 +125,7 @@ function durationAsString(duration: Duration): string { export function AlertInstances({ alert, + alertType, readOnly, alertInstanceSummary, muteAlertInstance, @@ -127,7 +140,7 @@ export function AlertInstances({ const alertInstances = Object.entries(alertInstanceSummary.instances) .map(([instanceId, instance]) => - alertInstanceToListItem(durationEpoch, alert, instanceId, instance) + alertInstanceToListItem(durationEpoch, alertType, instanceId, instance) ) .sort((leftInstance, rightInstance) => leftInstance.sortPriority - rightInstance.sortPriority); @@ -180,6 +193,7 @@ function getPage(items: any[], pagination: Pagination) { interface AlertInstanceListItemStatus { label: string; healthColor: string; + actionGroup?: string; } export interface AlertInstanceListItem { instance: string; @@ -200,16 +214,28 @@ const INACTIVE_LABEL = i18n.translate( { defaultMessage: 'OK' } ); +function getActionGroupName(alertType: AlertType, actionGroupId?: string): string | undefined { + actionGroupId = actionGroupId || alertType.defaultActionGroupId; + const actionGroup = alertType?.actionGroups?.find( + (group: ActionGroup) => group.id === actionGroupId + ); + return actionGroup?.name; +} + export function alertInstanceToListItem( durationEpoch: number, - alert: Alert, + alertType: AlertType, instanceId: string, instance: AlertInstanceStatus ): AlertInstanceListItem { const isMuted = !!instance?.muted; const status = instance?.status === 'Active' - ? { label: ACTIVE_LABEL, healthColor: 'primary' } + ? { + label: ACTIVE_LABEL, + actionGroup: getActionGroupName(alertType, instance?.actionGroupId), + healthColor: 'primary', + } : { label: INACTIVE_LABEL, healthColor: 'subdued' }; const start = instance?.activeStartDate ? new Date(instance.activeStartDate) : undefined; const duration = start ? durationEpoch - start.valueOf() : 0; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx index 603f06d0bbae4..3a171d469d4ad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx @@ -8,7 +8,7 @@ import uuid from 'uuid'; import { shallow } from 'enzyme'; import { ToastsApi } from 'kibana/public'; import { AlertInstancesRoute, getAlertInstanceSummary } from './alert_instances_route'; -import { Alert, AlertInstanceSummary } from '../../../../types'; +import { Alert, AlertInstanceSummary, AlertType } from '../../../../types'; import { EuiLoadingSpinner } from '@elastic/eui'; const fakeNow = new Date('2020-02-09T23:15:41.941Z'); @@ -23,10 +23,11 @@ jest.mock('../../../app_context', () => { describe('alert_instance_summary_route', () => { it('render a loader while fetching data', () => { const alert = mockAlert(); + const alertType = mockAlertType(); expect( shallow( - + ).containsMatchingElement() ).toBeTruthy(); }); @@ -140,6 +141,23 @@ function mockAlert(overloads: Partial = {}): Alert { }; } +function mockAlertType(overloads: Partial = {}): AlertType { + return { + id: 'test.testAlertType', + name: 'My Test Alert Type', + actionGroups: [{ id: 'default', name: 'Default Action Group' }], + actionVariables: { + context: [], + state: [], + params: [], + }, + defaultActionGroupId: 'default', + authorizedConsumers: {}, + producer: 'alerts', + ...overloads, + }; +} + function mockAlertInstanceSummary(overloads: Partial = {}): any { const summary: AlertInstanceSummary = { id: 'alert-id', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx index 9137a26a32dd4..83a09e9eafcc1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { ToastsApi } from 'kibana/public'; import React, { useState, useEffect } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { Alert, AlertInstanceSummary } from '../../../../types'; +import { Alert, AlertInstanceSummary, AlertType } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { ComponentOpts as AlertApis, @@ -18,12 +18,14 @@ import { AlertInstancesWithApi as AlertInstances } from './alert_instances'; type WithAlertInstanceSummaryProps = { alert: Alert; + alertType: AlertType; readOnly: boolean; requestRefresh: () => Promise; } & Pick; export const AlertInstancesRoute: React.FunctionComponent = ({ alert, + alertType, readOnly, requestRefresh, loadAlertInstanceSummary: loadAlertInstanceSummary, @@ -48,6 +50,7 @@ export const AlertInstancesRoute: React.FunctionComponent diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index a5dff437283ae..dbf8eb162fca7 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -134,7 +134,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { validateInstanceEvent(event, `resolved instance: 'instance'`); break; case 'active-instance': - validateInstanceEvent(event, `active instance: 'instance'`); + validateInstanceEvent(event, `active instance: 'instance' in actionGroup: 'default'`); break; // this will get triggered as we add new event actions default: diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts index 563127e028a62..22034328e5275 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts @@ -226,6 +226,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr instanceA: { status: 'Active', muted: false, + actionGroupId: 'default', activeStartDate: actualInstances.instanceA.activeStartDate, }, instanceB: { @@ -235,6 +236,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr instanceC: { status: 'Active', muted: true, + actionGroupId: 'default', activeStartDate: actualInstances.instanceC.activeStartDate, }, instanceD: { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 4c97c8556d7df..9e4006681dc8d 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -392,21 +392,21 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(instancesList.map((instance) => omit(instance, 'duration'))).to.eql([ { instance: 'us-central', - status: 'Active', + status: 'Active (Default)', start: moment(dateOnAllInstancesFromApiResponse['us-central']) .utc() .format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-east', - status: 'Active', + status: 'Active (Default)', start: moment(dateOnAllInstancesFromApiResponse['us-east']) .utc() .format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-west', - status: 'Active', + status: 'Active (Default)', start: moment(dateOnAllInstancesFromApiResponse['us-west']) .utc() .format('D MMM YYYY @ HH:mm:ss'), From 94d0e6070631b4bd1930666159edba86dce81c16 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 6 Nov 2020 13:47:55 +0100 Subject: [PATCH 57/57] [Lens] Implement time scaling function (#82104) --- src/plugins/data/common/search/aggs/types.ts | 2 +- .../aggs/utils/time_column_meta.test.ts | 41 +- .../search/aggs/utils/time_column_meta.ts | 10 +- .../data/public/search/expressions/esaggs.ts | 7 +- .../public/indexpattern_datasource/index.ts | 22 +- .../indexpattern_datasource/indexpattern.tsx | 1 + .../time_scale.test.ts | 368 ++++++++++++++++++ .../indexpattern_datasource/time_scale.ts | 167 ++++++++ 8 files changed, 601 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index 09a13762d4d70..897b60e91b100 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -94,7 +94,7 @@ export interface AggsCommonStart { */ getDateMetaByDatatableColumn: ( column: DatatableColumn - ) => Promise; + ) => Promise; createAggConfigs: ( indexPattern: IndexPattern, configStates?: CreateAggConfigParams[], diff --git a/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts b/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts index e56d622734554..8eb076f5b7906 100644 --- a/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts +++ b/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts @@ -91,6 +91,43 @@ describe('getDateMetaByDatatableColumn', () => { }); }); + it('throws if unable to resolve interval', async () => { + await expect( + getDateMetaByDatatableColumn(params)({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.DATE_HISTOGRAM, + params: { + time_zone: 'UTC', + interval: 'auto', + }, + }, + }, + }) + ).rejects.toBeDefined(); + + await expect( + getDateMetaByDatatableColumn(params)({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.DATE_HISTOGRAM, + params: { + time_zone: 'UTC', + }, + }, + }, + }) + ).rejects.toBeDefined(); + }); + it('returns resolved auto interval', async () => { expect( await getDateMetaByDatatableColumn(params)({ @@ -106,8 +143,8 @@ describe('getDateMetaByDatatableColumn', () => { interval: 'auto', }, appliedTimeRange: { - from: 'now-5d', - to: 'now', + from: '2020-10-05T00:00:00.000Z', + to: '2020-10-10T00:00:00.000Z', }, }, }, diff --git a/src/plugins/data/common/search/aggs/utils/time_column_meta.ts b/src/plugins/data/common/search/aggs/utils/time_column_meta.ts index 1bea716c6a049..7ed8cb79f63f4 100644 --- a/src/plugins/data/common/search/aggs/utils/time_column_meta.ts +++ b/src/plugins/data/common/search/aggs/utils/time_column_meta.ts @@ -38,11 +38,11 @@ export const getDateMetaByDatatableColumn = ({ getConfig, }: DateMetaByColumnDeps) => async ( column: DatatableColumn -): Promise => { +): Promise => { if (column.meta.source !== 'esaggs') return; if (column.meta.sourceParams?.type !== BUCKET_TYPES.DATE_HISTOGRAM) return; const params = column.meta.sourceParams.params as AggParamsDateHistogram; - const appliedTimeRange = column.meta.sourceParams.appliedTimeRange as TimeRange; + const appliedTimeRange = column.meta.sourceParams.appliedTimeRange as TimeRange | undefined; const tz = inferTimeZone( params, @@ -52,9 +52,11 @@ export const getDateMetaByDatatableColumn = ({ ); const interval = - params.interval === 'auto' ? calculateAutoTimeExpression(appliedTimeRange) : params.interval; + params.interval === 'auto' && appliedTimeRange + ? calculateAutoTimeExpression(appliedTimeRange) + : params.interval; - if (!interval) { + if (!interval || interval === 'auto') { throw new Error('time interval could not be determined'); } diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index dba77d398c8b6..3932484801fa8 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -267,6 +267,8 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ searchSource.setField('index', indexPattern); searchSource.setField('size', 0); + const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange); + const response = await handleCourierRequest({ searchSource, aggs, @@ -303,7 +305,10 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ input?.timeRange && args.timeFields && args.timeFields.includes(column.aggConfig.params.field?.name) - ? { from: input.timeRange.from, to: input.timeRange.to } + ? { + from: resolvedTimeRange?.min?.toISOString(), + to: resolvedTimeRange?.max?.toISOString(), + } : undefined, ...column.aggConfig.serialize(), }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 35987656f6670..92280b0fb6ce6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -33,19 +33,23 @@ export class IndexPatternDatasource { { expressions, editorFrame, charts }: IndexPatternDatasourceSetupPlugins ) { editorFrame.registerDatasource(async () => { - const { getIndexPatternDatasource, renameColumns, formatColumn } = await import( - '../async_services' - ); - expressions.registerFunction(renameColumns); - expressions.registerFunction(formatColumn); - return core.getStartServices().then(([coreStart, { data }]) => - getIndexPatternDatasource({ + const { + getIndexPatternDatasource, + renameColumns, + formatColumn, + getTimeScaleFunction, + } = await import('../async_services'); + return core.getStartServices().then(([coreStart, { data }]) => { + expressions.registerFunction(getTimeScaleFunction(data)); + expressions.registerFunction(renameColumns); + expressions.registerFunction(formatColumn); + return getIndexPatternDatasource({ core: coreStart, storage: new Storage(localStorage), data, charts, - }) - ) as Promise; + }); + }) as Promise; }); } } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 0d82292780808..ecca1b878e9a7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -76,6 +76,7 @@ export function columnToOperation(column: IndexPatternColumn, uniqueLabel?: stri export * from './rename_columns'; export * from './format_column'; +export * from './time_scale'; export function getIndexPatternDatasource({ core, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts new file mode 100644 index 0000000000000..c29e2cd9567dc --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts @@ -0,0 +1,368 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { Datatable } from 'src/plugins/expressions/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { functionWrapper } from 'src/plugins/expressions/common/expression_functions/specs/tests/utils'; +import { getTimeScaleFunction, TimeScaleArgs } from './time_scale'; + +describe('time_scale', () => { + let timeScale: (input: Datatable, args: TimeScaleArgs) => Promise; + let dataMock: jest.Mocked; + + const emptyTable: Datatable = { + type: 'datatable', + columns: [ + { + id: 'date', + name: 'date', + meta: { + type: 'date', + }, + }, + { + id: 'metric', + name: 'metric', + meta: { + type: 'number', + }, + }, + ], + rows: [], + }; + + const defaultArgs: TimeScaleArgs = { + dateColumnId: 'date', + inputColumnId: 'metric', + outputColumnId: 'scaledMetric', + targetUnit: 'h', + }; + + beforeEach(() => { + dataMock = dataPluginMock.createStartContract(); + (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + timeZone: 'UTC', + timeRange: { + from: '2020-10-05T00:00:00.000Z', + to: '2020-10-10T00:00:00.000Z', + }, + interval: '1d', + }); + (dataMock.query.timefilter.timefilter.calculateBounds as jest.Mock).mockImplementation( + ({ from, to }) => ({ + min: moment(from), + max: moment(to), + }) + ); + timeScale = functionWrapper(getTimeScaleFunction(dataMock)); + }); + + it('should apply time scale factor to each row', async () => { + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + date: moment('2020-10-05T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-06T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-07T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-08T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-09T00:00:00.000Z').valueOf(), + metric: 24, + }, + ], + }, + { + ...defaultArgs, + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1, 1]); + }); + + it('should skip gaps in the data', async () => { + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + date: moment('2020-10-05T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-06T00:00:00.000Z').valueOf(), + }, + { + date: moment('2020-10-07T00:00:00.000Z').valueOf(), + }, + { + date: moment('2020-10-08T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-09T00:00:00.000Z').valueOf(), + metric: 24, + }, + ], + }, + { + ...defaultArgs, + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([ + 1, + undefined, + undefined, + 1, + 1, + ]); + }); + + it('should return input unchanged if input column does not exist', async () => { + const mismatchedTable = { + ...emptyTable, + rows: [ + { + date: moment('2020-10-05T00:00:00.000Z').valueOf(), + metric: 24, + }, + ], + }; + const result = await timeScale(mismatchedTable, { + ...defaultArgs, + inputColumnId: 'nonexistent', + }); + + expect(result).toBe(mismatchedTable); + }); + + it('should be able to scale up as well', async () => { + (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + timeZone: 'UTC', + timeRange: { + from: '2020-10-05T12:00:00.000Z', + to: '2020-10-05T16:00:00.000Z', + }, + interval: '1h', + }); + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + date: moment('2020-10-05T12:00:00.000Z').valueOf(), + metric: 1, + }, + { + date: moment('2020-10-05T13:00:00.000Z').valueOf(), + metric: 1, + }, + { + date: moment('2020-10-05T14:00:00.000Z').valueOf(), + metric: 1, + }, + { + date: moment('2020-10-05T15:00:00.000Z').valueOf(), + metric: 1, + }, + ], + }, + { + ...defaultArgs, + targetUnit: 'd', + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([24, 24, 24, 24]); + }); + + it('can scale starting from unit multiple target intervals', async () => { + (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + timeZone: 'UTC', + timeRange: { + from: '2020-10-05T13:00:00.000Z', + to: '2020-10-05T23:00:00.000Z', + }, + interval: '3h', + }); + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + // bucket is cut off by one hour because of the time range + date: moment('2020-10-05T12:00:00.000Z').valueOf(), + metric: 2, + }, + { + date: moment('2020-10-05T15:00:00.000Z').valueOf(), + metric: 3, + }, + { + date: moment('2020-10-05T18:00:00.000Z').valueOf(), + metric: 3, + }, + { + // bucket is cut off by one hour because of the time range + date: moment('2020-10-05T21:00:00.000Z').valueOf(), + metric: 2, + }, + ], + }, + { + ...defaultArgs, + targetUnit: 'h', + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1]); + }); + + it('take start and end of timerange into account', async () => { + (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + timeZone: 'UTC', + timeRange: { + from: '2020-10-05T12:00:00.000Z', + to: '2020-10-09T12:00:00.000Z', + }, + interval: '1d', + }); + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + // this is a partial bucket because it starts before the start of the time range + date: moment('2020-10-05T00:00:00.000Z').valueOf(), + metric: 12, + }, + { + date: moment('2020-10-06T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-07T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-08T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + // this is a partial bucket because it ends earlier than the regular interval of 1d + date: moment('2020-10-09T00:00:00.000Z').valueOf(), + metric: 12, + }, + ], + }, + { + ...defaultArgs, + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1, 1]); + }); + + it('should respect DST switches', async () => { + (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + timeZone: 'Europe/Berlin', + timeRange: { + from: '2020-10-23T00:00:00.000+02:00', + to: '2020-10-27T00:00:00.000+01:00', + }, + interval: '1d', + }); + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + date: moment('2020-10-23T00:00:00.000+02:00').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-24T00:00:00.000+02:00').valueOf(), + metric: 24, + }, + { + // this day has one hour more in Europe/Berlin due to DST switch + date: moment('2020-10-25T00:00:00.000+02:00').valueOf(), + metric: 25, + }, + { + date: moment('2020-10-26T00:00:00.000+01:00').valueOf(), + metric: 24, + }, + ], + }, + { + ...defaultArgs, + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1]); + }); + + it('take leap years into account', async () => { + (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + timeZone: 'UTC', + timeRange: { + from: '2010-01-01T00:00:00.000Z', + to: '2015-01-01T00:00:00.000Z', + }, + interval: '1y', + }); + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + date: moment('2010-01-01T00:00:00.000Z').valueOf(), + metric: 365, + }, + { + date: moment('2011-01-01T00:00:00.000Z').valueOf(), + metric: 365, + }, + { + // 2012 is a leap year and has an additional day + date: moment('2012-01-01T00:00:00.000Z').valueOf(), + metric: 366, + }, + { + date: moment('2013-01-01T00:00:00.000Z').valueOf(), + metric: 365, + }, + { + date: moment('2014-01-01T00:00:00.000Z').valueOf(), + metric: 365, + }, + ], + }, + { + ...defaultArgs, + targetUnit: 'd', + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1, 1]); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts new file mode 100644 index 0000000000000..0937f40eeb6d3 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment-timezone'; +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition, Datatable } from 'src/plugins/expressions/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { search } from '../../../../../src/plugins/data/public'; + +type TimeScaleUnit = 's' | 'm' | 'h' | 'd'; + +export interface TimeScaleArgs { + dateColumnId: string; + inputColumnId: string; + outputColumnId: string; + targetUnit: TimeScaleUnit; + outputColumnName?: string; +} + +const unitInMs: Record = { + s: 1000, + m: 1000 * 60, + h: 1000 * 60 * 60, + d: 1000 * 60 * 60 * 24, +}; + +export function getTimeScaleFunction(data: DataPublicPluginStart) { + const timeScale: ExpressionFunctionDefinition< + 'lens_time_scale', + Datatable, + TimeScaleArgs, + Promise + > = { + name: 'lens_time_scale', + type: 'datatable', + help: '', + args: { + dateColumnId: { + types: ['string'], + help: '', + required: true, + }, + inputColumnId: { + types: ['string'], + help: '', + required: true, + }, + outputColumnId: { + types: ['string'], + help: '', + required: true, + }, + outputColumnName: { + types: ['string'], + help: '', + }, + targetUnit: { + types: ['string'], + options: ['s', 'm', 'h', 'd'], + help: '', + required: true, + }, + }, + inputTypes: ['datatable'], + async fn( + input, + { dateColumnId, inputColumnId, outputColumnId, outputColumnName, targetUnit }: TimeScaleArgs + ) { + if (input.columns.some((column) => column.id === outputColumnId)) { + throw new Error( + i18n.translate('xpack.lens.functions.timeScale.columnConflictMessage', { + defaultMessage: 'Specified outputColumnId {columnId} already exists.', + values: { + columnId: outputColumnId, + }, + }) + ); + } + + const dateColumnDefinition = input.columns.find((column) => column.id === dateColumnId); + + if (!dateColumnDefinition) { + throw new Error( + i18n.translate('xpack.lens.functions.timeScale.dateColumnMissingMessage', { + defaultMessage: 'Specified dateColumnId {columnId} does not exist.', + values: { + columnId: dateColumnId, + }, + }) + ); + } + + const inputColumnDefinition = input.columns.find((column) => column.id === inputColumnId); + + if (!inputColumnDefinition) { + return input; + } + + const outputColumnDefinition = { + ...inputColumnDefinition, + id: outputColumnId, + name: outputColumnName || outputColumnId, + }; + + const resultColumns = [...input.columns]; + // add output column after input column in the table + resultColumns.splice( + resultColumns.indexOf(inputColumnDefinition) + 1, + 0, + outputColumnDefinition + ); + + const targetUnitInMs = unitInMs[targetUnit]; + const timeInfo = await data.search.aggs.getDateMetaByDatatableColumn(dateColumnDefinition); + const intervalDuration = timeInfo && search.aggs.parseInterval(timeInfo.interval); + + if (!timeInfo || !intervalDuration) { + throw new Error( + i18n.translate('xpack.lens.functions.timeScale.timeInfoMissingMessage', { + defaultMessage: 'Could not fetch date histogram information', + }) + ); + } + // the datemath plugin always parses dates by using the current default moment time zone. + // to use the configured time zone, we are switching just for the bounds calculation. + const defaultTimezone = moment().zoneName(); + moment.tz.setDefault(timeInfo.timeZone); + + const timeBounds = + timeInfo.timeRange && data.query.timefilter.timefilter.calculateBounds(timeInfo.timeRange); + + const result = { + ...input, + columns: resultColumns, + rows: input.rows.map((row) => { + const newRow = { ...row }; + + let startOfBucket = moment(row[dateColumnId]); + let endOfBucket = startOfBucket.clone().add(intervalDuration); + if (timeBounds && timeBounds.min) { + startOfBucket = moment.max(startOfBucket, timeBounds.min); + } + if (timeBounds && timeBounds.max) { + endOfBucket = moment.min(endOfBucket, timeBounds.max); + } + const bucketSize = endOfBucket.diff(startOfBucket); + const factor = bucketSize / targetUnitInMs; + + const currentValue = newRow[inputColumnId]; + if (currentValue != null) { + newRow[outputColumnId] = Number(currentValue) / factor; + } + + return newRow; + }), + }; + // reset default moment timezone + moment.tz.setDefault(defaultTimezone); + + return result; + }, + }; + return timeScale; +}