Schema V2: Support v2 custom home dashboards (#99748)

This commit is contained in:
Dominik Prokop
2025-01-31 10:36:56 +01:00
committed by GitHub
parent bda4deb20c
commit bfdd00665b
5 changed files with 186 additions and 36 deletions
@@ -154,6 +154,22 @@ describe('DashboardScenePageStateManager v1', () => {
status: 500,
});
});
it('should throw when v2 custom home dashboard is provided', async () => {
setBackendSrv({
get: () => Promise.resolve({ dashboard: customHomeDashboardV2Spec, meta: {} }),
} as unknown as BackendSrv);
const loader = new DashboardScenePageStateManager({});
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
expect(loader.state.dashboard).toBeUndefined();
expect(loader.state.loadError).toEqual({
message: 'v2 dashboard spec is not supported. Enable useV2DashboardsAPI feature toggle',
messageId: undefined,
status: undefined,
});
});
});
describe('New dashboards', () => {
@@ -423,13 +439,12 @@ describe('DashboardScenePageStateManager v2', () => {
});
describe('Home dashboard', () => {
// TODO: Unskip when redirect is implemented in v2 API
it.skip('should handle home dashboard redirect', async () => {
it('should handle home dashboard redirect', async () => {
setBackendSrv({
get: () => Promise.resolve({ redirectUri: '/d/asd' }),
} as unknown as BackendSrv);
const loader = new DashboardScenePageStateManager({});
const loader = new DashboardScenePageStateManagerV2({});
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
expect(loader.state.dashboard).toBeUndefined();
@@ -455,6 +470,45 @@ describe('DashboardScenePageStateManager v2', () => {
status: 500,
});
});
it('should not transform v2 custom home dashboard spec', async () => {
setBackendSrv({
get: () =>
Promise.resolve({
dashboard: customHomeDashboardV2Spec,
meta: {
canSave: false,
canEdit: true,
canAdmin: false,
canStar: false,
canDelete: false,
slug: '',
url: '',
expires: '0001-01-01T00:00:00Z',
created: '0001-01-01T00:00:00Z',
updated: '0001-01-01T00:00:00Z',
updatedBy: '',
createdBy: '',
version: 0,
hasAcl: false,
isFolder: false,
folderId: 0,
folderUid: '',
folderTitle: 'General',
folderUrl: '',
provisioned: false,
provisionedExternalId: '',
annotationsPermissions: null,
},
}),
} as unknown as BackendSrv);
const loader = new DashboardScenePageStateManagerV2({});
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
expect(loader.state.dashboard?.getInitialSaveModel()).toEqual(customHomeDashboardV2Spec);
expect(loader.state.loadError).toBeUndefined();
});
});
describe('New dashboards', () => {
@@ -593,3 +647,78 @@ describe('DashboardScenePageStateManager v2', () => {
});
});
});
const customHomeDashboardV2Spec = {
title: 'Home Dashboard v2 schema',
cursorSync: 'Off',
preload: false,
editable: true,
links: [],
tags: [],
timeSettings: {
timezone: 'browser',
from: 'now-6h',
to: 'now',
autoRefresh: '',
autoRefreshIntervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'],
quickRanges: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'],
hideTimepicker: false,
weekStart: '',
fiscalYearStartMonth: 0,
},
variables: [],
elements: {
text_panel: {
kind: 'Panel',
spec: {
id: 0,
title: 'Welcome',
description: 'Welcome to the home dashboard!',
links: [],
data: {
kind: 'QueryGroup',
spec: {
queries: [],
transformations: [],
queryOptions: {},
},
},
vizConfig: {
kind: 'text',
spec: {
pluginVersion: '',
options: {
mode: 'markdown',
content: '# Welcome to the home dashboard!\n\n## Example of v2 schema home dashboard',
},
fieldConfig: {
defaults: {},
overrides: [],
},
},
},
},
},
},
annotations: [],
layout: {
kind: 'GridLayout',
spec: {
items: [
{
kind: 'GridLayoutItem',
spec: {
x: 6,
y: 0,
width: 12,
height: 6,
element: {
kind: 'ElementReference',
name: 'text_panel',
},
},
},
],
},
},
};
@@ -10,11 +10,18 @@ import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
import { AnnoKeyFolder } from 'app/features/apiserver/types';
import { ResponseTransformers } from 'app/features/dashboard/api/ResponseTransformers';
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import { isDashboardV2Spec } from 'app/features/dashboard/api/utils';
import { dashboardLoaderSrv, DashboardLoaderSrvV2 } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor';
import { trackDashboardSceneLoaded } from 'app/features/dashboard/utils/tracking';
import { DashboardDTO, DashboardRoutes } from 'app/types';
import {
DashboardDataDTO,
DashboardDTO,
DashboardRoutes,
HomeDashboardRedirectDTO,
isRedirectResponse,
} from 'app/types';
import { PanelEditor } from '../panel-edit/PanelEditor';
import { DashboardScene } from '../scene/DashboardScene';
@@ -68,6 +75,10 @@ export interface LoadDashboardOptions {
};
}
export type HomeDashboardDTO = DashboardDTO & {
dashboard: DashboardDataDTO | DashboardV2Spec;
};
interface DashboardScenePageStateManagerLike<T> {
fetchDashboard(options: LoadDashboardOptions): Promise<T | null>;
getDashboardFromCache(cacheKey: string): T | null;
@@ -167,6 +178,11 @@ abstract class DashboardScenePageStateManagerBase<T>
private async loadScene(options: LoadDashboardOptions): Promise<DashboardScene | null> {
this.setState({ dashboard: undefined, isLoading: true });
const rsp = await this.fetchDashboard(options);
if (!rsp) {
return null;
}
return this.transformResponseToScene(rsp, options);
}
@@ -235,12 +251,6 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
return scene;
}
if (rsp?.redirectUri) {
const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri);
locationService.replace(newUrl);
return null;
}
throw new Error('Dashboard not found');
}
@@ -271,7 +281,7 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
}
}
let rsp: DashboardDTO;
let rsp: DashboardDTO | HomeDashboardRedirectDTO;
try {
switch (route) {
@@ -280,10 +290,16 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
break;
case DashboardRoutes.Home:
rsp = await getBackendSrv().get('/api/dashboards/home');
rsp = await getBackendSrv().get<HomeDashboardDTO | HomeDashboardRedirectDTO>('/api/dashboards/home');
if (rsp.redirectUri) {
return rsp;
if (isRedirectResponse(rsp)) {
const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri);
locationService.replace(newUrl);
return null;
}
if (isDashboardV2Spec(rsp.dashboard)) {
throw new Error('v2 dashboard spec is not supported. Enable useV2DashboardsAPI feature toggle');
}
if (rsp?.meta) {
@@ -453,13 +469,6 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
return scene;
}
// TOD)[schema v2]: Figure out redirect utl
// if (rsp?.redirectUri) {
// const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri);
// locationService.replace(newUrl);
// return null;
// }
throw new Error('Dashboard not found');
}
@@ -487,17 +496,25 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
rsp = await buildNewDashboardSaveModelV2(urlFolderUid);
break;
case DashboardRoutes.Home:
// throw new Error('Method not implemented.');
const dto = await getBackendSrv().get<DashboardDTO>('/api/dashboards/home');
const dto = await getBackendSrv().get<HomeDashboardDTO | HomeDashboardRedirectDTO>('/api/dashboards/home');
if (isRedirectResponse(dto)) {
const newUrl = locationUtil.stripBaseFromUrl(dto.redirectUri);
locationService.replace(newUrl);
return null;
}
rsp = ResponseTransformers.ensureV2Response(dto);
// if custom home dashboard is v2 spec already, ignore the spec transformation
if (isDashboardV2Spec(dto.dashboard)) {
rsp.spec = dto.dashboard;
}
rsp.access.canSave = false;
rsp.access.canShare = false;
rsp.access.canStar = false;
// if (rsp.redirectUri) {
// return rsp;
// }
break;
case DashboardRoutes.Public: {
return await this.dashboardLoader.loadDashboard('public', '', uid);
@@ -103,13 +103,8 @@ export function ensureV2Response(
if (isDashboardResource(dto)) {
accessMeta = dto.access;
annotationsMeta = {
[AnnoKeyCreatedBy]: dto.metadata.annotations?.[AnnoKeyCreatedBy],
[AnnoKeyUpdatedBy]: dto.metadata.annotations?.[AnnoKeyUpdatedBy],
[AnnoKeyUpdatedTimestamp]: dto.metadata.annotations?.[AnnoKeyUpdatedTimestamp],
[AnnoKeyFolder]: dto.metadata.annotations?.[AnnoKeyFolder],
[AnnoKeySlug]: dto.metadata.annotations?.[AnnoKeySlug],
...dto.metadata.annotations,
[AnnoKeyDashboardGnetId]: dashboard.gnetId ?? undefined,
[AnnoKeyDashboardIsSnapshot]: dto.metadata.annotations?.[AnnoKeyDashboardIsSnapshot],
};
creationTimestamp = dto.metadata.creationTimestamp;
labelsMeta = {
@@ -24,6 +24,8 @@ import {
DashboardDTO,
DashboardInitPhase,
DashboardRoutes,
HomeDashboardRedirectDTO,
isRedirectResponse,
StoreState,
ThunkDispatch,
ThunkResult,
@@ -69,10 +71,10 @@ async function fetchDashboard(
}
// load home dash
const dashDTO: DashboardDTO = await backendSrv.get('/api/dashboards/home');
const dashDTO = await backendSrv.get<DashboardDTO | HomeDashboardRedirectDTO>('/api/dashboards/home');
// if user specified a custom home dashboard redirect to that
if (dashDTO.redirectUri) {
if (isRedirectResponse(dashDTO)) {
const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri);
locationService.replace(newUrl);
return null;
+8 -1
View File
@@ -3,8 +3,11 @@ import { Dashboard, DataSourceRef } from '@grafana/schema';
import { ObjectMeta } from 'app/features/apiserver/types';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
export interface HomeDashboardRedirectDTO {
redirectUri: string;
}
export interface DashboardDTO {
redirectUri?: string;
dashboard: DashboardDataDTO;
meta: DashboardMeta;
}
@@ -140,3 +143,7 @@ export interface DashboardState {
}
export const DASHBOARD_FROM_LS_KEY = 'DASHBOARD_FROM_LS_KEY';
export function isRedirectResponse(dto: DashboardDTO | HomeDashboardRedirectDTO): dto is HomeDashboardRedirectDTO {
return 'redirectUri' in dto;
}