Skip to content

Commit af6c536

Browse files
Dashboards: Implement reloadDashboard for schema v2 (#103612)
1 parent 890484f commit af6c536

File tree

2 files changed

+274
-15
lines changed

2 files changed

+274
-15
lines changed

public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts

Lines changed: 212 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import {
88
import store from 'app/core/store';
99
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
1010
import { DashboardVersionError, DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
11+
import {
12+
DashboardLoaderSrv,
13+
DashboardLoaderSrvV2,
14+
setDashboardLoaderSrv,
15+
} from 'app/features/dashboard/services/DashboardLoaderSrv';
1116
import { getDashboardSnapshotSrv } from 'app/features/dashboard/services/SnapshotSrv';
1217
import { DASHBOARD_FROM_LS_KEY, DashboardDataDTO, DashboardDTO, DashboardRoutes } from 'app/types';
1318

@@ -16,9 +21,9 @@ import { setupLoadDashboardMock, setupLoadDashboardMockReject } from '../utils/t
1621

1722
import {
1823
DashboardScenePageStateManager,
24+
DashboardScenePageStateManagerV2,
1925
UnifiedDashboardScenePageStateManager,
2026
DASHBOARD_CACHE_TTL,
21-
DashboardScenePageStateManagerV2,
2227
} from './DashboardScenePageStateManager';
2328

2429
jest.mock('app/features/dashboard/api/dashboard_api', () => ({
@@ -60,6 +65,22 @@ const setupV1FailureV2Success = (
6065
return getDashSpy;
6166
};
6267

68+
// Mock the DashboardLoaderSrv
69+
const mockDashboardLoader = {
70+
loadDashboard: jest.fn(),
71+
loadSnapshot: jest.fn(),
72+
};
73+
74+
// Set the mock loader
75+
setDashboardLoaderSrv(mockDashboardLoader as unknown as DashboardLoaderSrv);
76+
77+
// Reset the mock between tests
78+
beforeEach(() => {
79+
jest.clearAllMocks();
80+
mockDashboardLoader.loadDashboard.mockReset();
81+
mockDashboardLoader.loadSnapshot.mockReset();
82+
});
83+
6384
describe('DashboardScenePageStateManager v1', () => {
6485
afterEach(() => {
6586
store.delete(DASHBOARD_FROM_LS_KEY);
@@ -585,6 +606,170 @@ describe('DashboardScenePageStateManager v2', () => {
585606
expect(getDashSpy).toHaveBeenCalledTimes(2);
586607
});
587608
});
609+
610+
describe('reloadDashboard', () => {
611+
it('should reload dashboard with updated parameters', async () => {
612+
const getDashSpy = jest.fn();
613+
setupDashboardAPI(
614+
{
615+
access: {},
616+
apiVersion: 'v2alpha1',
617+
kind: 'DashboardWithAccessInfo',
618+
metadata: {
619+
name: 'fake-dash',
620+
creationTimestamp: '',
621+
resourceVersion: '1',
622+
generation: 1,
623+
},
624+
spec: { ...defaultDashboardV2Spec() },
625+
},
626+
getDashSpy
627+
);
628+
629+
const loader = new DashboardScenePageStateManagerV2({});
630+
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
631+
632+
expect(getDashSpy).toHaveBeenCalledTimes(1);
633+
expect(loader.state.dashboard).toBeDefined();
634+
635+
// Setup a new response with updated generation
636+
setupDashboardAPI(
637+
{
638+
access: {},
639+
apiVersion: 'v2alpha1',
640+
kind: 'DashboardWithAccessInfo',
641+
metadata: {
642+
name: 'fake-dash',
643+
creationTimestamp: '',
644+
resourceVersion: '2',
645+
generation: 2,
646+
},
647+
spec: { ...defaultDashboardV2Spec() },
648+
},
649+
getDashSpy
650+
);
651+
652+
const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
653+
await loader.reloadDashboard(options);
654+
655+
expect(getDashSpy).toHaveBeenCalledTimes(2);
656+
expect(loader.state.dashboard?.state.version).toBe(2);
657+
});
658+
659+
it('should not reload dashboard if parameters are the same', async () => {
660+
const getDashSpy = jest.fn();
661+
setupDashboardAPI(
662+
{
663+
access: {},
664+
apiVersion: 'v2alpha1',
665+
kind: 'DashboardWithAccessInfo',
666+
metadata: {
667+
name: 'fake-dash',
668+
creationTimestamp: '',
669+
resourceVersion: '1',
670+
generation: 1,
671+
},
672+
spec: { ...defaultDashboardV2Spec() },
673+
},
674+
getDashSpy
675+
);
676+
677+
const loader = new DashboardScenePageStateManagerV2({});
678+
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
679+
680+
expect(getDashSpy).toHaveBeenCalledTimes(1);
681+
const initialDashboard = loader.state.dashboard;
682+
683+
const mockDashboard: DashboardWithAccessInfo<DashboardV2Spec> = {
684+
access: {},
685+
apiVersion: 'v2alpha1',
686+
kind: 'DashboardWithAccessInfo',
687+
metadata: {
688+
name: 'fake-dash',
689+
creationTimestamp: '',
690+
resourceVersion: '1',
691+
generation: 1,
692+
},
693+
spec: { ...defaultDashboardV2Spec() },
694+
};
695+
696+
const fetchDashboardSpy = jest.spyOn(loader, 'fetchDashboard').mockResolvedValue(mockDashboard);
697+
698+
const options = { version: 1, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
699+
await loader.reloadDashboard(options);
700+
701+
expect(fetchDashboardSpy).toHaveBeenCalledTimes(1);
702+
expect(loader.state.dashboard).toBe(initialDashboard);
703+
704+
fetchDashboardSpy.mockRestore();
705+
});
706+
707+
it('should handle errors during reload', async () => {
708+
const getDashSpy = jest.fn();
709+
setupDashboardAPI(
710+
{
711+
access: {},
712+
apiVersion: 'v2alpha1',
713+
kind: 'DashboardWithAccessInfo',
714+
metadata: {
715+
name: 'fake-dash',
716+
creationTimestamp: '',
717+
resourceVersion: '1',
718+
generation: 1,
719+
},
720+
spec: { ...defaultDashboardV2Spec() },
721+
},
722+
getDashSpy
723+
);
724+
725+
const loader = new DashboardScenePageStateManagerV2({});
726+
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
727+
728+
const mockLoader = {
729+
loadDashboard: jest.fn().mockRejectedValue(new Error('Failed to load dashboard')),
730+
};
731+
732+
loader['dashboardLoader'] = mockLoader as unknown as DashboardLoaderSrvV2;
733+
734+
const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
735+
await loader.reloadDashboard(options);
736+
737+
expect(loader.state.loadError).toBeDefined();
738+
expect(loader.state.loadError?.message).toBe('Failed to load dashboard');
739+
});
740+
741+
it('should handle DashboardVersionError during reload', async () => {
742+
const getDashSpy = jest.fn();
743+
setupDashboardAPI(
744+
{
745+
access: {},
746+
apiVersion: 'v2alpha1',
747+
kind: 'DashboardWithAccessInfo',
748+
metadata: {
749+
name: 'fake-dash',
750+
creationTimestamp: '',
751+
resourceVersion: '1',
752+
generation: 1,
753+
},
754+
spec: { ...defaultDashboardV2Spec() },
755+
},
756+
getDashSpy
757+
);
758+
759+
const loader = new DashboardScenePageStateManagerV2({});
760+
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
761+
762+
const mockLoader = {
763+
loadDashboard: jest.fn().mockRejectedValue(new DashboardVersionError('v2alpha1')),
764+
};
765+
766+
loader['dashboardLoader'] = mockLoader as unknown as DashboardLoaderSrvV2;
767+
768+
const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
769+
770+
await expect(loader.reloadDashboard(options)).rejects.toThrow(DashboardVersionError);
771+
});
772+
});
588773
});
589774
});
590775

@@ -639,10 +824,32 @@ describe('UnifiedDashboardScenePageStateManager', () => {
639824
await manager.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
640825
expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2);
641826

642-
// Reload for v2 is not supported yet
643-
await expect(
644-
manager.reloadDashboard({ version: 1, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} })
645-
).rejects.toThrow('Method not implemented.');
827+
// Reload should now work with v2 manager
828+
const options = { version: 1, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} };
829+
830+
// Mock the fetchDashboard method to return a dashboard
831+
const v2Manager = manager['activeManager'] as DashboardScenePageStateManagerV2;
832+
const originalFetchDashboard = v2Manager.fetchDashboard;
833+
v2Manager.fetchDashboard = jest.fn().mockResolvedValue({
834+
access: {},
835+
apiVersion: 'v2alpha1',
836+
kind: 'DashboardWithAccessInfo',
837+
metadata: {
838+
name: 'fake-dash',
839+
creationTimestamp: '',
840+
resourceVersion: '2',
841+
generation: 2,
842+
},
843+
spec: { ...defaultDashboardV2Spec() },
844+
});
845+
846+
await manager.reloadDashboard(options);
847+
848+
// Restore the original method
849+
v2Manager.fetchDashboard = originalFetchDashboard;
850+
851+
// Verify that the dashboard was reloaded
852+
expect(manager.state.dashboard).toBeDefined();
646853
});
647854

648855
it('should transform responses correctly based on dashboard version', async () => {

public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -425,10 +425,6 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
425425
params,
426426
};
427427

428-
// We shouldn't check all params since:
429-
// - version doesn't impact the new dashboard, and it's there for increased compatibility
430-
// - time range is almost always different for relative time ranges and absolute time ranges do not trigger subsequent reloads
431-
// - other params don't affect the dashboard content
432428
if (
433429
isEqual(options.params?.variables, stateOptions.params?.variables) &&
434430
isEqual(options.params?.scopes, stateOptions.params?.scopes)
@@ -473,8 +469,6 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
473469
status,
474470
},
475471
});
476-
// If the error is a DashboardVersionError, we want to throw it so that the error boundary is triggered
477-
// This enables us to switch to the correct version of the dashboard
478472
if (err instanceof DashboardVersionError) {
479473
throw err;
480474
}
@@ -522,10 +516,6 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
522516
throw new Error('Dashboard not found');
523517
}
524518

