Skip to content

Commit

Permalink
dashboard state fixes for drilldowns
Browse files Browse the repository at this point in the history
  • Loading branch information
Dosant committed Mar 27, 2020
1 parent 5b8de94 commit 71fe97c
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
IndexPattern,
IndexPatternsContract,
Query,
QueryState,
SavedQuery,
syncQueryStateWithUrl,
} from '../../../../../../plugins/data/public';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = () =>
Expand Down Expand Up @@ -660,7 +669,8 @@ export class DashboardAppController {
// 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);
dashboardStateManager.syncTimefilterWithDashboardTime(timefilter);
dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter);
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,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');
Expand All @@ -74,7 +74,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');
Expand All @@ -89,7 +89,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
ReduxLikeStateContainer,
syncState,
} from '../../../../../../plugins/kibana_utils/public';
import { getDashboardIdFromUrl } from './url_helper';

/**
* Dashboard state manager handles connecting angular and redux state between the angular and react portions of the
Expand Down Expand Up @@ -175,6 +176,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 destroy 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,
Expand Down Expand Up @@ -203,6 +212,7 @@ export class DashboardStateManager {

public handleDashboardContainerChanges(dashboardContainer: DashboardContainer) {
let dirty = false;
let dirtyBecauseOfInitialStateMigration = false;

const savedDashboardPanelMap: { [key: string]: SavedDashboardPanel } = {};

Expand Down Expand Up @@ -236,11 +246,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) {
dirtyBecauseOfInitialStateMigration = true;
}
}
});

if (dirty) {
this.stateContainer.transitions.set('panels', Object.values(convertedPanelStateMap));
if (dirtyBecauseOfInitialStateMigration) {
this.saveState({ replace: true });
}
}

if (input.isFullScreenMode !== this.getFullScreenMode()) {
Expand Down Expand Up @@ -498,7 +517,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', {
Expand All @@ -513,6 +532,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jest.mock('../legacy_imports', () => {

import {
addEmbeddableToDashboardUrl,
getDashboardIdFromUrl,
getLensUrlFromDashboardAbsoluteUrl,
getUrlVars,
} from './url_helper';
Expand Down Expand Up @@ -115,4 +116,30 @@ describe('Dashboard URL Helper', () => {
'http://localhost:5601/app/kibana#/lens/edit/1244'
);
});

it('getDashboardIdFromDashboardUrl', () => {
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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,24 @@ export function getLensUrlFromDashboardAbsoluteUrl(
function getUrlWithoutQueryParams(url: string): string {
return url.split('?')[0];
}

/**
* 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&param2=y&param3=z
* output: undefined
* input: http://localhost:5601/lib/app/kibana#/dashboard/39292992?param1=x&param2=y&param3=z
* output: 39292992
*/
export function getDashboardIdFromUrl(url: string): string | undefined {
const [, match1, match2, match3] = url.match(
/dashboard\/(.*)\/|dashboard\/(.*)\?|dashboard\/(.*)$/
) ?? [
undefined, // full match
undefined, // group1 - dashboardId is before `/`
undefined, // group2 - dashboardId is before `?`
undefined, // group3 - dashboardID is in the end
];
return match1 ?? match2 ?? match3 ?? undefined;
}
21 changes: 21 additions & 0 deletions test/functional/apps/dashboard/bwc_shared_urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
}
14 changes: 14 additions & 0 deletions test/functional/apps/dashboard/dashboard_time.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 15 additions & 12 deletions test/functional/apps/management/_kibana_settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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);
Expand Down

0 comments on commit 71fe97c

Please sign in to comment.