Dashboard: Fix dashboard reload behavior (#96427)

This commit is contained in:
Bogdan Matei 2024-11-18 17:09:03 +02:00 committed by GitHub
parent 8e0e3397a3
commit 984fbac1ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 116 additions and 103 deletions

View File

@ -44,7 +44,15 @@ export interface LoadDashboardOptions {
uid: string; uid: string;
route: DashboardRoutes; route: DashboardRoutes;
urlFolderUid?: string; urlFolderUid?: string;
queryParams?: UrlQueryMap; params?: {
version: number;
scopes: string[];
timeRange: {
from: string;
to: string;
};
variables: UrlQueryMap;
};
} }
export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> { export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> {
@ -59,11 +67,11 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
uid, uid,
route, route,
urlFolderUid, urlFolderUid,
queryParams, params,
}: LoadDashboardOptions): Promise<DashboardDTO | null> { }: LoadDashboardOptions): Promise<DashboardDTO | null> {
const cacheKey = route === DashboardRoutes.Home ? HOME_DASHBOARD_CACHE_KEY : uid; const cacheKey = route === DashboardRoutes.Home ? HOME_DASHBOARD_CACHE_KEY : uid;
if (!queryParams) { if (!params) {
const cachedDashboard = this.getDashboardFromCache(cacheKey); const cachedDashboard = this.getDashboardFromCache(cacheKey);
if (cachedDashboard) { if (cachedDashboard) {
@ -97,6 +105,15 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
return await dashboardLoaderSrv.loadDashboard('public', '', uid); return await dashboardLoaderSrv.loadDashboard('public', '', uid);
} }
default: default:
const queryParams = params
? {
version: params.version,
scopes: params.scopes,
from: params.timeRange.from,
to: params.timeRange.to,
...params.variables,
}
: undefined;
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid, queryParams); rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid, queryParams);
if (route === DashboardRoutes.Embedded) { if (route === DashboardRoutes.Embedded) {
@ -188,17 +205,26 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
} }
} }
public async reloadDashboard(queryParams?: LoadDashboardOptions['queryParams'] | undefined) { public async reloadDashboard(params: LoadDashboardOptions['params']) {
if (!this.state.options) { const stateOptions = this.state.options;
if (!stateOptions) {
return; return;
} }
const options = { const options = {
...this.state.options, ...stateOptions,
queryParams, params,
}; };
if (isEqual(options, this.state.options)) { // We shouldn't check all params since:
// - version doesn't impact the new dashboard, and it's there for increased compatibility
// - time range is almost always different for relative time ranges and absolute time ranges do not trigger subsequent reloads
// - other params don't affect the dashboard content
if (
isEqual(options.params?.variables, stateOptions.params?.variables) &&
isEqual(options.params?.scopes, stateOptions.params?.scopes)
) {
return; return;
} }

View File

@ -1,8 +1,8 @@
import { isEqual } from 'lodash'; import { debounce, isEqual } from 'lodash';
import { UrlQueryMap } from '@grafana/data'; import { UrlQueryMap } from '@grafana/data';
import { sceneGraph, SceneObjectBase, SceneObjectState, VariableDependencyConfig } from '@grafana/scenes'; import { sceneGraph, SceneObjectBase, SceneObjectState, VariableDependencyConfig } from '@grafana/scenes';
import { getClosestScopesFacade, ScopesFacade } from 'app/features/scopes'; import { getClosestScopesFacade } from 'app/features/scopes';
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager'; import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager';
@ -13,27 +13,25 @@ export interface DashboardReloadBehaviorState extends SceneObjectState {
} }
export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBehaviorState> { export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBehaviorState> {
private _scopesFacade: ScopesFacade | null = null;
constructor(state: DashboardReloadBehaviorState) { constructor(state: DashboardReloadBehaviorState) {
const shouldReload = state.reloadOnParamsChange && state.uid; const shouldReload = state.reloadOnParamsChange && state.uid;
super(state); super(state);
this.reloadDashboard = this.reloadDashboard.bind(this); // Sometimes the reload is triggered multiple subsequent times
// Debouncing it prevents double/triple reloads
this.reloadDashboard = debounce(this.reloadDashboard).bind(this);
if (shouldReload) { if (shouldReload) {
this.addActivationHandler(() => { this.addActivationHandler(() => {
this._scopesFacade = getClosestScopesFacade(this); getClosestScopesFacade(this)?.setState({
handler: this.reloadDashboard,
});
this._variableDependency = new VariableDependencyConfig(this, { this._variableDependency = new VariableDependencyConfig(this, {
onAnyVariableChanged: this.reloadDashboard, onAnyVariableChanged: this.reloadDashboard,
}); });
this._scopesFacade?.setState({
handler: this.reloadDashboard,
});
this._subs.add( this._subs.add(
sceneGraph.getTimeRange(this).subscribeToState((newState, prevState) => { sceneGraph.getTimeRange(this).subscribeToState((newState, prevState) => {
if (!isEqual(newState.value, prevState.value)) { if (!isEqual(newState.value, prevState.value)) {
@ -41,6 +39,8 @@ export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBeha
} }
}) })
); );
this.reloadDashboard();
}); });
} }
} }
@ -59,21 +59,25 @@ export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBeha
if (!this.isEditing() && !this.isWaitingForVariables()) { if (!this.isEditing() && !this.isWaitingForVariables()) {
const timeRange = sceneGraph.getTimeRange(this); const timeRange = sceneGraph.getTimeRange(this);
let params: UrlQueryMap = { // This is wrapped in setTimeout in order to allow variables and scopes to be set in the URL before actually reloading the dashboard
version: this.state.version, setTimeout(() => {
scopes: this._scopesFacade?.value.map((scope) => scope.metadata.name), getDashboardScenePageStateManager().reloadDashboard({
...timeRange.urlSync?.getUrlState(), version: this.state.version!,
}; scopes: getClosestScopesFacade(this)?.value.map((scope) => scope.metadata.name) ?? [],
// We're not using the getUrlState from timeRange since it makes more sense to pass the absolute timestamps as opposed to relative time
params = sceneGraph.getVariables(this).state.variables.reduce<UrlQueryMap>( timeRange: {
(acc, variable) => ({ from: timeRange.state.value.from.toISOString(),
...acc, to: timeRange.state.value.to.toISOString(),
...variable.urlSync?.getUrlState(), },
}), variables: sceneGraph.getVariables(this).state.variables.reduce<UrlQueryMap>(
params (acc, variable) => ({
); ...acc,
...variable.urlSync?.getUrlState(),
getDashboardScenePageStateManager().reloadDashboard(params); }),
{}
),
});
});
} }
} }
} }

