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;
route: DashboardRoutes;
urlFolderUid?: string;
queryParams?: UrlQueryMap;
params?: {
version: number;
scopes: string[];
timeRange: {
from: string;
to: string;
};
variables: UrlQueryMap;
};
}
export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> {
@ -59,11 +67,11 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
uid,
route,
urlFolderUid,
queryParams,
params,
}: LoadDashboardOptions): Promise<DashboardDTO | null> {
const cacheKey = route === DashboardRoutes.Home ? HOME_DASHBOARD_CACHE_KEY : uid;
if (!queryParams) {
if (!params) {
const cachedDashboard = this.getDashboardFromCache(cacheKey);
if (cachedDashboard) {
@ -97,6 +105,15 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
return await dashboardLoaderSrv.loadDashboard('public', '', uid);
}
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);
if (route === DashboardRoutes.Embedded) {
@ -188,17 +205,26 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
}
}
public async reloadDashboard(queryParams?: LoadDashboardOptions['queryParams'] | undefined) {
if (!this.state.options) {
public async reloadDashboard(params: LoadDashboardOptions['params']) {
const stateOptions = this.state.options;
if (!stateOptions) {
return;
}
const options = {
...this.state.options,
queryParams,
...stateOptions,
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;
}

View File

@ -1,8 +1,8 @@
import { isEqual } from 'lodash';
import { debounce, isEqual } from 'lodash';
import { UrlQueryMap } from '@grafana/data';
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';
@ -13,27 +13,25 @@ export interface DashboardReloadBehaviorState extends SceneObjectState {
}
export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBehaviorState> {
private _scopesFacade: ScopesFacade | null = null;
constructor(state: DashboardReloadBehaviorState) {
const shouldReload = state.reloadOnParamsChange && state.uid;
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) {
this.addActivationHandler(() => {
this._scopesFacade = getClosestScopesFacade(this);
getClosestScopesFacade(this)?.setState({
handler: this.reloadDashboard,
});
this._variableDependency = new VariableDependencyConfig(this, {
onAnyVariableChanged: this.reloadDashboard,
});
this._scopesFacade?.setState({
handler: this.reloadDashboard,
});
this._subs.add(
sceneGraph.getTimeRange(this).subscribeToState((newState, prevState) => {
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()) {
const timeRange = sceneGraph.getTimeRange(this);
let params: UrlQueryMap = {
version: this.state.version,
scopes: this._scopesFacade?.value.map((scope) => scope.metadata.name),
...timeRange.urlSync?.getUrlState(),
};
params = sceneGraph.getVariables(this).state.variables.reduce<UrlQueryMap>(
// This is wrapped in setTimeout in order to allow variables and scopes to be set in the URL before actually reloading the dashboard
setTimeout(() => {
getDashboardScenePageStateManager().reloadDashboard({
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
timeRange: {
from: timeRange.state.value.from.toISOString(),
to: timeRange.state.value.to.toISOString(),
},
variables: sceneGraph.getVariables(this).state.variables.reduce<UrlQueryMap>(
(acc, variable) => ({
...acc,
...variable.urlSync?.getUrlState(),
}),
params
);
getDashboardScenePageStateManager().reloadDashboard(params);
{}
),
});
});
}
}
}

View File

@ -1,7 +1,8 @@
import { config } from '@grafana/runtime';
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 { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render';
@ -15,16 +16,30 @@ jest.mock('@grafana/runtime', () => ({
usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
}));
const runTest = async (
reloadDashboardsOnParamsChange: boolean,
reloadOnParamsChange: boolean,
withUid: boolean,
editMode: boolean
) => {
describe('Dashboard reload', () => {
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.groupByVariable = true;
});
it.each([
[false, false, false, false],
[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);
const uid = 'dash-1';
const dashboardScene = renderDashboard({ uid: withUid ? uid : undefined }, { reloadOnParamsChange });
const dashboardScene = renderDashboard({ uid: withUid ? 'dash-1' : undefined }, { reloadOnParamsChange });
if (editMode) {
await enterEditMode(dashboardScene);
@ -33,6 +48,7 @@ const runTest = async (
const shouldReload = reloadDashboardsOnParamsChange && reloadOnParamsChange && withUid && !editMode;
await updateTimeRange(dashboardScene);
await jest.advanceTimersToNextTimerAsync();
if (!shouldReload) {
expectNotDashboardReload();
} else {
@ -40,6 +56,7 @@ const runTest = async (
}
await updateMyVar(dashboardScene, '2');
await jest.advanceTimersToNextTimerAsync();
if (!shouldReload) {
expectNotDashboardReload();
} else {
@ -47,52 +64,18 @@ const runTest = async (
}
await updateScopes(['grafana']);
await jest.advanceTimersToNextTimerAsync();
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
};
describe('Dashboard reload', () => {
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.groupByVariable = true;
});
afterEach(async () => {
getDashboardScenePageStateManager().clearDashboardCache();
getDashboardScenePageStateManager().clearSceneCache();
setDashboardAPI(undefined);
await resetScenes();
});
describe('reloadDashboardsOnParamsChange off', () => {
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', () => {
it('with UID - no reload', () => runTest(false, true, true, false));
it('without UID - no reload', () => runTest(false, true, false, false));
});
});
describe('reloadDashboardsOnParamsChange on', () => {
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', () => {
describe('edit mode on', () => {
it('with UID - no reload', () => runTest(true, true, true, true));
it('without UID - no reload', () => runTest(true, true, false, true));
});
describe('edit mode off', () => {
it('with UID - reload', () => runTest(true, true, true, false));
it('without UID - no reload', () => runTest(true, true, false, false));
});
});
});
clearMocks();
}
);
});