From d1d8e0b30fa5faba9fa6a90b49edae68fb54cc3f Mon Sep 17 00:00:00 2001 From: Haris Rozajac Date: Mon, 24 Nov 2025 07:28:51 -0700 Subject: [PATCH] wip --- eslint-suppressions.json | 10 -- .../sharing/ExportButton/ResourceExport.tsx | 1 - .../sharing/ShareExportTab.test.tsx | 21 --- .../sharing/ShareExportTab.tsx | 55 ++------ .../components/ImportDashboardOverview.tsx | 125 +++++++++++++++++- 5 files changed, 133 insertions(+), 79 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 84e5d786552..3ee8bb3172e 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2136,11 +2136,6 @@ "count": 2 } }, - "public/app/features/dashboard-scene/sharing/ShareExportTab.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx": { "no-restricted-syntax": { "count": 4 @@ -2942,11 +2937,6 @@ "count": 1 } }, - "public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/manage-dashboards/state/actions.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx index b58bf8377af..226c6074bb8 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx @@ -19,7 +19,6 @@ interface Props { dashboardJson: AsyncState<{ json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown }; hasLibraryPanels?: boolean; - initialSaveModelVersion: 'v1' | 'v2'; }>; isSharingExternally: boolean; exportMode: ExportMode; diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx index 1e7bee6b613..90c5edf904e 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx @@ -113,9 +113,6 @@ describe('ShareExportTab', () => { // Should call transformSceneToV1 (not transform V2→V1) expect(transformSceneToV1Spy).toHaveBeenCalled(); expect(transformV2ToV1Spy).not.toHaveBeenCalled(); - - // Should report correct initial version - expect(result.initialSaveModelVersion).toBe('v1'); }); // If V2 dashboard → V1 Resource should auto-transform with V1 apiVersion @@ -136,9 +133,6 @@ describe('ShareExportTab', () => { // Should auto-transform V2→V1 expect(transformSceneToV2Spy).toHaveBeenCalled(); // Get V2 spec first expect(transformV2ToV1Spy).toHaveBeenCalled(); // Then transform to V1 - - // Should report correct initial version - expect(result.initialSaveModelVersion).toBe('v2'); }); // If V2 dashboard → V1 Resource with external sharing should transform and apply external sharing @@ -164,9 +158,6 @@ describe('ShareExportTab', () => { // Should call makeExportableV1 for external sharing expect(makeExportableV1Spy).toHaveBeenCalled(); - - // Should report correct initial version - expect(result.initialSaveModelVersion).toBe('v2'); }); }); @@ -187,9 +178,6 @@ describe('ShareExportTab', () => { // Should not call V2→V1 transformation since source is already V2 expect(transformV2ToV1Spy).not.toHaveBeenCalled(); - - // Should report correct initial version - expect(result.initialSaveModelVersion).toBe('v2'); }); // If V1 dashboard → V2 Resource should detect library panels correctly @@ -201,7 +189,6 @@ describe('ShareExportTab', () => { // Should detect library panels from V1 dashboard expect(result.hasLibraryPanels).toBe(true); - expect(result.initialSaveModelVersion).toBe('v1'); }); // If V1 dashboard with dashboardNewLayouts disabled → V2 Resource should detect library panels correctly @@ -213,7 +200,6 @@ describe('ShareExportTab', () => { // Should detect library panels from V1 dashboard (first branch of the logic) expect(result.hasLibraryPanels).toBe(true); - expect(result.initialSaveModelVersion).toBe('v1'); }); // If V1 dashboard without library panels → V2 Resource should return false @@ -225,7 +211,6 @@ describe('ShareExportTab', () => { // Should not detect library panels expect(result.hasLibraryPanels).toBe(false); - expect(result.initialSaveModelVersion).toBe('v1'); }); }); @@ -247,7 +232,6 @@ describe('ShareExportTab', () => { // Should detect library panels from V2 dashboard elements (second branch of the logic) expect(result.hasLibraryPanels).toBe(true); - expect(result.initialSaveModelVersion).toBe('v2'); }); // Test the second branch: V2 dashboard with V1 initial save model @@ -259,7 +243,6 @@ describe('ShareExportTab', () => { // Should detect library panels from V2 dashboard elements (second branch of the logic) expect(result.hasLibraryPanels).toBe(true); - expect(result.initialSaveModelVersion).toBe('v1'); }); // If V2 dashboard without library panels → V2 Resource should return false @@ -271,7 +254,6 @@ describe('ShareExportTab', () => { // Should not detect library panels expect(result.hasLibraryPanels).toBe(false); - expect(result.initialSaveModelVersion).toBe('v2'); }); }); @@ -294,9 +276,6 @@ describe('ShareExportTab', () => { expect(result.json).not.toHaveProperty('apiVersion'); expect(result.json).not.toHaveProperty('kind'); expect(result.json).not.toHaveProperty('status'); - - // Should report correct initial version - expect(result.initialSaveModelVersion).toBe('v1'); }); }); diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx index 0e9d6ad49c5..bf8253c6dc8 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx @@ -102,24 +102,16 @@ export class ShareExportTab extends SceneObjectBase impleme public getExportableDashboardJson = async (): Promise<{ json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown }; hasLibraryPanels?: boolean; - initialSaveModelVersion: 'v1' | 'v2'; }> => { const { isSharingExternally, exportMode } = this.state; const scene = getDashboardSceneFor(this); const exportableDashboard = await scene.serializer.makeExportableExternally(scene); - const initialSaveModel = scene.getInitialSaveModel(); - const initialSaveModelVersion = initialSaveModel && isDashboardV2Spec(initialSaveModel) ? 'v2' : 'v1'; const origDashboard = scene.serializer.getSaveModel(scene); const exportable = isSharingExternally ? exportableDashboard : origDashboard; const metadata = getMetadata(scene, Boolean(isSharingExternally)); - if ( - isDashboardV2Spec(origDashboard) && - 'elements' in exportable && - initialSaveModelVersion === 'v2' && - exportMode !== ExportMode.V1Resource - ) { + if (isDashboardV2Spec(origDashboard) && 'elements' in exportable && exportMode !== ExportMode.V1Resource) { this.setState({ exportMode: ExportMode.V2Resource, }); @@ -132,7 +124,6 @@ export class ShareExportTab extends SceneObjectBase impleme if ('error' in result) { return { json: { error: result.error }, - initialSaveModelVersion, hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'), }; } @@ -147,14 +138,13 @@ export class ShareExportTab extends SceneObjectBase impleme spec: finalSpec, status: {}, }, - initialSaveModelVersion, hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'), }; } if (exportMode === ExportMode.V1Resource) { // Check if source is V2 and auto-transform to V1 - if (isDashboardV2Spec(origDashboard) && initialSaveModelVersion === 'v2') { + if (isDashboardV2Spec(origDashboard)) { try { const spec = transformSceneToSaveModelSchemaV2(scene); const metadata = getMetadata(scene, Boolean(isSharingExternally)); @@ -185,7 +175,6 @@ export class ShareExportTab extends SceneObjectBase impleme spec: exportableV1, status: {}, }, - initialSaveModelVersion, hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec1), }; } catch (err) { @@ -193,7 +182,6 @@ export class ShareExportTab extends SceneObjectBase impleme json: { error: `Failed to convert dashboard to v1. ${err}`, }, - initialSaveModelVersion, hasLibraryPanels: undefined, }; } @@ -209,7 +197,6 @@ export class ShareExportTab extends SceneObjectBase impleme spec, status: {}, }, - initialSaveModelVersion, hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec), }; } @@ -223,7 +210,7 @@ export class ShareExportTab extends SceneObjectBase impleme // Check if dashboard contains library panels based on dashboard version let hasLibraryPanels = false; // Case: V1 dashboard loaded (with kubernetesDashboards enabled and dashboardNewLayouts disabled), and user explicitly selected V2Resource export mode - if (initialSaveModelVersion === 'v1' && !isDashboardV2Spec(origDashboard)) { + if (!isDashboardV2Spec(origDashboard)) { hasLibraryPanels = hasLibraryPanelsInV1Dashboard(origDashboard); } else if (isDashboardV2Spec(origDashboard)) { // Case: V2 dashboard (either originally V2 or transformed from V1) being exported as V2Resource @@ -239,35 +226,10 @@ export class ShareExportTab extends SceneObjectBase impleme spec: exportableV2, status: {}, }, - initialSaveModelVersion, hasLibraryPanels, }; } - // Classic mode - // This handles a case when: - // 1. dashboardNewLayouts feature toggle is enabled - // 2. v1 dashboard is loaded - // 3. dashboard hasn't been edited yet - if it was edited, user would be forced to save it in v2 version - if ( - initialSaveModelVersion === 'v1' && - isDashboardV2Spec(origDashboard) && - initialSaveModel && - 'panels' in initialSaveModel - ) { - const oldModel = new DashboardModel(initialSaveModel, undefined, { - getVariablesFromState: () => { - return getVariablesCompatibility(window.__grafanaSceneContext); - }, - }); - const exportableV1 = isSharingExternally ? await makeExportableV1(oldModel) : initialSaveModel; - return { - json: exportableV1, - hasLibraryPanels: hasLibraryPanelsInV1Dashboard(initialSaveModel), - initialSaveModelVersion, - }; - } - // legacy mode or classic mode when dashboardNewLayouts is disabled // At this point we know that dashboard should be V1 or could have produced an error return { @@ -276,7 +238,6 @@ export class ShareExportTab extends SceneObjectBase impleme 'error' in exportable || !isV1ClassicDashboard(origDashboard) ? false : hasLibraryPanelsInV1Dashboard(origDashboard), - initialSaveModelVersion, }; }; @@ -297,9 +258,11 @@ export class ShareExportTab extends SceneObjectBase impleme const extension = isViewingYAML ? 'yaml' : 'json'; saveAs(blob, `${title}-${time}.${extension}`); + const isV2Dashboard = 'spec' in dashboard.json && 'elements' in dashboard.json.spec; + DashboardInteractions.exportDownloadJsonClicked({ externally: isSharingExternally, - dashboard_schema_version: dashboard.initialSaveModelVersion, + dashboard_schema_version: isV2Dashboard ? 'v2' : 'v1', has_library_panels: Boolean(dashboard.hasLibraryPanels), format: isViewingYAML ? 'yaml' : 'json', action: 'download', @@ -310,9 +273,11 @@ export class ShareExportTab extends SceneObjectBase impleme const dashboard = await this.getExportableDashboardJson(); const { isSharingExternally, isViewingYAML, exportMode } = this.state; + const isV2Dashboard = 'spec' in dashboard.json && 'elements' in dashboard.json.spec; + DashboardInteractions.exportCopyJsonClicked({ externally: isSharingExternally, - dashboard_schema_version: dashboard.initialSaveModelVersion, + dashboard_schema_version: isV2Dashboard ? 'v2' : 'v1', has_library_panels: Boolean(dashboard.hasLibraryPanels), export_mode: exportMode || 'classic', format: isViewingYAML ? 'yaml' : 'json', @@ -402,7 +367,7 @@ function ShareExportTabRenderer({ model }: SceneComponentProps) /> ) : ( - + { onSubmit = (form: ImportDashboardDTO) => { reportInteraction(IMPORT_FINISHED_EVENT_NAME); + const { dashboard, inputs, folder } = this.props; + + // TODO: add logic when kubernetesDashboards are enabled + if (config.featureToggles.kubernetesDashboards) { + // 1. process datasources so the template placeholder is replaced with the actual value user selected + + const annotations = dashboard.annotations.list.map((annotation: AnnotationQuery) => { + if (annotation.datasource && annotation.datasource.uid && annotation.datasource.uid.startsWith('$')) { + // clean ${} from the datasource name + const dsName = annotation.datasource.uid.replace(/\$\{(.*)\}/, '$1'); + const input = inputs.dataSources.find((ds) => ds.name === dsName); + const userInput = input && form.dataSources.find((ds) => ds.type === input.pluginId); + if (userInput) { + return { + ...annotation, + datasource: { + ...annotation.datasource, + uid: userInput.uid, + }, + }; + } + } + return annotation; + }); + + const panels = dashboard.panels.map((panel: any) => { + if (panel.datasource && panel.datasource.uid && panel.datasource.uid.startsWith('$')) { + // clean ${} from the datasource name + const dsName = panel.datasource.uid.replace(/\$\{(.*)\}/, '$1'); + const input = inputs.dataSources.find((ds) => ds.name === dsName); + const userInput = input && form.dataSources.find((ds) => ds.type === input.pluginId); + + const queries = panel.targets.map((target: any) => { + if (target.datasource && target.datasource.uid && target.datasource.uid.startsWith('$')) { + const dsName = target.datasource.uid.replace(/\$\{(.*)\}/, '$1'); + const input = inputs.dataSources.find((ds) => ds.name === dsName); + const userInput = input && form.dataSources.find((ds) => ds.type === input.pluginId); + if (userInput) { + return { + ...target, + datasource: { + ...target.datasource, + uid: userInput.uid, + }, + }; + } + } + return target; + }); + + panel = { + ...panel, + targets: queries, + }; + + if (userInput) { + return { + ...panel, + datasource: { + ...panel.datasource, + uid: userInput.uid, + }, + }; + } + } + return panel; + }); + + const variables = dashboard.templating.list.map((variable: TypedVariableModel) => { + if (variable.type === 'query') { + if (variable.datasource && variable.datasource.uid?.startsWith('$')) { + // clean ${} from the datasource name + const dsName = variable.datasource.uid.replace(/\$\{(.*)\}/, '$1'); + const input = inputs.dataSources.find((ds) => ds.name === dsName); + const userInput = input && form.dataSources.find((ds) => ds.type === input.pluginId); + if (userInput) { + return { + ...variable, + datasource: userInput.uid, + }; + } + } + } + + if (variable.type === 'datasource') { + if (variable.current && variable.current.value && String(variable.current.value).startsWith('$')) { + // clean ${} from the datasource name + const dsName = String(variable.current.value).replace(/\$\{(.*)\}/, '$1'); + const input = inputs.dataSources.find((ds) => ds.name === dsName); + const userInput = input && form.dataSources.find((ds) => ds.type === input.pluginId); + if (userInput) { + return { + ...variable, + current: { + ...variable.current, + value: userInput.uid, + }, + }; + } + } + } + + return variable; + }); + + const dashboardWithDataSources: DashboardDataDTO = { + ...dashboard, + title: form.title, + annotations, + }; + // 2. if library panel doesn't exist in the instance, create it by hitting the library panel API + // you can use getLibraryPanel or getLibraryPanelInputs to check if panel exists and addLibraryPanel from library panel from public/app/features/library-panels/state/api.ts + + // 3. hit v1 API POST directly + + return; + } + this.props.importDashboard(form); };