View File

@ -1,7 +1,8 @@
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
import { enterEditMode, updateMyVar, updateScopes, updateTimeRange } from './utils/actions'; import { clearMocks, enterEditMode, updateMyVar, updateScopes, updateTimeRange } from './utils/actions';
import { expectDashboardReload, expectNotDashboardReload } from './utils/assertions'; import { expectDashboardReload, expectNotDashboardReload } from './utils/assertions';
import { getDatasource, getInstanceSettings, getMock } from './utils/mocks'; import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render'; import { renderDashboard, resetScenes } from './utils/render';
@ -15,84 +16,66 @@ jest.mock('@grafana/runtime', () => ({
usePluginLinks: jest.fn().mockReturnValue({ links: [] }), usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
})); }));
const runTest = async (
reloadDashboardsOnParamsChange: boolean,
reloadOnParamsChange: boolean,
withUid: boolean,
editMode: boolean
) => {
config.featureToggles.reloadDashboardsOnParamsChange = reloadDashboardsOnParamsChange;
setDashboardAPI(undefined);
const uid = 'dash-1';
const dashboardScene = renderDashboard({ uid: withUid ? uid : undefined }, { reloadOnParamsChange });
if (editMode) {
await enterEditMode(dashboardScene);
}
const shouldReload = reloadDashboardsOnParamsChange && reloadOnParamsChange && withUid && !editMode;
await updateTimeRange(dashboardScene);
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
await updateMyVar(dashboardScene, '2');
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
await updateScopes(['grafana']);
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
};
describe('Dashboard reload', () => { describe('Dashboard reload', () => {
beforeAll(() => { beforeAll(() => {
config.featureToggles.scopeFilters = true; config.featureToggles.scopeFilters = true;
config.featureToggles.groupByVariable = true; config.featureToggles.groupByVariable = true;
}); });
afterEach(async () => { it.each([
setDashboardAPI(undefined); [false, false, false, false],
await resetScenes(); [false, false, true, false],
}); [false, true, false, false],
[false, true, true, false],
[true, false, false, false],
[true, false, true, false],
[true, true, false, true],
[true, true, true, true],
[true, true, false, false],
[true, true, true, false],
])(
`reloadDashboardsOnParamsChange: %s, reloadOnParamsChange: %s, withUid: %s, editMode: %s`,
async (reloadDashboardsOnParamsChange, reloadOnParamsChange, withUid, editMode) => {
config.featureToggles.reloadDashboardsOnParamsChange = reloadDashboardsOnParamsChange;
setDashboardAPI(undefined);
describe('reloadDashboardsOnParamsChange off', () => { const dashboardScene = renderDashboard({ uid: withUid ? 'dash-1' : undefined }, { reloadOnParamsChange });
describe('reloadOnParamsChange off', () => {
it('with UID - no reload', () => runTest(false, false, true, false));
it('without UID - no reload', () => runTest(false, false, false, false));
});
describe('reloadOnParamsChange on', () => { if (editMode) {
it('with UID - no reload', () => runTest(false, true, true, false)); await enterEditMode(dashboardScene);
it('without UID - no reload', () => runTest(false, true, false, false)); }
});
});
describe('reloadDashboardsOnParamsChange on', () => { const shouldReload = reloadDashboardsOnParamsChange && reloadOnParamsChange && withUid && !editMode;
describe('reloadOnParamsChange off', () => {
it('with UID - no reload', () => runTest(true, false, true, false));
it('without UID - no reload', () => runTest(true, false, false, false));
});
describe('reloadOnParamsChange on', () => { await updateTimeRange(dashboardScene);
describe('edit mode on', () => { await jest.advanceTimersToNextTimerAsync();
it('with UID - no reload', () => runTest(true, true, true, true)); if (!shouldReload) {
it('without UID - no reload', () => runTest(true, true, false, true)); expectNotDashboardReload();
}); } else {
expectDashboardReload();
}
describe('edit mode off', () => { await updateMyVar(dashboardScene, '2');
it('with UID - reload', () => runTest(true, true, true, false)); await jest.advanceTimersToNextTimerAsync();
it('without UID - no reload', () => runTest(true, true, false, false)); if (!shouldReload) {
}); expectNotDashboardReload();
}); } else {
}); expectDashboardReload();
}
await updateScopes(['grafana']);
await jest.advanceTimersToNextTimerAsync();
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
getDashboardScenePageStateManager().clearDashboardCache();
getDashboardScenePageStateManager().clearSceneCache();
setDashboardAPI(undefined);
await resetScenes();
clearMocks();
}
);
}); });