Improve search typescript (#69333)

* [search] Refactor the way search strategies are registered/retrieved on the server

* Fix types and tests and update docs

* Fix failing test

* Fix build of example plugin

* Fix functional test

* Make server strategies sync

* Move strategy name into options

* docs

* Remove FE strategies

* TypeScript of hell
delete search explorer

* Fix search interceptor OSS tests

* typos

* test cleanup

* Delete search example
fix interceptor async tests to use fake timers

* docs

* fix

* return search wrapper

* Update search interceptor tests and abort utils

* ts

* jest test fix

* code review

* change how logs consume search API

Co-authored-by: Lukas Olson <olson.lukas@gmail.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Liza Katz 2020-07-08 00:34:28 +03:00 committed by GitHub
parent 53ee7a762d
commit f290c68696
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 1048 additions and 3049 deletions

View file

@ -10,16 +10,7 @@
export declare function getSearchParamsFromRequest(searchRequest: SearchRequest, dependencies: {
injectedMetadata: CoreStart['injectedMetadata'];
uiSettings: IUiSettingsClient;
}): {
rest_total_hits_as_int: boolean;
ignore_unavailable: boolean;
ignore_throttled: boolean;
max_concurrent_shard_requests: any;
preference: any;
timeout: string | undefined;
index: any;
body: any;
};
}): ISearchRequestParams;
```
## Parameters
@ -31,14 +22,5 @@ export declare function getSearchParamsFromRequest(searchRequest: SearchRequest,
<b>Returns:</b>
`{
rest_total_hits_as_int: boolean;
ignore_unavailable: boolean;
ignore_throttled: boolean;
max_concurrent_shard_requests: any;
preference: any;
timeout: string | undefined;
index: any;
body: any;
}`
`ISearchRequestParams`

View file

@ -15,5 +15,5 @@ export interface IEsSearchRequest extends IKibanaSearchRequest
| Property | Type | Description |
| --- | --- | --- |
| [indexType](./kibana-plugin-plugins-data-public.iessearchrequest.indextype.md) | <code>string</code> | |
| [params](./kibana-plugin-plugins-data-public.iessearchrequest.params.md) | <code>SearchParams</code> | |
| [params](./kibana-plugin-plugins-data-public.iessearchrequest.params.md) | <code>ISearchRequestParams</code> | |

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
params: SearchParams;
params?: ISearchRequestParams;
```

View file

@ -7,12 +7,12 @@
<b>Signature:</b>
```typescript
export interface IEsSearchResponse<Hits = unknown> extends IKibanaSearchResponse
export interface IEsSearchResponse extends IKibanaSearchResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [rawResponse](./kibana-plugin-plugins-data-public.iessearchresponse.rawresponse.md) | <code>SearchResponse&lt;Hits&gt;</code> | |
| [rawResponse](./kibana-plugin-plugins-data-public.iessearchresponse.rawresponse.md) | <code>SearchResponse&lt;any&gt;</code> | |

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
rawResponse: SearchResponse<Hits>;
rawResponse: SearchResponse<any>;
```

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [IRequestTypesMap](./kibana-plugin-plugins-data-public.irequesttypesmap.md) &gt; [es](./kibana-plugin-plugins-data-public.irequesttypesmap.es.md)
## IRequestTypesMap.es property
<b>Signature:</b>
```typescript
[ES_SEARCH_STRATEGY]: IEsSearchRequest;
```

View file

@ -1,19 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [IRequestTypesMap](./kibana-plugin-plugins-data-public.irequesttypesmap.md)
## IRequestTypesMap interface
<b>Signature:</b>
```typescript
export interface IRequestTypesMap
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [es](./kibana-plugin-plugins-data-public.irequesttypesmap.es.md) | <code>IEsSearchRequest</code> | |
| [SYNC\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.irequesttypesmap.sync_search_strategy.md) | <code>ISyncSearchRequest</code> | |

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [IRequestTypesMap](./kibana-plugin-plugins-data-public.irequesttypesmap.md) &gt; [SYNC\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.irequesttypesmap.sync_search_strategy.md)
## IRequestTypesMap.SYNC\_SEARCH\_STRATEGY property
<b>Signature:</b>
```typescript
[SYNC_SEARCH_STRATEGY]: ISyncSearchRequest;
```

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [IResponseTypesMap](./kibana-plugin-plugins-data-public.iresponsetypesmap.md) &gt; [es](./kibana-plugin-plugins-data-public.iresponsetypesmap.es.md)
## IResponseTypesMap.es property
<b>Signature:</b>
```typescript
[ES_SEARCH_STRATEGY]: IEsSearchResponse;
```

View file

@ -1,19 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [IResponseTypesMap](./kibana-plugin-plugins-data-public.iresponsetypesmap.md)
## IResponseTypesMap interface
<b>Signature:</b>
```typescript
export interface IResponseTypesMap
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [es](./kibana-plugin-plugins-data-public.iresponsetypesmap.es.md) | <code>IEsSearchResponse</code> | |
| [SYNC\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.iresponsetypesmap.sync_search_strategy.md) | <code>IKibanaSearchResponse</code> | |

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [IResponseTypesMap](./kibana-plugin-plugins-data-public.iresponsetypesmap.md) &gt; [SYNC\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.iresponsetypesmap.sync_search_strategy.md)
## IResponseTypesMap.SYNC\_SEARCH\_STRATEGY property
<b>Signature:</b>
```typescript
[SYNC_SEARCH_STRATEGY]: IKibanaSearchResponse;
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
export declare type ISearch<T extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY> = (request: IRequestTypesMap[T], options?: ISearchOptions) => Observable<IResponseTypesMap[T]>;
export declare type ISearch = (request: IKibanaSearchRequest, options?: ISearchOptions) => Observable<IKibanaSearchResponse>;
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
export declare type ISearchGeneric = <T extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY>(request: IRequestTypesMap[T], options?: ISearchOptions, strategy?: T) => Observable<IResponseTypesMap[T]>;
export declare type ISearchGeneric = (request: IEsSearchRequest, options?: IStrategyOptions) => Observable<IEsSearchResponse>;
```

View file

@ -1,20 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ISearchStrategy](./kibana-plugin-plugins-data-public.isearchstrategy.md)
## ISearchStrategy interface
Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response.
<b>Signature:</b>
```typescript
export interface ISearchStrategy<T extends TStrategyTypes>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [search](./kibana-plugin-plugins-data-public.isearchstrategy.search.md) | <code>ISearch&lt;T&gt;</code> | |

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ISearchStrategy](./kibana-plugin-plugins-data-public.isearchstrategy.md) &gt; [search](./kibana-plugin-plugins-data-public.isearchstrategy.search.md)
## ISearchStrategy.search property
<b>Signature:</b>
```typescript
search: ISearch<T>;
```

View file

@ -1,18 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ISyncSearchRequest](./kibana-plugin-plugins-data-public.isyncsearchrequest.md)
## ISyncSearchRequest interface
<b>Signature:</b>
```typescript
export interface ISyncSearchRequest extends IKibanaSearchRequest
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [serverStrategy](./kibana-plugin-plugins-data-public.isyncsearchrequest.serverstrategy.md) | <code>string</code> | |

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ISyncSearchRequest](./kibana-plugin-plugins-data-public.isyncsearchrequest.md) &gt; [serverStrategy](./kibana-plugin-plugins-data-public.isyncsearchrequest.serverstrategy.md)
## ISyncSearchRequest.serverStrategy property
<b>Signature:</b>
```typescript
serverStrategy: string;
```

View file