525-
reloadDashboard(params: LoadDashboardOptions['params']): Promise<void> {
526-
throw new Error('Method not implemented.');
527-
}
528-
529519
public async fetchDashboard({
530520
type,
531521
slug,
@@ -593,6 +583,68 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
593583
}
594584
return rsp;
595585
}
586+
587+
public async reloadDashboard(params: LoadDashboardOptions['params']): Promise<void> {
588+
const stateOptions = this.state.options;
589+
590+
if (!stateOptions) {
591+
return;
592+
}
593+
594+
const options = {
595+
...stateOptions,
596+
params,
597+
};
598+
599+
if (
600+
isEqual(options.params?.variables, stateOptions.params?.variables) &&
601+
isEqual(options.params?.scopes, stateOptions.params?.scopes)
602+
) {
603+
return;
604+
}
605+
606+
try {
607+
this.setState({ isLoading: true });
608+
609+
const rsp = await this.fetchDashboard(options);
610+
const fromCache = this.getSceneFromCache(options.uid);
611+
612+
if (fromCache && fromCache.state.version === rsp?.metadata.generation) {
613+
this.setState({ isLoading: false });
614+
return;
615+
}
616+
617+
if (!rsp?.spec) {
618+
this.setState({
619+
isLoading: false,
620+
loadError: {
621+
status: 404,
622+
message: 'Dashboard not found',
623+
},
624+
});
625+
return;
626+
}
627+
628+
const scene = transformSaveModelSchemaV2ToScene(rsp);
629+
630+
this.setSceneCache(options.uid, scene);
631+
632+
this.setState({ dashboard: scene, isLoading: false, options });
633+
} catch (err) {
634+
const status = getStatusFromError(err);
635+
const message = getMessageFromError(err);
636+
this.setState({
637+
isLoading: false,
638+
loadError: {
639+
message,
640+
status,
641+
},
642+
});
643+
if (err instanceof DashboardVersionError) {
644+
throw err;
645+
}
646+
}
647+
}
596648
}
597649

598650
export class UnifiedDashboardScenePageStateManager extends DashboardScenePageStateManagerBase<

0 commit comments

Comments
 (0)