From af6c536c22703bd8711b5c1f13ec201e66ede220 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Wed, 9 Apr 2025 16:02:25 +0200 Subject: [PATCH] Dashboards: Implement reloadDashboard for schema v2 (#103612) --- .../DashboardScenePageStateManager.test.ts | 217 +++++++++++++++++- .../pages/DashboardScenePageStateManager.ts | 72 +++++- 2 files changed, 274 insertions(+), 15 deletions(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 7bdaaee666a..40f058b6d0e 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -8,6 +8,11 @@ import { import store from 'app/core/store'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardVersionError, DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; +import { + DashboardLoaderSrv, + DashboardLoaderSrvV2, + setDashboardLoaderSrv, +} from 'app/features/dashboard/services/DashboardLoaderSrv'; import { getDashboardSnapshotSrv } from 'app/features/dashboard/services/SnapshotSrv'; import { DASHBOARD_FROM_LS_KEY, DashboardDataDTO, DashboardDTO, DashboardRoutes } from 'app/types'; @@ -16,9 +21,9 @@ import { setupLoadDashboardMock, setupLoadDashboardMockReject } from '../utils/t import { DashboardScenePageStateManager, + DashboardScenePageStateManagerV2, UnifiedDashboardScenePageStateManager, DASHBOARD_CACHE_TTL, - DashboardScenePageStateManagerV2, } from './DashboardScenePageStateManager'; jest.mock('app/features/dashboard/api/dashboard_api', () => ({ @@ -60,6 +65,22 @@ const setupV1FailureV2Success = ( return getDashSpy; }; +// Mock the DashboardLoaderSrv +const mockDashboardLoader = { + loadDashboard: jest.fn(), + loadSnapshot: jest.fn(), +}; + +// Set the mock loader +setDashboardLoaderSrv(mockDashboardLoader as unknown as DashboardLoaderSrv); + +// Reset the mock between tests +beforeEach(() => { + jest.clearAllMocks(); + mockDashboardLoader.loadDashboard.mockReset(); + mockDashboardLoader.loadSnapshot.mockReset(); +}); + describe('DashboardScenePageStateManager v1', () => { afterEach(() => { store.delete(DASHBOARD_FROM_LS_KEY); @@ -585,6 +606,170 @@ describe('DashboardScenePageStateManager v2', () => { expect(getDashSpy).toHaveBeenCalledTimes(2); }); }); + + describe('reloadDashboard', () => { + it('should reload dashboard with updated parameters', async () => { + const getDashSpy = jest.fn(); + setupDashboardAPI( + { + access: {}, + apiVersion: 'v2alpha1', + kind: 'DashboardWithAccessInfo', + metadata: { + name: 'fake-dash', + creationTimestamp: '', + resourceVersion: '1', + generation: 1, + }, + spec: { ...defaultDashboardV2Spec() }, + }, + getDashSpy + ); + + const loader = new DashboardScenePageStateManagerV2({}); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); + + expect(getDashSpy).toHaveBeenCalledTimes(1); + expect(loader.state.dashboard).toBeDefined(); + + // Setup a new response with updated generation + setupDashboardAPI( + { + access: {}, + apiVersion: 'v2alpha1', + kind: 'DashboardWithAccessInfo', + metadata: { + name: 'fake-dash', + creationTimestamp: '', + resourceVersion: '2', + generation: 2, + }, + spec: { ...defaultDashboardV2Spec() }, + }, + getDashSpy + ); + + const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} }; + await loader.reloadDashboard(options); + + expect(getDashSpy).toHaveBeenCalledTimes(2); + expect(loader.state.dashboard?.state.version).toBe(2); + }); + + it('should not reload dashboard if parameters are the same', async () => { + const getDashSpy = jest.fn(); + setupDashboardAPI( + { + access: {}, + apiVersion: 'v2alpha1', + kind: 'DashboardWithAccessInfo', + metadata: { + name: 'fake-dash', + creationTimestamp: '', + resourceVersion: '1', + generation: 1, + }, + spec: { ...defaultDashboardV2Spec() }, + }, + getDashSpy + ); + + const loader = new DashboardScenePageStateManagerV2({}); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); + + expect(getDashSpy).toHaveBeenCalledTimes(1); + const initialDashboard = loader.state.dashboard; + + const mockDashboard: DashboardWithAccessInfo = { + 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); + + expect(fetchDashboardSpy).toHaveBeenCalledTimes(1); + expect(loader.state.dashboard).toBe(initialDashboard); + + fetchDashboardSpy.mockRestore(); + }); + + it('should handle errors during reload', async () => { + const getDashSpy = jest.fn(); + setupDashboardAPI( + { + access: {}, + apiVersion: 'v2alpha1', + kind: 'DashboardWithAccessInfo', + metadata: { + name: 'fake-dash', + creationTimestamp: '', + resourceVersion: '1', + generation: 1, + }, + spec: { ...defaultDashboardV2Spec() }, + }, + getDashSpy + ); + + const loader = new DashboardScenePageStateManagerV2({}); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); + + const mockLoader = { + loadDashboard: jest.fn().mockRejectedValue(new Error('Failed to load dashboard')), + }; + + loader['dashboardLoader'] = mockLoader as unknown as DashboardLoaderSrvV2; + + const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} }; + await loader.reloadDashboard(options); + + expect(loader.state.loadError).toBeDefined(); + expect(loader.state.loadError?.message).toBe('Failed to load dashboard'); + }); + + it('should handle DashboardVersionError during reload', async () => { + const getDashSpy = jest.fn(); + setupDashboardAPI( + { + access: {}, + apiVersion: 'v2alpha1', + kind: 'DashboardWithAccessInfo', + metadata: { + name: 'fake-dash', + creationTimestamp: '', + resourceVersion: '1', + generation: 1, + }, + spec: { ...defaultDashboardV2Spec() }, + }, + getDashSpy + ); + + const loader = new DashboardScenePageStateManagerV2({}); + await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); + + const mockLoader = { + loadDashboard: jest.fn().mockRejectedValue(new DashboardVersionError('v2alpha1')), + }; + + loader['dashboardLoader'] = mockLoader as unknown as DashboardLoaderSrvV2; + + const options = { version: 2, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} }; + + await expect(loader.reloadDashboard(options)).rejects.toThrow(DashboardVersionError); + }); + }); }); }); @@ -639,10 +824,32 @@ describe('UnifiedDashboardScenePageStateManager', () => { await manager.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2); - // Reload for v2 is not supported yet - await expect( - manager.reloadDashboard({ version: 1, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} }) - ).rejects.toThrow('Method not implemented.'); + // Reload should now work with v2 manager + const options = { version: 1, scopes: [], timeRange: { from: 'now-1h', to: 'now' }, variables: {} }; + + // Mock the fetchDashboard method to return a dashboard + const v2Manager = manager['activeManager'] as DashboardScenePageStateManagerV2; + const originalFetchDashboard = v2Manager.fetchDashboard; + v2Manager.fetchDashboard = jest.fn().mockResolvedValue({ + access: {}, + apiVersion: 'v2alpha1', + kind: 'DashboardWithAccessInfo', + metadata: { + name: 'fake-dash', + creationTimestamp: '', + resourceVersion: '2', + generation: 2, + }, + spec: { ...defaultDashboardV2Spec() }, + }); + + await manager.reloadDashboard(options); + + // Restore the original method + v2Manager.fetchDashboard = originalFetchDashboard; + + // Verify that the dashboard was reloaded + expect(manager.state.dashboard).toBeDefined(); }); it('should transform responses correctly based on dashboard version', async () => { diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index e331893052a..33b8661681d 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -425,10 +425,6 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag params, }; - // 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) @@ -473,8 +469,6 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag status, }, }); - // If the error is a DashboardVersionError, we want to throw it so that the error boundary is triggered - // This enables us to switch to the correct version of the dashboard if (err instanceof DashboardVersionError) { throw err; } @@ -522,10 +516,6 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan throw new Error('Dashboard not found'); } - reloadDashboard(params: LoadDashboardOptions['params']): Promise { - throw new Error('Method not implemented.'); - } - public async fetchDashboard({ type, slug, @@ -593,6 +583,68 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan } return rsp; } + + public async reloadDashboard(params: LoadDashboardOptions['params']): Promise { + const stateOptions = this.state.options; + + if (!stateOptions) { + return; + } + + const options = { + ...stateOptions, + params, + }; + + if ( + isEqual(options.params?.variables, stateOptions.params?.variables) && + isEqual(options.params?.scopes, stateOptions.params?.scopes) + ) { + return; + } + + try { + this.setState({ isLoading: true }); + + const rsp = await this.fetchDashboard(options); + const fromCache = this.getSceneFromCache(options.uid); + + if (fromCache && fromCache.state.version === rsp?.metadata.generation) { + this.setState({ isLoading: false }); + return; + } + + if (!rsp?.spec) { + this.setState({ + isLoading: false, + loadError: { + status: 404, + message: 'Dashboard not found', + }, + }); + return; + } + + const scene = transformSaveModelSchemaV2ToScene(rsp); + + this.setSceneCache(options.uid, scene); + + this.setState({ dashboard: scene, isLoading: false, options }); + } catch (err) { + const status = getStatusFromError(err); + const message = getMessageFromError(err); + this.setState({ + isLoading: false, + loadError: { + message, + status, + }, + }); + if (err instanceof DashboardVersionError) { + throw err; + } + } + } } export class UnifiedDashboardScenePageStateManager extends DashboardScenePageStateManagerBase<