[Dashboard] Read App State from URL on Soft Refresh (#109354)
Subscribe to app changes from URL to allow dashboard URL to be used as an API. On URL change, update filters, timerange, and query
This commit is contained in:
parent
c692ad3724
commit
e704a52f6e
|
@ -35,7 +35,7 @@ import {
|
|||
syncDashboardFilterState,
|
||||
loadSavedDashboardState,
|
||||
buildDashboardContainer,
|
||||
loadDashboardUrlState,
|
||||
syncDashboardUrlState,
|
||||
diffDashboardState,
|
||||
areTimeRangesEqual,
|
||||
} from '../lib';
|
||||
|
@ -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.
|
||||
|
@ -291,6 +296,7 @@ export const useDashboardAppState = ({
|
|||
|
||||
onDestroy = () => {
|
||||
stopSyncingContainerInput();
|
||||
stopWatchingAppStateInUrl();
|
||||
stopSyncingDashboardFilterState();
|
||||
lastSavedSubscription.unsubscribe();
|
||||
indexPatternsSubscription.unsubscribe();
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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[]) => {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
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,
|
||||
|
@ -17,17 +21,60 @@ import type {
|
|||
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 {};
|
||||
|
|
@ -171,7 +171,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();
|
||||
|
@ -186,16 +186,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}'`);
|
||||
|
||||
// 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');
|
||||
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');
|
||||
});
|
||||
|
||||
it('for query parameter with hard refresh', async function () {
|
||||
await changeQuery(true, 'hi:hello');
|
||||
await queryBar.clearQuery();
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue