mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
1. Change logic around deciding wether to use time from url or from saved object. Previously code looked only into if _g is present in the url at all. And didn't consider edge case if time or refreshInterval is missing in _g 2. Fix initial syncing of time from savedobject causing redundant history record. _This changed caused order of _a and g params in url change. One test was affected by it because it relied on the order. I don't think it should be considered breaking as order app puts it's query params shouldn't matter. 3. Fix another race condition between state syncing with url and angular controller $destroy. Similar fix was done before in #57795, but this on covers case when we stay within dashboard app, but change dashboard 4. Fix initial panel state migration causing redundant browser history records Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
7f95fd8535
commit
233e309343
9 changed files with 195 additions and 39 deletions
|
@ -36,6 +36,7 @@ import {
|
|||
IndexPattern,
|
||||
IndexPatternsContract,
|
||||
Query,
|
||||
QueryState,
|
||||
SavedQuery,
|
||||
syncQueryStateWithUrl,
|
||||
} from '../../../../../../plugins/data/public';
|
||||
|
@ -132,13 +133,6 @@ export class DashboardAppController {
|
|||
const queryFilter = filterManager;
|
||||
const timefilter = queryService.timefilter.timefilter;
|
||||
|
||||
// starts syncing `_g` portion of url with query services
|
||||
// note: dashboard_state_manager.ts syncs `_a` portion of url
|
||||
const {
|
||||
stop: stopSyncingQueryServiceStateWithUrl,
|
||||
hasInheritedQueryFromUrl: hasInheritedGlobalStateFromUrl,
|
||||
} = syncQueryStateWithUrl(queryService, kbnUrlStateStorage);
|
||||
|
||||
let lastReloadRequestTime = 0;
|
||||
const dash = ($scope.dash = $route.current.locals.dash);
|
||||
if (dash.id) {
|
||||
|
@ -170,9 +164,24 @@ export class DashboardAppController {
|
|||
|
||||
// The hash check is so we only update the time filter on dashboard open, not during
|
||||
// normal cross app navigation.
|
||||
if (dashboardStateManager.getIsTimeSavedWithDashboard() && !hasInheritedGlobalStateFromUrl) {
|
||||
dashboardStateManager.syncTimefilterWithDashboard(timefilter);
|
||||
if (dashboardStateManager.getIsTimeSavedWithDashboard()) {
|
||||
const initialGlobalStateInUrl = kbnUrlStateStorage.get<QueryState>('_g');
|
||||
if (!initialGlobalStateInUrl?.time) {
|
||||
dashboardStateManager.syncTimefilterWithDashboardTime(timefilter);
|
||||
}
|
||||
if (!initialGlobalStateInUrl?.refreshInterval) {
|
||||
dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter);
|
||||
}
|
||||
}
|
||||
|
||||
// starts syncing `_g` portion of url with query services
|
||||
// note: dashboard_state_manager.ts syncs `_a` portion of url
|
||||
// it is important to start this syncing after `dashboardStateManager.syncTimefilterWithDashboard(timefilter);` above is run,
|
||||
// otherwise it will case redundant browser history record
|
||||
const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl(
|
||||
queryService,
|
||||
kbnUrlStateStorage
|
||||
);
|
||||
$scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean;
|
||||
|
||||
const getShouldShowEditHelp = () =>
|
||||
|
@ -643,6 +652,14 @@ export class DashboardAppController {
|
|||
// This is only necessary for new dashboards, which will default to Edit mode.
|
||||
updateViewMode(ViewMode.VIEW);
|
||||
|
||||
// We need to do a hard reset of the timepicker. appState will not reload like
|
||||
// it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on
|
||||
// reload will cause it not to sync.
|
||||
if (dashboardStateManager.getIsTimeSavedWithDashboard()) {
|
||||
dashboardStateManager.syncTimefilterWithDashboardTime(timefilter);
|
||||
dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter);
|
||||
}
|
||||
|
||||
// Angular's $location skips this update because of history updates from syncState which happen simultaneously
|
||||
// when calling kbnUrl.change() angular schedules url update and when angular finally starts to process it,
|
||||
// the update is considered outdated and angular skips it
|
||||
|
@ -650,19 +667,6 @@ export class DashboardAppController {
|
|||
dashboardStateManager.changeDashboardUrl(
|
||||
dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL
|
||||
);
|
||||
|
||||
// We need to do a hard reset of the timepicker. appState will not reload like
|
||||
// it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on
|
||||
// reload will cause it not to sync.
|
||||
if (dashboardStateManager.getIsTimeSavedWithDashboard()) {
|
||||
// have to use $evalAsync here until '_g' is migrated from $location to state sync utility ('history')
|
||||
// When state sync utility changes url, angular's $location is missing it's own updates which happen during the same digest cycle
|
||||
// temporary solution is to delay $location updates to next digest cycle
|
||||
// unfortunately, these causes 2 browser history entries, but this is temporary and will be fixed after migrating '_g' to state_sync utilities
|
||||
$scope.$evalAsync(() => {
|
||||
dashboardStateManager.syncTimefilterWithDashboard(timefilter);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
overlays
|
||||
|
|
|
@ -63,7 +63,7 @@ describe('DashboardState', function() {
|
|||
mockTime.to = '2015-09-29 06:31:44.000';
|
||||
|
||||
initDashboardState();
|
||||
dashboardState.syncTimefilterWithDashboard(mockTimefilter);
|
||||
dashboardState.syncTimefilterWithDashboardTime(mockTimefilter);
|
||||
|
||||
expect(mockTime.to).toBe('now/w');
|
||||
expect(mockTime.from).toBe('now/w');
|
||||
|
@ -78,7 +78,7 @@ describe('DashboardState', function() {
|
|||
mockTime.to = '2015-09-29 06:31:44.000';
|
||||
|
||||
initDashboardState();
|
||||
dashboardState.syncTimefilterWithDashboard(mockTimefilter);
|
||||
dashboardState.syncTimefilterWithDashboardTime(mockTimefilter);
|
||||
|
||||
expect(mockTime.to).toBe('now');
|
||||
expect(mockTime.from).toBe('now-13d');
|
||||
|
@ -93,7 +93,7 @@ describe('DashboardState', function() {
|
|||
mockTime.to = 'now/w';
|
||||
|
||||
initDashboardState();
|
||||
dashboardState.syncTimefilterWithDashboard(mockTimefilter);
|
||||
dashboardState.syncTimefilterWithDashboardTime(mockTimefilter);
|
||||
|
||||
expect(mockTime.to).toBe(savedDashboard.timeTo);
|
||||
expect(mockTime.from).toBe(savedDashboard.timeFrom);
|
||||
|
|
|
@ -35,7 +35,7 @@ import {
|
|||
TimefilterContract as Timefilter,
|
||||
} from '../../../../../../plugins/data/public';
|
||||
|
||||
import { getAppStateDefaults, migrateAppState } from './lib';
|
||||
import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib';
|
||||
import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters';
|
||||
import { FilterUtils } from './lib/filter_utils';
|
||||
import {
|
||||
|
@ -175,6 +175,14 @@ export class DashboardStateManager {
|
|||
// sync state required state container to be able to handle null
|
||||
// overriding set() so it could handle null coming from url
|
||||
if (state) {
|
||||
// Skip this update if current dashboardId in the url is different from what we have in the current instance of state manager
|
||||
// As dashboard is driven by angular at the moment, the destroy cycle happens async,
|
||||
// If the dashboardId has changed it means this instance
|
||||
// is going to be destroyed soon and we shouldn't sync state anymore,
|
||||
// as it could potentially trigger further url updates
|
||||
const currentDashboardIdInUrl = getDashboardIdFromUrl(history.location.pathname);
|
||||
if (currentDashboardIdInUrl !== this.savedDashboard.id) return;
|
||||
|
||||
this.stateContainer.set({
|
||||
...this.stateDefaults,
|
||||
...state,
|
||||
|
@ -203,6 +211,7 @@ export class DashboardStateManager {
|
|||
|
||||
public handleDashboardContainerChanges(dashboardContainer: DashboardContainer) {
|
||||
let dirty = false;
|
||||
let dirtyBecauseOfInitialStateMigration = false;
|
||||
|
||||
const savedDashboardPanelMap: { [key: string]: SavedDashboardPanel } = {};
|
||||
|
||||
|
@ -236,11 +245,20 @@ export class DashboardStateManager {
|
|||
) {
|
||||
// A panel was changed
|
||||
dirty = true;
|
||||
|
||||
const oldVersion = savedDashboardPanelMap[panelState.explicitInput.id]?.version;
|
||||
const newVersion = convertedPanelStateMap[panelState.explicitInput.id]?.version;
|
||||
if (oldVersion && newVersion && oldVersion !== newVersion) {
|
||||
dirtyBecauseOfInitialStateMigration = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (dirty) {
|
||||
this.stateContainer.transitions.set('panels', Object.values(convertedPanelStateMap));
|
||||
if (dirtyBecauseOfInitialStateMigration) {
|
||||
this.saveState({ replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (input.isFullScreenMode !== this.getFullScreenMode()) {
|
||||
|
@ -498,7 +516,7 @@ export class DashboardStateManager {
|
|||
* @param timeFilter.setTime
|
||||
* @param timeFilter.setRefreshInterval
|
||||
*/
|
||||
public syncTimefilterWithDashboard(timeFilter: Timefilter) {
|
||||
public syncTimefilterWithDashboardTime(timeFilter: Timefilter) {
|
||||
if (!this.getIsTimeSavedWithDashboard()) {
|
||||
throw new Error(
|
||||
i18n.translate('kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage', {
|
||||
|
@ -513,6 +531,20 @@ export class DashboardStateManager {
|
|||
to: this.savedDashboard.timeTo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates timeFilter to match the refreshInterval saved with the dashboard.
|
||||
* @param timeFilter
|
||||
*/
|
||||
public syncTimefilterWithDashboardRefreshInterval(timeFilter: Timefilter) {
|
||||
if (!this.getIsTimeSavedWithDashboard()) {
|
||||
throw new Error(
|
||||
i18n.translate('kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage', {
|
||||
defaultMessage: 'The time is not saved with this dashboard so should not be synced.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this.savedDashboard.refreshInterval) {
|
||||
timeFilter.setRefreshInterval(this.savedDashboard.refreshInterval);
|
||||
|
|
|
@ -20,3 +20,4 @@
|
|||
export { saveDashboard } from './save_dashboard';
|
||||
export { getAppStateDefaults } from './get_app_state_defaults';
|
||||
export { migrateAppState } from './migrate_app_state';
|
||||
export { getDashboardIdFromUrl } from './url';
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { getDashboardIdFromUrl } from './url';
|
||||
|
||||
test('getDashboardIdFromUrl', () => {
|
||||
let url =
|
||||
"http://localhost:5601/wev/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()";
|
||||
expect(getDashboardIdFromUrl(url)).toEqual(undefined);
|
||||
|
||||
url =
|
||||
"http://localhost:5601/wev/app/kibana#/dashboard/625357282?_a=(description:'',filters:!()&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))";
|
||||
expect(getDashboardIdFromUrl(url)).toEqual('625357282');
|
||||
|
||||
url = 'http://myserver.mydomain.com:5601/wev/app/kibana#/dashboard/777182';
|
||||
expect(getDashboardIdFromUrl(url)).toEqual('777182');
|
||||
|
||||
url =
|
||||
"http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()";
|
||||
expect(getDashboardIdFromUrl(url)).toEqual(undefined);
|
||||
|
||||
url = '/dashboard/test/?_g=(refreshInterval:';
|
||||
expect(getDashboardIdFromUrl(url)).toEqual('test');
|
||||
|
||||
url = 'dashboard/test/?_g=(refreshInterval:';
|
||||
expect(getDashboardIdFromUrl(url)).toEqual('test');
|
||||
|
||||
url = '/other-app/test/';
|
||||
expect(getDashboardIdFromUrl(url)).toEqual(undefined);
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns dashboard id from URL
|
||||
* literally looks from id after `dashboard/` string and before `/`, `?` and end of string
|
||||
* @param url to extract dashboardId from
|
||||
* input: http://localhost:5601/lib/app/kibana#/dashboard?param1=x¶m2=y¶m3=z
|
||||
* output: undefined
|
||||
* input: http://localhost:5601/lib/app/kibana#/dashboard/39292992?param1=x¶m2=y¶m3=z
|
||||
* output: 39292992
|
||||
*/
|
||||
export function getDashboardIdFromUrl(url: string): string | undefined {
|
||||
const [, dashboardId] = url.match(/dashboard\/(.*?)(\/|\?|$)/) ?? [
|
||||
undefined, // full match
|
||||
undefined, // group with dashboardId
|
||||
];
|
||||
return dashboardId ?? undefined;
|
||||
}
|
|
@ -135,6 +135,27 @@ export default function({ getService, getPageObjects }) {
|
|||
|
||||
await dashboardExpect.selectedLegendColorCount('#000000', 5);
|
||||
});
|
||||
|
||||
it('back button works for old dashboards after state migrations', async () => {
|
||||
await PageObjects.dashboard.preserveCrossAppState();
|
||||
const oldId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardExpect.selectedLegendColorCount('#000000', 5);
|
||||
|
||||
const url = `${kibanaBaseUrl}#/dashboard?${urlQuery}`;
|
||||
log.debug(`Navigating to ${url}`);
|
||||
await browser.get(url);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardExpect.selectedLegendColorCount('#F9D9F9', 5);
|
||||
await browser.goBack();
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const newId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl();
|
||||
expect(newId).to.be.equal(oldId);
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardExpect.selectedLegendColorCount('#000000', 5);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -91,6 +91,20 @@ export default function({ getPageObjects, getService }) {
|
|||
expect(time.start).to.equal('~ an hour ago');
|
||||
expect(time.end).to.equal('now');
|
||||
});
|
||||
|
||||
it('should use saved time, if time is missing in global state, but _g is present in the url', async function() {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
const kibanaBaseUrl = currentUrl.substring(0, currentUrl.indexOf('#'));
|
||||
const id = await PageObjects.dashboard.getDashboardIdFromCurrentUrl();
|
||||
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
|
||||
const urlWithGlobalTime = `${kibanaBaseUrl}#/dashboard/${id}?_g=(filters:!())`;
|
||||
await browser.get(urlWithGlobalTime, false);
|
||||
const time = await PageObjects.timePicker.getTimeConfig();
|
||||
expect(time.start).to.equal(PageObjects.timePicker.defaultStartTime);
|
||||
expect(time.end).to.equal(PageObjects.timePicker.defaultEndTime);
|
||||
});
|
||||
});
|
||||
|
||||
// If the user has time stored with a dashboard, it's supposed to override the current time settings
|
||||
|
|
|
@ -46,6 +46,18 @@ export default function({ getService, getPageObjects }) {
|
|||
});
|
||||
|
||||
describe('state:storeInSessionStorage', () => {
|
||||
async function getStateFromUrl() {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
let match = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/);
|
||||
if (match) return [match[2], match[3]];
|
||||
match = currentUrl.match(/(.*)?_a=(.*)&_g=(.*)/);
|
||||
if (match) return [match[3], match[2]];
|
||||
|
||||
if (!match) {
|
||||
throw new Error('State in url is missing or malformed');
|
||||
}
|
||||
}
|
||||
|
||||
it('defaults to null', async () => {
|
||||
await PageObjects.settings.clickKibanaSettings();
|
||||
const storeInSessionStorage = await PageObjects.settings.getAdvancedSettingCheckbox(
|
||||
|
@ -58,10 +70,7 @@ export default function({ getService, getPageObjects }) {
|
|||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/);
|
||||
const globalState = urlPieces[2];
|
||||
const appState = urlPieces[3];
|
||||
const [globalState, appState] = await getStateFromUrl();
|
||||
|
||||
// We don't have to be exact, just need to ensure it's greater than when the hashed variation is being used,
|
||||
// which is less than 20 characters.
|
||||
|
@ -83,10 +92,7 @@ export default function({ getService, getPageObjects }) {
|
|||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/);
|
||||
const globalState = urlPieces[2];
|
||||
const appState = urlPieces[3];
|
||||
const [globalState, appState] = await getStateFromUrl();
|
||||
|
||||
// We don't have to be exact, just need to ensure it's less than the unhashed version, which will be
|
||||
// greater than 20 characters with the default state plus a time.
|
||||
|
@ -100,10 +106,7 @@ export default function({ getService, getPageObjects }) {
|
|||
await PageObjects.settings.clickKibanaSettings();
|
||||
await PageObjects.settings.toggleAdvancedSettingCheckbox('state:storeInSessionStorage');
|
||||
await PageObjects.header.clickDashboard();
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/);
|
||||
const globalState = urlPieces[2];
|
||||
const appState = urlPieces[3];
|
||||
const [globalState, appState] = await getStateFromUrl();
|
||||
// We don't have to be exact, just need to ensure it's greater than when the hashed variation is being used,
|
||||
// which is less than 20 characters.
|
||||
expect(globalState.length).to.be.greaterThan(20);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue