Dashboards: Implement reloadDashboard for schema v2 (#103612)

pull/104489/head
Ivan Ortega Alba 1 month ago committed by GitHub
parent 890484ff6a
commit af6c536c22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 217
      public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts
  2. 72
      public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.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<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);
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 () => {

@ -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<void> {
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<void> {
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<

Loading…
Cancel
Save