From fd9b44273c7f0c7942436c131e74a56efa42629b Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 4 Nov 2020 11:10:15 +0100 Subject: [PATCH] [Search][Dashboard] Restore searchSessionId from URL (#81489) (#82577) --- .../application/dashboard_app_controller.tsx | 28 ++++++++++-- .../dashboard/public/dashboard_constants.ts | 1 + .../dashboard/public/url_generator.test.ts | 21 +++++++++ src/plugins/dashboard/public/url_generator.ts | 19 ++++++-- .../public/history/get_query_params.test.ts | 41 ++++++++++++++++++ .../public/history/get_query_params.ts | 27 ++++++++++++ .../kibana_utils/public/history/index.ts | 1 + .../public/history/remove_query_param.ts | 6 +-- src/plugins/kibana_utils/public/index.ts | 2 +- .../test_suites/data_plugin/session.ts | 43 ++++++++++++++++++- .../dashboard/async_search/async_search.ts | 40 ++++++++++++----- 11 files changed, 206 insertions(+), 23 deletions(-) create mode 100644 src/plugins/kibana_utils/public/history/get_query_params.test.ts create mode 100644 src/plugins/kibana_utils/public/history/get_query_params.ts diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 35ee1d82d8ec..6023cd133d76 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -82,7 +82,13 @@ import { getDashboardTitle } from './dashboard_strings'; import { DashboardAppScope } from './dashboard_app'; import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters'; import { RenderDeps } from './application'; -import { IKbnUrlStateStorage, setStateToKbnUrl, unhashUrl } from '../../../kibana_utils/public'; +import { + IKbnUrlStateStorage, + removeQueryParam, + setStateToKbnUrl, + unhashUrl, + getQueryParams, +} from '../../../kibana_utils/public'; import { addFatalError, AngularHttpError, @@ -121,6 +127,9 @@ interface UrlParamValues extends Omit + getQueryParams(history.location)[DashboardConstants.SEARCH_SESSION_ID] as string | undefined; + export class DashboardAppController { // Part of the exposed plugin API - do not remove without careful consideration. appStatus: { @@ -420,7 +429,11 @@ export class DashboardAppController { >(DASHBOARD_CONTAINER_TYPE); if (dashboardFactory) { - const searchSessionId = searchService.session.start(); + const searchSessionIdFromURL = getSearchSessionIdFromURL(history); + if (searchSessionIdFromURL) { + searchService.session.restore(searchSessionIdFromURL); + } + const searchSessionId = searchSessionIdFromURL ?? searchService.session.start(); dashboardFactory .create({ ...getDashboardInput(), searchSessionId }) .then((container: DashboardContainer | ErrorEmbeddable | undefined) => { @@ -599,8 +612,15 @@ export class DashboardAppController { const refreshDashboardContainer = () => { const changes = getChangesFromAppStateForContainerState(); if (changes && dashboardContainer) { - const searchSessionId = searchService.session.start(); - dashboardContainer.updateInput({ ...changes, searchSessionId }); + if (getSearchSessionIdFromURL(history)) { + // going away from a background search results + removeQueryParam(history, DashboardConstants.SEARCH_SESSION_ID, true); + } + + dashboardContainer.updateInput({ + ...changes, + searchSessionId: searchService.session.start(), + }); } }; diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts index e68dd24b03c1..12f6b7061790 100644 --- a/src/plugins/dashboard/public/dashboard_constants.ts +++ b/src/plugins/dashboard/public/dashboard_constants.ts @@ -24,6 +24,7 @@ export const DashboardConstants = { ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', DASHBOARDS_ID: 'dashboards', DASHBOARD_ID: 'dashboard', + SEARCH_SESSION_ID: 'searchSessionId', }; export function createDashboardEditUrl(id: string) { diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index 208b229318a9..461caedc5cba 100644 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ b/src/plugins/dashboard/public/url_generator.test.ts @@ -121,6 +121,27 @@ describe('dashboard url generator', () => { ); }); + test('searchSessionId', async () => { + const generator = createDashboardUrlGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) + ); + const url = await generator.createUrl!({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + refreshInterval: { pause: false, value: 300 }, + dashboardId: '123', + filters: [], + query: { query: 'bye', language: 'kuery' }, + searchSessionId: '__sessionSearchId__', + }); + expect(url).toMatchInlineSnapshot( + `"xyz/app/dashboards#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__"` + ); + }); + 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 68a50396e00d..b23b26e4022d 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -29,6 +29,7 @@ import { setStateToKbnUrl } from '../../kibana_utils/public'; import { UrlGeneratorsDefinition } from '../../share/public'; import { SavedObjectLoader } from '../../saved_objects/public'; import { ViewMode } from '../../embeddable/public'; +import { DashboardConstants } from './dashboard_constants'; export const STATE_STORAGE_KEY = '_a'; export const GLOBAL_STATE_STORAGE_KEY = '_g'; @@ -79,6 +80,12 @@ export interface DashboardUrlGeneratorState { * View mode of the dashboard. */ viewMode?: ViewMode; + + /** + * Search search session ID to restore. + * (Background search) + */ + searchSessionId?: string; } export const createDashboardUrlGenerator = ( @@ -124,7 +131,7 @@ export const createDashboardUrlGenerator = ( ...state.filters, ]; - const appStateUrl = setStateToKbnUrl( + let url = setStateToKbnUrl( STATE_STORAGE_KEY, cleanEmptyKeys({ query: state.query, @@ -135,7 +142,7 @@ export const createDashboardUrlGenerator = ( `${appBasePath}#/${hash}` ); - return setStateToKbnUrl( + url = setStateToKbnUrl( GLOBAL_STATE_STORAGE_KEY, cleanEmptyKeys({ time: state.timeRange, @@ -143,7 +150,13 @@ export const createDashboardUrlGenerator = ( refreshInterval: state.refreshInterval, }), { useHash }, - appStateUrl + url ); + + if (state.searchSessionId) { + url = `${url}&${DashboardConstants.SEARCH_SESSION_ID}=${state.searchSessionId}`; + } + + return url; }, }); diff --git a/src/plugins/kibana_utils/public/history/get_query_params.test.ts b/src/plugins/kibana_utils/public/history/get_query_params.test.ts new file mode 100644 index 000000000000..dcdf796c04db --- /dev/null +++ b/src/plugins/kibana_utils/public/history/get_query_params.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { getQueryParams } from './get_query_params'; +import { Location } from 'history'; + +describe('getQueryParams', () => { + it('should getQueryParams', () => { + const location: Location = { + pathname: '/dashboard/c3a76790-3134-11ea-b024-83a7b4783735', + search: "?_a=(description:'')&_b=3", + state: null, + hash: '', + }; + + const query = getQueryParams(location); + + expect(query).toMatchInlineSnapshot(` + Object { + "_a": "(description:'')", + "_b": "3", + } + `); + }); +}); diff --git a/src/plugins/kibana_utils/public/history/get_query_params.ts b/src/plugins/kibana_utils/public/history/get_query_params.ts new file mode 100644 index 000000000000..e28aafd2d3be --- /dev/null +++ b/src/plugins/kibana_utils/public/history/get_query_params.ts @@ -0,0 +1,27 @@ +/* + * 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 { parse, ParsedQuery } from 'query-string'; +import { Location } from 'history'; + +export function getQueryParams(location: Location): ParsedQuery { + const search = (location.search || '').replace(/^\?/, ''); + const query = parse(search, { sort: false }); + return query; +} diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts index bb13ea09f928..88693a971250 100644 --- a/src/plugins/kibana_utils/public/history/index.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -19,3 +19,4 @@ export { removeQueryParam } from './remove_query_param'; export { redirectWhenMissing } from './redirect_when_missing'; +export { getQueryParams } from './get_query_params'; diff --git a/src/plugins/kibana_utils/public/history/remove_query_param.ts b/src/plugins/kibana_utils/public/history/remove_query_param.ts index bf945e5b064a..d3491057d24e 100644 --- a/src/plugins/kibana_utils/public/history/remove_query_param.ts +++ b/src/plugins/kibana_utils/public/history/remove_query_param.ts @@ -17,14 +17,14 @@ * under the License. */ -import { parse, stringify } from 'query-string'; +import { stringify } from 'query-string'; import { History, Location } from 'history'; import { url } from '../../common'; +import { getQueryParams } from './get_query_params'; export function removeQueryParam(history: History, param: string, replace: boolean = true) { const oldLocation = history.location; - const search = (oldLocation.search || '').replace(/^\?/, ''); - const query = parse(search, { sort: false }); + const query = getQueryParams(oldLocation); delete query[param]; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 7edf62ce04e8..9ba42d39139d 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -74,7 +74,7 @@ export { StopSyncStateFnType, } from './state_sync'; export { Configurable, CollectConfigProps } from './ui'; -export { removeQueryParam, redirectWhenMissing } from './history'; +export { removeQueryParam, redirectWhenMissing, getQueryParams } from './history'; export { applyDiff } from './state_management/utils/diff_object'; export { createStartServicesGetter, StartServicesGetter } from './core/create_start_service_getter'; diff --git a/test/plugin_functional/test_suites/data_plugin/session.ts b/test/plugin_functional/test_suites/data_plugin/session.ts index 88241fffae90..93980313838f 100644 --- a/test/plugin_functional/test_suites/data_plugin/session.ts +++ b/test/plugin_functional/test_suites/data_plugin/session.ts @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const filterBar = getService('filterBar'); const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); + const esArchiver = getService('esArchiver'); const getSessionIds = async () => { const sessionsBtn = await testSubjects.find('showSessionsButton'); @@ -33,7 +34,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide return sessionIds.split(','); }; - describe('Session management', function describeIndexTests() { + describe('Session management', function describeSessionManagementTests() { describe('Discover', () => { before(async () => { await PageObjects.common.navigateToApp('discover'); @@ -79,5 +80,45 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide expect(sessionIds.length).to.be(1); }); }); + + describe('Dashboard', () => { + before(async () => { + await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/dashboard/current/data'); + await esArchiver.loadIfNeeded( + '../functional/fixtures/es_archiver/dashboard/current/kibana' + ); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('dashboard with filter'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + afterEach(async () => { + await testSubjects.click('clearSessionsButton'); + await toasts.dismissAllToasts(); + }); + + after(async () => { + await esArchiver.unload('../functional/fixtures/es_archiver/dashboard/current/data'); + await esArchiver.unload('../functional/fixtures/es_archiver/dashboard/current/kibana'); + }); + + it('on load there is a single session', async () => { + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + + it('starts a session on refresh', async () => { + await testSubjects.click('querySubmitButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + + it('starts a session on filter change', async () => { + await filterBar.removeAllFilters(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + }); }); } 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 4d37ee158916..17497c832677 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 @@ -15,6 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const inspector = getService('inspector'); const queryBar = getService('queryBar'); + const browser = getService('browser'); describe('dashboard with async search', () => { before(async function () { @@ -61,17 +62,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // but only single error toast because searches are grouped expect((await testSubjects.findAll('searchTimeoutError')).length).to.be(1); - // check that session ids are the same - const getSearchSessionIdByPanel = async (panelTitle: string) => { - await dashboardPanelActions.openInspectorByTitle(panelTitle); - await inspector.openInspectorRequestsView(); - const searchSessionId = await ( - await testSubjects.find('inspectorRequestSearchSessionId') - ).getAttribute('data-search-session-id'); - await inspector.close(); - return searchSessionId; - }; - const panel1SessionId1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); const panel2SessionId1 = await getSearchSessionIdByPanel( 'Sum of Bytes by Extension (Delayed 5s)' @@ -87,5 +77,33 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(panel1SessionId2).to.be(panel2SessionId2); 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); + }); }); + + // HELPERS + async function getSearchSessionIdByPanel(panelTitle: string) { + await dashboardPanelActions.openInspectorByTitle(panelTitle); + await inspector.openInspectorRequestsView(); + const searchSessionId = await ( + await testSubjects.find('inspectorRequestSearchSessionId') + ).getAttribute('data-search-session-id'); + await inspector.close(); + return searchSessionId; + } }