Skip to content

Commit

Permalink
[Dashboard] Read App State from URL on Soft Refresh (#109354) (#111052)
Browse files Browse the repository at this point in the history
Subscribe to app changes from URL to allow dashboard URL to be used as an API. On URL change, update filters, timerange, and query
  • Loading branch information
ThomThomson authored Sep 3, 2021
1 parent ee32afb commit 5078f4a
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
syncDashboardFilterState,
loadSavedDashboardState,
buildDashboardContainer,
loadDashboardUrlState,
syncDashboardUrlState,
diffDashboardState,
areTimeRangesEqual,
} from '../lib';
Expand Down Expand Up @@ -151,15 +151,20 @@ export const useDashboardAppState = ({
* Combine initial state from the saved object, session storage, and URL, then dispatch it to Redux.
*/
const dashboardSessionStorageState = dashboardSessionStorage.getState(savedDashboardId) || {};
const dashboardURLState = loadDashboardUrlState(dashboardBuildContext);

const forwardedAppState = loadDashboardHistoryLocationState(
scopedHistory()?.location?.state as undefined | DashboardAppLocatorParams
);

const { initialDashboardStateFromUrl, stopWatchingAppStateInUrl } = syncDashboardUrlState({
...dashboardBuildContext,
savedDashboard,
});

const initialDashboardState = {
...savedDashboardState,
...dashboardSessionStorageState,
...dashboardURLState,
...initialDashboardStateFromUrl,
...forwardedAppState,

// if there is an incoming embeddable, dashboard always needs to be in edit mode to receive it.
Expand Down Expand Up @@ -291,6 +296,7 @@ export const useDashboardAppState = ({

onDestroy = () => {
stopSyncingContainerInput();
stopWatchingAppStateInUrl();
stopSyncingDashboardFilterState();
lastSavedSubscription.unsubscribe();
indexPatternsSubscription.unsubscribe();
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/dashboard/public/application/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export { saveDashboard } from './save_dashboard';
export { migrateAppState } from './migrate_app_state';
export { addHelpMenuToAppChrome } from './help_menu_util';
export { getTagsFromSavedDashboard } from './dashboard_tagging';
export { loadDashboardUrlState } from './load_dashboard_url_state';
export { syncDashboardUrlState } from './sync_dashboard_url_state';
export { DashboardSessionStorage } from './dashboard_session_storage';
export { loadSavedDashboardState } from './load_saved_dashboard_state';
export { attemptLoadDashboardByTitle } from './load_dashboard_by_title';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,34 +48,13 @@ export const syncDashboardFilterState = ({
const { filterManager, queryString, timefilter } = queryService;
const { timefilter: timefilterService } = timefilter;

// apply initial filters to the query service and to the saved dashboard
filterManager.setAppFilters(_.cloneDeep(initialDashboardState.filters));
savedDashboard.searchSource.setField('filter', initialDashboardState.filters);

// apply initial query to the query service and to the saved dashboard
queryString.setQuery(initialDashboardState.query);
savedDashboard.searchSource.setField('query', initialDashboardState.query);

/**
* If a global time range is not set explicitly and the time range was saved with the dashboard, apply
* initial time range and refresh interval to the query service.
*/
if (initialDashboardState.timeRestore) {
const initialGlobalQueryState = kbnUrlStateStorage.get<QueryState>('_g');
if (!initialGlobalQueryState?.time) {
if (savedDashboard.timeFrom && savedDashboard.timeTo) {
timefilterService.setTime({
from: savedDashboard.timeFrom,
to: savedDashboard.timeTo,
});
}
}
if (!initialGlobalQueryState?.refreshInterval) {
if (savedDashboard.refreshInterval) {
timefilterService.setRefreshInterval(savedDashboard.refreshInterval);
}
}
}
// apply initial dashboard filter state.
applyDashboardFilterState({
currentDashboardState: initialDashboardState,
kbnUrlStateStorage,
savedDashboard,
queryService,
});

// this callback will be used any time new filters and query need to be applied.
const applyFilters = (query: Query, filters: Filter[]) => {
Expand Down Expand Up @@ -155,3 +134,49 @@ export const syncDashboardFilterState = ({

return { applyFilters, stopSyncingDashboardFilterState };
};

interface ApplyDashboardFilterStateProps {
kbnUrlStateStorage: DashboardBuildContext['kbnUrlStateStorage'];
queryService: DashboardBuildContext['query'];
currentDashboardState: DashboardState;
savedDashboard: DashboardSavedObject;
}

export const applyDashboardFilterState = ({
currentDashboardState,
kbnUrlStateStorage,
savedDashboard,
queryService,
}: ApplyDashboardFilterStateProps) => {
const { filterManager, queryString, timefilter } = queryService;
const { timefilter: timefilterService } = timefilter;

// apply filters to the query service and to the saved dashboard
filterManager.setAppFilters(_.cloneDeep(currentDashboardState.filters));
savedDashboard.searchSource.setField('filter', currentDashboardState.filters);

// apply query to the query service and to the saved dashboard
queryString.setQuery(currentDashboardState.query);
savedDashboard.searchSource.setField('query', currentDashboardState.query);

/**
* If a global time range is not set explicitly and the time range was saved with the dashboard, apply
* time range and refresh interval to the query service.
*/
if (currentDashboardState.timeRestore) {
const globalQueryState = kbnUrlStateStorage.get<QueryState>('_g');
if (!globalQueryState?.time) {
if (savedDashboard.timeFrom && savedDashboard.timeTo) {
timefilterService.setTime({
from: savedDashboard.timeFrom,
to: savedDashboard.timeTo,
});
}
}
if (!globalQueryState?.refreshInterval) {
if (savedDashboard.refreshInterval) {
timefilterService.setRefreshInterval(savedDashboard.refreshInterval);
}
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,72 @@
import _ from 'lodash';

import { migrateAppState } from '.';
import { DashboardSavedObject } from '../..';
import { setDashboardState } from '../state';
import { migrateLegacyQuery } from './migrate_legacy_query';
import { replaceUrlHashQuery } from '../../../../kibana_utils/public';
import { applyDashboardFilterState } from './sync_dashboard_filter_state';
import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants';
import type {
DashboardBuildContext,
DashboardPanelMap,
DashboardState,
RawDashboardState,
} from '../../types';
import { migrateLegacyQuery } from './migrate_legacy_query';
import { convertSavedPanelsToPanelMap } from './convert_saved_panels_to_panel_map';

type SyncDashboardUrlStateProps = DashboardBuildContext & { savedDashboard: DashboardSavedObject };

export const syncDashboardUrlState = ({
dispatchDashboardStateChange,
getLatestDashboardState,
query: queryService,
kbnUrlStateStorage,
usageCollection,
savedDashboard,
kibanaVersion,
}: SyncDashboardUrlStateProps) => {
// load initial state before subscribing to avoid state removal triggering update.
const loadDashboardStateProps = { kbnUrlStateStorage, usageCollection, kibanaVersion };
const initialDashboardStateFromUrl = loadDashboardUrlState(loadDashboardStateProps);

const appStateSubscription = kbnUrlStateStorage
.change$(DASHBOARD_STATE_STORAGE_KEY)
.subscribe(() => {
const stateFromUrl = loadDashboardUrlState(loadDashboardStateProps);

const updatedDashboardState = { ...getLatestDashboardState(), ...stateFromUrl };
applyDashboardFilterState({
currentDashboardState: updatedDashboardState,
kbnUrlStateStorage,
queryService,
savedDashboard,
});

if (Object.keys(stateFromUrl).length === 0) return;
dispatchDashboardStateChange(setDashboardState(updatedDashboardState));
});

const stopWatchingAppStateInUrl = () => {
appStateSubscription.unsubscribe();
};
return { initialDashboardStateFromUrl, stopWatchingAppStateInUrl };
};

interface LoadDashboardUrlStateProps {
kibanaVersion: DashboardBuildContext['kibanaVersion'];
usageCollection: DashboardBuildContext['usageCollection'];
kbnUrlStateStorage: DashboardBuildContext['kbnUrlStateStorage'];
}

/**
* Loads any dashboard state from the URL, and removes the state from the URL.
*/
export const loadDashboardUrlState = ({
const loadDashboardUrlState = ({
kibanaVersion,
usageCollection,
kbnUrlStateStorage,
}: DashboardBuildContext): Partial<DashboardState> => {
}: LoadDashboardUrlStateProps): Partial<DashboardState> => {
const rawAppStateInUrl = kbnUrlStateStorage.get<RawDashboardState>(DASHBOARD_STATE_STORAGE_KEY);
if (!rawAppStateInUrl) return {};

Expand Down
25 changes: 16 additions & 9 deletions test/functional/apps/dashboard/dashboard_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
};

const hardRefresh = async (newUrl: string) => {
// We need to add a timestamp to the URL because URL changes now only work with a hard refresh.
// We add a timestamp here to force a hard refresh
await browser.get(newUrl.toString());
const alert = await browser.getAlert();
await alert?.accept();
Expand All @@ -221,16 +221,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.timePicker.setHistoricalDataRange();
});

it('for query parameter', async function () {
const currentQuery = await queryBar.getQueryString();
expect(currentQuery).to.equal('');
const changeQuery = async (useHardRefresh: boolean, newQuery: string) => {
await queryBar.clickQuerySubmitButton();
const oldQuery = await queryBar.getQueryString();
const currentUrl = await getUrlFromShare();
const newUrl = currentUrl.replace(`query:''`, `query:'hi:hello'`);
const newUrl = currentUrl.replace(`query:'${oldQuery}'`, `query:'${newQuery}'`);

await browser.get(newUrl.toString(), !useHardRefresh);
const queryBarContentsAfterRefresh = await queryBar.getQueryString();
expect(queryBarContentsAfterRefresh).to.equal(newQuery);
};

it('for query parameter with soft refresh', async function () {
await changeQuery(false, 'hi:goodbye');
});

// We need to add a timestamp to the URL because URL changes now only work with a hard refresh.
await browser.get(newUrl.toString());
const newQuery = await queryBar.getQueryString();
expect(newQuery).to.equal('hi:hello');
it('for query parameter with hard refresh', async function () {
await changeQuery(true, 'hi:hello');
await queryBar.clearQuery();
await queryBar.clickQuerySubmitButton();
});
Expand Down

0 comments on commit 5078f4a

Please sign in to comment.