diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 64d850e9047..d885e17fdda 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -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', + }, + }, + }, + ], + }, + }, +}; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 8989681da8d..161ee48f10c 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -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 { fetchDashboard(options: LoadDashboardOptions): Promise; getDashboardFromCache(cacheKey: string): T | null; @@ -167,6 +178,11 @@ abstract class DashboardScenePageStateManagerBase private async loadScene(options: LoadDashboardOptions): Promise { 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('/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('/api/dashboards/home'); + const dto = await getBackendSrv().get('/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); diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 1831507291b..535265f606d 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -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 = { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 4ee7261cf02..23b36244519 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -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('/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; diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index fb4eb3fd962..51478ac3829 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -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; +}