Skip to content

Commit

Permalink
[Search][Dashboard] Restore searchSessionId from URL (#81489) (#82577)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dosant authored Nov 4, 2020
1 parent 2103911 commit fd9b442
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -121,6 +127,9 @@ interface UrlParamValues extends Omit<UrlParamsSelectedMap, UrlParams.SHOW_FILTE
[UrlParams.HIDE_FILTER_BAR]: boolean;
}

const getSearchSessionIdFromURL = (history: History): string | undefined =>
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: {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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(),
});
}
};

Expand Down
1 change: 1 addition & 0 deletions src/plugins/dashboard/public/dashboard_constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
21 changes: 21 additions & 0 deletions src/plugins/dashboard/public/url_generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
19 changes: 16 additions & 3 deletions src/plugins/dashboard/public/url_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -124,7 +131,7 @@ export const createDashboardUrlGenerator = (
...state.filters,
];

const appStateUrl = setStateToKbnUrl(
let url = setStateToKbnUrl(
STATE_STORAGE_KEY,
cleanEmptyKeys({
query: state.query,
Expand All @@ -135,15 +142,21 @@ export const createDashboardUrlGenerator = (
`${appBasePath}#/${hash}`
);

return setStateToKbnUrl<QueryState>(
url = setStateToKbnUrl<QueryState>(
GLOBAL_STATE_STORAGE_KEY,
cleanEmptyKeys({
time: state.timeRange,
filters: filters?.filter((f) => esFilters.isFilterPinned(f)),
refreshInterval: state.refreshInterval,
}),
{ useHash },
appStateUrl
url
);

if (state.searchSessionId) {
url = `${url}&${DashboardConstants.SEARCH_SESSION_ID}=${state.searchSessionId}`;
}

return url;
},
});
41 changes: 41 additions & 0 deletions src/plugins/kibana_utils/public/history/get_query_params.test.ts
Original file line number Diff line number Diff line change
@@ -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<any> = {
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",
}
`);
});
});
27 changes: 27 additions & 0 deletions src/plugins/kibana_utils/public/history/get_query_params.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions src/plugins/kibana_utils/public/history/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@

export { removeQueryParam } from './remove_query_param';
export { redirectWhenMissing } from './redirect_when_missing';
export { getQueryParams } from './get_query_params';
6 changes: 3 additions & 3 deletions src/plugins/kibana_utils/public/history/remove_query_param.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down
2 changes: 1 addition & 1 deletion src/plugins/kibana_utils/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
43 changes: 42 additions & 1 deletion test/plugin_functional/test_suites/data_plugin/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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);
});
});
});
}
40 changes: 29 additions & 11 deletions x-pack/test/functional/apps/dashboard/async_search/async_search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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)'
Expand All @@ -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;
}
}

0 comments on commit fd9b442

Please sign in to comment.