DashboardReload: Refactor / move state diffing out of DashboardPageStateManager and into reload behavior, simplify (#106039)

* DashboardReload refactor

* Update

* revert accidental commit

* fix

* merge fix

* fix cache issue

* Update scenes

* Update

* Update

* revert debug

* Update scenes

* Update tests

* add back reload on activation, fix infinite reload bug

* Update prevState

* Remove time range subscription

* restore iso time format

* Update scenes

* Mock console for reload tests (for now)

* Only update scopes from URL when scopes in URL change

* fix equality check inside ScopesSelectorService

* branch fix drone

* Update

* revert drone verbosity

* Update scenes

---------

Co-authored-by: Victor Marin <victor.marin@grafana.com>
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
pull/106316/head
Torkel Ödegaard 3 weeks ago committed by GitHub
parent f34d372bd3
commit 1b9d062583
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      package.json
  2. 116
      public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts
  3. 133
      public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
  4. 79
      public/app/features/dashboard-scene/scene/DashboardReloadBehavior.ts
  5. 1
      public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
  6. 1
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
  7. 8
      public/app/features/scopes/ScopesService.ts
  8. 5
      public/app/features/scopes/selector/ScopesSelectorService.ts
  9. 4
      public/app/features/scopes/tests/dashboardReload.test.ts
  10. 22
      yarn.lock

@ -284,8 +284,8 @@
"@grafana/plugin-ui": "0.10.6",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^6.12.0",
"@grafana/scenes-react": "^6.12.0",
"@grafana/scenes": "^6.18.0",
"@grafana/scenes-react": "^6.18.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",

@ -112,7 +112,7 @@ describe('DashboardScenePageStateManager v1', () => {
const loader = new DashboardScenePageStateManager({});
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash', undefined);
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash');
// should use cache second time
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
@ -664,8 +664,8 @@ describe('DashboardScenePageStateManager v2', () => {
getDashSpy
);
const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
await loader.reloadDashboard(options);
const params = { version: 2, scopes: [], from: 'now-1h', to: 'now' };
await loader.reloadDashboard(params);
expect(getDashSpy).toHaveBeenCalledTimes(2);
expect(loader.state.dashboard?.state.version).toBe(2);
@ -695,28 +695,11 @@ describe('DashboardScenePageStateManager v2', () => {
expect(getDashSpy).toHaveBeenCalledTimes(1);
const initialDashboard = loader.state.dashboard;
const mockDashboard: DashboardWithAccessInfo<DashboardV2Spec> = {
access: {},
apiVersion: 'v2alpha1',
kind: 'DashboardWithAccessInfo',
metadata: {
name: 'fake-dash',
creationTimestamp: '',
resourceVersion: '1',
generation: 1,
},
spec: { ...defaultDashboardV2Spec() },
};
const fetchDashboardSpy = jest.spyOn(loader, 'fetchDashboard').mockResolvedValue(mockDashboard);
const options = { version: 1, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
await loader.reloadDashboard(options);
const params = { version: 1, scopes: [], from: 'now-1h', to: 'now' };
await loader.reloadDashboard(params);
expect(fetchDashboardSpy).toHaveBeenCalledTimes(1);
expect(getDashSpy).toHaveBeenCalledTimes(2);
expect(loader.state.dashboard).toBe(initialDashboard);
fetchDashboardSpy.mockRestore();
});
it('should not use cache if cache version and current dashboard state version differ', async () => {
@ -742,32 +725,32 @@ describe('DashboardScenePageStateManager v2', () => {
expect(getDashSpy).toHaveBeenCalledTimes(1);
const mockDashboard: DashboardWithAccessInfo<DashboardV2Spec> = {
access: {},
apiVersion: 'v2alpha1',
kind: 'DashboardWithAccessInfo',
metadata: {
name: 'fake-dash',
creationTimestamp: '',
resourceVersion: '1',
generation: 2,
setupDashboardAPI(
{
access: {},
apiVersion: 'v2alpha1',
kind: 'DashboardWithAccessInfo',
metadata: {
name: 'fake-dash',
creationTimestamp: '',
resourceVersion: '1',
generation: 2,
},
spec: { ...defaultDashboardV2Spec() },
},
spec: { ...defaultDashboardV2Spec() },
};
const fetchDashboardSpy = jest.spyOn(loader, 'fetchDashboard').mockResolvedValue(mockDashboard);
getDashSpy
);
// mimic navigating from db1 to db2 and then back to db1, which maintains the cache. but on
// db1 load the initial version will be 1. Since the cache is set we also need to verify against the
// current dashboard state whether we should reload or not
loader.setSceneCache('fake-dash', loader.state.dashboard!.clone({ version: 2 }));
const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
await loader.reloadDashboard(options);
expect(fetchDashboardSpy).toHaveBeenCalledTimes(1);
expect(loader.state.dashboard?.state.version).toBe(2);
const params = { version: 2, scopes: [], from: 'now-1h', to: 'now' };
await loader.reloadDashboard(params);
fetchDashboardSpy.mockRestore();
expect(getDashSpy).toHaveBeenCalledTimes(2);
expect(loader.state.dashboard?.state.version).toBe(2);
});
it('should handle errors during reload', async () => {
@ -797,8 +780,8 @@ describe('DashboardScenePageStateManager v2', () => {
loader['dashboardLoader'] = mockLoader as unknown as DashboardLoaderSrvV2;
const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
await loader.reloadDashboard(options);
const params = { version: 2, scopes: [], from: 'now-1h', to: 'now' };
await loader.reloadDashboard(params);
expect(loader.state.loadError).toBeDefined();
expect(loader.state.loadError?.message).toBe('Failed to load dashboard');
@ -831,9 +814,9 @@ describe('DashboardScenePageStateManager v2', () => {
loader['dashboardLoader'] = mockLoader as unknown as DashboardLoaderSrvV2;
const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
const params = { version: 2, scopes: [], from: 'now-1h', to: 'now' };
await expect(loader.reloadDashboard(options)).rejects.toThrow(DashboardVersionError);
await expect(loader.reloadDashboard(params)).rejects.toThrow(DashboardVersionError);
});
});
});
@ -852,7 +835,7 @@ describe('UnifiedDashboardScenePageStateManager', () => {
const manager = new UnifiedDashboardScenePageStateManager({});
await manager.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash', undefined);
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash');
expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManager);
});
@ -892,7 +875,7 @@ describe('UnifiedDashboardScenePageStateManager', () => {
expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2);
// Reload should now work with v2 manager
const options = { version: 1, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
const params = { version: 1, scopes: [], from: 'now-1h', to: 'now' };
// Mock the fetchDashboard method to return a dashboard
const v2Manager = manager['activeManager'] as DashboardScenePageStateManagerV2;
@ -910,7 +893,7 @@ describe('UnifiedDashboardScenePageStateManager', () => {
spec: { ...defaultDashboardV2Spec() },
});
await manager.reloadDashboard(options);
await manager.reloadDashboard(params);
// Restore the original method
v2Manager.fetchDashboard = originalFetchDashboard;
@ -959,21 +942,16 @@ describe('UnifiedDashboardScenePageStateManager', () => {
await manager.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash', undefined);
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash');
expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManager);
loadDashboardMock.mockClear();
const reloadMock = jest.fn();
manager['activeManager'].reloadDashboard = reloadMock;
const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
await manager.reloadDashboard(options);
const params = { version: 2, scopes: [], from: 'now-1h', to: 'now' };
await manager.reloadDashboard(params);
expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManager);
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash', {
from: 'now-1h',
to: 'now',
version: 2,
scopes: [],
});
expect(reloadMock).toHaveBeenCalledTimes(1);
});
it('should reload v2 dashboard with v2 manager', async () => {
@ -984,9 +962,10 @@ describe('UnifiedDashboardScenePageStateManager', () => {
expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2);
const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
const params = { version: 2, scopes: [], from: 'now-1h', to: 'now' };
try {
await manager.reloadDashboard(options);
await manager.reloadDashboard(params);
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect((e as Error).message).toBe('Method not implemented.');
@ -1004,11 +983,9 @@ describe('UnifiedDashboardScenePageStateManager', () => {
const manager = new UnifiedDashboardScenePageStateManager({});
await manager.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash', undefined);
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash');
expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManager);
loadDashboardMock.mockClear();
const mockDashboard: DashboardDTO = {
dashboard: {
uid: 'fake-dash',
@ -1018,19 +995,18 @@ describe('UnifiedDashboardScenePageStateManager', () => {
meta: {},
};
const fetchDashboardSpy = jest.spyOn(manager['activeManager'], 'fetchDashboard').mockResolvedValue(mockDashboard);
loadDashboardMock.mockResolvedValue(mockDashboard);
// mimic navigating from db1 to db2 and then back to db1, which maintains the cache. but on
// db1 load the initial version will be 1. Since the cache is set we also need to verify against the
// current dashboard state whether we should reload or not
manager.setSceneCache('fake-dash', manager.state.dashboard!.clone({ version: 2 }));
const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
await manager.reloadDashboard(options);
expect(fetchDashboardSpy).toHaveBeenCalledTimes(1);
expect(manager.state.dashboard?.state.version).toBe(2);
const params = { version: 2, scopes: [], from: 'now-1h', to: 'now' };
await manager.reloadDashboard(params);
fetchDashboardSpy.mockRestore();
expect(loadDashboardMock).toHaveBeenCalledTimes(2);
expect(manager.state.dashboard?.state.version).toBe(2);
});
});

@ -1,5 +1,3 @@
import { isEqual } from 'lodash';
import { locationUtil, UrlQueryMap } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
@ -48,7 +46,6 @@ export interface LoadError {
export interface DashboardScenePageState {
dashboard?: DashboardScene;
options?: LoadDashboardOptions;
panelEditor?: PanelEditor;
isLoading?: boolean;
loadError?: LoadError;
@ -73,15 +70,6 @@ export interface LoadDashboardOptions {
slug?: string;
type?: string;
urlFolderUid?: string;
params?: {
version: number;
scopes: string[];
timeRange: {
from: string;
to: string;
};
variables: UrlQueryMap;
};
}
export type HomeDashboardDTO = DashboardDTO & {
@ -93,7 +81,7 @@ interface DashboardScenePageStateManagerLike<T> {
getDashboardFromCache(cacheKey: string): T | null;
loadDashboard(options: LoadDashboardOptions): Promise<void>;
transformResponseToScene(rsp: T | null, options: LoadDashboardOptions): DashboardScene | null;
reloadDashboard(params: LoadDashboardOptions['params']): Promise<void>;
reloadDashboard(queryParams: UrlQueryMap): Promise<void>;
loadSnapshot(slug: string): Promise<void>;
setDashboardCache(cacheKey: string, dashboard: T): void;
clearSceneCache(): void;
@ -108,7 +96,7 @@ abstract class DashboardScenePageStateManagerBase<T>
implements DashboardScenePageStateManagerLike<T>
{
abstract fetchDashboard(options: LoadDashboardOptions): Promise<T | null>;
abstract reloadDashboard(params: LoadDashboardOptions['params']): Promise<void>;
abstract reloadDashboard(queryParams: UrlQueryMap): Promise<void>;
abstract transformResponseToScene(rsp: T | null, options: LoadDashboardOptions): DashboardScene | null;
abstract loadSnapshotScene(slug: string): Promise<DashboardScene>;
@ -274,7 +262,7 @@ abstract class DashboardScenePageStateManagerBase<T>
restoreDashboardStateFromLocalStorage(dashboard);
}
this.setState({ dashboard: dashboard, isLoading: false, options });
this.setState({ dashboard: dashboard, isLoading: false });
const measure = stopMeasure(LOAD_SCENE_MEASUREMENT);
const queryController = sceneGraph.getQueryController(dashboard);
@ -418,16 +406,13 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
uid,
route,
urlFolderUid,
params,
}: LoadDashboardOptions): Promise<DashboardDTO | null> {
const cacheKey = route === DashboardRoutes.Home ? HOME_DASHBOARD_CACHE_KEY : uid;
if (!params) {
const cachedDashboard = this.getDashboardFromCache(cacheKey);
const cachedDashboard = this.getDashboardFromCache(cacheKey);
if (cachedDashboard) {
return cachedDashboard;
}
if (cachedDashboard) {
return cachedDashboard;
}
let rsp: DashboardDTO;
@ -454,17 +439,7 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
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(type || 'db', slug || '', uid, queryParams);
rsp = await dashboardLoaderSrv.loadDashboard(type || 'db', slug || '', uid);
if (route === DashboardRoutes.Embedded) {
rsp.meta.isEmbedded = true;
@ -504,30 +479,20 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
return rsp;
}
public async reloadDashboard(params: LoadDashboardOptions['params']) {
const stateOptions = this.state.options;
public async reloadDashboard(queryParams: UrlQueryMap): Promise<void> {
const dashboard = this.state.dashboard;
if (!stateOptions) {
if (!dashboard || !dashboard.state.uid) {
return;
}
const options = {
...stateOptions,
params,
};
if (
isEqual(options.params?.variables, stateOptions.params?.variables) &&
isEqual(options.params?.scopes, stateOptions.params?.scopes)
) {
return;
}
const uid = dashboard.state.uid;
try {
this.setState({ isLoading: true });
const rsp = await this.fetchDashboard(options);
const fromCache = this.getSceneFromCache(options.uid);
const rsp = await dashboardLoaderSrv.loadDashboard('db', dashboard.state.meta.slug, uid, queryParams);
const fromCache = this.getSceneFromCache(uid);
// check if cached db version is same as both
// response and current db state. There are scenarios where they can differ
@ -564,16 +529,16 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
const scene = transformSaveModelToScene(rsp);
// we need to call and restore dashboard state on every reload that pulls a new dashboard version
if (config.featureToggles.preserveDashboardStateWhenNavigating && Boolean(options.uid)) {
if (config.featureToggles.preserveDashboardStateWhenNavigating && Boolean(uid)) {
restoreDashboardStateFromLocalStorage(scene);
}
this.setSceneCache(options.uid, scene);
this.setState({ dashboard: scene, isLoading: false, options });
this.setSceneCache(uid, scene);
this.setState({ dashboard: scene, isLoading: false });
} catch (err) {
const status = getStatusFromError(err);
const message = getMessageFromError(err);
this.setState({
isLoading: false,
loadError: {
@ -581,6 +546,7 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
status,
},
});
if (err instanceof DashboardVersionError) {
throw err;
}
@ -634,15 +600,14 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
uid,
route,
urlFolderUid,
params,
}: LoadDashboardOptions): Promise<DashboardWithAccessInfo<DashboardV2Spec> | null> {
const cacheKey = route === DashboardRoutes.Home ? HOME_DASHBOARD_CACHE_KEY : uid;
if (!params) {
const cachedDashboard = this.getDashboardFromCache(cacheKey);
if (cachedDashboard) {
return cachedDashboard;
}
const cachedDashboard = this.getDashboardFromCache(cacheKey);
if (cachedDashboard) {
return cachedDashboard;
}
let rsp: DashboardWithAccessInfo<DashboardV2Spec>;
try {
switch (route) {
@ -656,16 +621,8 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
return await this.dashboardLoader.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 this.dashboardLoader.loadDashboard(type || 'db', slug || '', uid, queryParams);
rsp = await this.dashboardLoader.loadDashboard(type || 'db', slug || '', uid);
if (route === DashboardRoutes.Embedded) {
throw new Error('Method not implemented.');
// rsp.meta.isEmbedded = true;
@ -699,30 +656,20 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
return rsp;
}
public async reloadDashboard(params: LoadDashboardOptions['params']): Promise<void> {
const stateOptions = this.state.options;
public async reloadDashboard(queryParams: UrlQueryMap): Promise<void> {
const dashboard = this.state.dashboard;
if (!stateOptions) {
if (!dashboard || !dashboard.state.uid) {
return;
}
const options = {
...stateOptions,
params,
};
if (
isEqual(options.params?.variables, stateOptions.params?.variables) &&
isEqual(options.params?.scopes, stateOptions.params?.scopes)
) {
return;
}
const uid = dashboard.state.uid;
try {
this.setState({ isLoading: true });
const rsp = await this.fetchDashboard(options);
const fromCache = this.getSceneFromCache(options.uid);
const rsp = await this.dashboardLoader.loadDashboard('db', dashboard.state.meta.slug, uid, queryParams);
const fromCache = this.getSceneFromCache(uid);
if (
fromCache &&
@ -750,13 +697,13 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
const scene = transformSaveModelSchemaV2ToScene(rsp);
// we need to call and restore dashboard state on every reload that pulls a new dashboard version
if (config.featureToggles.preserveDashboardStateWhenNavigating && Boolean(options.uid)) {
if (config.featureToggles.preserveDashboardStateWhenNavigating && Boolean(uid)) {
restoreDashboardStateFromLocalStorage(scene);
}
this.setSceneCache(options.uid, scene);
this.setSceneCache(uid, scene);
this.setState({ dashboard: scene, isLoading: false, options });
this.setState({ dashboard: scene, isLoading: false });
} catch (err) {
const status = getStatusFromError(err);
const message = getMessageFromError(err);
@ -815,8 +762,8 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta
);
}
public async reloadDashboard(params: LoadDashboardOptions['params']) {
return this.withVersionHandling((manager) => manager.reloadDashboard.call(this, params));
public async reloadDashboard(queryParams: UrlQueryMap) {
return this.withVersionHandling((manager) => manager.reloadDashboard.call(this, queryParams));
}
public getDashboardFromCache(uid: string) {
@ -864,6 +811,14 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta
this.cache = {};
}
public getSceneFromCache(key: string) {
return this.activeManager.getSceneFromCache(key);
}
public setSceneCache(cacheKey: string, scene: DashboardScene): void {
this.activeManager.setSceneCache(cacheKey, scene);
}
public getCache() {
return this.activeManager.getCache();
}

@ -5,9 +5,11 @@ import {
sceneGraph,
SceneObjectBase,
SceneObjectState,
SceneTimeRangeLike,
sceneUtils,
SceneVariable,
VariableDependencyConfig,
} from '@grafana/scenes';
import { createLogger } from '@grafana/ui';
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager';
@ -16,12 +18,12 @@ import { DashboardScene } from './DashboardScene';
export interface DashboardReloadBehaviorState extends SceneObjectState {
reloadOnParamsChange?: boolean;
uid?: string;
version?: number;
}
export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBehaviorState> {
private _timeRange: SceneTimeRangeLike | undefined;
private _dashboardScene: DashboardScene | undefined;
private _prevState?: UrlQueryMap;
private _log = createLogger('DashboardReloadBehavior');
constructor(state: DashboardReloadBehaviorState) {
super(state);
@ -37,61 +39,70 @@ export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBeha
return;
}
this._timeRange = sceneGraph.getTimeRange(this);
this._dashboardScene = sceneGraph.getAncestor(this, DashboardScene);
this._variableDependency = new VariableDependencyConfig(this, {
onAnyVariableChanged: this.reloadDashboard,
onAnyVariableChanged: (variable: SceneVariable) => {
this._log.logger('onAnyVariableChanged', variable.state.name, JSON.stringify(variable.getValue()));
this.reloadDashboard();
},
dependsOnScopes: true,
});
this._subs.add(
this._timeRange.subscribeToState((newState, prevState) => {
if (!isEqual(newState.value, prevState.value)) {
this.reloadDashboard();
}
})
);
this.reloadDashboard();
});
}
private getCurrentState(): UrlQueryMap {
const scopes = sceneGraph.getScopes(this) ?? [];
const timeRange = sceneGraph.getTimeRange(this).state.value;
return {
scopes: scopes.map((scope) => scope.metadata.name),
from: timeRange.from.toISOString(),
to: timeRange.to.toISOString(),
...sceneUtils.getUrlState(this._dashboardScene?.state.$variables!),
version: this._dashboardScene?.state.version,
};
}
private isEditing() {
return !!this._dashboardScene?.state.isEditing;
}
private isWaitingForVariables() {
const varSet = sceneGraph.getVariables(this.parent!);
return varSet.state.variables.some((variable) => varSet.isVariableLoadingOrWaitingToUpdate(variable));
}
private reloadDashboard() {
if (this.isEditing() || this.isWaitingForVariables()) {
this._log.logger('DashboardReloadBehavior reloadDashboard isEditing or waiting for variables, skipping reload');
return;
}
const newState = this.getCurrentState();
const prevState = this._prevState ?? {};
// Ignore time range changes for now
prevState.from = newState.from;
prevState.to = newState.to;
const stateChanged = !isEqual(newState, this._prevState);
this._log.logger(
`DashboardReloadBehavior reloadDashboard stateChanged ${stateChanged ? 'true' : 'false'}`,
this._prevState,
newState
);
if (!stateChanged) {
return;
}
this._prevState = newState;
// This is wrapped in setTimeout in order to allow variables and scopes to be set in the URL before actually reloading the dashboard
setTimeout(() => {
const scopes = sceneGraph.getScopes(this) ?? [];
getDashboardScenePageStateManager().reloadDashboard({
version: this.state.version!,
scopes: scopes.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: this._timeRange!.state.value.from.toISOString(),
to: this._timeRange!.state.value.to.toISOString(),
},
variables: sceneGraph.getVariables(this).state.variables.reduce<UrlQueryMap>(
(acc, variable) => ({
...acc,
...variable.urlSync?.getUrlState(),
}),
{}
),
});
getDashboardScenePageStateManager().reloadDashboard(newState);
});
}
}

@ -199,7 +199,6 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
new DashboardReloadBehavior({
reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && false,
uid: dashboardId?.toString(),
version: 1,
}),
],
$data: new DashboardDataLayerSet({

@ -308,7 +308,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
new DashboardReloadBehavior({
reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange,
uid,
version: oldModel.version,
}),
];

@ -78,6 +78,7 @@ export class ScopesService implements ScopesContextValue {
}
const queryParams = new URLSearchParams(location.search);
const scopes = queryParams.getAll('scopes');
//const scopesFromState = this.state.value.map((scope) => scope.metadata.name);
if (scopes.length) {
// We only update scopes but never delete them. This is to keep the scopes in memory if user navigates to
// page that does not use scopes (like from dashboard to dashboard list back to dashboard). If user
@ -93,12 +94,7 @@ export class ScopesService implements ScopesContextValue {
const oldScopeNames = prev.selectedScopes.map((scope) => scope.scope.metadata.name);
const newScopeNames = state.selectedScopes.map((scope) => scope.scope.metadata.name);
if (!isEqual(oldScopeNames, newScopeNames)) {
this.locationService.partial(
{
scopes: newScopeNames,
},
true
);
this.locationService.partial({ scopes: newScopeNames }, true);
}
})
);

@ -212,7 +212,10 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
* before for example by toggling the scopes in the scoped tree UI.
*/
private setNewScopes = async (treeScopes = this.state.treeScopes) => {
if (isEqual(treeScopes, getTreeScopesFromSelectedScopes(this.state.selectedScopes))) {
const newNames = treeScopes.map(({ scopeName }) => scopeName);
const currentNames = this.state.selectedScopes.map((scope) => scope.scope.metadata.name);
if (isEqual(newNames, currentNames)) {
return;
}

@ -17,7 +17,9 @@ jest.mock('@grafana/runtime', () => ({
describe('Dashboard reload', () => {
let dashboardReloadSpy: jest.SpyInstance;
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation();
});
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.groupByVariable = true;

@ -3443,11 +3443,11 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes-react@npm:^6.12.0":
version: 6.16.0
resolution: "@grafana/scenes-react@npm:6.16.0"
"@grafana/scenes-react@npm:^6.18.0":
version: 6.18.0
resolution: "@grafana/scenes-react@npm:6.18.0"
dependencies:
"@grafana/scenes": "npm:6.16.0"
"@grafana/scenes": "npm:6.18.0"
lru-cache: "npm:^10.2.2"
react-use: "npm:^17.4.0"
peerDependencies:
@ -3459,13 +3459,13 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/a1a7824b1c8f05fd5149d93a5aa0037014d5ff3e777669d5212c2426812973510dbeea225cb4f5d5e5a9038910a590d6d170b5ebe681c5be6e2fd951a3fabc07
checksum: 10/7173458c5a4c128f3026fc3ad5b7cb135cd4a99340939c2c6f3081b2f8d7028347f27ba79259c0f8aa2315f59abc5cccdd2110b3a343b2f4c2ab576236dc60e4
languageName: node
linkType: hard
"@grafana/scenes@npm:6.16.0, @grafana/scenes@npm:^6.12.0":
version: 6.16.0
resolution: "@grafana/scenes@npm:6.16.0"
"@grafana/scenes@npm:6.18.0, @grafana/scenes@npm:^6.18.0":
version: 6.18.0
resolution: "@grafana/scenes@npm:6.18.0"
dependencies:
"@floating-ui/react": "npm:^0.26.16"
"@leeoniya/ufuzzy": "npm:^1.0.16"
@ -3483,7 +3483,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/e379ba53fc835b6b3a6eb19d7527a6e775c10684d4eed650f85e5ad8c3fdb682fe20428c2ea467e66fc9050b19b19eb46ebed2ef5253dd1c3f6a58fd04d42205
checksum: 10/9363488c0a5f57745497a5887ec50a3fdee7c93a3b40c83e000470bd60b437d9614d1cf2d62209f1415e6c58432f2d2dc1287c1c762381582fc21b986722656b
languageName: node
linkType: hard
@ -17777,8 +17777,8 @@ __metadata:
"@grafana/plugin-ui": "npm:0.10.6"
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/scenes": "npm:^6.12.0"
"@grafana/scenes-react": "npm:^6.12.0"
"@grafana/scenes": "npm:^6.18.0"
"@grafana/scenes-react": "npm:^6.18.0"
"@grafana/schema": "workspace:*"
"@grafana/sql": "workspace:*"
"@grafana/test-utils": "workspace:*"

Loading…
Cancel
Save