This commit is contained in:
Haris Rozajac
2025-11-24 07:28:51 -07:00
parent 8108d3c795
commit d1d8e0b30f
5 changed files with 133 additions and 79 deletions
-10
View File
@@ -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
@@ -19,7 +19,6 @@ interface Props {
dashboardJson: AsyncState<{
json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown };
hasLibraryPanels?: boolean;
initialSaveModelVersion: 'v1' | 'v2';
}>;
isSharingExternally: boolean;
exportMode: ExportMode;
@@ -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');
});
});
@@ -102,24 +102,16 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> 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<ShareExportTabState> 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<ShareExportTabState> 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<ShareExportTabState> impleme
spec: exportableV1,
status: {},
},
initialSaveModelVersion,
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec1),
};
} catch (err) {
@@ -193,7 +182,6 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
json: {
error: `Failed to convert dashboard to v1. ${err}`,
},
initialSaveModelVersion,
hasLibraryPanels: undefined,
};
}
@@ -209,7 +197,6 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
spec,
status: {},
},
initialSaveModelVersion,
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec),
};
}
@@ -223,7 +210,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> 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<ShareExportTabState> 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<ShareExportTabState> impleme
'error' in exportable || !isV1ClassicDashboard(origDashboard)
? false
: hasLibraryPanelsInV1Dashboard(origDashboard),
initialSaveModelVersion,
};
};
@@ -297,9 +258,11 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> 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<ShareExportTabState> 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<ShareExportTab>)
/>
) : (
<Stack gap={2} direction="column">
<Field label={exportExternallyTranslation}>
<Field noMargin label={exportExternallyTranslation}>
<Switch
id="share-externally-toggle"
value={isSharingExternally}
@@ -1,11 +1,14 @@
import { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { dateTimeFormat } from '@grafana/data';
import { dateTimeFormat, TypedVariableModel } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { locationService, reportInteraction } from '@grafana/runtime';
import { locationService, reportInteraction, config } from '@grafana/runtime';
import { AnnotationQuery } from '@grafana/schema/dist/esm/veneer/dashboard.types';
import { Box, Legend, TextLink } from '@grafana/ui';
import { Form } from 'app/core/components/Form/Form';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard';
import { StoreState } from 'app/types/store';
import { clearLoadedDashboard, importDashboard } from '../state/actions';
@@ -48,6 +51,124 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
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);
};