diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md
index b2f8e83d8e6545..a370c67f460f4e 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md
@@ -17,6 +17,7 @@ export interface ISearchSetup
| Property | Type | Description |
| --- | --- | --- |
| [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup
| |
-| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService
| session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
+| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService
| Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
+| [sessionsClient](./kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md) | ISessionsClient
| Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) |
| [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector
| |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md
index 739fdfdeb5fc36..451dbc86b86b66 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md
@@ -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)
Signature:
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md
new file mode 100644
index 00000000000000..d9af202cf1018f
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md
@@ -0,0 +1,13 @@
+
+
+[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)
+
+Signature:
+
+```typescript
+sessionsClient: ISessionsClient;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md
index dba60c7bdf147d..a27e155dda1117 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md
@@ -19,6 +19,7 @@ export interface ISearchStart
| [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart
| agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) |
| [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric
| low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) |
| [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource
| high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) |
-| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService
| session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
+| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService
| Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
+| [sessionsClient](./kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md) | ISessionsClient
| Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) |
| [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void
| |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md
index 1ad194a9bec868..892b0fa6acb607 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md
@@ -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)
Signature:
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md
new file mode 100644
index 00000000000000..9c3210d2ec4175
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md
@@ -0,0 +1,13 @@
+
+
+[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)
+
+Signature:
+
+```typescript
+sessionsClient: ISessionsClient;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionsclient.md
new file mode 100644
index 00000000000000..d6efabb1b9518e
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionsclient.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md)
+
+## ISessionsClient type
+
+Signature:
+
+```typescript
+export declare type ISessionsClient = PublicContract;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md
deleted file mode 100644
index fc3d214eb4cad1..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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.
-
-Signature:
-
-```typescript
-clear: () => void;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md
deleted file mode 100644
index eabb966160c4d8..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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
-
-Signature:
-
-```typescript
-delete: (sessionId: string) => Promise;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md
deleted file mode 100644
index 58e2fea0e6fe93..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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
-
-Signature:
-
-```typescript
-find: (options: SearchSessionFindOptions) => Promise>;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md
deleted file mode 100644
index a2dff2f18253bc..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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
-
-Signature:
-
-```typescript
-get: (sessionId: string) => Promise>;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md
deleted file mode 100644
index e30c89fb1a9fdf..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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
-
-Signature:
-
-```typescript
-getSession$: () => Observable;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md
deleted file mode 100644
index 838023ff1d8b90..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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
-
-Signature:
-
-```typescript
-getSessionId: () => string | undefined;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md
deleted file mode 100644
index 8d8cd1f31bb957..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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)
-
-Signature:
-
-```typescript
-isRestore: () => boolean;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md
deleted file mode 100644
index db737880bb84e9..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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)
-
-Signature:
-
-```typescript
-isStored: () => boolean;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md
index 02c0a821e552dc..8938c880a04718 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md
@@ -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
Signature:
```typescript
-export interface ISessionService
+export declare type ISessionService = PublicContract;
```
-
-## Properties
-
-| Property | Type | Description |
-| --- | --- | --- |
-| [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) | () => void
| Clears the active session. |
-| [delete](./kibana-plugin-plugins-data-public.isessionservice.delete.md) | (sessionId: string) => Promise<void>
| Deletes a session |
-| [find](./kibana-plugin-plugins-data-public.isessionservice.find.md) | (options: SearchSessionFindOptions) => Promise<SavedObjectsFindResponse<BackgroundSessionSavedObjectAttributes>>
| Gets a list of saved sessions |
-| [get](./kibana-plugin-plugins-data-public.isessionservice.get.md) | (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>
| Gets a saved session |
-| [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) | () => Observable<string | undefined>
| Returns the observable that emits an update every time the session ID changes |
-| [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) | () => string | undefined
| Returns the active session ID |
-| [isRestore](./kibana-plugin-plugins-data-public.isessionservice.isrestore.md) | () => boolean
| Whether the active session is restored (i.e. reusing previous search IDs) |
-| [isStored](./kibana-plugin-plugins-data-public.isessionservice.isstored.md) | () => boolean
| Whether the active session is already saved (i.e. sent to background) |
-| [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) | (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>
| Restores existing session |
-| [save](./kibana-plugin-plugins-data-public.isessionservice.save.md) | (name: string, url: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>>
| Saves a session |
-| [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) | () => string
| Starts a new session |
-| [update](./kibana-plugin-plugins-data-public.isessionservice.update.md) | (sessionId: string, attributes: Partial<BackgroundSessionSavedObjectAttributes>) => Promise<any>
| Updates a session |
-
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md
deleted file mode 100644
index 96106a6ef7e2d1..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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
-
-Signature:
-
-```typescript
-restore: (sessionId: string) => Promise>;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md
deleted file mode 100644
index 4ac4a966144676..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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
-
-Signature:
-
-```typescript
-save: (name: string, url: string) => Promise>;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md
deleted file mode 100644
index 9e14c5ed267658..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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
-
-Signature:
-
-```typescript
-start: () => string;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md
deleted file mode 100644
index 5e2ff53d58ab77..00000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[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
-
-Signature:
-
-```typescript
-update: (sessionId: string, attributes: Partial) => Promise;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
index b8e45cde3c18b1..9121b0aade4706 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
@@ -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) | |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md
new file mode 100644
index 00000000000000..0f0b616066dd66
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md
@@ -0,0 +1,13 @@
+
+
+[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
+
+Signature:
+
+```typescript
+getName: () => Promise;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md
new file mode 100644
index 00000000000000..207adaf2bd50b6
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md
@@ -0,0 +1,15 @@
+
+
+[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
+
+Signature:
+
+```typescript
+getUrlGeneratorData: () => Promise<{
+ urlGeneratorId: ID;
+ initialState: UrlGeneratorStateMapping[ID]['State'];
+ restoreState: UrlGeneratorStateMapping[ID]['State'];
+ }>;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md
new file mode 100644
index 00000000000000..a3d294f5e33038
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md
@@ -0,0 +1,21 @@
+
+
+[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
+
+Signature:
+
+```typescript
+export interface SearchSessionInfoProvider
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md) | () => Promise<string>
| 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) | () => Promise<{
urlGeneratorId: ID;
initialState: UrlGeneratorStateMapping[ID]['State'];
restoreState: UrlGeneratorStateMapping[ID]['State'];
}>
| |
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md
new file mode 100644
index 00000000000000..9a60a5b2a9f9be
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md
@@ -0,0 +1,26 @@
+
+
+[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
+
+Signature:
+
+```typescript
+export declare enum SessionState
+```
+
+## Enumeration Members
+
+| Member | Value | Description |
+| --- | --- | --- |
+| BackgroundCompleted | "backgroundCompleted"
| Page load completed with background session created. |
+| BackgroundLoading | "backgroundLoading"
| Search request was sent to the background. The page is loading in background. |
+| Canceled | "canceled"
| Current session requests where explicitly canceled by user Displaying none or partial results |
+| Completed | "completed"
| No action was taken and the page completed loading without background session creation. |
+| Loading | "loading"
| Pending search request has not been sent to the background yet |
+| None | "none"
| Session is not active, e.g. didn't start |
+| Restored | "restored"
| Revisiting the page after background completion |
+
diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
index c99e4e4e069878..0d9e7e51b4a972 100644
--- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
@@ -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();
});
}
diff --git a/src/plugins/dashboard/public/application/lib/index.ts b/src/plugins/dashboard/public/application/lib/index.ts
index e9ebe73c3b34dc..6741bbbc5d4b13 100644
--- a/src/plugins/dashboard/public/application/lib/index.ts
+++ b/src/plugins/dashboard/public/application/lib/index.ts
@@ -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';
diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.ts b/src/plugins/dashboard/public/application/lib/session_restoration.ts
new file mode 100644
index 00000000000000..f8ea8f8dcd76dc
--- /dev/null
+++ b/src/plugins/dashboard/public/application/lib/session_restoration.ts
@@ -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(),
+ };
+}
diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts
index 461caedc5cba76..0272e9d3ebdf70 100644
--- a/src/plugins/dashboard/public/url_generator.test.ts
+++ b/src/plugins/dashboard/public/url_generator.test.ts
@@ -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({
diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts
index b23b26e4022dd0..182020d032e4e2 100644
--- a/src/plugins/dashboard/public/url_generator.ts
+++ b/src/plugins/dashboard/public/url_generator.ts
@@ -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}`
diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts
index 4f4a593764b1e8..bf6fe11f746f96 100644
--- a/src/plugins/data/common/search/aggs/agg_type.ts
+++ b/src/plugins/data/common/search/aggs/agg_type.ts
@@ -55,7 +55,8 @@ export interface AggTypeConfig<
aggConfig: TAggConfig,
searchSource: ISearchSource,
inspectorRequestAdapter?: RequestAdapter,
- abortSignal?: AbortSignal
+ abortSignal?: AbortSignal,
+ searchSessionId?: string
) => Promise;
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;
/**
* Get the serialized format for the values produced by this agg type,
diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts
index ac65e7fa813b3d..7071d9c1dc9c44 100644
--- a/src/plugins/data/common/search/aggs/buckets/terms.ts
+++ b/src/plugins/data/common/search/aggs/buckets/terms.ts
@@ -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))
diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts
index d1ab22057695a7..50ca3ca390ece0 100644
--- a/src/plugins/data/common/search/session/types.ts
+++ b/src/plugins/data/common/search/session/types.ts
@@ -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;
-
- /**
- * 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>;
-
- /**
- * Clears the active session.
- */
- clear: () => void;
-
- /**
- * Saves a session
- */
- save: (name: string, url: string) => Promise>;
-
- /**
- * Gets a saved session
- */
- get: (sessionId: string) => Promise>;
-
- /**
- * Gets a list of saved sessions
- */
- find: (
- options: SearchSessionFindOptions
- ) => Promise>;
-
+export interface BackgroundSessionSavedObjectAttributes {
/**
- * Updates a session
+ * User-facing session name to be displayed in session management
*/
- update: (
- sessionId: string,
- attributes: Partial
- ) => Promise;
-
+ name: string;
/**
- * Deletes a session
+ * App that created the session. e.g 'discover'
*/
- delete: (sessionId: string) => Promise;
-}
-
-export interface BackgroundSessionSavedObjectAttributes {
- name: string;
+ appId: string;
created: string;
expires: string;
status: string;
+ urlGeneratorId: string;
initialState: Record;
restoreState: Record;
idMapping: Record;
diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json
index 3e4d08c8faa1b6..06b083e0ff3aaa 100644
--- a/src/plugins/data/kibana.json
+++ b/src/plugins/data/kibana.json
@@ -6,7 +6,8 @@
"requiredPlugins": [
"bfetch",
"expressions",
- "uiActions"
+ "uiActions",
+ "share"
],
"optionalPlugins": ["usageCollection"],
"extraPublicDirs": ["common"],
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index e0b0c5a0ea980f..1c07b4b99e4c09 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -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';
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 8ceb91269adbea..ad1861cecea0be 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -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';
@@ -1478,6 +1482,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)
@@ -1493,6 +1498,7 @@ export interface ISearchStart {
search: ISearchGeneric;
searchSource: ISearchStartSearchSource;
session: ISessionService;
+ sessionsClient: ISessionsClient;
// (undocumented)
showError: (e: Error) => void;
}
@@ -1508,25 +1514,17 @@ export interface ISearchStartSearchSource {
// @public (undocumented)
export const isErrorResponse: (response?: IKibanaSearchResponse | 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;
+
+// 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;
- // Warning: (ae-forgotten-export) The symbol "SearchSessionFindOptions" needs to be exported by the entry point index.d.ts
- find: (options: SearchSessionFindOptions) => Promise>;
- get: (sessionId: string) => Promise>;
- getSession$: () => Observable;
- 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>;
- save: (name: string, url: string) => Promise>;
- start: () => string;
- update: (sessionId: string, attributes: Partial) => Promise;
-}
+export type ISessionService = PublicContract;
// Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -2107,6 +2105,7 @@ export class SearchInterceptor {
timeoutSignal: AbortSignal;
combinedSignal: AbortSignal;
cleanup: () => void;
+ abort: () => void;
};
// (undocumented)
showError(e: Error): void;
@@ -2135,6 +2134,20 @@ export interface SearchInterceptorDeps {
// @internal
export type SearchRequest = Record;
+// 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 {
+ getName: () => Promise;
+ // (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
@@ -2240,6 +2253,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)
@@ -2415,22 +2439,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)
diff --git a/src/plugins/data/public/search/expressions/esaggs/request_handler.ts b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts
index 93b5705b821c02..7a27d652671499 100644
--- a/src/plugins/data/public/search/expressions/esaggs/request_handler.ts
+++ b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts
@@ -182,7 +182,8 @@ export const handleRequest = async ({
agg,
requestSearchSource,
inspectorAdapters.requests,
- abortSignal
+ abortSignal,
+ searchSessionId
);
}
}
diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts
index f6bd46c17192c9..2a767d1bf7c0d1 100644
--- a/src/plugins/data/public/search/index.ts
+++ b/src/plugins/data/public/search/index.ts
@@ -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';
diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts
index 836ddb618e7468..b799c661051fa5 100644
--- a/src/plugins/data/public/search/mocks.ts
+++ b/src/plugins/data/public/search/mocks.ts
@@ -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 {
return {
aggs: searchAggsSetupMock(),
__enhance: jest.fn(),
session: getSessionServiceMock(),
+ sessionsClient: getSessionsClientMock(),
};
}
@@ -36,6 +37,7 @@ function createStartContract(): jest.Mocked {
search: jest.fn(),
showError: jest.fn(),
session: getSessionServiceMock(),
+ sessionsClient: getSessionsClientMock(),
searchSource: searchSourceMock.createStartContract(),
};
}
diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts
index 6dc52d7016797e..947dac1b326400 100644
--- a/src/plugins/data/public/search/search_interceptor.test.ts
+++ b/src/plugins/data/public/search/search_interceptor.test.ts
@@ -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;
+ 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;
+ 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', () => {
diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts
index 055b3a71705bf2..8548a2a9f2b2a0 100644
--- a/src/plugins/data/public/search/search_interceptor.ts
+++ b/src/plugins/data/public/search/search_interceptor.ts
@@ -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 {
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();
+ },
};
}
diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts
index 3179da4d03a1a4..d617010d13011e 100644
--- a/src/plugins/data/public/search/search_service.test.ts
+++ b/src/plugins/data/public/search/search_service.test.ts
@@ -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');
});
});
});
diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts
index b76b5846d3d938..60d2dfdf866cfb 100644
--- a/src/plugins/data/public/search/search_service.ts
+++ b/src/plugins/data/public/search/search_service.ts
@@ -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 {
private searchInterceptor!: ISearchInterceptor;
private usageCollector?: SearchUsageCollector;
private sessionService!: ISessionService;
+ private sessionsClient!: ISessionsClient;
constructor(private initializerContext: PluginInitializerContext) {}
@@ -76,7 +76,12 @@ export class SearchService implements Plugin {
): 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 {
this.searchInterceptor = enhancements.searchInterceptor;
},
session: this.sessionService,
+ sessionsClient: this.sessionsClient,
};
}
@@ -146,6 +152,7 @@ export class SearchService implements Plugin {
this.searchInterceptor.showError(e);
},
session: this.sessionService,
+ sessionsClient: this.sessionsClient,
searchSource: this.searchSourceService.start(indexPatterns, searchSourceDependencies),
};
}
diff --git a/src/plugins/data/common/mocks.ts b/src/plugins/data/public/search/session/index.ts
similarity index 78%
rename from src/plugins/data/common/mocks.ts
rename to src/plugins/data/public/search/session/index.ts
index dde70b1d07443b..ee0121aacad5e8 100644
--- a/src/plugins/data/common/mocks.ts
+++ b/src/plugins/data/public/search/session/index.ts
@@ -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';
diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts
similarity index 67%
rename from src/plugins/data/common/search/session/mocks.ts
rename to src/plugins/data/public/search/session/mocks.ts
index 4604e15e4e93b8..0ff99b3080365a 100644
--- a/src/plugins/data/common/search/session/mocks.ts
+++ b/src/plugins/data/public/search/session/mocks.ts
@@ -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 {
+ return {
+ get: jest.fn(),
+ create: jest.fn(),
+ find: jest.fn(),
+ update: jest.fn(),
+ delete: jest.fn(),
+ };
+}
export function getSessionServiceMock(): jest.Mocked {
return {
@@ -27,12 +39,15 @@ export function getSessionServiceMock(): jest.Mocked {
restore: jest.fn(),
getSessionId: jest.fn(),
getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()),
+ state$: new BehaviorSubject(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(),
};
}
diff --git a/src/plugins/data/public/search/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts
similarity index 53%
rename from src/plugins/data/public/search/session_service.test.ts
rename to src/plugins/data/public/search/session/session_service.test.ts
index bcfd06944d9831..83c3185ead63eb 100644
--- a/src/plugins/data/public/search/session_service.test.ts
+++ b/src/plugins/data/public/search/session/session_service.test.ts
@@ -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;
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.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);
+ });
});
});
diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts
new file mode 100644
index 00000000000000..ef0b36a33be52e
--- /dev/null
+++ b/src/plugins/data/public/search/session/session_service.ts
@@ -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;
+
+export interface TrackSearchDescriptor {
+ abort: () => void;
+}
+
+/**
+ * Provide info about current search session to be stored in backgroundSearch saved object
+ */
+export interface SearchSessionInfoProvider {
+ /**
+ * User-facing name of the session.
+ * e.g. will be displayed in background sessions management list
+ */
+ getName: () => Promise;
+ 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;
+ private readonly state: SessionStateContainer;
+
+ private searchSessionInfoProvider?: SearchSessionInfoProvider;
+ private appChangeSubscription$?: Subscription;
+ private curApp?: string;
+
+ constructor(
+ initializerContext: PluginInitializerContext,
+ getStartServices: StartServicesAccessor,
+ private readonly sessionsClient: ISessionsClient,
+ { freezeState = true }: { freezeState: boolean } = { freezeState: true }
+ ) {
+ const { stateContainer, sessionState$ } = createSessionStateContainer({
+ 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(
+ searchSessionInfoProvider: SearchSessionInfoProvider | 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();
+ /**
+ * 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 {
+ 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 {
+ 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,
+ initialState: (initialState as unknown) as Record,
+ urlGeneratorId,
+ sessionId,
+ });
+
+ // if we are still interested in this result
+ if (this.getSessionId() === sessionId) {
+ this.state.transitions.store();
+ }
+ }
+}
diff --git a/src/plugins/data/public/search/session/session_state.test.ts b/src/plugins/data/public/search/session/session_state.test.ts
new file mode 100644
index 00000000000000..5f709c75bb5d21
--- /dev/null
+++ b/src/plugins/data/public/search/session/session_state.test.ts
@@ -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);
+ });
+ });
+});
diff --git a/src/plugins/data/public/search/session/session_state.ts b/src/plugins/data/public/search/session/session_state.ts
new file mode 100644
index 00000000000000..087417263e5bf0
--- /dev/null
+++ b/src/plugins/data/public/search/session/session_state.ts
@@ -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 {
+ /**
+ * 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 = () => ({
+ sessionId: undefined,
+ isStored: false,
+ isRestore: false,
+ isCanceled: false,
+ isStarted: false,
+ pendingSearches: [],
+});
+
+export interface SessionPureTransitions<
+ SearchDescriptor = unknown,
+ S = SessionStateInternal
+> {
+ 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
+> {
+ 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 = StateContainer<
+ SessionStateInternal,
+ SessionPureTransitions,
+ SessionPureSelectors
+>;
+
+export const createSessionStateContainer = (
+ { freeze = true }: { freeze: boolean } = { freeze: true }
+): {
+ stateContainer: SessionStateContainer;
+ sessionState$: Observable;
+} => {
+ const stateContainer = createStateContainer(
+ createSessionDefaultState(),
+ sessionPureTransitions,
+ sessionPureSelectors,
+ freeze ? undefined : { freeze: (s) => s }
+ ) as SessionStateContainer;
+
+ const sessionState$: Observable = stateContainer.state$.pipe(
+ map(() => stateContainer.selectors.getState()),
+ distinctUntilChanged(),
+ shareReplay(1)
+ );
+ return {
+ stateContainer,
+ sessionState$,
+ };
+};
diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts
new file mode 100644
index 00000000000000..c19c5db0640946
--- /dev/null
+++ b/src/plugins/data/public/search/session/sessions_client.ts
@@ -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;
+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> {
+ return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`);
+ }
+
+ public create({
+ name,
+ appId,
+ urlGeneratorId,
+ initialState,
+ restoreState,
+ sessionId,
+ }: {
+ name: string;
+ appId: string;
+ initialState: Record;
+ restoreState: Record;
+ urlGeneratorId: string;
+ sessionId: string;
+ }): Promise> {
+ return this.http.post(`/internal/session`, {
+ body: JSON.stringify({
+ name,
+ initialState,
+ restoreState,
+ sessionId,
+ appId,
+ urlGeneratorId,
+ }),
+ });
+ }
+
+ public find(
+ options: SearchSessionFindOptions
+ ): Promise> {
+ return this.http!.post(`/internal/session`, {
+ body: JSON.stringify(options),
+ });
+ }
+
+ public update(
+ sessionId: string,
+ attributes: Partial
+ ): Promise> {
+ return this.http!.put(`/internal/session/${encodeURIComponent(sessionId)}`, {
+ body: JSON.stringify(attributes),
+ });
+ }
+
+ public delete(sessionId: string): Promise {
+ return this.http!.delete(`/internal/session/${encodeURIComponent(sessionId)}`);
+ }
+}
diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts
deleted file mode 100644
index 0141cff258a9f2..00000000000000
--- a/src/plugins/data/public/search/session_service.ts
+++ /dev/null
@@ -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(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,
- 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) {
- return this.http.put(`/internal/session/${encodeURIComponent(sessionId)}`, {
- body: JSON.stringify(attributes),
- });
- }
-
- public delete(sessionId: string) {
- return this.http.delete(`/internal/session/${encodeURIComponent(sessionId)}`);
- }
-}
diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts
index c08d9f4c7be3f6..057b242c22f20a 100644
--- a/src/plugins/data/public/search/types.ts
+++ b/src/plugins/data/public/search/types.ts
@@ -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';
diff --git a/src/plugins/data/server/saved_objects/background_session.ts b/src/plugins/data/server/saved_objects/background_session.ts
index 74b03c4d867e40..e81272628c091a 100644
--- a/src/plugins/data/server/saved_objects/background_session.ts
+++ b/src/plugins/data/server/saved_objects/background_session.ts
@@ -39,6 +39,12 @@ export const backgroundSessionMapping: SavedObjectsType = {
status: {
type: 'keyword',
},
+ appId: {
+ type: 'keyword',
+ },
+ urlGeneratorId: {
+ type: 'keyword',
+ },
initialState: {
type: 'object',
enabled: false,
diff --git a/src/plugins/data/server/search/routes/session.ts b/src/plugins/data/server/search/routes/session.ts
index 93f07ecfb92ff6..f7dfc776565e08 100644
--- a/src/plugins/data/server/search/routes/session.ts
+++ b/src/plugins/data/server/search/routes/session.ts
@@ -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,
});
diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts
index 1ceebae967d4c6..5ff6d4b932487c 100644
--- a/src/plugins/data/server/search/session/session_service.test.ts
+++ b/src/plugins/data/server/search/session/session_service.test.ts
@@ -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: [],
diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts
index eca5f428b8555f..b9a738413ede4f 100644
--- a/src/plugins/data/server/search/session/session_service.ts
+++ b/src/plugins/data/server/search/session/session_service.ts
@@ -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,
{ 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();
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(
BACKGROUND_SESSION_TYPE,
attributes,
diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js
index 7059593c0c4e73..d0340c2cf4edd1 100644
--- a/src/plugins/discover/public/application/angular/discover.js
+++ b/src/plugins/discover/public/application/angular/discover.js
@@ -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) {
diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts
index 3c6ef1d3e4334e..7de4ac27dd81fe 100644
--- a/src/plugins/discover/public/application/angular/discover_state.ts
+++ b/src/plugins/discover/public/application/angular/discover_state.ts
@@ -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;
+ 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;
+ 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,
+ };
+}
diff --git a/src/plugins/discover/public/url_generator.test.ts b/src/plugins/discover/public/url_generator.test.ts
index 98b7625e63c72f..95bff6b1fdc9c1 100644
--- a/src/plugins/discover/public/url_generator.test.ts
+++ b/src/plugins/discover/public/url_generator.test.ts
@@ -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 () => {
diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts
index df9b16a4627ec7..6d86818910b11b 100644
--- a/src/plugins/discover/public/url_generator.ts
+++ b/src/plugins/discover/public/url_generator.ts
@@ -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 => {
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)
diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md
index 023cb3d19b632d..534ab0f331e87d 100644
--- a/src/plugins/embeddable/public/public.api.md
+++ b/src/plugins/embeddable/public/public.api.md
@@ -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';
diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts
index fa3206446f9fcf..393a28c289e82f 100644
--- a/x-pack/plugins/data_enhanced/public/plugin.ts
+++ b/x-pack/plugins/data_enhanced/public/plugin.ts
@@ -68,6 +68,7 @@ export class DataEnhancedPlugin
React.createElement(
createConnectedBackgroundSessionIndicator({
sessionService: plugins.data.search.session,
+ application: core.application,
})
)
),
diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
index f4d7422d1c7e2b..20b55d9688edba 100644
--- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
+++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
@@ -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;
+ let sessionState$: BehaviorSubject;
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
mockCoreStart = coreMock.createStart();
+ sessionState$ = new BehaviorSubject(SessionState.None);
const dataPluginMockStart = dataPluginMock.createStartContract();
+ sessionService = {
+ ...(dataPluginMockStart.search.session as jest.Mocked),
+ 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);
+ });
+ });
});
diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
index 9aa35b460b1e85..0e87c093d2a8d9 100644
--- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
+++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
@@ -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();
+ }
})
);
}
diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx
index 9cef76c62279c1..c7195ea486e2f7 100644
--- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx
@@ -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', () => (
<>
-
+
-
+
-
+
-
+
-
+
>
));
diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx
index 5b7ab2e4f9b1f0..f401a460113c76 100644
--- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx
@@ -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 {children};
@@ -19,7 +19,7 @@ test('Loading state', async () => {
const onCancel = jest.fn();
render(
-
+
);
@@ -33,10 +33,7 @@ test('Completed state', async () => {
const onSave = jest.fn();
render(
-
+
);
@@ -50,10 +47,7 @@ test('Loading in the background state', async () => {
const onCancel = jest.fn();
render(
-
+
);
@@ -64,30 +58,26 @@ test('Loading in the background state', async () => {
});
test('BackgroundCompleted state', async () => {
- const onViewSession = jest.fn();
render(
);
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(
-
+
);
@@ -96,3 +86,17 @@ test('Restored state', async () => {
expect(onRefresh).toBeCalled();
});
+
+test('Canceled state', async () => {
+ const onRefresh = jest.fn();
+ render(
+
+
+
+ );
+
+ await userEvent.click(screen.getByLabelText('Canceled'));
+ await userEvent.click(screen.getByText('Refresh'));
+
+ expect(onRefresh).toBeCalled();
+});
diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx
index b55bd6b6553716..674ce392aa2d0d 100644
--- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx
@@ -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) => (
-
+
{},
buttonProps = {},
}: ActionButtonProps) => (
-
+
{},
+ viewBackgroundSessionsLink = 'management',
buttonProps = {},
}: ActionButtonProps) => (
- // TODO: make this a link
-
+
{}, buttonProps = {} }: ActionButtonProps) => (
-
+
{}, buttonProps = {} }: ActionButtonP
);
const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButtonProps) => (
-
+
{}, buttonProps = {} }: ActionButton
);
const backgroundSessionIndicatorViewStateToProps: {
- [state in BackgroundSessionViewState]: {
- button: Pick & { tooltipText: string };
+ [state in SessionState]: {
+ button: Pick & {
+ tooltipText: string;
+ };
popover: {
text: string;
primaryAction?: React.ComponentType;
secondaryAction?: React.ComponentType;
};
- };
+ } | 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 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 (
diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx
index d97d10512783c7..4c9fd50dc8c4c5 100644
--- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx
@@ -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;
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();
// 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();
await waitFor(() => getByTestId('backgroundSessionIndicator'));
diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx
index d097a1aecb66af..1cc2fffcea8c53 100644
--- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx
@@ -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 ;
+ const state = useObservable(sessionService.state$.pipe(debounceTime(500)));
+ if (!state) return null;
+ return (
+
+ {
+ sessionService.save();
+ }}
+ onSaveResults={() => {
+ sessionService.save();
+ }}
+ onRefresh={() => {
+ sessionService.refresh();
+ }}
+ onCancel={() => {
+ sessionService.cancel();
+ }}
+ />
+
+ );
};
};
diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts
index adbb6edbbfcf3f..223a0537129df5 100644
--- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts
+++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts
@@ -8,4 +8,3 @@ export {
BackgroundSessionIndicatorDeps,
createConnectedBackgroundSessionIndicator,
} from './connected_background_session_indicator';
-export { BackgroundSessionViewState } from './background_session_view_state';
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
index 6b7cc8167ede6d..92657df7f9bb5b 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
@@ -45,13 +45,13 @@ describe('alert actions', () => {
updateTimelineIsLoading = jest.fn() as jest.Mocked;
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) => {
diff --git a/x-pack/test/functional/apps/dashboard/async_search/async_search.ts b/x-pack/test/functional/apps/dashboard/async_search/async_search.ts
index 17497c83267779..c9db2b1221545a 100644
--- a/x-pack/test/functional/apps/dashboard/async_search/async_search.ts
+++ b/x-pack/test/functional/apps/dashboard/async_search/async_search.ts
@@ -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);
+ });
});
});
diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js
index 70e92e88e60bef..e3f83f08eb7581 100644
--- a/x-pack/test/functional/config.js
+++ b/x-pack/test/functional/config.js
@@ -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: {
diff --git a/x-pack/test/functional/services/data/index.ts b/x-pack/test/functional/services/data/index.ts
new file mode 100644
index 00000000000000..c2e3fcb41a7c9d
--- /dev/null
+++ b/x-pack/test/functional/services/data/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { SendToBackgroundProvider } from './send_to_background';
diff --git a/x-pack/test/functional/services/data/send_to_background.ts b/x-pack/test/functional/services/data/send_to_background.ts
new file mode 100644
index 00000000000000..f6a28c59b737dc
--- /dev/null
+++ b/x-pack/test/functional/services/data/send_to_background.ts
@@ -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 {
+ return testSubjects.find(SEND_TO_BACKGROUND_TEST_SUBJ);
+ }
+
+ public async exists(): Promise {
+ 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));
+ });
+ }
+ })();
+}
diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts
index 1aa6216236827d..d6d921d5bce174 100644
--- a/x-pack/test/functional/services/index.ts
+++ b/x-pack/test/functional/services/index.ts
@@ -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,
};