Schema V2: Handle v2 custom home dashboard (#103332)
* Schema V2: Handle v2 custom home dashboard * Clean interface * Fix old arch
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { advanceBy } from 'jest-date-mock';
|
||||
|
||||
import { BackendSrv, setBackendSrv } from '@grafana/runtime';
|
||||
import { BackendSrv, locationService, setBackendSrv } from '@grafana/runtime';
|
||||
import {
|
||||
Spec as DashboardV2Spec,
|
||||
defaultSpec as defaultDashboardV2Spec,
|
||||
@@ -142,56 +142,6 @@ describe('DashboardScenePageStateManager v1', () => {
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
describe('Home dashboard', () => {
|
||||
it('should handle home dashboard redirect', async () => {
|
||||
setBackendSrv({
|
||||
get: () => Promise.resolve({ redirectUri: '/d/asd' }),
|
||||
} as unknown as BackendSrv);
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
|
||||
|
||||
expect(loader.state.dashboard).toBeUndefined();
|
||||
expect(loader.state.loadError).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle invalid home dashboard request', async () => {
|
||||
setBackendSrv({
|
||||
get: () =>
|
||||
Promise.reject({
|
||||
status: 500,
|
||||
data: { message: 'Failed to load home dashboard' },
|
||||
}),
|
||||
} 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: 'Failed to load home dashboard',
|
||||
messageId: undefined,
|
||||
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: 'You are trying to load a v2 dashboard spec as v1. Use DashboardScenePageStateManagerV2 instead.',
|
||||
messageId: undefined,
|
||||
status: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('New dashboards', () => {
|
||||
it('Should have new empty model and should not be cached', async () => {
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
@@ -472,53 +422,6 @@ describe('DashboardScenePageStateManager v2', () => {
|
||||
status: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not transform v2 custom home dashboard spec', async () => {
|
||||
setBackendSrv({
|
||||
get: () => {
|
||||
return Promise.resolve({
|
||||
access: {
|
||||
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,
|
||||
},
|
||||
apiVersion: 'v2alpha1',
|
||||
kind: 'DashboardWithAccessInfo',
|
||||
metadata: {
|
||||
name: 'home',
|
||||
creationTimestamp: '',
|
||||
resourceVersion: '1',
|
||||
},
|
||||
spec: customHomeDashboardV2Spec,
|
||||
});
|
||||
},
|
||||
} 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', () => {
|
||||
@@ -794,8 +697,122 @@ describe('UnifiedDashboardScenePageStateManager', () => {
|
||||
expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Home dashboard', () => {
|
||||
it('should handle home dashboard redirect', async () => {
|
||||
setBackendSrv({
|
||||
get: () => Promise.resolve({ redirectUri: '/d/asd' }),
|
||||
} as unknown as BackendSrv);
|
||||
|
||||
const loader = new UnifiedDashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
|
||||
|
||||
expect(loader.state.dashboard).toBeUndefined();
|
||||
expect(loader.state.loadError).toBeUndefined();
|
||||
expect(locationService.getLocation().pathname).toBe('/d/asd');
|
||||
});
|
||||
|
||||
it('should handle invalid home dashboard request', async () => {
|
||||
setBackendSrv({
|
||||
get: () =>
|
||||
Promise.reject({
|
||||
status: 500,
|
||||
data: { message: 'Failed to load home dashboard' },
|
||||
}),
|
||||
} as unknown as BackendSrv);
|
||||
|
||||
const loader = new UnifiedDashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
|
||||
|
||||
expect(loader.state.dashboard).toBeUndefined();
|
||||
expect(loader.state.loadError).toEqual({
|
||||
message: 'Failed to load home dashboard',
|
||||
messageId: undefined,
|
||||
status: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle custom v1 home dashboard ', async () => {
|
||||
setBackendSrv({
|
||||
get: () => Promise.resolve({ dashboard: customHomeDashboardV1Spec, meta: {} }),
|
||||
} as unknown as BackendSrv);
|
||||
|
||||
const loader = new UnifiedDashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
|
||||
|
||||
expect(loader.state.dashboard).toBeDefined();
|
||||
expect(loader.state.dashboard!.serializer.initialSaveModel).toEqual(customHomeDashboardV1Spec);
|
||||
});
|
||||
|
||||
it('should transform v2 custom home dashboard to v1', async () => {
|
||||
setBackendSrv({
|
||||
get: () => Promise.resolve({ dashboard: customHomeDashboardV2Spec, meta: {} }),
|
||||
} as unknown as BackendSrv);
|
||||
|
||||
const loader = new UnifiedDashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
|
||||
|
||||
expect(loader.state.dashboard).toBeDefined();
|
||||
expect(loader.state.dashboard!.serializer.initialSaveModel).toEqual(customHomeDashboardV1Spec);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const customHomeDashboardV1Spec = {
|
||||
annotations: {
|
||||
list: [
|
||||
{
|
||||
builtIn: 1,
|
||||
datasource: {
|
||||
type: 'grafana',
|
||||
uid: '-- Grafana --',
|
||||
},
|
||||
enable: true,
|
||||
hide: true,
|
||||
iconColor: 'rgba(0, 211, 255, 1)',
|
||||
name: 'Annotations & Alerts',
|
||||
type: 'dashboard',
|
||||
},
|
||||
],
|
||||
},
|
||||
editable: true,
|
||||
fiscalYearStartMonth: 0,
|
||||
graphTooltip: 0,
|
||||
links: [],
|
||||
panels: [
|
||||
{
|
||||
description: 'Welcome to the home dashboard!',
|
||||
fieldConfig: { defaults: {}, overrides: [] },
|
||||
gridPos: { h: 6, w: 12, x: 6, y: 0 },
|
||||
id: 0,
|
||||
links: [],
|
||||
options: {
|
||||
content: '# Welcome to the home dashboard!\n\n## Example of v2 schema home dashboard',
|
||||
mode: 'markdown',
|
||||
},
|
||||
pluginVersion: '',
|
||||
targets: [{ refId: 'A' }],
|
||||
title: 'Welcome',
|
||||
transformations: [],
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
preload: false,
|
||||
refresh: '',
|
||||
schemaVersion: 40,
|
||||
tags: [],
|
||||
templating: { list: [] },
|
||||
time: { from: 'now-6h', to: 'now' },
|
||||
timepicker: {
|
||||
hidden: false,
|
||||
refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'],
|
||||
},
|
||||
timezone: 'browser',
|
||||
title: 'Home Dashboard v2 schema',
|
||||
uid: '',
|
||||
version: 0,
|
||||
};
|
||||
|
||||
const customHomeDashboardV2Spec = {
|
||||
title: 'Home Dashboard v2 schema',
|
||||
cursorSync: 'Off',
|
||||
|
||||
@@ -8,6 +8,7 @@ import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||
import { getMessageFromError, getMessageIdFromError, getStatusFromError } from 'app/core/utils/errors';
|
||||
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
|
||||
import { AnnoKeyFolder } from 'app/features/apiserver/types';
|
||||
import { transformDashboardV2SpecToV1 } from 'app/features/dashboard/api/ResponseTransformers';
|
||||
import { DashboardVersionError, DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
|
||||
import { isDashboardV2Resource, isDashboardV2Spec } from 'app/features/dashboard/api/utils';
|
||||
import { dashboardLoaderSrv, DashboardLoaderSrvV2 } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||
@@ -112,6 +113,43 @@ abstract class DashboardScenePageStateManagerBase<T>
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
protected async fetchHomeDashboard(): Promise<DashboardDTO | null> {
|
||||
const rsp = await getBackendSrv().get<HomeDashboardDTO | HomeDashboardRedirectDTO>('/api/dashboards/home');
|
||||
|
||||
if (isRedirectResponse(rsp)) {
|
||||
const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri);
|
||||
locationService.replace(newUrl);
|
||||
return null;
|
||||
}
|
||||
|
||||
// If dashboard is on v2 schema convert to v1 schema, there's curently no v2 API for home dashboard
|
||||
if (isDashboardV2Spec(rsp.dashboard)) {
|
||||
rsp.dashboard = transformDashboardV2SpecToV1(rsp.dashboard, {
|
||||
name: '',
|
||||
generation: 0,
|
||||
resourceVersion: '0',
|
||||
creationTimestamp: '',
|
||||
});
|
||||
}
|
||||
|
||||
if (rsp?.meta) {
|
||||
rsp.meta.canSave = false;
|
||||
rsp.meta.canShare = false;
|
||||
rsp.meta.canStar = false;
|
||||
}
|
||||
|
||||
return rsp;
|
||||
}
|
||||
|
||||
private async loadHomeDashboard(): Promise<DashboardScene | null> {
|
||||
const rsp = await this.fetchHomeDashboard();
|
||||
if (rsp) {
|
||||
return transformSaveModelToScene(rsp);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async loadSnapshot(slug: string) {
|
||||
try {
|
||||
const dashboard = await this.loadSnapshotScene(slug);
|
||||
@@ -177,13 +215,20 @@ 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;
|
||||
// Home dashboard is not handled through legacy API and is not versioned.
|
||||
// Handling home dashboard flow separately from regular dashboard flow.
|
||||
if (options.route === DashboardRoutes.Home) {
|
||||
return await this.loadHomeDashboard();
|
||||
} else {
|
||||
const rsp = await this.fetchDashboard(options);
|
||||
|
||||
if (!rsp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.transformResponseToScene(rsp, options);
|
||||
}
|
||||
|
||||
return this.transformResponseToScene(rsp, options);
|
||||
}
|
||||
|
||||
public getDashboardFromCache(cacheKey: string): T | null {
|
||||
@@ -283,36 +328,23 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
|
||||
}
|
||||
}
|
||||
|
||||
let rsp: DashboardDTO | HomeDashboardRedirectDTO;
|
||||
let rsp: DashboardDTO;
|
||||
|
||||
try {
|
||||
switch (route) {
|
||||
case DashboardRoutes.New:
|
||||
rsp = await buildNewDashboardSaveModel(urlFolderUid);
|
||||
|
||||
break;
|
||||
case DashboardRoutes.Home:
|
||||
// TODO: Move this fetching to APIClient.getHomeDashboard() to be able to redirect to the correct api depending on the format for the saved dashboard
|
||||
rsp = await getBackendSrv().get<HomeDashboardDTO | HomeDashboardRedirectDTO>('/api/dashboards/home');
|
||||
// For legacy dashboarding we keep this logic here, as dashboard can be loaded through state manager's fetchDashboard method directly
|
||||
// See DashboardPageProxy.
|
||||
const homeDashboard = await this.fetchHomeDashboard();
|
||||
|
||||
if (isRedirectResponse(rsp)) {
|
||||
const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri);
|
||||
locationService.replace(newUrl);
|
||||
if (!homeDashboard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isDashboardV2Spec(rsp.dashboard)) {
|
||||
throw new Error(
|
||||
'You are trying to load a v2 dashboard spec as v1. Use DashboardScenePageStateManagerV2 instead.'
|
||||
);
|
||||
}
|
||||
|
||||
if (rsp?.meta) {
|
||||
rsp.meta.canSave = false;
|
||||
rsp.meta.canShare = false;
|
||||
rsp.meta.canStar = false;
|
||||
}
|
||||
|
||||
rsp = homeDashboard;
|
||||
break;
|
||||
case DashboardRoutes.New:
|
||||
rsp = await buildNewDashboardSaveModel(urlFolderUid);
|
||||
break;
|
||||
case DashboardRoutes.Provisioning: {
|
||||
return await dashboardLoaderSrv.loadDashboard('provisioning', slug, uid);
|
||||
@@ -499,27 +531,6 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
|
||||
switch (route) {
|
||||
case DashboardRoutes.New:
|
||||
rsp = await buildNewDashboardSaveModelV2(urlFolderUid);
|
||||
break;
|
||||
case DashboardRoutes.Home:
|
||||
// TODO: Move this fetching to APIClient.getHomeDashboard() to be able to redirect to the correct api depending on the format for the saved dashboard
|
||||
const dto = await getBackendSrv().get<HomeDashboardDTO | HomeDashboardRedirectDTO>('/api/dashboards/home');
|
||||
|
||||
if (isRedirectResponse(dto)) {
|
||||
const newUrl = locationUtil.stripBaseFromUrl(dto.redirectUri);
|
||||
locationService.replace(newUrl);
|
||||
return null;
|
||||
}
|
||||
|
||||
// if custom home dashboard is v2 spec already, ignore the spec transformation
|
||||
if (!isDashboardV2Resource(dto)) {
|
||||
throw new Error('Custom home dashboard is not a v2 spec');
|
||||
}
|
||||
|
||||
rsp = dto;
|
||||
dto.access.canSave = false;
|
||||
dto.access.canShare = false;
|
||||
dto.access.canStar = false;
|
||||
|
||||
break;
|
||||
case DashboardRoutes.Public: {
|
||||
return await this.dashboardLoader.loadDashboard('public', '', uid);
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
AnnoKeyUpdatedBy,
|
||||
AnnoKeyUpdatedTimestamp,
|
||||
DeprecatedInternalId,
|
||||
ObjectMeta,
|
||||
} from 'app/features/apiserver/types';
|
||||
import { GRID_ROW_HEIGHT } from 'app/features/dashboard-scene/serialization/const';
|
||||
import { TypedVariableModelV2 } from 'app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene';
|
||||
@@ -210,9 +211,6 @@ export function ensureV1Response(
|
||||
};
|
||||
} else {
|
||||
// if dashboard is on v2 schema convert to v1 schema
|
||||
const annotations = getAnnotationsV1(spec.annotations);
|
||||
const variables = getVariablesV1(spec.variables);
|
||||
const panels = getPanelsV1(spec.elements, spec.layout);
|
||||
return {
|
||||
meta: {
|
||||
created: dashboard.metadata.creationTimestamp,
|
||||
@@ -230,38 +228,7 @@ export function ensureV1Response(
|
||||
canStar: dashboard.access.canStar,
|
||||
annotationsPermissions: dashboard.access.annotationsPermissions,
|
||||
},
|
||||
dashboard: {
|
||||
uid: dashboard.metadata.name,
|
||||
title: spec.title,
|
||||
description: spec.description,
|
||||
tags: spec.tags,
|
||||
schemaVersion: 40,
|
||||
graphTooltip: transformCursorSyncV2ToV1(spec.cursorSync),
|
||||
preload: spec.preload,
|
||||
liveNow: spec.liveNow,
|
||||
editable: spec.editable,
|
||||
gnetId: dashboard.metadata.annotations?.[AnnoKeyDashboardGnetId],
|
||||
revision: spec.revision,
|
||||
time: {
|
||||
from: spec.timeSettings.from,
|
||||
to: spec.timeSettings.to,
|
||||
},
|
||||
timezone: spec.timeSettings.timezone,
|
||||
refresh: spec.timeSettings.autoRefresh,
|
||||
timepicker: {
|
||||
refresh_intervals: spec.timeSettings.autoRefreshIntervals,
|
||||
hidden: spec.timeSettings.hideTimepicker,
|
||||
quick_ranges: spec.timeSettings.quickRanges,
|
||||
nowDelay: spec.timeSettings.nowDelay,
|
||||
},
|
||||
fiscalYearStartMonth: spec.timeSettings.fiscalYearStartMonth,
|
||||
weekStart: spec.timeSettings.weekStart,
|
||||
version: dashboard.metadata.generation,
|
||||
links: spec.links,
|
||||
annotations: { list: annotations },
|
||||
panels,
|
||||
templating: { list: variables },
|
||||
},
|
||||
dashboard: transformDashboardV2SpecToV1(spec, dashboard.metadata),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1134,3 +1101,41 @@ function transformToV1VariableTypes(variable: TypedVariableModelV2): VariableTyp
|
||||
throw new Error(`Unknown variable type: ${variable}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function transformDashboardV2SpecToV1(spec: DashboardV2Spec, metadata: ObjectMeta): DashboardDataDTO {
|
||||
const annotations = getAnnotationsV1(spec.annotations);
|
||||
const variables = getVariablesV1(spec.variables);
|
||||
const panels = getPanelsV1(spec.elements, spec.layout);
|
||||
return {
|
||||
uid: metadata.name,
|
||||
title: spec.title,
|
||||
description: spec.description,
|
||||
tags: spec.tags,
|
||||
schemaVersion: 40,
|
||||
graphTooltip: transformCursorSyncV2ToV1(spec.cursorSync),
|
||||
preload: spec.preload,
|
||||
liveNow: spec.liveNow,
|
||||
editable: spec.editable,
|
||||
gnetId: metadata.annotations?.[AnnoKeyDashboardGnetId],
|
||||
revision: spec.revision,
|
||||
time: {
|
||||
from: spec.timeSettings.from,
|
||||
to: spec.timeSettings.to,
|
||||
},
|
||||
timezone: spec.timeSettings.timezone,
|
||||
refresh: spec.timeSettings.autoRefresh,
|
||||
timepicker: {
|
||||
refresh_intervals: spec.timeSettings.autoRefreshIntervals,
|
||||
hidden: spec.timeSettings.hideTimepicker,
|
||||
quick_ranges: spec.timeSettings.quickRanges,
|
||||
nowDelay: spec.timeSettings.nowDelay,
|
||||
},
|
||||
fiscalYearStartMonth: spec.timeSettings.fiscalYearStartMonth,
|
||||
weekStart: spec.timeSettings.weekStart,
|
||||
version: metadata.generation,
|
||||
links: spec.links,
|
||||
annotations: { list: annotations },
|
||||
panels,
|
||||
templating: { list: variables },
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user