@ -67,11 +67,7 @@
| [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) | |
| [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) | Use data plugin interface instead |
| [IndexPatternTypeMeta](./kibana-plugin-plugins-data-public.indexpatterntypemeta.md) | |
| [IRequestTypesMap](./kibana-plugin-plugins-data-public.irequesttypesmap.md) | |
| [IResponseTypesMap](./kibana-plugin-plugins-data-public.iresponsetypesmap.md) | |
| [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) | |
| [ISearchStrategy](./kibana-plugin-plugins-data-public.isearchstrategy.md) | Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. |
| [ISyncSearchRequest](./kibana-plugin-plugins-data-public.isyncsearchrequest.md) | |
| [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | |
| [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | |
| [Query](./kibana-plugin-plugins-data-public.query.md) | |
@ -83,6 +79,7 @@
| [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) | |
| [SavedQuery](./kibana-plugin-plugins-data-public.savedquery.md) | |
| [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | |
| [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | |
| [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | |
| [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* |
| [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* |
@ -118,7 +115,6 @@
| [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | |
| [search](./kibana-plugin-plugins-data-public.search.md) | |
| [SearchBar](./kibana-plugin-plugins-data-public.searchbar.md) | |
| [SYNC\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.sync_search_strategy.md) | |
| [syncQueryStateWithUrl](./kibana-plugin-plugins-data-public.syncquerystatewithurl.md) | Helper to setup syncing of global data with the URL |
| [UI\_SETTINGS](./kibana-plugin-plugins-data-public.ui_settings.md) | |

View file

@ -9,14 +9,13 @@ This class should be instantiated with a `requestTimeout` corresponding with how
<b>Signature:</b>
```typescript
constructor(toasts: ToastsStart, application: ApplicationStart, requestTimeout?: number | undefined);
constructor(deps: SearchInterceptorDeps, requestTimeout?: number | undefined);
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| toasts | <code>ToastsStart</code> | |
| application | <code>ApplicationStart</code> | |
| deps | <code>SearchInterceptorDeps</code> | |
| requestTimeout | <code>number &#124; undefined</code> | |

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [application](./kibana-plugin-plugins-data-public.searchinterceptor.application.md)
## SearchInterceptor.application property
<b>Signature:</b>
```typescript
protected readonly application: ApplicationStart;
```

View file

@ -1,11 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [toasts](./kibana-plugin-plugins-data-public.searchinterceptor.toasts.md)
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [deps](./kibana-plugin-plugins-data-public.searchinterceptor.deps.md)
## SearchInterceptor.toasts property
## SearchInterceptor.deps property
<b>Signature:</b>
```typescript
protected readonly toasts: ToastsStart;
protected readonly deps: SearchInterceptorDeps;
```

View file

@ -9,5 +9,5 @@ Returns an `Observable` over the current number of pending searches. This could
<b>Signature:</b>
```typescript
getPendingCount$: () => import("rxjs").Observable<number>;
getPendingCount$: () => Observable<number>;
```

View file

@ -14,20 +14,28 @@ export declare class SearchInterceptor
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(toasts, application, requestTimeout)](./kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md) | | This class should be instantiated with a <code>requestTimeout</code> corresponding with how many ms after requests are initiated that they should automatically cancel. |
| [(constructor)(deps, requestTimeout)](./kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md) | | This class should be instantiated with a <code>requestTimeout</code> corresponding with how many ms after requests are initiated that they should automatically cancel. |
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [abortController](./kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md) | | <code>AbortController</code> | <code>abortController</code> used to signal all searches to abort. |
| [application](./kibana-plugin-plugins-data-public.searchinterceptor.application.md) | | <code>ApplicationStart</code> | |
| [getPendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) | | <code>() =&gt; import(&quot;rxjs&quot;).Observable&lt;number&gt;</code> | Returns an <code>Observable</code> over the current number of pending searches. This could mean that one of the search requests is still in flight, or that it has only received partial responses. |
| [deps](./kibana-plugin-plugins-data-public.searchinterceptor.deps.md) | | <code>SearchInterceptorDeps</code> | |
| [getPendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) | | <code>() =&gt; Observable&lt;number&gt;</code> | Returns an <code>Observable</code> over the current number of pending searches. This could mean that one of the search requests is still in flight, or that it has only received partial responses. |
| [hideToast](./kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md) | | <code>() =&gt; void</code> | |
| [longRunningToast](./kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md) | | <code>Toast</code> | The current long-running toast (if there is one). |
| [pendingCount](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md) | | <code>number</code> | The number of pending search requests. |
| [pendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md) | | <code>BehaviorSubject&lt;number&gt;</code> | Observable that emits when the number of pending requests changes. |
| [requestTimeout](./kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md) | | <code>number &#124; undefined</code> | |
| [search](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | <code>(search: ISearchGeneric, request: IKibanaSearchRequest, options?: ISearchOptions &#124; undefined) =&gt; import(&quot;rxjs&quot;).Observable&lt;import(&quot;../../common/search&quot;).IEsSearchResponse&lt;unknown&gt;&gt;</code> | Searches using the given <code>search</code> method. Overrides the <code>AbortSignal</code> with one that will abort either when <code>cancelPending</code> is called, when the request times out, or when the original <code>AbortSignal</code> is aborted. Updates the <code>pendingCount</code> when the request is started/finalized. |
| [showToast](./kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md) | | <code>() =&gt; void</code> | |
| [timeoutSubscriptions](./kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md) | | <code>Set&lt;Subscription&gt;</code> | The subscriptions from scheduling the automatic timeout for each request. |
| [toasts](./kibana-plugin-plugins-data-public.searchinterceptor.toasts.md) | | <code>ToastsStart</code> | |
| [timeoutSubscriptions](./kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md) | | <code>Subscription</code> | The subscriptions from scheduling the automatic timeout for each request. |
## Methods
| Method | Modifiers | Description |
| --- | --- | --- |
| [runSearch(request, combinedSignal)](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) | | |
| [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given <code>search</code> method. Overrides the <code>AbortSignal</code> with one that will abort either when <code>cancelPending</code> is called, when the request times out, or when the original <code>AbortSignal</code> is aborted. Updates the <code>pendingCount</code> when the request is started/finalized. |
| [setupTimers(options)](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md) | | |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [pendingCount](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md)
## SearchInterceptor.pendingCount property
The number of pending search requests.
<b>Signature:</b>
```typescript
protected pendingCount: number;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [pendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md)
## SearchInterceptor.pendingCount$ property
Observable that emits when the number of pending requests changes.
<b>Signature:</b>
```typescript
protected pendingCount$: BehaviorSubject<number>;
```

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [runSearch](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md)
## SearchInterceptor.runSearch() method
<b>Signature:</b>
```typescript
protected runSearch(request: IEsSearchRequest, combinedSignal: AbortSignal): Observable<IEsSearchResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| request | <code>IEsSearchRequest</code> | |
| combinedSignal | <code>AbortSignal</code> | |
<b>Returns:</b>
`Observable<IEsSearchResponse>`

View file

@ -2,12 +2,24 @@
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [search](./kibana-plugin-plugins-data-public.searchinterceptor.search.md)
## SearchInterceptor.search property
## SearchInterceptor.search() method
Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort either when `cancelPending` is called, when the request times out, or when the original `AbortSignal` is aborted. Updates the `pendingCount` when the request is started/finalized.
<b>Signature:</b>
```typescript
search: (search: ISearchGeneric, request: IKibanaSearchRequest, options?: ISearchOptions | undefined) => import("rxjs").Observable<import("../../common/search").IEsSearchResponse<unknown>>;
search(request: IEsSearchRequest, options?: ISearchOptions): Observable<IEsSearchResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| request | <code>IEsSearchRequest</code> | |
| options | <code>ISearchOptions</code> | |
<b>Returns:</b>
`Observable<IEsSearchResponse>`

View file

@ -0,0 +1,28 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [setupTimers](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md)
## SearchInterceptor.setupTimers() method
<b>Signature:</b>
```typescript
protected setupTimers(options?: ISearchOptions): {
combinedSignal: AbortSignal;
cleanup: () => void;
};
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| options | <code>ISearchOptions</code> | |
<b>Returns:</b>
`{
combinedSignal: AbortSignal;
cleanup: () => void;
}`

View file

@ -9,5 +9,5 @@ The subscriptions from scheduling the automatic timeout for each request.
<b>Signature:</b>
```typescript
protected timeoutSubscriptions: Set<Subscription>;
protected timeoutSubscriptions: Subscription;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) &gt; [application](./kibana-plugin-plugins-data-public.searchinterceptordeps.application.md)
## SearchInterceptorDeps.application property
<b>Signature:</b>
```typescript
application: ApplicationStart;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) &gt; [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md)
## SearchInterceptorDeps.http property
<b>Signature:</b>
```typescript
http: CoreStart['http'];
```

View file

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md)
## SearchInterceptorDeps interface
<b>Signature:</b>
```typescript
export interface SearchInterceptorDeps
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [application](./kibana-plugin-plugins-data-public.searchinterceptordeps.application.md) | <code>ApplicationStart</code> | |
| [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | <code>CoreStart['http']</code> | |
| [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | <code>ToastsStart</code> | |
| [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | <code>CoreStart['uiSettings']</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) &gt; [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md)
## SearchInterceptorDeps.toasts property
<b>Signature:</b>
```typescript
toasts: ToastsStart;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) &gt; [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md)
## SearchInterceptorDeps.uiSettings property
<b>Signature:</b>
```typescript
uiSettings: CoreStart['uiSettings'];
```

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SYNC\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.sync_search_strategy.md)
## SYNC\_SEARCH\_STRATEGY variable
<b>Signature:</b>
```typescript
SYNC_SEARCH_STRATEGY = "SYNC_SEARCH_STRATEGY"
```

View file

@ -1,8 +0,0 @@
## Demo search strategy
This example registers a custom search strategy that simply takes a name string in the request and returns the
string `Hello {name}`
To see the demo search strategy in action, navigate to the `Search explorer` app.
To run these examples, use the command `yarn start --run-examples`.

View file

@ -1,40 +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 { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../src/plugins/data/public';
export const DEMO_SEARCH_STRATEGY = 'DEMO_SEARCH_STRATEGY';
export const ASYNC_DEMO_SEARCH_STRATEGY = 'ASYNC_DEMO_SEARCH_STRATEGY';
export interface IDemoRequest extends IKibanaSearchRequest {
mood: string | 'sad' | 'happy';
name: string;
}
export interface IDemoResponse extends IKibanaSearchResponse {
greeting: string;
}
export interface IAsyncDemoRequest extends IKibanaSearchRequest {
fibonacciNumbers: number;
}
export interface IAsyncDemoResponse extends IKibanaSearchResponse {
fibonacciSequence: number[];
}

View file

@ -1,10 +0,0 @@
{
"id": "demoSearch",
"version": "0.0.1",
"kibanaVersion": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["data"],
"optionalPlugins": [],
"extraPublicDirs": ["common"]
}

View file

@ -1,43 +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 { Observable, from } from 'rxjs';
import { CoreSetup } from 'kibana/public';
import { flatMap } from 'rxjs/operators';
import { ISearch } from '../../../src/plugins/data/public';
import { ASYNC_SEARCH_STRATEGY } from '../../../x-pack/plugins/data_enhanced/public';
import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoResponse } from '../common';
import { DemoDataSearchStartDependencies } from './types';
export function asyncDemoClientSearchStrategyProvider(core: CoreSetup) {
const search: ISearch<typeof ASYNC_DEMO_SEARCH_STRATEGY> = (request, options) => {
return from(core.getStartServices()).pipe(
flatMap((startServices) => {
const asyncStrategy = (startServices[1] as DemoDataSearchStartDependencies).data.search.getSearchStrategy(
ASYNC_SEARCH_STRATEGY
);
return asyncStrategy.search(
{ ...request, serverStrategy: ASYNC_DEMO_SEARCH_STRATEGY },
options
) as Observable<IAsyncDemoResponse>;
})
);
};
return { search };
}

View file

@ -1,65 +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 { Observable, from } from 'rxjs';
import { flatMap } from 'rxjs/operators';
import { CoreSetup } from 'kibana/public';
import { ISearch, SYNC_SEARCH_STRATEGY } from '../../../src/plugins/data/public';
import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common';
import { DemoDataSearchStartDependencies } from './types';
/**
* This demo search strategy provider simply provides a shortcut for calling the DEMO_SEARCH_STRATEGY
* on the server side, without users having to pass it in explicitly, and it takes advantage of the
* already registered SYNC_SEARCH_STRATEGY that exists on the client.
*
* so instead of callers having to do:
*
* ```
* data.search.search(
* { ...request, serverStrategy: DEMO_SEARCH_STRATEGY },
* options,
* SYNC_SEARCH_STRATEGY
* ) as Observable<IDemoResponse>,
*```
* They can instead just do
*
* ```
* data.search.search(request, options, DEMO_SEARCH_STRATEGY);
* ```
*
* and are ensured type safety in regard to the request and response objects.
*/
export function demoClientSearchStrategyProvider(core: CoreSetup) {
const search: ISearch<typeof DEMO_SEARCH_STRATEGY> = (request, options) => {
return from(core.getStartServices()).pipe(
flatMap((startServices) => {
const syncStrategy = (startServices[1] as DemoDataSearchStartDependencies).data.search.getSearchStrategy(
SYNC_SEARCH_STRATEGY
);
return syncStrategy.search(
{ ...request, serverStrategy: DEMO_SEARCH_STRATEGY },
options
) as Observable<IDemoResponse>;
})
);
};
return { search };
}

View file

@ -1,26 +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 { PluginInitializer } from 'kibana/public';
import { DemoDataPlugin } from './plugin';
export { DEMO_SEARCH_STRATEGY } from '../common';
export const plugin: PluginInitializer<void, void> = () => new DemoDataPlugin();

View file

@ -1,65 +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 { Plugin, CoreSetup } from '../../../src/core/public';
import {
DEMO_SEARCH_STRATEGY,
IDemoRequest,
IDemoResponse,
ASYNC_DEMO_SEARCH_STRATEGY,
IAsyncDemoRequest,
IAsyncDemoResponse,
} from '../common';
import { demoClientSearchStrategyProvider } from './demo_search_strategy';
import { asyncDemoClientSearchStrategyProvider } from './async_demo_search_strategy';
import { DemoDataSearchSetupDependencies, DemoDataSearchStartDependencies } from './types';
/**
* Add the typescript mappings for our search strategy to the request and
* response types. This allows typescript to require the right shapes if
* making the call:
* const response = context.search.search(request, {}, DEMO_SEARCH_STRATEGY);
*
* If the caller does not pass in the right `request` shape, typescript will
* complain. The caller will also get a typed response.
*/
declare module '../../../src/plugins/data/public' {
export interface IRequestTypesMap {
[DEMO_SEARCH_STRATEGY]: IDemoRequest;
[ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoRequest;
}
export interface IResponseTypesMap {
[DEMO_SEARCH_STRATEGY]: IDemoResponse;
[ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoResponse;
}
}
export class DemoDataPlugin
implements Plugin<void, void, DemoDataSearchSetupDependencies, DemoDataSearchStartDependencies> {
public setup(core: CoreSetup, { data }: DemoDataSearchSetupDependencies) {
const demoClientSearchStrategy = demoClientSearchStrategyProvider(core);
const asyncDemoClientSearchStrategy = asyncDemoClientSearchStrategyProvider(core);
data.search.registerSearchStrategy(DEMO_SEARCH_STRATEGY, demoClientSearchStrategy);
data.search.registerSearchStrategy(ASYNC_DEMO_SEARCH_STRATEGY, asyncDemoClientSearchStrategy);
}
public start() {}
public stop() {}
}

View file

@ -1,28 +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 { DataPublicPluginStart, DataPublicPluginSetup } from '../../../src/plugins/data/public';
export interface DemoDataSearchSetupDependencies {
data: DataPublicPluginSetup;
}
export interface DemoDataSearchStartDependencies {
data: DataPublicPluginStart;
}

View file

@ -1,62 +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 { ISearchStrategy } from '../../../src/plugins/data/server';
import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoRequest } from '../common';
export const asyncDemoSearchStrategyProvider = (): ISearchStrategy<
typeof ASYNC_DEMO_SEARCH_STRATEGY
> => {
function getFibonacciSequence(n = 0) {
const beginning = [0, 1].slice(0, n);
return Array(Math.max(0, n))
.fill(null)
.reduce((sequence, value, i) => {
if (i < 2) return sequence;
return [...sequence, sequence[i - 1] + sequence[i - 2]];
}, beginning);
}
const generateId = (() => {
let id = 0;
return () => `${id++}`;
})();
const loadedMap = new Map<string, number>();
const totalMap = new Map<string, number>();
return {
search: async (context, request: IAsyncDemoRequest) => {
const id = request.id ?? generateId();
const loaded = (loadedMap.get(id) ?? 0) + 1;
loadedMap.set(id, loaded);
const total = request.fibonacciNumbers ?? totalMap.get(id);
totalMap.set(id, total);
const fibonacciSequence = getFibonacciSequence(loaded);
return { id, total, loaded, fibonacciSequence };
},
cancel: async (context, id) => {
loadedMap.delete(id);
totalMap.delete(id);
},
};
};

View file

@ -1,34 +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 { ISearchStrategy } from '../../../src/plugins/data/server';
import { DEMO_SEARCH_STRATEGY, IDemoRequest } from '../common';
export const demoSearchStrategyProvider = (): ISearchStrategy<typeof DEMO_SEARCH_STRATEGY> => {
return {
search: (context, request: IDemoRequest) => {
return Promise.resolve({
greeting:
request.mood === 'happy'
? `Lovely to meet you, ${request.name}`
: `Hope you feel better, ${request.name}`,
});
},
};
};

View file

@ -1,22 +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 { DemoDataPlugin } from './plugin';
export const plugin = () => new DemoDataPlugin();

View file

@ -1,71 +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 { Plugin, CoreSetup } from 'kibana/server';
import { PluginSetup as DataPluginSetup } from 'src/plugins/data/server';
import { demoSearchStrategyProvider } from './demo_search_strategy';
import {
DEMO_SEARCH_STRATEGY,
IDemoRequest,
IDemoResponse,
ASYNC_DEMO_SEARCH_STRATEGY,
IAsyncDemoRequest,
IAsyncDemoResponse,
} from '../common';
import { asyncDemoSearchStrategyProvider } from './async_demo_search_strategy';
interface IDemoSearchExplorerDeps {
data: DataPluginSetup;
}
/**
* Add the typescript mappings for our search strategy to the request and
* response types. This allows typescript to require the right shapes if
* making the call:
* const response = context.search.search(request, DEMO_SEARCH_STRATEGY);
*
* If the caller does not pass in the right `request` shape, typescript will
* complain. The caller will also get a typed response.
*/
declare module '../../../src/plugins/data/server' {
export interface IRequestTypesMap {
[DEMO_SEARCH_STRATEGY]: IDemoRequest;
[ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoRequest;
}
export interface IResponseTypesMap {
[DEMO_SEARCH_STRATEGY]: IDemoResponse;
[ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoResponse;
}
}
export class DemoDataPlugin implements Plugin<void, void, IDemoSearchExplorerDeps> {
constructor() {}
public setup(core: CoreSetup, deps: IDemoSearchExplorerDeps) {
deps.data.search.registerSearchStrategy(DEMO_SEARCH_STRATEGY, demoSearchStrategyProvider());
deps.data.search.registerSearchStrategy(
ASYNC_DEMO_SEARCH_STRATEGY,
asyncDemoSearchStrategyProvider()
);
}
public start() {}
public stop() {}
}

View file

@ -1,16 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"common/**/*.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../typings/**/*"
],
"exclude": []
}

View file

@ -1,8 +0,0 @@
## Search explorer
This example search explorer app shows how to use different search strategies in order to retrieve data.
One demo uses the built in elasticsearch search strategy, and runs a search against data in elasticsearch. The
other demo uses the custom demo search strategy, a custom search strategy registerd inside the [demo_search plugin](../demo_search).
To run this example, use the command `yarn start --run-examples`.

View file

@ -1,9 +0,0 @@
{
"id": "searchExplorer",
"version": "0.0.1",
"kibanaVersion": "kibana",
"server": false,
"ui": true,
"requiredPlugins": ["data", "demoSearch", "developerExamples"],
"optionalPlugins": []
}

View file

@ -1,133 +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 React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from 'react-router-dom';
import {
EuiPage,
EuiPageSideBar,
// @ts-ignore
EuiSideNav,
} from '@elastic/eui';
import { AppMountParameters, CoreStart } from '../../../src/core/public';
import { EsSearchTest } from './es_strategy';
import { Page } from './page';
import { DemoStrategy } from './demo_strategy';
import { AsyncDemoStrategy } from './async_demo_strategy';
import { DocumentationPage } from './documentation';
import { SearchApiPage } from './search_api';
import { AppPluginStartDependencies, SearchBarComponentParams } from './types';
const Home = () => <DocumentationPage />;
interface PageDef {
title: string;
id: string;
component: React.ReactNode;
}
type NavProps = RouteComponentProps & {
navigateToApp: CoreStart['application']['navigateToApp'];
pages: PageDef[];
};
const Nav = withRouter(({ history, navigateToApp, pages }: NavProps) => {
const navItems = pages.map((page) => ({
id: page.id,
name: page.title,
onClick: () => history.push(`/${page.id}`),
'data-test-subj': page.id,
}));
return (
<EuiSideNav
items={[
{
name: 'Search explorer',
id: 'home',
items: [...navItems],
},
]}
/>
);
});
const buildPage = (page: PageDef) => <Page title={page.title}>{page.component}</Page>;
const SearchApp = ({ basename, data, application }: SearchBarComponentParams) => {
const pages: PageDef[] = [
{
id: 'home',
title: 'Home',
component: <Home />,
},
{
title: 'Search API',
id: 'searchAPI',
component: <SearchApiPage />,
},
{
title: 'ES search strategy',
id: 'esSearch',
component: <EsSearchTest search={data.search.search} />,
},
{
title: 'Demo search strategy',
id: 'demoSearch',
component: <DemoStrategy search={data.search.search} />,
},
{
title: 'Async demo search strategy',
id: 'asyncDemoSearch',
component: <AsyncDemoStrategy search={data.search.search} />,
},
];
const routes = pages.map((page, i) => (
<Route key={i} path={`/${page.id}`} render={(props) => buildPage(page)} />
));
return (
<Router basename={basename}>
<EuiPage>
<EuiPageSideBar>
<Nav navigateToApp={application.navigateToApp} pages={pages} />
</EuiPageSideBar>
<Route path="/" exact component={Home} />
{routes}
</EuiPage>
</Router>
);
};
export const renderApp = (
coreStart: CoreStart,
deps: AppPluginStartDependencies,
{ appBasePath, element }: AppMountParameters
) => {
ReactDOM.render(
<SearchApp basename={appBasePath} data={deps.data} application={coreStart.application} />,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -1,123 +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 React from 'react';
import {
EuiPageContentBody,
EuiFormRow,
EuiFlexItem,
EuiFlexGroup,
EuiFieldNumber,
} from '@elastic/eui';
import { ISearchGeneric } from '../../../src/plugins/data/public';
import { DoSearch } from './do_search';
import { GuideSection } from './guide_section';
import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoRequest } from '../../demo_search/common';
// @ts-ignore
import demoStrategyServerProvider from '!!raw-loader!./../../demo_search/server/async_demo_search_strategy';
// @ts-ignore
import demoStrategyPublicProvider from '!!raw-loader!./../../demo_search/public/async_demo_search_strategy';
// @ts-ignore
import demoStrategyServerPlugin from '!!raw-loader!./../../demo_search/server/plugin';
// @ts-ignore
import demoStrategyPublicPlugin from '!!raw-loader!./../../demo_search/public/plugin';
interface Props {
search: ISearchGeneric;
}
interface State {
searching: boolean;
fibonacciNumbers: number;
changes: boolean;
error?: any;
}
export class AsyncDemoStrategy extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
searching: false,
changes: false,
fibonacciNumbers: 5,
};
}
renderDemo = () => {
const request: IAsyncDemoRequest = {
fibonacciNumbers: this.state.fibonacciNumbers,
};
return (
<React.Fragment>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow label="How many Fibonacci numbers to generate?">
<EuiFieldNumber
value={this.state.fibonacciNumbers}
onChange={(e) => this.setState({ fibonacciNumbers: parseFloat(e.target.value) })}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<DoSearch
request={request}
strategy={ASYNC_DEMO_SEARCH_STRATEGY}
search={(signal: AbortSignal) =>
this.props.search(request, { signal }, ASYNC_DEMO_SEARCH_STRATEGY)
}
/>
</React.Fragment>
);
};
render() {
return (
<EuiPageContentBody>
<GuideSection
codeSections={[
{
title: 'Public',
code: [
{ description: 'plugin.ts', snippet: demoStrategyPublicPlugin },
{
description: 'async_demo_search_strategy.ts',
snippet: demoStrategyPublicProvider,
},
],
},
{
title: 'Server',
code: [
{ description: 'plugin.ts', snippet: demoStrategyServerPlugin },
{
description: 'async_demo_search_strategy.ts',
snippet: demoStrategyServerProvider,
},
],
},
]}
demo={this.renderDemo()}
/>
</EuiPageContentBody>
);
}
}

View file

@ -1,134 +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 React from 'react';
import {
EuiPageContentBody,
EuiFormRow,
EuiFlexItem,
EuiFlexGroup,
EuiFieldText,
} from '@elastic/eui';
import { ISearchGeneric } from '../../../src/plugins/data/public';
import { DoSearch } from './do_search';
import { GuideSection } from './guide_section';
import { DEMO_SEARCH_STRATEGY } from '../../demo_search/public';
import { IDemoResponse, IDemoRequest } from '../../demo_search/common';
// @ts-ignore
import doSearch from '!!raw-loader!./do_search.tsx';
// @ts-ignore
import demoStrategyServerProvider from '!!raw-loader!./../../demo_search/server/demo_search_strategy';
// @ts-ignore
import demoStrategyPublicProvider from '!!raw-loader!./../../demo_search/public/demo_search_strategy';
// @ts-ignore
import demoStrategyServerPlugin from '!!raw-loader!./../../demo_search/server/plugin';
// @ts-ignore
import demoStrategyPublicPlugin from '!!raw-loader!./../../demo_search/public/plugin';
interface Props {
search: ISearchGeneric;
}
interface State {
results?: IDemoResponse;
searching: boolean;
name: string;
mood: string;
changes: boolean;
error?: any;
}
export class DemoStrategy extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
searching: false,
changes: false,
name: 'Molly',
mood: 'happy',
};
}
renderDemo = () => {
const request: IDemoRequest = {
name: this.state.name,
mood: this.state.mood,
};
return (
<React.Fragment>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow label="What is your name?">
<EuiFieldText
value={this.state.name}
onChange={(e) => this.setState({ name: e.target.value })}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow label="How are you feeling today?">
<EuiFieldText
value={this.state.mood}
onChange={(e) => this.setState({ mood: e.target.value })}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<DoSearch
request={request}
strategy={DEMO_SEARCH_STRATEGY}
search={(signal: AbortSignal) =>
this.props.search(request, { signal }, DEMO_SEARCH_STRATEGY)
}
/>
</React.Fragment>
);
};
render() {
return (
<EuiPageContentBody>
<GuideSection
codeSections={[
{
title: 'Public',
code: [
{ description: 'plugin.ts', snippet: demoStrategyPublicPlugin },
{ description: 'demo_search_strategy.ts', snippet: demoStrategyPublicProvider },
],
},
{
title: 'Server',
code: [
{ description: 'plugin.ts', snippet: demoStrategyServerPlugin },
{ description: 'demo_search_strategy.ts', snippet: demoStrategyServerProvider },
],
},
]}
demo={this.renderDemo()}
/>
</EuiPageContentBody>
);
}
}

View file

@ -1,138 +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 React from 'react';
import { EuiButton, EuiCodeBlock, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui';
import { EuiProgress } from '@elastic/eui';
import { Observable } from 'rxjs';
import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../../src/plugins/data/public';
interface Props {
request: IKibanaSearchRequest;
strategy?: string;
search: (signal: AbortSignal) => Observable<IKibanaSearchResponse>;
}
interface State {
searching: boolean;
response?: IKibanaSearchResponse;
error?: any;
}
export class DoSearch extends React.Component<Props, State> {
private abortController?: AbortController;
constructor(props: Props) {
super(props);
this.state = {
searching: false,
response: undefined,
};
}
search = async () => {
if (this.state.searching && this.abortController) {
this.abortController.abort();
}
this.setState({
searching: true,
response: undefined,
error: undefined,
});
this.abortController = new AbortController();
this.props.search(this.abortController.signal).subscribe(
(response) => {
this.setState({ response, error: undefined });
},
(error) => {
this.setState({ error, searching: false, response: undefined });
},
() => {
this.setState({ searching: false, error: undefined });
}
);
};
cancel = () => {
if (this.abortController) {
this.abortController.abort();
}
};
render() {
let responseStr = this.state.error
? JSON.stringify(this.state.error, null, 2)
: JSON.stringify(this.state.response, null, 2);
responseStr = responseStr ? responseStr.substring(0, 2000) : '';
const requestStr = JSON.stringify(this.props.request, null, 2);
return (
<React.Fragment>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton data-test-subj="doSearch" onClick={this.search}>
Search
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton disabled={!this.state.searching} onClick={this.cancel}>
Cancel
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem>
<EuiText>Request:</EuiText>
<EuiCodeBlock language="json" fontSize="m" paddingSize="m" color="dark">
{this.props.strategy
? `data.search
(
${requestStr},
"${this.props.strategy}"
)`
: `data.search
(
${requestStr}
)`}
</EuiCodeBlock>
</EuiFlexItem>
<EuiFlexItem>
<EuiText>Response:</EuiText>
<EuiProgress
value={this.state.response?.loaded ?? 0}
max={this.state.response?.total ?? 0}
/>
<EuiCodeBlock
language="json"
fontSize="m"
paddingSize="m"
color="dark"
data-test-subj="response"
>
{responseStr}
</EuiCodeBlock>
</EuiFlexItem>
</EuiFlexGroup>
</React.Fragment>
);
}
}

View file

@ -1,102 +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 React from 'react';
import {
EuiText,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
EuiListGroup,
} from '@elastic/eui';
export const DocumentationPage = () => (
<EuiPageBody data-test-subj="dataPluginExplorerHome">
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Welcome to the data plugin portal!</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2>Documentation links</h2>
</EuiTitle>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiText>
<h2>Search Services</h2>
<ul>
<li>Provide an abstraction on top of advanced query settings</li>
<li>
Providing an abstraction layer for query cancellation semantics allows us to avoid
wide-spread code changes when ES API changes, allows us to provide a minimum set of
useful functionality first, and allows us to continue adding more advanced features
down the road
</li>
<li>Provide a clean separation of OSS and commercial search strategies.</li>
</ul>
<h2>Extensibility</h2>
<p>
Plugins can register or use different client side, and server side{' '}
<i>search strategies</i>. Search strategies can take advantage of other search stratgies
already registered. For example, the `DEMO_SEARCH_STRATEGY` uses the
`ASYNC_SEARCH_STRATEGY` which uses the `SYNC_SEARCH_STRATEGY`
</p>
<h2>References</h2>
<EuiListGroup
listItems={[
{
label: 'Design document',
href:
'https://docs.google.com/document/d/1ROLq29V1TeLux4ASQIJNllRGkv-xa5XIE72gTU6u16Q/edit#heading=h.3aa9ppqzkvdd',
iconType: 'document',
size: 's',
},
{
label: 'Roadmap',
href: 'https://github.com/elastic/kibana/issues/44661',
iconType: 'logoGithub',
size: 's',
},
{
label: 'Data access API issue',
href: 'https://github.com/elastic/kibana/issues/43371',
iconType: 'logoGithub',
size: 's',
},
]}
/>
</EuiText>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);

View file

@ -1,136 +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 React from 'react';
import {
EuiPageContentBody,
EuiFieldText,
EuiFormRow,
EuiFlexItem,
EuiFlexGroup,
} from '@elastic/eui';
import {
ISearchGeneric,
IEsSearchResponse,
IEsSearchRequest,
} from '../../../src/plugins/data/public';
import { DoSearch } from './do_search';
import { GuideSection } from './guide_section';
// @ts-ignore
import serverStrategy from '!!raw-loader!./../../../src/plugins/data/server/search/es_search/es_search_strategy';
// @ts-ignore
import publicStrategy from '!!raw-loader!./../../../src/plugins/data/public/search/es_search/es_search_strategy';
interface Props {
search: ISearchGeneric;
}
interface State {
query: string;
results?: IEsSearchResponse;
index: string;
searching: boolean;
request: IEsSearchRequest;
strategy?: string;
changes: boolean;
error?: any;
}
export class EsSearchTest extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
query: '*',
index: '*',
searching: false,
request: this.getRequest({ index: '*', query: '*' }),
changes: false,
};
}
getRequest({ index, query }: { index: string; query: string }): IEsSearchRequest {
return {
debug: true,
params: {
index,
body: {
query: {
query_string: {
query,
},
},
},
},
};
}
renderDemo() {
const request: IEsSearchRequest = this.getRequest(this.state);
return (
<React.Fragment>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow label="Index pattern">
<EuiFieldText
value={this.state.index}
onChange={(e) => this.setState({ index: e.target.value, changes: true })}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow label="Query string query">
<EuiFieldText
value={this.state.query}
onChange={(e) => this.setState({ query: e.target.value, changes: true })}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<DoSearch
request={request}
search={(signal: AbortSignal) => this.props.search(request, { signal })}
/>
</React.Fragment>
);
}
render() {
return (
<EuiPageContentBody>
<GuideSection
codeSections={[
{
title: 'Public',
code: [{ description: 'es_search_strategy.ts', snippet: publicStrategy }],
},
{
title: 'Server',
code: [{ description: 'es_search_strategy.ts', snippet: serverStrategy }],
},
]}
demo={this.renderDemo()}
/>
</EuiPageContentBody>
);
}
}

View file

@ -1,137 +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 React from 'react';
import { EuiTab, EuiTabs, EuiCodeBlock } from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
import { EuiHorizontalRule } from '@elastic/eui';
export interface CodeSection {
title: string;
code: Array<{ description?: string; snippet: string }> | string;
}
interface Props {
demo?: React.ReactNode;
codeSections?: CodeSection[];
}
interface State {
selectedTab: string;
}
export class GuideSection extends React.Component<Props, State> {
private tabs: Array<{ name: string; displayName: string }>;
constructor(props: Props) {
super(props);
if (!props.demo && !props.codeSections) {
throw new Error('Must supply either demo or code sections');
}
if (props.demo) {
this.tabs = [
{
name: 'demo',
displayName: 'Demo',
},
];
} else {
this.tabs = [];
}
if (props.codeSections) {
props.codeSections.forEach((section) => {
this.tabs.push({
name: section.title,
displayName: section.title,
});
});
}
this.state = {
selectedTab: this.tabs[0].name,
};
}
onSelectedTabChanged = (selectedTab: string) => {
this.setState({
selectedTab,
});
};
renderTabs() {
return this.tabs.map((tab) => (
<EuiTab
onClick={() => this.onSelectedTabChanged(tab.name)}
isSelected={tab.name === this.state.selectedTab}
key={tab.name}
>
{tab.displayName}
</EuiTab>
));
}
removeLicenseBlock(code: string) {
return code.replace(/\/\*[\w\'\s\r\n\*\.\,\(\)\"\;\:\/\-]*\s*\//m, '');
}
renderCodeBlocks() {
if (!this.props.codeSections) {
return undefined;
}
const section = this.props.codeSections.find((s) => s.title === this.state.selectedTab);
if (!section) {
throw new Error('No section named ' + this.state.selectedTab);
}
const code = section.code;
if (typeof code === 'string') {
return <EuiCodeBlock language="ts">{this.removeLicenseBlock(code)}</EuiCodeBlock>;
}
return code.map((codeBlock, i) => (
<React.Fragment key={i}>
<EuiSpacer />
<h3>{codeBlock.description}</h3>
<EuiCodeBlock language="ts">{this.removeLicenseBlock(codeBlock.snippet)}</EuiCodeBlock>
<EuiHorizontalRule />
</React.Fragment>
));
}
renderContent() {
if (this.state.selectedTab === 'demo') {
return this.props.demo;
} else if (this.props.codeSections) {
return this.renderCodeBlocks();
}
}
render() {
return (
<React.Fragment>
<EuiTabs>{this.renderTabs()}</EuiTabs>
{this.renderContent()}
</React.Fragment>
);
}
}

View file

@ -1,22 +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 { SearchExplorerPlugin } from './plugin';
export const plugin = () => new SearchExplorerPlugin();

View file

@ -1,51 +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 React from 'react';
import {
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
} from '@elastic/eui';
interface PageProps {
title: string;
children: React.ReactNode;
}
export function Page({ title, children }: PageProps) {
return (
<EuiPageBody data-test-subj="searchTestPage">
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>{title}</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>{children}</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
}

View file

@ -1,65 +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 { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public';
import { AppPluginStartDependencies } from './types';
import { DeveloperExamplesSetup } from '../../developer_examples/public';
interface SetupDeps {
developerExamples: DeveloperExamplesSetup;
}
export class SearchExplorerPlugin implements Plugin {
public setup(
core: CoreSetup<AppPluginStartDependencies, void>,
{ developerExamples }: SetupDeps
) {
core.application.register({
id: 'searchExplorer',
title: 'Search Explorer',
navLinkStatus: AppNavLinkStatus.hidden,
async mount(params: AppMountParameters) {
const [coreStart, depsStart] = await core.getStartServices();
const { renderApp } = await import('./application');
return renderApp(coreStart, depsStart, params);
},
});
developerExamples.register({
appId: 'searchExplorer',
title: 'Data search strategy services',
description: `Data search services can be used to query Elasticsearch in away that supports background search
and partial results, when available. It also automatically incorporates settings such as requestTimeout and includeFrozen.
Use the provided ES search strategy, or register your own.
`,
links: [
{
label: 'README',
href:
'https://github.com/elastic/kibana/blob/master/src/plugins/data/public/search/README.md',
iconType: 'logoGithub',
size: 's',
target: '_blank',
},
],
});
}
public start() {}
public stop() {}
}

View file

@ -1,56 +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 React from 'react';
import { GuideSection } from './guide_section';
// @ts-ignore
import publicSearch from '!!raw-loader!./../../../src/plugins/data/public/search/i_search';
// @ts-ignore
import publicPlugin from '!!raw-loader!./../../../src/plugins/data/public/search/search_service';
// @ts-ignore
import serverPlugin from '!!raw-loader!./../../../src/plugins/data/server/search/search_service';
export const SearchApiPage = () => (
<GuideSection
codeSections={[
{
title: 'Public',
code: [
{
description: 'search_service.ts',
snippet: publicPlugin,
},
{
description: 'i_search',
snippet: publicSearch,
},
],
},
{
title: 'Server',
code: [
{
description: 'search_service.ts',
snippet: serverPlugin,
},
],
},
]}
/>
);

View file

@ -1,31 +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 { CoreStart } from 'kibana/public';
import { DataPublicPluginStart } from '../../../src/plugins/data/public';
export interface AppPluginStartDependencies {
data: DataPublicPluginStart;
}
export interface SearchBarComponentParams {
application: CoreStart['application'];
basename: string;
data: DataPublicPluginStart;
}

View file

@ -1,15 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../typings/**/*",
],
"exclude": []
}

View file

@ -17,4 +17,9 @@
* under the License.
*/
export { IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY } from './types';
export {
ISearchRequestParams,
IEsSearchRequest,
IEsSearchResponse,
ES_SEARCH_STRATEGY,
} from './types';

View file

@ -21,11 +21,15 @@ import { IKibanaSearchRequest, IKibanaSearchResponse } from '../types';
export const ES_SEARCH_STRATEGY = 'es';
export type ISearchRequestParams = {
trackTotalHits?: boolean;
} & SearchParams;
export interface IEsSearchRequest extends IKibanaSearchRequest {
params: SearchParams;
params?: ISearchRequestParams;
indexType?: string;
}
export interface IEsSearchResponse<Hits = unknown> extends IKibanaSearchResponse {
rawResponse: SearchResponse<Hits>;
export interface IEsSearchResponse extends IKibanaSearchResponse {
rawResponse: SearchResponse<any>;
}

View file

@ -23,4 +23,9 @@ export { IKibanaSearchResponse, IKibanaSearchRequest } from './types';
export const DEFAULT_SEARCH_STRATEGY = ES_SEARCH_STRATEGY;
export { IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY } from './es_search';
export {
IEsSearchRequest,
IEsSearchResponse,
ES_SEARCH_STRATEGY,
ISearchRequestParams,
} from './es_search';

View file

@ -38,31 +38,10 @@ describe('AbortUtils', () => {
});
describe('toPromise', () => {
describe('resolves', () => {
test('should not resolve if the signal does not abort', async () => {
const controller = new AbortController();
const promise = toPromise(controller.signal);
const whenResolved = jest.fn();
promise.then(whenResolved);
await flushPromises();
expect(whenResolved).not.toBeCalled();
});
test('should resolve if the signal does abort', async () => {
const controller = new AbortController();
const promise = toPromise(controller.signal);
const whenResolved = jest.fn();
promise.then(whenResolved);
controller.abort();
await flushPromises();
expect(whenResolved).toBeCalled();
});
});
describe('rejects', () => {
test('should not reject if the signal does not abort', async () => {
const controller = new AbortController();
const promise = toPromise(controller.signal, true);
const promise = toPromise(controller.signal);
const whenRejected = jest.fn();
promise.catch(whenRejected);
await flushPromises();
@ -71,12 +50,13 @@ describe('AbortUtils', () => {
test('should reject if the signal does abort', async () => {
const controller = new AbortController();
const promise = toPromise(controller.signal, true);
const promise = toPromise(controller.signal);
const whenRejected = jest.fn();
promise.catch(whenRejected);
controller.abort();
await flushPromises();
expect(whenRejected).toBeCalled();
expect(whenRejected.mock.calls[0][0]).toBeInstanceOf(AbortError);
});
});
});
@ -110,5 +90,13 @@ describe('AbortUtils', () => {
await flushPromises();
expect(signal.aborted).toBe(true);
});
test('should be aborted if any of the signals is already aborted', async () => {
const controller1 = new AbortController();
const controller2 = new AbortController();
controller1.abort();
const signal = getCombinedSignal([controller1.signal, controller2.signal]);
expect(signal.aborted).toBe(true);
});
});
});

View file

@ -35,24 +35,16 @@ export class AbortError extends Error {
* with any other expected errors (or completions).
*
* @param signal The `AbortSignal` to generate the `Promise` from
* @param shouldReject If `false`, the promise will be resolved, otherwise it will be rejected
*/
export function toPromise(signal: AbortSignal, shouldReject?: false): Promise<undefined | Event>;
export function toPromise(signal: AbortSignal, shouldReject?: true): Promise<never>;
export function toPromise(signal: AbortSignal, shouldReject: boolean = false) {
const promise = new Promise((resolve, reject) => {
const action = shouldReject ? reject : resolve;
if (signal.aborted) action();
signal.addEventListener('abort', action);
export function toPromise(signal: AbortSignal): Promise<never> {
return new Promise((resolve, reject) => {
if (signal.aborted) reject(new AbortError());
const abortHandler = () => {
signal.removeEventListener('abort', abortHandler);
reject(new AbortError());
};
signal.addEventListener('abort', abortHandler);
});
/**
* Below is to make sure we don't have unhandled promise rejections. Otherwise
* Jest tests fail.
*/
promise.catch(() => {});
return promise;
}
/**
@ -61,8 +53,12 @@ export function toPromise(signal: AbortSignal, shouldReject: boolean = false) {
* @param signals
*/
export function getCombinedSignal(signals: AbortSignal[]) {
const promises = signals.map((signal) => toPromise(signal));
const controller = new AbortController();
Promise.race(promises).then(() => controller.abort());
if (signals.some((signal) => signal.aborted)) {
controller.abort();
} else {
const promises = signals.map((signal) => toPromise(signal));
Promise.race(promises).catch(() => controller.abort());
}
return controller.signal;
}

View file

@ -335,18 +335,13 @@ export {
OptionedValueProp,
// search
ES_SEARCH_STRATEGY,
SYNC_SEARCH_STRATEGY,
getEsPreference,
getSearchErrorType,
ISearchStrategy,
ISearch,
ISearchOptions,
IRequestTypesMap,
IResponseTypesMap,
ISearchGeneric,
IEsSearchResponse,
IEsSearchRequest,
ISyncSearchRequest,
IKibanaSearchResponse,
IKibanaSearchRequest,
SearchRequest,
@ -366,6 +361,7 @@ export {
TabbedAggRow,
TabbedTable,
SearchInterceptor,
SearchInterceptorDeps,
RequestTimeoutError,
} from './search';

View file

@ -9,6 +9,7 @@ import _ from 'lodash';
import { Action } from 'history';
import { ApplicationStart } from 'kibana/public';
import { Assign } from '@kbn/utility-types';
import { BehaviorSubject } from 'rxjs';
import Boom from 'boom';
import { Breadcrumb } from '@elastic/eui';
import { BulkIndexDocumentsParams } from 'elasticsearch';
@ -760,22 +761,14 @@ export function getQueryLog(uiSettings: IUiSettingsClient, storage: IStorageWrap
// @public (undocumented)
export function getSearchErrorType({ message }: Pick<SearchError, 'message'>): "UNSUPPORTED_QUERY" | undefined;
// Warning: (ae-forgotten-export) The symbol "ISearchRequestParams" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "getSearchParamsFromRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function getSearchParamsFromRequest(searchRequest: SearchRequest, dependencies: {
injectedMetadata: CoreStart['injectedMetadata'];
uiSettings: IUiSettingsClient_3;
}): {
rest_total_hits_as_int: boolean;
ignore_unavailable: boolean;
ignore_throttled: boolean;
max_concurrent_shard_requests: any;
preference: any;
timeout: string | undefined;
index: any;
body: any;
};
}): ISearchRequestParams;
// Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@ -828,15 +821,15 @@ export interface IEsSearchRequest extends IKibanaSearchRequest {
// (undocumented)
indexType?: string;
// (undocumented)
params: SearchParams;
params?: ISearchRequestParams;
}
// Warning: (ae-missing-release-tag) "IEsSearchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface IEsSearchResponse<Hits = unknown> extends IKibanaSearchResponse {
export interface IEsSearchResponse extends IKibanaSearchResponse {
// (undocumented)
rawResponse: SearchResponse_2<Hits>;
rawResponse: SearchResponse_2<any>;
}
// Warning: (ae-missing-release-tag) "IFieldFormat" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@ -1237,41 +1230,16 @@ export type InputTimeRange = TimeRange | {
to: Moment;
};
// Warning: (ae-missing-release-tag) "IRequestTypesMap" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface IRequestTypesMap {
// (undocumented)
[key: string]: IKibanaSearchRequest;
// (undocumented)
[ES_SEARCH_STRATEGY]: IEsSearchRequest;
// (undocumented)
[SYNC_SEARCH_STRATEGY]: ISyncSearchRequest;
}
// Warning: (ae-missing-release-tag) "IResponseTypesMap" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface IResponseTypesMap {
// (undocumented)
[key: string]: IKibanaSearchResponse;
// (undocumented)
[ES_SEARCH_STRATEGY]: IEsSearchResponse;
// (undocumented)
[SYNC_SEARCH_STRATEGY]: IKibanaSearchResponse;
}
// Warning: (ae-forgotten-export) The symbol "TStrategyTypes" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "DEFAULT_SEARCH_STRATEGY" needs to be exported by the entry point index.d.ts
// 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<T extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY> = (request: IRequestTypesMap[T], options?: ISearchOptions) => Observable<IResponseTypesMap[T]>;
export type ISearch = (request: IKibanaSearchRequest, options?: ISearchOptions) => Observable<IKibanaSearchResponse>;
// Warning: (ae-forgotten-export) The symbol "IStrategyOptions" needs to be exported by the entry point index.d.ts
// 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)
export type ISearchGeneric = <T extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY>(request: IRequestTypesMap[T], options?: ISearchOptions, strategy?: T) => Observable<IResponseTypesMap[T]>;
export type ISearchGeneric = (request: IEsSearchRequest, options?: IStrategyOptions) => Observable<IEsSearchResponse>;
// Warning: (ae-missing-release-tag) "ISearchOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@ -1286,14 +1254,6 @@ export interface ISearchOptions {
// @public (undocumented)
export type ISearchSource = Pick<SearchSource, keyof SearchSource>;
// Warning: (ae-missing-release-tag) "ISearchStrategy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export interface ISearchStrategy<T extends TStrategyTypes> {
// (undocumented)
search: ISearch<T>;
}
// Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -1314,14 +1274,6 @@ export const isQuery: (x: unknown) => x is Query;
// @public (undocumented)
export const isTimeRange: (x: unknown) => x is TimeRange;
// Warning: (ae-missing-release-tag) "ISyncSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ISyncSearchRequest extends IKibanaSearchRequest {
// (undocumented)
serverStrategy: string;
}
// @public (undocumented)
export enum KBN_FIELD_TYPES {
// (undocumented)
@ -1781,22 +1733,43 @@ export class SearchError extends Error {
//
// @public (undocumented)
export class SearchInterceptor {
constructor(toasts: ToastsStart, application: ApplicationStart, requestTimeout?: number | undefined);
constructor(deps: SearchInterceptorDeps, requestTimeout?: number | undefined);
protected abortController: AbortController;
// (undocumented)
protected readonly application: ApplicationStart;
getPendingCount$: () => import("rxjs").Observable<number>;
protected readonly deps: SearchInterceptorDeps;
getPendingCount$: () => Observable<number>;
// (undocumented)
protected hideToast: () => void;
protected longRunningToast?: Toast;
protected pendingCount$: BehaviorSubject<number>;
protected pendingCount: number;
// (undocumented)
protected readonly requestTimeout?: number | undefined;
search: (search: ISearchGeneric, request: IKibanaSearchRequest, options?: ISearchOptions | undefined) => import("rxjs").Observable<import("../../common/search").IEsSearchResponse<unknown>>;
// (undocumented)
protected runSearch(request: IEsSearchRequest, combinedSignal: AbortSignal): Observable<IEsSearchResponse>;
search(request: IEsSearchRequest, options?: ISearchOptions): Observable<IEsSearchResponse>;
// (undocumented)
protected setupTimers(options?: ISearchOptions): {
combinedSignal: AbortSignal;
cleanup: () => void;
};
// (undocumented)
protected showToast: () => void;
protected timeoutSubscriptions: Set<Subscription>;
protected timeoutSubscriptions: Subscription;
}
// Warning: (ae-missing-release-tag) "SearchInterceptorDeps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface SearchInterceptorDeps {
// (undocumented)
protected readonly toasts: ToastsStart;
application: ApplicationStart;
// (undocumented)
http: CoreStart['http'];
// (undocumented)
toasts: ToastsStart;
// (undocumented)
uiSettings: CoreStart['uiSettings'];
}
// Warning: (ae-missing-release-tag) "SearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@ -1869,11 +1842,6 @@ export type StatefulSearchBarProps = SearchBarOwnProps & {
onSavedQueryIdChange?: (savedQueryId?: string) => void;
};
// Warning: (ae-missing-release-tag) "SYNC_SEARCH_STRATEGY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const SYNC_SEARCH_STRATEGY = "SYNC_SEARCH_STRATEGY";
// Warning: (ae-forgotten-export) The symbol "IKbnUrlStateStorage" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "syncQueryStateWithUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@ -2011,20 +1979,20 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:375:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:376:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:371:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:372:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:381:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:382:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:383:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:395: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:40:60 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:53:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts

View file

@ -1,47 +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 { CoreSetup } from '../../../../../core/public';
import { coreMock } from '../../../../../core/public/mocks';
import { esSearchStrategyProvider } from './es_search_strategy';
import { ES_SEARCH_STRATEGY } from '../../../common/search/es_search';
describe('ES search strategy', () => {
let mockCoreSetup: MockedKeys<CoreSetup>;
const mockSearch = { search: jest.fn() };
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
mockSearch.search.mockClear();
});
it('returns a strategy with `search` that calls the sync search `search`', () => {
const request = { params: {} };
const options = {};
const esSearch = esSearchStrategyProvider(mockCoreSetup, mockSearch);
esSearch.search(request, options);
expect(mockSearch.search.mock.calls[0][0]).toEqual({
...request,
serverStrategy: ES_SEARCH_STRATEGY,
});
expect(mockSearch.search.mock.calls[0][1]).toBe(options);
});
});

View file

@ -1,43 +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 { Observable } from 'rxjs';
import { CoreSetup } from '../../../../../core/public';
import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../../common/search';
import { ISearch } from '../i_search';
import { ISearchStrategy } from '../types';
import { SYNC_SEARCH_STRATEGY } from '../sync_search_strategy';
import { getEsPreference } from './get_es_preference';
export function esSearchStrategyProvider(
core: CoreSetup,
syncStrategy: ISearchStrategy<typeof SYNC_SEARCH_STRATEGY>
) {
const search: ISearch<typeof ES_SEARCH_STRATEGY> = (request, options) => {
request.params = {
preference: getEsPreference(core.uiSettings),
...request.params,
};
return syncStrategy.search(
{ ...request, serverStrategy: ES_SEARCH_STRATEGY },
options
) as Observable<IEsSearchResponse>;
};
return { search };
}

View file

@ -17,5 +17,4 @@
* under the License.
*/
export { esSearchStrategyProvider } from './es_search_strategy';
export { getEsPreference } from './get_es_preference';

View file

@ -18,7 +18,7 @@
*/
import { IUiSettingsClient, CoreStart } from 'kibana/public';
import { UI_SETTINGS } from '../../../common';
import { UI_SETTINGS, ISearchRequestParams } from '../../../common';
import { SearchRequest } from './types';
const sessionId = Date.now();
@ -58,7 +58,7 @@ export function getTimeout(esShardTimeout: number) {
export function getSearchParamsFromRequest(
searchRequest: SearchRequest,
dependencies: { injectedMetadata: CoreStart['injectedMetadata']; uiSettings: IUiSettingsClient }
) {
): ISearchRequestParams {
const { injectedMetadata, uiSettings } = dependencies;
const esShardTimeout = injectedMetadata.getInjectedVar('esShardTimeout') as number;
const searchParams = getSearchParams(uiSettings, esShardTimeout);

View file

@ -1,59 +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 { Observable } from 'rxjs';
import { TStrategyTypes } from './strategy_types';
import {
DEFAULT_SEARCH_STRATEGY,
IKibanaSearchRequest,
IKibanaSearchResponse,
} from '../../common/search';
import { SYNC_SEARCH_STRATEGY, ISyncSearchRequest } from './sync_search_strategy';
import {
ES_SEARCH_STRATEGY,
IEsSearchRequest,
IEsSearchResponse,
} from '../../common/search/es_search';
export interface ISearchOptions {
signal?: AbortSignal;
}
export interface IRequestTypesMap {
[SYNC_SEARCH_STRATEGY]: ISyncSearchRequest;
[ES_SEARCH_STRATEGY]: IEsSearchRequest;
[key: string]: IKibanaSearchRequest;
}
export interface IResponseTypesMap {
[SYNC_SEARCH_STRATEGY]: IKibanaSearchResponse;
[ES_SEARCH_STRATEGY]: IEsSearchResponse;
[key: string]: IKibanaSearchResponse;
}
export type ISearchGeneric = <T extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY>(
request: IRequestTypesMap[T],
options?: ISearchOptions,
strategy?: T
) => Observable<IResponseTypesMap[T]>;
export type ISearch<T extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY> = (
request: IRequestTypesMap[T],
options?: ISearchOptions
) => Observable<IResponseTypesMap[T]>;

View file

@ -21,20 +21,11 @@ export * from './aggs';
export * from './expressions';
export * from './tabify';
export { ISearchSetup, ISearchStart, ISearchStrategy } from './types';
export {
ISearch,
ISearchOptions,
IRequestTypesMap,
IResponseTypesMap,
ISearchGeneric,
} from './i_search';
export { ISearch, ISearchOptions, ISearchGeneric, ISearchSetup, ISearchStart } from './types';
export { IEsSearchResponse, IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../common/search';
export { ISyncSearchRequest, SYNC_SEARCH_STRATEGY } from './sync_search_strategy';
export { esSearchStrategyProvider, getEsPreference } from './es_search';
export { getEsPreference } from './es_search';
export { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search';
@ -59,5 +50,5 @@ export {
parseSearchSourceJSON,
} from './search_source';
export { SearchInterceptor } from './search_interceptor';
export { SearchInterceptor, SearchInterceptorDeps } from './search_interceptor';
export { RequestTimeoutError } from './request_timeout_error';

View file

@ -26,7 +26,6 @@ export * from './search_source/mocks';
function createSetupContract(): jest.Mocked<ISearchSetup> {
return {
aggs: searchAggsSetupMock(),
registerSearchStrategy: jest.fn(),
};
}
@ -34,7 +33,6 @@ function createStartContract(): jest.Mocked<ISearchStart> {
return {
aggs: searchAggsStartMock(),
setInterceptor: jest.fn(),
getSearchStrategy: jest.fn(),
search: jest.fn(),
searchSource: searchSourceMock,
__LEGACY: {

View file

@ -17,114 +17,169 @@
* under the License.
*/
import { Observable, Subject } from 'rxjs';
import { CoreStart } from '../../../../core/public';
import { coreMock } from '../../../../core/public/mocks';
import { IKibanaSearchRequest } from '../../common/search';
import { RequestTimeoutError } from './request_timeout_error';
import { IEsSearchRequest } from '../../common/search';
import { SearchInterceptor } from './search_interceptor';
import { AbortError } from '../../common';
jest.useFakeTimers();
const mockSearch = jest.fn();
let searchInterceptor: SearchInterceptor;
let mockCoreStart: MockedKeys<CoreStart>;
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
jest.useFakeTimers();
describe('SearchInterceptor', () => {
beforeEach(() => {
mockCoreStart = coreMock.createStart();
mockSearch.mockClear();
searchInterceptor = new SearchInterceptor(
mockCoreStart.notifications.toasts,
mockCoreStart.application,
{
toasts: mockCoreStart.notifications.toasts,
application: mockCoreStart.application,
uiSettings: mockCoreStart.uiSettings,
http: mockCoreStart.http,
},
1000
);
});
describe('search', () => {
test('should invoke `search` with the request', () => {
const mockResponse = new Subject();
mockSearch.mockReturnValue(mockResponse.asObservable());
const mockRequest: IKibanaSearchRequest = {};
const response = searchInterceptor.search(mockSearch, mockRequest);
mockResponse.complete();
test('Observable should resolve if fetch is successful', async () => {
const mockResponse: any = { result: 200 };
mockCoreStart.http.fetch.mockResolvedValueOnce(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
};
const response = searchInterceptor.search(mockRequest);
response.subscribe();
expect(mockSearch.mock.calls[0][0]).toBe(mockRequest);
const result = await response.toPromise();
expect(result).toBe(mockResponse);
});
test('should mirror the observable to completion if the request does not time out', () => {
const mockResponse = new Subject();
mockSearch.mockReturnValue(mockResponse.asObservable());
const response = searchInterceptor.search(mockSearch, {});
test('Observable should fail if fetch has an error', async () => {
const mockResponse: any = { result: 500 };
mockCoreStart.http.fetch.mockRejectedValueOnce(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
};
const response = searchInterceptor.search(mockRequest);
setTimeout(() => mockResponse.next('hi'), 250);
setTimeout(() => mockResponse.complete(), 500);
const next = jest.fn();
const complete = jest.fn();
response.subscribe({ next, complete });
jest.advanceTimersByTime(1000);
expect(next).toHaveBeenCalledWith('hi');
expect(complete).toHaveBeenCalled();
try {
await response.toPromise();
} catch (e) {
expect(e).toBe(mockResponse);
}
});
test('should mirror the observable to error if the request does not time out', () => {
const mockResponse = new Subject();
mockSearch.mockReturnValue(mockResponse.asObservable());
const response = searchInterceptor.search(mockSearch, {});
test('Observable should fail if fetch times out (test merged signal)', async () => {
mockCoreStart.http.fetch.mockImplementationOnce((options: any) => {
return new Promise((resolve, reject) => {
options.signal.addEventListener('abort', () => {
reject(new AbortError());
});
setTimeout(() => mockResponse.next('hi'), 250);
setTimeout(() => mockResponse.error('error'), 500);
setTimeout(resolve, 5000);
});
});
const mockRequest: IEsSearchRequest = {
params: {},
};
const response = searchInterceptor.search(mockRequest);
const next = jest.fn();
const error = jest.fn();
const error = (e: any) => {
expect(next).not.toBeCalled();
expect(e).toBeInstanceOf(AbortError);
};
response.subscribe({ next, error });
jest.advanceTimersByTime(1000);
jest.advanceTimersByTime(5000);
expect(next).toHaveBeenCalledWith('hi');
expect(error).toHaveBeenCalledWith('error');
await flushPromises();
});
test('should return a `RequestTimeoutError` if the request times out', () => {
mockSearch.mockReturnValue(new Observable());
const response = searchInterceptor.search(mockSearch, {});
test('Observable should fail if user aborts (test merged signal)', async () => {
const abortController = new AbortController();
mockCoreStart.http.fetch.mockImplementationOnce((options: any) => {
return new Promise((resolve, reject) => {
options.signal.addEventListener('abort', () => {
reject(new AbortError());
});
const error = jest.fn();
setTimeout(resolve, 500);
});
});
const mockRequest: IEsSearchRequest = {
params: {},
};
const response = searchInterceptor.search(mockRequest, { signal: abortController.signal });
const next = jest.fn();
const error = (e: any) => {
expect(next).not.toBeCalled();
expect(e).toBeInstanceOf(AbortError);
};
response.subscribe({ next, error });
setTimeout(() => abortController.abort(), 200);
jest.advanceTimersByTime(5000);
await flushPromises();
});
test('Immediatelly aborts if passed an aborted abort signal', async (done) => {
const abort = new AbortController();
const mockRequest: IEsSearchRequest = {
params: {},
};
const response = searchInterceptor.search(mockRequest, { signal: abort.signal });
abort.abort();
const error = (e: any) => {
expect(e).toBeInstanceOf(AbortError);
expect(mockCoreStart.http.fetch).not.toBeCalled();
done();
};
response.subscribe({ error });
jest.advanceTimersByTime(1000);
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0] instanceof RequestTimeoutError).toBe(true);
});
});
describe('getPendingCount$', () => {
test('should observe the number of pending requests', () => {
let i = 0;
const mockResponses = [new Subject(), new Subject()];
mockSearch.mockImplementation(() => mockResponses[i++]);
const pendingCount$ = searchInterceptor.getPendingCount$();
const pendingNext = jest.fn();
pendingCount$.subscribe(pendingNext);
const next = jest.fn();
pendingCount$.subscribe(next);
const mockResponse: any = { result: 200 };
mockCoreStart.http.fetch.mockResolvedValue(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
};
const response = searchInterceptor.search(mockRequest);
const error = jest.fn();
searchInterceptor.search(mockSearch, {}).subscribe({ error });
searchInterceptor.search(mockSearch, {}).subscribe({ error });
response.subscribe({
complete: () => {
expect(pendingNext.mock.calls).toEqual([[0], [1], [0]]);
},
});
});
setTimeout(() => mockResponses[0].complete(), 250);
setTimeout(() => mockResponses[1].error('error'), 500);
test('should observe the number of pending requests on error', () => {
const pendingCount$ = searchInterceptor.getPendingCount$();
const pendingNext = jest.fn();
pendingCount$.subscribe(pendingNext);
jest.advanceTimersByTime(500);
const mockResponse: any = { result: 500 };
mockCoreStart.http.fetch.mockRejectedValue(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
};
const response = searchInterceptor.search(mockRequest);
expect(next).toHaveBeenCalled();
expect(next.mock.calls).toEqual([[0], [1], [2], [1], [0]]);
response.subscribe({
complete: () => {
expect(pendingNext.mock.calls).toEqual([[0], [1], [0]]);
},
});
});
});
});

View file

@ -17,17 +17,23 @@
* under the License.
*/
import { BehaviorSubject, throwError, timer, Subscription, defer, fromEvent } from 'rxjs';
import { takeUntil, finalize, mergeMapTo, filter } from 'rxjs/operators';
import { ApplicationStart, Toast, ToastsStart } from 'kibana/public';
import { BehaviorSubject, throwError, timer, Subscription, defer, from, Observable } from 'rxjs';
import { finalize, filter } from 'rxjs/operators';
import { ApplicationStart, Toast, ToastsStart, CoreStart } from 'kibana/public';
import { getCombinedSignal, AbortError } from '../../common/utils';
import { IKibanaSearchRequest } from '../../common/search';
import { ISearchGeneric, ISearchOptions } from './i_search';
import { RequestTimeoutError } from './request_timeout_error';
import { IEsSearchRequest, IEsSearchResponse } from '../../common/search';
import { ISearchOptions } from './types';
import { getLongQueryNotification } from './long_query_notification';
const LONG_QUERY_NOTIFICATION_DELAY = 10000;
export interface SearchInterceptorDeps {
toasts: ToastsStart;
application: ApplicationStart;
http: CoreStart['http'];
uiSettings: CoreStart['uiSettings'];
}
export class SearchInterceptor {
/**
* `abortController` used to signal all searches to abort.
@ -37,17 +43,17 @@ export class SearchInterceptor {
/**
* The number of pending search requests.
*/
private pendingCount = 0;
protected pendingCount = 0;
/**
* Observable that emits when the number of pending requests changes.
*/
private pendingCount$ = new BehaviorSubject(this.pendingCount);
protected pendingCount$ = new BehaviorSubject(this.pendingCount);
/**
* The subscriptions from scheduling the automatic timeout for each request.
*/
protected timeoutSubscriptions: Set<Subscription> = new Set();
protected timeoutSubscriptions: Subscription = new Subscription();
/**
* The current long-running toast (if there is one).
@ -62,10 +68,11 @@ export class SearchInterceptor {
* @param requestTimeout Usually config value `elasticsearch.requestTimeout`
*/
constructor(
protected readonly toasts: ToastsStart,
protected readonly application: ApplicationStart,
protected readonly deps: SearchInterceptorDeps,
protected readonly requestTimeout?: number
) {
this.deps.http.addLoadingCountSource(this.pendingCount$);
// When search requests go out, a notification is scheduled allowing users to continue the
// request past the timeout. When all search requests complete, we remove the notification.
this.getPendingCount$()
@ -81,71 +88,91 @@ export class SearchInterceptor {
return this.pendingCount$.asObservable();
};
protected runSearch(
request: IEsSearchRequest,
combinedSignal: AbortSignal
): Observable<IEsSearchResponse> {
return from(
this.deps.http.fetch({
path: `/internal/search/es`,
method: 'POST',
body: JSON.stringify(request),
signal: combinedSignal,
})
);
}
/**
* Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort
* either when `cancelPending` is called, when the request times out, or when the original
* `AbortSignal` is aborted. Updates the `pendingCount` when the request is started/finalized.
*/
public search = (
search: ISearchGeneric,
request: IKibanaSearchRequest,
public search(
request: IEsSearchRequest,
options?: ISearchOptions
) => {
): Observable<IEsSearchResponse> {
// Defer the following logic until `subscribe` is actually called
return defer(() => {
if (options?.signal?.aborted) {
return throwError(new AbortError());
}
const { combinedSignal, cleanup } = this.setupTimers(options);
this.pendingCount$.next(++this.pendingCount);
// Schedule this request to automatically timeout after some interval
const timeoutController = new AbortController();
const { signal: timeoutSignal } = timeoutController;
const timeout$ = timer(this.requestTimeout);
const subscription = timeout$.subscribe(() => timeoutController.abort());
this.timeoutSubscriptions.add(subscription);
// If the request timed out, throw a `RequestTimeoutError`
const timeoutError$ = fromEvent(timeoutSignal, 'abort').pipe(
mergeMapTo(throwError(new RequestTimeoutError()))
);
const userAbort$ = fromEvent(this.abortController.signal, 'abort').pipe(
mergeMapTo(throwError(new AbortError()))
);
// Schedule the notification to allow users to cancel or wait beyond the timeout
const notificationSubscription = timer(LONG_QUERY_NOTIFICATION_DELAY).subscribe(
this.showToast
);
// Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs:
// 1. The user manually aborts (via `cancelPending`)
// 2. The request times out
// 3. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines)
const signals = [
this.abortController.signal,
timeoutSignal,
...(options?.signal ? [options.signal] : []),
];
const combinedSignal = getCombinedSignal(signals);
return search(request as any, { ...options, signal: combinedSignal }).pipe(
takeUntil(timeoutError$),
takeUntil(userAbort$),
return this.runSearch(request, combinedSignal).pipe(
finalize(() => {
this.pendingCount$.next(--this.pendingCount);
this.timeoutSubscriptions.delete(subscription);
notificationSubscription.unsubscribe();
cleanup();
})
);
});
};
}
protected setupTimers(options?: ISearchOptions) {
// Schedule this request to automatically timeout after some interval
const timeoutController = new AbortController();
const { signal: timeoutSignal } = timeoutController;
const timeout$ = timer(this.requestTimeout);
const subscription = timeout$.subscribe(() => {
timeoutController.abort();
});
this.timeoutSubscriptions.add(subscription);
// Schedule the notification to allow users to cancel or wait beyond the timeout
const notificationSubscription = timer(LONG_QUERY_NOTIFICATION_DELAY).subscribe(this.showToast);
// Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs:
// 1. The user manually aborts (via `cancelPending`)
// 2. The request times out
// 3. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines)
const signals = [
this.abortController.signal,
timeoutSignal,
...(options?.signal ? [options.signal] : []),
];
const combinedSignal = getCombinedSignal(signals);
const cleanup = () => {
this.timeoutSubscriptions.remove(subscription);
notificationSubscription.unsubscribe();
};
combinedSignal.addEventListener('abort', cleanup);
return {
combinedSignal,
cleanup,
};
}
protected showToast = () => {
if (this.longRunningToast) return;
this.longRunningToast = this.toasts.addInfo(
this.longRunningToast = this.deps.toasts.addInfo(
{
title: 'Your query is taking awhile',
text: getLongQueryNotification({
application: this.application,
application: this.deps.application,
}),
},
{
@ -156,7 +183,7 @@ export class SearchInterceptor {
protected hideToast = () => {
if (this.longRunningToast) {
this.toasts.remove(this.longRunningToast);
this.deps.toasts.remove(this.longRunningToast);
delete this.longRunningToast;
}
};

View file

@ -18,7 +18,7 @@
*/
import { coreMock } from '../../../../core/public/mocks';
import { CoreSetup } from '../../../../core/public';
import { CoreSetup, CoreStart } from '../../../../core/public';
import { expressionsPluginMock } from '../../../../plugins/expressions/public/mocks';
import { SearchService } from './search_service';
@ -26,10 +26,12 @@ import { SearchService } from './search_service';
describe('Search service', () => {
let searchService: SearchService;
let mockCoreSetup: MockedKeys<CoreSetup>;
let mockCoreStart: MockedKeys<CoreStart>;
beforeEach(() => {
searchService = new SearchService();
mockCoreSetup = coreMock.createSetup();
mockCoreStart = coreMock.createStart();
});
describe('setup()', () => {
@ -38,7 +40,17 @@ describe('Search service', () => {
packageInfo: { version: '8' },
expressions: expressionsPluginMock.createSetupContract(),
} as any);
expect(setup).toHaveProperty('registerSearchStrategy');
expect(setup).toHaveProperty('aggs');
});
});
describe('start()', () => {
it('exposes proper contract', async () => {
const start = searchService.start(mockCoreStart, {
indexPatterns: {},
} as any);
expect(start).toHaveProperty('setInterceptor');
expect(start).toHaveProperty('search');
});
});
});

View file

@ -18,17 +18,14 @@
*/
import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public';
import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy';
import { ISearchSetup, ISearchStart, TSearchStrategiesMap, ISearchStrategy } from './types';
import { ISearchSetup, ISearchStart } from './types';
import { ExpressionsSetup } from '../../../../plugins/expressions/public';
import { createSearchSource, SearchSource, SearchSourceDependencies } from './search_source';
import { TStrategyTypes } from './strategy_types';
import { getEsClient, LegacyApiCaller } from './legacy';
import { getForceNow } from '../query/timefilter/lib/get_force_now';
import { calculateBounds, TimeRange } from '../../common/query';
import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search';
import { esSearchStrategyProvider } from './es_search';
import { IndexPatternsContract } from '../index_patterns/index_patterns';
import { GetInternalStartServicesFn } from '../types';
import { SearchInterceptor } from './search_interceptor';
@ -39,7 +36,7 @@ import {
AggConfigs,
getCalculateAutoTimeExpression,
} from './aggs';
import { ISearchGeneric } from './i_search';
import { ISearchGeneric } from './types';
interface SearchServiceSetupDependencies {
expressions: ExpressionsSetup;
@ -51,38 +48,11 @@ interface SearchServiceStartDependencies {
indexPatterns: IndexPatternsContract;
}
/**
* The search plugin exposes a method `registerSearchStrategy` for other plugins
* to add their own custom search strategies.
*
* It also comes with two search strategy implementations - SYNC_SEARCH_STRATEGY and ES_SEARCH_STRATEGY.
*/
export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
/**
* A mapping of search strategies keyed by a unique identifier. Plugins can use this unique identifier
* to override certain strategy implementations.
*/
private searchStrategies: TSearchStrategiesMap = {};
private esClient?: LegacyApiCaller;
private readonly aggTypesRegistry = new AggTypesRegistry();
private searchInterceptor!: SearchInterceptor;
private registerSearchStrategy = <T extends TStrategyTypes>(
name: T,
strategy: ISearchStrategy<T>
) => {
this.searchStrategies[name] = strategy;
};
private getSearchStrategy = <T extends TStrategyTypes>(name: T): ISearchStrategy<T> => {
const strategy = this.searchStrategies[name];
if (!strategy) {
throw new Error(`Search strategy ${name} not found`);
}
return strategy;
};
/**
* getForceNow uses window.location, so we must have a separate implementation
* of calculateBounds on the client and the server.
@ -96,11 +66,6 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
): ISearchSetup {
this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo);
const syncSearchStrategy = syncSearchStrategyProvider(core);
const esSearchStrategy = esSearchStrategyProvider(core, syncSearchStrategy);
this.registerSearchStrategy(SYNC_SEARCH_STRATEGY, syncSearchStrategy);
this.registerSearchStrategy(ES_SEARCH_STRATEGY, esSearchStrategy);
const aggTypesSetup = this.aggTypesRegistry.setup();
// register each agg type
@ -121,7 +86,6 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings),
types: aggTypesSetup,
},
registerSearchStrategy: this.registerSearchStrategy,
};
}
@ -133,18 +97,19 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
* their own search collector instances
*/
this.searchInterceptor = new SearchInterceptor(
core.notifications.toasts,
core.application,
{
toasts: core.notifications.toasts,
application: core.application,
http: core.http,
uiSettings: core.uiSettings,
},
core.injectedMetadata.getInjectedVar('esRequestTimeout') as number
);
const aggTypesStart = this.aggTypesRegistry.start();
const search: ISearchGeneric = (request, options, strategyName) => {
const { search: defaultSearch } = this.getSearchStrategy(
strategyName || DEFAULT_SEARCH_STRATEGY
);
return this.searchInterceptor.search(defaultSearch as any, request, options);
const search: ISearchGeneric = (request, options) => {
return this.searchInterceptor.search(request, options);
};
const legacySearch = {
@ -168,7 +133,6 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
},
types: aggTypesStart,
},
getSearchStrategy: this.getSearchStrategy,
search,
searchSource: {
create: createSearchSource(dependencies.indexPatterns, searchSourceDependencies),

View file

@ -1,40 +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 { ES_SEARCH_STRATEGY } from '../../common/search/es_search';
import { SYNC_SEARCH_STRATEGY } from './sync_search_strategy';
/**
* Contains all known strategy type identifiers that will be used to map to
* request and response shapes. Plugins that wish to add their own custom search
* strategies should extend this type via:
*
* const MY_STRATEGY = 'MY_STRATEGY';
*
* declare module 'src/plugins/data/public' {
* export interface IRequestTypesMap {
* [MY_STRATEGY]: IMySearchRequest;
* }
*
* export interface IResponseTypesMap {
* [MY_STRATEGY]: IMySearchResponse
* }
* }
*/
export type TStrategyTypes = typeof SYNC_SEARCH_STRATEGY | typeof ES_SEARCH_STRATEGY | string;

View file

@ -1,84 +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 { coreMock } from '../../../../core/public/mocks';
import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy';
import { CoreSetup } from 'kibana/public';
describe('Sync search strategy', () => {
let mockCoreSetup: MockedKeys<CoreSetup>;
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
});
it('returns a strategy with `search` that calls the backend API', () => {
mockCoreSetup.http.fetch.mockImplementationOnce(() => Promise.resolve());
const syncSearch = syncSearchStrategyProvider(mockCoreSetup);
const request = { serverStrategy: SYNC_SEARCH_STRATEGY };
syncSearch.search(request, {});
expect(mockCoreSetup.http.fetch.mock.calls[0][0]).toEqual({
path: `/internal/search/${SYNC_SEARCH_STRATEGY}`,
body: JSON.stringify({
serverStrategy: 'SYNC_SEARCH_STRATEGY',
}),
method: 'POST',
signal: undefined,
});
});
it('increments and decrements loading count on success', async () => {
const expectedLoadingCountValues = [0, 1, 0];
const receivedLoadingCountValues: number[] = [];
mockCoreSetup.http.fetch.mockResolvedValueOnce('response');
const syncSearch = syncSearchStrategyProvider(mockCoreSetup);
const request = { serverStrategy: SYNC_SEARCH_STRATEGY };
const loadingCount$ = mockCoreSetup.http.addLoadingCountSource.mock.calls[0][0];
loadingCount$.subscribe((value) => receivedLoadingCountValues.push(value));
await syncSearch.search(request, {}).toPromise();
expect(receivedLoadingCountValues).toEqual(expectedLoadingCountValues);
});
it('increments and decrements loading count on failure', async () => {
expect.assertions(1);
const expectedLoadingCountValues = [0, 1, 0];
const receivedLoadingCountValues: number[] = [];
mockCoreSetup.http.fetch.mockRejectedValueOnce('error');
const syncSearch = syncSearchStrategyProvider(mockCoreSetup);
const request = { serverStrategy: SYNC_SEARCH_STRATEGY };
const loadingCount$ = mockCoreSetup.http.addLoadingCountSource.mock.calls[0][0];
loadingCount$.subscribe((value) => receivedLoadingCountValues.push(value));
try {
await syncSearch.search(request, {}).toPromise();
} catch (e) {
expect(receivedLoadingCountValues).toEqual(expectedLoadingCountValues);
}
});
});

View file

@ -1,50 +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 { BehaviorSubject, from } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { CoreSetup } from '../../../../core/public';
import { IKibanaSearchRequest } from '../../common/search';
import { ISearch } from './i_search';
export const SYNC_SEARCH_STRATEGY = 'SYNC_SEARCH_STRATEGY';
export interface ISyncSearchRequest extends IKibanaSearchRequest {
serverStrategy: string;
}
export function syncSearchStrategyProvider(core: CoreSetup) {
const loadingCount$ = new BehaviorSubject(0);
core.http.addLoadingCountSource(loadingCount$);
const search: ISearch<typeof SYNC_SEARCH_STRATEGY> = (request, options) => {
loadingCount$.next(loadingCount$.getValue() + 1);
return from(
core.http.fetch({
path: `/internal/search/${request.serverStrategy}`,
method: 'POST',
body: JSON.stringify(request),
signal: options?.signal,
})
).pipe(finalize(() => loadingCount$.next(loadingCount$.getValue() - 1)));
};
return { search };
}

View file

@ -17,38 +17,37 @@
* under the License.
*/
import { Observable } from 'rxjs';
import { SearchAggsSetup, SearchAggsStart } from './aggs';
import { ISearch, ISearchGeneric } from './i_search';
import { TStrategyTypes } from './strategy_types';
import { LegacyApiCaller } from './legacy/es_client';
import { SearchInterceptor } from './search_interceptor';
import { ISearchSource, SearchSourceFields } from './search_source';
/**
* Search strategy interface contains a search method that takes in
* a request and returns a promise that resolves to a response.
*/
export interface ISearchStrategy<T extends TStrategyTypes> {
search: ISearch<T>;
import {
IKibanaSearchRequest,
IKibanaSearchResponse,
IEsSearchRequest,
IEsSearchResponse,
} from '../../common/search';
export interface ISearchOptions {
signal?: AbortSignal;
}
export type TSearchStrategiesMap = {
[K in TStrategyTypes]?: ISearchStrategy<any>;
};
export type ISearch = (
request: IKibanaSearchRequest,
options?: ISearchOptions
) => Observable<IKibanaSearchResponse>;
/**
* Extension point exposed for other plugins to register their own search
* strategies.
*/
export type TRegisterSearchStrategy = <T extends TStrategyTypes>(
name: T,
searchStrategy: ISearchStrategy<T>
) => void;
// Service API types
export interface IStrategyOptions extends ISearchOptions {
strategy?: string;
}
/**
* Used if a plugin needs access to an already registered search strategy.
*/
export type TGetSearchStrategy = <T extends TStrategyTypes>(name: T) => ISearchStrategy<T>;
export type ISearchGeneric = (
request: IEsSearchRequest,
options?: IStrategyOptions
) => Observable<IEsSearchResponse>;
export interface ISearchStartLegacy {
esClient: LegacyApiCaller;
@ -60,22 +59,11 @@ export interface ISearchStartLegacy {
*/
export interface ISearchSetup {
aggs: SearchAggsSetup;
/**
* Extension point exposed for other plugins to register their own search
* strategies.
*/
registerSearchStrategy: TRegisterSearchStrategy;
}
export interface ISearchStart {
aggs: SearchAggsStart;
setInterceptor: (searchInterceptor: SearchInterceptor) => void;
/**
* Used if a plugin needs access to an already registered search strategy.
*/
getSearchStrategy: TGetSearchStrategy;
search: ISearchGeneric;
searchSource: {
create: (fields?: SearchSourceFields) => Promise<ISearchSource>;

View file

@ -101,7 +101,7 @@ export class Execution<
/**
* Promise that rejects if/when abort controller sends "abort" signal.
*/
private readonly abortRejection = toPromise(this.abortController.signal, true);
private readonly abortRejection = toPromise(this.abortController.signal);
/**
* Races a given promise against the "abort" event of `abortController`.

View file

@ -25,7 +25,6 @@ export default async function ({ readConfigFile }) {
return {
testFiles: [
require.resolve('./search'),
require.resolve('./embeddables'),
require.resolve('./bfetch_explorer'),
require.resolve('./ui_actions'),

View file

@ -1,36 +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 { FtrProviderContext } from 'test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function ({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
describe('demo search strategy', () => {
before(async () => {
await testSubjects.click('demoSearch');
});
it('data is returned', async () => {
await testSubjects.click('doSearch');
await testSubjects.stringExistsInCodeBlockOrFail('response', '"Lovely to meet you, Molly"');
});
});
}

View file

@ -1,35 +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 { FtrProviderContext } from 'test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function ({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
describe('es search strategy', () => {
before(async () => {
await testSubjects.click('esSearch');
});
it('data is returned', async () => {
await testSubjects.click('doSearch');
await testSubjects.stringExistsInCodeBlockOrFail('response', '"animal weights"');
});
});
}

View file

@ -1,49 +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 { FtrProviderContext } from 'test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) {
const browser = getService('browser');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'header']);
describe('search services', function () {
before(async () => {
await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/dashboard/current/data');
await esArchiver.load('../functional/fixtures/es_archiver/dashboard/current/kibana');
await kibanaServer.uiSettings.replace({
'dateFormat:tz': 'Australia/North',
defaultIndex: 'logstash-*',
});
await browser.setWindowSize(1300, 900);
await PageObjects.common.navigateToApp('searchExplorer');
});
after(async function () {
await esArchiver.unload('../functional/fixtures/es_archiver/dashboard/current/data');
await esArchiver.unload('../functional/fixtures/es_archiver/dashboard/current/kibana');
});
loadTestFile(require.resolve('./demo_data'));
loadTestFile(require.resolve('./es_search'));
});
}

View file

@ -4,4 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { EnhancedSearchParams, IEnhancedEsSearchRequest } from './search';
export {
EnhancedSearchParams,
IEnhancedEsSearchRequest,
IAsyncSearchRequest,
IAsyncSearchResponse,
} from './search';

View file

@ -4,4 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { EnhancedSearchParams, IEnhancedEsSearchRequest } from './types';
export {
EnhancedSearchParams,
IEnhancedEsSearchRequest,
IAsyncSearchRequest,
IAsyncSearchResponse,
} from './types';

View file

@ -4,13 +4,36 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SearchParams } from 'elasticsearch';
import { IEsSearchRequest } from '../../../../../src/plugins/data/common';
import {
IEsSearchRequest,
IEsSearchResponse,
ISearchRequestParams,
} from '../../../../../src/plugins/data/common';
export interface EnhancedSearchParams extends SearchParams {
export interface EnhancedSearchParams extends ISearchRequestParams {
ignoreThrottled: boolean;
}
export interface IAsyncSearchRequest extends IEsSearchRequest {
/**
* The ID received from the response from the initial request
*/
id?: string;
params?: EnhancedSearchParams;
}
export interface IAsyncSearchResponse extends IEsSearchResponse {
/**
* Indicates whether async search is still in flight
*/
is_running?: boolean;
/**
* Indicates whether the results returned are complete or partial
*/
is_partial?: boolean;
}
export interface IEnhancedEsSearchRequest extends IEsSearchRequest {
/**
* Used to determine whether to use the _rollups_search or a regular search endpoint.

View file

@ -9,5 +9,3 @@ import { DataEnhancedPlugin, DataEnhancedSetup, DataEnhancedStart } from './plug
export const plugin = () => new DataEnhancedPlugin();
export { DataEnhancedSetup, DataEnhancedStart };
export { ASYNC_SEARCH_STRATEGY, IAsyncSearchRequest, IAsyncSearchOptions } from './search';

View file

@ -5,18 +5,10 @@
*/
import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
import {
DataPublicPluginSetup,
DataPublicPluginStart,
ES_SEARCH_STRATEGY,
} from '../../../../src/plugins/data/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { setAutocompleteService } from './services';
import { setupKqlQuerySuggestionProvider, KUERY_LANGUAGE_NAME } from './autocomplete';
import {
ASYNC_SEARCH_STRATEGY,
asyncSearchStrategyProvider,
enhancedEsSearchStrategyProvider,
} from './search';
import { EnhancedSearchInterceptor } from './search/search_interceptor';
export interface DataEnhancedSetupDependencies {
@ -39,17 +31,17 @@ export class DataEnhancedPlugin
KUERY_LANGUAGE_NAME,
setupKqlQuerySuggestionProvider(core)
);
const asyncSearchStrategy = asyncSearchStrategyProvider(core);
const esSearchStrategy = enhancedEsSearchStrategyProvider(core, asyncSearchStrategy);
data.search.registerSearchStrategy(ASYNC_SEARCH_STRATEGY, asyncSearchStrategy);
data.search.registerSearchStrategy(ES_SEARCH_STRATEGY, esSearchStrategy);
}
public start(core: CoreStart, plugins: DataEnhancedStartDependencies) {
setAutocompleteService(plugins.data.autocomplete);
const enhancedSearchInterceptor = new EnhancedSearchInterceptor(
core.notifications.toasts,
core.application,
{
toasts: core.notifications.toasts,
application: core.application,
http: core.http,
uiSettings: core.uiSettings,
},
core.injectedMetadata.getInjectedVar('esRequestTimeout') as number
);
plugins.data.search.setInterceptor(enhancedSearchInterceptor);

View file

@ -1,141 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { of } from 'rxjs';
import { AbortController } from 'abort-controller';
import { CoreSetup } from '../../../../../src/core/public';
import { coreMock } from '../../../../../src/core/public/mocks';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { asyncSearchStrategyProvider } from './async_search_strategy';
import { IAsyncSearchOptions } from '.';
import { DataEnhancedStartDependencies } from '../plugin';
describe('Async search strategy', () => {
let mockCoreSetup: jest.Mocked<CoreSetup<DataEnhancedStartDependencies>>;
let mockDataStart: jest.Mocked<DataPublicPluginStart>;
const mockSearch = jest.fn();
const mockRequest = { params: {}, serverStrategy: 'foo' };
const mockOptions: IAsyncSearchOptions = { pollInterval: 0 };
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
mockDataStart = dataPluginMock.createStartContract();
(mockDataStart.search.getSearchStrategy as jest.Mock).mockReturnValue({ search: mockSearch });
mockCoreSetup.getStartServices.mockResolvedValue([
undefined as any,
{ data: mockDataStart },
undefined,
]);
mockSearch.mockReset();
});
it('only sends one request if the first response is complete', async () => {
mockSearch.mockReturnValueOnce(of({ id: 1, total: 1, loaded: 1 }));
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
await asyncSearch.search(mockRequest, mockOptions).toPromise();
expect(mockSearch.mock.calls[0][0]).toEqual(mockRequest);
expect(mockSearch.mock.calls[0][1]).toEqual({});
expect(mockSearch).toBeCalledTimes(1);
});
it('stops polling when the response is complete', async () => {
mockSearch
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true }))
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false }))
.mockReturnValueOnce(
of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })
);
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
await asyncSearch.search(mockRequest, mockOptions).toPromise();
expect(mockSearch).toBeCalledTimes(2);
});
it('stops polling when the response is an error', async () => {
mockSearch
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true }))
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true }))
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true }));
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
await asyncSearch
.search(mockRequest, mockOptions)
.toPromise()
.catch(() => {
expect(mockSearch).toBeCalledTimes(2);
});
});
// For bug fixed in https://github.com/elastic/kibana/pull/64155
it('Continues polling if no records are returned on first async request', async () => {
mockSearch
.mockReturnValueOnce(of({ id: 1, total: 0, loaded: 0, is_running: true, is_partial: true }))
.mockReturnValueOnce(
of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })
);
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
await asyncSearch.search(mockRequest, mockOptions).toPromise();
expect(mockDataStart.search.getSearchStrategy).toBeCalledTimes(1);
expect(mockSearch).toBeCalledTimes(2);
expect(mockSearch.mock.calls[0][0]).toEqual(mockRequest);
expect(mockSearch.mock.calls[1][0]).toEqual({ id: 1, serverStrategy: 'foo' });
});
it('only sends the ID and server strategy after the first request', async () => {
mockSearch
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true }))
.mockReturnValueOnce(
of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })
);
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
await asyncSearch.search(mockRequest, mockOptions).toPromise();
expect(mockSearch).toBeCalledTimes(2);
expect(mockSearch.mock.calls[0][0]).toEqual(mockRequest);
expect(mockSearch.mock.calls[1][0]).toEqual({ id: 1, serverStrategy: 'foo' });
});
it('sends a DELETE request and stops polling when the signal is aborted', async () => {
mockSearch
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 }))
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 }))
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 }));
const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
const abortController = new AbortController();
const options = { ...mockOptions, signal: abortController.signal };
const promise = asyncSearch.search(mockRequest, options).toPromise();
abortController.abort();
try {
await promise;
} catch (e) {
expect(e.name).toBe('AbortError');
expect(mockSearch).toBeCalledTimes(1);
expect(mockCoreSetup.http.delete).toBeCalled();
}
});
});

View file

@ -1,83 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EMPTY, fromEvent, NEVER, throwError, timer, Observable, from } from 'rxjs';
import { mergeMap, expand, takeUntil, share, flatMap } from 'rxjs/operators';
import { CoreSetup } from '../../../../../src/core/public';
import { AbortError } from '../../../../../src/plugins/data/common';
import {
ISearch,
ISearchStrategy,
ISyncSearchRequest,
SYNC_SEARCH_STRATEGY,
} from '../../../../../src/plugins/data/public';
import { IAsyncSearchOptions, IAsyncSearchResponse, IAsyncSearchRequest } from './types';
import { DataEnhancedStartDependencies } from '../plugin';
export const ASYNC_SEARCH_STRATEGY = 'ASYNC_SEARCH_STRATEGY';
declare module '../../../../../src/plugins/data/public' {
export interface IRequestTypesMap {
[ASYNC_SEARCH_STRATEGY]: IAsyncSearchRequest;
}
}
export function asyncSearchStrategyProvider(
core: CoreSetup<DataEnhancedStartDependencies>
): ISearchStrategy<typeof ASYNC_SEARCH_STRATEGY> {
const startServices$ = from(core.getStartServices()).pipe(share());
const search: ISearch<typeof ASYNC_SEARCH_STRATEGY> = (
request: ISyncSearchRequest,
{ pollInterval = 1000, ...options }: IAsyncSearchOptions = {}
) => {
const { serverStrategy } = request;
let { id } = request;
const aborted$ = options.signal
? fromEvent(options.signal, 'abort').pipe(
mergeMap(() => {
// If we haven't received the response to the initial request, including the ID, then
// we don't need to send a follow-up request to delete this search. Otherwise, we
// send the follow-up request to delete this search, then throw an abort error.
if (id !== undefined) {
core.http.delete(`/internal/search/${request.serverStrategy}/${id}`);
}
return throwError(new AbortError());
})
)
: NEVER;
return startServices$.pipe(
flatMap((startServices) => {
const syncSearch = startServices[1].data.search.getSearchStrategy(SYNC_SEARCH_STRATEGY);
return (syncSearch.search(request, options) as Observable<IAsyncSearchResponse>).pipe(
expand((response) => {
// If the response indicates of an error, stop polling and complete the observable
if (!response || (response.is_partial && !response.is_running)) {
return throwError(new AbortError());
}
// If the response indicates it is complete, stop polling and complete the observable
if (!response.is_running) return EMPTY;
id = response.id;
// Delay by the given poll interval
return timer(pollInterval).pipe(
// Send future requests using just the ID from the response
mergeMap(() => {
return syncSearch.search({ id, serverStrategy }, options);
})
);
}),
takeUntil(aborted$)
);
})
);
};
return { search };
}

View file

@ -1,35 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup } from '../../../../../src/core/public';
import { coreMock } from '../../../../../src/core/public/mocks';
import { ES_SEARCH_STRATEGY } from '../../../../../src/plugins/data/common';
import { enhancedEsSearchStrategyProvider } from './es_search_strategy';
import { IAsyncSearchOptions } from '.';
describe('Enhanced ES search strategy', () => {
let mockCoreSetup: jest.Mocked<CoreSetup>;
const mockSearch = { search: jest.fn() };
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
mockSearch.search.mockClear();
});
it('returns a strategy with `search` that calls the async search `search`', () => {
const request = { params: {} };
const options: IAsyncSearchOptions = { pollInterval: 0 };
const esSearch = enhancedEsSearchStrategyProvider(mockCoreSetup, mockSearch);
esSearch.search(request, options);
expect(mockSearch.search.mock.calls[0][0]).toEqual({
...request,
serverStrategy: ES_SEARCH_STRATEGY,
});
expect(mockSearch.search.mock.calls[0][1]).toEqual(options);
});
});

View file

@ -1,44 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Observable } from 'rxjs';
import { CoreSetup } from '../../../../../src/core/public';
import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../../../../src/plugins/data/common';
import {
ISearch,
getEsPreference,
ISearchStrategy,
UI_SETTINGS,
} from '../../../../../src/plugins/data/public';
import { IEnhancedEsSearchRequest, EnhancedSearchParams } from '../../common';
import { ASYNC_SEARCH_STRATEGY } from './async_search_strategy';
import { IAsyncSearchOptions } from './types';
export function enhancedEsSearchStrategyProvider(
core: CoreSetup,
asyncStrategy: ISearchStrategy<typeof ASYNC_SEARCH_STRATEGY>
) {
const search: ISearch<typeof ES_SEARCH_STRATEGY> = (
request: IEnhancedEsSearchRequest,
options
) => {
const params: EnhancedSearchParams = {
ignoreThrottled: !core.uiSettings.get<boolean>(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
preference: getEsPreference(core.uiSettings),
...request.params,
};
request.params = params;
const asyncOptions: IAsyncSearchOptions = { pollInterval: 0, ...options };
return asyncStrategy.search(
{ ...request, serverStrategy: ES_SEARCH_STRATEGY },
asyncOptions
) as Observable<IEsSearchResponse>;
};
return { search };
}

View file

@ -4,6 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { ASYNC_SEARCH_STRATEGY, asyncSearchStrategyProvider } from './async_search_strategy';
export { enhancedEsSearchStrategyProvider } from './es_search_strategy';
export { IAsyncSearchRequest, IAsyncSearchOptions } from './types';
export { IAsyncSearchOptions } from './types';

Some files were not shown because too many files have changed in this diff Show more