Data plugin: Suggested enhance pattern (#74505)

* improve test stability

* Enhance pattern

* fix tests

* fix test

* Rename enhance to __enhance

* Deleted unnecessary attribute

* ISearchInterceptor interface

* docs

* Clean up internal docs

* jest

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Liza Katz 2020-08-13 11:28:39 +03:00 committed by GitHub
parent b249af128e
commit 290f9bfde2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 199 additions and 218 deletions

View file

@ -1,13 +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; [abortController](./kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md)
## SearchInterceptor.abortController property
`abortController` used to signal all searches to abort.
<b>Signature:</b>
```typescript
protected abortController: AbortController;
```

View file

@ -2,12 +2,16 @@
[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; [getPendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md)
## SearchInterceptor.getPendingCount$ property
## SearchInterceptor.getPendingCount$() method
Returns an `Observable` 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.
<b>Signature:</b>
```typescript
getPendingCount$: () => Observable<number>;
getPendingCount$(): Observable<number>;
```
<b>Returns:</b>
`Observable<number>`

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; [hideToast](./kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md)
## SearchInterceptor.hideToast property
<b>Signature:</b>
```typescript
protected hideToast: () => void;
```

View file

@ -1,13 +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; [longRunningToast](./kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md)
## SearchInterceptor.longRunningToast property
The current long-running toast (if there is one).
<b>Signature:</b>
```typescript
protected longRunningToast?: Toast;
```

View file

@ -20,22 +20,15 @@ export declare class SearchInterceptor
| 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. |
| [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> | |
| [showToast](./kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md) | | <code>() =&gt; void</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 |
| --- | --- | --- |
| [getPendingCount$()](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) | | 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. |
| [runSearch(request, signal, strategy)](./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. |
| [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 <code>pendingCount$</code> when the request is started/finalized. |
| [setupTimers(options)](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md) | | |

View file

@ -1,13 +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; [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

@ -1,13 +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; [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

@ -4,7 +4,7 @@
## 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.
Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort either when `cancelPending` is called, when the request times out, or when the original `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized.
<b>Signature:</b>

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; [showToast](./kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md)
## SearchInterceptor.showToast property
<b>Signature:</b>
```typescript
protected showToast: () => void;
```

View file

@ -1,13 +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; [timeoutSubscriptions](./kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md)
## SearchInterceptor.timeoutSubscriptions property
The subscriptions from scheduling the automatic timeout for each request.
<b>Signature:</b>
```typescript
protected timeoutSubscriptions: Subscription;
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
http: CoreStart['http'];
http: CoreSetup['http'];
```

View file

@ -14,9 +14,9 @@ export interface SearchInterceptorDeps
| 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> | |
| [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | <code>CoreSetup['http']</code> | |
| [startServices](./kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md) | <code>Promise&lt;[CoreStart, any, unknown]&gt;</code> | |
| [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | <code>ToastsSetup</code> | |
| [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | <code>CoreSetup['uiSettings']</code> | |
| [usageCollector](./kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md) | <code>SearchUsageCollector</code> | |

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; [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) &gt; [application](./kibana-plugin-plugins-data-public.searchinterceptordeps.application.md)
[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; [startServices](./kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md)
## SearchInterceptorDeps.application property
## SearchInterceptorDeps.startServices property
<b>Signature:</b>
```typescript
application: ApplicationStart;
startServices: Promise<[CoreStart, any, unknown]>;
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
toasts: ToastsStart;
toasts: ToastsSetup;
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
uiSettings: CoreStart['uiSettings'];
uiSettings: CoreSetup['uiSettings'];
```

View file

@ -44,6 +44,7 @@ const createSetupContract = (): Setup => {
search: searchServiceMock.createSetupContract(),
fieldFormats: fieldFormatsServiceMock.createSetupContract(),
query: querySetupMock,
__enhance: jest.fn(),
};
};

View file

@ -34,6 +34,7 @@ import {
DataSetupDependencies,
DataStartDependencies,
InternalStartServices,
DataPublicPluginEnhancements,
} from './types';
import { AutocompleteService } from './autocomplete';
import { SearchService } from './search/search_service';
@ -156,16 +157,21 @@ export class DataPublicPlugin
}))
);
const searchService = this.searchService.setup(core, {
expressions,
usageCollection,
getInternalStartServices,
packageInfo: this.packageInfo,
});
return {
autocomplete: this.autocomplete.setup(core),
search: this.searchService.setup(core, {
expressions,
usageCollection,
getInternalStartServices,
packageInfo: this.packageInfo,
}),
search: searchService,
fieldFormats: this.fieldFormatsService.setup(core),
query: queryService,
__enhance: (enhancements: DataPublicPluginEnhancements) => {
searchService.__enhance(enhancements.search);
},
};
}

View file

@ -8,12 +8,12 @@ import { $Values } from '@kbn/utility-types';
import _ from 'lodash';
import { Action } from 'history';
import { ApiResponse } from '@elastic/elasticsearch/lib/Transport';
import { ApplicationStart } from 'kibana/public';
import { Assign } from '@kbn/utility-types';
import { BehaviorSubject } from 'rxjs';
import Boom from 'boom';
import { Component } from 'react';
import { CoreSetup } from 'src/core/public';
import { CoreSetup as CoreSetup_2 } from 'kibana/public';
import { CoreStart } from 'kibana/public';
import { CoreStart as CoreStart_2 } from 'src/core/public';
import { Ensure } from '@kbn/utility-types';
@ -65,7 +65,7 @@ import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/ex
import { Subscription } from 'rxjs';
import { Toast } from 'kibana/public';
import { ToastInputFields } from 'src/core/public/notifications';
import { ToastsStart } from 'kibana/public';
import { ToastsSetup } from 'kibana/public';
import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport';
import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport';
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
@ -222,6 +222,10 @@ export type CustomFilter = Filter & {
//
// @public (undocumented)
export interface DataPublicPluginSetup {
// Warning: (ae-forgotten-export) The symbol "DataPublicPluginEnhancements" needs to be exported by the entry point index.d.ts
//
// @internal (undocumented)
__enhance: (enhancements: DataPublicPluginEnhancements) => void;
// Warning: (ae-forgotten-export) The symbol "AutocompleteSetup" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@ -1714,15 +1718,19 @@ export interface SearchError {
// @public (undocumented)
export class SearchInterceptor {
constructor(deps: SearchInterceptorDeps, requestTimeout?: number | undefined);
// @internal
protected abortController: AbortController;
// @internal (undocumented)
protected application: CoreStart['application'];
// (undocumented)
protected readonly deps: SearchInterceptorDeps;
getPendingCount$: () => Observable<number>;
// (undocumented)
getPendingCount$(): Observable<number>;
// @internal (undocumented)
protected hideToast: () => void;
// @internal
protected longRunningToast?: Toast;
// @internal
protected pendingCount$: BehaviorSubject<number>;
protected pendingCount: number;
// (undocumented)
protected readonly requestTimeout?: number | undefined;
// (undocumented)
@ -1733,8 +1741,9 @@ export class SearchInterceptor {
combinedSignal: AbortSignal;
cleanup: () => void;
};
// (undocumented)
// @internal (undocumented)
protected showToast: () => void;
// @internal
protected timeoutSubscriptions: Subscription;
}
@ -1743,13 +1752,13 @@ export class SearchInterceptor {
// @public (undocumented)
export interface SearchInterceptorDeps {
// (undocumented)
application: ApplicationStart;
http: CoreSetup_2['http'];
// (undocumented)
http: CoreStart['http'];
startServices: Promise<[CoreStart, any, unknown]>;
// (undocumented)
toasts: ToastsStart;
toasts: ToastsSetup;
// (undocumented)
uiSettings: CoreStart['uiSettings'];
uiSettings: CoreSetup_2['uiSettings'];
// Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@ -1980,9 +1989,9 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:54:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:55:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:62:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:71:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)

View file

@ -21,7 +21,14 @@ export * from './aggs';
export * from './expressions';
export * from './tabify';
export { ISearch, ISearchOptions, ISearchGeneric, ISearchSetup, ISearchStart } from './types';
export {
ISearch,
ISearchOptions,
ISearchGeneric,
ISearchSetup,
ISearchStart,
SearchEnhancements,
} from './types';
export { IEsSearchResponse, IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../common/search';

View file

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

View file

@ -17,27 +17,27 @@
* under the License.
*/
import { CoreStart } from '../../../../core/public';
import { CoreSetup } from '../../../../core/public';
import { coreMock } from '../../../../core/public/mocks';
import { IEsSearchRequest } from '../../common/search';
import { SearchInterceptor } from './search_interceptor';
import { AbortError } from '../../common';
let searchInterceptor: SearchInterceptor;
let mockCoreStart: MockedKeys<CoreStart>;
let mockCoreSetup: MockedKeys<CoreSetup>;
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
jest.useFakeTimers();
describe('SearchInterceptor', () => {
beforeEach(() => {
mockCoreStart = coreMock.createStart();
mockCoreSetup = coreMock.createSetup();
searchInterceptor = new SearchInterceptor(
{
toasts: mockCoreStart.notifications.toasts,
application: mockCoreStart.application,
uiSettings: mockCoreStart.uiSettings,
http: mockCoreStart.http,
toasts: mockCoreSetup.notifications.toasts,
startServices: mockCoreSetup.getStartServices(),
uiSettings: mockCoreSetup.uiSettings,
http: mockCoreSetup.http,
},
1000
);
@ -46,7 +46,7 @@ describe('SearchInterceptor', () => {
describe('search', () => {
test('Observable should resolve if fetch is successful', async () => {
const mockResponse: any = { result: 200 };
mockCoreStart.http.fetch.mockResolvedValueOnce(mockResponse);
mockCoreSetup.http.fetch.mockResolvedValueOnce(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
};
@ -58,7 +58,7 @@ describe('SearchInterceptor', () => {
test('Observable should fail if fetch has an error', async () => {
const mockResponse: any = { result: 500 };
mockCoreStart.http.fetch.mockRejectedValueOnce(mockResponse);
mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
};
@ -72,7 +72,7 @@ describe('SearchInterceptor', () => {
});
test('Observable should fail if fetch times out (test merged signal)', async () => {
mockCoreStart.http.fetch.mockImplementationOnce((options: any) => {
mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => {
return new Promise((resolve, reject) => {
options.signal.addEventListener('abort', () => {
reject(new AbortError());
@ -100,7 +100,7 @@ describe('SearchInterceptor', () => {
test('Observable should fail if user aborts (test merged signal)', async () => {
const abortController = new AbortController();
mockCoreStart.http.fetch.mockImplementationOnce((options: any) => {
mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => {
return new Promise((resolve, reject) => {
options.signal.addEventListener('abort', () => {
reject(new AbortError());
@ -136,7 +136,7 @@ describe('SearchInterceptor', () => {
const error = (e: any) => {
expect(e).toBeInstanceOf(AbortError);
expect(mockCoreStart.http.fetch).not.toBeCalled();
expect(mockCoreSetup.http.fetch).not.toBeCalled();
done();
};
response.subscribe({ error });
@ -150,7 +150,7 @@ describe('SearchInterceptor', () => {
pendingCount$.subscribe(pendingNext);
const mockResponse: any = { result: 200 };
mockCoreStart.http.fetch.mockResolvedValue(mockResponse);
mockCoreSetup.http.fetch.mockResolvedValue(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
};
@ -169,7 +169,7 @@ describe('SearchInterceptor', () => {
pendingCount$.subscribe(pendingNext);
const mockResponse: any = { result: 500 };
mockCoreStart.http.fetch.mockRejectedValue(mockResponse);
mockCoreSetup.http.fetch.mockRejectedValue(mockResponse);
const mockRequest: IEsSearchRequest = {
params: {},
};

View file

@ -20,7 +20,7 @@
import { trimEnd } from 'lodash';
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 { Toast, CoreStart, ToastsSetup, CoreSetup } from 'kibana/public';
import { getCombinedSignal, AbortError } from '../../common/utils';
import { IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY } from '../../common/search';
import { ISearchOptions } from './types';
@ -30,39 +30,43 @@ import { SearchUsageCollector } from './collectors';
const LONG_QUERY_NOTIFICATION_DELAY = 10000;
export interface SearchInterceptorDeps {
toasts: ToastsStart;
application: ApplicationStart;
http: CoreStart['http'];
uiSettings: CoreStart['uiSettings'];
toasts: ToastsSetup;
http: CoreSetup['http'];
uiSettings: CoreSetup['uiSettings'];
startServices: Promise<[CoreStart, any, unknown]>;
usageCollector?: SearchUsageCollector;
}
export class SearchInterceptor {
/**
* `abortController` used to signal all searches to abort.
* @internal
*/
protected abortController = new AbortController();
/**
* The number of pending search requests.
*/
protected pendingCount = 0;
/**
* Observable that emits when the number of pending requests changes.
* @internal
*/
protected pendingCount$ = new BehaviorSubject(this.pendingCount);
protected pendingCount$ = new BehaviorSubject(0);
/**
* The subscriptions from scheduling the automatic timeout for each request.
* @internal
*/
protected timeoutSubscriptions: Subscription = new Subscription();
/**
* The current long-running toast (if there is one).
* @internal
*/
protected longRunningToast?: Toast;
/**
* @internal
*/
protected application!: CoreStart['application'];
/**
* This class should be instantiated with a `requestTimeout` corresponding with how many ms after
* requests are initiated that they should automatically cancel.
@ -76,6 +80,10 @@ export class SearchInterceptor {
) {
this.deps.http.addLoadingCountSource(this.pendingCount$);
this.deps.startServices.then(([coreStart]) => {
this.application = coreStart.application;
});
// 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$()
@ -87,9 +95,9 @@ export class SearchInterceptor {
* Returns an `Observable` 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.
*/
public getPendingCount$ = () => {
public getPendingCount$() {
return this.pendingCount$.asObservable();
};
}
protected runSearch(
request: IEsSearchRequest,
@ -112,7 +120,7 @@ export class SearchInterceptor {
/**
* 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.
* `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized.
*/
public search(
request: IEsSearchRequest,
@ -125,11 +133,11 @@ export class SearchInterceptor {
}
const { combinedSignal, cleanup } = this.setupTimers(options);
this.pendingCount$.next(++this.pendingCount);
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
return this.runSearch(request, combinedSignal, options?.strategy).pipe(
finalize(() => {
this.pendingCount$.next(--this.pendingCount);
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
cleanup();
})
);
@ -173,13 +181,16 @@ export class SearchInterceptor {
};
}
/**
* @internal
*/
protected showToast = () => {
if (this.longRunningToast) return;
this.longRunningToast = this.deps.toasts.addInfo(
{
title: 'Your query is taking a while',
text: getLongQueryNotification({
application: this.deps.application,
application: this.application,
}),
},
{
@ -188,6 +199,9 @@ export class SearchInterceptor {
);
};
/**
* @internal
*/
protected hideToast = () => {
if (this.longRunningToast) {
this.deps.toasts.remove(this.longRunningToast);
@ -198,3 +212,5 @@ export class SearchInterceptor {
}
};
}
export type ISearchInterceptor = PublicMethodsOf<SearchInterceptor>;

View file

@ -41,6 +41,7 @@ describe('Search service', () => {
expressions: expressionsPluginMock.createSetupContract(),
} as any);
expect(setup).toHaveProperty('aggs');
expect(setup).toHaveProperty('__enhance');
});
});
@ -49,7 +50,6 @@ describe('Search service', () => {
const start = searchService.start(mockCoreStart, {
indexPatterns: {},
} as any);
expect(start).toHaveProperty('setInterceptor');
expect(start).toHaveProperty('search');
});
});

View file

@ -18,7 +18,7 @@
*/
import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public';
import { ISearchSetup, ISearchStart } from './types';
import { ISearchSetup, ISearchStart, SearchEnhancements } from './types';
import { ExpressionsSetup } from '../../../../plugins/expressions/public';
import { createSearchSource, SearchSource, SearchSourceDependencies } from './search_source';
@ -28,7 +28,7 @@ import { calculateBounds, TimeRange } from '../../common/query';
import { IndexPatternsContract } from '../index_patterns/index_patterns';
import { GetInternalStartServicesFn } from '../types';
import { SearchInterceptor } from './search_interceptor';
import { ISearchInterceptor, SearchInterceptor } from './search_interceptor';
import {
getAggTypes,
getAggTypesFunctions,
@ -54,7 +54,7 @@ interface SearchServiceStartDependencies {
export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
private esClient?: LegacyApiCaller;
private readonly aggTypesRegistry = new AggTypesRegistry();
private searchInterceptor!: SearchInterceptor;
private searchInterceptor!: ISearchInterceptor;
private usageCollector?: SearchUsageCollector;
/**
@ -91,15 +91,6 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
const aggFunctions = getAggTypesFunctions();
aggFunctions.forEach((fn) => expressions.registerFunction(fn));
return {
aggs: {
calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings),
types: aggTypesSetup,
},
};
}
public start(core: CoreStart, dependencies: SearchServiceStartDependencies): ISearchStart {
/**
* A global object that intercepts all searches and provides convenience methods for cancelling
* all pending search requests, as well as getting the number of pending search requests.
@ -109,14 +100,27 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
this.searchInterceptor = new SearchInterceptor(
{
toasts: core.notifications.toasts,
application: core.application,
http: core.http,
uiSettings: core.uiSettings,
startServices: core.getStartServices(),
usageCollector: this.usageCollector!,
},
core.injectedMetadata.getInjectedVar('esRequestTimeout') as number
);
return {
usageCollector: this.usageCollector!,
__enhance: (enhancements: SearchEnhancements) => {
this.searchInterceptor = enhancements.searchInterceptor;
},
aggs: {
calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings),
types: aggTypesSetup,
},
};
}
public start(core: CoreStart, dependencies: SearchServiceStartDependencies): ISearchStart {
const aggTypesStart = this.aggTypesRegistry.start();
const search: ISearchGeneric = (request, options) => {
@ -145,17 +149,12 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
types: aggTypesStart,
},
search,
usageCollector: this.usageCollector!,
searchSource: {
create: createSearchSource(dependencies.indexPatterns, searchSourceDependencies),
createEmpty: () => {
return new SearchSource({}, searchSourceDependencies);
},
},
setInterceptor: (searchInterceptor: SearchInterceptor) => {
// TODO: should an intercepror have a destroy method?
this.searchInterceptor = searchInterceptor;
},
__LEGACY: legacySearch,
};
}

View file

@ -21,7 +21,7 @@ import { Observable } from 'rxjs';
import { PackageInfo } from 'kibana/server';
import { SearchAggsSetup, SearchAggsStart } from './aggs';
import { LegacyApiCaller } from './legacy/es_client';
import { SearchInterceptor } from './search_interceptor';
import { ISearchInterceptor } from './search_interceptor';
import { ISearchSource, SearchSourceFields } from './search_source';
import { SearchUsageCollector } from './collectors';
import {
@ -54,23 +54,33 @@ export interface ISearchStartLegacy {
esClient: LegacyApiCaller;
}
export interface SearchEnhancements {
searchInterceptor: ISearchInterceptor;
}
/**
* The setup contract exposed by the Search plugin exposes the search strategy extension
* point.
*/
export interface ISearchSetup {
aggs: SearchAggsSetup;
usageCollector?: SearchUsageCollector;
/**
* @internal
*/
__enhance: (enhancements: SearchEnhancements) => void;
}
export interface ISearchStart {
aggs: SearchAggsStart;
setInterceptor: (searchInterceptor: SearchInterceptor) => void;
search: ISearchGeneric;
searchSource: {
create: (fields?: SearchSourceFields) => Promise<ISearchSource>;
createEmpty: () => ISearchSource;
};
usageCollector?: SearchUsageCollector;
/**
* @deprecated
* @internal
*/
__LEGACY: ISearchStartLegacy;
}

View file

@ -25,13 +25,17 @@ import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public';
import { AutocompleteSetup, AutocompleteStart } from './autocomplete';
import { FieldFormatsSetup, FieldFormatsStart } from './field_formats';
import { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction } from './actions';
import { ISearchSetup, ISearchStart } from './search';
import { ISearchSetup, ISearchStart, SearchEnhancements } from './search';
import { QuerySetup, QueryStart } from './query';
import { IndexPatternSelectProps } from './ui/index_pattern_select';
import { IndexPatternsContract } from './index_patterns';
import { StatefulSearchBarProps } from './ui/search_bar/create_search_bar';
import { UsageCollectionSetup } from '../../usage_collection/public';
export interface DataPublicPluginEnhancements {
search: SearchEnhancements;
}
export interface DataSetupDependencies {
expressions: ExpressionsSetup;
uiActions: UiActionsSetup;
@ -47,6 +51,10 @@ export interface DataPublicPluginSetup {
search: ISearchSetup;
fieldFormats: FieldFormatsSetup;
query: QuerySetup;
/**
* @internal
*/
__enhance: (enhancements: DataPublicPluginEnhancements) => void;
}
export interface DataPublicPluginStart {

View file

@ -31,20 +31,26 @@ export class DataEnhancedPlugin
KUERY_LANGUAGE_NAME,
setupKqlQuerySuggestionProvider(core)
);
const enhancedSearchInterceptor = new EnhancedSearchInterceptor(
{
toasts: core.notifications.toasts,
http: core.http,
uiSettings: core.uiSettings,
startServices: core.getStartServices(),
usageCollector: data.search.usageCollector,
},
core.injectedMetadata.getInjectedVar('esRequestTimeout') as number
);
data.__enhance({
search: {
searchInterceptor: enhancedSearchInterceptor,
},
});
}
public start(core: CoreStart, plugins: DataEnhancedStartDependencies) {
setAutocompleteService(plugins.data.autocomplete);
const enhancedSearchInterceptor = new EnhancedSearchInterceptor(
{
toasts: core.notifications.toasts,
application: core.application,
http: core.http,
uiSettings: core.uiSettings,
usageCollector: plugins.data.search.usageCollector,
},
core.injectedMetadata.getInjectedVar('esRequestTimeout') as number
);
plugins.data.search.setInterceptor(enhancedSearchInterceptor);
}
}

View file

@ -6,7 +6,7 @@
import { coreMock } from '../../../../../src/core/public/mocks';
import { EnhancedSearchInterceptor } from './search_interceptor';
import { CoreStart } from 'kibana/public';
import { CoreSetup, CoreStart } from 'kibana/public';
import { AbortError } from '../../../../../src/plugins/data/common';
const timeTravel = (msToRun = 0) => {
@ -19,13 +19,14 @@ const error = jest.fn();
const complete = jest.fn();
let searchInterceptor: EnhancedSearchInterceptor;
let mockCoreSetup: MockedKeys<CoreSetup>;
let mockCoreStart: MockedKeys<CoreStart>;
jest.useFakeTimers();
function mockFetchImplementation(responses: any[]) {
let i = 0;
mockCoreStart.http.fetch.mockImplementation(() => {
mockCoreSetup.http.fetch.mockImplementation(() => {
const { time = 0, value = {}, isError = false } = responses[i++];
return new Promise((resolve, reject) =>
setTimeout(() => {
@ -39,6 +40,7 @@ describe('EnhancedSearchInterceptor', () => {
let mockUsageCollector: any;
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
mockCoreStart = coreMock.createStart();
next.mockClear();
@ -54,12 +56,20 @@ describe('EnhancedSearchInterceptor', () => {
trackLongQueryRunBeyondTimeout: jest.fn(),
};
const mockPromise = new Promise((resolve) => {
resolve([
{
application: mockCoreStart.application,
},
]);
});
searchInterceptor = new EnhancedSearchInterceptor(
{
toasts: mockCoreStart.notifications.toasts,
application: mockCoreStart.application,
http: mockCoreStart.http,
uiSettings: mockCoreStart.uiSettings,
toasts: mockCoreSetup.notifications.toasts,
startServices: mockPromise as any,
http: mockCoreSetup.http,
uiSettings: mockCoreSetup.uiSettings,
usageCollector: mockUsageCollector,
},
1000
@ -229,8 +239,8 @@ describe('EnhancedSearchInterceptor', () => {
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
expect(mockCoreStart.http.fetch).toHaveBeenCalledTimes(2);
expect(mockCoreStart.http.delete).toHaveBeenCalled();
expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2);
expect(mockCoreSetup.http.delete).toHaveBeenCalled();
});
test('should not DELETE a running async search on async timeout prior to first response', async () => {
@ -253,8 +263,8 @@ describe('EnhancedSearchInterceptor', () => {
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
expect(mockCoreStart.http.fetch).toHaveBeenCalled();
expect(mockCoreStart.http.delete).not.toHaveBeenCalled();
expect(mockCoreSetup.http.fetch).toHaveBeenCalled();
expect(mockCoreSetup.http.delete).not.toHaveBeenCalled();
});
test('should DELETE a running async search on async timeout after first response', async () => {
@ -285,16 +295,16 @@ describe('EnhancedSearchInterceptor', () => {
expect(next).toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
expect(mockCoreStart.http.fetch).toHaveBeenCalled();
expect(mockCoreStart.http.delete).not.toHaveBeenCalled();
expect(mockCoreSetup.http.fetch).toHaveBeenCalled();
expect(mockCoreSetup.http.delete).not.toHaveBeenCalled();
// Long enough to reach the timeout but not long enough to reach the next response
await timeTravel(1000);
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
expect(mockCoreStart.http.fetch).toHaveBeenCalledTimes(2);
expect(mockCoreStart.http.delete).toHaveBeenCalled();
expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2);
expect(mockCoreSetup.http.delete).toHaveBeenCalled();
});
test('should DELETE a running async search on async timeout on error from fetch', async () => {
@ -327,16 +337,16 @@ describe('EnhancedSearchInterceptor', () => {
expect(next).toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
expect(mockCoreStart.http.fetch).toHaveBeenCalled();
expect(mockCoreStart.http.delete).not.toHaveBeenCalled();
expect(mockCoreSetup.http.fetch).toHaveBeenCalled();
expect(mockCoreSetup.http.delete).not.toHaveBeenCalled();
// Long enough to reach the timeout but not long enough to reach the next response
await timeTravel(10);
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBe(responses[1].value);
expect(mockCoreStart.http.fetch).toHaveBeenCalledTimes(2);
expect(mockCoreStart.http.delete).toHaveBeenCalled();
expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2);
expect(mockCoreSetup.http.delete).toHaveBeenCalled();
});
});
@ -367,7 +377,7 @@ describe('EnhancedSearchInterceptor', () => {
await timeTravel();
const areAllRequestsAborted = mockCoreStart.http.fetch.mock.calls.every(
const areAllRequestsAborted = mockCoreSetup.http.fetch.mock.calls.every(
([{ signal }]) => signal?.aborted
);
expect(areAllRequestsAborted).toBe(true);

View file

@ -20,8 +20,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
/**
* This class should be instantiated with a `requestTimeout` corresponding with how many ms after
* requests are initiated that they should automatically cancel.
* @param toasts The `core.notifications.toasts` service
* @param application The `core.application` service
* @param deps `SearchInterceptorDeps`
* @param requestTimeout Usually config value `elasticsearch.requestTimeout`
*/
constructor(deps: SearchInterceptorDeps, requestTimeout?: number) {
@ -78,7 +77,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
const { combinedSignal, cleanup } = this.setupTimers(options);
const aborted$ = from(toPromise(combinedSignal));
this.pendingCount$.next(++this.pendingCount);
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
return this.runSearch(request, combinedSignal, options?.strategy).pipe(
expand((response) => {
@ -113,7 +112,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
},
}),
finalize(() => {
this.pendingCount$.next(--this.pendingCount);
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
cleanup();
})
);