From 6b03adbbb07fd3a02392daf60d38a329d831fb55 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 9 Jan 2025 08:14:41 -0700 Subject: [PATCH] Dashboard V2: Snapshot variables and read snapshot (#98463) * Introduce DashboardScenePageStateManagerLike interface * Implement dash loader for handling v2 api * Transformation improvements * Update response transformer test * v2 schema: Remove defaultOptionEnabled from ds variable schema * v2 schema: Make annotations filter optional * WIP render dashboard from v2 schema * Force dashbaord scene for v2 api * V2 schema -> scene meta transformation * v2 api: Handle home dashboard * Use correct api client in DashboardScenePage * Correctly use v2 dashboard scene serializer * Remove unnecesary type assertions * Handle v2 dashboard not found * Fix type * Fix test * Some more tests fix * snapshot * Add dashboard id annotation * Nits * Nits * Rename v2 api * Enable snapshot variables * Support getSnapshotUrl() for v2 serializer * fix * Make metadata available in the serializer * Test * Decouple meta info * State Manager: Extract snapshot loading into loadSnapshot * Fix tests * Add test for snapshot url * Don't expose dashboardLoaderSrvV2 * Remove TODO * Bubble up loading snapshot error to error boundary * Fix test --------- Co-authored-by: Dominik Prokop --- public/app/features/apiserver/types.ts | 2 + .../DashboardScenePageStateManager.test.ts | 28 +++---- .../pages/DashboardScenePageStateManager.ts | 35 +++++--- .../dashboard-scene/scene/DashboardScene.tsx | 15 +++- .../DashboardSceneSerializer.test.ts | 14 +++- .../serialization/DashboardSceneSerializer.ts | 23 ++++-- .../transformSaveModelSchemaV2ToScene.test.ts | 39 +++++++++ .../transformSaveModelSchemaV2ToScene.ts | 15 ++-- .../dashboard-scene/utils/test-utils.ts | 2 + .../dashboard/api/ResponseTransformers.ts | 82 +++++++++++-------- .../dashboard/services/DashboardLoaderSrv.ts | 53 +++++++++--- 11 files changed, 218 insertions(+), 90 deletions(-) diff --git a/public/app/features/apiserver/types.ts b/public/app/features/apiserver/types.ts index d90393f0bf1..054aa62d810 100644 --- a/public/app/features/apiserver/types.ts +++ b/public/app/features/apiserver/types.ts @@ -49,6 +49,7 @@ export const AnnoKeyRepoTimestamp = 'grafana.app/repoTimestamp'; export const AnnoKeySavedFromUI = 'grafana.app/saved-from-ui'; export const AnnoKeyDashboardNotFound = 'grafana.app/dashboard-not-found'; export const AnnoKeyDashboardIsSnapshot = 'grafana.app/dashboard-is-snapshot'; +export const AnnoKeyDashboardSnapshotOriginalUrl = 'grafana.app/dashboard-snapshot-original-url'; export const AnnoKeyDashboardIsNew = 'grafana.app/dashboard-is-new'; export const AnnoKeyDashboardGnetId = 'grafana.app/dashboard-gnet-id'; @@ -77,6 +78,7 @@ type GrafanaClientAnnotations = { [AnnoKeySavedFromUI]?: string; [AnnoKeyDashboardNotFound]?: boolean; [AnnoKeyDashboardIsSnapshot]?: boolean; + [AnnoKeyDashboardSnapshotOriginalUrl]?: string; [AnnoKeyDashboardIsNew]?: boolean; // TODO: This should be provided by the API diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 9bd7abc085c..b1e60f6e0db 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -8,6 +8,7 @@ import { import store from 'app/core/store'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; +import { getDashboardSnapshotSrv } from 'app/features/dashboard/services/SnapshotSrv'; import { DASHBOARD_FROM_LS_KEY, DashboardRoutes } from 'app/types'; import { DashboardScene } from '../scene/DashboardScene'; @@ -26,6 +27,10 @@ jest.mock('app/features/dashboard/api/dashboard_api', () => ({ describe('DashboardScenePageStateManager v1', () => { afterEach(() => { store.delete(DASHBOARD_FROM_LS_KEY); + + setBackendSrv({ + get: jest.fn(), + } as unknown as BackendSrv); }); describe('when fetching/loading a dashboard', () => { @@ -379,21 +384,16 @@ describe('DashboardScenePageStateManager v2', () => { }); it('should use DashboardScene creator to initialize the snapshot scene', async () => { - const getDashSpy = jest.fn(); - setupDashboardAPI( - { - access: {}, - apiVersion: 'v2alpha1', - kind: 'DashboardWithAccessInfo', - metadata: { - name: 'fake-dash', - creationTimestamp: '', - resourceVersion: '1', - }, - spec: { ...defaultDashboardV2Spec() }, + jest.spyOn(getDashboardSnapshotSrv(), 'getSnapshot').mockResolvedValue({ + // getSnapshot will return v1 dashboard + // but ResponseTransformer in DashboardLoaderSrv will convert it to v2 + dashboard: { + uid: 'fake-dash', + title: 'Fake dashboard', + schemaVersion: 40, }, - getDashSpy - ); + meta: { isSnapshot: true }, + }); const loader = new DashboardScenePageStateManagerV2({}); await loader.loadSnapshot('fake-slug'); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 8fd7ca26ce5..d13ddb3504d 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -82,6 +82,7 @@ abstract class DashboardScenePageStateManagerBase abstract fetchDashboard(options: LoadDashboardOptions): Promise; abstract reloadDashboard(params: LoadDashboardOptions['params']): Promise; abstract transformResponseToScene(rsp: T | null, options: LoadDashboardOptions): DashboardScene | null; + abstract loadSnapshotScene(slug: string): Promise; protected cache: Record = {}; @@ -102,17 +103,6 @@ abstract class DashboardScenePageStateManagerBase } } - private async loadSnapshotScene(slug: string): Promise { - const rsp = await dashboardLoaderSrv.loadDashboard('snapshot', slug, ''); - - if (rsp?.dashboard) { - const scene = transformSaveModelToScene(rsp); - return scene; - } - - throw new Error('Snapshot not found'); - } - public async loadDashboard(options: LoadDashboardOptions) { try { startMeasure(LOAD_SCENE_MEASUREMENT); @@ -222,6 +212,18 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag throw new Error('Dashboard not found'); } + + public async loadSnapshotScene(slug: string): Promise { + const rsp = await dashboardLoaderSrv.loadSnapshot(slug); + + if (rsp?.dashboard) { + const scene = transformSaveModelToScene(rsp); + return scene; + } + + throw new Error('Snapshot not found'); + } + public async fetchDashboard({ uid, route, @@ -370,6 +372,17 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan > { private dashboardLoader = new DashboardLoaderSrvV2(); + public async loadSnapshotScene(slug: string): Promise { + const rsp = await this.dashboardLoader.loadSnapshot(slug); + + if (rsp?.spec) { + const scene = transformSaveModelSchemaV2ToScene(rsp); + return scene; + } + + throw new Error('Snapshot not found'); + } + transformResponseToScene( rsp: DashboardWithAccessInfo | null, options: LoadDashboardOptions diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index d033f2da590..893b31321a8 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -31,6 +31,7 @@ import { ScrollRefElement } from 'app/core/components/NativeScrollbar'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { getNavModel } from 'app/core/selectors/navModel'; import store from 'app/core/store'; +import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; @@ -173,8 +174,10 @@ export class DashboardScene extends SceneObjectBase { private _scrollRef?: ScrollRefElement; private _prevScrollPos?: number; - // TODO: use feature toggle to allow v2 serializer - private _serializer: DashboardSceneSerializerLike = getDashboardSceneSerializer(); + private _serializer: DashboardSceneSerializerLike< + Dashboard | DashboardV2Spec, + DashboardMeta | DashboardWithAccessInfo['metadata'] + > = getDashboardSceneSerializer(); public constructor(state: Partial) { super({ @@ -649,8 +652,14 @@ export class DashboardScene extends SceneObjectBase { }; /** Hacky temp function until we refactor transformSaveModelToScene a bit */ - public setInitialSaveModel(saveModel?: Dashboard | DashboardV2Spec) { + setInitialSaveModel(model?: Dashboard, meta?: DashboardMeta): void; + setInitialSaveModel(model?: DashboardV2Spec, meta?: DashboardWithAccessInfo['metadata']): void; + public setInitialSaveModel( + saveModel?: Dashboard | DashboardV2Spec, + meta?: DashboardMeta | DashboardWithAccessInfo['metadata'] + ): void { this._serializer.initialSaveModel = saveModel; + this._serializer.metadata = meta; } public getTrackingInformation() { diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index cefd2ba5473..10cf5de3bf6 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -13,6 +13,7 @@ import { defaultPanelSpec, defaultTimeSettingsSpec, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; +import { AnnoKeyDashboardSnapshotOriginalUrl } from 'app/features/apiserver/types'; import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; @@ -622,9 +623,18 @@ describe('DashboardSceneSerializer', () => { ).toThrow('Method not implemented.'); }); - it('should throw on getSnapshotUrl', () => { + it('should allow retrieving snapshot url', () => { const serializer = new V2DashboardSerializer(); - expect(() => serializer.getSnapshotUrl()).toThrow('Method not implemented.'); + serializer.metadata = { + name: 'dashboard-test', + resourceVersion: '1', + creationTimestamp: '2023-01-01T00:00:00Z', + annotations: { + [AnnoKeyDashboardSnapshotOriginalUrl]: 'originalUrl/snapshot', + }, + }; + + expect(serializer.getSnapshotUrl()).toBe('originalUrl/snapshot'); }); }); }); diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts index 1db278a049f..8ff45f88f6f 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts @@ -1,13 +1,15 @@ import { config } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; +import { AnnoKeyDashboardSnapshotOriginalUrl } from 'app/features/apiserver/types'; +import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types'; import { getPanelPluginCounts, getV1SchemaVariables, getV2SchemaVariables, } from 'app/features/dashboard/utils/tracking'; -import { SaveDashboardResponseDTO } from 'app/types'; +import { DashboardMeta, SaveDashboardResponseDTO } from 'app/types'; import { getRawDashboardChanges, getRawDashboardV2Changes } from '../saving/getDashboardChanges'; import { DashboardChangeInfo } from '../saving/shared'; @@ -16,11 +18,12 @@ import { DashboardScene } from '../scene/DashboardScene'; import { transformSceneToSaveModel } from './transformSceneToSaveModel'; import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2'; -export interface DashboardSceneSerializerLike { +export interface DashboardSceneSerializerLike { /** * The save model which the dashboard scene was originally created from */ initialSaveModel?: T; + metadata?: M; getSaveModel: (s: DashboardScene) => T; getSaveAsModel: (s: DashboardScene, options: SaveDashboardAsOptions) => T; getDashboardChangesFromScene: ( @@ -45,8 +48,9 @@ interface DashboardTrackingInfo { settings_livenow?: boolean; } -export class V1DashboardSerializer implements DashboardSceneSerializerLike { +export class V1DashboardSerializer implements DashboardSceneSerializerLike { initialSaveModel?: Dashboard; + metadata?: DashboardMeta; getSaveModel(s: DashboardScene) { return transformSceneToSaveModel(s); @@ -121,8 +125,11 @@ export class V1DashboardSerializer implements DashboardSceneSerializerLike { +export class V2DashboardSerializer + implements DashboardSceneSerializerLike['metadata']> +{ initialSaveModel?: DashboardV2Spec; + metadata?: DashboardWithAccessInfo['metadata']; getSaveModel(s: DashboardScene) { return transformSceneToSaveModelSchemaV2(s); @@ -187,12 +194,14 @@ export class V2DashboardSerializer implements DashboardSceneSerializerLike { +export function getDashboardSceneSerializer(): DashboardSceneSerializerLike< + Dashboard | DashboardV2Spec, + DashboardMeta | DashboardWithAccessInfo['metadata'] +> { if (config.featureToggles.useV2DashboardsAPI) { return new V2DashboardSerializer(); } diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index c5fa51af6d5..1bc3fe195a0 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -37,6 +37,7 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getQueryRunnerFor } from '../utils/utils'; import { validateVariable, validateVizPanel } from '../v2schema/test-helpers'; +import { SnapshotVariable } from './custom-variables/SnapshotVariable'; import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene'; import { transformCursorSynctoEnum } from './transformToV2TypesUtils'; @@ -316,6 +317,44 @@ describe('transformSaveModelSchemaV2ToScene', () => { expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME); }); + describe('When creating a snapshot dashboard scene', () => { + it('should initialize a dashboard scene with SnapshotVariables', () => { + const snapshot: DashboardWithAccessInfo = { + ...defaultDashboard, + metadata: { + ...defaultDashboard.metadata, + annotations: { + ...defaultDashboard.metadata.annotations, + 'grafana.app/dashboard-is-snapshot': true, + }, + }, + }; + + const scene = transformSaveModelSchemaV2ToScene(snapshot); + + // check variables were converted to snapshot variables + expect(scene.state.$variables?.state.variables).toHaveLength(8); + expect(scene.state.$variables?.getByName('customVar')).toBeInstanceOf(SnapshotVariable); + expect(scene.state.$variables?.getByName('adhocVar')).toBeInstanceOf(AdHocFiltersVariable); + expect(scene.state.$variables?.getByName('intervalVar')).toBeInstanceOf(SnapshotVariable); + // custom snapshot + const customSnapshot = scene.state.$variables?.getByName('customVar') as SnapshotVariable; + expect(customSnapshot.state.value).toBe('option1'); + expect(customSnapshot.state.text).toBe('option1'); + expect(customSnapshot.state.isReadOnly).toBe(true); + // adhoc snapshot + const adhocSnapshot = scene.state.$variables?.getByName('adhocVar') as AdHocFiltersVariable; + const adhocVariable = snapshot.spec.variables[7] as AdhocVariableKind; + expect(adhocSnapshot.state.filters).toEqual(adhocVariable.spec.filters); + expect(adhocSnapshot.state.readOnly).toBe(true); + // interval snapshot + const intervalSnapshot = scene.state.$variables?.getByName('intervalVar') as SnapshotVariable; + expect(intervalSnapshot.state.value).toBe('1m'); + expect(intervalSnapshot.state.text).toBe('1m'); + expect(intervalSnapshot.state.isReadOnly).toBe(true); + }); + }); + describe('meta', () => { describe('initializes meta based on k8s resource', () => { it('handles undefined access values', () => { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 81ec961171c..fb1619ba497 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -59,6 +59,7 @@ import { AnnoKeyUpdatedBy, AnnoKeyUpdatedTimestamp, AnnoKeyDashboardIsNew, + AnnoKeyDashboardIsSnapshot, } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; @@ -140,6 +141,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo, spy?: jest.Mock) { const loadDashboardMock = (spy || jest.fn()).mockResolvedValue(rsp); + const loadSnapshotMock = (spy || jest.fn()).mockResolvedValue(rsp); // disabling type checks since this is a test util // eslint-disable-next-line @typescript-eslint/consistent-type-assertions setDashboardLoaderSrv({ loadDashboard: loadDashboardMock, + loadSnapshot: loadSnapshotMock, } as unknown as DashboardLoaderSrv); return loadDashboardMock; } diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 1f46a08b280..64739f64a7f 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -17,6 +17,8 @@ import { AnnoKeyCreatedBy, AnnoKeyDashboardGnetId, AnnoKeyDashboardId, + AnnoKeyDashboardIsSnapshot, + AnnoKeyDashboardSnapshotOriginalUrl, AnnoKeyFolder, AnnoKeySlug, AnnoKeyUpdatedBy, @@ -56,17 +58,51 @@ export function ensureV2Response( const variables = getVariables(dashboard.templating?.list || []); const annotations = getAnnotations(dashboard.annotations?.list || []); - const accessAndMeta = isDashboardResource(dto) - ? { - ...dto.access, - created: dto.metadata.creationTimestamp, - createdBy: dto.metadata.annotations?.[AnnoKeyCreatedBy], - updatedBy: dto.metadata.annotations?.[AnnoKeyUpdatedBy], - updated: dto.metadata.annotations?.[AnnoKeyUpdatedTimestamp], - folderUid: dto.metadata.annotations?.[AnnoKeyFolder], - slug: dto.metadata.annotations?.[AnnoKeySlug], - } - : dto.meta; + let accessMeta: DashboardWithAccessInfo['access']; + let annotationsMeta: DashboardWithAccessInfo['metadata']['annotations']; + let creationTimestamp; + + 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], + [AnnoKeyDashboardId]: dashboard.id ?? undefined, + [AnnoKeyDashboardGnetId]: dashboard.gnetId ?? undefined, + [AnnoKeyDashboardIsSnapshot]: dto.metadata.annotations?.[AnnoKeyDashboardIsSnapshot], + }; + creationTimestamp = dto.metadata.creationTimestamp; + } else { + accessMeta = { + url: dto.meta.url, + slug: dto.meta.slug, + canSave: dto.meta.canSave, + canEdit: dto.meta.canEdit, + canDelete: dto.meta.canDelete, + canShare: dto.meta.canShare, + canStar: dto.meta.canStar, + canAdmin: dto.meta.canAdmin, + annotationsPermissions: dto.meta.annotationsPermissions, + }; + annotationsMeta = { + [AnnoKeyCreatedBy]: dto.meta.createdBy, + [AnnoKeyUpdatedBy]: dto.meta.updatedBy, + [AnnoKeyUpdatedTimestamp]: dto.meta.updated, + [AnnoKeyFolder]: dto.meta.folderUid, + [AnnoKeySlug]: dto.meta.slug, + [AnnoKeyDashboardId]: dashboard.id ?? undefined, + [AnnoKeyDashboardGnetId]: dashboard.gnetId ?? undefined, + [AnnoKeyDashboardIsSnapshot]: dto.meta.isSnapshot, + }; + creationTimestamp = dto.meta.created; + } + + if (annotationsMeta?.[AnnoKeyDashboardIsSnapshot]) { + annotationsMeta[AnnoKeyDashboardSnapshotOriginalUrl] = dashboard.snapshot?.originalUrl; + } const spec: DashboardV2Spec = { title: dashboard.title, @@ -101,31 +137,13 @@ export function ensureV2Response( apiVersion: 'v2alpha1', kind: 'DashboardWithAccessInfo', metadata: { - creationTimestamp: accessAndMeta.created || '', // TODO verify this empty string is valid + creationTimestamp: creationTimestamp || '', // TODO verify this empty string is valid name: dashboard.uid, resourceVersion: dashboard.version?.toString() || '0', - annotations: { - [AnnoKeyCreatedBy]: accessAndMeta.createdBy, - [AnnoKeyUpdatedBy]: accessAndMeta.updatedBy, - [AnnoKeyUpdatedTimestamp]: accessAndMeta.updated, - [AnnoKeyFolder]: accessAndMeta.folderUid, - [AnnoKeySlug]: accessAndMeta.slug, - [AnnoKeyDashboardId]: dashboard.id ?? undefined, - [AnnoKeyDashboardGnetId]: dashboard.gnetId ?? undefined, - }, + annotations: annotationsMeta, }, spec, - access: { - url: accessAndMeta.url || '', - canAdmin: accessAndMeta.canAdmin, - canDelete: accessAndMeta.canDelete, - canEdit: accessAndMeta.canEdit, - canSave: accessAndMeta.canSave, - canShare: accessAndMeta.canShare, - canStar: accessAndMeta.canStar, - slug: accessAndMeta.slug, - annotationsPermissions: accessAndMeta.annotationsPermissions, - }, + access: accessMeta, }; } diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index ddcc2e6d6e3..ede6a20a6e6 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -10,6 +10,7 @@ import { } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; import { backendSrv } from 'app/core/services/backend_srv'; import impressionSrv from 'app/core/services/impression_srv'; +import { getMessageFromError } from 'app/core/utils/errors'; import kbn from 'app/core/utils/kbn'; import { AnnoKeyDashboardIsSnapshot, AnnoKeyDashboardNotFound } from 'app/features/apiserver/types'; import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager'; @@ -42,6 +43,7 @@ abstract class DashboardLoaderSrvBase implements DashboardLoaderSrvLike { uid: string | undefined, params?: UrlQueryMap ): Promise; + abstract loadSnapshot(slug: string): Promise; protected loadScriptedDashboard(file: string) { const url = 'public/dashboards/' + file.replace(/\.(?!js)/, '/') + '?' + new Date().getTime(); @@ -144,12 +146,6 @@ export class DashboardLoaderSrv extends DashboardLoaderSrvBase { if (type === 'script' && slug) { promise = this.loadScriptedDashboard(slug); - } else if (type === 'snapshot' && slug) { - promise = getDashboardSnapshotSrv() - .getSnapshot(slug) - .catch(() => { - return this._dashboardLoadFailed('Snapshot not found', true); - }); } else if (type === 'public' && uid) { promise = backendSrv .getPublicDashboardByUid(uid) @@ -213,6 +209,24 @@ export class DashboardLoaderSrv extends DashboardLoaderSrvBase { return promise; } + + loadSnapshot(slug: string): Promise { + const promise = getDashboardSnapshotSrv() + .getSnapshot(slug) + .catch(() => { + return this._dashboardLoadFailed('Snapshot not found', true); + }); + + promise.then((result: DashboardDTO) => { + if (result.meta.dashboardNotFound !== true) { + impressionSrv.addDashboardImpression(result.dashboard.uid); + } + + return result; + }); + + return promise; + } } export class DashboardLoaderSrvV2 extends DashboardLoaderSrvBase> { @@ -257,13 +271,6 @@ export class DashboardLoaderSrvV2 extends DashboardLoaderSrvBase ResponseTransformers.ensureV2Response(r)); - } else if (type === 'snapshot' && slug) { - promise = getDashboardSnapshotSrv() - .getSnapshot(slug) - .then((r) => ResponseTransformers.ensureV2Response(r)) - .catch(() => { - return this._dashboardLoadFailed('Snapshot not found', true); - }); } else if (type === 'public' && uid) { promise = backendSrv .getPublicDashboardByUid(uid) @@ -327,6 +334,26 @@ export class DashboardLoaderSrvV2 extends DashboardLoaderSrvBase> { + const promise = getDashboardSnapshotSrv() + .getSnapshot(slug) + .then((r) => ResponseTransformers.ensureV2Response(r)) + .catch((e) => { + const msg = getMessageFromError(e); + throw new Error(`Failed to load snapshot: ${msg}`); + }); + + promise.then((result: DashboardWithAccessInfo) => { + if (result.metadata.annotations?.[AnnoKeyDashboardNotFound] !== true) { + impressionSrv.addDashboardImpression(result.metadata.name); + } + + return result; + }); + + return promise; + } } let dashboardLoaderSrv = new DashboardLoaderSrv();