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.indexpatternsservice.getfieldsforindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md index f288573cd7abb..c06c3c6f68492 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md @@ -9,5 +9,5 @@ Get field list by providing an index patttern (or spec) Signature: ```typescript -getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; +getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md index 32bf6fc13b02c..aec84866b9e58 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md @@ -9,5 +9,5 @@ Get field list by providing { pattern } Signature: ```typescript -getFieldsForWildcard: (options: GetFieldsOptions) => Promise; +getFieldsForWildcard: (options?: GetFieldsOptions) => Promise; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md index 57bb98de09ebd..34df8656e9175 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -26,8 +26,8 @@ export declare class IndexPatternsService | [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | | [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | -| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | -| [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | +| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise<any> | Get field list by providing an index patttern (or spec) | +| [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options?: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | | [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | | [getIdsWithTitle](./kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
id: string;
title: string;
}>> | Get list of index pattern ids with titles | | [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | 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.getcapabilitiesforrollupindices.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md deleted file mode 100644 index ba2efcc9b75ca..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [getCapabilitiesForRollupIndices](./kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md) - -## getCapabilitiesForRollupIndices() function - -Signature: - -```typescript -export declare function getCapabilitiesForRollupIndices(indices: { - [key: string]: any; -}): { - [key: string]: any; -}; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| indices | {
[key: string]: any;
} | | - -Returns: - -`{ - [key: string]: any; -}` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md index f0989097a727d..addd29916d81d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md @@ -15,8 +15,6 @@ getFieldsForWildcard(options: { fieldCapsOptions?: { allow_no_indices: boolean; }; - type?: string; - rollupIndex?: string; }): Promise; ``` @@ -24,7 +22,7 @@ getFieldsForWildcard(options: { | Parameter | Type | Description | | --- | --- | --- | -| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allow_no_indices: boolean;
};
type?: string;
rollupIndex?: string;
} | | +| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allow_no_indices: boolean;
};
} | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md index 6528b1c213cca..e7c331bad64e8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract) => Promise; }; ``` @@ -22,6 +22,6 @@ start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): Returns: `{ - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract) => Promise; }` 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 4b4e861aef784..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 @@ -28,7 +28,6 @@ | Function | Description | | --- | --- | -| [getCapabilitiesForRollupIndices(indices)](./kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md) | | | [getDefaultSearchParams(uiSettingsClient)](./kibana-plugin-plugins-data-server.getdefaultsearchparams.md) | | | [getShardTimeout(config)](./kibana-plugin-plugins-data-server.getshardtimeout.md) | | | [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-server.gettime.md) | | @@ -59,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) | \* | @@ -77,7 +77,6 @@ | [esQuery](./kibana-plugin-plugins-data-server.esquery.md) | | | [fieldFormats](./kibana-plugin-plugins-data-server.fieldformats.md) | | | [indexPatterns](./kibana-plugin-plugins-data-server.indexpatterns.md) | | -| [mergeCapabilitiesWithFields](./kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md) | | | [search](./kibana-plugin-plugins-data-server.search.md) | | | [UI\_SETTINGS](./kibana-plugin-plugins-data-server.ui_settings.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md deleted file mode 100644 index 2880e2d0d8f2c..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [mergeCapabilitiesWithFields](./kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md) - -## mergeCapabilitiesWithFields variable - -Signature: - -```typescript -mergeCapabilitiesWithFields: (rollupIndexCapabilities: { - [key: string]: any; -}, fieldsFromFieldCapsApi: { - [key: string]: any; -}, previousFields?: FieldDescriptor[]) => FieldDescriptor[] -``` 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 8546ec51a1536..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, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => 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, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => 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/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap b/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap deleted file mode 100644 index cc4e75a7a0fb4..0000000000000 --- a/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap +++ /dev/null @@ -1,142 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`calls "kbn:bootstrap" scripts and links executables after installing deps: link bins 1`] = ` -Array [ - Array [ - Map { - "kibana" => Project { - "allDependencies": Object { - "bar": "link:packages/bar", - }, - "devDependencies": Object {}, - "isSinglePackageJsonProject": true, - "json": Object { - "dependencies": Object { - "bar": "link:packages/bar", - }, - "name": "kibana", - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/package.json", - "path": "/packages/kbn-pm/src/commands", - "productionDependencies": Object { - "bar": "link:packages/bar", - }, - "scripts": Object {}, - "targetLocation": "/packages/kbn-pm/src/commands/target", - "version": "1.0.0", - }, - "bar" => Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isSinglePackageJsonProject": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - }, - Map { - "kibana" => Array [ - Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isSinglePackageJsonProject": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - ], - "bar" => Array [], - }, - ], -] -`; - -exports[`calls "kbn:bootstrap" scripts and links executables after installing deps: script 1`] = ` -Array [ - Array [ - Object { - "args": Array [], - "debug": undefined, - "pkg": Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isSinglePackageJsonProject": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - "script": "kbn:bootstrap", - }, - ], -] -`; - -exports[`does not run installer if no deps in package: install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [], - ], -] -`; - -exports[`handles "frozen-lockfile": install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [ - "--frozen-lockfile", - ], - ], -] -`; - -exports[`handles dependencies of dependencies: install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [], - ], -] -`; diff --git a/packages/kbn-pm/src/commands/bootstrap.test.ts b/packages/kbn-pm/src/commands/bootstrap.test.ts deleted file mode 100644 index dbd5278d283b6..0000000000000 --- a/packages/kbn-pm/src/commands/bootstrap.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -jest.mock('../utils/scripts'); -jest.mock('../utils/link_project_executables'); -jest.mock('../utils/validate_dependencies'); - -import { resolve } from 'path'; - -import { ToolingLogCollectingWriter } from '@kbn/dev-utils/tooling_log'; - -import { absolutePathSnapshotSerializer, stripAnsiSnapshotSerializer } from '../test_helpers'; -import { linkProjectExecutables } from '../utils/link_project_executables'; -import { IPackageJson } from '../utils/package_json'; -import { Project } from '../utils/project'; -import { buildProjectGraph } from '../utils/projects'; -import { installInDir, runScriptInPackageStreaming } from '../utils/scripts'; -import { BootstrapCommand } from './bootstrap'; -import { Kibana } from '../utils/kibana'; -import { log } from '../utils/log'; - -const mockInstallInDir = installInDir as jest.Mock; -const mockRunScriptInPackageStreaming = runScriptInPackageStreaming as jest.Mock; -const mockLinkProjectExecutables = linkProjectExecutables as jest.Mock; - -const logWriter = new ToolingLogCollectingWriter('debug'); -log.setLogLevel('silent'); -log.setWriters([logWriter]); -beforeEach(() => { - logWriter.messages.length = 0; -}); - -const createProject = (packageJson: IPackageJson, path = '.') => { - const project = new Project( - { - name: 'kibana', - version: '1.0.0', - ...packageJson, - }, - resolve(__dirname, path) - ); - - return project; -}; -expect.addSnapshotSerializer(absolutePathSnapshotSerializer); -expect.addSnapshotSerializer(stripAnsiSnapshotSerializer); - -afterEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); -}); - -test('handles dependencies of dependencies', async () => { - const kibana = createProject({ - dependencies: { - bar: 'link:packages/bar', - }, - }); - const foo = createProject( - { - dependencies: { - bar: 'link:../bar', - }, - name: 'foo', - }, - 'packages/foo' - ); - const bar = createProject( - { - dependencies: { - baz: 'link:../baz', - }, - name: 'bar', - }, - 'packages/bar' - ); - const baz = createProject( - { - name: 'baz', - }, - 'packages/baz' - ); - - const projects = new Map([ - ['kibana', kibana], - ['foo', foo], - ['bar', bar], - ['baz', baz], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); - expect(logWriter.messages).toMatchInlineSnapshot(` - Array [ - info [kibana] running yarn, - "", - "", - ] - `); -}); - -test('does not run installer if no deps in package', async () => { - const kibana = createProject({ - dependencies: { - bar: 'link:packages/bar', - }, - }); - // bar has no dependencies - const bar = createProject( - { - name: 'bar', - }, - 'packages/bar' - ); - - const projects = new Map([ - ['kibana', kibana], - ['bar', bar], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); - expect(logWriter.messages).toMatchInlineSnapshot(` - Array [ - info [kibana] running yarn, - "", - "", - ] - `); -}); - -test('handles "frozen-lockfile"', async () => { - const kibana = createProject({ - dependencies: { - foo: '2.2.0', - }, - }); - - const projects = new Map([['kibana', kibana]]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: { - 'frozen-lockfile': true, - }, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); -}); - -test('calls "kbn:bootstrap" scripts and links executables after installing deps', async () => { - const kibana = createProject({ - dependencies: { - bar: 'link:packages/bar', - }, - }); - const bar = createProject( - { - name: 'bar', - scripts: { - 'kbn:bootstrap': 'node ./bar.js', - }, - }, - 'packages/bar' - ); - - const projects = new Map([ - ['kibana', kibana], - ['bar', bar], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockLinkProjectExecutables.mock.calls).toMatchSnapshot('link bins'); - expect(mockRunScriptInPackageStreaming.mock.calls).toMatchSnapshot('script'); -}); diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile index c47edfb9cf63d..d17b597eb6648 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile @@ -52,8 +52,10 @@ EXPOSE 5601 {{/ubi}} RUN for iter in {1..10}; do \ - # update microdnf to have exclusion feature for dnf configuration - {{packageManager}} update microdnf --setopt=tsflags=nodocs -y && \ + {{#ubi}} + # update microdnf to have exclusion feature for dnf configuration + {{packageManager}} update microdnf --setopt=tsflags=nodocs -y && \ + {{/ubi}} {{packageManager}} update --setopt=tsflags=nodocs -y && \ {{packageManager}} install --setopt=tsflags=nodocs -y \ fontconfig freetype shadow-utils libnss3.so {{#ubi}}findutils{{/ubi}} && \ diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 284a6b4b696ae..aae9b89cdc61f 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -215,13 +215,13 @@ export class IndexPatternsService { * Get field list by providing { pattern } * @param options */ - getFieldsForWildcard = async (options: GetFieldsOptions) => { + getFieldsForWildcard = async (options: GetFieldsOptions = {}) => { const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); return this.apiClient.getFieldsForWildcard({ pattern: options.pattern, metaFields, type: options.type, - rollupIndex: options.rollupIndex, + params: options.params || {}, }); }; @@ -231,13 +231,13 @@ export class IndexPatternsService { */ getFieldsForIndexPattern = async ( indexPattern: IndexPattern | IndexPatternSpec, - options?: GetFieldsOptions + options: GetFieldsOptions = {} ) => this.getFieldsForWildcard({ - type: indexPattern.type, - rollupIndex: indexPattern?.typeMeta?.params?.rollup_index, - ...options, pattern: indexPattern.title as string, + ...options, + type: indexPattern.type, + params: indexPattern.typeMeta && indexPattern.typeMeta.params, }); /** @@ -374,10 +374,10 @@ export class IndexPatternsService { try { spec.fields = isFieldRefreshRequired ? await this.refreshFieldSpecMap(spec.fields || {}, id, spec.title as string, { - pattern: title as string, + pattern: title, metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), type, - rollupIndex: typeMeta?.params?.rollupIndex, + params: typeMeta && typeMeta.params, }) : spec.fields; } catch (err) { diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index b381cc0963333..3387bc3b3c19e 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -86,22 +86,15 @@ export interface SavedObjectsClientCommon { } export interface GetFieldsOptions { - pattern: string; + pattern?: string; type?: string; + params?: any; lookBack?: boolean; metaFields?: string[]; - rollupIndex?: string; -} - -export interface GetFieldsOptionsTimePattern { - pattern: string; - metaFields: string[]; - lookBack: number; - interval: string; } export interface IIndexPatternsApiClient { - getFieldsForTimePattern: (options: GetFieldsOptionsTimePattern) => Promise; + getFieldsForTimePattern: (options: GetFieldsOptions) => Promise; getFieldsForWildcard: (options: GetFieldsOptions) => Promise; } 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/index_patterns/index_patterns/index_patterns_api_client.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts index 8c48ee44fba9c..37ee80c2c29e4 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts @@ -32,12 +32,7 @@ describe('IndexPatternsApiClient', () => { test('uses the right URI to fetch fields for time patterns', async function () { const expectedPath = '/api/index_patterns/_fields_for_time_pattern'; - await indexPatternsApiClient.getFieldsForTimePattern({ - pattern: 'blah', - metaFields: [], - lookBack: 5, - interval: '', - }); + await indexPatternsApiClient.getFieldsForTimePattern(); expect(fetchSpy).toHaveBeenCalledWith(expectedPath, expect.any(Object)); }); @@ -45,7 +40,15 @@ describe('IndexPatternsApiClient', () => { test('uses the right URI to fetch fields for wildcard', async function () { const expectedPath = '/api/index_patterns/_fields_for_wildcard'; - await indexPatternsApiClient.getFieldsForWildcard({ pattern: 'blah' }); + await indexPatternsApiClient.getFieldsForWildcard(); + + expect(fetchSpy).toHaveBeenCalledWith(expectedPath, expect.any(Object)); + }); + + test('uses the right URI to fetch fields for wildcard given a type', async function () { + const expectedPath = '/api/index_patterns/rollup/_fields_for_wildcard'; + + await indexPatternsApiClient.getFieldsForWildcard({ type: 'rollup' }); expect(fetchSpy).toHaveBeenCalledWith(expectedPath, expect.any(Object)); }); diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts index ca0f35d6612b2..377a3f7f91a50 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts @@ -19,11 +19,7 @@ import { HttpSetup } from 'src/core/public'; import { IndexPatternMissingIndices } from '../../../common/index_patterns/lib'; -import { - GetFieldsOptions, - IIndexPatternsApiClient, - GetFieldsOptionsTimePattern, -} from '../../../common/index_patterns/types'; +import { GetFieldsOptions, IIndexPatternsApiClient } from '../../../common/index_patterns/types'; const API_BASE_URL: string = `/api/index_patterns/`; @@ -52,7 +48,7 @@ export class IndexPatternsApiClient implements IIndexPatternsApiClient { return API_BASE_URL + path.filter(Boolean).map(encodeURIComponent).join('/'); } - getFieldsForTimePattern(options: GetFieldsOptionsTimePattern) { + getFieldsForTimePattern(options: GetFieldsOptions = {}) { const { pattern, lookBack, metaFields } = options; const url = this._getUrl(['_fields_for_time_pattern']); @@ -64,12 +60,27 @@ export class IndexPatternsApiClient implements IIndexPatternsApiClient { }).then((resp: any) => resp.fields); } - getFieldsForWildcard({ pattern, metaFields, type, rollupIndex }: GetFieldsOptions) { - return this._request(this._getUrl(['_fields_for_wildcard']), { - pattern, - meta_fields: metaFields, - type, - rollup_index: rollupIndex, - }).then((resp: any) => resp.fields); + getFieldsForWildcard(options: GetFieldsOptions = {}) { + const { pattern, metaFields, type, params } = options; + + let url; + let query; + + if (type) { + url = this._getUrl([type, '_fields_for_wildcard']); + query = { + pattern, + meta_fields: metaFields, + params: JSON.stringify(params), + }; + } else { + url = this._getUrl(['_fields_for_wildcard']); + query = { + pattern, + meta_fields: metaFields, + }; + } + + return this._request(url, query).then((resp: any) => resp.fields); } } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index f911378ce97b2..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; } @@ -1339,9 +1338,9 @@ export class IndexPatternsService { // (undocumented) getCache: () => Promise[] | null | undefined>; getDefault: () => Promise; - getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; + getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise; // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts - getFieldsForWildcard: (options: GetFieldsOptions) => Promise; + getFieldsForWildcard: (options?: GetFieldsOptions) => Promise; getIds: (refresh?: boolean) => Promise; getIdsWithTitle: (refresh?: boolean) => Promise | 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 c85ef536dbdd4..9a9b8b67730cc 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -134,8 +134,6 @@ export { FieldDescriptor as IndexPatternFieldDescriptor, shouldReadFieldFromDocValues, // used only in logstash_fields fixture FieldDescriptor, - mergeCapabilitiesWithFields, - getCapabilitiesForRollupIndices, } from './index_patterns'; export { @@ -223,6 +221,7 @@ export { ISearchStrategy, ISearchSetup, ISearchStart, + SearchStrategyDependencies, getDefaultSearchParams, getShardTimeout, shimHitsTotal, diff --git a/src/plugins/data/server/index_patterns/fetcher/index.ts b/src/plugins/data/server/index_patterns/fetcher/index.ts index 5d8ef62df9627..19306696885db 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index.ts @@ -18,8 +18,4 @@ */ export * from './index_patterns_fetcher'; -export { - shouldReadFieldFromDocValues, - mergeCapabilitiesWithFields, - getCapabilitiesForRollupIndices, -} from './lib'; +export { shouldReadFieldFromDocValues } from './lib'; diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts index 24dad39088b8f..e75b8761984ec 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts @@ -18,15 +18,8 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { keyBy } from 'lodash'; -import { - getFieldCapabilities, - resolveTimePattern, - createNoMatchingIndicesError, - getCapabilitiesForRollupIndices, - mergeCapabilitiesWithFields, -} from './lib'; +import { getFieldCapabilities, resolveTimePattern, createNoMatchingIndicesError } from './lib'; export interface FieldDescriptor { aggregatable: boolean; @@ -65,44 +58,11 @@ export class IndexPatternsFetcher { pattern: string | string[]; metaFields?: string[]; fieldCapsOptions?: { allow_no_indices: boolean }; - type?: string; - rollupIndex?: string; }): Promise { - const { pattern, metaFields, fieldCapsOptions, type, rollupIndex } = options; - const fieldCapsResponse = await getFieldCapabilities( - this.elasticsearchClient, - pattern, - metaFields, - { - allow_no_indices: fieldCapsOptions - ? fieldCapsOptions.allow_no_indices - : this.allowNoIndices, - } - ); - if (type === 'rollup' && rollupIndex) { - const rollupFields: FieldDescriptor[] = []; - const rollupIndexCapabilities = getCapabilitiesForRollupIndices( - ( - await this.elasticsearchClient.rollup.getRollupIndexCaps({ - index: rollupIndex, - }) - ).body - )[rollupIndex].aggs; - const fieldCapsResponseObj = keyBy(fieldCapsResponse, 'name'); - - // Keep meta fields - metaFields!.forEach( - (field: string) => - fieldCapsResponseObj[field] && rollupFields.push(fieldCapsResponseObj[field]) - ); - - return mergeCapabilitiesWithFields( - rollupIndexCapabilities, - fieldCapsResponseObj, - rollupFields - ); - } - return fieldCapsResponse; + const { pattern, metaFields, fieldCapsOptions } = options; + return await getFieldCapabilities(this.elasticsearchClient, pattern, metaFields, { + allow_no_indices: fieldCapsOptions ? fieldCapsOptions.allow_no_indices : this.allowNoIndices, + }); } /** diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/index.js b/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/index.js deleted file mode 100644 index d675702ae54e9..0000000000000 --- a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { jobs } from './jobs'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/index.ts b/src/plugins/data/server/index_patterns/fetcher/lib/index.ts index b2fd3a1a09a25..20e74d2b1a579 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/index.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/index.ts @@ -20,5 +20,3 @@ export { getFieldCapabilities, shouldReadFieldFromDocValues } from './field_capabilities'; export { resolveTimePattern } from './resolve_time_pattern'; export { createNoMatchingIndicesError } from './errors'; -export * from './merge_capabilities_with_fields'; -export * from './map_capabilities'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/map_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/map_capabilities.ts deleted file mode 100644 index 6187174834012..0000000000000 --- a/src/plugins/data/server/index_patterns/fetcher/lib/map_capabilities.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { mergeJobConfigurations } from './jobs_compatibility'; - -export function getCapabilitiesForRollupIndices(indices: { [key: string]: any }) { - const indexNames = Object.keys(indices); - const capabilities = {} as { [key: string]: any }; - - indexNames.forEach((index) => { - try { - capabilities[index] = mergeJobConfigurations(indices[index].rollup_jobs); - } catch (e) { - capabilities[index] = { - error: e.message, - }; - } - }); - - return capabilities; -} diff --git a/src/plugins/data/server/index_patterns/index.ts b/src/plugins/data/server/index_patterns/index.ts index 3305b1bb9a92f..683d1c445fd72 100644 --- a/src/plugins/data/server/index_patterns/index.ts +++ b/src/plugins/data/server/index_patterns/index.ts @@ -17,11 +17,5 @@ * under the License. */ export * from './utils'; -export { - IndexPatternsFetcher, - FieldDescriptor, - shouldReadFieldFromDocValues, - mergeCapabilitiesWithFields, - getCapabilitiesForRollupIndices, -} from './fetcher'; +export { IndexPatternsFetcher, FieldDescriptor, shouldReadFieldFromDocValues } from './fetcher'; export { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns_service'; diff --git a/src/plugins/data/server/index_patterns/index_patterns_api_client.ts b/src/plugins/data/server/index_patterns/index_patterns_api_client.ts index 21a3bf6e73e61..2dc6f40c5a6f1 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_api_client.ts @@ -17,30 +17,13 @@ * under the License. */ -import { ElasticsearchClient } from 'kibana/server'; -import { - GetFieldsOptions, - IIndexPatternsApiClient, - GetFieldsOptionsTimePattern, -} from '../../common/index_patterns/types'; -import { IndexPatternsFetcher } from './fetcher'; +import { GetFieldsOptions, IIndexPatternsApiClient } from '../../common/index_patterns/types'; export class IndexPatternsApiServer implements IIndexPatternsApiClient { - esClient: ElasticsearchClient; - constructor(elasticsearchClient: ElasticsearchClient) { - this.esClient = elasticsearchClient; + async getFieldsForTimePattern(options: GetFieldsOptions = {}) { + throw new Error('IndexPatternsApiServer - getFieldsForTimePattern not defined'); } - async getFieldsForWildcard({ pattern, metaFields, type, rollupIndex }: GetFieldsOptions) { - const indexPatterns = new IndexPatternsFetcher(this.esClient); - return await indexPatterns.getFieldsForWildcard({ - pattern, - metaFields, - type, - rollupIndex, - }); - } - async getFieldsForTimePattern(options: GetFieldsOptionsTimePattern) { - const indexPatterns = new IndexPatternsFetcher(this.esClient); - return await indexPatterns.getFieldsForTimePattern(options); + async getFieldsForWildcard(options: GetFieldsOptions = {}) { + throw new Error('IndexPatternsApiServer - getFieldsForWildcard not defined'); } } diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index af2d4d6a73e0f..d665e3715fa72 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -17,14 +17,7 @@ * under the License. */ -import { - CoreSetup, - CoreStart, - Plugin, - Logger, - SavedObjectsClientContract, - ElasticsearchClient, -} from 'kibana/server'; +import { CoreSetup, CoreStart, Plugin, Logger, SavedObjectsClientContract } from 'kibana/server'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; @@ -36,8 +29,7 @@ import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper export interface IndexPatternsServiceStart { indexPatternsServiceFactory: ( - savedObjectsClient: SavedObjectsClientContract, - elasticsearchClient: ElasticsearchClient + savedObjectsClient: SavedObjectsClientContract ) => Promise; } @@ -58,17 +50,14 @@ export class IndexPatternsService implements Plugin { + indexPatternsServiceFactory: async (savedObjectsClient: SavedObjectsClientContract) => { const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); return new IndexPatternsCommonService({ uiSettings: new UiSettingsServerToCommon(uiSettingsClient), savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), - apiClient: new IndexPatternsApiServer(elasticsearchClient), + apiClient: new IndexPatternsApiServer(), fieldFormats: formats, onError: (error) => { logger.error(error); diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index f8af52954fc61..041eb235d01e0 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -42,15 +42,13 @@ export function registerRoutes(http: HttpServiceSetup) { meta_fields: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { defaultValue: [], }), - type: schema.maybe(schema.string()), - rollup_index: schema.maybe(schema.string()), }), }, }, async (context, request, response) => { const { asCurrentUser } = context.core.elasticsearch.client; const indexPatterns = new IndexPatternsFetcher(asCurrentUser); - const { pattern, meta_fields: metaFields, type, rollup_index: rollupIndex } = request.query; + const { pattern, meta_fields: metaFields } = request.query; let parsedFields: string[] = []; try { @@ -63,8 +61,6 @@ export function registerRoutes(http: HttpServiceSetup) { const fields = await indexPatterns.getFieldsForWildcard({ pattern, metaFields: parsedFields, - type, - rollupIndex, }); return response.ok({ diff --git a/src/plugins/data/server/search/aggs/aggs_service.test.ts b/src/plugins/data/server/search/aggs/aggs_service.test.ts index e58420f6c2f07..cb4239cc339c4 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { KibanaRequest, ElasticsearchClient } from 'src/core/server'; +import { KibanaRequest } from 'src/core/server'; import { coreMock } from '../../../../../core/server/mocks'; import { expressionsPluginMock } from '../../../../../plugins/expressions/server/mocks'; @@ -63,8 +63,7 @@ describe('AggsService - server', () => { expect(start).toHaveProperty('asScopedToClient'); const contract = await start.asScopedToClient( - savedObjects.getScopedClient({} as KibanaRequest), - {} as ElasticsearchClient + savedObjects.getScopedClient({} as KibanaRequest) ); expect(contract).toHaveProperty('calculateAutoTimeExpression'); expect(contract).toHaveProperty('createAggConfigs'); @@ -75,10 +74,7 @@ describe('AggsService - server', () => { service.setup(setupDeps); const start = await service .start(startDeps) - .asScopedToClient( - savedObjects.getScopedClient({} as KibanaRequest), - {} as ElasticsearchClient - ); + .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); expect(start.types.get('terms').name).toBe('terms'); }); @@ -87,10 +83,7 @@ describe('AggsService - server', () => { service.setup(setupDeps); const start = await service .start(startDeps) - .asScopedToClient( - savedObjects.getScopedClient({} as KibanaRequest), - {} as ElasticsearchClient - ); + .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); const aggTypes = getAggTypes(); expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length); @@ -110,10 +103,7 @@ describe('AggsService - server', () => { const start = await service .start(startDeps) - .asScopedToClient( - savedObjects.getScopedClient({} as KibanaRequest), - {} as ElasticsearchClient - ); + .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); const aggTypes = getAggTypes(); expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length + 1); diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts index c23f748b1eeb5..c805c8af6694c 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -19,11 +19,7 @@ import { pick } from 'lodash'; -import { - UiSettingsServiceStart, - SavedObjectsClientContract, - ElasticsearchClient, -} from 'src/core/server'; +import { UiSettingsServiceStart, SavedObjectsClientContract } from 'src/core/server'; import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; import { AggsCommonService, @@ -69,10 +65,7 @@ export class AggsService { public start({ fieldFormats, uiSettings, indexPatterns }: AggsStartDependencies): AggsStart { return { - asScopedToClient: async ( - savedObjectsClient: SavedObjectsClientContract, - elasticsearchClient: ElasticsearchClient - ) => { + asScopedToClient: async (savedObjectsClient: SavedObjectsClientContract) => { const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); @@ -89,9 +82,8 @@ export class AggsService { types, } = this.aggsCommonService.start({ getConfig, - getIndexPattern: ( - await indexPatterns.indexPatternsServiceFactory(savedObjectsClient, elasticsearchClient) - ).get, + getIndexPattern: (await indexPatterns.indexPatternsServiceFactory(savedObjectsClient)) + .get, isDefaultTimezone, }); diff --git a/src/plugins/data/server/search/aggs/types.ts b/src/plugins/data/server/search/aggs/types.ts index 2c28c970cbb84..1b21d948b25d9 100644 --- a/src/plugins/data/server/search/aggs/types.ts +++ b/src/plugins/data/server/search/aggs/types.ts @@ -17,14 +17,11 @@ * under the License. */ -import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { AggsCommonSetup, AggsStart as Start } from '../../../common'; export type AggsSetup = AggsCommonSetup; export interface AggsStart { - asScopedToClient: ( - savedObjectsClient: SavedObjectsClientContract, - elasticsearchClient: ElasticsearchClient - ) => Promise; + asScopedToClient: (savedObjectsClient: SavedObjectsClientContract) => Promise; } 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 6394c37c993b3..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,25 +161,23 @@ 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, - }), + 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); const savedObjectsClient = savedObjects.getScopedClient(request); const scopedIndexPatterns = await indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - esClient.asCurrentUser + savedObjectsClient ); const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); @@ -174,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: { @@ -246,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]; @@ -268,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 06c8a053e5ad2..f62a70c9e4ce1 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -15,16 +15,17 @@ import { CoreStart } from 'src/core/server'; import { CoreStart as CoreStart_2 } from 'kibana/server'; import { DatatableColumn } from 'src/plugins/expressions'; import { Duration } from 'moment'; -import { ElasticsearchClient } from 'src/core/server'; -import { ElasticsearchClient as ElasticsearchClient_2 } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ISavedObjectsRepository } from 'kibana/server'; +import { 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'; @@ -42,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'; @@ -361,20 +361,11 @@ export type Filter = { query?: any; }; -// Warning: (ae-missing-release-tag) "getCapabilitiesForRollupIndices" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function getCapabilitiesForRollupIndices(indices: { - [key: string]: any; -}): { - [key: string]: any; -}; - // Warning: (ae-forgotten-export) The symbol "IUiSettingsClient" needs to be exported by the entry point index.d.ts // 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; @@ -669,7 +660,7 @@ export const indexPatterns: { // // @public (undocumented) export class IndexPatternsFetcher { - constructor(elasticsearchClient: ElasticsearchClient_2, allowNoIndices?: boolean); + constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices?: boolean); getFieldsForTimePattern(options: { pattern: string; metaFields: string[]; @@ -682,8 +673,6 @@ export class IndexPatternsFetcher { fieldCapsOptions?: { allow_no_indices: boolean; }; - type?: string; - rollupIndex?: string; }): Promise; } @@ -698,7 +687,7 @@ export class IndexPatternsService implements Plugin_3 Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract_2) => Promise; }; } @@ -735,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; @@ -749,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) @@ -800,15 +791,6 @@ export interface KueryNode { type: keyof NodeTypes; } -// Warning: (ae-missing-release-tag) "mergeCapabilitiesWithFields" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const mergeCapabilitiesWithFields: (rollupIndexCapabilities: { - [key: string]: any; -}, fieldsFromFieldCapsApi: { - [key: string]: any; -}, previousFields?: FieldDescriptor[]) => FieldDescriptor[]; - // Warning: (ae-missing-release-tag) "METRIC_TYPES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -909,10 +891,10 @@ export class Plugin implements Plugin_2 Promise; + fieldFormatServiceFactory: (uiSettings: import("src/core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }; @@ -981,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; @@ -1028,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) @@ -1168,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:236:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:236:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:236:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:236:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:5 - (ae-forgotten-export) The symbol "getTotalLoaded" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:252:5 - (ae-forgotten-export) The symbol "toSnakeCase" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:273:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:277:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:280: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:58:14 - (ae-forgotten-export) The symbol "IndexPatternsService" 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/kibana_react/public/code_editor/editor_theme.ts b/src/plugins/kibana_react/public/code_editor/editor_theme.ts index 91d66ce8cbf81..57d1d715151b7 100644 --- a/src/plugins/kibana_react/public/code_editor/editor_theme.ts +++ b/src/plugins/kibana_react/public/code_editor/editor_theme.ts @@ -35,7 +35,7 @@ export function createTheme( { token: '', foreground: euiTheme.euiColorDarkestShade, - background: euiTheme.euiColorEmptyShade, + background: euiTheme.euiFormBackgroundColor, }, { token: 'invalid', foreground: euiTheme.euiColorAccent }, { token: 'emphasis', fontStyle: 'italic' }, @@ -94,7 +94,7 @@ export function createTheme( ], colors: { 'editor.foreground': euiTheme.euiColorDarkestShade, - 'editor.background': euiTheme.euiColorEmptyShade, + 'editor.background': euiTheme.euiFormBackgroundColor, 'editorLineNumber.foreground': euiTheme.euiColorDarkShade, 'editorLineNumber.activeForeground': euiTheme.euiColorDarkShade, 'editorIndentGuide.background': euiTheme.euiColorLightShade, diff --git a/src/plugins/security_oss/tsconfig.json b/src/plugins/security_oss/tsconfig.json new file mode 100644 index 0000000000000..d211a70f12df3 --- /dev/null +++ b/src/plugins/security_oss/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*", "server/**/*"], + "references": [{ "path": "../../core/tsconfig.json" }] +} diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 5a853972d34a8..2ac3de510f8ae 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -8,7 +8,13 @@ To integrate with the telemetry services for usage collection of your feature, t ## Creating and Registering Usage Collector -All you need to provide is a `type` for organizing your fields, `schema` field to define the expected types of usage fields reported, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it. +Your usage collector needs to provide +- a `type` for organizing your fields, +- `schema` field to define the expected types of usage fields reported, +- a `fetch` method for returning your usage data, and +- an `isReady` method (that returns true or false) for letting the telemetry service know if it needs to wait for any asynchronous action (initialization of clients or other services) before calling the `fetch` method. + +Then you need to make the Telemetry service aware of the collector by registering it. 1. Make sure `usageCollection` is in your optional Plugins: @@ -62,6 +68,8 @@ All you need to provide is a `type` for organizing your fields, `schema` field t total: 'long', }, }, + isReady: () => isCollectorFetchReady, // Method to return `true`/`false` or Promise(`true`/`false`) to confirm if the collector is ready for the `fetch` method to be called. + fetch: async (collectorFetchContext: CollectorFetchContext) => { // query ES or saved objects and get some data @@ -84,6 +92,9 @@ All you need to provide is a `type` for organizing your fields, `schema` field t Some background: - `MY_USAGE_TYPE` can be any string. It usually matches the plugin name. As a safety mechanism, we double check there are no duplicates at the moment of registering the collector. + +- `isReady` (added in v7.2.0 and v6.8.4) is a way for a usage collector to announce that some async process must finish first before it can return data in the `fetch` method (e.g. a client needs to ne initialized, or the task manager needs to run a task first). If any collector reports that it is not ready when we call its `fetch` method, we reset a flag to try again and, after a set amount of time, collect data from those collectors that are ready and skip any that are not. This means that if a collector returns `true` for `isReady` and it actually isn't ready to return data, there won't be telemetry data from that collector in that telemetry report (usually once per day). You should consider what it means if your collector doesn't return data in the first few documents when Kibana starts or, if we should wait for any other reason (e.g. the task manager needs to run your task first). If you need to tell telemetry collection to wait, you should implement this function with custom logic. If your `fetch` method can run without the need of any previous dependencies, then you can return true for `isReady` as shown in the example below. + - The `fetch` method needs to support multiple contexts in which it is called. For example, when stats are pulled from a Kibana Metricbeat module, the Beat calls Kibana's stats API to invoke usage collection. In this case, the `fetch` method is called as a result of an HTTP API request and `callCluster` wraps `callWithRequest` or `esClient` wraps `asCurrentUser`, where the request headers are expected to have read privilege on the entire `.kibana' index. The `fetch` method also exposes the saved objects client that will have the correct scope when the collectors' `fetch` method is called. @@ -154,7 +165,7 @@ If any of your properties is an array, the schema definition must follow the con ```ts export const myCollector = makeUsageCollector({ type: 'my_working_collector', - isReady: () => true, + isReady: () => true, // `fetch` doesn't require any validation for dependencies to be met fetch() { return { my_greeting: 'hello', diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 73febc0183fc5..c04b087d4adf5 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -72,7 +72,7 @@ export interface CollectorOptions { type: string; init?: Function; /** - * Method to return `true`/`false` to confirm if the collector is ready for the `fetch` method to be called. + * Method to return `true`/`false` or Promise(`true`/`false`) to confirm if the collector is ready for the `fetch` method to be called. */ isReady: () => Promise | boolean; /** @@ -101,6 +101,7 @@ export class Collector { * @param {Function} options.init (optional) - initialization function * @param {Function} options.fetch - function to query data * @param {Function} options.formatForBulkUpload - optional + * @param {Function} options.isReady - method that returns a boolean or Promise of a boolean to indicate the collector is ready to report data * @param {Function} options.rest - optional other properties */ constructor( 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/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index dc49e280a2bb7..b52188129f77f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -62,12 +62,10 @@ export async function getFields( let indexPatternString = indexPattern; if (!indexPatternString) { - const [{ savedObjects, elasticsearch }, { data }] = await framework.core.getStartServices(); + const [{ savedObjects }, { data }] = await framework.core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(request); - const clusterClient = elasticsearch.client.asScoped(request).asCurrentUser; const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - clusterClient + savedObjectsClient ); const defaultIndexPattern = await indexPatternsService.getDefault(); indexPatternString = get(defaultIndexPattern, 'title', ''); 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/tasks/config/run.js b/tasks/config/run.js index 7814f4aa0e224..0a1bb9617e1f9 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -17,8 +17,6 @@ * under the License. */ -import { getFunctionalTestGroupRunConfigs } from '../function_test_groups'; - const { version } = require('../../package.json'); const KIBANA_INSTALL_DIR = process.env.KIBANA_INSTALL_DIR || @@ -239,9 +237,5 @@ module.exports = function () { 'test:jest_integration' ), test_projects: gruntTaskWithGithubChecks('Project tests', 'test:projects'), - - ...getFunctionalTestGroupRunConfigs({ - kibanaInstallDir: KIBANA_INSTALL_DIR, - }), }; }; diff --git a/tasks/function_test_groups.js b/tasks/function_test_groups.js index 7dafc03cfab03..0b456dcb0da13 100644 --- a/tasks/function_test_groups.js +++ b/tasks/function_test_groups.js @@ -29,44 +29,6 @@ const TEST_TAGS = safeLoad(JOBS_YAML) .JOB.filter((id) => id.startsWith('kibana-ciGroup')) .map((id) => id.replace(/^kibana-/, '')); -const getDefaultArgs = (tag) => { - return [ - 'scripts/functional_tests', - '--include-tag', - tag, - '--config', - 'test/functional/config.js', - '--config', - 'test/ui_capabilities/newsfeed_err/config.ts', - // '--config', 'test/functional/config.firefox.js', - '--bail', - '--debug', - '--config', - 'test/new_visualize_flow/config.ts', - '--config', - 'test/security_functional/config.ts', - ]; -}; - -export function getFunctionalTestGroupRunConfigs({ kibanaInstallDir } = {}) { - return { - // include a run task for each test group - ...TEST_TAGS.reduce( - (acc, tag) => ({ - ...acc, - [`functionalTests_${tag}`]: { - cmd: process.execPath, - args: [ - ...getDefaultArgs(tag), - ...(!!process.env.CODE_COVERAGE ? [] : ['--kibana-install-dir', kibanaInstallDir]), - ], - }, - }), - {} - ), - }; -} - grunt.registerTask( 'functionalTests:ensureAllTestsInCiGroup', 'Check that all of the functional tests are in a CI group', diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index ca3e26be5d74b..e29f75b806574 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -350,7 +350,6 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider if (timefield) { await this.selectTimeFieldOption(timefield); } - await new Promise((r) => setTimeout(r, 5000 * 60)); await (await this.getCreateIndexPatternButton()).click(); }); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/plugin_functional/plugins/data_search/server/plugin.ts b/test/plugin_functional/plugins/data_search/server/plugin.ts index ca22e82188403..e016ef56802f3 100644 --- a/test/plugin_functional/plugins/data_search/server/plugin.ts +++ b/test/plugin_functional/plugins/data_search/server/plugin.ts @@ -58,16 +58,14 @@ export class DataSearchTestPlugin }, }, async (context, req, res) => { - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const service = await data.search.searchSource.asScoped(req); - const clusterClient = elasticsearch.client.asScoped(req).asCurrentUser; const savedObjectsClient = savedObjects.getScopedClient(req); // Since the index pattern ID can change on each test run, we need // to look it up on the fly and insert it into the request. const indexPatterns = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - clusterClient + savedObjectsClient ); const ids = await indexPatterns.getIds(); // @ts-expect-error Force overwriting the request diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts index 7dc5e975c528e..a54502b740211 100644 --- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -36,34 +36,12 @@ export class IndexPatternsTestPlugin public setup(core: CoreSetup) { const router = core.http.createRouter(); - router.post( - { - path: '/api/index-patterns-plugin/create', - validate: { - body: schema.object({}, { unknowns: 'allow' }), - }, - }, - async (context, req, res) => { - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); - const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - elasticsearch.client.asScoped(req).asCurrentUser - ); - const ids = await service.createAndSave(req.body); - return res.ok({ body: ids }); - } - ); - router.get( { path: '/api/index-patterns-plugin/get-all', validate: false }, async (context, req, res) => { - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - elasticsearch.client.asScoped(req).asCurrentUser - ); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ids = await service.getIds(); return res.ok({ body: ids }); } @@ -80,12 +58,9 @@ export class IndexPatternsTestPlugin }, async (context, req, res) => { const id = (req.params as Record).id; - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - elasticsearch.client.asScoped(req).asCurrentUser - ); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ip = await service.get(id); return res.ok({ body: ip.toSpec() }); } @@ -101,13 +76,10 @@ export class IndexPatternsTestPlugin }, }, async (context, req, res) => { - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - elasticsearch.client.asScoped(req).asCurrentUser - ); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ip = await service.get(id); await service.updateSavedObject(ip); return res.ok(); @@ -124,13 +96,10 @@ export class IndexPatternsTestPlugin }, }, async (context, req, res) => { - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - elasticsearch.client.asScoped(req).asCurrentUser - ); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); await service.delete(id); return res.ok(); } diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 6d8f65fa38777..2c846dc780311 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -25,30 +25,13 @@ export default function ({ getService }: PluginFunctionalProviderContext) { // skipping the tests as it deletes index patterns created by other test causing unexpected failures // https://github.com/elastic/kibana/issues/79886 - describe('index patterns', function () { + describe.skip('index patterns', function () { let indexPatternId = ''; - it('can create an index pattern', async () => { - const title = 'shakes*'; - const fieldFormats = { bytes: { id: 'bytes' } }; - const body = await ( - await supertest - .post('/api/index-patterns-plugin/create') - .set('kbn-xsrf', 'anything') - .send({ title, fieldFormats }) - .expect(200) - ).body; - - indexPatternId = body.id; - expect(body.id).not.empty(); - expect(body.title).to.equal(title); - expect(body.fields.length).to.equal(15); - expect(body.fieldFormatMap).to.eql(fieldFormats); - }); - it('can get all ids', async () => { const body = await (await supertest.get('/api/index-patterns-plugin/get-all').expect(200)) .body; + indexPatternId = body[0]; expect(body.length > 0).to.equal(true); }); diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 2542d7032e83b..f9e9d40cd8b0d 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -3,7 +3,13 @@ source test/scripts/jenkins_test_setup_oss.sh if [[ -z "$CODE_COVERAGE" ]]; then - checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; + echo " -> Running functional and api tests" + + checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --include-tag "ciGroup$CI_GROUP" if [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then source test/scripts/jenkins_build_kbn_sample_panel_action.sh @@ -21,7 +27,6 @@ else cd "kibana${CI_GROUP}" echo " -> running tests from the clone folder" - #yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --exclude-tag "skipCoverage" || true; if [[ -d target/kibana-coverage/functional ]]; then diff --git a/tsconfig.json b/tsconfig.json index 3554027d4e320..00b33bd0b4451 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "src/plugins/kibana_usage_collection/**/*", "src/plugins/kibana_utils/**/*", "src/plugins/newsfeed/**/*", + "src/plugins/security_oss/**/*", "src/plugins/share/**/*", "src/plugins/telemetry/**/*", "src/plugins/telemetry_collection_manager/**/*", @@ -34,6 +35,7 @@ { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 47655da68f2a5..55d63f516b998 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -9,6 +9,7 @@ { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 28eb94405abbb..521637a1b0f7f 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -109,16 +109,16 @@ def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { def functionalTestProcess(String name, Closure closure) { return { - withFunctionalTestEnv(["JOB=${name}"], closure) + notifyOnError { + withFunctionalTestEnv(["JOB=${name}"], closure) + } } } def functionalTestProcess(String name, String script) { return functionalTestProcess(name) { - notifyOnError { - retryable(name) { - runbld(script, "Execute ${name}") - } + retryable(name) { + runbld(script, "Execute ${name}") } } } diff --git a/x-pack/plugins/canvas/public/components/expression/expression.scss b/x-pack/plugins/canvas/public/components/expression/expression.scss index 1635446a74012..da95eca2b4f61 100644 --- a/x-pack/plugins/canvas/public/components/expression/expression.scss +++ b/x-pack/plugins/canvas/public/components/expression/expression.scss @@ -45,4 +45,5 @@ .canvasExpression__settings { padding: $euiSizeM $euiSize; border-top: $euiBorderThin; + background-color: $euiColorEmptyShade; } diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss index 41bc718dcfec1..c46a2ec7a1e22 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss @@ -6,7 +6,7 @@ position: absolute; top: $euiSize * -1.25; left: 50%; - background-color: $euiColorLightestShade; + background-color: $euiFormBackgroundColor; margin: 0; border-radius: $euiBorderRadius $euiBorderRadius 0 0; diff --git a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.scss b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.scss index 34a9a12aac3e6..0ed47a761cd4f 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.scss +++ b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.scss @@ -4,11 +4,7 @@ } .canvasTray__panel { - background-color: $euiPageBackgroundColor; + background-color: $euiFormBackgroundColor; border-radius: 0; - - &.canvasTray__panel--holdingExpression { - background-color: $euiColorEmptyShade; - } } 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/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 5bd15ce411002..72703843f4bab 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -207,7 +207,7 @@ describe('callEnterpriseSearchConfigAPI', () => { callEnterpriseSearchConfigAPI(mockDependencies); jest.advanceTimersByTime(150); expect(mockDependencies.log.warn).toHaveBeenCalledWith( - 'Enterprise Search access check took over 100ms. Please ensure your Enterprise Search server is respondingly normally and not adversely impacting Kibana load speeds.' + 'Enterprise Search access check took over 100ms. Please ensure your Enterprise Search server is responding normally and not adversely impacting Kibana load speeds.' ); // Timeout diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index dcc696f6d01e2..325d7b0ce48f9 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -38,7 +38,7 @@ export const callEnterpriseSearchConfigAPI = async ({ }: IParams): Promise => { if (!config.host) return {}; - const TIMEOUT_WARNING = `Enterprise Search access check took over ${config.accessCheckTimeoutWarning}ms. Please ensure your Enterprise Search server is respondingly normally and not adversely impacting Kibana load speeds.`; + const TIMEOUT_WARNING = `Enterprise Search access check took over ${config.accessCheckTimeoutWarning}ms. Please ensure your Enterprise Search server is responding normally and not adversely impacting Kibana load speeds.`; const TIMEOUT_MESSAGE = `Exceeded ${config.accessCheckTimeout}ms timeout while checking ${config.host}. Please consider increasing your enterpriseSearch.accessCheckTimeout value so that users aren't prevented from accessing Enterprise Search plugins due to slow responses.`; const CONNECTION_ERROR = 'Could not perform access check to Enterprise Search'; 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 b5f5ad2530a12..095c0ac2b6ab1 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 @@ -45,6 +45,7 @@ describe('log settings routes', () => { }); it('creates a request to enterprise search', () => { + mockRouter.callRoute({}); expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/as/log_settings', }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 4bda56ca5087c..d82c7b092c38a 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -357,7 +357,7 @@ describe('Datatable Visualization', () => { datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ dataType: 'string', - isBucketed: true, + isBucketed: false, // <= make them metrics label: 'label', }); @@ -365,6 +365,7 @@ describe('Datatable Visualization', () => { { layers: [layer] }, frame.datasourceLayers ) as Ast; + const tableArgs = buildExpression(expression).findFunction('lens_datatable_columns'); expect(tableArgs).toHaveLength(1); @@ -372,5 +373,61 @@ describe('Datatable Visualization', () => { columnIds: ['c', 'b'], }); }); + + it('returns no expression if the metric dimension is not defined', () => { + const datasource = createMockDatasource('test'); + const layer = { layerId: 'a', columns: ['b', 'c'] }; + const frame = mockFrame(); + frame.datasourceLayers = { a: datasource.publicAPIMock }; + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); + datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + isBucketed: true, // move it from the metric to the break down by side + label: 'label', + }); + + const expression = datatableVisualization.toExpression( + { layers: [layer] }, + frame.datasourceLayers + ); + + expect(expression).toEqual(null); + }); + }); + + describe('#getErrorMessages', () => { + it('returns undefined if the datasource is missing a metric dimension', () => { + const datasource = createMockDatasource('test'); + const layer = { layerId: 'a', columns: ['b', 'c'] }; + const frame = mockFrame(); + frame.datasourceLayers = { a: datasource.publicAPIMock }; + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); + datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + isBucketed: true, // move it from the metric to the break down by side + label: 'label', + }); + + const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame); + + expect(error).not.toBeDefined(); + }); + + it('returns undefined if the metric dimension is defined', () => { + const datasource = createMockDatasource('test'); + const layer = { layerId: 'a', columns: ['b', 'c'] }; + const frame = mockFrame(); + frame.datasourceLayers = { a: datasource.publicAPIMock }; + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); + datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + isBucketed: false, // keep it a metric + label: 'label', + }); + + const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame); + + expect(error).not.toBeDefined(); + }); }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 1464ae6988a2d..e0f6ae31719ca 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -6,7 +6,13 @@ import { Ast } from '@kbn/interpreter/common'; import { i18n } from '@kbn/i18n'; -import { SuggestionRequest, Visualization, VisualizationSuggestion, Operation } from '../types'; +import { + SuggestionRequest, + Visualization, + VisualizationSuggestion, + Operation, + DatasourcePublicAPI, +} from '../types'; import { LensIconChartDatatable } from '../assets/chart_datatable'; export interface LayerState { @@ -128,16 +134,13 @@ export const datatableVisualization: Visualization }, getConfiguration({ state, frame, layerId }) { - const layer = state.layers.find((l) => l.layerId === layerId); - if (!layer) { + const { sortedColumns, datasource } = + getDataSourceAndSortedColumns(state, frame.datasourceLayers, layerId) || {}; + + if (!sortedColumns) { return { groups: [] }; } - const datasource = frame.datasourceLayers[layer.layerId]; - const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); - // When we add a column it could be empty, and therefore have no order - const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); - return { groups: [ { @@ -146,7 +149,9 @@ export const datatableVisualization: Visualization defaultMessage: 'Break down by', }), layerId: state.layers[0].layerId, - accessors: sortedColumns.filter((c) => datasource.getOperationForColumnId(c)?.isBucketed), + accessors: sortedColumns.filter( + (c) => datasource!.getOperationForColumnId(c)?.isBucketed + ), supportsMoreColumns: true, filterOperations: (op) => op.isBucketed, dataTestSubj: 'lnsDatatable_column', @@ -158,7 +163,7 @@ export const datatableVisualization: Visualization }), layerId: state.layers[0].layerId, accessors: sortedColumns.filter( - (c) => !datasource.getOperationForColumnId(c)?.isBucketed + (c) => !datasource!.getOperationForColumnId(c)?.isBucketed ), supportsMoreColumns: true, filterOperations: (op) => !op.isBucketed, @@ -194,14 +199,19 @@ export const datatableVisualization: Visualization }; }, - toExpression(state, datasourceLayers, { title, description } = {}): Ast { - const layer = state.layers[0]; - const datasource = datasourceLayers[layer.layerId]; - const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); - // When we add a column it could be empty, and therefore have no order - const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); - const operations = sortedColumns - .map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) })) + toExpression(state, datasourceLayers, { title, description } = {}): Ast | null { + const { sortedColumns, datasource } = + getDataSourceAndSortedColumns(state, datasourceLayers, state.layers[0].layerId) || {}; + + if ( + sortedColumns?.length && + sortedColumns.filter((c) => !datasource!.getOperationForColumnId(c)?.isBucketed).length === 0 + ) { + return null; + } + + const operations = sortedColumns! + .map((columnId) => ({ columnId, operation: datasource!.getOperationForColumnId(columnId) })) .filter((o): o is { columnId: string; operation: Operation } => !!o.operation); return { @@ -232,4 +242,24 @@ export const datatableVisualization: Visualization ], }; }, + + getErrorMessages(state, frame) { + return undefined; + }, }; + +function getDataSourceAndSortedColumns( + state: DatatableVisualizationState, + datasourceLayers: Record, + layerId: string +) { + const layer = state.layers.find((l: LayerState) => l.layerId === layerId); + if (!layer) { + return undefined; + } + const datasource = datasourceLayers[layer.layerId]; + const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); + // When we add a column it could be empty, and therefore have no order + const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); + return { datasource, sortedColumns }; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 8b0334ab98c14..28ad6c531e255 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -6,7 +6,7 @@ import { SavedObjectReference } from 'kibana/public'; import { Ast } from '@kbn/interpreter/common'; -import { Datasource, DatasourcePublicAPI, Visualization } from '../../types'; +import { Datasource, DatasourcePublicAPI, FramePublicAPI, Visualization } from '../../types'; import { buildExpression } from './expression_helpers'; import { Document } from '../../persistence/saved_object_store'; import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public'; @@ -91,3 +91,29 @@ export async function persistedStateToExpression( datasourceLayers, }); } + +export const validateDatasourceAndVisualization = ( + currentDataSource: Datasource | null, + currentDatasourceState: unknown | null, + currentVisualization: Visualization | null, + currentVisualizationState: unknown | undefined, + frameAPI: FramePublicAPI +): + | Array<{ + shortMessage: string; + longMessage: string; + }> + | undefined => { + const datasourceValidationErrors = currentDatasourceState + ? currentDataSource?.getErrorMessages(currentDatasourceState) + : undefined; + + const visualizationValidationErrors = currentVisualizationState + ? currentVisualization?.getErrorMessages(currentVisualizationState, frameAPI) + : undefined; + + if (datasourceValidationErrors || visualizationValidationErrors) { + return [...(datasourceValidationErrors || []), ...(visualizationValidationErrors || [])]; + } + return undefined; +}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 63ee02ac0404d..201c91ee91676 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -34,6 +34,7 @@ import { import { prependDatasourceExpression } from './expression_helpers'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; +import { validateDatasourceAndVisualization } from './state_helpers'; const MAX_SUGGESTIONS_DISPLAYED = 5; @@ -61,11 +62,28 @@ const PreviewRenderer = ({ withLabel, ExpressionRendererComponent, expression, + hasError, }: { withLabel: boolean; - expression: string; + expression: string | null | undefined; ExpressionRendererComponent: ReactExpressionRendererType; + hasError: boolean; }) => { + const onErrorMessage = ( +
+ +
+ ); return (
- { - return ( -
- -
- ); - }} - /> + {!expression || hasError ? ( + onErrorMessage + ) : ( + { + return onErrorMessage; + }} + /> + )}
); }; @@ -112,6 +120,7 @@ const SuggestionPreview = ({ expression?: Ast | null; icon: IconType; title: string; + error?: boolean; }; ExpressionRenderer: ReactExpressionRendererType; selected: boolean; @@ -129,11 +138,12 @@ const SuggestionPreview = ({ data-test-subj="lnsSuggestion" onClick={onSelect} > - {preview.expression ? ( + {preview.expression || preview.error ? ( ) : ( @@ -170,47 +180,81 @@ export function SuggestionPanel({ ? stagedPreview.visualization.activeId : activeVisualizationId; - const { suggestions, currentStateExpression } = useMemo(() => { - const newSuggestions = getSuggestions({ - datasourceMap, - datasourceStates: currentDatasourceStates, - visualizationMap, - activeVisualizationId: currentVisualizationId, - visualizationState: currentVisualizationState, - }) - .map((suggestion) => ({ - ...suggestion, - previewExpression: preparePreviewExpression( - suggestion, - visualizationMap[suggestion.visualizationId], - datasourceMap, - currentDatasourceStates, - frame - ), - })) - .filter((suggestion) => !suggestion.hide) - .slice(0, MAX_SUGGESTIONS_DISPLAYED); - - const newStateExpression = - currentVisualizationState && currentVisualizationId - ? preparePreviewExpression( - { visualizationState: currentVisualizationState }, - visualizationMap[currentVisualizationId], + const { suggestions, currentStateExpression, currentStateError } = useMemo( + () => { + const newSuggestions = getSuggestions({ + datasourceMap, + datasourceStates: currentDatasourceStates, + visualizationMap, + activeVisualizationId: currentVisualizationId, + visualizationState: currentVisualizationState, + }) + .filter((suggestion) => !suggestion.hide) + .filter( + ({ + visualizationId, + visualizationState: suggestionVisualizationState, + datasourceState: suggestionDatasourceState, + datasourceId: suggetionDatasourceId, + }) => { + return ( + validateDatasourceAndVisualization( + suggetionDatasourceId ? datasourceMap[suggetionDatasourceId] : null, + suggestionDatasourceState, + visualizationMap[visualizationId], + suggestionVisualizationState, + frame + ) == null + ); + } + ) + .slice(0, MAX_SUGGESTIONS_DISPLAYED) + .map((suggestion) => ({ + ...suggestion, + previewExpression: preparePreviewExpression( + suggestion, + visualizationMap[suggestion.visualizationId], datasourceMap, currentDatasourceStates, frame - ) - : undefined; + ), + })); + + const validationErrors = validateDatasourceAndVisualization( + activeDatasourceId ? datasourceMap[activeDatasourceId] : null, + activeDatasourceId && currentDatasourceStates[activeDatasourceId]?.state, + currentVisualizationId ? visualizationMap[currentVisualizationId] : null, + currentVisualizationState, + frame + ); - return { suggestions: newSuggestions, currentStateExpression: newStateExpression }; + const newStateExpression = + currentVisualizationState && currentVisualizationId && !validationErrors + ? preparePreviewExpression( + { visualizationState: currentVisualizationState }, + visualizationMap[currentVisualizationId], + datasourceMap, + currentDatasourceStates, + frame + ) + : undefined; + + return { + suggestions: newSuggestions, + currentStateExpression: newStateExpression, + currentStateError: validationErrors, + }; + }, // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - currentDatasourceStates, - currentVisualizationState, - currentVisualizationId, - datasourceMap, - visualizationMap, - ]); + [ + currentDatasourceStates, + currentVisualizationState, + currentVisualizationId, + activeDatasourceId, + datasourceMap, + visualizationMap, + ] + ); const context: ExecutionContextSearch = useMemo( () => ({ @@ -305,6 +349,7 @@ export function SuggestionPanel({ {currentVisualizationId && ( { expect(expressionRendererMock).toHaveBeenCalledTimes(2); }); + it('should show an error message if validation on datasource does not pass', () => { + mockDatasource.getErrorMessages.mockReturnValue([ + { shortMessage: 'An error occurred', longMessage: 'An long description here' }, + ]); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + expect(instance.find('[data-test-subj="configuration-failure"]').exists()).toBeTruthy(); + expect(instance.find(expressionRendererMock)).toHaveLength(0); + }); + + it('should show an error message if validation on visualization does not pass', () => { + mockDatasource.getErrorMessages.mockReturnValue(undefined); + mockDatasource.getLayers.mockReturnValue(['first']); + mockVisualization.getErrorMessages.mockReturnValue([ + { shortMessage: 'Some error happened', longMessage: 'Some long description happened' }, + ]); + mockVisualization.toExpression.mockReturnValue('vis'); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + instance = mount( + {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + expect(instance.find('[data-test-subj="configuration-failure"]').exists()).toBeTruthy(); + expect(instance.find(expressionRendererMock)).toHaveLength(0); + }); + + it('should show an error message if validation on both datasource and visualization do not pass', () => { + mockDatasource.getErrorMessages.mockReturnValue([ + { shortMessage: 'An error occurred', longMessage: 'An long description here' }, + ]); + mockDatasource.getLayers.mockReturnValue(['first']); + mockVisualization.getErrorMessages.mockReturnValue([ + { shortMessage: 'Some error happened', longMessage: 'Some long description happened' }, + ]); + mockVisualization.toExpression.mockReturnValue('vis'); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + instance = mount( + {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + // EuiFlexItem duplicates internally the attribute, so we need to filter only the most inner one here + expect( + instance.find('[data-test-subj="configuration-failure-more-errors"]').last().text() + ).toEqual(' +1 error'); + expect(instance.find(expressionRendererMock)).toHaveLength(0); + }); + it('should show an error message if the expression fails to parse', () => { mockDatasource.toExpression.mockReturnValue('|||'); mockDatasource.getLayers.mockReturnValue(['first']); @@ -487,7 +613,7 @@ describe('workspace_panel', () => { /> ); - expect(instance.find('[data-test-subj="expression-failure"]').first()).toBeTruthy(); + expect(instance.find('[data-test-subj="expression-failure"]').exists()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); 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 c4235a5514a54..e79060fb77329 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 @@ -9,7 +9,16 @@ import classNames from 'classnames'; import { FormattedMessage } from '@kbn/i18n/react'; import { Ast } from '@kbn/interpreter/common'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + EuiTextColor, + EuiButtonEmpty, + EuiLink, + EuiTitle, +} from '@elastic/eui'; import { CoreStart, CoreSetup } from 'kibana/public'; import { ExecutionContextSearch } from 'src/plugins/expressions'; import { @@ -42,6 +51,7 @@ import { import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { DropIllustration } from '../../../assets/drop_illustration'; import { getOriginalRequestErrorMessage } from '../../error_helper'; +import { validateDatasourceAndVisualization } from '../state_helpers'; export interface WorkspacePanelProps { activeVisualizationId: string | null; @@ -66,7 +76,7 @@ export interface WorkspacePanelProps { } interface WorkspaceState { - expressionBuildError: string | undefined; + expressionBuildError?: Array<{ shortMessage: string; longMessage: string }>; expandError: boolean; } @@ -124,26 +134,58 @@ export function WorkspacePanel({ ); const [localState, setLocalState] = useState({ - expressionBuildError: undefined as string | undefined, + expressionBuildError: undefined, expandError: false, }); const activeVisualization = activeVisualizationId ? visualizationMap[activeVisualizationId] : null; + + // Note: mind to all these eslint disable lines: the frameAPI will change too frequently + // and to prevent race conditions it is ok to leave them there. + + const configurationValidationError = useMemo( + () => + validateDatasourceAndVisualization( + activeDatasourceId ? datasourceMap[activeDatasourceId] : null, + activeDatasourceId && datasourceStates[activeDatasourceId]?.state, + activeVisualization, + visualizationState, + framePublicAPI + ), + // eslint-disable-next-line react-hooks/exhaustive-deps + [activeVisualization, visualizationState, activeDatasourceId, datasourceMap, datasourceStates] + ); + const expression = useMemo( () => { - try { - return buildExpression({ - visualization: activeVisualization, - visualizationState, - datasourceMap, - datasourceStates, - datasourceLayers: framePublicAPI.datasourceLayers, - }); - } catch (e) { - // Most likely an error in the expression provided by a datasource or visualization - setLocalState((s) => ({ ...s, expressionBuildError: e.toString() })); + if (!configurationValidationError) { + try { + return buildExpression({ + visualization: activeVisualization, + visualizationState, + datasourceMap, + datasourceStates, + datasourceLayers: framePublicAPI.datasourceLayers, + }); + } catch (e) { + const buildMessages = activeVisualization?.getErrorMessages( + visualizationState, + framePublicAPI + ); + const defaultMessage = { + shortMessage: i18n.translate('xpack.lens.editorFrame.buildExpressionError', { + defaultMessage: 'An unexpected error occurred while preparing the chart', + }), + longMessage: e.toString(), + }; + // Most likely an error in the expression provided by a datasource or visualization + setLocalState((s) => ({ + ...s, + expressionBuildError: buildMessages ?? [defaultMessage], + })); + } } }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -256,7 +298,7 @@ export function WorkspacePanel({ timefilter={plugins.data.query.timefilter.timefilter} onEvent={onEvent} setLocalState={setLocalState} - localState={localState} + localState={{ ...localState, configurationValidationError }} ExpressionRendererComponent={ExpressionRendererComponent} /> ); @@ -304,7 +346,9 @@ export const InnerVisualizationWrapper = ({ timefilter: TimefilterContract; onEvent: (event: ExpressionRendererEvent) => void; setLocalState: (dispatch: (prevState: WorkspaceState) => WorkspaceState) => void; - localState: WorkspaceState; + localState: WorkspaceState & { + configurationValidationError?: Array<{ shortMessage: string; longMessage: string }>; + }; ExpressionRendererComponent: ReactExpressionRendererType; }) => { const autoRefreshFetch$ = useMemo(() => timefilter.getAutoRefreshFetch$(), [timefilter]); @@ -326,6 +370,66 @@ export const InnerVisualizationWrapper = ({ ] ); + if (localState.configurationValidationError) { + let showExtraErrors = null; + if (localState.configurationValidationError.length > 1) { + if (localState.expandError) { + showExtraErrors = localState.configurationValidationError + .slice(1) + .map(({ longMessage }) => ( + + {longMessage} + + )); + } else { + showExtraErrors = ( + + { + setLocalState((prevState: WorkspaceState) => ({ + ...prevState, + expandError: !prevState.expandError, + })); + }} + > + {i18n.translate('xpack.lens.editorFrame.configurationFailureMoreErrors', { + defaultMessage: ` +{errors} {errors, plural, one {error} other {errors}}`, + values: { errors: localState.configurationValidationError.length - 1 }, + })} + + + ); + } + } + + return ( + + + + + + + + + + + + + {localState.configurationValidationError[0].longMessage} + + {showExtraErrors} + + ); + } + if (localState.expressionBuildError) { return ( @@ -338,10 +442,11 @@ export const InnerVisualizationWrapper = ({ defaultMessage="An error occurred in the expression" /> - {localState.expressionBuildError} + {localState.expressionBuildError[0].longMessage} ); } + return (
{ const visibleErrorMessage = getOriginalRequestErrorMessage(error) || errorMessage; + return ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 7e85ce5ecef71..5ab410a1c0af2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -53,6 +53,7 @@ export function createMockVisualization(): jest.Mocked { setDimension: jest.fn(), removeDimension: jest.fn(), + getErrorMessages: jest.fn((_state, _frame) => undefined), }; } @@ -92,6 +93,7 @@ export function createMockDatasource(id: string): DatasourceMock { // this is an additional property which doesn't exist on real datasources // but can be used to validate whether specific API mock functions are called publicAPIMock, + getErrorMessages: jest.fn((_state) => undefined), }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 1609ff1dbc80e..a3f48b162475a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -614,4 +614,178 @@ describe('IndexPattern Data Source', () => { }); }); }); + + describe('#getErrorMessages', () => { + it('should detect a missing reference in a layer', () => { + const state = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'bytes', + }, + }, + }, + }, + currentIndexPatternId: '1', + }; + const messages = indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState); + expect(messages).toHaveLength(1); + expect(messages![0]).toEqual({ + shortMessage: 'Invalid reference.', + longMessage: 'Field "bytes" has an invalid reference.', + }); + }); + + it('should detect and batch missing references in a layer', () => { + const state = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'bytes', + }, + col2: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'memory', + }, + }, + }, + }, + currentIndexPatternId: '1', + }; + const messages = indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState); + expect(messages).toHaveLength(1); + expect(messages![0]).toEqual({ + shortMessage: 'Invalid references.', + longMessage: 'Fields "bytes", "memory" have invalid reference.', + }); + }); + + it('should detect and batch missing references in multiple layers', () => { + const state = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'bytes', + }, + col2: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'memory', + }, + }, + }, + second: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'string', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'source', + }, + }, + }, + }, + currentIndexPatternId: '1', + }; + const messages = indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState); + expect(messages).toHaveLength(2); + expect(messages).toEqual([ + { + shortMessage: 'Invalid references on Layer 1.', + longMessage: 'Layer 1 has invalid references in fields "bytes", "memory".', + }, + { + shortMessage: 'Invalid reference on Layer 2.', + longMessage: 'Layer 2 has an invalid reference in field "source".', + }, + ]); + }); + + it('should return no errors if all references are satified', () => { + const state = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'document', + sourceField: 'bytes', + }, + }, + }, + }, + currentIndexPatternId: '1', + }; + expect( + indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState) + ).not.toBeDefined(); + }); + + it('should return no errors with layers with no columns', () => { + const state: IndexPatternPrivateState = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + }, + currentIndexPatternId: '1', + }; + expect(indexPatternDatasource.getErrorMessages(state)).not.toBeDefined(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index edc984f5e8016..0d82292780808 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -39,7 +39,12 @@ import { getDatasourceSuggestionsForVisualizeField, } from './indexpattern_suggestions'; -import { isDraggedField, normalizeOperationDataType } from './utils'; +import { + getInvalidFieldReferencesForLayer, + getInvalidReferences, + isDraggedField, + normalizeOperationDataType, +} from './utils'; import { LayerPanel } from './layerpanel'; import { IndexPatternColumn } from './operations'; import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; @@ -49,6 +54,7 @@ import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/pub import { deleteColumn } from './state_helpers'; import { Datasource, StateSetter } from '../index'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; +import { FieldBasedIndexPatternColumn } from './operations/definitions/column_types'; import { Dragging } from '../drag_drop/providers'; export { OperationType, IndexPatternColumn } from './operations'; @@ -335,6 +341,84 @@ export function getIndexPatternDatasource({ }, getDatasourceSuggestionsFromCurrentState, getDatasourceSuggestionsForVisualizeField, + + getErrorMessages(state) { + if (!state) { + return; + } + const invalidLayers = getInvalidReferences(state); + + if (invalidLayers.length === 0) { + return; + } + + const realIndex = Object.values(state.layers) + .map((layer, i) => { + const filteredIndex = invalidLayers.indexOf(layer); + if (filteredIndex > -1) { + return [filteredIndex, i + 1]; + } + }) + .filter(Boolean) as Array<[number, number]>; + const invalidFieldsPerLayer: string[][] = getInvalidFieldReferencesForLayer( + invalidLayers, + state.indexPatterns + ); + const originalLayersList = Object.keys(state.layers); + + return realIndex.map(([filteredIndex, layerIndex]) => { + const fieldsWithBrokenReferences: string[] = invalidFieldsPerLayer[filteredIndex].map( + (columnId) => { + const column = invalidLayers[filteredIndex].columns[ + columnId + ] as FieldBasedIndexPatternColumn; + return column.sourceField; + } + ); + + if (originalLayersList.length === 1) { + return { + shortMessage: i18n.translate( + 'xpack.lens.indexPattern.dataReferenceFailureShortSingleLayer', + { + defaultMessage: 'Invalid {fields, plural, one {reference} other {references}}.', + values: { + fields: fieldsWithBrokenReferences.length, + }, + } + ), + longMessage: i18n.translate( + 'xpack.lens.indexPattern.dataReferenceFailureLongSingleLayer', + { + defaultMessage: `{fieldsLength, plural, one {Field} other {Fields}} "{fields}" {fieldsLength, plural, one {has an} other {have}} invalid reference.`, + values: { + fields: fieldsWithBrokenReferences.join('", "'), + fieldsLength: fieldsWithBrokenReferences.length, + }, + } + ), + }; + } + return { + shortMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureShort', { + defaultMessage: + 'Invalid {fieldsLength, plural, one {reference} other {references}} on Layer {layer}.', + values: { + layer: layerIndex, + fieldsLength: fieldsWithBrokenReferences.length, + }, + }), + longMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureLong', { + defaultMessage: `Layer {layer} has {fieldsLength, plural, one {an invalid} other {invalid}} {fieldsLength, plural, one {reference} other {references}} in {fieldsLength, plural, one {field} other {fields}} "{fields}".`, + values: { + layer: layerIndex, + fields: fieldsWithBrokenReferences.join('", "'), + fieldsLength: fieldsWithBrokenReferences.length, + }, + }), + }; + }); + }, }; return indexPatternDatasource; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index df6bde0ba1a35..d3d65617f2253 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -5,7 +5,7 @@ */ import { DataType } from '../types'; -import { IndexPatternPrivateState, IndexPattern } from './types'; +import { IndexPatternPrivateState, IndexPattern, IndexPatternLayer } from './types'; import { DraggedField } from './indexpattern'; import { BaseIndexPatternColumn, @@ -43,7 +43,11 @@ export function isDraggedField(fieldCandidate: unknown): fieldCandidate is Dragg } export function hasInvalidReference(state: IndexPatternPrivateState) { - return Object.values(state.layers).some((layer) => { + return getInvalidReferences(state).length > 0; +} + +export function getInvalidReferences(state: IndexPatternPrivateState) { + return Object.values(state.layers).filter((layer) => { return layer.columnOrder.some((columnId) => { const column = layer.columns[columnId]; return ( @@ -58,19 +62,39 @@ export function hasInvalidReference(state: IndexPatternPrivateState) { }); } +export function getInvalidFieldReferencesForLayer( + layers: IndexPatternLayer[], + indexPatternMap: Record +) { + return layers.map((layer) => { + return layer.columnOrder.filter((columnId) => { + const column = layer.columns[columnId]; + return ( + hasField(column) && + fieldIsInvalid( + column.sourceField, + column.operationType, + indexPatternMap[layer.indexPatternId] + ) + ); + }); + }); +} + export function fieldIsInvalid( sourceField: string | undefined, operationType: OperationType | undefined, indexPattern: IndexPattern ) { const operationDefinition = operationType && operationDefinitionMap[operationType]; + return Boolean( sourceField && operationDefinition && !indexPattern.fields.some( (field) => field.name === sourceField && - operationDefinition.input === 'field' && + operationDefinition?.input === 'field' && operationDefinition.getPossibleOperationForField(field) !== undefined ) ); diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts index 80c7a174b3264..5ee33f9b4b3dd 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts @@ -193,4 +193,28 @@ describe('metric_visualization', () => { `); }); }); + + describe('#getErrorMessages', () => { + it('returns undefined if no error is raised', () => { + const datasource: DatasourcePublicAPI = { + ...createMockDatasource('l1').publicAPIMock, + getOperationForColumnId(_: string) { + return { + id: 'a', + dataType: 'number', + isBucketed: false, + label: 'shazm', + }; + }, + }; + const frame = { + ...mockFrame(), + datasourceLayers: { l1: datasource }, + }; + + const error = metricVisualization.getErrorMessages(exampleState(), frame); + + expect(error).not.toBeDefined(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index 77d189ce53d01..b75ac89d7e4d8 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -115,4 +115,9 @@ export const metricVisualization: Visualization = { removeDimension({ prevState }) { return { ...prevState, accessor: undefined }; }, + + getErrorMessages(state, frame) { + // Is it possible to break it? + return undefined; + }, }; diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts new file mode 100644 index 0000000000000..628d42d3de667 --- /dev/null +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getPieVisualization } from './visualization'; +import { PieVisualizationState } from './types'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; +import { DatasourcePublicAPI, FramePublicAPI } from '../types'; +import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; + +jest.mock('../id_generator'); + +const LAYER_ID = 'l1'; + +const pieVisualization = getPieVisualization({ + paletteService: chartPluginMock.createPaletteRegistry(), +}); + +function exampleState(): PieVisualizationState { + return { + shape: 'pie', + layers: [ + { + layerId: LAYER_ID, + groups: [], + metric: undefined, + numberDisplay: 'percent', + categoryDisplay: 'default', + legendDisplay: 'default', + nestedLegend: false, + }, + ], + }; +} + +function mockFrame(): FramePublicAPI { + return { + ...createMockFramePublicAPI(), + addNewLayer: () => LAYER_ID, + datasourceLayers: { + [LAYER_ID]: createMockDatasource(LAYER_ID).publicAPIMock, + }, + }; +} + +// Just a basic bootstrap here to kickstart the tests +describe('pie_visualization', () => { + describe('#getErrorMessages', () => { + it('returns undefined if no error is raised', () => { + const datasource: DatasourcePublicAPI = { + ...createMockDatasource('l1').publicAPIMock, + getOperationForColumnId(_: string) { + return { + id: 'a', + dataType: 'number', + isBucketed: false, + label: 'shazm', + }; + }, + }; + const frame = { + ...mockFrame(), + datasourceLayers: { l1: datasource }, + }; + + const error = pieVisualization.getErrorMessages(exampleState(), frame); + + expect(error).not.toBeDefined(); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 791480162b7fa..62e99396edbc7 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -233,4 +233,9 @@ export const getPieVisualization = ({ domElement ); }, + + getErrorMessages(state, frame) { + // not possible to break it? + return undefined; + }, }); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 6696a9328c837..27ab8f258bba8 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -181,6 +181,7 @@ export interface Datasource { getDatasourceSuggestionsFromCurrentState: (state: T) => Array>; getPublicAPI: (props: PublicAPIProps) => DatasourcePublicAPI; + getErrorMessages: (state: T) => Array<{ shortMessage: string; longMessage: string }> | undefined; /** * uniqueLabels of dimensions exposed for aria-labels of dragged dimensions */ @@ -571,6 +572,14 @@ export interface Visualization { state: T, datasourceLayers: Record ) => Ast | string | null; + /** + * The frame will call this function on all visualizations at few stages (pre-build/build error) in order + * to provide more context to the error and show it to the user + */ + getErrorMessages: ( + state: T, + frame: FramePublicAPI + ) => Array<{ shortMessage: string; longMessage: string }> | undefined; } export interface LensFilterEvent { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 4dde646ab64a5..7c49afa53af3e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -407,4 +407,219 @@ describe('xy_visualization', () => { expect(ops.filter(filterOperations).map((x) => x.dataType)).toEqual(['number']); }); }); + + describe('#getErrorMessages', () => { + it("should not return an error when there's only one dimension (X or Y)", () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + createMockFramePublicAPI() + ) + ).not.toBeDefined(); + }); + it("should not return an error when there's only one dimension on multiple layers (same axis everywhere)", () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + createMockFramePublicAPI() + ) + ).not.toBeDefined(); + }); + it('should not return an error when mixing different valid configurations in multiple layers', () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: ['a'], + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: undefined, + accessors: ['a'], + splitAccessor: 'a', + }, + ], + }, + createMockFramePublicAPI() + ) + ).not.toBeDefined(); + }); + it("should not return an error when there's only one splitAccessor dimension configured", () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + splitAccessor: 'a', + }, + ], + }, + createMockFramePublicAPI() + ) + ).not.toBeDefined(); + + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + splitAccessor: 'a', + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + splitAccessor: 'a', + }, + ], + }, + createMockFramePublicAPI() + ) + ).not.toBeDefined(); + }); + it('should return an error when there are multiple layers, one axis configured for each layer (but different axis from each other)', () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: undefined, + accessors: ['a'], + }, + ], + }, + createMockFramePublicAPI() + ) + ).toEqual([ + { + shortMessage: 'Missing Vertical axis.', + longMessage: 'Layer 1 requires a field for the Vertical axis.', + }, + ]); + }); + it('should return an error with batched messages for the same error with multiple layers', () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: ['a'], + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + splitAccessor: 'a', + }, + { + layerId: 'third', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + splitAccessor: 'a', + }, + ], + }, + createMockFramePublicAPI() + ) + ).toEqual([ + { + shortMessage: 'Missing Vertical axis.', + longMessage: 'Layers 2, 3 require a field for the Vertical axis.', + }, + ]); + }); + it("should return an error when some layers are complete but other layers aren't", () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: 'a', + accessors: ['a'], + }, + { + layerId: 'third', + seriesType: 'area', + xAccessor: 'a', + accessors: ['a'], + }, + ], + }, + createMockFramePublicAPI() + ) + ).toEqual([ + { + shortMessage: 'Missing Vertical axis.', + longMessage: 'Layer 1 requires a field for the Vertical axis.', + }, + ]); + }); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index c41d8e977297b..c7f775586ca0d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -174,29 +174,16 @@ export const getXyVisualization = ({ groups: [ { groupId: 'x', - groupLabel: isHorizontal - ? i18n.translate('xpack.lens.xyChart.verticalAxisLabel', { - defaultMessage: 'Vertical axis', - }) - : i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', { - defaultMessage: 'Horizontal axis', - }), + groupLabel: getAxisName('x', { isHorizontal }), accessors: layer.xAccessor ? [layer.xAccessor] : [], filterOperations: isBucketed, suggestedPriority: 1, supportsMoreColumns: !layer.xAccessor, - required: !layer.seriesType.includes('percentage'), dataTestSubj: 'lnsXY_xDimensionPanel', }, { groupId: 'y', - groupLabel: isHorizontal - ? i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', { - defaultMessage: 'Horizontal axis', - }) - : i18n.translate('xpack.lens.xyChart.verticalAxisLabel', { - defaultMessage: 'Vertical axis', - }), + groupLabel: getAxisName('y', { isHorizontal }), accessors: sortedAccessors, filterOperations: isNumericMetric, supportsMoreColumns: true, @@ -309,8 +296,117 @@ export const getXyVisualization = ({ toExpression: (state, layers, attributes) => toExpression(state, layers, paletteService, attributes), toPreviewExpression: (state, layers) => toPreviewExpression(state, layers, paletteService), + + getErrorMessages(state, frame) { + // Data error handling below here + const hasNoAccessors = ({ accessors }: LayerConfig) => + accessors == null || accessors.length === 0; + const hasNoSplitAccessor = ({ splitAccessor, seriesType }: LayerConfig) => + seriesType.includes('percentage') && splitAccessor == null; + + const errors: Array<{ + shortMessage: string; + longMessage: string; + }> = []; + + // check if the layers in the state are compatible with this type of chart + if (state && state.layers.length > 1) { + // Order is important here: Y Axis is fundamental to exist to make it valid + const checks: Array<[string, (layer: LayerConfig) => boolean]> = [ + ['Y', hasNoAccessors], + ['Break down', hasNoSplitAccessor], + ]; + + // filter out those layers with no accessors at all + const filteredLayers = state.layers.filter( + ({ accessors, xAccessor, splitAccessor }: LayerConfig) => + accessors.length > 0 || xAccessor != null || splitAccessor != null + ); + for (const [dimension, criteria] of checks) { + const result = validateLayersForDimension(dimension, filteredLayers, criteria); + if (!result.valid) { + errors.push(result.payload); + } + } + } + + return errors.length ? errors : undefined; + }, }); +function validateLayersForDimension( + dimension: string, + layers: LayerConfig[], + missingCriteria: (layer: LayerConfig) => boolean +): + | { valid: true } + | { + valid: false; + payload: { shortMessage: string; longMessage: string }; + } { + // Multiple layers must be consistent: + // * either a dimension is missing in ALL of them + // * or should not miss on any + if (layers.every(missingCriteria) || !layers.some(missingCriteria)) { + return { valid: true }; + } + // otherwise it's an error and it has to be reported + const layerMissingAccessors = layers.reduce((missing: number[], layer, i) => { + if (missingCriteria(layer)) { + missing.push(i); + } + return missing; + }, []); + + return { + valid: false, + payload: getMessageIdsForDimension(dimension, layerMissingAccessors, isHorizontalChart(layers)), + }; +} + +function getAxisName(axis: 'x' | 'y', { isHorizontal }: { isHorizontal: boolean }) { + const vertical = i18n.translate('xpack.lens.xyChart.verticalAxisLabel', { + defaultMessage: 'Vertical axis', + }); + const horizontal = i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', { + defaultMessage: 'Horizontal axis', + }); + if (axis === 'x') { + return isHorizontal ? vertical : horizontal; + } + return isHorizontal ? horizontal : vertical; +} + +// i18n ids cannot be dynamically generated, hence the function below +function getMessageIdsForDimension(dimension: string, layers: number[], isHorizontal: boolean) { + const layersList = layers.map((i: number) => i + 1).join(', '); + switch (dimension) { + case 'Break down': + return { + shortMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureSplitShort', { + defaultMessage: `Missing {axis}.`, + values: { axis: 'Break down by axis' }, + }), + longMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureSplitLong', { + defaultMessage: `{layers, plural, one {Layer} other {Layers}} {layersList} {layers, plural, one {requires} other {require}} a field for the {axis}.`, + values: { layers: layers.length, layersList, axis: 'Break down by axis' }, + }), + }; + case 'Y': + return { + shortMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureYShort', { + defaultMessage: `Missing {axis}.`, + values: { axis: getAxisName('y', { isHorizontal }) }, + }), + longMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureYLong', { + defaultMessage: `{layers, plural, one {Layer} other {Layers}} {layersList} {layers, plural, one {requires} other {require}} a field for the {axis}.`, + values: { layers: layers.length, layersList, axis: getAxisName('y', { isHorizontal }) }, + }), + }; + } + return { shortMessage: '', longMessage: '' }; +} + function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig { return { layerId, diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 100b2afcc97ce..73163cb70ada9 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -98,7 +98,7 @@ class AnnotationsTableUI extends Component { if (dataCounts.processed_record_count > 0) { // Load annotations for the selected job. ml.annotations - .getAnnotations({ + .getAnnotations$({ jobIds: [job.job_id], earliestMs: null, latestMs: null, diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js index 11e196b1c8e3f..b19328f89fbe4 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js @@ -24,7 +24,7 @@ jest.mock('../../../services/ml_api_service', () => { return { ml: { annotations: { - getAnnotations: jest.fn().mockReturnValue(mockAnnotations$), + getAnnotations$: jest.fn().mockReturnValue(mockAnnotations$), }, }, }; 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, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index c3bdacde5abd8..f6889c9a6f24c 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -392,7 +392,7 @@ export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, return new Promise((resolve) => { ml.annotations - .getAnnotations({ + .getAnnotations$({ jobIds, earliestMs: timeRange.earliestMs, latestMs: timeRange.latestMs, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts index f9e19ba6f757e..d028bacb49a77 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts @@ -13,7 +13,7 @@ import { http, http$ } from '../http_service'; import { basePath } from './index'; export const annotations = { - getAnnotations(obj: { + getAnnotations$(obj: { jobIds: string[]; earliestMs: number; latestMs: number; @@ -30,6 +30,23 @@ export const annotations = { }); }, + getAnnotations(obj: { + jobIds: string[]; + earliestMs: number | null; + latestMs: number | null; + maxAnnotations: number; + fields?: FieldToBucket[]; + detectorIndex?: number; + entities?: any[]; + }) { + const body = JSON.stringify(obj); + return http({ + path: `${basePath()}/annotations`, + method: 'POST', + body, + }); + }, + indexAnnotation(obj: Annotation) { const body = JSON.stringify(obj); return http({ diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss index 4399327c55dca..0c38d8e7ca171 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss @@ -76,3 +76,25 @@ $mlAnnotationRectDefaultFillOpacity: 0.05; .mlAnnotationHidden { display: none; } + +// context annotation marker +.mlContextAnnotationRect { + stroke: $euiColorFullShade; + stroke-width: $mlAnnotationBorderWidth; + stroke-opacity: $mlAnnotationRectDefaultStrokeOpacity; + transition: stroke-opacity $euiAnimSpeedFast; + + fill: $euiColorFullShade; + fill-opacity: $mlAnnotationRectDefaultFillOpacity; + transition: fill-opacity $euiAnimSpeedFast; + + shape-rendering: geometricPrecision; +} + +.mlContextAnnotationRect-isBlur { + stroke-opacity: $mlAnnotationRectDefaultStrokeOpacity / 2; + transition: stroke-opacity $euiAnimSpeedFast; + + fill-opacity: $mlAnnotationRectDefaultFillOpacity / 2; + transition: fill-opacity $euiAnimSpeedFast; +} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts index 9a06f6d6b8e03..04b666b4fc684 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts @@ -6,6 +6,7 @@ import d3 from 'd3'; +import React from 'react'; import { Annotation } from '../../../../../common/types/annotations'; import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; import { ChartTooltipService } from '../../../components/chart_tooltip'; @@ -19,6 +20,33 @@ interface State { annotation: Annotation | null; } -export interface TimeseriesChart extends React.Component { +interface TimeseriesChartProps { + annotation: object; + autoZoomDuration: number; + bounds: object; + contextAggregationInterval: object; + contextChartData: any[]; + contextForecastData: any[]; + contextChartSelected: any; + detectorIndex: number; + focusAggregationInterval: object; + focusAnnotationData: Annotation[]; + focusChartData: any[]; + focusForecastData: any[]; + modelPlotEnabled: boolean; + renderFocusChartOnly: boolean; + selectedJob: CombinedJob; + showForecast: boolean; + showModelBounds: boolean; + svgWidth: number; + swimlaneData: any[]; + zoomFrom: object; + zoomTo: object; + zoomFromFocusLoaded: object; + zoomToFocusLoaded: object; + tooltipService: object; +} + +declare class TimeseriesChart extends React.Component { focusXScale: d3.scale.Ordinal<{}, number>; } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 448d39db3e444..3169ecfd1bbc7 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -25,6 +25,7 @@ import { annotation$ } from '../../../services/annotations_service'; import { formatValue } from '../../../formatters/format_value'; import { LINE_CHART_ANOMALY_RADIUS, + ANNOTATION_SYMBOL_HEIGHT, MULTI_BUCKET_SYMBOL_SIZE, SCHEDULED_EVENT_SYMBOL_HEIGHT, drawLineChartDots, @@ -48,6 +49,7 @@ import { renderAnnotations, highlightFocusChartAnnotation, unhighlightFocusChartAnnotation, + ANNOTATION_MIN_WIDTH, } from './timeseries_chart_annotations'; const focusZoomPanelHeight = 25; @@ -57,6 +59,8 @@ const contextChartHeight = 60; const contextChartLineTopMargin = 3; const chartSpacing = 25; const swimlaneHeight = 30; +const ctxAnnotationMargin = 2; +const annotationHeight = ANNOTATION_SYMBOL_HEIGHT + ctxAnnotationMargin * 2; const margin = { top: 10, right: 10, bottom: 15, left: 40 }; const ZOOM_INTERVAL_OPTIONS = [ @@ -80,9 +84,16 @@ const anomalyGrayScale = d3.scale .domain([3, 25, 50, 75, 100]) .range(['#dce7ed', '#b0c5d6', '#b1a34e', '#b17f4e', '#c88686']); -function getSvgHeight() { +function getSvgHeight(showAnnotations) { + const adjustedAnnotationHeight = showAnnotations ? annotationHeight : 0; return ( - focusHeight + contextChartHeight + swimlaneHeight + chartSpacing + margin.top + margin.bottom + focusHeight + + contextChartHeight + + swimlaneHeight + + adjustedAnnotationHeight + + chartSpacing + + margin.top + + margin.bottom ); } @@ -225,7 +236,12 @@ class TimeseriesChartIntl extends Component { } componentDidUpdate(prevProps) { - if (this.props.renderFocusChartOnly === false || prevProps.svgWidth !== this.props.svgWidth) { + if ( + this.props.renderFocusChartOnly === false || + prevProps.svgWidth !== this.props.svgWidth || + prevProps.showAnnotations !== this.props.showAnnotations || + prevProps.annotationData !== this.props.annotationData + ) { this.renderChart(); this.drawContextChartSelection(); } @@ -246,6 +262,7 @@ class TimeseriesChartIntl extends Component { modelPlotEnabled, selectedJob, svgWidth, + showAnnotations, } = this.props; const createFocusChart = this.createFocusChart.bind(this); @@ -254,7 +271,7 @@ class TimeseriesChartIntl extends Component { const focusYAxis = this.focusYAxis; const focusYScale = this.focusYScale; - const svgHeight = getSvgHeight(); + const svgHeight = getSvgHeight(showAnnotations); // Clear any existing elements from the visualization, // then build the svg elements for the bubble chart. @@ -367,7 +384,13 @@ class TimeseriesChartIntl extends Component { // Draw each of the component elements. createFocusChart(focus, this.vizWidth, focusHeight); - drawContextElements(context, this.vizWidth, contextChartHeight, swimlaneHeight); + drawContextElements( + context, + this.vizWidth, + contextChartHeight, + swimlaneHeight, + annotationHeight + ); } contextChartInitialized = false; @@ -947,10 +970,19 @@ class TimeseriesChartIntl extends Component { } drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) { - const { bounds, contextChartData, contextForecastData, modelPlotEnabled } = this.props; - + const { + bounds, + contextChartData, + contextForecastData, + modelPlotEnabled, + annotationData, + showAnnotations, + } = this.props; const data = contextChartData; + const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); + const hideFocusChartTooltip = this.props.tooltipService.hide.bind(this.props.tooltipService); + this.contextXScale = d3.time .scale() .range([0, cxtWidth]) @@ -997,20 +1029,26 @@ class TimeseriesChartIntl extends Component { .domain([chartLimits.min, chartLimits.max]); const borders = cxtGroup.append('g').attr('class', 'axis'); + const brushChartHeight = showAnnotations + ? cxtChartHeight + swlHeight + annotationHeight + : cxtChartHeight + swlHeight; // Add borders left and right. + borders.append('line').attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', brushChartHeight); borders .append('line') - .attr('x1', 0) + .attr('x1', cxtWidth) .attr('y1', 0) - .attr('x2', 0) - .attr('y2', cxtChartHeight + swlHeight); + .attr('x2', cxtWidth) + .attr('y2', brushChartHeight); + + // Add bottom borders borders .append('line') - .attr('x1', cxtWidth) - .attr('y1', 0) + .attr('x1', 0) + .attr('y1', brushChartHeight) .attr('x2', cxtWidth) - .attr('y2', cxtChartHeight + swlHeight); + .attr('y2', brushChartHeight); // Add x axis. const timeBuckets = getTimeBucketsFromCache(); @@ -1065,6 +1103,61 @@ class TimeseriesChartIntl extends Component { cxtGroup.append('path').datum(data).attr('class', 'values-line').attr('d', contextValuesLine); drawLineChartDots(data, cxtGroup, contextValuesLine, 1); + // Add annotation markers to the context area + cxtGroup.append('g').classed('mlContextAnnotations', true); + + const [contextXRangeStart, contextXRangeEnd] = this.contextXScale.range(); + const ctxAnnotations = cxtGroup + .select('.mlContextAnnotations') + .selectAll('g.mlContextAnnotation') + .data(showAnnotations && annotationData ? annotationData : [], (d) => d._id || ''); + + ctxAnnotations.enter().append('g').classed('mlContextAnnotation', true); + + const ctxAnnotationRects = ctxAnnotations + .selectAll('.mlContextAnnotationRect') + .data((d) => [d]); + + ctxAnnotationRects + .enter() + .append('rect') + .attr('rx', ctxAnnotationMargin) + .attr('ry', ctxAnnotationMargin) + .on('mouseover', function (d) { + showFocusChartTooltip(d, this); + }) + .on('mouseout', () => hideFocusChartTooltip()) + .classed('mlContextAnnotationRect', true); + + ctxAnnotationRects + .attr('x', (d) => { + const date = moment(d.timestamp); + let xPos = this.contextXScale(date); + + if (xPos - ANNOTATION_SYMBOL_HEIGHT <= contextXRangeStart) { + xPos = 0; + } + if (xPos + ANNOTATION_SYMBOL_HEIGHT >= contextXRangeEnd) { + xPos = contextXRangeEnd - ANNOTATION_SYMBOL_HEIGHT; + } + + return xPos; + }) + .attr('y', cxtChartHeight + swlHeight + 2) + .attr('height', ANNOTATION_SYMBOL_HEIGHT) + .attr('width', (d) => { + const start = this.contextXScale(moment(d.timestamp)) + 1; + const end = + typeof d.end_timestamp !== 'undefined' + ? this.contextXScale(moment(d.end_timestamp)) - 1 + : start + ANNOTATION_MIN_WIDTH; + const width = Math.max(ANNOTATION_MIN_WIDTH, end - start); + return width; + }); + + ctxAnnotations.classed('mlAnnotationHidden', !showAnnotations); + ctxAnnotationRects.exit().remove(); + // Create the path elements for the forecast value line and bounds area. if (contextForecastData !== undefined) { cxtGroup diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts index 0b541d54ee7b3..bd86d07dcd8b7 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts @@ -90,7 +90,7 @@ const ANNOTATION_DEFAULT_LEVEL = 1; const ANNOTATION_LEVEL_HEIGHT = 28; const ANNOTATION_UPPER_RECT_MARGIN = 0; const ANNOTATION_UPPER_TEXT_MARGIN = -7; -const ANNOTATION_MIN_WIDTH = 2; +export const ANNOTATION_MIN_WIDTH = 2; const ANNOTATION_RECT_BORDER_RADIUS = 2; const ANNOTATION_TEXT_VERTICAL_OFFSET = 26; const ANNOTATION_TEXT_RECT_VERTICAL_OFFSET = 12; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx new file mode 100644 index 0000000000000..89e7d292dbdf2 --- /dev/null +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { FC, useEffect, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { MlTooltipComponent } from '../../../components/chart_tooltip'; +import { TimeseriesChart } from './timeseries_chart'; +import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; +import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../common/constants/search'; +import { extractErrorMessage } from '../../../../../common/util/errors'; +import { Annotation } from '../../../../../common/types/annotations'; +import { useMlKibana, useNotifications } from '../../../contexts/kibana'; +import { getBoundsRoundedToInterval } from '../../../util/time_buckets'; +import { ANNOTATION_EVENT_USER } from '../../../../../common/constants/annotations'; +import { getControlsForDetector } from '../../get_controls_for_detector'; + +interface TimeSeriesChartWithTooltipsProps { + bounds: any; + detectorIndex: number; + renderFocusChartOnly: boolean; + selectedJob: CombinedJob; + selectedEntities: Record; + showAnnotations: boolean; + showForecast: boolean; + showModelBounds: boolean; + chartProps: any; + lastRefresh: number; + contextAggregationInterval: any; +} +export const TimeSeriesChartWithTooltips: FC = ({ + bounds, + detectorIndex, + renderFocusChartOnly, + selectedJob, + selectedEntities, + showAnnotations, + showForecast, + showModelBounds, + chartProps, + lastRefresh, + contextAggregationInterval, +}) => { + const { toasts: toastNotifications } = useNotifications(); + const { + services: { + mlServices: { mlApiServices }, + }, + } = useMlKibana(); + + const [annotationData, setAnnotationData] = useState([]); + + const showAnnotationErrorToastNotification = useCallback((error?: string) => { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.ml.timeSeriesExplorer.mlSingleMetricViewerChart.annotationsErrorTitle', + { + defaultMessage: 'An error occurred fetching annotations', + } + ), + ...(error ? { text: extractErrorMessage(error) } : {}), + }); + }, []); + + useEffect(() => { + let unmounted = false; + const entities = getControlsForDetector(detectorIndex, selectedEntities, selectedJob.job_id); + const nonBlankEntities = Array.isArray(entities) + ? entities.filter((entity) => entity.fieldValue !== null) + : undefined; + const searchBounds = getBoundsRoundedToInterval(bounds, contextAggregationInterval, false); + + /** + * Loads the full list of annotations for job without any aggs or time boundaries + * used to indicate existence of annotations that are beyond the selected time + * in the time series brush area + */ + const loadAnnotations = async (jobId: string) => { + try { + const resp = await mlApiServices.annotations.getAnnotations({ + jobIds: [jobId], + earliestMs: searchBounds.min.valueOf(), + latestMs: searchBounds.max.valueOf(), + maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + fields: [ + { + field: 'event', + missing: ANNOTATION_EVENT_USER, + }, + ], + detectorIndex, + entities: nonBlankEntities, + }); + if (!unmounted) { + if (Array.isArray(resp.annotations[jobId])) { + setAnnotationData(resp.annotations[jobId]); + } + } + } catch (error) { + showAnnotationErrorToastNotification(error); + } + }; + + loadAnnotations(selectedJob.job_id); + + return () => { + unmounted = true; + }; + }, [ + selectedJob.job_id, + detectorIndex, + lastRefresh, + selectedEntities, + bounds, + contextAggregationInterval, + ]); + + return ( +
+ + {(tooltipService) => ( + + )} + +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts index cb66b8d53e660..530ba567ed9f7 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts @@ -6,7 +6,7 @@ import { FC } from 'react'; -import { getDateFormatTz, TimeRangeBounds } from '../explorer/explorer_utils'; +import { TimeRangeBounds } from '../explorer/explorer_utils'; declare const TimeSeriesExplorer: FC<{ appStateHandler: (action: string, payload: any) => void; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 5e452dab2f883..720c1377d4035 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -47,12 +47,10 @@ import { import { AnnotationFlyout } from '../components/annotations/annotation_flyout'; import { AnnotationsTable } from '../components/annotations/annotations_table'; import { AnomaliesTable } from '../components/anomalies_table/anomalies_table'; -import { MlTooltipComponent } from '../components/chart_tooltip'; import { ForecastingModal } from './components/forecasting_modal/forecasting_modal'; import { LoadingIndicator } from '../components/loading_indicator/loading_indicator'; import { SelectInterval } from '../components/controls/select_interval/select_interval'; import { SelectSeverity } from '../components/controls/select_severity/select_severity'; -import { TimeseriesChart } from './components/timeseries_chart/timeseries_chart'; import { TimeseriesexplorerNoChartData } from './components/timeseriesexplorer_no_chart_data'; import { TimeSeriesExplorerPage } from './timeseriesexplorer_page'; @@ -83,6 +81,7 @@ import { import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings'; import { getControlsForDetector } from './get_controls_for_detector'; import { SeriesControls } from './components/series_controls'; +import { TimeSeriesChartWithTooltips } from './components/timeseries_chart/timeseries_chart_with_tooltip'; // Used to indicate the chart is being plotted across // all partition field values, where the cardinality of the field cannot be @@ -175,6 +174,7 @@ export class TimeSeriesExplorer extends React.Component { this.resizeRef.current !== null ? this.resizeRef.current.offsetWidth - containerPadding : 0, }); }; + unmounted = false; /** * Subject for listening brush time range selection. @@ -877,6 +877,7 @@ export class TimeSeriesExplorer extends React.Component { componentWillUnmount() { this.subscriptions.unsubscribe(); this.resizeChecker.destroy(); + this.unmounted = true; } render() { @@ -957,7 +958,6 @@ export class TimeSeriesExplorer extends React.Component { isEqual(this.previousChartProps.focusForecastData, chartProps.focusForecastData) && isEqual(this.previousChartProps.focusChartData, chartProps.focusChartData) && isEqual(this.previousChartProps.focusAnnotationData, chartProps.focusAnnotationData) && - this.previousShowAnnotations === showAnnotations && this.previousShowForecast === showForecast && this.previousShowModelBounds === showModelBounds && this.props.previousRefresh === lastRefresh @@ -966,7 +966,6 @@ export class TimeSeriesExplorer extends React.Component { } this.previousChartProps = chartProps; - this.previousShowAnnotations = showAnnotations; this.previousShowForecast = showForecast; this.previousShowModelBounds = showModelBounds; @@ -1134,23 +1133,19 @@ export class TimeSeriesExplorer extends React.Component {
)}
-
- - {(tooltipService) => ( - - )} - -
+ {focusAnnotationError !== undefined && ( <> { return { type: this.type, - rollupIndex: this.rollupIndex, + params: { + rollup_index: this.rollupIndex, + }, }; }; } diff --git a/x-pack/plugins/rollup/server/lib/__tests__/fixtures/index.js b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/index.js new file mode 100644 index 0000000000000..e97606c1fadfb --- /dev/null +++ b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/index.js @@ -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 { jobs } from './jobs'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/jobs.js b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js similarity index 65% rename from src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/jobs.js rename to x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js index 39ebd9595eeaf..c03b7c33abe0a 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/jobs.js +++ b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ export const jobs = [ diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/jobs_compatibility.js b/x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js similarity index 81% rename from src/plugins/data/server/index_patterns/fetcher/lib/__tests__/jobs_compatibility.js rename to x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js index e3c93ac1f8616..a67f67de859f5 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/jobs_compatibility.js +++ b/x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js @@ -1,22 +1,8 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ - import expect from '@kbn/expect'; import { areJobsCompatible, mergeJobConfigurations } from '../jobs_compatibility'; import { jobs } from './fixtures'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/jobs_compatibility.ts b/x-pack/plugins/rollup/server/lib/jobs_compatibility.ts similarity index 79% rename from src/plugins/data/server/index_patterns/fetcher/lib/jobs_compatibility.ts rename to x-pack/plugins/rollup/server/lib/jobs_compatibility.ts index f21de8907ee24..f5f54cf9a54e8 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/jobs_compatibility.ts +++ b/x-pack/plugins/rollup/server/lib/jobs_compatibility.ts @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { isEqual } from 'lodash'; diff --git a/x-pack/plugins/rollup/server/lib/map_capabilities.ts b/x-pack/plugins/rollup/server/lib/map_capabilities.ts new file mode 100644 index 0000000000000..233c6d1dd4b4b --- /dev/null +++ b/x-pack/plugins/rollup/server/lib/map_capabilities.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mergeJobConfigurations } from './jobs_compatibility'; + +export function getCapabilitiesForRollupIndices(indices: { [key: string]: any }) { + const indexNames = Object.keys(indices); + const capabilities = {} as { [key: string]: any }; + + indexNames.forEach((index) => { + try { + capabilities[index] = mergeJobConfigurations(indices[index].rollup_jobs); + } catch (e) { + capabilities[index] = { + error: e.message, + }; + } + }); + + return capabilities; +} diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/merge_capabilities_with_fields.ts b/x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts similarity index 70% rename from src/plugins/data/server/index_patterns/fetcher/lib/merge_capabilities_with_fields.ts rename to x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts index dd69f4b7ff007..51111e9e45d0a 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/merge_capabilities_with_fields.ts +++ b/x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts @@ -1,30 +1,20 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ // Merge rollup capabilities information with field information -import { FieldDescriptor } from '../index_patterns_fetcher'; +export interface Field { + name?: string; + [key: string]: any; +} export const mergeCapabilitiesWithFields = ( rollupIndexCapabilities: { [key: string]: any }, fieldsFromFieldCapsApi: { [key: string]: any }, - previousFields: FieldDescriptor[] = [] + previousFields: Field[] = [] ) => { const rollupFields = [...previousFields]; const rollupFieldNames: string[] = []; diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts index dcf6629d35397..f439ac555aed9 100644 --- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts +++ b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts @@ -6,11 +6,8 @@ import { keyBy, isString } from 'lodash'; import { ILegacyScopedClusterClient } from 'src/core/server'; import { ReqFacade } from '../../../../../../src/plugins/vis_type_timeseries/server'; - -import { - mergeCapabilitiesWithFields, - getCapabilitiesForRollupIndices, -} from '../../../../../../src/plugins/data/server'; +import { mergeCapabilitiesWithFields } from '../merge_capabilities_with_fields'; +import { getCapabilitiesForRollupIndices } from '../map_capabilities'; const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData); diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index 51920af7c8cbc..fe193150fc1ca 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -36,7 +36,8 @@ import { registerRollupSearchStrategy } from './lib/search_strategies'; import { elasticsearchJsPlugin } from './client/elasticsearch_rollup'; import { isEsError } from './shared_imports'; import { formatEsError } from './lib/format_es_error'; -import { getCapabilitiesForRollupIndices } from '../../../../src/plugins/data/server'; +import { getCapabilitiesForRollupIndices } from './lib/map_capabilities'; +import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields'; interface RollupContext { client: ILegacyScopedClusterClient; @@ -106,6 +107,7 @@ export class RollupPlugin implements Plugin { isEsError, formatEsError, getCapabilitiesForRollupIndices, + mergeCapabilitiesWithFields, }, sharedImports: { IndexPatternsFetcher, diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts new file mode 100644 index 0000000000000..7bf525ca4aa98 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { RouteDependencies } from '../../../types'; +import { registerFieldsForWildcardRoute } from './register_fields_for_wildcard_route'; + +export function registerIndexPatternsRoutes(dependencies: RouteDependencies) { + registerFieldsForWildcardRoute(dependencies); +} diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts new file mode 100644 index 0000000000000..df9907fbf731a --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { keyBy } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { Field } from '../../../lib/merge_capabilities_with_fields'; +import { RouteDependencies } from '../../../types'; +import type { IndexPatternsFetcher as IndexPatternsFetcherType } from '../../../../../../../src/plugins/data/server'; + +const parseMetaFields = (metaFields: string | string[]) => { + let parsedFields: string[] = []; + if (typeof metaFields === 'string') { + parsedFields = JSON.parse(metaFields); + } else { + parsedFields = metaFields; + } + return parsedFields; +}; + +const getFieldsForWildcardRequest = async ( + context: any, + request: any, + response: any, + IndexPatternsFetcher: typeof IndexPatternsFetcherType +) => { + const { asCurrentUser } = context.core.elasticsearch.client; + const indexPatterns = new IndexPatternsFetcher(asCurrentUser); + const { pattern, meta_fields: metaFields } = request.query; + + let parsedFields: string[] = []; + try { + parsedFields = parseMetaFields(metaFields); + } catch (error) { + return response.badRequest({ + body: error, + }); + } + + try { + const fields = await indexPatterns.getFieldsForWildcard({ + pattern, + metaFields: parsedFields, + }); + + return response.ok({ + body: { fields }, + headers: { + 'content-type': 'application/json', + }, + }); + } catch (error) { + return response.notFound(); + } +}; + +/** + * Get list of fields for rollup index pattern, in the format of regular index pattern fields + */ +export const registerFieldsForWildcardRoute = ({ + router, + license, + lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices, mergeCapabilitiesWithFields }, + sharedImports: { IndexPatternsFetcher }, +}: RouteDependencies) => { + const querySchema = schema.object({ + pattern: schema.string(), + meta_fields: schema.arrayOf(schema.string(), { + defaultValue: [], + }), + params: schema.string({ + validate(value) { + try { + const params = JSON.parse(value); + const keys = Object.keys(params); + const { rollup_index: rollupIndex } = params; + + if (!rollupIndex) { + return '[request query.params]: "rollup_index" is required'; + } else if (keys.length > 1) { + const invalidParams = keys.filter((key) => key !== 'rollup_index'); + return `[request query.params]: ${invalidParams.join(', ')} is not allowed`; + } + } catch (err) { + return '[request query.params]: expected JSON string'; + } + }, + }), + }); + + router.get( + { + path: '/api/index_patterns/rollup/_fields_for_wildcard', + validate: { + query: querySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { params, meta_fields: metaFields } = request.query; + + try { + // Make call and use field information from response + const { payload } = await getFieldsForWildcardRequest( + context, + request, + response, + IndexPatternsFetcher + ); + const fields = payload.fields; + const parsedParams = JSON.parse(params); + const rollupIndex = parsedParams.rollup_index; + const rollupFields: Field[] = []; + const fieldsFromFieldCapsApi: { [key: string]: any } = keyBy(fields, 'name'); + const rollupIndexCapabilities = getCapabilitiesForRollupIndices( + await context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', { + indexPattern: rollupIndex, + }) + )[rollupIndex].aggs; + + // Keep meta fields + metaFields.forEach( + (field: string) => + fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field]) + ); + + const mergedRollupFields = mergeCapabilitiesWithFields( + rollupIndexCapabilities, + fieldsFromFieldCapsApi, + rollupFields + ); + return response.ok({ body: { fields: mergedRollupFields } }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/index.ts b/x-pack/plugins/rollup/server/routes/index.ts index 322003c0ee325..b25480855b4a2 100644 --- a/x-pack/plugins/rollup/server/routes/index.ts +++ b/x-pack/plugins/rollup/server/routes/index.ts @@ -6,11 +6,13 @@ import { RouteDependencies } from '../types'; +import { registerIndexPatternsRoutes } from './api/index_patterns'; import { registerIndicesRoutes } from './api/indices'; import { registerJobsRoutes } from './api/jobs'; import { registerSearchRoutes } from './api/search'; export function registerApiRoutes(dependencies: RouteDependencies) { + registerIndexPatternsRoutes(dependencies); registerIndicesRoutes(dependencies); registerJobsRoutes(dependencies); registerSearchRoutes(dependencies); diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts index 89e13e69c4da2..b167806cf8d5d 100644 --- a/x-pack/plugins/rollup/server/types.ts +++ b/x-pack/plugins/rollup/server/types.ts @@ -8,7 +8,6 @@ import { IRouter } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; -import { getCapabilitiesForRollupIndices } from 'src/plugins/data/server'; import { IndexManagementPluginSetup } from '../../index_management/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -16,6 +15,8 @@ import { License } from './services'; import { IndexPatternsFetcher } from './shared_imports'; import { isEsError } from './shared_imports'; import { formatEsError } from './lib/format_es_error'; +import { getCapabilitiesForRollupIndices } from './lib/map_capabilities'; +import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields'; export interface Dependencies { indexManagement?: IndexManagementPluginSetup; @@ -32,6 +33,7 @@ export interface RouteDependencies { isEsError: typeof isEsError; formatEsError: typeof formatEsError; getCapabilitiesForRollupIndices: typeof getCapabilitiesForRollupIndices; + mergeCapabilitiesWithFields: typeof mergeCapabilitiesWithFields; }; sharedImports: { IndexPatternsFetcher: typeof IndexPatternsFetcher; 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); } }, }; diff --git a/x-pack/test/api_integration/apis/management/rollup/constants.js b/x-pack/test/api_integration/apis/management/rollup/constants.js index 0313434cf716c..fe899c4c10c88 100644 --- a/x-pack/test/api_integration/apis/management/rollup/constants.js +++ b/x-pack/test/api_integration/apis/management/rollup/constants.js @@ -5,7 +5,7 @@ */ export const API_BASE_PATH = '/api/rollup'; -export const INDEX_PATTERNS_EXTENSION_BASE_PATH = '/api/index_patterns'; +export const INDEX_PATTERNS_EXTENSION_BASE_PATH = '/api/index_patterns/rollup'; export const ROLLUP_INDEX_NAME = 'rollup_index'; export const INDEX_TO_ROLLUP_MAPPINGS = { properties: { diff --git a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js index 0a93e8b8bd1e3..357b952e7e66d 100644 --- a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js +++ b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @@ -26,6 +26,7 @@ export default function ({ getService }) { describe('query params validation', () => { let uri; let body; + let params; it('"pattern" is required', async () => { uri = `${BASE_URI}`; @@ -35,17 +36,62 @@ export default function ({ getService }) { ); }); + it('"params" is required', async () => { + params = { pattern: 'foo' }; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; + ({ body } = await supertest.get(uri).expect(400)); + expect(body.message).to.contain( + '[request query.params]: expected value of type [string]' + ); + }); + + it('"params" must be a valid JSON string', async () => { + params = { pattern: 'foo', params: 'foobarbaz' }; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; + ({ body } = await supertest.get(uri).expect(400)); + expect(body.message).to.contain('[request query.params]: expected JSON string'); + }); + + it('"params" requires a "rollup_index" property', async () => { + params = { pattern: 'foo', params: JSON.stringify({}) }; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; + ({ body } = await supertest.get(uri).expect(400)); + expect(body.message).to.contain('[request query.params]: "rollup_index" is required'); + }); + + it('"params" only accepts a "rollup_index" property', async () => { + params = { + pattern: 'foo', + params: JSON.stringify({ rollup_index: 'my_index', someProp: 'bar' }), + }; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; + ({ body } = await supertest.get(uri).expect(400)); + expect(body.message).to.contain('[request query.params]: someProp is not allowed'); + }); + + it('"meta_fields" must be an Array', async () => { + params = { + pattern: 'foo', + params: JSON.stringify({ rollup_index: 'bar' }), + meta_fields: 'stringValue', + }; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; + ({ body } = await supertest.get(uri).expect(400)); + expect(body.message).to.contain( + '[request query.meta_fields]: could not parse array value from json input' + ); + }); + it('should return 404 the rollup index to query does not exist', async () => { uri = `${BASE_URI}?${stringify( { pattern: 'foo', - type: 'rollup', - rollup_index: 'bar', + params: JSON.stringify({ rollup_index: 'bar' }), }, { sort: false } )}`; ({ body } = await supertest.get(uri).expect(404)); - expect(body.message).to.contain('No indices match pattern "foo"'); + expect(body.message).to.contain('[index_not_found_exception] no such index [bar]'); }); }); @@ -59,8 +105,7 @@ export default function ({ getService }) { // Query for wildcard const params = { pattern: indexName, - type: 'rollup', - rollup_index: rollupIndex, + params: JSON.stringify({ rollup_index: rollupIndex }), }; const uri = `${BASE_URI}?${stringify(params, { sort: false })}`; const { body } = await supertest.get(uri).expect(200);