mirror of
https://github.com/grafana/grafana.git
synced 2024-11-24 09:50:29 -06:00
Preserve variables and time range when navigating between dashboards (#87966)
* POC preserve filters and group by when navigating between dashboards * Save all variables and time range * minor refactor * Add feature toggle * Update feature toggle usage * Delete local storage item if nothing to preserve * Structural changes * Simplify restore params code * Use session storage * Add tests * Merge fix * Remove unused code * And make it better, errrrrr * Minor deduplication refactor * last minor
This commit is contained in:
parent
0b1aec6767
commit
6e9543e0ad
@ -193,4 +193,5 @@ export interface FeatureToggles {
|
||||
dashboardRestore?: boolean;
|
||||
datasourceProxyDisableRBAC?: boolean;
|
||||
alertingDisableSendAlertsExternal?: boolean;
|
||||
preserveDashboardStateWhenNavigating?: boolean;
|
||||
}
|
||||
|
@ -1306,6 +1306,14 @@ var (
|
||||
HideFromDocs: true,
|
||||
HideFromAdminPage: true,
|
||||
},
|
||||
{
|
||||
Name: "preserveDashboardStateWhenNavigating",
|
||||
Description: "Enables possibility to preserve dashboard variables and time range when navigating between dashboards",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaDashboardsSquad,
|
||||
HideFromDocs: true,
|
||||
HideFromAdminPage: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -174,3 +174,4 @@ notificationBanner,experimental,@grafana/grafana-frontend-platform,false,false,f
|
||||
dashboardRestore,experimental,@grafana/grafana-frontend-platform,false,false,false
|
||||
datasourceProxyDisableRBAC,GA,@grafana/identity-access-team,false,false,false
|
||||
alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false
|
||||
preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false
|
||||
|
|
@ -706,4 +706,8 @@ const (
|
||||
// FlagAlertingDisableSendAlertsExternal
|
||||
// Disables the ability to send alerts to an external Alertmanager datasource.
|
||||
FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal"
|
||||
|
||||
// FlagPreserveDashboardStateWhenNavigating
|
||||
// Enables possibility to preserve dashboard variables and time range when navigating between dashboards
|
||||
FlagPreserveDashboardStateWhenNavigating = "preserveDashboardStateWhenNavigating"
|
||||
)
|
||||
|
@ -2250,6 +2250,20 @@
|
||||
"codeowner": "@grafana/alerting-squad",
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "preserveDashboardStateWhenNavigating",
|
||||
"resourceVersion": "1716555778767",
|
||||
"creationTimestamp": "2024-05-24T13:02:58Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables possibility to preserve dashboard variables and time range when navigating between dashboards",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/dashboards-squad",
|
||||
"hideFromAdminPage": true,
|
||||
"hideFromDocs": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -16,6 +16,7 @@ import { PanelEditor } from '../panel-edit/PanelEditor';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { buildNewDashboardSaveModel } from '../serialization/buildNewDashboardSaveModel';
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { restoreDashboardStateFromLocalStorage } from '../utils/dashboardSessionState';
|
||||
|
||||
import { updateNavModel } from './utils';
|
||||
|
||||
@ -177,6 +178,10 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.featureToggles.preserveDashboardStateWhenNavigating && Boolean(options.uid)) {
|
||||
restoreDashboardStateFromLocalStorage(dashboard);
|
||||
}
|
||||
|
||||
if (!(config.publicDashboardAccessToken && dashboard.state.controls?.state.hideTimeControls)) {
|
||||
dashboard.startUrlSync();
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
import { RowActions } from '../scene/row-actions/RowActions';
|
||||
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
|
||||
import { createPanelDataProvider } from '../utils/createPanelDataProvider';
|
||||
import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import {
|
||||
getCurrentValueForOldIntervalModel,
|
||||
@ -278,6 +279,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
||||
registerDashboardMacro,
|
||||
registerPanelInteractionsReporter,
|
||||
new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }),
|
||||
preserveDashboardSceneStateInLocalStorage,
|
||||
],
|
||||
$data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }),
|
||||
controls: new DashboardControls({
|
||||
|
@ -0,0 +1,125 @@
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { CustomVariable } from '@grafana/scenes';
|
||||
import { DashboardDataDTO } from 'app/types';
|
||||
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
|
||||
import { PRESERVED_SCENE_STATE_KEY, restoreDashboardStateFromLocalStorage } from './dashboardSessionState';
|
||||
|
||||
describe('dashboardSessionState', () => {
|
||||
beforeAll(() => {
|
||||
config.featureToggles.preserveDashboardStateWhenNavigating = true;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.featureToggles.preserveDashboardStateWhenNavigating = false;
|
||||
});
|
||||
|
||||
beforeEach(() => {});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('should do nothing for default home dashboard', () => {
|
||||
const scene = buildTestScene();
|
||||
scene.setState({ uid: undefined });
|
||||
|
||||
const deactivate = scene.activate();
|
||||
expect(window.sessionStorage.getItem(PRESERVED_SCENE_STATE_KEY)).toBeNull();
|
||||
|
||||
deactivate();
|
||||
expect(window.sessionStorage.getItem(PRESERVED_SCENE_STATE_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('should capture dashboard scene state and save it to session storage on deactivation', () => {
|
||||
const scene = buildTestScene();
|
||||
|
||||
const deactivate = scene.activate();
|
||||
expect(window.sessionStorage.getItem(PRESERVED_SCENE_STATE_KEY)).toBeNull();
|
||||
|
||||
deactivate();
|
||||
expect(window.sessionStorage.getItem(PRESERVED_SCENE_STATE_KEY)).toBe(
|
||||
'?from=now-6h&to=now&timezone=browser&var-customVar=a'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreDashboardStateFromLocalStorage', () => {
|
||||
it('should restore dashboard state from session storage', () => {
|
||||
window.sessionStorage.setItem(PRESERVED_SCENE_STATE_KEY, '?var-customVar=b&from=now-5m&to=now&timezone=browser');
|
||||
const scene = buildTestScene();
|
||||
|
||||
restoreDashboardStateFromLocalStorage(scene);
|
||||
const variable = scene.state.$variables!.getByName('customVar') as CustomVariable;
|
||||
const timeRange = scene.state.$timeRange;
|
||||
scene.startUrlSync();
|
||||
|
||||
expect(variable!.state!.value).toEqual(['b']);
|
||||
expect(variable!.state!.text).toEqual(['b']);
|
||||
expect(timeRange?.state.from).toEqual('now-5m');
|
||||
expect(timeRange?.state.to).toEqual('now');
|
||||
});
|
||||
|
||||
it('should remove query params that are not applicable on a target dashboard', () => {
|
||||
window.sessionStorage.setItem(
|
||||
PRESERVED_SCENE_STATE_KEY,
|
||||
'?var-customVar=b&var-nonApplicableVar=b&from=now-5m&to=now&timezone=browser'
|
||||
);
|
||||
const scene = buildTestScene();
|
||||
|
||||
restoreDashboardStateFromLocalStorage(scene);
|
||||
|
||||
expect(locationService.getSearch().toString()).toBe('var-customVar=b&from=now-5m&to=now&timezone=browser');
|
||||
});
|
||||
|
||||
// handles case when user navigates back to a dashboard with the same state, i.e. using back button
|
||||
it('should remove duplicate query params', () => {
|
||||
locationService.replace({ search: 'var-customVar=b&from=now-6h&to=now&timezone=browser' });
|
||||
|
||||
window.sessionStorage.setItem(PRESERVED_SCENE_STATE_KEY, '?var-customVar=b&from=now-5m&to=now&timezone=browser');
|
||||
const scene = buildTestScene();
|
||||
|
||||
restoreDashboardStateFromLocalStorage(scene);
|
||||
|
||||
expect(locationService.getSearch().toString()).toBe('var-customVar=b&from=now-6h&to=now&timezone=browser');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildTestScene() {
|
||||
const testDashboard: DashboardDataDTO = {
|
||||
annotations: { list: [] },
|
||||
editable: true,
|
||||
fiscalYearStartMonth: 0,
|
||||
graphTooltip: 0,
|
||||
id: 2483,
|
||||
links: [],
|
||||
panels: [],
|
||||
schemaVersion: 39,
|
||||
tags: [],
|
||||
templating: {
|
||||
list: [
|
||||
{
|
||||
multi: true,
|
||||
name: 'customVar',
|
||||
query: 'a,b,c',
|
||||
type: 'custom',
|
||||
},
|
||||
],
|
||||
},
|
||||
time: {
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
},
|
||||
timepicker: {},
|
||||
timezone: 'browser',
|
||||
title: 'Test dashboard',
|
||||
uid: 'edhmd9stpd6o0a',
|
||||
version: 24,
|
||||
weekStart: '',
|
||||
};
|
||||
const scene = transformSaveModelToScene({ dashboard: testDashboard, meta: {} });
|
||||
|
||||
// Removing data layers to avoid mocking built-in Grafana data source
|
||||
scene.setState({ $data: undefined });
|
||||
|
||||
return scene;
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import { UrlQueryMap, urlUtil } from '@grafana/data';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { UrlSyncManager } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
|
||||
export const PRESERVED_SCENE_STATE_KEY = `grafana.dashboard.preservedUrlFiltersState`;
|
||||
|
||||
export function restoreDashboardStateFromLocalStorage(dashboard: DashboardScene) {
|
||||
const preservedUrlState = window.sessionStorage.getItem(PRESERVED_SCENE_STATE_KEY);
|
||||
|
||||
if (preservedUrlState) {
|
||||
const preservedQueryParams = new URLSearchParams(preservedUrlState);
|
||||
const currentQueryParams = locationService.getSearch();
|
||||
|
||||
// iterate over preserved query params and append them to current query params if they don't already exist
|
||||
preservedQueryParams.forEach((value, key) => {
|
||||
if (!currentQueryParams.has(key)) {
|
||||
currentQueryParams.append(key, value);
|
||||
} else {
|
||||
if (!currentQueryParams.getAll(key).includes(value)) {
|
||||
currentQueryParams.append(key, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const key of Array.from(currentQueryParams.keys())) {
|
||||
// preserve non-variable query params, i.e. time range
|
||||
if (!key.startsWith('var-')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// remove params for variables that are not present on the target dashboard
|
||||
if (!dashboard.state.$variables?.getByName(key.replace('var-', ''))) {
|
||||
currentQueryParams.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
const finalParams = currentQueryParams.toString();
|
||||
if (finalParams) {
|
||||
locationService.replace({
|
||||
search: finalParams,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenes behavior that will capture currently selected variables and time range and save them to local storage, so that they can be applied when the next dashboard is loaded.
|
||||
*/
|
||||
export function preserveDashboardSceneStateInLocalStorage(scene: DashboardScene) {
|
||||
if (!config.featureToggles.preserveDashboardStateWhenNavigating) {
|
||||
return;
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Skipping saving state for default home dashboard
|
||||
if (!scene.state.uid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const urlStates: UrlQueryMap = Object.fromEntries(
|
||||
Object.entries(new UrlSyncManager().getUrlState(scene)).filter(
|
||||
([key]) => key.startsWith('var-') || key === 'from' || key === 'to' || key === 'timezone'
|
||||
)
|
||||
);
|
||||
|
||||
const nonEmptyUrlStates = Object.fromEntries(
|
||||
Object.entries(urlStates).filter(([key, value]) => !(Array.isArray(value) && value.length === 0))
|
||||
);
|
||||
|
||||
// If there's anything to preserve, save it to local storage
|
||||
if (Object.keys(nonEmptyUrlStates).length > 0) {
|
||||
window.sessionStorage.setItem(PRESERVED_SCENE_STATE_KEY, urlUtil.renderUrl('', nonEmptyUrlStates));
|
||||
} else {
|
||||
window.sessionStorage.removeItem(PRESERVED_SCENE_STATE_KEY);
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user