mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
This commit is contained in:
parent
6a76c1ccfe
commit
b59c6529ab
76 changed files with 1910 additions and 578 deletions
|
@ -17,6 +17,7 @@ export interface ISearchSetup
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | <code>AggsSetup</code> | |
|
||||
| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | <code>ISessionService</code> | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
|
||||
| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | <code>ISessionService</code> | Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
|
||||
| [sessionsClient](./kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md) | <code>ISessionsClient</code> | Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) |
|
||||
| [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | <code>SearchUsageCollector</code> | |
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## ISearchSetup.session property
|
||||
|
||||
session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md)
|
||||
Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) > [sessionsClient](./kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md)
|
||||
|
||||
## ISearchSetup.sessionsClient property
|
||||
|
||||
Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
sessionsClient: ISessionsClient;
|
||||
```
|
|
@ -19,6 +19,7 @@ export interface ISearchStart
|
|||
| [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | <code>AggsStart</code> | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) |
|
||||
| [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | <code>ISearchGeneric</code> | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) |
|
||||
| [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | <code>ISearchStartSearchSource</code> | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) |
|
||||
| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | <code>ISessionService</code> | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
|
||||
| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | <code>ISessionService</code> | Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
|
||||
| [sessionsClient](./kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md) | <code>ISessionsClient</code> | Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) |
|
||||
| [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | <code>(e: Error) => void</code> | |
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## ISearchStart.session property
|
||||
|
||||
session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md)
|
||||
Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [sessionsClient](./kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md)
|
||||
|
||||
## ISearchStart.sessionsClient property
|
||||
|
||||
Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
sessionsClient: ISessionsClient;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md)
|
||||
|
||||
## ISessionsClient type
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type ISessionsClient = PublicContract<SessionsClient>;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md)
|
||||
|
||||
## ISessionService.clear property
|
||||
|
||||
Clears the active session.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
clear: () => void;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [delete](./kibana-plugin-plugins-data-public.isessionservice.delete.md)
|
||||
|
||||
## ISessionService.delete property
|
||||
|
||||
Deletes a session
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
delete: (sessionId: string) => Promise<void>;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [find](./kibana-plugin-plugins-data-public.isessionservice.find.md)
|
||||
|
||||
## ISessionService.find property
|
||||
|
||||
Gets a list of saved sessions
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
find: (options: SearchSessionFindOptions) => Promise<SavedObjectsFindResponse<BackgroundSessionSavedObjectAttributes>>;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [get](./kibana-plugin-plugins-data-public.isessionservice.get.md)
|
||||
|
||||
## ISessionService.get property
|
||||
|
||||
Gets a saved session
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
get: (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md)
|
||||
|
||||
## ISessionService.getSession$ property
|
||||
|
||||
Returns the observable that emits an update every time the session ID changes
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getSession$: () => Observable<string | undefined>;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md)
|
||||
|
||||
## ISessionService.getSessionId property
|
||||
|
||||
Returns the active session ID
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getSessionId: () => string | undefined;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [isRestore](./kibana-plugin-plugins-data-public.isessionservice.isrestore.md)
|
||||
|
||||
## ISessionService.isRestore property
|
||||
|
||||
Whether the active session is restored (i.e. reusing previous search IDs)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
isRestore: () => boolean;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [isStored](./kibana-plugin-plugins-data-public.isessionservice.isstored.md)
|
||||
|
||||
## ISessionService.isStored property
|
||||
|
||||
Whether the active session is already saved (i.e. sent to background)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
isStored: () => boolean;
|
||||
```
|
|
@ -2,28 +2,10 @@
|
|||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md)
|
||||
|
||||
## ISessionService interface
|
||||
## ISessionService type
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface ISessionService
|
||||
export declare type ISessionService = PublicContract<SessionService>;
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) | <code>() => void</code> | Clears the active session. |
|
||||
| [delete](./kibana-plugin-plugins-data-public.isessionservice.delete.md) | <code>(sessionId: string) => Promise<void></code> | Deletes a session |
|
||||
| [find](./kibana-plugin-plugins-data-public.isessionservice.find.md) | <code>(options: SearchSessionFindOptions) => Promise<SavedObjectsFindResponse<BackgroundSessionSavedObjectAttributes>></code> | Gets a list of saved sessions |
|
||||
| [get](./kibana-plugin-plugins-data-public.isessionservice.get.md) | <code>(sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>></code> | Gets a saved session |
|
||||
| [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) | <code>() => Observable<string | undefined></code> | Returns the observable that emits an update every time the session ID changes |
|
||||
| [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) | <code>() => string | undefined</code> | Returns the active session ID |
|
||||
| [isRestore](./kibana-plugin-plugins-data-public.isessionservice.isrestore.md) | <code>() => boolean</code> | Whether the active session is restored (i.e. reusing previous search IDs) |
|
||||
| [isStored](./kibana-plugin-plugins-data-public.isessionservice.isstored.md) | <code>() => boolean</code> | Whether the active session is already saved (i.e. sent to background) |
|
||||
| [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) | <code>(sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>></code> | Restores existing session |
|
||||
| [save](./kibana-plugin-plugins-data-public.isessionservice.save.md) | <code>(name: string, url: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>></code> | Saves a session |
|
||||
| [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) | <code>() => string</code> | Starts a new session |
|
||||
| [update](./kibana-plugin-plugins-data-public.isessionservice.update.md) | <code>(sessionId: string, attributes: Partial<BackgroundSessionSavedObjectAttributes>) => Promise<any></code> | Updates a session |
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md)
|
||||
|
||||
## ISessionService.restore property
|
||||
|
||||
Restores existing session
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
restore: (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [save](./kibana-plugin-plugins-data-public.isessionservice.save.md)
|
||||
|
||||
## ISessionService.save property
|
||||
|
||||
Saves a session
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
save: (name: string, url: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [start](./kibana-plugin-plugins-data-public.isessionservice.start.md)
|
||||
|
||||
## ISessionService.start property
|
||||
|
||||
Starts a new session
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
start: () => string;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [update](./kibana-plugin-plugins-data-public.isessionservice.update.md)
|
||||
|
||||
## ISessionService.update property
|
||||
|
||||
Updates a session
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
update: (sessionId: string, attributes: Partial<BackgroundSessionSavedObjectAttributes>) => Promise<any>;
|
||||
```
|
|
@ -34,6 +34,7 @@
|
|||
| [KBN\_FIELD\_TYPES](./kibana-plugin-plugins-data-public.kbn_field_types.md) | \* |
|
||||
| [METRIC\_TYPES](./kibana-plugin-plugins-data-public.metric_types.md) | |
|
||||
| [QuerySuggestionTypes](./kibana-plugin-plugins-data-public.querysuggestiontypes.md) | |
|
||||
| [SessionState](./kibana-plugin-plugins-data-public.sessionstate.md) | Possible state that current session can be in |
|
||||
| [SortDirection](./kibana-plugin-plugins-data-public.sortdirection.md) | |
|
||||
| [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md) | |
|
||||
|
||||
|
@ -74,7 +75,6 @@
|
|||
| [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. |
|
||||
| [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) | search service |
|
||||
| [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | high level search service |
|
||||
| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | |
|
||||
| [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | |
|
||||
| [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | |
|
||||
| [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state |
|
||||
|
@ -89,6 +89,7 @@
|
|||
| [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | |
|
||||
| [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | |
|
||||
| [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | |
|
||||
| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object |
|
||||
| [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields |
|
||||
| [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* |
|
||||
| [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* |
|
||||
|
@ -166,6 +167,8 @@
|
|||
| [InputTimeRange](./kibana-plugin-plugins-data-public.inputtimerange.md) | |
|
||||
| [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | |
|
||||
| [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | search source interface |
|
||||
| [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | |
|
||||
| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | |
|
||||
| [KibanaContext](./kibana-plugin-plugins-data-public.kibanacontext.md) | |
|
||||
| [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | |
|
||||
| [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md)
|
||||
|
||||
## SearchSessionInfoProvider.getName property
|
||||
|
||||
User-facing name of the session. e.g. will be displayed in background sessions management list
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getName: () => Promise<string>;
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md)
|
||||
|
||||
## SearchSessionInfoProvider.getUrlGeneratorData property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getUrlGeneratorData: () => Promise<{
|
||||
urlGeneratorId: ID;
|
||||
initialState: UrlGeneratorStateMapping[ID]['State'];
|
||||
restoreState: UrlGeneratorStateMapping[ID]['State'];
|
||||
}>;
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md)
|
||||
|
||||
## SearchSessionInfoProvider interface
|
||||
|
||||
Provide info about current search session to be stored in backgroundSearch saved object
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SearchSessionInfoProvider<ID extends UrlGeneratorId = UrlGeneratorId>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md) | <code>() => Promise<string></code> | User-facing name of the session. e.g. will be displayed in background sessions management list |
|
||||
| [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) | <code>() => Promise<{</code><br/><code> urlGeneratorId: ID;</code><br/><code> initialState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> restoreState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> }></code> | |
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SessionState](./kibana-plugin-plugins-data-public.sessionstate.md)
|
||||
|
||||
## SessionState enum
|
||||
|
||||
Possible state that current session can be in
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare enum SessionState
|
||||
```
|
||||
|
||||
## Enumeration Members
|
||||
|
||||
| Member | Value | Description |
|
||||
| --- | --- | --- |
|
||||
| BackgroundCompleted | <code>"backgroundCompleted"</code> | Page load completed with background session created. |
|
||||
| BackgroundLoading | <code>"backgroundLoading"</code> | Search request was sent to the background. The page is loading in background. |
|
||||
| Canceled | <code>"canceled"</code> | Current session requests where explicitly canceled by user Displaying none or partial results |
|
||||
| Completed | <code>"completed"</code> | No action was taken and the page completed loading without background session creation. |
|
||||
| Loading | <code>"loading"</code> | Pending search request has not been sent to the background yet |
|
||||
| None | <code>"none"</code> | Session is not active, e.g. didn't start |
|
||||
| Restored | <code>"restored"</code> | Revisiting the page after background completion |
|
||||
|
|
@ -74,7 +74,7 @@ import { NavAction, SavedDashboardPanel } from '../types';
|
|||
import { showOptionsPopover } from './top_nav/show_options_popover';
|
||||
import { DashboardSaveModal, SaveOptions } from './top_nav/save_modal';
|
||||
import { showCloneModal } from './top_nav/show_clone_modal';
|
||||
import { saveDashboard } from './lib';
|
||||
import { createSessionRestorationDataProvider, saveDashboard } from './lib';
|
||||
import { DashboardStateManager } from './dashboard_state_manager';
|
||||
import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constants';
|
||||
import { getTopNavConfig } from './top_nav/get_top_nav_config';
|
||||
|
@ -150,7 +150,7 @@ export class DashboardAppController {
|
|||
dashboardCapabilities,
|
||||
scopedHistory,
|
||||
embeddableCapabilities: { visualizeCapabilities, mapsCapabilities },
|
||||
data: { query: queryService, search: searchService },
|
||||
data,
|
||||
core: {
|
||||
notifications,
|
||||
overlays,
|
||||
|
@ -168,6 +168,8 @@ export class DashboardAppController {
|
|||
navigation,
|
||||
savedObjectsTagging,
|
||||
}: DashboardAppControllerDependencies) {
|
||||
const queryService = data.query;
|
||||
const searchService = data.search;
|
||||
const filterManager = queryService.filterManager;
|
||||
const timefilter = queryService.timefilter.timefilter;
|
||||
const queryStringManager = queryService.queryString;
|
||||
|
@ -262,6 +264,16 @@ export class DashboardAppController {
|
|||
|
||||
$scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean;
|
||||
|
||||
const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`;
|
||||
|
||||
const getDashTitle = () =>
|
||||
getDashboardTitle(
|
||||
dashboardStateManager.getTitle(),
|
||||
dashboardStateManager.getViewMode(),
|
||||
dashboardStateManager.getIsDirty(timefilter),
|
||||
dashboardStateManager.isNew()
|
||||
);
|
||||
|
||||
const getShouldShowEditHelp = () =>
|
||||
!dashboardStateManager.getPanels().length &&
|
||||
dashboardStateManager.getIsEditMode() &&
|
||||
|
@ -429,6 +441,15 @@ export class DashboardAppController {
|
|||
DashboardContainer
|
||||
>(DASHBOARD_CONTAINER_TYPE);
|
||||
|
||||
searchService.session.setSearchSessionInfoProvider(
|
||||
createSessionRestorationDataProvider({
|
||||
data,
|
||||
getDashboardTitle: () => getDashTitle(),
|
||||
getDashboardId: () => dash.id,
|
||||
getAppState: () => dashboardStateManager.getAppState(),
|
||||
})
|
||||
);
|
||||
|
||||
if (dashboardFactory) {
|
||||
const searchSessionIdFromURL = getSearchSessionIdFromURL(history);
|
||||
if (searchSessionIdFromURL) {
|
||||
|
@ -552,16 +573,6 @@ export class DashboardAppController {
|
|||
filterManager.getFilters()
|
||||
);
|
||||
|
||||
const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`;
|
||||
|
||||
const getDashTitle = () =>
|
||||
getDashboardTitle(
|
||||
dashboardStateManager.getTitle(),
|
||||
dashboardStateManager.getViewMode(),
|
||||
dashboardStateManager.getIsDirty(timefilter),
|
||||
dashboardStateManager.isNew()
|
||||
);
|
||||
|
||||
// Push breadcrumbs to new header navigation
|
||||
const updateBreadcrumbs = () => {
|
||||
chrome.setBreadcrumbs([
|
||||
|
@ -638,6 +649,13 @@ export class DashboardAppController {
|
|||
}
|
||||
};
|
||||
|
||||
const searchServiceSessionRefreshSubscribtion = searchService.session.onRefresh$.subscribe(
|
||||
() => {
|
||||
lastReloadRequestTime = new Date().getTime();
|
||||
refreshDashboardContainer();
|
||||
}
|
||||
);
|
||||
|
||||
const updateStateFromSavedQuery = (savedQuery: SavedQuery) => {
|
||||
const allFilters = filterManager.getFilters();
|
||||
dashboardStateManager.applyFilters(savedQuery.attributes.query, allFilters);
|
||||
|
@ -1199,6 +1217,7 @@ export class DashboardAppController {
|
|||
if (dashboardContainer) {
|
||||
dashboardContainer.destroy();
|
||||
}
|
||||
searchServiceSessionRefreshSubscribtion.unsubscribe();
|
||||
searchService.session.clear();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,3 +21,4 @@ export { saveDashboard } from './save_dashboard';
|
|||
export { getAppStateDefaults } from './get_app_state_defaults';
|
||||
export { migrateAppState } from './migrate_app_state';
|
||||
export { getDashboardIdFromUrl } from './url';
|
||||
export { createSessionRestorationDataProvider } from './session_restoration';
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { DASHBOARD_APP_URL_GENERATOR, DashboardUrlGeneratorState } from '../../url_generator';
|
||||
import { DataPublicPluginStart } from '../../../../data/public';
|
||||
import { DashboardAppState } from '../../types';
|
||||
|
||||
export function createSessionRestorationDataProvider(deps: {
|
||||
data: DataPublicPluginStart;
|
||||
getAppState: () => DashboardAppState;
|
||||
getDashboardTitle: () => string;
|
||||
getDashboardId: () => string;
|
||||
}) {
|
||||
return {
|
||||
getName: async () => deps.getDashboardTitle(),
|
||||
getUrlGeneratorData: async () => {
|
||||
return {
|
||||
urlGeneratorId: DASHBOARD_APP_URL_GENERATOR,
|
||||
initialState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: false }),
|
||||
restoreState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: true }),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getUrlGeneratorState({
|
||||
data,
|
||||
getAppState,
|
||||
getDashboardId,
|
||||
forceAbsoluteTime, // TODO: not implemented
|
||||
}: {
|
||||
data: DataPublicPluginStart;
|
||||
getAppState: () => DashboardAppState;
|
||||
getDashboardId: () => string;
|
||||
forceAbsoluteTime: boolean;
|
||||
}): DashboardUrlGeneratorState {
|
||||
const appState = getAppState();
|
||||
return {
|
||||
dashboardId: getDashboardId(),
|
||||
timeRange: data.query.timefilter.timefilter.getTime(),
|
||||
filters: data.query.filterManager.getFilters(),
|
||||
query: data.query.queryString.formatQuery(appState.query),
|
||||
savedQuery: appState.savedQuery,
|
||||
useHash: false,
|
||||
preserveSavedFilters: false,
|
||||
viewMode: appState.viewMode,
|
||||
panels: getDashboardId() ? undefined : appState.panels,
|
||||
searchSessionId: data.search.session.getSessionId(),
|
||||
};
|
||||
}
|
|
@ -142,6 +142,39 @@ describe('dashboard url generator', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('savedQuery', async () => {
|
||||
const generator = createDashboardUrlGenerator(() =>
|
||||
Promise.resolve({
|
||||
appBasePath: APP_BASE_PATH,
|
||||
useHashedUrl: false,
|
||||
savedDashboardLoader: createMockDashboardLoader(),
|
||||
})
|
||||
);
|
||||
const url = await generator.createUrl!({
|
||||
savedQuery: '__savedQueryId__',
|
||||
});
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"xyz/app/dashboards#/create?_a=(savedQuery:__savedQueryId__)&_g=()"`
|
||||
);
|
||||
expect(url).toContain('__savedQueryId__');
|
||||
});
|
||||
|
||||
test('panels', async () => {
|
||||
const generator = createDashboardUrlGenerator(() =>
|
||||
Promise.resolve({
|
||||
appBasePath: APP_BASE_PATH,
|
||||
useHashedUrl: false,
|
||||
savedDashboardLoader: createMockDashboardLoader(),
|
||||
})
|
||||
);
|
||||
const url = await generator.createUrl!({
|
||||
panels: [{ fakePanelContent: 'fakePanelContent' } as any],
|
||||
});
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"xyz/app/dashboards#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()"`
|
||||
);
|
||||
});
|
||||
|
||||
test('if no useHash setting is given, uses the one was start services', async () => {
|
||||
const generator = createDashboardUrlGenerator(() =>
|
||||
Promise.resolve({
|
||||
|
|
|
@ -30,6 +30,7 @@ import { UrlGeneratorsDefinition } from '../../share/public';
|
|||
import { SavedObjectLoader } from '../../saved_objects/public';
|
||||
import { ViewMode } from '../../embeddable/public';
|
||||
import { DashboardConstants } from './dashboard_constants';
|
||||
import { SavedDashboardPanel } from '../common/types';
|
||||
|
||||
export const STATE_STORAGE_KEY = '_a';
|
||||
export const GLOBAL_STATE_STORAGE_KEY = '_g';
|
||||
|
@ -86,6 +87,16 @@ export interface DashboardUrlGeneratorState {
|
|||
* (Background search)
|
||||
*/
|
||||
searchSessionId?: string;
|
||||
|
||||
/**
|
||||
* List of dashboard panels
|
||||
*/
|
||||
panels?: SavedDashboardPanel[];
|
||||
|
||||
/**
|
||||
* Saved query ID
|
||||
*/
|
||||
savedQuery?: string;
|
||||
}
|
||||
|
||||
export const createDashboardUrlGenerator = (
|
||||
|
@ -137,6 +148,8 @@ export const createDashboardUrlGenerator = (
|
|||
query: state.query,
|
||||
filters: filters?.filter((f) => !esFilters.isFilterPinned(f)),
|
||||
viewMode: state.viewMode,
|
||||
panels: state.panels,
|
||||
savedQuery: state.savedQuery,
|
||||
}),
|
||||
{ useHash },
|
||||
`${appBasePath}#/${hash}`
|
||||
|
|
|
@ -55,7 +55,8 @@ export interface AggTypeConfig<
|
|||
aggConfig: TAggConfig,
|
||||
searchSource: ISearchSource,
|
||||
inspectorRequestAdapter?: RequestAdapter,
|
||||
abortSignal?: AbortSignal
|
||||
abortSignal?: AbortSignal,
|
||||
searchSessionId?: string
|
||||
) => Promise<any>;
|
||||
getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat;
|
||||
getValue?: (agg: TAggConfig, bucket: any) => any;
|
||||
|
@ -182,6 +183,8 @@ export class AggType<
|
|||
* @param searchSourceAggs - SearchSource aggregation configuration
|
||||
* @param resp - Response to the main request
|
||||
* @param nestedSearchSource - the new SearchSource that will be used to make post flight request
|
||||
* @param abortSignal - `AbortSignal` to abort the request
|
||||
* @param searchSessionId - searchSessionId to be used for grouping requests into a single search session
|
||||
* @return {Promise}
|
||||
*/
|
||||
postFlightRequest: (
|
||||
|
@ -190,7 +193,8 @@ export class AggType<
|
|||
aggConfig: TAggConfig,
|
||||
searchSource: ISearchSource,
|
||||
inspectorRequestAdapter?: RequestAdapter,
|
||||
abortSignal?: AbortSignal
|
||||
abortSignal?: AbortSignal,
|
||||
searchSessionId?: string
|
||||
) => Promise<any>;
|
||||
/**
|
||||
* Get the serialized format for the values produced by this agg type,
|
||||
|
|
|
@ -102,7 +102,8 @@ export const getTermsBucketAgg = () =>
|
|||
aggConfig,
|
||||
searchSource,
|
||||
inspectorRequestAdapter,
|
||||
abortSignal
|
||||
abortSignal,
|
||||
searchSessionId
|
||||
) => {
|
||||
if (!resp.aggregations) return resp;
|
||||
const nestedSearchSource = searchSource.createChild();
|
||||
|
@ -124,6 +125,7 @@ export const getTermsBucketAgg = () =>
|
|||
'This request counts the number of documents that fall ' +
|
||||
'outside the criterion of the data buckets.',
|
||||
}),
|
||||
searchSessionId,
|
||||
}
|
||||
);
|
||||
nestedSearchSource.getSearchRequestBody().then((body) => {
|
||||
|
@ -132,7 +134,10 @@ export const getTermsBucketAgg = () =>
|
|||
request.stats(getRequestInspectorStats(nestedSearchSource));
|
||||
}
|
||||
|
||||
const response = await nestedSearchSource.fetch({ abortSignal });
|
||||
const response = await nestedSearchSource.fetch({
|
||||
abortSignal,
|
||||
sessionId: searchSessionId,
|
||||
});
|
||||
if (request) {
|
||||
request
|
||||
.stats(getResponseInspectorStats(response, nestedSearchSource))
|
||||
|
|
|
@ -17,82 +17,19 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server';
|
||||
|
||||
export interface ISessionService {
|
||||
/**
|
||||
* Returns the active session ID
|
||||
* @returns The active session ID
|
||||
*/
|
||||
getSessionId: () => string | undefined;
|
||||
/**
|
||||
* Returns the observable that emits an update every time the session ID changes
|
||||
* @returns `Observable`
|
||||
*/
|
||||
getSession$: () => Observable<string | undefined>;
|
||||
|
||||
/**
|
||||
* Whether the active session is already saved (i.e. sent to background)
|
||||
*/
|
||||
isStored: () => boolean;
|
||||
|
||||
/**
|
||||
* Whether the active session is restored (i.e. reusing previous search IDs)
|
||||
*/
|
||||
isRestore: () => boolean;
|
||||
|
||||
/**
|
||||
* Starts a new session
|
||||
*/
|
||||
start: () => string;
|
||||
|
||||
/**
|
||||
* Restores existing session
|
||||
*/
|
||||
restore: (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>;
|
||||
|
||||
/**
|
||||
* Clears the active session.
|
||||
*/
|
||||
clear: () => void;
|
||||
|
||||
/**
|
||||
* Saves a session
|
||||
*/
|
||||
save: (name: string, url: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>;
|
||||
|
||||
/**
|
||||
* Gets a saved session
|
||||
*/
|
||||
get: (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>;
|
||||
|
||||
/**
|
||||
* Gets a list of saved sessions
|
||||
*/
|
||||
find: (
|
||||
options: SearchSessionFindOptions
|
||||
) => Promise<SavedObjectsFindResponse<BackgroundSessionSavedObjectAttributes>>;
|
||||
|
||||
/**
|
||||
* Updates a session
|
||||
*/
|
||||
update: (
|
||||
sessionId: string,
|
||||
attributes: Partial<BackgroundSessionSavedObjectAttributes>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Deletes a session
|
||||
*/
|
||||
delete: (sessionId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface BackgroundSessionSavedObjectAttributes {
|
||||
/**
|
||||
* User-facing session name to be displayed in session management
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* App that created the session. e.g 'discover'
|
||||
*/
|
||||
appId: string;
|
||||
created: string;
|
||||
expires: string;
|
||||
status: string;
|
||||
urlGeneratorId: string;
|
||||
initialState: Record<string, unknown>;
|
||||
restoreState: Record<string, unknown>;
|
||||
idMapping: Record<string, string>;
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
"requiredPlugins": [
|
||||
"bfetch",
|
||||
"expressions",
|
||||
"uiActions"
|
||||
"uiActions",
|
||||
"share"
|
||||
],
|
||||
"optionalPlugins": ["usageCollection"],
|
||||
"extraPublicDirs": ["common"],
|
||||
|
|
|
@ -385,6 +385,7 @@ export {
|
|||
SearchRequest,
|
||||
SearchSourceFields,
|
||||
SortDirection,
|
||||
SessionState,
|
||||
// expression functions and types
|
||||
EsdslExpressionFunctionDefinition,
|
||||
EsRawResponseExpressionTypeDefinition,
|
||||
|
@ -395,7 +396,12 @@ export {
|
|||
PainlessError,
|
||||
} from './search';
|
||||
|
||||
export type { SearchSource, ISessionService } from './search';
|
||||
export type {
|
||||
SearchSource,
|
||||
ISessionService,
|
||||
SearchSessionInfoProvider,
|
||||
ISessionsClient,
|
||||
} from './search';
|
||||
|
||||
export { ISearchOptions, isErrorResponse, isCompleteResponse, isPartialResponse } from '../common';
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ import { ExpressionValueBoxed } from 'src/plugins/expressions/common';
|
|||
import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils';
|
||||
import { History } from 'history';
|
||||
import { Href } from 'history';
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { IconType } from '@elastic/eui';
|
||||
import { InjectedIntl } from '@kbn/i18n/react';
|
||||
import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public';
|
||||
|
@ -62,7 +63,9 @@ import { PackageInfo } from '@kbn/config';
|
|||
import { Path } from 'history';
|
||||
import { Plugin as Plugin_2 } from 'src/core/public';
|
||||
import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public';
|
||||
import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public';
|
||||
import { PopoverAnchorPosition } from '@elastic/eui';
|
||||
import { PublicContract } from '@kbn/utility-types';
|
||||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { PublicUiSettingsParams } from 'src/core/server/types';
|
||||
import React from 'react';
|
||||
|
@ -82,6 +85,7 @@ import { SavedObjectsFindResponse } from 'kibana/server';
|
|||
import { Search } from '@elastic/elasticsearch/api/requestParams';
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common';
|
||||
import { StartServicesAccessor } from 'kibana/public';
|
||||
import { ToastInputFields } from 'src/core/public/notifications';
|
||||
import { ToastsSetup } from 'kibana/public';
|
||||
import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport';
|
||||
|
@ -1486,6 +1490,7 @@ export interface ISearchSetup {
|
|||
// (undocumented)
|
||||
aggs: AggsSetup;
|
||||
session: ISessionService;
|
||||
sessionsClient: ISessionsClient;
|
||||
// Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
|
@ -1501,6 +1506,7 @@ export interface ISearchStart {
|
|||
search: ISearchGeneric;
|
||||
searchSource: ISearchStartSearchSource;
|
||||
session: ISessionService;
|
||||
sessionsClient: ISessionsClient;
|
||||
// (undocumented)
|
||||
showError: (e: Error) => void;
|
||||
}
|
||||
|
@ -1516,25 +1522,17 @@ export interface ISearchStartSearchSource {
|
|||
// @public (undocumented)
|
||||
export const isErrorResponse: (response?: IKibanaSearchResponse<any> | undefined) => boolean | undefined;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "SessionsClient" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "ISessionsClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type ISessionsClient = PublicContract<SessionsClient>;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "SessionService" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "ISessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface ISessionService {
|
||||
clear: () => void;
|
||||
delete: (sessionId: string) => Promise<void>;
|
||||
// Warning: (ae-forgotten-export) The symbol "SearchSessionFindOptions" needs to be exported by the entry point index.d.ts
|
||||
find: (options: SearchSessionFindOptions) => Promise<SavedObjectsFindResponse<BackgroundSessionSavedObjectAttributes>>;
|
||||
get: (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>;
|
||||
getSession$: () => Observable<string | undefined>;
|
||||
getSessionId: () => string | undefined;
|
||||
isRestore: () => boolean;
|
||||
isStored: () => boolean;
|
||||
// Warning: (ae-forgotten-export) The symbol "BackgroundSessionSavedObjectAttributes" needs to be exported by the entry point index.d.ts
|
||||
restore: (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>;
|
||||
save: (name: string, url: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>;
|
||||
start: () => string;
|
||||
update: (sessionId: string, attributes: Partial<BackgroundSessionSavedObjectAttributes>) => Promise<any>;
|
||||
}
|
||||
export type ISessionService = PublicContract<SessionService>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
|
@ -2115,6 +2113,7 @@ export class SearchInterceptor {
|
|||
timeoutSignal: AbortSignal;
|
||||
combinedSignal: AbortSignal;
|
||||
cleanup: () => void;
|
||||
abort: () => void;
|
||||
};
|
||||
// (undocumented)
|
||||
showError(e: Error): void;
|
||||
|
@ -2143,6 +2142,20 @@ export interface SearchInterceptorDeps {
|
|||
// @internal
|
||||
export type SearchRequest = Record<string, any>;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "UrlGeneratorId" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "SearchSessionInfoProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export interface SearchSessionInfoProvider<ID extends UrlGeneratorId = UrlGeneratorId> {
|
||||
getName: () => Promise<string>;
|
||||
// (undocumented)
|
||||
getUrlGeneratorData: () => Promise<{
|
||||
urlGeneratorId: ID;
|
||||
initialState: UrlGeneratorStateMapping[ID]['State'];
|
||||
restoreState: UrlGeneratorStateMapping[ID]['State'];
|
||||
}>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export class SearchSource {
|
||||
// Warning: (ae-forgotten-export) The symbol "SearchSourceDependencies" needs to be exported by the entry point index.d.ts
|
||||
|
@ -2248,6 +2261,17 @@ export class SearchTimeoutError extends KbnError {
|
|||
mode: TimeoutErrorMode;
|
||||
}
|
||||
|
||||
// @public
|
||||
export enum SessionState {
|
||||
BackgroundCompleted = "backgroundCompleted",
|
||||
BackgroundLoading = "backgroundLoading",
|
||||
Canceled = "canceled",
|
||||
Completed = "completed",
|
||||
Loading = "loading",
|
||||
None = "none",
|
||||
Restored = "restored"
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SortDirection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
@ -2423,22 +2447,23 @@ export const UI_SETTINGS: {
|
|||
// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:429:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:432:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:436: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/search/session/session_service.ts:46:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
|
|
|
@ -182,7 +182,8 @@ export const handleRequest = async ({
|
|||
agg,
|
||||
requestSearchSource,
|
||||
inspectorAdapters.requests,
|
||||
abortSignal
|
||||
abortSignal,
|
||||
searchSessionId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,9 +40,15 @@ export {
|
|||
SearchSourceDependencies,
|
||||
SearchSourceFields,
|
||||
SortDirection,
|
||||
ISessionService,
|
||||
} from '../../common/search';
|
||||
|
||||
export {
|
||||
SessionService,
|
||||
ISessionService,
|
||||
SearchSessionInfoProvider,
|
||||
SessionState,
|
||||
SessionsClient,
|
||||
ISessionsClient,
|
||||
} from './session';
|
||||
export { getEsPreference } from './es_search';
|
||||
|
||||
export { SearchInterceptor, SearchInterceptorDeps } from './search_interceptor';
|
||||
|
|
|
@ -20,13 +20,14 @@
|
|||
import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks';
|
||||
import { searchSourceMock } from './search_source/mocks';
|
||||
import { ISearchSetup, ISearchStart } from './types';
|
||||
import { getSessionServiceMock } from '../../common/mocks';
|
||||
import { getSessionsClientMock, getSessionServiceMock } from './session/mocks';
|
||||
|
||||
function createSetupContract(): jest.Mocked<ISearchSetup> {
|
||||
return {
|
||||
aggs: searchAggsSetupMock(),
|
||||
__enhance: jest.fn(),
|
||||
session: getSessionServiceMock(),
|
||||
sessionsClient: getSessionsClientMock(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -36,6 +37,7 @@ function createStartContract(): jest.Mocked<ISearchStart> {
|
|||
search: jest.fn(),
|
||||
showError: jest.fn(),
|
||||
session: getSessionServiceMock(),
|
||||
sessionsClient: getSessionsClientMock(),
|
||||
searchSource: searchSourceMock.createStartContract(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import { SearchInterceptor } from './search_interceptor';
|
|||
import { AbortError } from '../../../kibana_utils/public';
|
||||
import { SearchTimeoutError, PainlessError, TimeoutErrorMode } from './errors';
|
||||
import { searchServiceMock } from './mocks';
|
||||
import { ISearchStart } from '.';
|
||||
import { ISearchStart, ISessionService } from '.';
|
||||
import { bfetchPluginMock } from '../../../bfetch/public/mocks';
|
||||
import { BfetchPublicSetup } from 'src/plugins/bfetch/public';
|
||||
|
||||
|
@ -104,7 +104,99 @@ describe('SearchInterceptor', () => {
|
|||
params: {},
|
||||
};
|
||||
const response = searchInterceptor.search(mockRequest);
|
||||
expect(response.toPromise()).resolves.toBe(mockResponse);
|
||||
await expect(response.toPromise()).resolves.toBe(mockResponse);
|
||||
});
|
||||
|
||||
describe('Search session', () => {
|
||||
const setup = ({
|
||||
isRestore = false,
|
||||
isStored = false,
|
||||
sessionId,
|
||||
}: {
|
||||
isRestore?: boolean;
|
||||
isStored?: boolean;
|
||||
sessionId?: string;
|
||||
}) => {
|
||||
const sessionServiceMock = searchMock.session as jest.Mocked<ISessionService>;
|
||||
sessionServiceMock.getSessionId.mockImplementation(() => sessionId);
|
||||
sessionServiceMock.isRestore.mockImplementation(() => isRestore);
|
||||
sessionServiceMock.isStored.mockImplementation(() => isStored);
|
||||
fetchMock.mockResolvedValue({ result: 200 });
|
||||
};
|
||||
|
||||
const mockRequest: IEsSearchRequest = {
|
||||
params: {},
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
const sessionServiceMock = searchMock.session as jest.Mocked<ISessionService>;
|
||||
sessionServiceMock.getSessionId.mockReset();
|
||||
sessionServiceMock.isRestore.mockReset();
|
||||
sessionServiceMock.isStored.mockReset();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
test('infers isRestore from session service state', async () => {
|
||||
const sessionId = 'sid';
|
||||
setup({
|
||||
isRestore: true,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
await searchInterceptor.search(mockRequest, { sessionId }).toPromise();
|
||||
expect(fetchMock.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
options: { sessionId: 'sid', isStored: false, isRestore: true },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('infers isStored from session service state', async () => {
|
||||
const sessionId = 'sid';
|
||||
setup({
|
||||
isStored: true,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
await searchInterceptor.search(mockRequest, { sessionId }).toPromise();
|
||||
expect(fetchMock.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
options: { sessionId: 'sid', isStored: true, isRestore: false },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('skips isRestore & isStore in case not a current session Id', async () => {
|
||||
setup({
|
||||
isStored: true,
|
||||
isRestore: true,
|
||||
sessionId: 'session id',
|
||||
});
|
||||
|
||||
await searchInterceptor
|
||||
.search(mockRequest, { sessionId: 'different session id' })
|
||||
.toPromise();
|
||||
expect(fetchMock.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
options: { sessionId: 'different session id', isStored: false, isRestore: false },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('skips isRestore & isStore in case no session Id', async () => {
|
||||
setup({
|
||||
isStored: true,
|
||||
isRestore: true,
|
||||
sessionId: undefined,
|
||||
});
|
||||
|
||||
await searchInterceptor.search(mockRequest, { sessionId: 'sessionId' }).toPromise();
|
||||
expect(fetchMock.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
options: { sessionId: 'sessionId', isStored: false, isRestore: false },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Should throw typed errors', () => {
|
||||
|
|
|
@ -24,12 +24,7 @@ import { PublicMethodsOf } from '@kbn/utility-types';
|
|||
import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { BatchedFunc, BfetchPublicSetup } from 'src/plugins/bfetch/public';
|
||||
import {
|
||||
IKibanaSearchRequest,
|
||||
IKibanaSearchResponse,
|
||||
ISearchOptions,
|
||||
ISessionService,
|
||||
} from '../../common';
|
||||
import { IKibanaSearchRequest, IKibanaSearchResponse, ISearchOptions } from '../../common';
|
||||
import { SearchUsageCollector } from './collectors';
|
||||
import {
|
||||
SearchTimeoutError,
|
||||
|
@ -42,6 +37,7 @@ import {
|
|||
} from './errors';
|
||||
import { toMountPoint } from '../../../kibana_react/public';
|
||||
import { AbortError, getCombinedAbortSignal } from '../../../kibana_utils/public';
|
||||
import { ISessionService } from './session';
|
||||
|
||||
export interface SearchInterceptorDeps {
|
||||
bfetch: BfetchPublicSetup;
|
||||
|
@ -133,10 +129,18 @@ export class SearchInterceptor {
|
|||
options?: ISearchOptions
|
||||
): Promise<IKibanaSearchResponse> {
|
||||
const { abortSignal, ...requestOptions } = options || {};
|
||||
|
||||
const isCurrentSession =
|
||||
options?.sessionId && this.deps.session.getSessionId() === options.sessionId;
|
||||
|
||||
return this.batchedFetch(
|
||||
{
|
||||
request,
|
||||
options: requestOptions,
|
||||
options: {
|
||||
...requestOptions,
|
||||
isStored: isCurrentSession ? this.deps.session.isStored() : false,
|
||||
isRestore: isCurrentSession ? this.deps.session.isRestore() : false,
|
||||
},
|
||||
},
|
||||
abortSignal
|
||||
);
|
||||
|
@ -160,13 +164,18 @@ export class SearchInterceptor {
|
|||
timeoutController.abort();
|
||||
});
|
||||
|
||||
const selfAbortController = new AbortController();
|
||||
|
||||
// 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)
|
||||
// 3. abort() is called on `selfAbortController`. This is used by session service to abort all pending searches that it tracks
|
||||
// in the current session
|
||||
// 4. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines)
|
||||
const signals = [
|
||||
this.abortController.signal,
|
||||
timeoutSignal,
|
||||
selfAbortController.signal,
|
||||
...(abortSignal ? [abortSignal] : []),
|
||||
];
|
||||
|
||||
|
@ -184,6 +193,9 @@ export class SearchInterceptor {
|
|||
timeoutSignal,
|
||||
combinedSignal,
|
||||
cleanup,
|
||||
abort: () => {
|
||||
selfAbortController.abort();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,8 @@ describe('Search service', () => {
|
|||
expect(setup).toHaveProperty('aggs');
|
||||
expect(setup).toHaveProperty('usageCollector');
|
||||
expect(setup).toHaveProperty('__enhance');
|
||||
expect(setup).toHaveProperty('sessionsClient');
|
||||
expect(setup).toHaveProperty('session');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -61,6 +63,8 @@ describe('Search service', () => {
|
|||
expect(start).toHaveProperty('aggs');
|
||||
expect(start).toHaveProperty('search');
|
||||
expect(start).toHaveProperty('searchSource');
|
||||
expect(start).toHaveProperty('sessionsClient');
|
||||
expect(start).toHaveProperty('session');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,7 +28,6 @@ import {
|
|||
kibanaContext,
|
||||
kibanaContextFunction,
|
||||
ISearchGeneric,
|
||||
ISessionService,
|
||||
SearchSourceDependencies,
|
||||
SearchSourceService,
|
||||
} from '../../common/search';
|
||||
|
@ -40,7 +39,7 @@ import { SearchUsageCollector, createUsageCollector } from './collectors';
|
|||
import { UsageCollectionSetup } from '../../../usage_collection/public';
|
||||
import { esdsl, esRawResponse } from './expressions';
|
||||
import { ExpressionsSetup } from '../../../expressions/public';
|
||||
import { SessionService } from './session_service';
|
||||
import { ISessionsClient, ISessionService, SessionsClient, SessionService } from './session';
|
||||
import { ConfigSchema } from '../../config';
|
||||
import {
|
||||
SHARD_DELAY_AGG_NAME,
|
||||
|
@ -67,6 +66,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
private searchInterceptor!: ISearchInterceptor;
|
||||
private usageCollector?: SearchUsageCollector;
|
||||
private sessionService!: ISessionService;
|
||||
private sessionsClient!: ISessionsClient;
|
||||
|
||||
constructor(private initializerContext: PluginInitializerContext<ConfigSchema>) {}
|
||||
|
||||
|
@ -76,7 +76,12 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
): ISearchSetup {
|
||||
this.usageCollector = createUsageCollector(getStartServices, usageCollection);
|
||||
|
||||
this.sessionService = new SessionService(this.initializerContext, getStartServices);
|
||||
this.sessionsClient = new SessionsClient({ http });
|
||||
this.sessionService = new SessionService(
|
||||
this.initializerContext,
|
||||
getStartServices,
|
||||
this.sessionsClient
|
||||
);
|
||||
/**
|
||||
* 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.
|
||||
|
@ -115,6 +120,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
this.searchInterceptor = enhancements.searchInterceptor;
|
||||
},
|
||||
session: this.sessionService,
|
||||
sessionsClient: this.sessionsClient,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -146,6 +152,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
this.searchInterceptor.showError(e);
|
||||
},
|
||||
session: this.sessionService,
|
||||
sessionsClient: this.sessionsClient,
|
||||
searchSource: this.searchSourceService.start(indexPatterns, searchSourceDependencies),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -17,4 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { getSessionServiceMock } from './search/session/mocks';
|
||||
export { SessionService, ISessionService, SearchSessionInfoProvider } from './session_service';
|
||||
export { SessionState } from './session_state';
|
||||
export { SessionsClient, ISessionsClient } from './sessions_client';
|
|
@ -17,8 +17,20 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ISessionService } from './types';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { ISessionsClient } from './sessions_client';
|
||||
import { ISessionService } from './session_service';
|
||||
import { SessionState } from './session_state';
|
||||
|
||||
export function getSessionsClientMock(): jest.Mocked<ISessionsClient> {
|
||||
return {
|
||||
get: jest.fn(),
|
||||
create: jest.fn(),
|
||||
find: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getSessionServiceMock(): jest.Mocked<ISessionService> {
|
||||
return {
|
||||
|
@ -27,12 +39,15 @@ export function getSessionServiceMock(): jest.Mocked<ISessionService> {
|
|||
restore: jest.fn(),
|
||||
getSessionId: jest.fn(),
|
||||
getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()),
|
||||
state$: new BehaviorSubject<SessionState>(SessionState.None).asObservable(),
|
||||
setSearchSessionInfoProvider: jest.fn(),
|
||||
trackSearch: jest.fn((searchDescriptor) => () => {}),
|
||||
destroy: jest.fn(),
|
||||
onRefresh$: new Subject(),
|
||||
refresh: jest.fn(),
|
||||
cancel: jest.fn(),
|
||||
isStored: jest.fn(),
|
||||
isRestore: jest.fn(),
|
||||
save: jest.fn(),
|
||||
get: jest.fn(),
|
||||
find: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
}
|
|
@ -17,20 +17,27 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { SessionService } from './session_service';
|
||||
import { ISessionService } from '../../common';
|
||||
import { coreMock } from '../../../../core/public/mocks';
|
||||
import { SessionService, ISessionService } from './session_service';
|
||||
import { coreMock } from '../../../../../core/public/mocks';
|
||||
import { take, toArray } from 'rxjs/operators';
|
||||
import { getSessionsClientMock } from './mocks';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { SessionState } from './session_state';
|
||||
|
||||
describe('Session service', () => {
|
||||
let sessionService: ISessionService;
|
||||
let state$: BehaviorSubject<SessionState>;
|
||||
|
||||
beforeEach(() => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext();
|
||||
sessionService = new SessionService(
|
||||
initializerContext,
|
||||
coreMock.createSetup().getStartServices
|
||||
coreMock.createSetup().getStartServices,
|
||||
getSessionsClientMock(),
|
||||
{ freezeState: false } // needed to use mocks inside state container
|
||||
);
|
||||
state$ = new BehaviorSubject<SessionState>(SessionState.None);
|
||||
sessionService.state$.subscribe(state$);
|
||||
});
|
||||
|
||||
describe('Session management', () => {
|
||||
|
@ -55,5 +62,35 @@ describe('Session service', () => {
|
|||
|
||||
expect(await emittedValues).toEqual(['1', '2', undefined]);
|
||||
});
|
||||
|
||||
it('Tracks searches for current session', () => {
|
||||
expect(() => sessionService.trackSearch({ abort: () => {} })).toThrowError();
|
||||
expect(state$.getValue()).toBe(SessionState.None);
|
||||
|
||||
sessionService.start();
|
||||
const untrack1 = sessionService.trackSearch({ abort: () => {} });
|
||||
expect(state$.getValue()).toBe(SessionState.Loading);
|
||||
const untrack2 = sessionService.trackSearch({ abort: () => {} });
|
||||
expect(state$.getValue()).toBe(SessionState.Loading);
|
||||
untrack1();
|
||||
expect(state$.getValue()).toBe(SessionState.Loading);
|
||||
untrack2();
|
||||
expect(state$.getValue()).toBe(SessionState.Completed);
|
||||
});
|
||||
|
||||
it('Cancels all tracked searches within current session', async () => {
|
||||
const abort = jest.fn();
|
||||
|
||||
sessionService.start();
|
||||
sessionService.trackSearch({ abort });
|
||||
sessionService.trackSearch({ abort });
|
||||
sessionService.trackSearch({ abort });
|
||||
const untrack = sessionService.trackSearch({ abort });
|
||||
|
||||
untrack();
|
||||
await sessionService.cancel();
|
||||
|
||||
expect(abort).toBeCalledTimes(3);
|
||||
});
|
||||
});
|
||||
});
|
242
src/plugins/data/public/search/session/session_service.ts
Normal file
242
src/plugins/data/public/search/session/session_service.ts
Normal file
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* 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 { PublicContract } from '@kbn/utility-types';
|
||||
import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
|
||||
import { Observable, Subject, Subscription } from 'rxjs';
|
||||
import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public';
|
||||
import { UrlGeneratorId, UrlGeneratorStateMapping } from '../../../../share/public/';
|
||||
import { ConfigSchema } from '../../../config';
|
||||
import { createSessionStateContainer, SessionState, SessionStateContainer } from './session_state';
|
||||
import { ISessionsClient } from './sessions_client';
|
||||
|
||||
export type ISessionService = PublicContract<SessionService>;
|
||||
|
||||
export interface TrackSearchDescriptor {
|
||||
abort: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide info about current search session to be stored in backgroundSearch saved object
|
||||
*/
|
||||
export interface SearchSessionInfoProvider<ID extends UrlGeneratorId = UrlGeneratorId> {
|
||||
/**
|
||||
* User-facing name of the session.
|
||||
* e.g. will be displayed in background sessions management list
|
||||
*/
|
||||
getName: () => Promise<string>;
|
||||
getUrlGeneratorData: () => Promise<{
|
||||
urlGeneratorId: ID;
|
||||
initialState: UrlGeneratorStateMapping[ID]['State'];
|
||||
restoreState: UrlGeneratorStateMapping[ID]['State'];
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for tracking a current search session. Supports only a single session at a time.
|
||||
*/
|
||||
export class SessionService {
|
||||
public readonly state$: Observable<SessionState>;
|
||||
private readonly state: SessionStateContainer<TrackSearchDescriptor>;
|
||||
|
||||
private searchSessionInfoProvider?: SearchSessionInfoProvider;
|
||||
private appChangeSubscription$?: Subscription;
|
||||
private curApp?: string;
|
||||
|
||||
constructor(
|
||||
initializerContext: PluginInitializerContext<ConfigSchema>,
|
||||
getStartServices: StartServicesAccessor,
|
||||
private readonly sessionsClient: ISessionsClient,
|
||||
{ freezeState = true }: { freezeState: boolean } = { freezeState: true }
|
||||
) {
|
||||
const { stateContainer, sessionState$ } = createSessionStateContainer<TrackSearchDescriptor>({
|
||||
freeze: freezeState,
|
||||
});
|
||||
this.state$ = sessionState$;
|
||||
this.state = stateContainer;
|
||||
|
||||
getStartServices().then(([coreStart]) => {
|
||||
// Apps required to clean up their sessions before unmounting
|
||||
// Make sure that apps don't leave sessions open.
|
||||
this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => {
|
||||
if (this.state.get().sessionId) {
|
||||
const message = `Application '${this.curApp}' had an open session while navigating`;
|
||||
if (initializerContext.env.mode.dev) {
|
||||
// TODO: This setTimeout is necessary due to a race condition while navigating.
|
||||
setTimeout(() => {
|
||||
coreStart.fatalErrors.add(message);
|
||||
}, 100);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(message);
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
this.curApp = appName;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a provider of info about current session
|
||||
* This will be used for creating a background session saved object
|
||||
* @param searchSessionInfoProvider
|
||||
*/
|
||||
public setSearchSessionInfoProvider<ID extends UrlGeneratorId = UrlGeneratorId>(
|
||||
searchSessionInfoProvider: SearchSessionInfoProvider<ID> | undefined
|
||||
) {
|
||||
this.searchSessionInfoProvider = searchSessionInfoProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to track pending searches within current session
|
||||
*
|
||||
* @param searchDescriptor - uniq object that will be used to untrack the search
|
||||
* @returns untrack function
|
||||
*/
|
||||
public trackSearch(searchDescriptor: TrackSearchDescriptor): () => void {
|
||||
this.state.transitions.trackSearch(searchDescriptor);
|
||||
return () => {
|
||||
this.state.transitions.unTrackSearch(searchDescriptor);
|
||||
};
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (this.appChangeSubscription$) {
|
||||
this.appChangeSubscription$.unsubscribe();
|
||||
}
|
||||
this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session id
|
||||
*/
|
||||
public getSessionId() {
|
||||
return this.state.get().sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get observable for current session id
|
||||
*/
|
||||
public getSession$() {
|
||||
return this.state.state$.pipe(
|
||||
startWith(this.state.get()),
|
||||
map((s) => s.sessionId),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is current session already saved as SO (send to background)
|
||||
*/
|
||||
public isStored() {
|
||||
return this.state.get().isStored;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is restoring the older saved searches
|
||||
*/
|
||||
public isRestore() {
|
||||
return this.state.get().isRestore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new search session
|
||||
* @returns sessionId
|
||||
*/
|
||||
public start() {
|
||||
this.state.transitions.start();
|
||||
return this.getSessionId()!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore previously saved search session
|
||||
* @param sessionId
|
||||
*/
|
||||
public restore(sessionId: string) {
|
||||
this.state.transitions.restore(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up current state
|
||||
*/
|
||||
public clear() {
|
||||
this.state.transitions.clear();
|
||||
this.setSearchSessionInfoProvider(undefined);
|
||||
}
|
||||
|
||||
private refresh$ = new Subject<void>();
|
||||
/**
|
||||
* Observable emits when search result refresh was requested
|
||||
* For example, search to background UI could have it's own "refresh" button
|
||||
* Application would use this observable to handle user interaction on that button
|
||||
*/
|
||||
public onRefresh$ = this.refresh$.asObservable();
|
||||
|
||||
/**
|
||||
* Request a search results refresh
|
||||
*/
|
||||
public refresh() {
|
||||
this.refresh$.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a cancellation of on-going search requests within current session
|
||||
*/
|
||||
public async cancel(): Promise<void> {
|
||||
const isStoredSession = this.state.get().isStored;
|
||||
this.state.get().pendingSearches.forEach((s) => {
|
||||
s.abort();
|
||||
});
|
||||
this.state.transitions.cancel();
|
||||
if (isStoredSession) {
|
||||
await this.sessionsClient.delete(this.state.get().sessionId!);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current session as SO to get back to results later
|
||||
* (Send to background)
|
||||
*/
|
||||
public async save(): Promise<void> {
|
||||
const sessionId = this.getSessionId();
|
||||
if (!sessionId) throw new Error('No current session');
|
||||
if (!this.curApp) throw new Error('No current app id');
|
||||
const currentSessionInfoProvider = this.searchSessionInfoProvider;
|
||||
if (!currentSessionInfoProvider) throw new Error('No info provider for current session');
|
||||
const [name, { initialState, restoreState, urlGeneratorId }] = await Promise.all([
|
||||
currentSessionInfoProvider.getName(),
|
||||
currentSessionInfoProvider.getUrlGeneratorData(),
|
||||
]);
|
||||
|
||||
await this.sessionsClient.create({
|
||||
name,
|
||||
appId: this.curApp,
|
||||
restoreState: (restoreState as unknown) as Record<string, unknown>,
|
||||
initialState: (initialState as unknown) as Record<string, unknown>,
|
||||
urlGeneratorId,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
// if we are still interested in this result
|
||||
if (this.getSessionId() === sessionId) {
|
||||
this.state.transitions.store();
|
||||
}
|
||||
}
|
||||
}
|
124
src/plugins/data/public/search/session/session_state.test.ts
Normal file
124
src/plugins/data/public/search/session/session_state.test.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 { createSessionStateContainer, SessionState } from './session_state';
|
||||
|
||||
describe('Session state container', () => {
|
||||
const { stateContainer: state } = createSessionStateContainer();
|
||||
|
||||
afterEach(() => {
|
||||
state.transitions.clear();
|
||||
});
|
||||
|
||||
describe('transitions', () => {
|
||||
test('start', () => {
|
||||
state.transitions.start();
|
||||
expect(state.selectors.getState()).toBe(SessionState.None);
|
||||
expect(state.get().sessionId).not.toBeUndefined();
|
||||
});
|
||||
|
||||
test('track', () => {
|
||||
expect(() => state.transitions.trackSearch({})).toThrowError();
|
||||
|
||||
state.transitions.start();
|
||||
state.transitions.trackSearch({});
|
||||
|
||||
expect(state.selectors.getState()).toBe(SessionState.Loading);
|
||||
});
|
||||
|
||||
test('untrack', () => {
|
||||
state.transitions.start();
|
||||
const search = {};
|
||||
state.transitions.trackSearch(search);
|
||||
expect(state.selectors.getState()).toBe(SessionState.Loading);
|
||||
state.transitions.unTrackSearch(search);
|
||||
expect(state.selectors.getState()).toBe(SessionState.Completed);
|
||||
});
|
||||
|
||||
test('clear', () => {
|
||||
state.transitions.start();
|
||||
state.transitions.clear();
|
||||
expect(state.selectors.getState()).toBe(SessionState.None);
|
||||
expect(state.get().sessionId).toBeUndefined();
|
||||
});
|
||||
|
||||
test('cancel', () => {
|
||||
expect(() => state.transitions.cancel()).toThrowError();
|
||||
|
||||
state.transitions.start();
|
||||
const search = {};
|
||||
state.transitions.trackSearch(search);
|
||||
expect(state.selectors.getState()).toBe(SessionState.Loading);
|
||||
state.transitions.cancel();
|
||||
expect(state.selectors.getState()).toBe(SessionState.Canceled);
|
||||
state.transitions.clear();
|
||||
expect(state.selectors.getState()).toBe(SessionState.None);
|
||||
});
|
||||
|
||||
test('store -> completed', () => {
|
||||
expect(() => state.transitions.store()).toThrowError();
|
||||
|
||||
state.transitions.start();
|
||||
const search = {};
|
||||
state.transitions.trackSearch(search);
|
||||
expect(state.selectors.getState()).toBe(SessionState.Loading);
|
||||
state.transitions.store();
|
||||
expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading);
|
||||
state.transitions.unTrackSearch(search);
|
||||
expect(state.selectors.getState()).toBe(SessionState.BackgroundCompleted);
|
||||
state.transitions.clear();
|
||||
expect(state.selectors.getState()).toBe(SessionState.None);
|
||||
});
|
||||
test('store -> cancel', () => {
|
||||
state.transitions.start();
|
||||
const search = {};
|
||||
state.transitions.trackSearch(search);
|
||||
expect(state.selectors.getState()).toBe(SessionState.Loading);
|
||||
state.transitions.store();
|
||||
expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading);
|
||||
state.transitions.cancel();
|
||||
expect(state.selectors.getState()).toBe(SessionState.Canceled);
|
||||
|
||||
state.transitions.trackSearch(search);
|
||||
expect(state.selectors.getState()).toBe(SessionState.Canceled);
|
||||
|
||||
state.transitions.start();
|
||||
expect(state.selectors.getState()).toBe(SessionState.None);
|
||||
});
|
||||
|
||||
test('restore', () => {
|
||||
const id = 'id';
|
||||
state.transitions.restore(id);
|
||||
expect(state.selectors.getState()).toBe(SessionState.None);
|
||||
const search = {};
|
||||
state.transitions.trackSearch(search);
|
||||
expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading);
|
||||
state.transitions.unTrackSearch(search);
|
||||
|
||||
expect(state.selectors.getState()).toBe(SessionState.Restored);
|
||||
expect(() => state.transitions.store()).toThrowError();
|
||||
expect(state.selectors.getState()).toBe(SessionState.Restored);
|
||||
expect(() => state.transitions.cancel()).toThrowError();
|
||||
expect(state.selectors.getState()).toBe(SessionState.Restored);
|
||||
|
||||
state.transitions.start();
|
||||
expect(state.selectors.getState()).toBe(SessionState.None);
|
||||
});
|
||||
});
|
||||
});
|
234
src/plugins/data/public/search/session/session_state.ts
Normal file
234
src/plugins/data/public/search/session/session_state.ts
Normal file
|
@ -0,0 +1,234 @@
|
|||
/*
|
||||
* 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 uuid from 'uuid';
|
||||
import { Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
|
||||
import { createStateContainer, StateContainer } from '../../../../kibana_utils/public';
|
||||
|
||||
/**
|
||||
* Possible state that current session can be in
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum SessionState {
|
||||
/**
|
||||
* Session is not active, e.g. didn't start
|
||||
*/
|
||||
None = 'none',
|
||||
|
||||
/**
|
||||
* Pending search request has not been sent to the background yet
|
||||
*/
|
||||
Loading = 'loading',
|
||||
|
||||
/**
|
||||
* No action was taken and the page completed loading without background session creation.
|
||||
*/
|
||||
Completed = 'completed',
|
||||
|
||||
/**
|
||||
* Search request was sent to the background.
|
||||
* The page is loading in background.
|
||||
*/
|
||||
BackgroundLoading = 'backgroundLoading',
|
||||
|
||||
/**
|
||||
* Page load completed with background session created.
|
||||
*/
|
||||
BackgroundCompleted = 'backgroundCompleted',
|
||||
|
||||
/**
|
||||
* Revisiting the page after background completion
|
||||
*/
|
||||
Restored = 'restored',
|
||||
|
||||
/**
|
||||
* Current session requests where explicitly canceled by user
|
||||
* Displaying none or partial results
|
||||
*/
|
||||
Canceled = 'canceled',
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal state of SessionService
|
||||
* {@link SessionState} is inferred from this state
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export interface SessionStateInternal<SearchDescriptor = unknown> {
|
||||
/**
|
||||
* Current session Id
|
||||
* Empty means there is no current active session.
|
||||
*/
|
||||
sessionId?: string;
|
||||
|
||||
/**
|
||||
* Has the session already been stored (i.e. "sent to background")?
|
||||
*/
|
||||
isStored: boolean;
|
||||
|
||||
/**
|
||||
* Is this session a restored session (have these requests already been made, and we're just
|
||||
* looking to re-use the previous search IDs)?
|
||||
*/
|
||||
isRestore: boolean;
|
||||
|
||||
/**
|
||||
* Set of currently running searches
|
||||
* within a session and any info associated with them
|
||||
*/
|
||||
pendingSearches: SearchDescriptor[];
|
||||
|
||||
/**
|
||||
* There was at least a single search in this session
|
||||
*/
|
||||
isStarted: boolean;
|
||||
|
||||
/**
|
||||
* If user has explicitly canceled search requests
|
||||
*/
|
||||
isCanceled: boolean;
|
||||
}
|
||||
|
||||
const createSessionDefaultState: <
|
||||
SearchDescriptor = unknown
|
||||
>() => SessionStateInternal<SearchDescriptor> = () => ({
|
||||
sessionId: undefined,
|
||||
isStored: false,
|
||||
isRestore: false,
|
||||
isCanceled: false,
|
||||
isStarted: false,
|
||||
pendingSearches: [],
|
||||
});
|
||||
|
||||
export interface SessionPureTransitions<
|
||||
SearchDescriptor = unknown,
|
||||
S = SessionStateInternal<SearchDescriptor>
|
||||
> {
|
||||
start: (state: S) => () => S;
|
||||
restore: (state: S) => (sessionId: string) => S;
|
||||
clear: (state: S) => () => S;
|
||||
store: (state: S) => () => S;
|
||||
trackSearch: (state: S) => (search: SearchDescriptor) => S;
|
||||
unTrackSearch: (state: S) => (search: SearchDescriptor) => S;
|
||||
cancel: (state: S) => () => S;
|
||||
}
|
||||
|
||||
export const sessionPureTransitions: SessionPureTransitions = {
|
||||
start: (state) => () => ({ ...createSessionDefaultState(), sessionId: uuid.v4() }),
|
||||
restore: (state) => (sessionId: string) => ({
|
||||
...createSessionDefaultState(),
|
||||
sessionId,
|
||||
isRestore: true,
|
||||
isStored: true,
|
||||
}),
|
||||
clear: (state) => () => createSessionDefaultState(),
|
||||
store: (state) => () => {
|
||||
if (!state.sessionId) throw new Error("Can't store session. Missing sessionId");
|
||||
if (state.isStored || state.isRestore)
|
||||
throw new Error('Can\'t store because current session is already stored"');
|
||||
return {
|
||||
...state,
|
||||
isStored: true,
|
||||
};
|
||||
},
|
||||
trackSearch: (state) => (search) => {
|
||||
if (!state.sessionId) throw new Error("Can't track search. Missing sessionId");
|
||||
return {
|
||||
...state,
|
||||
isStarted: true,
|
||||
pendingSearches: state.pendingSearches.concat(search),
|
||||
};
|
||||
},
|
||||
unTrackSearch: (state) => (search) => {
|
||||
return {
|
||||
...state,
|
||||
pendingSearches: state.pendingSearches.filter((s) => s !== search),
|
||||
};
|
||||
},
|
||||
cancel: (state) => () => {
|
||||
if (!state.sessionId) throw new Error("Can't cancel searches. Missing sessionId");
|
||||
if (state.isRestore) throw new Error("Can't cancel searches when restoring older searches");
|
||||
return {
|
||||
...state,
|
||||
pendingSearches: [],
|
||||
isCanceled: true,
|
||||
isStored: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export interface SessionPureSelectors<
|
||||
SearchDescriptor = unknown,
|
||||
S = SessionStateInternal<SearchDescriptor>
|
||||
> {
|
||||
getState: (state: S) => () => SessionState;
|
||||
}
|
||||
|
||||
export const sessionPureSelectors: SessionPureSelectors = {
|
||||
getState: (state) => () => {
|
||||
if (!state.sessionId) return SessionState.None;
|
||||
if (!state.isStarted) return SessionState.None;
|
||||
if (state.isCanceled) return SessionState.Canceled;
|
||||
switch (true) {
|
||||
case state.isRestore:
|
||||
return state.pendingSearches.length > 0
|
||||
? SessionState.BackgroundLoading
|
||||
: SessionState.Restored;
|
||||
case state.isStored:
|
||||
return state.pendingSearches.length > 0
|
||||
? SessionState.BackgroundLoading
|
||||
: SessionState.BackgroundCompleted;
|
||||
default:
|
||||
return state.pendingSearches.length > 0 ? SessionState.Loading : SessionState.Completed;
|
||||
}
|
||||
return SessionState.None;
|
||||
},
|
||||
};
|
||||
|
||||
export type SessionStateContainer<SearchDescriptor = unknown> = StateContainer<
|
||||
SessionStateInternal<SearchDescriptor>,
|
||||
SessionPureTransitions<SearchDescriptor>,
|
||||
SessionPureSelectors<SearchDescriptor>
|
||||
>;
|
||||
|
||||
export const createSessionStateContainer = <SearchDescriptor = unknown>(
|
||||
{ freeze = true }: { freeze: boolean } = { freeze: true }
|
||||
): {
|
||||
stateContainer: SessionStateContainer<SearchDescriptor>;
|
||||
sessionState$: Observable<SessionState>;
|
||||
} => {
|
||||
const stateContainer = createStateContainer(
|
||||
createSessionDefaultState(),
|
||||
sessionPureTransitions,
|
||||
sessionPureSelectors,
|
||||
freeze ? undefined : { freeze: (s) => s }
|
||||
) as SessionStateContainer<SearchDescriptor>;
|
||||
|
||||
const sessionState$: Observable<SessionState> = stateContainer.state$.pipe(
|
||||
map(() => stateContainer.selectors.getState()),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1)
|
||||
);
|
||||
return {
|
||||
stateContainer,
|
||||
sessionState$,
|
||||
};
|
||||
};
|
91
src/plugins/data/public/search/session/sessions_client.ts
Normal file
91
src/plugins/data/public/search/session/sessions_client.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { PublicContract } from '@kbn/utility-types';
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server';
|
||||
import { BackgroundSessionSavedObjectAttributes, SearchSessionFindOptions } from '../../../common';
|
||||
|
||||
export type ISessionsClient = PublicContract<SessionsClient>;
|
||||
export interface SessionsClientDeps {
|
||||
http: HttpSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
* CRUD backgroundSession SO
|
||||
*/
|
||||
export class SessionsClient {
|
||||
private readonly http: HttpSetup;
|
||||
|
||||
constructor(deps: SessionsClientDeps) {
|
||||
this.http = deps.http;
|
||||
}
|
||||
|
||||
public get(sessionId: string): Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> {
|
||||
return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`);
|
||||
}
|
||||
|
||||
public create({
|
||||
name,
|
||||
appId,
|
||||
urlGeneratorId,
|
||||
initialState,
|
||||
restoreState,
|
||||
sessionId,
|
||||
}: {
|
||||
name: string;
|
||||
appId: string;
|
||||
initialState: Record<string, unknown>;
|
||||
restoreState: Record<string, unknown>;
|
||||
urlGeneratorId: string;
|
||||
sessionId: string;
|
||||
}): Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> {
|
||||
return this.http.post(`/internal/session`, {
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
initialState,
|
||||
restoreState,
|
||||
sessionId,
|
||||
appId,
|
||||
urlGeneratorId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
public find(
|
||||
options: SearchSessionFindOptions
|
||||
): Promise<SavedObjectsFindResponse<BackgroundSessionSavedObjectAttributes>> {
|
||||
return this.http!.post(`/internal/session`, {
|
||||
body: JSON.stringify(options),
|
||||
});
|
||||
}
|
||||
|
||||
public update(
|
||||
sessionId: string,
|
||||
attributes: Partial<BackgroundSessionSavedObjectAttributes>
|
||||
): Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> {
|
||||
return this.http!.put(`/internal/session/${encodeURIComponent(sessionId)}`, {
|
||||
body: JSON.stringify(attributes),
|
||||
});
|
||||
}
|
||||
|
||||
public delete(sessionId: string): Promise<void> {
|
||||
return this.http!.delete(`/internal/session/${encodeURIComponent(sessionId)}`);
|
||||
}
|
||||
}
|
|
@ -1,149 +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 uuid from 'uuid';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { HttpStart, PluginInitializerContext, StartServicesAccessor } from 'kibana/public';
|
||||
import { ConfigSchema } from '../../config';
|
||||
import {
|
||||
ISessionService,
|
||||
BackgroundSessionSavedObjectAttributes,
|
||||
SearchSessionFindOptions,
|
||||
} from '../../common';
|
||||
|
||||
export class SessionService implements ISessionService {
|
||||
private session$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
private get sessionId() {
|
||||
return this.session$.getValue();
|
||||
}
|
||||
private appChangeSubscription$?: Subscription;
|
||||
private curApp?: string;
|
||||
private http!: HttpStart;
|
||||
|
||||
/**
|
||||
* Has the session already been stored (i.e. "sent to background")?
|
||||
*/
|
||||
private _isStored: boolean = false;
|
||||
|
||||
/**
|
||||
* Is this session a restored session (have these requests already been made, and we're just
|
||||
* looking to re-use the previous search IDs)?
|
||||
*/
|
||||
private _isRestore: boolean = false;
|
||||
|
||||
constructor(
|
||||
initializerContext: PluginInitializerContext<ConfigSchema>,
|
||||
getStartServices: StartServicesAccessor
|
||||
) {
|
||||
/*
|
||||
Make sure that apps don't leave sessions open.
|
||||
*/
|
||||
getStartServices().then(([coreStart]) => {
|
||||
this.http = coreStart.http;
|
||||
|
||||
this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => {
|
||||
if (this.sessionId) {
|
||||
const message = `Application '${this.curApp}' had an open session while navigating`;
|
||||
if (initializerContext.env.mode.dev) {
|
||||
// TODO: This setTimeout is necessary due to a race condition while navigating.
|
||||
setTimeout(() => {
|
||||
coreStart.fatalErrors.add(message);
|
||||
}, 100);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(message);
|
||||
}
|
||||
}
|
||||
this.curApp = appName;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.appChangeSubscription$?.unsubscribe();
|
||||
}
|
||||
|
||||
public getSessionId() {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
public getSession$() {
|
||||
return this.session$.asObservable();
|
||||
}
|
||||
|
||||
public isStored() {
|
||||
return this._isStored;
|
||||
}
|
||||
|
||||
public isRestore() {
|
||||
return this._isRestore;
|
||||
}
|
||||
|
||||
public start() {
|
||||
this._isStored = false;
|
||||
this._isRestore = false;
|
||||
this.session$.next(uuid.v4());
|
||||
return this.sessionId!;
|
||||
}
|
||||
|
||||
public restore(sessionId: string) {
|
||||
this._isStored = true;
|
||||
this._isRestore = true;
|
||||
this.session$.next(sessionId);
|
||||
return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this._isStored = false;
|
||||
this._isRestore = false;
|
||||
this.session$.next(undefined);
|
||||
}
|
||||
|
||||
public async save(name: string, url: string) {
|
||||
const response = await this.http.post(`/internal/session`, {
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
url,
|
||||
sessionId: this.sessionId,
|
||||
}),
|
||||
});
|
||||
this._isStored = true;
|
||||
return response;
|
||||
}
|
||||
|
||||
public get(sessionId: string) {
|
||||
return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`);
|
||||
}
|
||||
|
||||
public find(options: SearchSessionFindOptions) {
|
||||
return this.http.post(`/internal/session`, {
|
||||
body: JSON.stringify(options),
|
||||
});
|
||||
}
|
||||
|
||||
public update(sessionId: string, attributes: Partial<BackgroundSessionSavedObjectAttributes>) {
|
||||
return this.http.put(`/internal/session/${encodeURIComponent(sessionId)}`, {
|
||||
body: JSON.stringify(attributes),
|
||||
});
|
||||
}
|
||||
|
||||
public delete(sessionId: string) {
|
||||
return this.http.delete(`/internal/session/${encodeURIComponent(sessionId)}`);
|
||||
}
|
||||
}
|
|
@ -21,9 +21,10 @@ import { PackageInfo } from 'kibana/server';
|
|||
import { ISearchInterceptor } from './search_interceptor';
|
||||
import { SearchUsageCollector } from './collectors';
|
||||
import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } from './aggs';
|
||||
import { ISearchGeneric, ISessionService, ISearchStartSearchSource } from '../../common/search';
|
||||
import { ISearchGeneric, ISearchStartSearchSource } from '../../common/search';
|
||||
import { IndexPatternsContract } from '../../common/index_patterns/index_patterns';
|
||||
import { UsageCollectionSetup } from '../../../usage_collection/public';
|
||||
import { ISessionsClient, ISessionService } from './session';
|
||||
|
||||
export { ISearchStartSearchSource };
|
||||
|
||||
|
@ -39,10 +40,15 @@ export interface ISearchSetup {
|
|||
aggs: AggsSetup;
|
||||
usageCollector?: SearchUsageCollector;
|
||||
/**
|
||||
* session management
|
||||
* Current session management
|
||||
* {@link ISessionService}
|
||||
*/
|
||||
session: ISessionService;
|
||||
/**
|
||||
* Background search sessions SO CRUD
|
||||
* {@link ISessionsClient}
|
||||
*/
|
||||
sessionsClient: ISessionsClient;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -73,10 +79,15 @@ export interface ISearchStart {
|
|||
*/
|
||||
searchSource: ISearchStartSearchSource;
|
||||
/**
|
||||
* session management
|
||||
* Current session management
|
||||
* {@link ISessionService}
|
||||
*/
|
||||
session: ISessionService;
|
||||
/**
|
||||
* Background search sessions SO CRUD
|
||||
* {@link ISessionsClient}
|
||||
*/
|
||||
sessionsClient: ISessionsClient;
|
||||
}
|
||||
|
||||
export { SEARCH_EVENT_TYPE } from './collectors';
|
||||
|
|
|
@ -39,6 +39,12 @@ export const backgroundSessionMapping: SavedObjectsType = {
|
|||
status: {
|
||||
type: 'keyword',
|
||||
},
|
||||
appId: {
|
||||
type: 'keyword',
|
||||
},
|
||||
urlGeneratorId: {
|
||||
type: 'keyword',
|
||||
},
|
||||
initialState: {
|
||||
type: 'object',
|
||||
enabled: false,
|
||||
|
|
|
@ -28,19 +28,31 @@ export function registerSessionRoutes(router: IRouter): void {
|
|||
body: schema.object({
|
||||
sessionId: schema.string(),
|
||||
name: schema.string(),
|
||||
appId: schema.string(),
|
||||
expires: schema.maybe(schema.string()),
|
||||
urlGeneratorId: schema.string(),
|
||||
initialState: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||
restoreState: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, res) => {
|
||||
const { sessionId, name, expires, initialState, restoreState } = request.body;
|
||||
const {
|
||||
sessionId,
|
||||
name,
|
||||
expires,
|
||||
initialState,
|
||||
restoreState,
|
||||
appId,
|
||||
urlGeneratorId,
|
||||
} = request.body;
|
||||
|
||||
try {
|
||||
const response = await context.search!.session.save(sessionId, {
|
||||
name,
|
||||
appId,
|
||||
expires,
|
||||
urlGeneratorId,
|
||||
initialState,
|
||||
restoreState,
|
||||
});
|
||||
|
|
|
@ -33,6 +33,8 @@ describe('BackgroundSessionService', () => {
|
|||
type: BACKGROUND_SESSION_TYPE,
|
||||
attributes: {
|
||||
name: 'my_name',
|
||||
appId: 'my_app_id',
|
||||
urlGeneratorId: 'my_url_generator_id',
|
||||
idMapping: {},
|
||||
},
|
||||
references: [],
|
||||
|
@ -121,6 +123,8 @@ describe('BackgroundSessionService', () => {
|
|||
const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
|
||||
const isStored = false;
|
||||
const name = 'my saved background search session';
|
||||
const appId = 'my_app_id';
|
||||
const urlGeneratorId = 'my_url_generator_id';
|
||||
const created = new Date().toISOString();
|
||||
const expires = new Date().toISOString();
|
||||
|
||||
|
@ -133,7 +137,11 @@ describe('BackgroundSessionService', () => {
|
|||
|
||||
expect(savedObjectsClient.update).not.toHaveBeenCalled();
|
||||
|
||||
await service.save(sessionId, { name, created, expires }, { savedObjectsClient });
|
||||
await service.save(
|
||||
sessionId,
|
||||
{ name, created, expires, appId, urlGeneratorId },
|
||||
{ savedObjectsClient }
|
||||
);
|
||||
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith(
|
||||
BACKGROUND_SESSION_TYPE,
|
||||
|
@ -145,6 +153,8 @@ describe('BackgroundSessionService', () => {
|
|||
restoreState: {},
|
||||
status: BackgroundSessionStatus.IN_PROGRESS,
|
||||
idMapping: { [requestHash]: searchId },
|
||||
appId,
|
||||
urlGeneratorId,
|
||||
},
|
||||
{ id: sessionId }
|
||||
);
|
||||
|
@ -215,6 +225,8 @@ describe('BackgroundSessionService', () => {
|
|||
type: BACKGROUND_SESSION_TYPE,
|
||||
attributes: {
|
||||
name: 'my_name',
|
||||
appId: 'my_app_id',
|
||||
urlGeneratorId: 'my_url_generator_id',
|
||||
idMapping: { [requestHash]: searchId },
|
||||
},
|
||||
references: [],
|
||||
|
|
|
@ -64,20 +64,34 @@ export class BackgroundSessionService {
|
|||
sessionId: string,
|
||||
{
|
||||
name,
|
||||
appId,
|
||||
created = new Date().toISOString(),
|
||||
expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(),
|
||||
status = BackgroundSessionStatus.IN_PROGRESS,
|
||||
urlGeneratorId,
|
||||
initialState = {},
|
||||
restoreState = {},
|
||||
}: Partial<BackgroundSessionSavedObjectAttributes>,
|
||||
{ savedObjectsClient }: BackgroundSessionDependencies
|
||||
) => {
|
||||
if (!name) throw new Error('Name is required');
|
||||
if (!appId) throw new Error('AppId is required');
|
||||
if (!urlGeneratorId) throw new Error('UrlGeneratorId is required');
|
||||
|
||||
// Get the mapping of request hash/search ID for this session
|
||||
const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map<string, string>();
|
||||
const idMapping = Object.fromEntries(searchMap.entries());
|
||||
const attributes = { name, created, expires, status, initialState, restoreState, idMapping };
|
||||
const attributes = {
|
||||
name,
|
||||
created,
|
||||
expires,
|
||||
status,
|
||||
initialState,
|
||||
restoreState,
|
||||
idMapping,
|
||||
urlGeneratorId,
|
||||
appId,
|
||||
};
|
||||
const session = await savedObjectsClient.create<BackgroundSessionSavedObjectAttributes>(
|
||||
BACKGROUND_SESSION_TYPE,
|
||||
attributes,
|
||||
|
|
|
@ -23,7 +23,7 @@ import { debounceTime } from 'rxjs/operators';
|
|||
import moment from 'moment';
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getState, splitState } from './discover_state';
|
||||
import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state';
|
||||
|
||||
import { RequestAdapter } from '../../../../inspector/public';
|
||||
import {
|
||||
|
@ -60,14 +60,14 @@ import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_patte
|
|||
import { addFatalError } from '../../../../kibana_legacy/public';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator';
|
||||
import { removeQueryParam, getQueryParams } from '../../../../kibana_utils/public';
|
||||
import { getQueryParams, removeQueryParam } from '../../../../kibana_utils/public';
|
||||
import {
|
||||
DEFAULT_COLUMNS_SETTING,
|
||||
MODIFY_COLUMNS_ON_SWITCH,
|
||||
SAMPLE_SIZE_SETTING,
|
||||
SEARCH_ON_PAGE_LOAD_SETTING,
|
||||
} from '../../../common';
|
||||
import { resolveIndexPattern, loadIndexPattern } from '../helpers/resolve_index_pattern';
|
||||
import { loadIndexPattern, resolveIndexPattern } from '../helpers/resolve_index_pattern';
|
||||
import { getTopNavLinks } from '../components/top_nav/get_top_nav_links';
|
||||
import { updateSearchSource } from '../helpers/update_search_source';
|
||||
import { calcFieldCounts } from '../helpers/calc_field_counts';
|
||||
|
@ -85,7 +85,7 @@ const {
|
|||
toastNotifications,
|
||||
uiSettings: config,
|
||||
trackUiMetric,
|
||||
} = services;
|
||||
} = getServices();
|
||||
|
||||
const fetchStatuses = {
|
||||
UNINITIALIZED: 'uninitialized',
|
||||
|
@ -204,12 +204,20 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
|
|||
// used for restoring background session
|
||||
let isInitialSearch = true;
|
||||
|
||||
// search session requested a data refresh
|
||||
subscriptions.add(
|
||||
data.search.session.onRefresh$.subscribe(() => {
|
||||
refetch$.next();
|
||||
})
|
||||
);
|
||||
|
||||
const state = getState({
|
||||
getStateDefaults,
|
||||
storeInSessionStorage: config.get('state:storeInSessionStorage'),
|
||||
history,
|
||||
toasts: core.notifications.toasts,
|
||||
});
|
||||
|
||||
const {
|
||||
appStateContainer,
|
||||
startSync: startStateSync,
|
||||
|
@ -280,6 +288,14 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
|
|||
}
|
||||
});
|
||||
|
||||
data.search.session.setSearchSessionInfoProvider(
|
||||
createSearchSessionRestorationDataProvider({
|
||||
appStateContainer,
|
||||
data,
|
||||
getSavedSearchId: () => savedSearch.id,
|
||||
})
|
||||
);
|
||||
|
||||
$scope.setIndexPattern = async (id) => {
|
||||
const nextIndexPattern = await indexPatterns.get(id);
|
||||
if (nextIndexPattern) {
|
||||
|
|
|
@ -20,15 +20,23 @@ import { isEqual } from 'lodash';
|
|||
import { History } from 'history';
|
||||
import { NotificationsStart } from 'kibana/public';
|
||||
import {
|
||||
createStateContainer,
|
||||
createKbnUrlStateStorage,
|
||||
syncState,
|
||||
ReduxLikeStateContainer,
|
||||
createStateContainer,
|
||||
IKbnUrlStateStorage,
|
||||
ReduxLikeStateContainer,
|
||||
StateContainer,
|
||||
syncState,
|
||||
withNotifyOnErrors,
|
||||
} from '../../../../kibana_utils/public';
|
||||
import { esFilters, Filter, Query } from '../../../../data/public';
|
||||
import {
|
||||
DataPublicPluginStart,
|
||||
esFilters,
|
||||
Filter,
|
||||
Query,
|
||||
SearchSessionInfoProvider,
|
||||
} from '../../../../data/public';
|
||||
import { migrateLegacyQuery } from '../helpers/migrate_legacy_query';
|
||||
import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../url_generator';
|
||||
|
||||
export interface AppState {
|
||||
/**
|
||||
|
@ -247,3 +255,47 @@ export function isEqualState(stateA: AppState, stateB: AppState) {
|
|||
const { filters: stateBFilters = [], ...stateBPartial } = stateB;
|
||||
return isEqual(stateAPartial, stateBPartial) && isEqualFilters(stateAFilters, stateBFilters);
|
||||
}
|
||||
|
||||
export function createSearchSessionRestorationDataProvider(deps: {
|
||||
appStateContainer: StateContainer<AppState>;
|
||||
data: DataPublicPluginStart;
|
||||
getSavedSearchId: () => string | undefined;
|
||||
}): SearchSessionInfoProvider {
|
||||
return {
|
||||
getName: async () => 'Discover',
|
||||
getUrlGeneratorData: async () => {
|
||||
return {
|
||||
urlGeneratorId: DISCOVER_APP_URL_GENERATOR,
|
||||
initialState: createUrlGeneratorState({ ...deps, forceAbsoluteTime: false }),
|
||||
restoreState: createUrlGeneratorState({ ...deps, forceAbsoluteTime: true }),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createUrlGeneratorState({
|
||||
appStateContainer,
|
||||
data,
|
||||
getSavedSearchId,
|
||||
forceAbsoluteTime, // TODO: not implemented
|
||||
}: {
|
||||
appStateContainer: StateContainer<AppState>;
|
||||
data: DataPublicPluginStart;
|
||||
getSavedSearchId: () => string | undefined;
|
||||
forceAbsoluteTime: boolean;
|
||||
}): DiscoverUrlGeneratorState {
|
||||
const appState = appStateContainer.get();
|
||||
return {
|
||||
filters: data.query.filterManager.getFilters(),
|
||||
indexPatternId: appState.index,
|
||||
query: appState.query,
|
||||
savedSearchId: getSavedSearchId(),
|
||||
timeRange: data.query.timefilter.timefilter.getTime(), // TODO: handle relative time range
|
||||
searchSessionId: data.search.session.getSessionId(),
|
||||
columns: appState.columns,
|
||||
sort: appState.sort,
|
||||
savedQuery: appState.savedQuery,
|
||||
interval: appState.interval,
|
||||
useHash: false,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -221,6 +221,19 @@ describe('Discover url generator', () => {
|
|||
expect(url).toContain('__test__');
|
||||
});
|
||||
|
||||
test('can specify columns, interval, sort and savedQuery', async () => {
|
||||
const { generator } = await setup();
|
||||
const url = await generator.createUrl({
|
||||
columns: ['_source'],
|
||||
interval: 'auto',
|
||||
sort: [['timestamp, asc']],
|
||||
savedQuery: '__savedQueryId__',
|
||||
});
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"xyz/app/discover#/?_g=()&_a=(columns:!(_source),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"`
|
||||
);
|
||||
});
|
||||
|
||||
describe('useHash property', () => {
|
||||
describe('when default useHash is set to false', () => {
|
||||
test('when using default, sets index pattern ID in the generated URL', async () => {
|
||||
|
|
|
@ -52,7 +52,7 @@ export interface DiscoverUrlGeneratorState {
|
|||
refreshInterval?: RefreshInterval;
|
||||
|
||||
/**
|
||||
* Optionally apply filers.
|
||||
* Optionally apply filters.
|
||||
*/
|
||||
filters?: Filter[];
|
||||
|
||||
|
@ -72,6 +72,24 @@ export interface DiscoverUrlGeneratorState {
|
|||
* Background search session id
|
||||
*/
|
||||
searchSessionId?: string;
|
||||
|
||||
/**
|
||||
* Columns displayed in the table
|
||||
*/
|
||||
columns?: string[];
|
||||
|
||||
/**
|
||||
* Used interval of the histogram
|
||||
*/
|
||||
interval?: string;
|
||||
/**
|
||||
* Array of the used sorting [[field,direction],...]
|
||||
*/
|
||||
sort?: string[][];
|
||||
/**
|
||||
* id of the used saved query
|
||||
*/
|
||||
savedQuery?: string;
|
||||
}
|
||||
|
||||
interface Params {
|
||||
|
@ -88,20 +106,28 @@ export class DiscoverUrlGenerator
|
|||
public readonly id = DISCOVER_APP_URL_GENERATOR;
|
||||
|
||||
public readonly createUrl = async ({
|
||||
useHash = this.params.useHash,
|
||||
filters,
|
||||
indexPatternId,
|
||||
query,
|
||||
refreshInterval,
|
||||
savedSearchId,
|
||||
timeRange,
|
||||
useHash = this.params.useHash,
|
||||
searchSessionId,
|
||||
columns,
|
||||
savedQuery,
|
||||
sort,
|
||||
interval,
|
||||
}: DiscoverUrlGeneratorState): Promise<string> => {
|
||||
const savedSearchPath = savedSearchId ? encodeURIComponent(savedSearchId) : '';
|
||||
const appState: {
|
||||
query?: Query;
|
||||
filters?: Filter[];
|
||||
index?: string;
|
||||
columns?: string[];
|
||||
interval?: string;
|
||||
sort?: string[][];
|
||||
savedQuery?: string;
|
||||
} = {};
|
||||
const queryState: QueryState = {};
|
||||
|
||||
|
@ -109,6 +135,10 @@ export class DiscoverUrlGenerator
|
|||
if (filters && filters.length)
|
||||
appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f));
|
||||
if (indexPatternId) appState.index = indexPatternId;
|
||||
if (columns) appState.columns = columns;
|
||||
if (savedQuery) appState.savedQuery = savedQuery;
|
||||
if (sort) appState.sort = sort;
|
||||
if (interval) appState.interval = interval;
|
||||
|
||||
if (timeRange) queryState.time = timeRange;
|
||||
if (filters && filters.length)
|
||||
|
|
|
@ -34,6 +34,7 @@ import { ExclusiveUnion } from '@elastic/eui';
|
|||
import { ExpressionAstFunction } from 'src/plugins/expressions/common';
|
||||
import { History } from 'history';
|
||||
import { Href } from 'history';
|
||||
import { HttpSetup as HttpSetup_2 } from 'kibana/public';
|
||||
import { I18nStart as I18nStart_2 } from 'src/core/public';
|
||||
import { IconType } from '@elastic/eui';
|
||||
import { ISearchOptions } from 'src/plugins/data/public';
|
||||
|
@ -56,7 +57,9 @@ import { OverlayStart as OverlayStart_2 } from 'src/core/public';
|
|||
import { PackageInfo } from '@kbn/config';
|
||||
import { Path } from 'history';
|
||||
import { PluginInitializerContext } from 'src/core/public';
|
||||
import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { PublicContract } from '@kbn/utility-types';
|
||||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { PublicUiSettingsParams } from 'src/core/server/types';
|
||||
import React from 'react';
|
||||
|
@ -77,6 +80,7 @@ import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/ex
|
|||
import { ShallowPromise } from '@kbn/utility-types';
|
||||
import { SimpleSavedObject as SimpleSavedObject_2 } from 'src/core/public';
|
||||
import { Start as Start_2 } from 'src/plugins/inspector/public';
|
||||
import { StartServicesAccessor as StartServicesAccessor_2 } from 'kibana/public';
|
||||
import { ToastInputFields as ToastInputFields_2 } from 'src/core/public/notifications';
|
||||
import { ToastsSetup as ToastsSetup_2 } from 'kibana/public';
|
||||
import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport';
|
||||
|
|
|
@ -68,6 +68,7 @@ export class DataEnhancedPlugin
|
|||
React.createElement(
|
||||
createConnectedBackgroundSessionIndicator({
|
||||
sessionService: plugins.data.search.session,
|
||||
application: core.application,
|
||||
})
|
||||
)
|
||||
),
|
||||
|
|
|
@ -9,9 +9,10 @@ import { EnhancedSearchInterceptor } from './search_interceptor';
|
|||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import { UI_SETTINGS } from '../../../../../src/plugins/data/common';
|
||||
import { AbortError } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import { SearchTimeoutError } from 'src/plugins/data/public';
|
||||
import { ISessionService, SearchTimeoutError, SessionState } from 'src/plugins/data/public';
|
||||
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
|
||||
import { bfetchPluginMock } from '../../../../../src/plugins/bfetch/public/mocks';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
const timeTravel = (msToRun = 0) => {
|
||||
jest.advanceTimersByTime(msToRun);
|
||||
|
@ -43,11 +44,18 @@ function mockFetchImplementation(responses: any[]) {
|
|||
|
||||
describe('EnhancedSearchInterceptor', () => {
|
||||
let mockUsageCollector: any;
|
||||
let sessionService: jest.Mocked<ISessionService>;
|
||||
let sessionState$: BehaviorSubject<SessionState>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCoreSetup = coreMock.createSetup();
|
||||
mockCoreStart = coreMock.createStart();
|
||||
sessionState$ = new BehaviorSubject<SessionState>(SessionState.None);
|
||||
const dataPluginMockStart = dataPluginMock.createStartContract();
|
||||
sessionService = {
|
||||
...(dataPluginMockStart.search.session as jest.Mocked<ISessionService>),
|
||||
state$: sessionState$,
|
||||
};
|
||||
fetchMock = jest.fn();
|
||||
|
||||
mockCoreSetup.uiSettings.get.mockImplementation((name: string) => {
|
||||
|
@ -87,7 +95,7 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
http: mockCoreSetup.http,
|
||||
uiSettings: mockCoreSetup.uiSettings,
|
||||
usageCollector: mockUsageCollector,
|
||||
session: dataPluginMockStart.search.session,
|
||||
session: sessionService,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -144,6 +152,7 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockFetchImplementation(responses);
|
||||
|
||||
const response = searchInterceptor.search({}, { pollInterval: 0 });
|
||||
|
@ -361,6 +370,54 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(mockCoreSetup.http.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should NOT DELETE a running SAVED async search on abort', async () => {
|
||||
const sessionId = 'sessionId';
|
||||
sessionService.getSessionId.mockImplementation(() => sessionId);
|
||||
const responses = [
|
||||
{
|
||||
time: 10,
|
||||
value: {
|
||||
isPartial: true,
|
||||
isRunning: true,
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
time: 300,
|
||||
value: {
|
||||
isPartial: false,
|
||||
isRunning: false,
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
mockFetchImplementation(responses);
|
||||
|
||||
const abortController = new AbortController();
|
||||
setTimeout(() => abortController.abort(), 250);
|
||||
|
||||
const response = searchInterceptor.search(
|
||||
{},
|
||||
{ abortSignal: abortController.signal, pollInterval: 0, sessionId }
|
||||
);
|
||||
response.subscribe({ next, error });
|
||||
|
||||
await timeTravel(10);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
|
||||
sessionState$.next(SessionState.BackgroundLoading);
|
||||
|
||||
await timeTravel(240);
|
||||
|
||||
expect(error).toHaveBeenCalled();
|
||||
expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(mockCoreSetup.http.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelPending', () => {
|
||||
|
@ -395,4 +452,108 @@ describe('EnhancedSearchInterceptor', () => {
|
|||
expect(mockUsageCollector.trackQueriesCancelled).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('session', () => {
|
||||
beforeEach(() => {
|
||||
const responses = [
|
||||
{
|
||||
time: 10,
|
||||
value: {
|
||||
isPartial: true,
|
||||
isRunning: true,
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
time: 300,
|
||||
value: {
|
||||
isPartial: false,
|
||||
isRunning: false,
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockFetchImplementation(responses);
|
||||
});
|
||||
|
||||
test('should track searches', async () => {
|
||||
const sessionId = 'sessionId';
|
||||
sessionService.getSessionId.mockImplementation(() => sessionId);
|
||||
|
||||
const untrack = jest.fn();
|
||||
sessionService.trackSearch.mockImplementation(() => untrack);
|
||||
|
||||
const response = searchInterceptor.search({}, { pollInterval: 0, sessionId });
|
||||
response.subscribe({ next, error });
|
||||
await timeTravel(10);
|
||||
expect(sessionService.trackSearch).toBeCalledTimes(1);
|
||||
expect(untrack).not.toBeCalled();
|
||||
await timeTravel(300);
|
||||
expect(sessionService.trackSearch).toBeCalledTimes(1);
|
||||
expect(untrack).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('session service should be able to cancel search', async () => {
|
||||
const sessionId = 'sessionId';
|
||||
sessionService.getSessionId.mockImplementation(() => sessionId);
|
||||
|
||||
const untrack = jest.fn();
|
||||
sessionService.trackSearch.mockImplementation(() => untrack);
|
||||
|
||||
const response = searchInterceptor.search({}, { pollInterval: 0, sessionId });
|
||||
response.subscribe({ next, error });
|
||||
await timeTravel(10);
|
||||
expect(sessionService.trackSearch).toBeCalledTimes(1);
|
||||
|
||||
const abort = sessionService.trackSearch.mock.calls[0][0].abort;
|
||||
expect(abort).toBeInstanceOf(Function);
|
||||
|
||||
abort();
|
||||
|
||||
await timeTravel(10);
|
||||
|
||||
expect(error).toHaveBeenCalled();
|
||||
expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
|
||||
});
|
||||
|
||||
test("don't track non current session searches", async () => {
|
||||
const sessionId = 'sessionId';
|
||||
sessionService.getSessionId.mockImplementation(() => sessionId);
|
||||
|
||||
const untrack = jest.fn();
|
||||
sessionService.trackSearch.mockImplementation(() => untrack);
|
||||
|
||||
const response1 = searchInterceptor.search(
|
||||
{},
|
||||
{ pollInterval: 0, sessionId: 'something different' }
|
||||
);
|
||||
response1.subscribe({ next, error });
|
||||
|
||||
const response2 = searchInterceptor.search({}, { pollInterval: 0, sessionId: undefined });
|
||||
response2.subscribe({ next, error });
|
||||
|
||||
await timeTravel(10);
|
||||
expect(sessionService.trackSearch).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("don't track if no current session", async () => {
|
||||
sessionService.getSessionId.mockImplementation(() => undefined);
|
||||
|
||||
const untrack = jest.fn();
|
||||
sessionService.trackSearch.mockImplementation(() => untrack);
|
||||
|
||||
const response1 = searchInterceptor.search(
|
||||
{},
|
||||
{ pollInterval: 0, sessionId: 'something different' }
|
||||
);
|
||||
response1.subscribe({ next, error });
|
||||
|
||||
const response2 = searchInterceptor.search({}, { pollInterval: 0, sessionId: undefined });
|
||||
response2.subscribe({ next, error });
|
||||
|
||||
await timeTravel(10);
|
||||
expect(sessionService.trackSearch).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,13 +5,14 @@
|
|||
*/
|
||||
|
||||
import { throwError, Subscription } from 'rxjs';
|
||||
import { tap, finalize, catchError } from 'rxjs/operators';
|
||||
import { tap, finalize, catchError, filter, take, skip } from 'rxjs/operators';
|
||||
import {
|
||||
TimeoutErrorMode,
|
||||
SearchInterceptor,
|
||||
SearchInterceptorDeps,
|
||||
UI_SETTINGS,
|
||||
IKibanaSearchRequest,
|
||||
SessionState,
|
||||
} from '../../../../../src/plugins/data/public';
|
||||
import { AbortError } from '../../../../../src/plugins/kibana_utils/common';
|
||||
import { ENHANCED_ES_SEARCH_STRATEGY, IAsyncSearchOptions, pollSearch } from '../../common';
|
||||
|
@ -54,7 +55,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
|
|||
};
|
||||
|
||||
public search({ id, ...request }: IKibanaSearchRequest, options: IAsyncSearchOptions = {}) {
|
||||
const { combinedSignal, timeoutSignal, cleanup } = this.setupAbortSignal({
|
||||
const { combinedSignal, timeoutSignal, cleanup, abort } = this.setupAbortSignal({
|
||||
abortSignal: options.abortSignal,
|
||||
timeout: this.searchTimeout,
|
||||
});
|
||||
|
@ -63,16 +64,41 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
|
|||
const search = () => this.runSearch({ id, ...request }, searchOptions);
|
||||
|
||||
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
|
||||
const isCurrentSession = () =>
|
||||
!!options.sessionId && options.sessionId === this.deps.session.getSessionId();
|
||||
|
||||
const untrackSearch = isCurrentSession() && this.deps.session.trackSearch({ abort });
|
||||
|
||||
// track if this search's session will be send to background
|
||||
// if yes, then we don't need to cancel this search when it is aborted
|
||||
let isSavedToBackground = false;
|
||||
const savedToBackgroundSub =
|
||||
isCurrentSession() &&
|
||||
this.deps.session.state$
|
||||
.pipe(
|
||||
skip(1), // ignore any state, we are only interested in transition x -> BackgroundLoading
|
||||
filter((state) => isCurrentSession() && state === SessionState.BackgroundLoading),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(() => {
|
||||
isSavedToBackground = true;
|
||||
});
|
||||
|
||||
return pollSearch(search, { ...options, abortSignal: combinedSignal }).pipe(
|
||||
tap((response) => (id = response.id)),
|
||||
catchError((e: AbortError) => {
|
||||
if (id) this.deps.http.delete(`/internal/search/${strategy}/${id}`);
|
||||
if (id && !isSavedToBackground) this.deps.http.delete(`/internal/search/${strategy}/${id}`);
|
||||
return throwError(this.handleSearchError(e, timeoutSignal, options));
|
||||
}),
|
||||
finalize(() => {
|
||||
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
|
||||
cleanup();
|
||||
if (untrackSearch && isCurrentSession()) {
|
||||
untrackSearch();
|
||||
}
|
||||
if (savedToBackgroundSub) {
|
||||
savedToBackgroundSub.unsubscribe();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,24 +7,24 @@
|
|||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { BackgroundSessionIndicator } from './background_session_indicator';
|
||||
import { BackgroundSessionViewState } from '../connected_background_session_indicator';
|
||||
import { SessionState } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
storiesOf('components/BackgroundSessionIndicator', module).add('default', () => (
|
||||
<>
|
||||
<div>
|
||||
<BackgroundSessionIndicator state={BackgroundSessionViewState.Loading} />
|
||||
<BackgroundSessionIndicator state={SessionState.Loading} />
|
||||
</div>
|
||||
<div>
|
||||
<BackgroundSessionIndicator state={BackgroundSessionViewState.Completed} />
|
||||
<BackgroundSessionIndicator state={SessionState.Completed} />
|
||||
</div>
|
||||
<div>
|
||||
<BackgroundSessionIndicator state={BackgroundSessionViewState.BackgroundLoading} />
|
||||
<BackgroundSessionIndicator state={SessionState.BackgroundLoading} />
|
||||
</div>
|
||||
<div>
|
||||
<BackgroundSessionIndicator state={BackgroundSessionViewState.BackgroundCompleted} />
|
||||
<BackgroundSessionIndicator state={SessionState.BackgroundCompleted} />
|
||||
</div>
|
||||
<div>
|
||||
<BackgroundSessionIndicator state={BackgroundSessionViewState.Restored} />
|
||||
<BackgroundSessionIndicator state={SessionState.Restored} />
|
||||
</div>
|
||||
</>
|
||||
));
|
||||
|
|
|
@ -8,8 +8,8 @@ import React, { ReactNode } from 'react';
|
|||
import { screen, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { BackgroundSessionIndicator } from './background_session_indicator';
|
||||
import { BackgroundSessionViewState } from '../connected_background_session_indicator';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { SessionState } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
function Container({ children }: { children?: ReactNode }) {
|
||||
return <IntlProvider locale="en">{children}</IntlProvider>;
|
||||
|
@ -19,7 +19,7 @@ test('Loading state', async () => {
|
|||
const onCancel = jest.fn();
|
||||
render(
|
||||
<Container>
|
||||
<BackgroundSessionIndicator state={BackgroundSessionViewState.Loading} onCancel={onCancel} />
|
||||
<BackgroundSessionIndicator state={SessionState.Loading} onCancel={onCancel} />
|
||||
</Container>
|
||||
);
|
||||
|
||||
|
@ -33,10 +33,7 @@ test('Completed state', async () => {
|
|||
const onSave = jest.fn();
|
||||
render(
|
||||
<Container>
|
||||
<BackgroundSessionIndicator
|
||||
state={BackgroundSessionViewState.Completed}
|
||||
onSaveResults={onSave}
|
||||
/>
|
||||
<BackgroundSessionIndicator state={SessionState.Completed} onSaveResults={onSave} />
|
||||
</Container>
|
||||
);
|
||||
|
||||
|
@ -50,10 +47,7 @@ test('Loading in the background state', async () => {
|
|||
const onCancel = jest.fn();
|
||||
render(
|
||||
<Container>
|
||||
<BackgroundSessionIndicator
|
||||
state={BackgroundSessionViewState.BackgroundLoading}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
<BackgroundSessionIndicator state={SessionState.BackgroundLoading} onCancel={onCancel} />
|
||||
</Container>
|
||||
);
|
||||
|
||||
|
@ -64,30 +58,26 @@ test('Loading in the background state', async () => {
|
|||
});
|
||||
|
||||
test('BackgroundCompleted state', async () => {
|
||||
const onViewSession = jest.fn();
|
||||
render(
|
||||
<Container>
|
||||
<BackgroundSessionIndicator
|
||||
state={BackgroundSessionViewState.BackgroundCompleted}
|
||||
onViewBackgroundSessions={onViewSession}
|
||||
state={SessionState.BackgroundCompleted}
|
||||
viewBackgroundSessionsLink={'__link__'}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Results loaded in the background'));
|
||||
await userEvent.click(screen.getByText('View background sessions'));
|
||||
|
||||
expect(onViewSession).toBeCalled();
|
||||
expect(screen.getByRole('link', { name: 'View background sessions' }).getAttribute('href')).toBe(
|
||||
'__link__'
|
||||
);
|
||||
});
|
||||
|
||||
test('Restored state', async () => {
|
||||
const onRefresh = jest.fn();
|
||||
render(
|
||||
<Container>
|
||||
<BackgroundSessionIndicator
|
||||
state={BackgroundSessionViewState.Restored}
|
||||
onRefresh={onRefresh}
|
||||
/>
|
||||
<BackgroundSessionIndicator state={SessionState.Restored} onRefresh={onRefresh} />
|
||||
</Container>
|
||||
);
|
||||
|
||||
|
@ -96,3 +86,17 @@ test('Restored state', async () => {
|
|||
|
||||
expect(onRefresh).toBeCalled();
|
||||
});
|
||||
|
||||
test('Canceled state', async () => {
|
||||
const onRefresh = jest.fn();
|
||||
render(
|
||||
<Container>
|
||||
<BackgroundSessionIndicator state={SessionState.Canceled} onRefresh={onRefresh} />
|
||||
</Container>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Canceled'));
|
||||
await userEvent.click(screen.getByText('Refresh'));
|
||||
|
||||
expect(onRefresh).toBeCalled();
|
||||
});
|
||||
|
|
|
@ -19,14 +19,15 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { BackgroundSessionViewState } from '../connected_background_session_indicator';
|
||||
|
||||
import './background_session_indicator.scss';
|
||||
import { SessionState } from '../../../../../../../src/plugins/data/public/';
|
||||
|
||||
export interface BackgroundSessionIndicatorProps {
|
||||
state: BackgroundSessionViewState;
|
||||
state: SessionState;
|
||||
onContinueInBackground?: () => void;
|
||||
onCancel?: () => void;
|
||||
onViewBackgroundSessions?: () => void;
|
||||
viewBackgroundSessionsLink?: string;
|
||||
onSaveResults?: () => void;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
@ -34,7 +35,11 @@ export interface BackgroundSessionIndicatorProps {
|
|||
type ActionButtonProps = BackgroundSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps };
|
||||
|
||||
const CancelButton = ({ onCancel = () => {}, buttonProps = {} }: ActionButtonProps) => (
|
||||
<EuiButtonEmpty onClick={onCancel} {...buttonProps}>
|
||||
<EuiButtonEmpty
|
||||
onClick={onCancel}
|
||||
data-test-subj={'backgroundSessionIndicatorCancelBtn'}
|
||||
{...buttonProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.data.backgroundSessionIndicator.cancelButtonText"
|
||||
defaultMessage="Cancel"
|
||||
|
@ -46,7 +51,11 @@ const ContinueInBackgroundButton = ({
|
|||
onContinueInBackground = () => {},
|
||||
buttonProps = {},
|
||||
}: ActionButtonProps) => (
|
||||
<EuiButtonEmpty onClick={onContinueInBackground} {...buttonProps}>
|
||||
<EuiButtonEmpty
|
||||
onClick={onContinueInBackground}
|
||||
data-test-subj={'backgroundSessionIndicatorContinueInBackgroundBtn'}
|
||||
{...buttonProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.data.backgroundSessionIndicator.continueInBackgroundButtonText"
|
||||
defaultMessage="Continue in background"
|
||||
|
@ -55,11 +64,14 @@ const ContinueInBackgroundButton = ({
|
|||
);
|
||||
|
||||
const ViewBackgroundSessionsButton = ({
|
||||
onViewBackgroundSessions = () => {},
|
||||
viewBackgroundSessionsLink = 'management',
|
||||
buttonProps = {},
|
||||
}: ActionButtonProps) => (
|
||||
// TODO: make this a link
|
||||
<EuiButtonEmpty onClick={onViewBackgroundSessions} {...buttonProps}>
|
||||
<EuiButtonEmpty
|
||||
href={viewBackgroundSessionsLink}
|
||||
data-test-subj={'backgroundSessionIndicatorViewBackgroundSessionsLink'}
|
||||
{...buttonProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.data.backgroundSessionIndicator.viewBackgroundSessionsLinkText"
|
||||
defaultMessage="View background sessions"
|
||||
|
@ -68,7 +80,11 @@ const ViewBackgroundSessionsButton = ({
|
|||
);
|
||||
|
||||
const RefreshButton = ({ onRefresh = () => {}, buttonProps = {} }: ActionButtonProps) => (
|
||||
<EuiButtonEmpty onClick={onRefresh} {...buttonProps}>
|
||||
<EuiButtonEmpty
|
||||
onClick={onRefresh}
|
||||
data-test-subj={'backgroundSessionIndicatorRefreshBtn'}
|
||||
{...buttonProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.data.backgroundSessionIndicator.refreshButtonText"
|
||||
defaultMessage="Refresh"
|
||||
|
@ -77,7 +93,11 @@ const RefreshButton = ({ onRefresh = () => {}, buttonProps = {} }: ActionButtonP
|
|||
);
|
||||
|
||||
const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButtonProps) => (
|
||||
<EuiButtonEmpty onClick={onSaveResults} {...buttonProps}>
|
||||
<EuiButtonEmpty
|
||||
onClick={onSaveResults}
|
||||
data-test-subj={'backgroundSessionIndicatorSaveBtn'}
|
||||
{...buttonProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.data.backgroundSessionIndicator.saveButtonText"
|
||||
defaultMessage="Save"
|
||||
|
@ -86,16 +106,19 @@ const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButton
|
|||
);
|
||||
|
||||
const backgroundSessionIndicatorViewStateToProps: {
|
||||
[state in BackgroundSessionViewState]: {
|
||||
button: Pick<EuiButtonIconProps, 'color' | 'iconType' | 'aria-label'> & { tooltipText: string };
|
||||
[state in SessionState]: {
|
||||
button: Pick<EuiButtonIconProps, 'color' | 'iconType' | 'aria-label'> & {
|
||||
tooltipText: string;
|
||||
};
|
||||
popover: {
|
||||
text: string;
|
||||
primaryAction?: React.ComponentType<ActionButtonProps>;
|
||||
secondaryAction?: React.ComponentType<ActionButtonProps>;
|
||||
};
|
||||
};
|
||||
} | null;
|
||||
} = {
|
||||
[BackgroundSessionViewState.Loading]: {
|
||||
[SessionState.None]: null,
|
||||
[SessionState.Loading]: {
|
||||
button: {
|
||||
color: 'subdued',
|
||||
iconType: 'clock',
|
||||
|
@ -116,7 +139,7 @@ const backgroundSessionIndicatorViewStateToProps: {
|
|||
secondaryAction: ContinueInBackgroundButton,
|
||||
},
|
||||
},
|
||||
[BackgroundSessionViewState.Completed]: {
|
||||
[SessionState.Completed]: {
|
||||
button: {
|
||||
color: 'subdued',
|
||||
iconType: 'checkInCircleFilled',
|
||||
|
@ -141,7 +164,7 @@ const backgroundSessionIndicatorViewStateToProps: {
|
|||
secondaryAction: ViewBackgroundSessionsButton,
|
||||
},
|
||||
},
|
||||
[BackgroundSessionViewState.BackgroundLoading]: {
|
||||
[SessionState.BackgroundLoading]: {
|
||||
button: {
|
||||
iconType: EuiLoadingSpinner,
|
||||
'aria-label': i18n.translate(
|
||||
|
@ -165,7 +188,7 @@ const backgroundSessionIndicatorViewStateToProps: {
|
|||
secondaryAction: ViewBackgroundSessionsButton,
|
||||
},
|
||||
},
|
||||
[BackgroundSessionViewState.BackgroundCompleted]: {
|
||||
[SessionState.BackgroundCompleted]: {
|
||||
button: {
|
||||
color: 'success',
|
||||
iconType: 'checkInCircleFilled',
|
||||
|
@ -192,7 +215,7 @@ const backgroundSessionIndicatorViewStateToProps: {
|
|||
primaryAction: ViewBackgroundSessionsButton,
|
||||
},
|
||||
},
|
||||
[BackgroundSessionViewState.Restored]: {
|
||||
[SessionState.Restored]: {
|
||||
button: {
|
||||
color: 'warning',
|
||||
iconType: 'refresh',
|
||||
|
@ -217,6 +240,25 @@ const backgroundSessionIndicatorViewStateToProps: {
|
|||
secondaryAction: ViewBackgroundSessionsButton,
|
||||
},
|
||||
},
|
||||
[SessionState.Canceled]: {
|
||||
button: {
|
||||
color: 'subdued',
|
||||
iconType: 'refresh',
|
||||
'aria-label': i18n.translate('xpack.data.backgroundSessionIndicator.canceledIconAriaLabel', {
|
||||
defaultMessage: 'Canceled',
|
||||
}),
|
||||
tooltipText: i18n.translate('xpack.data.backgroundSessionIndicator.canceledTooltipText', {
|
||||
defaultMessage: 'Search was canceled',
|
||||
}),
|
||||
},
|
||||
popover: {
|
||||
text: i18n.translate('xpack.data.backgroundSessionIndicator.canceledText', {
|
||||
defaultMessage: 'Search was canceled',
|
||||
}),
|
||||
primaryAction: RefreshButton,
|
||||
secondaryAction: ViewBackgroundSessionsButton,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const VerticalDivider: React.FC = () => (
|
||||
|
@ -228,7 +270,9 @@ export const BackgroundSessionIndicator: React.FC<BackgroundSessionIndicatorProp
|
|||
const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
|
||||
const { button, popover } = backgroundSessionIndicatorViewStateToProps[props.state];
|
||||
if (!backgroundSessionIndicatorViewStateToProps[props.state]) return null;
|
||||
|
||||
const { button, popover } = backgroundSessionIndicatorViewStateToProps[props.state]!;
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
|
@ -239,6 +283,7 @@ export const BackgroundSessionIndicator: React.FC<BackgroundSessionIndicatorProp
|
|||
panelPaddingSize={'s'}
|
||||
className="backgroundSessionIndicator"
|
||||
data-test-subj={'backgroundSessionIndicator'}
|
||||
data-state={props.state}
|
||||
button={
|
||||
<EuiToolTip content={button.tooltipText}>
|
||||
<EuiButtonIcon
|
||||
|
@ -255,6 +300,7 @@ export const BackgroundSessionIndicator: React.FC<BackgroundSessionIndicatorProp
|
|||
alignItems={'center'}
|
||||
gutterSize={'s'}
|
||||
className="backgroundSessionIndicator__popoverContainer"
|
||||
data-test-subj={'backgroundSessionIndicatorPopoverContainer'}
|
||||
>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiText size="s" color={'subdued'}>
|
||||
|
|
|
@ -9,13 +9,18 @@ import { render, waitFor } from '@testing-library/react';
|
|||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
import { createConnectedBackgroundSessionIndicator } from './connected_background_session_indicator';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ISessionService } from '../../../../../../../src/plugins/data/public';
|
||||
import { ISessionService, SessionState } from '../../../../../../../src/plugins/data/public';
|
||||
import { coreMock } from '../../../../../../../src/core/public/mocks';
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const sessionService = dataPluginMock.createStartContract().search
|
||||
.session as jest.Mocked<ISessionService>;
|
||||
|
||||
test("shouldn't show indicator in case no active search session", async () => {
|
||||
const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService });
|
||||
const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({
|
||||
sessionService,
|
||||
application: coreStart.application,
|
||||
});
|
||||
const { getByTestId, container } = render(<BackgroundSessionIndicator />);
|
||||
|
||||
// make sure `backgroundSessionIndicator` isn't appearing after some time (lazy-loading)
|
||||
|
@ -26,10 +31,11 @@ test("shouldn't show indicator in case no active search session", async () => {
|
|||
});
|
||||
|
||||
test('should show indicator in case there is an active search session', async () => {
|
||||
const session$ = new BehaviorSubject('session_id');
|
||||
sessionService.getSession$.mockImplementation(() => session$);
|
||||
sessionService.getSessionId.mockImplementation(() => session$.getValue());
|
||||
const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService });
|
||||
const state$ = new BehaviorSubject(SessionState.Loading);
|
||||
const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({
|
||||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
});
|
||||
const { getByTestId } = render(<BackgroundSessionIndicator />);
|
||||
|
||||
await waitFor(() => getByTestId('backgroundSessionIndicator'));
|
||||
|
|
|
@ -5,28 +5,43 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { BackgroundSessionIndicator } from '../background_session_indicator';
|
||||
import { ISessionService } from '../../../../../../../src/plugins/data/public/';
|
||||
import { BackgroundSessionViewState } from './background_session_view_state';
|
||||
import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ApplicationStart } from '../../../../../../../src/core/public';
|
||||
|
||||
export interface BackgroundSessionIndicatorDeps {
|
||||
sessionService: ISessionService;
|
||||
application: ApplicationStart;
|
||||
}
|
||||
|
||||
export const createConnectedBackgroundSessionIndicator = ({
|
||||
sessionService,
|
||||
application,
|
||||
}: BackgroundSessionIndicatorDeps): React.FC => {
|
||||
const sessionId$ = sessionService.getSession$();
|
||||
const hasActiveSession$ = sessionId$.pipe(
|
||||
map((sessionId) => !!sessionId),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
return () => {
|
||||
const isSession = useObservable(hasActiveSession$, !!sessionService.getSessionId());
|
||||
if (!isSession) return null;
|
||||
return <BackgroundSessionIndicator state={BackgroundSessionViewState.Loading} />;
|
||||
const state = useObservable(sessionService.state$.pipe(debounceTime(500)));
|
||||
if (!state) return null;
|
||||
return (
|
||||
<RedirectAppLinks application={application}>
|
||||
<BackgroundSessionIndicator
|
||||
state={state}
|
||||
onContinueInBackground={() => {
|
||||
sessionService.save();
|
||||
}}
|
||||
onSaveResults={() => {
|
||||
sessionService.save();
|
||||
}}
|
||||
onRefresh={() => {
|
||||
sessionService.refresh();
|
||||
}}
|
||||
onCancel={() => {
|
||||
sessionService.cancel();
|
||||
}}
|
||||
/>
|
||||
</RedirectAppLinks>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,4 +8,3 @@ export {
|
|||
BackgroundSessionIndicatorDeps,
|
||||
createConnectedBackgroundSessionIndicator,
|
||||
} from './connected_background_session_indicator';
|
||||
export { BackgroundSessionViewState } from './background_session_view_state';
|
||||
|
|
|
@ -45,13 +45,13 @@ describe('alert actions', () => {
|
|||
updateTimelineIsLoading = jest.fn() as jest.Mocked<UpdateTimelineLoading>;
|
||||
|
||||
searchStrategyClient = {
|
||||
...dataPluginMock.createStartContract().search,
|
||||
aggs: {} as ISearchStart['aggs'],
|
||||
showError: jest.fn(),
|
||||
search: jest
|
||||
.fn()
|
||||
.mockImplementation(() => ({ toPromise: () => ({ data: mockTimelineDetails }) })),
|
||||
searchSource: {} as ISearchStart['searchSource'],
|
||||
session: dataPluginMock.createStartContract().search.session,
|
||||
};
|
||||
|
||||
jest.spyOn(apolloClient, 'query').mockImplementation((obj) => {
|
||||
|
|
|
@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const inspector = getService('inspector');
|
||||
const queryBar = getService('queryBar');
|
||||
const browser = getService('browser');
|
||||
const sendToBackground = getService('sendToBackground');
|
||||
|
||||
describe('dashboard with async search', () => {
|
||||
before(async function () {
|
||||
|
@ -78,21 +79,53 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(panel1SessionId1).not.to.be(panel1SessionId2);
|
||||
});
|
||||
|
||||
// NOTE: this test will be revised when session functionality is really working
|
||||
it('Opens a dashboard with existing session', async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.loadSavedDashboard('Not Delayed');
|
||||
const url = await browser.getCurrentUrl();
|
||||
const fakeSessionId = '__fake__';
|
||||
const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`;
|
||||
await browser.navigateTo(savedSessionURL);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const session1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension');
|
||||
expect(session1).to.be(fakeSessionId);
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const session2 = await getSearchSessionIdByPanel('Sum of Bytes by Extension');
|
||||
expect(session2).not.to.be(fakeSessionId);
|
||||
describe('Send to background', () => {
|
||||
before(async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
});
|
||||
|
||||
it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes.', async () => {
|
||||
await PageObjects.dashboard.loadSavedDashboard('Not Delayed');
|
||||
const url = await browser.getCurrentUrl();
|
||||
const fakeSessionId = '__fake__';
|
||||
const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`;
|
||||
await browser.get(savedSessionURL);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await sendToBackground.expectState('restored');
|
||||
await testSubjects.existOrFail('embeddableErrorLabel'); // expected that panel errors out because of non existing session
|
||||
|
||||
const session1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension');
|
||||
expect(session1).to.be(fakeSessionId);
|
||||
|
||||
await sendToBackground.refresh();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await sendToBackground.expectState('completed');
|
||||
await testSubjects.missingOrFail('embeddableErrorLabel');
|
||||
const session2 = await getSearchSessionIdByPanel('Sum of Bytes by Extension');
|
||||
expect(session2).not.to.be(fakeSessionId);
|
||||
});
|
||||
|
||||
it('Saves and restores a session', async () => {
|
||||
await PageObjects.dashboard.loadSavedDashboard('Not Delayed');
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await sendToBackground.expectState('completed');
|
||||
await sendToBackground.save();
|
||||
await sendToBackground.expectState('backgroundCompleted');
|
||||
const savedSessionId = await getSearchSessionIdByPanel('Sum of Bytes by Extension');
|
||||
|
||||
// load URL to restore a saved session
|
||||
const url = await browser.getCurrentUrl();
|
||||
const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`;
|
||||
await browser.get(savedSessionURL);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
|
||||
// Check that session is restored
|
||||
await sendToBackground.expectState('restored');
|
||||
await testSubjects.missingOrFail('embeddableErrorLabel');
|
||||
const data = await PageObjects.visChart.getBarChartData('Sum of bytes');
|
||||
expect(data.length).to.be(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -89,6 +89,7 @@ export default async function ({ readConfigFile }) {
|
|||
'--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"',
|
||||
'--timelion.ui.enabled=true',
|
||||
'--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects
|
||||
'--xpack.data_enhanced.search.sendToBackground.enabled=true', // enable WIP send to background UI
|
||||
],
|
||||
},
|
||||
uiSettings: {
|
||||
|
|
7
x-pack/test/functional/services/data/index.ts
Normal file
7
x-pack/test/functional/services/data/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { SendToBackgroundProvider } from './send_to_background';
|
88
x-pack/test/functional/services/data/send_to_background.ts
Normal file
88
x-pack/test/functional/services/data/send_to_background.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper';
|
||||
|
||||
const SEND_TO_BACKGROUND_TEST_SUBJ = 'backgroundSessionIndicator';
|
||||
const SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ = 'backgroundSessionIndicatorPopoverContainer';
|
||||
|
||||
type SessionStateType =
|
||||
| 'none'
|
||||
| 'loading'
|
||||
| 'completed'
|
||||
| 'backgroundLoading'
|
||||
| 'backgroundCompleted'
|
||||
| 'restored'
|
||||
| 'canceled';
|
||||
|
||||
export function SendToBackgroundProvider({ getService }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const retry = getService('retry');
|
||||
const browser = getService('browser');
|
||||
|
||||
return new (class SendToBackgroundService {
|
||||
public async find(): Promise<WebElementWrapper> {
|
||||
return testSubjects.find(SEND_TO_BACKGROUND_TEST_SUBJ);
|
||||
}
|
||||
|
||||
public async exists(): Promise<boolean> {
|
||||
return testSubjects.exists(SEND_TO_BACKGROUND_TEST_SUBJ);
|
||||
}
|
||||
|
||||
public async expectState(state: SessionStateType) {
|
||||
return retry.waitFor(`sendToBackground indicator to get into state = ${state}`, async () => {
|
||||
const currentState = await (
|
||||
await testSubjects.find(SEND_TO_BACKGROUND_TEST_SUBJ)
|
||||
).getAttribute('data-state');
|
||||
return currentState === state;
|
||||
});
|
||||
}
|
||||
|
||||
public async viewBackgroundSessions() {
|
||||
await this.ensurePopoverOpened();
|
||||
await testSubjects.click('backgroundSessionIndicatorViewBackgroundSessionsLink');
|
||||
}
|
||||
|
||||
public async save() {
|
||||
await this.ensurePopoverOpened();
|
||||
await testSubjects.click('backgroundSessionIndicatorSaveBtn');
|
||||
await this.ensurePopoverClosed();
|
||||
}
|
||||
|
||||
public async cancel() {
|
||||
await this.ensurePopoverOpened();
|
||||
await testSubjects.click('backgroundSessionIndicatorCancelBtn');
|
||||
await this.ensurePopoverClosed();
|
||||
}
|
||||
|
||||
public async refresh() {
|
||||
await this.ensurePopoverOpened();
|
||||
await testSubjects.click('backgroundSessionIndicatorRefreshBtn');
|
||||
await this.ensurePopoverClosed();
|
||||
}
|
||||
|
||||
private async ensurePopoverOpened() {
|
||||
const isAlreadyOpen = await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ);
|
||||
if (isAlreadyOpen) return;
|
||||
return retry.waitFor(`sendToBackground popover opened`, async () => {
|
||||
await testSubjects.click(SEND_TO_BACKGROUND_TEST_SUBJ);
|
||||
return await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ);
|
||||
});
|
||||
}
|
||||
|
||||
private async ensurePopoverClosed() {
|
||||
const isAlreadyClosed = !(await testSubjects.exists(
|
||||
SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ
|
||||
));
|
||||
if (isAlreadyClosed) return;
|
||||
return retry.waitFor(`sendToBackground popover closed`, async () => {
|
||||
await browser.pressKeys(browser.keys.ESCAPE);
|
||||
return !(await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ));
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
|
@ -56,6 +56,7 @@ import {
|
|||
DashboardDrilldownsManageProvider,
|
||||
DashboardPanelTimeRangeProvider,
|
||||
} from './dashboard';
|
||||
import { SendToBackgroundProvider } from './data';
|
||||
|
||||
// define the name and providers for services that should be
|
||||
// available to your tests. If you don't specify anything here
|
||||
|
@ -103,4 +104,5 @@ export const services = {
|
|||
dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider,
|
||||
dashboardDrilldownsManage: DashboardDrilldownsManageProvider,
|
||||
dashboardPanelTimeRange: DashboardPanelTimeRangeProvider,
|
||||
sendToBackground: SendToBackgroundProvider,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue