Skip to content

Commit a08a623

Browse files
committed
[Search][Dashboard] Restore searchSessionId from URL (#81489)
1 parent 7490e8a commit a08a623

11 files changed

Lines changed: 206 additions & 23 deletions

File tree

src/plugins/dashboard/public/application/dashboard_app_controller.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,13 @@ import { getDashboardTitle } from './dashboard_strings';
8282
import { DashboardAppScope } from './dashboard_app';
8383
import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters';
8484
import { RenderDeps } from './application';
85-
import { IKbnUrlStateStorage, setStateToKbnUrl, unhashUrl } from '../../../kibana_utils/public';
85+
import {
86+
IKbnUrlStateStorage,
87+
removeQueryParam,
88+
setStateToKbnUrl,
89+
unhashUrl,
90+
getQueryParams,
91+
} from '../../../kibana_utils/public';
8692
import {
8793
addFatalError,
8894
AngularHttpError,
@@ -121,6 +127,9 @@ interface UrlParamValues extends Omit<UrlParamsSelectedMap, UrlParams.SHOW_FILTE
121127
[UrlParams.HIDE_FILTER_BAR]: boolean;
122128
}
123129

130+
const getSearchSessionIdFromURL = (history: History): string | undefined =>
131+
getQueryParams(history.location)[DashboardConstants.SEARCH_SESSION_ID] as string | undefined;
132+
124133
export class DashboardAppController {
125134
// Part of the exposed plugin API - do not remove without careful consideration.
126135
appStatus: {
@@ -420,7 +429,11 @@ export class DashboardAppController {
420429
>(DASHBOARD_CONTAINER_TYPE);
421430

422431
if (dashboardFactory) {
423-
const searchSessionId = searchService.session.start();
432+
const searchSessionIdFromURL = getSearchSessionIdFromURL(history);
433+
if (searchSessionIdFromURL) {
434+
searchService.session.restore(searchSessionIdFromURL);
435+
}
436+
const searchSessionId = searchSessionIdFromURL ?? searchService.session.start();
424437
dashboardFactory
425438
.create({ ...getDashboardInput(), searchSessionId })
426439
.then((container: DashboardContainer | ErrorEmbeddable | undefined) => {
@@ -599,8 +612,15 @@ export class DashboardAppController {
599612
const refreshDashboardContainer = () => {
600613
const changes = getChangesFromAppStateForContainerState();
601614
if (changes && dashboardContainer) {
602-
const searchSessionId = searchService.session.start();
603-
dashboardContainer.updateInput({ ...changes, searchSessionId });
615+
if (getSearchSessionIdFromURL(history)) {
616+
// going away from a background search results
617+
removeQueryParam(history, DashboardConstants.SEARCH_SESSION_ID, true);
618+
}
619+
620+
dashboardContainer.updateInput({
621+
...changes,
622+
searchSessionId: searchService.session.start(),
623+
});
604624
}
605625
};
606626

src/plugins/dashboard/public/dashboard_constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const DashboardConstants = {
2424
ADD_EMBEDDABLE_TYPE: 'addEmbeddableType',
2525
DASHBOARDS_ID: 'dashboards',
2626
DASHBOARD_ID: 'dashboard',
27+
SEARCH_SESSION_ID: 'searchSessionId',
2728
};
2829

2930
export function createDashboardEditUrl(id: string) {

src/plugins/dashboard/public/url_generator.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,27 @@ describe('dashboard url generator', () => {
121121
);
122122
});
123123

124+
test('searchSessionId', async () => {
125+
const generator = createDashboardUrlGenerator(() =>
126+
Promise.resolve({
127+
appBasePath: APP_BASE_PATH,
128+
useHashedUrl: false,
129+
savedDashboardLoader: createMockDashboardLoader(),
130+
})
131+
);
132+
const url = await generator.createUrl!({
133+
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
134+
refreshInterval: { pause: false, value: 300 },
135+
dashboardId: '123',
136+
filters: [],
137+
query: { query: 'bye', language: 'kuery' },
138+
searchSessionId: '__sessionSearchId__',
139+
});
140+
expect(url).toMatchInlineSnapshot(
141+
`"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__"`
142+
);
143+
});
144+
124145
test('if no useHash setting is given, uses the one was start services', async () => {
125146
const generator = createDashboardUrlGenerator(() =>
126147
Promise.resolve({

src/plugins/dashboard/public/url_generator.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { setStateToKbnUrl } from '../../kibana_utils/public';
2929
import { UrlGeneratorsDefinition } from '../../share/public';
3030
import { SavedObjectLoader } from '../../saved_objects/public';
3131
import { ViewMode } from '../../embeddable/public';
32+
import { DashboardConstants } from './dashboard_constants';
3233

3334
export const STATE_STORAGE_KEY = '_a';
3435
export const GLOBAL_STATE_STORAGE_KEY = '_g';
@@ -79,6 +80,12 @@ export interface DashboardUrlGeneratorState {
7980
* View mode of the dashboard.
8081
*/
8182
viewMode?: ViewMode;
83+
84+
/**
85+
* Search search session ID to restore.
86+
* (Background search)
87+
*/
88+
searchSessionId?: string;
8289
}
8390

8491
export const createDashboardUrlGenerator = (
@@ -124,7 +131,7 @@ export const createDashboardUrlGenerator = (
124131
...state.filters,
125132
];
126133

127-
const appStateUrl = setStateToKbnUrl(
134+
let url = setStateToKbnUrl(
128135
STATE_STORAGE_KEY,
129136
cleanEmptyKeys({
130137
query: state.query,
@@ -135,15 +142,21 @@ export const createDashboardUrlGenerator = (
135142
`${appBasePath}#/${hash}`
136143
);
137144

138-
return setStateToKbnUrl<QueryState>(
145+
url = setStateToKbnUrl<QueryState>(
139146
GLOBAL_STATE_STORAGE_KEY,
140147
cleanEmptyKeys({
141148
time: state.timeRange,
142149
filters: filters?.filter((f) => esFilters.isFilterPinned(f)),
143150
refreshInterval: state.refreshInterval,
144151
}),
145152
{ useHash },
146-
appStateUrl
153+
url
147154
);
155+
156+
if (state.searchSessionId) {
157+
url = `${url}&${DashboardConstants.SEARCH_SESSION_ID}=${state.searchSessionId}`;
158+
}
159+
160+
return url;
148161
},
149162
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { getQueryParams } from './get_query_params';
21+
import { Location } from 'history';
22+
23+
describe('getQueryParams', () => {
24+
it('should getQueryParams', () => {
25+
const location: Location<any> = {
26+
pathname: '/dashboard/c3a76790-3134-11ea-b024-83a7b4783735',
27+
search: "?_a=(description:'')&_b=3",
28+
state: null,
29+
hash: '',
30+
};
31+
32+
const query = getQueryParams(location);
33+
34+
expect(query).toMatchInlineSnapshot(`
35+
Object {
36+
"_a": "(description:'')",
37+
"_b": "3",
38+
}
39+
`);
40+
});
41+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { parse, ParsedQuery } from 'query-string';
21+
import { Location } from 'history';
22+
23+
export function getQueryParams(location: Location): ParsedQuery {
24+
const search = (location.search || '').replace(/^\?/, '');
25+
const query = parse(search, { sort: false });
26+
return query;
27+
}

src/plugins/kibana_utils/public/history/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@
1919

2020
export { removeQueryParam } from './remove_query_param';
2121
export { redirectWhenMissing } from './redirect_when_missing';
22+
export { getQueryParams } from './get_query_params';

src/plugins/kibana_utils/public/history/remove_query_param.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
* under the License.
1818
*/
1919

20-
import { parse, stringify } from 'query-string';
20+
import { stringify } from 'query-string';
2121
import { History, Location } from 'history';
2222
import { url } from '../../common';
23+
import { getQueryParams } from './get_query_params';
2324

2425
export function removeQueryParam(history: History, param: string, replace: boolean = true) {
2526
const oldLocation = history.location;
26-
const search = (oldLocation.search || '').replace(/^\?/, '');
27-
const query = parse(search, { sort: false });
27+
const query = getQueryParams(oldLocation);
2828

2929
delete query[param];
3030

src/plugins/kibana_utils/public/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export {
7474
StopSyncStateFnType,
7575
} from './state_sync';
7676
export { Configurable, CollectConfigProps } from './ui';
77-
export { removeQueryParam, redirectWhenMissing } from './history';
77+
export { removeQueryParam, redirectWhenMissing, getQueryParams } from './history';
7878
export { applyDiff } from './state_management/utils/diff_object';
7979
export { createStartServicesGetter, StartServicesGetter } from './core/create_start_service_getter';
8080

test/plugin_functional/test_suites/data_plugin/session.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
2424
const filterBar = getService('filterBar');
2525
const testSubjects = getService('testSubjects');
2626
const toasts = getService('toasts');
27+
const esArchiver = getService('esArchiver');
2728

2829
const getSessionIds = async () => {
2930
const sessionsBtn = await testSubjects.find('showSessionsButton');
@@ -33,7 +34,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
3334
return sessionIds.split(',');
3435
};
3536

36-
describe('Session management', function describeIndexTests() {
37+
describe('Session management', function describeSessionManagementTests() {
3738
describe('Discover', () => {
3839
before(async () => {
3940
await PageObjects.common.navigateToApp('discover');
@@ -79,5 +80,45 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
7980
expect(sessionIds.length).to.be(1);
8081
});
8182
});
83+
84+
describe('Dashboard', () => {
85+
before(async () => {
86+
await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/dashboard/current/data');
87+
await esArchiver.loadIfNeeded(
88+
'../functional/fixtures/es_archiver/dashboard/current/kibana'
89+
);
90+
await PageObjects.common.navigateToApp('dashboard');
91+
await PageObjects.dashboard.loadSavedDashboard('dashboard with filter');
92+
await PageObjects.header.waitUntilLoadingHasFinished();
93+
});
94+
95+
afterEach(async () => {
96+
await testSubjects.click('clearSessionsButton');
97+
await toasts.dismissAllToasts();
98+
});
99+
100+
after(async () => {
101+
await esArchiver.unload('../functional/fixtures/es_archiver/dashboard/current/data');
102+
await esArchiver.unload('../functional/fixtures/es_archiver/dashboard/current/kibana');
103+
});
104+
105+
it('on load there is a single session', async () => {
106+
const sessionIds = await getSessionIds();
107+
expect(sessionIds.length).to.be(1);
108+
});
109+
110+
it('starts a session on refresh', async () => {
111+
await testSubjects.click('querySubmitButton');
112+
await PageObjects.header.waitUntilLoadingHasFinished();
113+
const sessionIds = await getSessionIds();
114+
expect(sessionIds.length).to.be(1);
115+
});
116+
117+
it('starts a session on filter change', async () => {
118+
await filterBar.removeAllFilters();
119+
const sessionIds = await getSessionIds();
120+
expect(sessionIds.length).to.be(1);
121+
});
122+
});
82123
});
83124
}

0 commit comments

Comments
 (0)