[Search][Dashboard] Restore searchSessionId from URL (#81489) (#82577)

This commit is contained in:
Anton Dosov 2020-11-04 11:10:15 +01:00 committed by GitHub
parent 2103911c6d
commit fd9b44273c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 206 additions and 23 deletions

View file

@ -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<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: {
@ -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(),
});
}
};

View file

@ -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) {

View file

@ -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({

View file

@ -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<QueryState>(
url = setStateToKbnUrl<QueryState>(
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;
},
});

View file

@ -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",
}
`);
});
});

View file

@ -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;
}

View file

@ -19,3 +19,4 @@
export { removeQueryParam } from './remove_query_param';
export { redirectWhenMissing } from './redirect_when_missing';
export { getQueryParams } from './get_query_params';

View file

@ -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];

View file

@ -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';

View file

@ -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);
});
});
});
}

View file

@ -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;
}
}