parent
2103911c6d
commit
fd9b44273c
|
@ -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(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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
src/plugins/kibana_utils/public/history/get_query_params.ts
Normal file
27
src/plugins/kibana_utils/public/history/get_query_params.ts
Normal 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;
|
||||
}
|
|
@ -19,3 +19,4 @@
|
|||
|
||||
export { removeQueryParam } from './remove_query_param';
|
||||
export { redirectWhenMissing } from './redirect_when_missing';
|
||||
export { getQueryParams } from './get_query_params';
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue