From c88c29fadf2171d031cb30f4ce0a5f9d10762c3e Mon Sep 17 00:00:00 2001 From: Ivan Ortega Date: Tue, 16 Dec 2025 23:26:54 +0100 Subject: [PATCH] Use v2 serializer to transform v1 to v2 --- .../pages/DashboardScenePageStateManager.ts | 2 ++ .../serialization/transformSaveModelToScene.ts | 3 ++- .../sharing/ShareExportTab.test.tsx | 14 +++++++++++++- .../dashboard-scene/sharing/ShareExportTab.tsx | 16 ++++++++++++++-- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 3a1841e6b32..943cefe7d04 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -85,6 +85,8 @@ export interface LoadDashboardOptions { slug?: string; type?: string; urlFolderUid?: string; + /** Force the serializer version when creating the scene. Used for v1->v2 export. */ + forceSerializerVersion?: 'v1' | 'v2'; } export type HomeDashboardDTO = DashboardDTO & { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 3da62d6cdf4..abe13637d0c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -266,7 +266,8 @@ export function createDashboardSceneFromDashboardModel( let alertStatesLayer: AlertStatesDataLayer | undefined; const uid = oldModel.uid; const isReport = options?.route === DashboardRoutes.Report; - const serializerVersion = shouldForceV2API() && !oldModel.meta.isSnapshot && !isReport ? 'v2' : 'v1'; + const serializerVersion = + options?.forceSerializerVersion ?? (shouldForceV2API() && !oldModel.meta.isSnapshot && !isReport ? 'v2' : 'v1'); if (oldModel.meta.isSnapshot) { variables = createVariablesForSnapshot(oldModel); diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx index 1e7bee6b613..7d7c9e2c66e 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx @@ -1,5 +1,5 @@ import { config } from '@grafana/runtime'; -import { SceneTimeRange } from '@grafana/scenes'; +import { SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec, @@ -13,6 +13,7 @@ import { DashboardDataDTO } from 'app/types/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; import * as exporters from '../scene/export/exporters'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; +import * as v1ToScene from '../serialization/transformSaveModelToScene'; import * as sceneToV1 from '../serialization/transformSceneToSaveModel'; import * as sceneToV2 from '../serialization/transformSceneToSaveModelSchemaV2'; @@ -25,6 +26,7 @@ describe('ShareExportTab', () => { let makeExportableV1Spy: jest.SpyInstance; let transformSceneToV1Spy: jest.SpyInstance; let transformSceneToV2Spy: jest.SpyInstance; + let transformSaveModelToSceneSpy: jest.SpyInstance; beforeEach(() => { config.featureToggles.kubernetesDashboards = true; @@ -65,6 +67,16 @@ describe('ShareExportTab', () => { templating: { list: [] }, } as Dashboard); + // Mock transformSaveModelToScene to return a mock scene (used for v1->v2 export with rows) + transformSaveModelToSceneSpy = jest.spyOn(v1ToScene, 'transformSaveModelToScene').mockImplementation(() => { + return new DashboardScene({ + title: 'Mock Scene for V2 Export', + uid: 'mock-scene-uid', + body: new DefaultGridLayoutManager({ grid: new SceneGridLayout({ children: [] }) }), + $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), + }); + }); + transformSceneToV2Spy = jest.spyOn(sceneToV2, 'transformSceneToSaveModelSchemaV2').mockReturnValue({ title: 'Scene V2', annotations: [], diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx index 0e9d6ad49c5..71c930acf86 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx @@ -19,10 +19,11 @@ import { K8S_V2_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v2'; import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardJson } from 'app/features/manage-dashboards/types'; -import { DashboardDataDTO } from 'app/types/dashboard'; +import { DashboardDataDTO, DashboardRoutes } from 'app/types/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; import { makeExportableV1, makeExportableV2 } from '../scene/export/exporters'; +import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; import { transformSceneToSaveModelSchemaV2 } from '../serialization/transformSceneToSaveModelSchemaV2'; import { getVariablesCompatibility } from '../utils/getVariablesCompatibility'; @@ -216,7 +217,18 @@ export class ShareExportTab extends SceneObjectBase impleme } if (exportMode === ExportMode.V2Resource) { - const spec = transformSceneToSaveModelSchemaV2(scene); + // When the initial save model was v1, we need to recreate the scene with v2 serializer + // to properly handle rows (convert SceneGridRow to RowsLayout instead of losing them) + let sceneForExport = scene; + if (initialSaveModelVersion === 'v1' && !isDashboardV2Spec(origDashboard)) { + const v1SaveModel = transformSceneToSaveModel(scene); + sceneForExport = transformSaveModelToScene( + { dashboard: v1SaveModel, meta: scene.state.meta }, + { uid: scene.state.uid ?? '', route: DashboardRoutes.Normal, forceSerializerVersion: 'v2' } + ); + } + + const spec = transformSceneToSaveModelSchemaV2(sceneForExport); const specCopy = JSON.parse(JSON.stringify(spec)); const statelessSpec = await makeExportableV2(specCopy, isSharingExternally); const exportableV2 = isSharingExternally ? statelessSpec : spec;