From 3e102cd387284dbd28f8afe0c40d39055efdbf62 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 19 Nov 2020 14:47:16 +0100 Subject: [PATCH 01/18] wip wip docs --- ...plugin-plugins-data-public.isearchsetup.md | 3 +- ...lugins-data-public.isearchsetup.session.md | 2 +- ...data-public.isearchsetup.sessionsclient.md | 13 + ...plugin-plugins-data-public.isearchstart.md | 3 +- ...lugins-data-public.isearchstart.session.md | 2 +- ...data-public.isearchstart.sessionsclient.md | 13 + ...gin-plugins-data-public.isessionsclient.md | 11 + ...ugins-data-public.isessionservice.clear.md | 13 - ...gins-data-public.isessionservice.delete.md | 13 - ...lugins-data-public.isessionservice.find.md | 13 - ...plugins-data-public.isessionservice.get.md | 13 - ...data-public.isessionservice.getsession_.md | 13 - ...ata-public.isessionservice.getsessionid.md | 13 - ...s-data-public.isessionservice.isrestore.md | 13 - ...ns-data-public.isessionservice.isstored.md | 13 - ...gin-plugins-data-public.isessionservice.md | 22 +- ...ins-data-public.isessionservice.restore.md | 13 - ...lugins-data-public.isessionservice.save.md | 13 - ...ugins-data-public.isessionservice.start.md | 13 - ...gins-data-public.isessionservice.update.md | 13 - .../kibana-plugin-plugins-data-public.md | 5 +- ...hsessionrestorationinfoprovider.getname.md | 13 + ...orationinfoprovider.geturlgeneratordata.md | 15 ++ ...ic.searchsessionrestorationinfoprovider.md | 21 ++ ...plugin-plugins-data-public.sessionstate.md | 26 ++ .../application/dashboard_app_controller.tsx | 38 +++ .../dashboard/public/url_generator.test.ts | 33 +++ src/plugins/dashboard/public/url_generator.ts | 13 + .../data/common/search/aggs/agg_type.ts | 8 +- .../data/common/search/aggs/buckets/terms.ts | 9 +- .../data/common/search/session/types.ts | 75 +----- src/plugins/data/kibana.json | 3 +- src/plugins/data/public/index.ts | 8 +- src/plugins/data/public/public.api.md | 87 ++++--- src/plugins/data/public/search/index.ts | 10 +- src/plugins/data/public/search/mocks.ts | 4 +- .../public/search/search_interceptor.test.ts | 97 ++++++- .../data/public/search/search_interceptor.ts | 21 +- .../data/public/search/search_service.test.ts | 4 + .../data/public/search/search_service.ts | 13 +- .../search/session/index.ts} | 8 +- .../search/session/mocks.ts | 27 +- .../{ => session}/session_service.test.ts | 9 +- .../public/search/session/session_service.ts | 244 ++++++++++++++++++ .../search/session/session_state.test.ts | 126 +++++++++ .../public/search/session/session_state.ts | 232 +++++++++++++++++ .../public/search/session/sessions_client.ts | 91 +++++++ .../data/public/search/session_service.ts | 149 ----------- src/plugins/data/public/search/types.ts | 17 +- .../saved_objects/background_session.ts | 6 + .../data/server/search/routes/session.ts | 14 +- .../server/search/session/session_service.ts | 16 +- .../public/application/angular/discover.js | 34 ++- .../discover/public/url_generator.test.ts | 13 + src/plugins/discover/public/url_generator.ts | 34 ++- src/plugins/embeddable/public/public.api.md | 4 + x-pack/plugins/data_enhanced/public/plugin.ts | 1 + .../public/search/search_interceptor.ts | 4 +- .../background_session_indicator.stories.tsx | 12 +- .../background_session_indicator.test.tsx | 44 ++-- .../background_session_indicator.tsx | 51 +++- ...cted_background_session_indicator.test.tsx | 18 +- ...connected_background_session_indicator.tsx | 37 ++- .../index.ts | 1 - .../components/alerts_table/actions.test.tsx | 2 +- .../dashboard/async_search/async_search.ts | 43 +-- x-pack/test/functional/config.js | 1 + x-pack/test/functional/services/data/index.ts | 7 + .../services/data/send_to_background.ts | 58 +++++ x-pack/test/functional/services/index.ts | 2 + 70 files changed, 1491 insertions(+), 542 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionsclient.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md rename src/plugins/data/{common/mocks.ts => public/search/session/index.ts} (77%) rename src/plugins/data/{common => public}/search/session/mocks.ts (66%) rename src/plugins/data/public/search/{ => session}/session_service.test.ts (88%) create mode 100644 src/plugins/data/public/search/session/session_service.ts create mode 100644 src/plugins/data/public/search/session/session_state.test.ts create mode 100644 src/plugins/data/public/search/session/session_state.ts create mode 100644 src/plugins/data/public/search/session/sessions_client.ts delete mode 100644 src/plugins/data/public/search/session_service.ts create mode 100644 x-pack/test/functional/services/data/index.ts create mode 100644 x-pack/test/functional/services/data/send_to_background.ts 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 b2f8e83d8e654..a370c67f460f4 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 739fdfdeb5fc3..451dbc86b86b6 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 0000000000000..d9af202cf1018 --- /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 dba60c7bdf147..a27e155dda111 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 1ad194a9bec86..892b0fa6acb60 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 0000000000000..9c3210d2ec417 --- /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 0000000000000..d6efabb1b9518 --- /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 fc3d214eb4cad..0000000000000 --- 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 eabb966160c4d..0000000000000 --- 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 58e2fea0e6fe9..0000000000000 --- 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 a2dff2f18253b..0000000000000 --- 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 e30c89fb1a9fd..0000000000000 --- 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 838023ff1d8b9..0000000000000 --- 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 8d8cd1f31bb95..0000000000000 --- 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 db737880bb84e..0000000000000 --- 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 02c0a821e552d..8938c880a0471 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 96106a6ef7e2d..0000000000000 --- 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 4ac4a96614467..0000000000000 --- 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 9e14c5ed26765..0000000000000 --- 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 5e2ff53d58ab7..0000000000000 --- 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 bafcd8bdffff9..44a383d178c30 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) | | +| [SearchSessionRestorationInfoProvider](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.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) | \* | @@ -165,6 +166,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 0000000000000..ac3009ca2c025 --- /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) > [SearchSessionRestorationInfoProvider](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md) + +## SearchSessionRestorationInfoProvider.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 0000000000000..3ece1347fc434 --- /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) > [SearchSessionRestorationInfoProvider](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md) + +## SearchSessionRestorationInfoProvider.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 0000000000000..741a535bf6e06 --- /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) > [SearchSessionRestorationInfoProvider](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md) + +## SearchSessionRestorationInfoProvider interface + +Provide info about current search session to be stored in backgroundSearch saved object + +Signature: + +```typescript +export interface SearchSessionRestorationInfoProvider +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getName](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.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.searchsessionrestorationinfoprovider.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 0000000000000..9a60a5b2a9f9b --- /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 c99e4e4e06987..78a176866c18c 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -97,6 +97,11 @@ import { } from '../../../kibana_legacy/public'; import { migrateLegacyQuery } from './lib/migrate_legacy_query'; import { convertSavedDashboardPanelToPanelState } from '../../common/embeddable/embeddable_saved_object_converters'; +import { + createDashboardUrl, + DASHBOARD_APP_URL_GENERATOR, + DashboardUrlGeneratorState, +} from '../url_generator'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; @@ -429,6 +434,31 @@ export class DashboardAppController { DashboardContainer >(DASHBOARD_CONTAINER_TYPE); + searchService.session.setSearchSessionRestorationInfoProvider({ + getName: async () => 'Dashboard', + getUrlGeneratorData: async () => { + const appState = dashboardStateManager.getAppState(); + const state: DashboardUrlGeneratorState = { + dashboardId: dash.id, + timeRange: timefilter.getTime(), + filters: filterManager.getFilters(), + query: queryStringManager.formatQuery(appState.query), + savedQuery: appState.savedQuery, + useHash: false, + preserveSavedFilters: false, + viewMode: appState.viewMode, + panels: dash.id ? undefined : appState.panels, + searchSessionId: searchService.session.getSessionId(), + }; + + return { + urlGeneratorId: DASHBOARD_APP_URL_GENERATOR, + initialState: state, + restoreState: state, // TODO: handle relative time range + }; + }, + }); + if (dashboardFactory) { const searchSessionIdFromURL = getSearchSessionIdFromURL(history); if (searchSessionIdFromURL) { @@ -638,6 +668,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 +1236,7 @@ export class DashboardAppController { if (dashboardContainer) { dashboardContainer.destroy(); } + searchServiceSessionRefreshSubscribtion.unsubscribe(); searchService.session.clear(); }); } diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index 461caedc5cba7..0272e9d3ebdf7 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 b23b26e4022dd..182020d032e4e 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 4f4a593764b1e..bf6fe11f746f9 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 ac65e7fa813b3..7071d9c1dc9c4 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 d1ab22057695a..50ca3ca390ece 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 d6f2534bd5e3b..efcb7c60143c3 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -5,7 +5,8 @@ "ui": true, "requiredPlugins": [ "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 129addf3de70e..4101ce94b320f 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -375,6 +375,7 @@ export { SearchRequest, SearchSourceFields, SortDirection, + SessionState, // expression functions and types EsdslExpressionFunctionDefinition, EsRawResponseExpressionTypeDefinition, @@ -385,7 +386,12 @@ export { PainlessError, } from './search'; -export type { SearchSource, ISessionService } from './search'; +export type { + SearchSource, + ISessionService, + SearchSessionRestorationInfoProvider, + 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 6c4609e5506c2..b21841031322a 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -37,6 +37,7 @@ import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; 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'; @@ -59,7 +60,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'; @@ -79,6 +82,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'; @@ -1467,6 +1471,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) @@ -1482,6 +1487,7 @@ export interface ISearchStart { search: ISearchGeneric; searchSource: ISearchStartSearchSource; session: ISessionService; + sessionsClient: ISessionsClient; // (undocumented) showError: (e: Error) => void; } @@ -1497,25 +1503,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) // @@ -2092,6 +2090,7 @@ export class SearchInterceptor { timeoutSignal: AbortSignal; combinedSignal: AbortSignal; cleanup: () => void; + abort: () => void; }; // (undocumented) showError(e: Error): void; @@ -2118,6 +2117,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) "SearchSessionRestorationInfoProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface SearchSessionRestorationInfoProvider { + 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 @@ -2222,6 +2235,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) @@ -2395,22 +2419,23 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:426: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/index.ts b/src/plugins/data/public/search/index.ts index f6bd46c17192c..9219079ac3d57 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, + SearchSessionRestorationInfoProvider, + 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 836ddb618e746..b799c661051fa 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 60274261da25f..fa29b19ecafba 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 '.'; let searchInterceptor: SearchInterceptor; let mockCoreSetup: MockedKeys; @@ -99,7 +99,100 @@ 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); + mockCoreSetup.http.fetch.mockResolvedValue({ result: 200 }); + }; + + const mockRequest: IEsSearchRequest = { + params: {}, + }; + + afterEach(() => { + const sessionServiceMock = searchMock.session as jest.Mocked; + sessionServiceMock.getSessionId.mockReset(); + sessionServiceMock.isRestore.mockReset(); + sessionServiceMock.isStored.mockReset(); + mockCoreSetup.http.fetch.mockReset(); + }); + + test('infers isRestore from session service state', async () => { + const sessionId = 'sid'; + setup({ + isRestore: true, + sessionId, + }); + + await searchInterceptor.search(mockRequest, { sessionId }).toPromise(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalledWith( + expect.objectContaining({ + body: '{"sessionId":"sid","isStored":false,"isRestore":true,"params":{}}', + }) + ); + }); + + test('infers isStored from session service state', async () => { + const sessionId = 'sid'; + setup({ + isStored: true, + sessionId, + }); + + await searchInterceptor.search(mockRequest, { sessionId }).toPromise(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalledWith( + expect.objectContaining({ + body: '{"sessionId":"sid","isStored":true,"isRestore":false,"params":{}}', + }) + ); + }); + + 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(mockCoreSetup.http.fetch).toHaveBeenCalledWith( + expect.objectContaining({ + body: + '{"sessionId":"different session id","isStored":false,"isRestore":false,"params":{}}', + }) + ); + }); + + 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(mockCoreSetup.http.fetch).toHaveBeenCalledWith( + expect.objectContaining({ + body: '{"sessionId":"sessionId","isStored":false,"isRestore":false,"params":{}}', + }) + ); + }); }); 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 3fadb723b27cd..cdd9ec024b96a 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -28,7 +28,6 @@ import { IKibanaSearchResponse, ISearchOptions, ES_SEARCH_STRATEGY, - ISessionService, } from '../../common'; import { SearchUsageCollector } from './collectors'; import { @@ -42,6 +41,7 @@ import { } from './errors'; import { toMountPoint } from '../../../kibana_react/public'; import { AbortError, getCombinedAbortSignal } from '../../../kibana_utils/public'; +import { ISessionService } from './session'; export interface SearchInterceptorDeps { http: CoreSetup['http']; @@ -135,8 +135,14 @@ export class SearchInterceptor { ); const body = JSON.stringify({ sessionId: options?.sessionId, - isStored: options?.isStored, - isRestore: options?.isRestore, + isStored: + this.deps.session.getSessionId() === options?.sessionId + ? this.deps.session.isStored() + : false, + isRestore: + this.deps.session.getSessionId() === options?.sessionId + ? this.deps.session.isRestore() + : false, ...searchRequest, }); @@ -166,13 +172,17 @@ export class SearchInterceptor { timeoutController.abort(); }); + const externalAbortController = 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 `externalAbortController` + // 4. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines) const signals = [ this.abortController.signal, timeoutSignal, + externalAbortController.signal, ...(abortSignal ? [abortSignal] : []), ]; @@ -190,6 +200,9 @@ export class SearchInterceptor { timeoutSignal, combinedSignal, cleanup, + abort: () => { + externalAbortController.abort(); + }, }; } diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 20041a02067d9..11ba59d9540ad 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -46,6 +46,8 @@ describe('Search service', () => { expect(setup).toHaveProperty('aggs'); expect(setup).toHaveProperty('usageCollector'); expect(setup).toHaveProperty('__enhance'); + expect(setup).toHaveProperty('sessionsClient'); + expect(setup).toHaveProperty('session'); }); }); @@ -58,6 +60,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 96fb3f91ea85f..e567d2290bd9b 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -27,7 +27,6 @@ import { kibanaContext, kibanaContextFunction, ISearchGeneric, - ISessionService, SearchSourceDependencies, SearchSourceService, } from '../../common/search'; @@ -39,7 +38,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, @@ -65,6 +64,7 @@ export class SearchService implements Plugin { private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; private sessionService!: ISessionService; + private sessionsClient!: ISessionsClient; constructor(private initializerContext: PluginInitializerContext) {} @@ -74,7 +74,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. @@ -112,6 +117,7 @@ export class SearchService implements Plugin { this.searchInterceptor = enhancements.searchInterceptor; }, session: this.sessionService, + sessionsClient: this.sessionsClient, }; } @@ -143,6 +149,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 77% rename from src/plugins/data/common/mocks.ts rename to src/plugins/data/public/search/session/index.ts index dde70b1d07443..fd7c7c95b566d 100644 --- a/src/plugins/data/common/mocks.ts +++ b/src/plugins/data/public/search/session/index.ts @@ -17,4 +17,10 @@ * under the License. */ -export { getSessionServiceMock } from './search/session/mocks'; +export { + SessionService, + ISessionService, + SearchSessionRestorationInfoProvider, +} 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 66% rename from src/plugins/data/common/search/session/mocks.ts rename to src/plugins/data/public/search/session/mocks.ts index 4604e15e4e93b..b2c66e9e56b65 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(), + setSearchSessionRestorationInfoProvider: jest.fn(), + trackSearch: jest.fn((sessionId, 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 88% rename from src/plugins/data/public/search/session_service.test.ts rename to src/plugins/data/public/search/session/session_service.test.ts index bcfd06944d983..973daf61f0655 100644 --- a/src/plugins/data/public/search/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -17,10 +17,10 @@ * 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'; describe('Session service', () => { let sessionService: ISessionService; @@ -29,7 +29,8 @@ describe('Session service', () => { const initializerContext = coreMock.createPluginInitializerContext(); sessionService = new SessionService( initializerContext, - coreMock.createSetup().getStartServices + coreMock.createSetup().getStartServices, + getSessionsClientMock() ); }); 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 0000000000000..95bdfe58f152e --- /dev/null +++ b/src/plugins/data/public/search/session/session_service.ts @@ -0,0 +1,244 @@ +/* + * 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 SearchSessionRestorationInfoProvider { + /** + * 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 implements ISessionService { + public readonly state$: Observable; + private readonly state: SessionStateContainer; + + private searchSessionRestorationInfoProvider?: SearchSessionRestorationInfoProvider; + private appChangeSubscription$?: Subscription; + private curApp?: string; + + constructor( + initializerContext: PluginInitializerContext, + getStartServices: StartServicesAccessor, + private readonly sessionsClient: ISessionsClient + ) { + const { stateContainer, sessionState$ } = createSessionStateContainer(); + 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; + this.setSearchSessionRestorationInfoProvider(undefined); + }); + }); + } + + /** + * Set a provider of info about current session + * This will be used for creating a background session saved object + * @param searchSessionRestorationInfoProvider + */ + public setSearchSessionRestorationInfoProvider( + searchSessionRestorationInfoProvider: SearchSessionRestorationInfoProvider | undefined + ) { + this.searchSessionRestorationInfoProvider = searchSessionRestorationInfoProvider; + } + + /** + * Used to track pending searches within current session + * + * @param sessionId - sessionId that search belongs to. If not matching current session id, then search is not tracked + * @param searchDescriptor - uniq object that will be used to untrack the search + * @returns untrack function + */ + public trackSearch( + sessionId: string | undefined, + searchDescriptor: TrackSearchDescriptor + ): () => void { + if (!sessionId) return () => {}; + if (sessionId !== this.getSessionId()) return () => {}; + this.state.transitions.trackSearch(searchDescriptor); + return () => { + this.state.transitions.unTrackSearch(searchDescriptor); + }; + } + + public destroy() { + if (this.appChangeSubscription$) { + this.appChangeSubscription$.unsubscribe(); + } + this.clear(); + this.setSearchSessionRestorationInfoProvider(undefined); + } + + /** + * 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(); + } + + private refresh$ = new Subject(); + /** + * Observable emits when search result refresh was requested + */ + 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.searchSessionRestorationInfoProvider; + 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 0000000000000..7f11f61e2b3c3 --- /dev/null +++ b/src/plugins/data/public/search/session/session_state.test.ts @@ -0,0 +1,126 @@ +/* + * 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', () => { + expect(() => state.transitions.unTrackSearch({})).toThrowError(); + + 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 0000000000000..bfa41cf33dc81 --- /dev/null +++ b/src/plugins/data/public/search/session/session_state.ts @@ -0,0 +1,232 @@ +/* + * 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: () => SessionStateInternal< + SearchDescriptor +> = () => ({ + 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) => { + if (!state.sessionId) throw new Error("Can't untrack search. Missing sessionId"); + 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 = (): { + stateContainer: SessionStateContainer; + sessionState$: Observable; +} => { + const stateContainer = createStateContainer( + createSessionDefaultState(), + sessionPureTransitions, + sessionPureSelectors + ) 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 0000000000000..c19c5db064094 --- /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 0141cff258a9f..0000000000000 --- 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 c08d9f4c7be3f..057b242c22f20 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 74b03c4d867e4..e81272628c091 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 93f07ecfb92ff..f7dfc776565e0 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.ts b/src/plugins/data/server/search/session/session_service.ts index eca5f428b8555..b9a738413ede4 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 9319c58db3e33..b773938d23849 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -83,7 +83,7 @@ import { MODIFY_COLUMNS_ON_SWITCH, } from '../../../common'; import { METRIC_TYPE } from '@kbn/analytics'; -import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator'; +import { SEARCH_SESSION_ID_QUERY_PARAM, DISCOVER_APP_URL_GENERATOR } from '../../url_generator'; import { removeQueryParam, getQueryParams } from '../../../../kibana_utils/public'; const fetchStatuses = { @@ -216,6 +216,13 @@ 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 { appStateContainer, startSync: startStateSync, @@ -291,6 +298,31 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise } }); + data.search.session.setSearchSessionRestorationInfoProvider({ + getName: async () => 'Discover', + getUrlGeneratorData: async () => { + const appState = appStateContainer.getState(); + const state = { + filters: filterManager.getFilters(), + indexPatternId: appState.index, + query: appState.query, + savedSearchId: savedSearch.id, + timeRange: 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, + }; + return { + urlGeneratorId: DISCOVER_APP_URL_GENERATOR, + initialState: state, + restoreState: state, // TODO: handle relative time range + }; + }, + }); + $scope.setIndexPattern = async (id) => { const nextIndexPattern = await indexPatterns.get(id); if (nextIndexPattern) { diff --git a/src/plugins/discover/public/url_generator.test.ts b/src/plugins/discover/public/url_generator.test.ts index 98b7625e63c72..95bff6b1fdc9c 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 df9b16a4627ec..6d86818910b11 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 f3f3682404e32..9e76303bc33e4 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -33,6 +33,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'; @@ -55,7 +56,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'; @@ -76,6 +79,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 948858a5ed4c1..f4909877466cf 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -65,6 +65,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.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index e1bd71caddb4d..2c831b7c8bdbc 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -66,7 +66,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { ) { let { id } = request; - const { combinedSignal, timeoutSignal, cleanup } = this.setupAbortSignal({ + const { combinedSignal, timeoutSignal, cleanup, abort } = this.setupAbortSignal({ abortSignal: options.abortSignal, timeout: this.searchTimeout, }); @@ -75,6 +75,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { this.pendingCount$.next(this.pendingCount$.getValue() + 1); + const untrackSearch = this.deps.session.trackSearch(options.sessionId, { abort }); return doPartialSearch( () => this.runSearch(request, { ...options, strategy, abortSignal: combinedSignal }), (requestId) => @@ -103,6 +104,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { this.pendingCount$.next(this.pendingCount$.getValue() - 1); cleanup(); abortedPromise.cleanup(); + untrackSearch(); }) ); } 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 9cef76c62279c..c7195ea486e2f 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 5b7ab2e4f9b1f..f401a460113c7 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 b55bd6b655371..16083730d9ce8 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; } @@ -55,11 +56,10 @@ const ContinueInBackgroundButton = ({ ); const ViewBackgroundSessionsButton = ({ - onViewBackgroundSessions = () => {}, + viewBackgroundSessionsLink = 'management', buttonProps = {}, }: ActionButtonProps) => ( - // TODO: make this a link - + {}, buttonProps = {} }: ActionButton ); const backgroundSessionIndicatorViewStateToProps: { - [state in BackgroundSessionViewState]: { + [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 +117,7 @@ const backgroundSessionIndicatorViewStateToProps: { secondaryAction: ContinueInBackgroundButton, }, }, - [BackgroundSessionViewState.Completed]: { + [SessionState.Completed]: { button: { color: 'subdued', iconType: 'checkInCircleFilled', @@ -141,7 +142,7 @@ const backgroundSessionIndicatorViewStateToProps: { secondaryAction: ViewBackgroundSessionsButton, }, }, - [BackgroundSessionViewState.BackgroundLoading]: { + [SessionState.BackgroundLoading]: { button: { iconType: EuiLoadingSpinner, 'aria-label': i18n.translate( @@ -165,7 +166,7 @@ const backgroundSessionIndicatorViewStateToProps: { secondaryAction: ViewBackgroundSessionsButton, }, }, - [BackgroundSessionViewState.BackgroundCompleted]: { + [SessionState.BackgroundCompleted]: { button: { color: 'success', iconType: 'checkInCircleFilled', @@ -192,7 +193,7 @@ const backgroundSessionIndicatorViewStateToProps: { primaryAction: ViewBackgroundSessionsButton, }, }, - [BackgroundSessionViewState.Restored]: { + [SessionState.Restored]: { button: { color: 'warning', iconType: 'refresh', @@ -217,6 +218,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 +248,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 ( ; 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) @@ -27,10 +32,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 d097a1aecb66a..1cc2fffcea8c5 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 adbb6edbbfcf3..223a0537129df 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 ecc0fc54d0d47..140cb8039bef6 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 17497c8326777..86ea414aebce6 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,33 @@ 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', () => { + it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes.', 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(); + 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 queryBar.clickQuerySubmitButton(); // TODO: use refresh from the sendToBackgroundUI instead + 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); + }); + + // TODO: + // it.todo('Save and restore session'); + // it.todo('Cancel running search'); + // it.todo('Cancel saved search search. Saved session is deleted.'); }); }); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 70e92e88e60be..e3f83f08eb758 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 0000000000000..c2e3fcb41a7c9 --- /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 0000000000000..42a59de9e2c5f --- /dev/null +++ b/x-pack/test/functional/services/data/send_to_background.ts @@ -0,0 +1,58 @@ +/* + * 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'; +type SessionStateType = + | 'none' + | 'loading' + | 'completed' + | 'backgroundLoading' + | 'backgroundCompleted' + | 'restored' + | 'canceled'; + +export function SendToBackgroundProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + 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() { + throw new Error('TODO'); + } + + public async save() { + throw new Error('TODO'); + } + + public async cancel() { + throw new Error('TODO'); + } + + public async refresh() { + throw new Error('TODO'); + } + })(); +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 6d8eade25d7e6..7afc2fa4e10ff 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -55,6 +55,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 @@ -101,4 +102,5 @@ export const services = { dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider, dashboardDrilldownsManage: DashboardDrilldownsManageProvider, dashboardPanelTimeRange: DashboardPanelTimeRangeProvider, + sendToBackground: SendToBackgroundProvider, }; From 112d5e3b1badc3a006bfabc54fe8c8d2dbcd00b8 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 19 Nov 2020 15:22:58 +0100 Subject: [PATCH 02/18] fix --- .../application/dashboard_app_controller.tsx | 6 +----- .../server/search/session/session_service.test.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 78a176866c18c..f6094481ceddb 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -97,11 +97,7 @@ import { } from '../../../kibana_legacy/public'; import { migrateLegacyQuery } from './lib/migrate_legacy_query'; import { convertSavedDashboardPanelToPanelState } from '../../common/embeddable/embeddable_saved_object_converters'; -import { - createDashboardUrl, - DASHBOARD_APP_URL_GENERATOR, - DashboardUrlGeneratorState, -} from '../url_generator'; +import { DASHBOARD_APP_URL_GENERATOR, DashboardUrlGeneratorState } from '../url_generator'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; 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 1ceebae967d4c..5ff6d4b932487 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: [], From 95a1b83aa3094a703388ded82846ac2df41485c8 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 Nov 2020 13:10:11 +0100 Subject: [PATCH 03/18] tests --- .../search/session/session_service.test.ts | 43 +++++++++- .../public/search/session/session_service.ts | 7 +- .../public/search/session/session_state.ts | 7 +- .../public/search/search_interceptor.test.ts | 81 ++++++++++++++++++- 4 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index 973daf61f0655..7fe0cf35ea5f3 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -21,17 +21,23 @@ 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, - getSessionsClientMock() + getSessionsClientMock(), + { freezeState: false } // needed to use mocks inside state container ); + state$ = new BehaviorSubject(SessionState.None); + sessionService.state$.subscribe(state$); }); describe('Session management', () => { @@ -56,5 +62,40 @@ describe('Session service', () => { expect(await emittedValues).toEqual(['1', '2', undefined]); }); + + it('Only tracks searches for current session', () => { + sessionService.trackSearch(undefined, { abort: () => {} }); + expect(state$.getValue()).toBe(SessionState.None); + + const id = sessionService.start(); + sessionService.trackSearch(undefined, { abort: () => {} }); + expect(state$.getValue()).toBe(SessionState.None); + sessionService.trackSearch('other_id', { abort: () => {} }); + expect(state$.getValue()).toBe(SessionState.None); + + const untrack = sessionService.trackSearch(id, { abort: () => {} }); + expect(state$.getValue()).toBe(SessionState.Loading); + untrack(); + expect(state$.getValue()).toBe(SessionState.Completed); + }); + + it('Cancels all tracked searches within current session', async () => { + const nonCurrentAbort = jest.fn(); + const currentAbort = jest.fn(); + + sessionService.trackSearch(undefined, { abort: nonCurrentAbort }); + expect(state$.getValue()).toBe(SessionState.None); + + const id = sessionService.start(); + sessionService.trackSearch(undefined, { abort: nonCurrentAbort }); + sessionService.trackSearch('other_id', { abort: nonCurrentAbort }); + sessionService.trackSearch(id, { abort: currentAbort }); + sessionService.trackSearch(id, { abort: currentAbort }); + + await sessionService.cancel(); + + expect(nonCurrentAbort).not.toBeCalled(); + expect(currentAbort).toBeCalledTimes(2); + }); }); }); diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 95bdfe58f152e..2afd21e66cd57 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -62,9 +62,12 @@ export class SessionService implements ISessionService { constructor( initializerContext: PluginInitializerContext, getStartServices: StartServicesAccessor, - private readonly sessionsClient: ISessionsClient + private readonly sessionsClient: ISessionsClient, + { freezeState = true }: { freezeState: boolean } = { freezeState: true } ) { - const { stateContainer, sessionState$ } = createSessionStateContainer(); + const { stateContainer, sessionState$ } = createSessionStateContainer({ + freeze: freezeState, + }); this.state$ = sessionState$; this.state = stateContainer; diff --git a/src/plugins/data/public/search/session/session_state.ts b/src/plugins/data/public/search/session/session_state.ts index bfa41cf33dc81..2ea74d4a901ef 100644 --- a/src/plugins/data/public/search/session/session_state.ts +++ b/src/plugins/data/public/search/session/session_state.ts @@ -210,14 +210,17 @@ export type SessionStateContainer = StateContainer< SessionPureSelectors >; -export const createSessionStateContainer = (): { +export const createSessionStateContainer = ( + { freeze = true }: { freeze: boolean } = { freeze: true } +): { stateContainer: SessionStateContainer; sessionState$: Observable; } => { const stateContainer = createStateContainer( createSessionDefaultState(), sessionPureTransitions, - sessionPureSelectors + sessionPureSelectors, + freeze ? undefined : { freeze: (s) => s } ) as SessionStateContainer; const sessionState$: Observable = stateContainer.state$.pipe( 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 044489d58eb0e..3415ad16f600b 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,7 +9,7 @@ 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 } from 'src/plugins/data/public'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; const timeTravel = (msToRun = 0) => { @@ -41,11 +41,13 @@ function mockFetchImplementation(responses: any[]) { describe('EnhancedSearchInterceptor', () => { let mockUsageCollector: any; + let sessionService: jest.Mocked; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); const dataPluginMockStart = dataPluginMock.createStartContract(); + sessionService = dataPluginMockStart.search.session as jest.Mocked; mockCoreSetup.uiSettings.get.mockImplementation((name: string) => { switch (name) { @@ -80,7 +82,7 @@ describe('EnhancedSearchInterceptor', () => { http: mockCoreSetup.http, uiSettings: mockCoreSetup.uiSettings, usageCollector: mockUsageCollector, - session: dataPluginMockStart.search.session, + session: sessionService, }); }); @@ -392,4 +394,79 @@ describe('EnhancedSearchInterceptor', () => { expect(mockUsageCollector.trackQueriesCancelled).toBeCalledTimes(1); }); }); + + describe('session', () => { + test('should track searches', async () => { + const untrack = jest.fn(); + sessionService.trackSearch.mockImplementation(() => untrack); + + const responses = [ + { + time: 10, + value: { + isPartial: false, + isRunning: true, + id: 1, + }, + }, + { + time: 300, + value: { + isPartial: false, + isRunning: false, + id: 1, + }, + }, + ]; + mockFetchImplementation(responses); + + const response = searchInterceptor.search({}, { pollInterval: 0 }); + 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 untrack = jest.fn(); + sessionService.trackSearch.mockImplementation(() => untrack); + + const responses = [ + { + time: 10, + value: { + isPartial: false, + isRunning: true, + id: 1, + }, + }, + { + time: 300, + value: { + isPartial: false, + isRunning: false, + id: 1, + }, + }, + ]; + mockFetchImplementation(responses); + const response = searchInterceptor.search({}, { pollInterval: 0 }); + response.subscribe({ next, error }); + await timeTravel(10); + expect(sessionService.trackSearch).toBeCalledTimes(1); + + const abort = sessionService.trackSearch.mock.calls[0][1].abort; + expect(abort).toBeInstanceOf(Function); + + abort(); + + await timeTravel(10); + + expect(error).toHaveBeenCalled(); + expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); + }); + }); }); From 735a651da538a175df1dc03a9eb3ab4198d132b0 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 Nov 2020 13:59:04 +0100 Subject: [PATCH 04/18] tets --- .../background_session_indicator.tsx | 35 ++++++++++++++--- .../dashboard/async_search/async_search.ts | 34 +++++++++++++---- .../services/data/send_to_background.ts | 38 +++++++++++++++++-- 3 files changed, 90 insertions(+), 17 deletions(-) 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 16083730d9ce8..674ce392aa2d0 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 @@ -35,7 +35,11 @@ export interface BackgroundSessionIndicatorProps { type ActionButtonProps = BackgroundSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps }; const CancelButton = ({ onCancel = () => {}, buttonProps = {} }: ActionButtonProps) => ( - + {}, buttonProps = {}, }: ActionButtonProps) => ( - + ( - + {}, buttonProps = {} }: ActionButtonProps) => ( - + {}, buttonProps = {} }: ActionButtonP ); const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButtonProps) => ( - + {}, buttonProps = {} }: ActionButton const backgroundSessionIndicatorViewStateToProps: { [state in SessionState]: { - button: Pick & { tooltipText: string }; + button: Pick & { + tooltipText: string; + }; popover: { text: string; primaryAction?: React.ComponentType; @@ -278,6 +300,7 @@ export const BackgroundSessionIndicator: React.FC 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 86ea414aebce6..c9db2b1221545 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 @@ -80,13 +80,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); describe('Send to background', () => { - it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes.', async () => { + 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.navigateTo(savedSessionURL); + 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 @@ -94,7 +97,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const session1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); expect(session1).to.be(fakeSessionId); - await queryBar.clickQuerySubmitButton(); // TODO: use refresh from the sendToBackgroundUI instead + await sendToBackground.refresh(); await PageObjects.header.waitUntilLoadingHasFinished(); await sendToBackground.expectState('completed'); await testSubjects.missingOrFail('embeddableErrorLabel'); @@ -102,10 +105,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(session2).not.to.be(fakeSessionId); }); - // TODO: - // it.todo('Save and restore session'); - // it.todo('Cancel running search'); - // it.todo('Cancel saved search search. Saved session is deleted.'); + 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/services/data/send_to_background.ts b/x-pack/test/functional/services/data/send_to_background.ts index 42a59de9e2c5f..f6a28c59b737d 100644 --- a/x-pack/test/functional/services/data/send_to_background.ts +++ b/x-pack/test/functional/services/data/send_to_background.ts @@ -8,6 +8,8 @@ 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' @@ -20,6 +22,7 @@ type SessionStateType = 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 { @@ -40,19 +43,46 @@ export function SendToBackgroundProvider({ getService }: FtrProviderContext) { } public async viewBackgroundSessions() { - throw new Error('TODO'); + await this.ensurePopoverOpened(); + await testSubjects.click('backgroundSessionIndicatorViewBackgroundSessionsLink'); } public async save() { - throw new Error('TODO'); + await this.ensurePopoverOpened(); + await testSubjects.click('backgroundSessionIndicatorSaveBtn'); + await this.ensurePopoverClosed(); } public async cancel() { - throw new Error('TODO'); + await this.ensurePopoverOpened(); + await testSubjects.click('backgroundSessionIndicatorCancelBtn'); + await this.ensurePopoverClosed(); } public async refresh() { - throw new Error('TODO'); + 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)); + }); } })(); } From 31187f5b02c6322168d45424ade7b7ff5fa94b69 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 Nov 2020 14:07:38 +0100 Subject: [PATCH 05/18] prettier bump --- .../connected_background_session_indicator.test.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 a0937627f11ba..4c9fd50dc8c4c 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 @@ -13,9 +13,8 @@ import { ISessionService, SessionState } from '../../../../../../../src/plugins/ import { coreMock } from '../../../../../../../src/core/public/mocks'; const coreStart = coreMock.createStart(); -const sessionService = dataPluginMock.createStartContract().search.session as jest.Mocked< - ISessionService ->; +const sessionService = dataPluginMock.createStartContract().search + .session as jest.Mocked; test("shouldn't show indicator in case no active search session", async () => { const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ From e2f81ceba21d6380e4af12ff0e93a2b46bc46f6b Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 Nov 2020 15:54:04 +0100 Subject: [PATCH 06/18] prettier --- src/plugins/data/public/search/session/session_state.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/data/public/search/session/session_state.ts b/src/plugins/data/public/search/session/session_state.ts index 2ea74d4a901ef..c5606b3ac9c0b 100644 --- a/src/plugins/data/public/search/session/session_state.ts +++ b/src/plugins/data/public/search/session/session_state.ts @@ -107,9 +107,9 @@ export interface SessionStateInternal { isCanceled: boolean; } -const createSessionDefaultState: () => SessionStateInternal< - SearchDescriptor -> = () => ({ +const createSessionDefaultState: < + SearchDescriptor = unknown +>() => SessionStateInternal = () => ({ sessionId: undefined, isStored: false, isRestore: false, From 6d4c849a3db2475af7e8cbfa4a6289a3ce1ce751 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 24 Nov 2020 13:13:43 +0100 Subject: [PATCH 07/18] docs --- src/plugins/data/public/public.api.md | 87 +++++++++++++++++---------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index e1af3cc1d1b4d..582ab9a9e93df 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -39,6 +39,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'; @@ -61,7 +62,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'; @@ -81,6 +84,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'; @@ -1475,6 +1479,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) @@ -1490,6 +1495,7 @@ export interface ISearchStart { search: ISearchGeneric; searchSource: ISearchStartSearchSource; session: ISessionService; + sessionsClient: ISessionsClient; // (undocumented) showError: (e: Error) => void; } @@ -1505,25 +1511,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) // @@ -2104,6 +2102,7 @@ export class SearchInterceptor { timeoutSignal: AbortSignal; combinedSignal: AbortSignal; cleanup: () => void; + abort: () => void; }; // (undocumented) showError(e: Error): void; @@ -2130,6 +2129,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) "SearchSessionRestorationInfoProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface SearchSessionRestorationInfoProvider { + 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 @@ -2234,6 +2247,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) @@ -2409,22 +2433,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) From 1b9985dc76659da1b4c8bc75111dd70b5193e51d Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 24 Nov 2020 13:49:38 +0100 Subject: [PATCH 08/18] simplify trackSearch methods --- .../data/public/search/search_interceptor.ts | 3 +- .../search/session/session_service.test.ts | 39 ++++----- .../public/search/session/session_service.ts | 10 +-- .../public/search/search_interceptor.test.ts | 82 +++++++++++++------ .../public/search/search_interceptor.ts | 10 ++- 5 files changed, 86 insertions(+), 58 deletions(-) diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index cdd9ec024b96a..fa0b730d68761 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -177,7 +177,8 @@ export class SearchInterceptor { // 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. abort() is called on `externalAbortController` + // 3. abort() is called on `externalAbortController` which is used by session service + // to cancel searches that belong to the currently tracked session // 4. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines) const signals = [ this.abortController.signal, diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index 7fe0cf35ea5f3..83c3185ead63e 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -63,39 +63,34 @@ describe('Session service', () => { expect(await emittedValues).toEqual(['1', '2', undefined]); }); - it('Only tracks searches for current session', () => { - sessionService.trackSearch(undefined, { abort: () => {} }); + it('Tracks searches for current session', () => { + expect(() => sessionService.trackSearch({ abort: () => {} })).toThrowError(); expect(state$.getValue()).toBe(SessionState.None); - const id = sessionService.start(); - sessionService.trackSearch(undefined, { abort: () => {} }); - expect(state$.getValue()).toBe(SessionState.None); - sessionService.trackSearch('other_id', { abort: () => {} }); - expect(state$.getValue()).toBe(SessionState.None); - - const untrack = sessionService.trackSearch(id, { abort: () => {} }); + sessionService.start(); + const untrack1 = sessionService.trackSearch({ abort: () => {} }); expect(state$.getValue()).toBe(SessionState.Loading); - untrack(); + 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 nonCurrentAbort = jest.fn(); - const currentAbort = jest.fn(); - - sessionService.trackSearch(undefined, { abort: nonCurrentAbort }); - expect(state$.getValue()).toBe(SessionState.None); + const abort = jest.fn(); - const id = sessionService.start(); - sessionService.trackSearch(undefined, { abort: nonCurrentAbort }); - sessionService.trackSearch('other_id', { abort: nonCurrentAbort }); - sessionService.trackSearch(id, { abort: currentAbort }); - sessionService.trackSearch(id, { abort: currentAbort }); + sessionService.start(); + sessionService.trackSearch({ abort }); + sessionService.trackSearch({ abort }); + sessionService.trackSearch({ abort }); + const untrack = sessionService.trackSearch({ abort }); + untrack(); await sessionService.cancel(); - expect(nonCurrentAbort).not.toBeCalled(); - expect(currentAbort).toBeCalledTimes(2); + 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 index 2afd21e66cd57..7147817090f35 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -108,16 +108,12 @@ export class SessionService implements ISessionService { /** * Used to track pending searches within current session * - * @param sessionId - sessionId that search belongs to. If not matching current session id, then search is not tracked * @param searchDescriptor - uniq object that will be used to untrack the search * @returns untrack function */ - public trackSearch( - sessionId: string | undefined, - searchDescriptor: TrackSearchDescriptor - ): () => void { - if (!sessionId) return () => {}; - if (sessionId !== this.getSessionId()) return () => {}; + public trackSearch(searchDescriptor: TrackSearchDescriptor): () => void { + if (!this.getSessionId()) + throw new Error("Can't track search because there is no current session"); this.state.transitions.trackSearch(searchDescriptor); return () => { this.state.transitions.unTrackSearch(searchDescriptor); 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 3415ad16f600b..b0c07b11cd0c3 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 @@ -139,6 +139,7 @@ describe('EnhancedSearchInterceptor', () => { }, }, ]; + mockFetchImplementation(responses); const response = searchInterceptor.search({}, { pollInterval: 0 }); @@ -396,10 +397,7 @@ describe('EnhancedSearchInterceptor', () => { }); describe('session', () => { - test('should track searches', async () => { - const untrack = jest.fn(); - sessionService.trackSearch.mockImplementation(() => untrack); - + beforeEach(() => { const responses = [ { time: 10, @@ -418,9 +416,18 @@ describe('EnhancedSearchInterceptor', () => { }, }, ]; + mockFetchImplementation(responses); + }); - const response = searchInterceptor.search({}, { pollInterval: 0 }); + 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); @@ -431,34 +438,18 @@ describe('EnhancedSearchInterceptor', () => { }); 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 responses = [ - { - time: 10, - value: { - isPartial: false, - isRunning: true, - id: 1, - }, - }, - { - time: 300, - value: { - isPartial: false, - isRunning: false, - id: 1, - }, - }, - ]; - mockFetchImplementation(responses); - const response = searchInterceptor.search({}, { pollInterval: 0 }); + 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][1].abort; + const abort = sessionService.trackSearch.mock.calls[0][0].abort; expect(abort).toBeInstanceOf(Function); abort(); @@ -468,5 +459,44 @@ describe('EnhancedSearchInterceptor', () => { 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 2c831b7c8bdbc..fe64a7686908e 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -75,7 +75,11 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { this.pendingCount$.next(this.pendingCount$.getValue() + 1); - const untrackSearch = this.deps.session.trackSearch(options.sessionId, { abort }); + const isCurrentSession = + options.sessionId && options.sessionId === this.deps.session.getSessionId(); + + const untrackSearch = isCurrentSession && this.deps.session.trackSearch({ abort }); + return doPartialSearch( () => this.runSearch(request, { ...options, strategy, abortSignal: combinedSignal }), (requestId) => @@ -104,7 +108,9 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { this.pendingCount$.next(this.pendingCount$.getValue() - 1); cleanup(); abortedPromise.cleanup(); - untrackSearch(); + if (untrackSearch) { + untrackSearch(); + } }) ); } From 3a7f4acbb1f50a1d3efae6214a8dcab969e5bcad Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 24 Nov 2020 13:56:17 +0100 Subject: [PATCH 09/18] use dashboard's name --- .../application/dashboard_app_controller.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index f6094481ceddb..15082af2ce1af 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -263,6 +263,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() && @@ -431,7 +441,7 @@ export class DashboardAppController { >(DASHBOARD_CONTAINER_TYPE); searchService.session.setSearchSessionRestorationInfoProvider({ - getName: async () => 'Dashboard', + getName: async () => getDashTitle(), getUrlGeneratorData: async () => { const appState = dashboardStateManager.getAppState(); const state: DashboardUrlGeneratorState = { @@ -578,16 +588,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([ From a0495fb2de871d110ac71cec5ae59d216600d6be Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 24 Nov 2020 14:49:11 +0100 Subject: [PATCH 10/18] improve readability --- .../data/public/search/search_interceptor.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index fa0b730d68761..e1cd4b5154113 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -133,16 +133,14 @@ export class SearchInterceptor { `/internal/search/${options?.strategy ?? ES_SEARCH_STRATEGY}/${id ?? ''}`, '/' ); + + const isCurrentSession = + options?.sessionId && this.deps.session.getSessionId() === options.sessionId; + const body = JSON.stringify({ sessionId: options?.sessionId, - isStored: - this.deps.session.getSessionId() === options?.sessionId - ? this.deps.session.isStored() - : false, - isRestore: - this.deps.session.getSessionId() === options?.sessionId - ? this.deps.session.isRestore() - : false, + isStored: isCurrentSession ? this.deps.session.isStored() : false, + isRestore: isCurrentSession ? this.deps.session.isRestore() : false, ...searchRequest, }); From d331397dc81615af96dba93906798ff6e2d86e5f Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 24 Nov 2020 14:50:41 +0100 Subject: [PATCH 11/18] fix types --- src/plugins/data/public/search/session/mocks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts index b2c66e9e56b65..b99c8e5de012e 100644 --- a/src/plugins/data/public/search/session/mocks.ts +++ b/src/plugins/data/public/search/session/mocks.ts @@ -41,7 +41,7 @@ export function getSessionServiceMock(): jest.Mocked { getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), state$: new BehaviorSubject(SessionState.None).asObservable(), setSearchSessionRestorationInfoProvider: jest.fn(), - trackSearch: jest.fn((sessionId, searchDescriptor) => () => {}), + trackSearch: jest.fn((searchDescriptor) => () => {}), destroy: jest.fn(), onRefresh$: new Subject(), refresh: jest.fn(), From a5b9422c9683da6abed651df63cc8dfabff68a8a Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 25 Nov 2020 12:00:48 +0100 Subject: [PATCH 12/18] improve --- .../public/application/angular/discover.js | 33 +++--------- .../application/angular/discover_state.ts | 51 +++++++++++++++++-- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 7d8bb9b5a5e9c..a2beca24ece1c 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 { getState, splitState, createSearchSessionRestorationDataProvider } from './discover_state'; import { RequestAdapter } from '../../../../inspector/public'; import { @@ -287,30 +287,13 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise } }); - data.search.session.setSearchSessionRestorationInfoProvider({ - getName: async () => 'Discover', - getUrlGeneratorData: async () => { - const appState = appStateContainer.getState(); - const state = { - filters: filterManager.getFilters(), - indexPatternId: appState.index, - query: appState.query, - savedSearchId: savedSearch.id, - timeRange: 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, - }; - return { - urlGeneratorId: DISCOVER_APP_URL_GENERATOR, - initialState: state, - restoreState: state, // TODO: handle relative time range - }; - }, - }); + data.search.session.setSearchSessionRestorationInfoProvider( + createSearchSessionRestorationDataProvider({ + appStateContainer, + data, + getSavedSearchId: () => savedSearch.id, + }) + ); $scope.setIndexPattern = async (id) => { const nextIndexPattern = await indexPatterns.get(id); diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index 3c6ef1d3e4334..7626af6ceb9e3 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, + SearchSessionRestorationInfoProvider, +} from '../../../../data/public'; import { migrateLegacyQuery } from '../helpers/migrate_legacy_query'; +import { DISCOVER_APP_URL_GENERATOR } from '../../url_generator'; export interface AppState { /** @@ -247,3 +255,38 @@ export function isEqualState(stateA: AppState, stateB: AppState) { const { filters: stateBFilters = [], ...stateBPartial } = stateB; return isEqual(stateAPartial, stateBPartial) && isEqualFilters(stateAFilters, stateBFilters); } + +export function createSearchSessionRestorationDataProvider({ + appStateContainer, + data, + getSavedSearchId, +}: { + appStateContainer: StateContainer; + data: DataPublicPluginStart; + getSavedSearchId: () => string | undefined; +}): SearchSessionRestorationInfoProvider { + return { + getName: async () => 'Discover', + getUrlGeneratorData: async () => { + const appState = appStateContainer.get(); + const state = { + 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, + }; + return { + urlGeneratorId: DISCOVER_APP_URL_GENERATOR, + initialState: state, + restoreState: state, // TODO: handle relative time range + }; + }, + }; +} From ba5516e73192c609e885b63863ba3f4ffba211ce Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 25 Nov 2020 12:52:21 +0100 Subject: [PATCH 13/18] fix discover imports --- .../discover/public/application/angular/discover.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index a2beca24ece1c..d5e8e5f1984fd 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, createSearchSessionRestorationDataProvider } from './discover_state'; +import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state'; import { RequestAdapter } from '../../../../inspector/public'; import { @@ -59,15 +59,15 @@ import { popularizeField } from '../helpers/popularize_field'; import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_pattern_app_state'; import { addFatalError } from '../../../../kibana_legacy/public'; import { METRIC_TYPE } from '@kbn/analytics'; -import { SEARCH_SESSION_ID_QUERY_PARAM, DISCOVER_APP_URL_GENERATOR } from '../../url_generator'; -import { removeQueryParam, getQueryParams } from '../../../../kibana_utils/public'; +import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator'; +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'; From 3fdef3909261bd8f165f938a275d18326dbe57b8 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 25 Nov 2020 16:54:32 +0100 Subject: [PATCH 14/18] improve: clean up controllers, renamed 'externalAbortController' --- .../application/dashboard_app_controller.tsx | 39 ++++------- .../dashboard/public/application/lib/index.ts | 1 + .../application/lib/session_restoration.ts | 66 +++++++++++++++++++ .../data/public/search/search_interceptor.ts | 8 +-- .../application/angular/discover_state.ts | 53 ++++++++------- 5 files changed, 114 insertions(+), 53 deletions(-) create mode 100644 src/plugins/dashboard/public/application/lib/session_restoration.ts diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 15082af2ce1af..721d76aa0714b 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'; @@ -97,7 +97,6 @@ import { } from '../../../kibana_legacy/public'; import { migrateLegacyQuery } from './lib/migrate_legacy_query'; import { convertSavedDashboardPanelToPanelState } from '../../common/embeddable/embeddable_saved_object_converters'; -import { DASHBOARD_APP_URL_GENERATOR, DashboardUrlGeneratorState } from '../url_generator'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; @@ -151,7 +150,7 @@ export class DashboardAppController { dashboardCapabilities, scopedHistory, embeddableCapabilities: { visualizeCapabilities, mapsCapabilities }, - data: { query: queryService, search: searchService }, + data, core: { notifications, overlays, @@ -169,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; @@ -440,30 +441,14 @@ export class DashboardAppController { DashboardContainer >(DASHBOARD_CONTAINER_TYPE); - searchService.session.setSearchSessionRestorationInfoProvider({ - getName: async () => getDashTitle(), - getUrlGeneratorData: async () => { - const appState = dashboardStateManager.getAppState(); - const state: DashboardUrlGeneratorState = { - dashboardId: dash.id, - timeRange: timefilter.getTime(), - filters: filterManager.getFilters(), - query: queryStringManager.formatQuery(appState.query), - savedQuery: appState.savedQuery, - useHash: false, - preserveSavedFilters: false, - viewMode: appState.viewMode, - panels: dash.id ? undefined : appState.panels, - searchSessionId: searchService.session.getSessionId(), - }; - - return { - urlGeneratorId: DASHBOARD_APP_URL_GENERATOR, - initialState: state, - restoreState: state, // TODO: handle relative time range - }; - }, - }); + searchService.session.setSearchSessionRestorationInfoProvider( + createSessionRestorationDataProvider({ + data, + getDashboardTitle: () => getDashTitle(), + getDashboardId: () => dash.id, + getAppState: () => dashboardStateManager.getAppState(), + }) + ); if (dashboardFactory) { const searchSessionIdFromURL = getSearchSessionIdFromURL(history); diff --git a/src/plugins/dashboard/public/application/lib/index.ts b/src/plugins/dashboard/public/application/lib/index.ts index e9ebe73c3b34d..6741bbbc5d4b1 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 0000000000000..f8ea8f8dcd76d --- /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/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 61dae9edc0dbd..b0dedc8fb2080 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -165,18 +165,18 @@ export class SearchInterceptor { timeoutController.abort(); }); - const externalAbortController = new AbortController(); + 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. abort() is called on `externalAbortController` which is used by session service + // 3. abort() is called on `selfAbortController`. This is used by session service to abort all pending searches that it tracks. // to cancel searches that belong to the currently tracked session // 4. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines) const signals = [ this.abortController.signal, timeoutSignal, - externalAbortController.signal, + selfAbortController.signal, ...(abortSignal ? [abortSignal] : []), ]; @@ -195,7 +195,7 @@ export class SearchInterceptor { combinedSignal, cleanup, abort: () => { - externalAbortController.abort(); + selfAbortController.abort(); }, }; } diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index 7626af6ceb9e3..cdb94028b7d3e 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -36,7 +36,7 @@ import { SearchSessionRestorationInfoProvider, } from '../../../../data/public'; import { migrateLegacyQuery } from '../helpers/migrate_legacy_query'; -import { DISCOVER_APP_URL_GENERATOR } from '../../url_generator'; +import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../url_generator'; export interface AppState { /** @@ -256,11 +256,7 @@ export function isEqualState(stateA: AppState, stateB: AppState) { return isEqual(stateAPartial, stateBPartial) && isEqualFilters(stateAFilters, stateBFilters); } -export function createSearchSessionRestorationDataProvider({ - appStateContainer, - data, - getSavedSearchId, -}: { +export function createSearchSessionRestorationDataProvider(deps: { appStateContainer: StateContainer; data: DataPublicPluginStart; getSavedSearchId: () => string | undefined; @@ -268,25 +264,38 @@ export function createSearchSessionRestorationDataProvider({ return { getName: async () => 'Discover', getUrlGeneratorData: async () => { - const appState = appStateContainer.get(); - const state = { - 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, - }; return { urlGeneratorId: DISCOVER_APP_URL_GENERATOR, - initialState: state, - restoreState: state, // TODO: handle relative time range + 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, + }; +} From 150d90ae7a7994f6127335e9c71930ddc1baaa56 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 30 Nov 2020 16:03:45 +0100 Subject: [PATCH 15/18] do not delete saved searches --- .../public/search/session/session_service.ts | 4 +- .../search/session/session_state.test.ts | 2 - .../public/search/session/session_state.ts | 1 - .../public/search/search_interceptor.test.ts | 58 ++++++++++++++++++- .../public/search/search_interceptor.ts | 31 ++++++++-- 5 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 7147817090f35..51eaddb6abce9 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -51,7 +51,7 @@ export interface SearchSessionRestorationInfoProvider; private readonly state: SessionStateContainer; @@ -112,8 +112,6 @@ export class SessionService implements ISessionService { * @returns untrack function */ public trackSearch(searchDescriptor: TrackSearchDescriptor): () => void { - if (!this.getSessionId()) - throw new Error("Can't track search because there is no current session"); this.state.transitions.trackSearch(searchDescriptor); return () => { this.state.transitions.unTrackSearch(searchDescriptor); diff --git a/src/plugins/data/public/search/session/session_state.test.ts b/src/plugins/data/public/search/session/session_state.test.ts index 7f11f61e2b3c3..5f709c75bb5d2 100644 --- a/src/plugins/data/public/search/session/session_state.test.ts +++ b/src/plugins/data/public/search/session/session_state.test.ts @@ -43,8 +43,6 @@ describe('Session state container', () => { }); test('untrack', () => { - expect(() => state.transitions.unTrackSearch({})).toThrowError(); - state.transitions.start(); const search = {}; state.transitions.trackSearch(search); diff --git a/src/plugins/data/public/search/session/session_state.ts b/src/plugins/data/public/search/session/session_state.ts index c5606b3ac9c0b..087417263e5bf 100644 --- a/src/plugins/data/public/search/session/session_state.ts +++ b/src/plugins/data/public/search/session/session_state.ts @@ -158,7 +158,6 @@ export const sessionPureTransitions: SessionPureTransitions = { }; }, unTrackSearch: (state) => (search) => { - if (!state.sessionId) throw new Error("Can't untrack search. Missing sessionId"); return { ...state, pendingSearches: state.pendingSearches.filter((s) => s !== search), 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 a8d3657638fdb..20b55d9688edb 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 { ISessionService, 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); @@ -44,12 +45,17 @@ 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; + sessionService = { + ...(dataPluginMockStart.search.session as jest.Mocked), + state$: sessionState$, + }; fetchMock = jest.fn(); mockCoreSetup.uiSettings.get.mockImplementation((name: string) => { @@ -364,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', () => { 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 c745c7f81f7f9..0e87c093d2a8d 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'; @@ -63,23 +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 isCurrentSession = () => + !!options.sessionId && options.sessionId === this.deps.session.getSessionId(); - const untrackSearch = isCurrentSession && this.deps.session.trackSearch({ abort }); + 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) { + if (untrackSearch && isCurrentSession()) { untrackSearch(); } + if (savedToBackgroundSub) { + savedToBackgroundSub.unsubscribe(); + } }) ); } From b9bbbdbea98ffb7d2c04b5e8ee656962f32a57c8 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 30 Nov 2020 17:55:56 +0100 Subject: [PATCH 16/18] @lizozom review --- .../kibana-plugin-plugins-data-public.md | 2 +- ...hsessionrestorationinfoprovider.getname.md | 4 ++-- ...orationinfoprovider.geturlgeneratordata.md | 4 ++-- ...ic.searchsessionrestorationinfoprovider.md | 10 +++++----- .../application/dashboard_app_controller.tsx | 2 +- src/plugins/data/public/index.ts | 2 +- src/plugins/data/public/public.api.md | 4 ++-- src/plugins/data/public/search/index.ts | 2 +- .../data/public/search/search_interceptor.ts | 2 +- .../data/public/search/session/index.ts | 6 +----- .../data/public/search/session/mocks.ts | 2 +- .../public/search/session/session_service.ts | 20 ++++++++++--------- .../public/application/angular/discover.js | 2 +- .../application/angular/discover_state.ts | 4 ++-- 14 files changed, 32 insertions(+), 34 deletions(-) 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 e4b341e4e1ef2..9121b0aade470 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 @@ -89,7 +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) | | -| [SearchSessionRestorationInfoProvider](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object | +| [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) | \* | 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 index ac3009ca2c025..0f0b616066dd6 100644 --- 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 @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionRestorationInfoProvider](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md) +[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) -## SearchSessionRestorationInfoProvider.getName property +## SearchSessionInfoProvider.getName property User-facing name of the session. e.g. will be displayed in background sessions management list 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 index 3ece1347fc434..207adaf2bd50b 100644 --- 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 @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionRestorationInfoProvider](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md) +[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) -## SearchSessionRestorationInfoProvider.getUrlGeneratorData property +## SearchSessionInfoProvider.getUrlGeneratorData property Signature: 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 index 741a535bf6e06..a3d294f5e3303 100644 --- 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 @@ -1,21 +1,21 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionRestorationInfoProvider](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) -## SearchSessionRestorationInfoProvider interface +## SearchSessionInfoProvider interface Provide info about current search session to be stored in backgroundSearch saved object Signature: ```typescript -export interface SearchSessionRestorationInfoProvider +export interface SearchSessionInfoProvider ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [getName](./kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.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.searchsessionrestorationinfoprovider.geturlgeneratordata.md) | () => Promise<{
urlGeneratorId: ID;
initialState: UrlGeneratorStateMapping[ID]['State'];
restoreState: UrlGeneratorStateMapping[ID]['State'];
}> | | +| [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/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 721d76aa0714b..0d9e7e51b4a97 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -441,7 +441,7 @@ export class DashboardAppController { DashboardContainer >(DASHBOARD_CONTAINER_TYPE); - searchService.session.setSearchSessionRestorationInfoProvider( + searchService.session.setSearchSessionInfoProvider( createSessionRestorationDataProvider({ data, getDashboardTitle: () => getDashTitle(), diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index bbbdffcefbe61..1c07b4b99e4c0 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -399,7 +399,7 @@ export { export type { SearchSource, ISessionService, - SearchSessionRestorationInfoProvider, + SearchSessionInfoProvider, ISessionsClient, } from './search'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index dd2113ee8f439..e081ef0dd9f84 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2133,10 +2133,10 @@ export interface SearchInterceptorDeps { 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) "SearchSessionRestorationInfoProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// 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 SearchSessionRestorationInfoProvider { +export interface SearchSessionInfoProvider { getName: () => Promise; // (undocumented) getUrlGeneratorData: () => Promise<{ diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 9219079ac3d57..2a767d1bf7c0d 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -44,7 +44,7 @@ export { export { SessionService, ISessionService, - SearchSessionRestorationInfoProvider, + SearchSessionInfoProvider, SessionState, SessionsClient, ISessionsClient, diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 97836826386e7..dfc80d97dc196 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -170,7 +170,7 @@ export class SearchInterceptor { // 1. The user manually aborts (via `cancelPending`) // 2. The request times out // 3. abort() is called on `selfAbortController`. This is used by session service to abort all pending searches that it tracks. - // to cancel searches that belong to the currently tracked session + // to cancel searches that belong to the currently tracked session // 4. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines) const signals = [ this.abortController.signal, diff --git a/src/plugins/data/public/search/session/index.ts b/src/plugins/data/public/search/session/index.ts index fd7c7c95b566d..ee0121aacad5e 100644 --- a/src/plugins/data/public/search/session/index.ts +++ b/src/plugins/data/public/search/session/index.ts @@ -17,10 +17,6 @@ * under the License. */ -export { - SessionService, - ISessionService, - SearchSessionRestorationInfoProvider, -} from './session_service'; +export { SessionService, ISessionService, SearchSessionInfoProvider } from './session_service'; export { SessionState } from './session_state'; export { SessionsClient, ISessionsClient } from './sessions_client'; diff --git a/src/plugins/data/public/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts index b99c8e5de012e..0ff99b3080365 100644 --- a/src/plugins/data/public/search/session/mocks.ts +++ b/src/plugins/data/public/search/session/mocks.ts @@ -40,7 +40,7 @@ export function getSessionServiceMock(): jest.Mocked { getSessionId: jest.fn(), getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), state$: new BehaviorSubject(SessionState.None).asObservable(), - setSearchSessionRestorationInfoProvider: jest.fn(), + setSearchSessionInfoProvider: jest.fn(), trackSearch: jest.fn((searchDescriptor) => () => {}), destroy: jest.fn(), onRefresh$: new Subject(), diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 51eaddb6abce9..9217117f38baa 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -35,7 +35,7 @@ export interface TrackSearchDescriptor { /** * Provide info about current search session to be stored in backgroundSearch saved object */ -export interface SearchSessionRestorationInfoProvider { +export interface SearchSessionInfoProvider { /** * User-facing name of the session. * e.g. will be displayed in background sessions management list @@ -55,7 +55,7 @@ export class SessionService { public readonly state$: Observable; private readonly state: SessionStateContainer; - private searchSessionRestorationInfoProvider?: SearchSessionRestorationInfoProvider; + private searchSessionInfoProvider?: SearchSessionInfoProvider; private appChangeSubscription$?: Subscription; private curApp?: string; @@ -89,7 +89,6 @@ export class SessionService { } } this.curApp = appName; - this.setSearchSessionRestorationInfoProvider(undefined); }); }); } @@ -97,12 +96,12 @@ export class SessionService { /** * Set a provider of info about current session * This will be used for creating a background session saved object - * @param searchSessionRestorationInfoProvider + * @param searchSessionInfoProvider */ - public setSearchSessionRestorationInfoProvider( - searchSessionRestorationInfoProvider: SearchSessionRestorationInfoProvider | undefined + public setSearchSessionInfoProvider( + searchSessionInfoProvider: SearchSessionInfoProvider | undefined ) { - this.searchSessionRestorationInfoProvider = searchSessionRestorationInfoProvider; + this.searchSessionInfoProvider = searchSessionInfoProvider; } /** @@ -123,7 +122,7 @@ export class SessionService { this.appChangeSubscription$.unsubscribe(); } this.clear(); - this.setSearchSessionRestorationInfoProvider(undefined); + this.setSearchSessionInfoProvider(undefined); } /** @@ -180,11 +179,14 @@ export class SessionService { */ 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(); @@ -217,7 +219,7 @@ export class SessionService { 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.searchSessionRestorationInfoProvider; + 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(), diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 816f9393f3677..d0340c2cf4edd 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -288,7 +288,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise } }); - data.search.session.setSearchSessionRestorationInfoProvider( + data.search.session.setSearchSessionInfoProvider( createSearchSessionRestorationDataProvider({ appStateContainer, data, diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index cdb94028b7d3e..7de4ac27dd81f 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -33,7 +33,7 @@ import { esFilters, Filter, Query, - SearchSessionRestorationInfoProvider, + SearchSessionInfoProvider, } from '../../../../data/public'; import { migrateLegacyQuery } from '../helpers/migrate_legacy_query'; import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../url_generator'; @@ -260,7 +260,7 @@ export function createSearchSessionRestorationDataProvider(deps: { appStateContainer: StateContainer; data: DataPublicPluginStart; getSavedSearchId: () => string | undefined; -}): SearchSessionRestorationInfoProvider { +}): SearchSessionInfoProvider { return { getName: async () => 'Discover', getUrlGeneratorData: async () => { From 05400d17fdb1472ca0dd4e7edcf39b27c3f4de6f Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 1 Dec 2020 10:07:21 +0200 Subject: [PATCH 17/18] Update search_interceptor.ts --- src/plugins/data/public/search/search_interceptor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index dfc80d97dc196..8548a2a9f2b2a 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -169,8 +169,8 @@ export class SearchInterceptor { // 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. abort() is called on `selfAbortController`. This is used by session service to abort all pending searches that it tracks. - // to cancel searches that belong to the currently tracked session + // 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, From 70adabdfca47f7aea410eab08ad803afcaa3a584 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 1 Dec 2020 10:33:42 +0100 Subject: [PATCH 18/18] fix --- src/plugins/data/public/search/session/session_service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 9217117f38baa..ef0b36a33be52 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -122,7 +122,6 @@ export class SessionService { this.appChangeSubscription$.unsubscribe(); } this.clear(); - this.setSearchSessionInfoProvider(undefined); } /**