mirror of
https://github.com/grafana/grafana.git
synced 2025-12-22 12:44:34 +08:00
Compare commits
5 Commits
zoltan/pos
...
haris/dash
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44af4449a1 | ||
|
|
a5242dfaac | ||
|
|
4a5530c32f | ||
|
|
e0f210d8c4 | ||
|
|
d1d8e0b30f |
@@ -2136,11 +2136,6 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"public/app/features/dashboard-scene/sharing/ShareExportTab.tsx": {
|
|
||||||
"no-restricted-syntax": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx": {
|
"public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx": {
|
||||||
"no-restricted-syntax": {
|
"no-restricted-syntax": {
|
||||||
"count": 4
|
"count": 4
|
||||||
@@ -2942,11 +2937,6 @@
|
|||||||
"count": 1
|
"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": {
|
"public/app/features/manage-dashboards/state/actions.ts": {
|
||||||
"@typescript-eslint/consistent-type-assertions": {
|
"@typescript-eslint/consistent-type-assertions": {
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ interface Props {
|
|||||||
dashboardJson: AsyncState<{
|
dashboardJson: AsyncState<{
|
||||||
json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown };
|
json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown };
|
||||||
hasLibraryPanels?: boolean;
|
hasLibraryPanels?: boolean;
|
||||||
initialSaveModelVersion: 'v1' | 'v2';
|
|
||||||
}>;
|
}>;
|
||||||
isSharingExternally: boolean;
|
isSharingExternally: boolean;
|
||||||
exportMode: ExportMode;
|
exportMode: ExportMode;
|
||||||
@@ -41,13 +40,11 @@ export function ResourceExport({
|
|||||||
onViewYAML,
|
onViewYAML,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const hasLibraryPanels = dashboardJson.value?.hasLibraryPanels;
|
const hasLibraryPanels = dashboardJson.value?.hasLibraryPanels;
|
||||||
const initialSaveModelVersion = dashboardJson.value?.initialSaveModelVersion;
|
|
||||||
const isV2Dashboard =
|
const isV2Dashboard =
|
||||||
dashboardJson.value?.json && 'spec' in dashboardJson.value.json && 'elements' in dashboardJson.value.json.spec;
|
dashboardJson.value?.json && 'spec' in dashboardJson.value.json && 'elements' in dashboardJson.value.json.spec;
|
||||||
const showV2LibPanelAlert = isV2Dashboard && isSharingExternally && hasLibraryPanels;
|
const showV2LibPanelAlert = isV2Dashboard && isSharingExternally && hasLibraryPanels;
|
||||||
|
|
||||||
const switchExportLabel =
|
const switchExportLabel = isV2Dashboard
|
||||||
exportMode === ExportMode.V2Resource
|
|
||||||
? t('export.json.export-remove-ds-refs', 'Remove deployment details')
|
? t('export.json.export-remove-ds-refs', 'Remove deployment details')
|
||||||
: t('share-modal.export.share-externally-label', `Export for sharing externally`);
|
: t('share-modal.export.share-externally-label', `Export for sharing externally`);
|
||||||
const switchExportModeLabel = t('export.json.export-mode', 'Model');
|
const switchExportModeLabel = t('export.json.export-mode', 'Model');
|
||||||
@@ -56,34 +53,14 @@ export function ResourceExport({
|
|||||||
return (
|
return (
|
||||||
<Stack gap={2} direction="column">
|
<Stack gap={2} direction="column">
|
||||||
<Stack gap={1} direction="column">
|
<Stack gap={1} direction="column">
|
||||||
{initialSaveModelVersion === 'v1' && (
|
{!isV2Dashboard && (
|
||||||
<Stack alignItems="center">
|
|
||||||
<Label>{switchExportModeLabel}</Label>
|
|
||||||
<RadioButtonGroup
|
|
||||||
options={[
|
|
||||||
{ label: t('dashboard-scene.resource-export.label.classic', 'Classic'), value: ExportMode.Classic },
|
|
||||||
{
|
|
||||||
label: t('dashboard-scene.resource-export.label.v1-resource', 'V1 Resource'),
|
|
||||||
value: ExportMode.V1Resource,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('dashboard-scene.resource-export.label.v2-resource', 'V2 Resource'),
|
|
||||||
value: ExportMode.V2Resource,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={exportMode}
|
|
||||||
onChange={(value) => onExportModeChange(value)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{initialSaveModelVersion === 'v2' && (
|
|
||||||
<Stack alignItems="center">
|
<Stack alignItems="center">
|
||||||
<Label>{switchExportModeLabel}</Label>
|
<Label>{switchExportModeLabel}</Label>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
label: t('dashboard-scene.resource-export.label.v2-resource', 'V2 Resource'),
|
label: t('dashboard-scene.resource-export.label.classic', 'Classic'),
|
||||||
value: ExportMode.V2Resource,
|
value: ExportMode.Classic,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('dashboard-scene.resource-export.label.v1-resource', 'V1 Resource'),
|
label: t('dashboard-scene.resource-export.label.v1-resource', 'V1 Resource'),
|
||||||
@@ -108,9 +85,7 @@ export function ResourceExport({
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{(isV2Dashboard ||
|
{exportMode !== ExportMode.V1Resource && (
|
||||||
exportMode === ExportMode.Classic ||
|
|
||||||
(initialSaveModelVersion === 'v2' && exportMode === ExportMode.V1Resource)) && (
|
|
||||||
<Stack gap={1} alignItems="start">
|
<Stack gap={1} alignItems="start">
|
||||||
<Label>{switchExportLabel}</Label>
|
<Label>{switchExportLabel}</Label>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -113,9 +113,6 @@ describe('ShareExportTab', () => {
|
|||||||
// Should call transformSceneToV1 (not transform V2→V1)
|
// Should call transformSceneToV1 (not transform V2→V1)
|
||||||
expect(transformSceneToV1Spy).toHaveBeenCalled();
|
expect(transformSceneToV1Spy).toHaveBeenCalled();
|
||||||
expect(transformV2ToV1Spy).not.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
|
// If V2 dashboard → V1 Resource should auto-transform with V1 apiVersion
|
||||||
@@ -136,9 +133,6 @@ describe('ShareExportTab', () => {
|
|||||||
// Should auto-transform V2→V1
|
// Should auto-transform V2→V1
|
||||||
expect(transformSceneToV2Spy).toHaveBeenCalled(); // Get V2 spec first
|
expect(transformSceneToV2Spy).toHaveBeenCalled(); // Get V2 spec first
|
||||||
expect(transformV2ToV1Spy).toHaveBeenCalled(); // Then transform to V1
|
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
|
// 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
|
// Should call makeExportableV1 for external sharing
|
||||||
expect(makeExportableV1Spy).toHaveBeenCalled();
|
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
|
// Should not call V2→V1 transformation since source is already V2
|
||||||
expect(transformV2ToV1Spy).not.toHaveBeenCalled();
|
expect(transformV2ToV1Spy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
// Should report correct initial version
|
|
||||||
expect(result.initialSaveModelVersion).toBe('v2');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If V1 dashboard → V2 Resource should detect library panels correctly
|
// If V1 dashboard → V2 Resource should detect library panels correctly
|
||||||
@@ -201,7 +189,6 @@ describe('ShareExportTab', () => {
|
|||||||
|
|
||||||
// Should detect library panels from V1 dashboard
|
// Should detect library panels from V1 dashboard
|
||||||
expect(result.hasLibraryPanels).toBe(true);
|
expect(result.hasLibraryPanels).toBe(true);
|
||||||
expect(result.initialSaveModelVersion).toBe('v1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If V1 dashboard with dashboardNewLayouts disabled → V2 Resource should detect library panels correctly
|
// 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)
|
// Should detect library panels from V1 dashboard (first branch of the logic)
|
||||||
expect(result.hasLibraryPanels).toBe(true);
|
expect(result.hasLibraryPanels).toBe(true);
|
||||||
expect(result.initialSaveModelVersion).toBe('v1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If V1 dashboard without library panels → V2 Resource should return false
|
// If V1 dashboard without library panels → V2 Resource should return false
|
||||||
@@ -225,7 +211,6 @@ describe('ShareExportTab', () => {
|
|||||||
|
|
||||||
// Should not detect library panels
|
// Should not detect library panels
|
||||||
expect(result.hasLibraryPanels).toBe(false);
|
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)
|
// Should detect library panels from V2 dashboard elements (second branch of the logic)
|
||||||
expect(result.hasLibraryPanels).toBe(true);
|
expect(result.hasLibraryPanels).toBe(true);
|
||||||
expect(result.initialSaveModelVersion).toBe('v2');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test the second branch: V2 dashboard with V1 initial save model
|
// 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)
|
// Should detect library panels from V2 dashboard elements (second branch of the logic)
|
||||||
expect(result.hasLibraryPanels).toBe(true);
|
expect(result.hasLibraryPanels).toBe(true);
|
||||||
expect(result.initialSaveModelVersion).toBe('v1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If V2 dashboard without library panels → V2 Resource should return false
|
// If V2 dashboard without library panels → V2 Resource should return false
|
||||||
@@ -271,7 +254,6 @@ describe('ShareExportTab', () => {
|
|||||||
|
|
||||||
// Should not detect library panels
|
// Should not detect library panels
|
||||||
expect(result.hasLibraryPanels).toBe(false);
|
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('apiVersion');
|
||||||
expect(result.json).not.toHaveProperty('kind');
|
expect(result.json).not.toHaveProperty('kind');
|
||||||
expect(result.json).not.toHaveProperty('status');
|
expect(result.json).not.toHaveProperty('status');
|
||||||
|
|
||||||
// Should report correct initial version
|
|
||||||
expect(result.initialSaveModelVersion).toBe('v1');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -102,28 +102,16 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
|||||||
public getExportableDashboardJson = async (): Promise<{
|
public getExportableDashboardJson = async (): Promise<{
|
||||||
json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown };
|
json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown };
|
||||||
hasLibraryPanels?: boolean;
|
hasLibraryPanels?: boolean;
|
||||||
initialSaveModelVersion: 'v1' | 'v2';
|
|
||||||
}> => {
|
}> => {
|
||||||
const { isSharingExternally, exportMode } = this.state;
|
const { isSharingExternally, exportMode } = this.state;
|
||||||
|
|
||||||
const scene = getDashboardSceneFor(this);
|
const scene = getDashboardSceneFor(this);
|
||||||
const exportableDashboard = await scene.serializer.makeExportableExternally(scene);
|
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 origDashboard = scene.serializer.getSaveModel(scene);
|
||||||
const exportable = isSharingExternally ? exportableDashboard : origDashboard;
|
const exportable = isSharingExternally ? exportableDashboard : origDashboard;
|
||||||
const metadata = getMetadata(scene, Boolean(isSharingExternally));
|
const metadata = getMetadata(scene, Boolean(isSharingExternally));
|
||||||
|
|
||||||
if (
|
if (isDashboardV2Spec(origDashboard) && 'elements' in exportable && exportMode !== ExportMode.V1Resource) {
|
||||||
isDashboardV2Spec(origDashboard) &&
|
|
||||||
'elements' in exportable &&
|
|
||||||
initialSaveModelVersion === 'v2' &&
|
|
||||||
exportMode !== ExportMode.V1Resource
|
|
||||||
) {
|
|
||||||
this.setState({
|
|
||||||
exportMode: ExportMode.V2Resource,
|
|
||||||
});
|
|
||||||
|
|
||||||
// For automatic V2 path, also process library panels when sharing externally
|
// For automatic V2 path, also process library panels when sharing externally
|
||||||
let finalSpec = exportable;
|
let finalSpec = exportable;
|
||||||
if (isSharingExternally && isDashboardV2Spec(exportable)) {
|
if (isSharingExternally && isDashboardV2Spec(exportable)) {
|
||||||
@@ -132,7 +120,6 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
|||||||
if ('error' in result) {
|
if ('error' in result) {
|
||||||
return {
|
return {
|
||||||
json: { error: result.error },
|
json: { error: result.error },
|
||||||
initialSaveModelVersion,
|
|
||||||
hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'),
|
hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -147,14 +134,13 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
|||||||
spec: finalSpec,
|
spec: finalSpec,
|
||||||
status: {},
|
status: {},
|
||||||
},
|
},
|
||||||
initialSaveModelVersion,
|
|
||||||
hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'),
|
hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exportMode === ExportMode.V1Resource) {
|
if (exportMode === ExportMode.V1Resource) {
|
||||||
// Check if source is V2 and auto-transform to V1
|
// Check if source is V2 and auto-transform to V1
|
||||||
if (isDashboardV2Spec(origDashboard) && initialSaveModelVersion === 'v2') {
|
if (isDashboardV2Spec(origDashboard)) {
|
||||||
try {
|
try {
|
||||||
const spec = transformSceneToSaveModelSchemaV2(scene);
|
const spec = transformSceneToSaveModelSchemaV2(scene);
|
||||||
const metadata = getMetadata(scene, Boolean(isSharingExternally));
|
const metadata = getMetadata(scene, Boolean(isSharingExternally));
|
||||||
@@ -185,7 +171,6 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
|||||||
spec: exportableV1,
|
spec: exportableV1,
|
||||||
status: {},
|
status: {},
|
||||||
},
|
},
|
||||||
initialSaveModelVersion,
|
|
||||||
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec1),
|
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec1),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -193,7 +178,6 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
|||||||
json: {
|
json: {
|
||||||
error: `Failed to convert dashboard to v1. ${err}`,
|
error: `Failed to convert dashboard to v1. ${err}`,
|
||||||
},
|
},
|
||||||
initialSaveModelVersion,
|
|
||||||
hasLibraryPanels: undefined,
|
hasLibraryPanels: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -209,7 +193,6 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
|||||||
spec,
|
spec,
|
||||||
status: {},
|
status: {},
|
||||||
},
|
},
|
||||||
initialSaveModelVersion,
|
|
||||||
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec),
|
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -223,7 +206,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
|||||||
// Check if dashboard contains library panels based on dashboard version
|
// Check if dashboard contains library panels based on dashboard version
|
||||||
let hasLibraryPanels = false;
|
let hasLibraryPanels = false;
|
||||||
// Case: V1 dashboard loaded (with kubernetesDashboards enabled and dashboardNewLayouts disabled), and user explicitly selected V2Resource export mode
|
// 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);
|
hasLibraryPanels = hasLibraryPanelsInV1Dashboard(origDashboard);
|
||||||
} else if (isDashboardV2Spec(origDashboard)) {
|
} else if (isDashboardV2Spec(origDashboard)) {
|
||||||
// Case: V2 dashboard (either originally V2 or transformed from V1) being exported as V2Resource
|
// Case: V2 dashboard (either originally V2 or transformed from V1) being exported as V2Resource
|
||||||
@@ -239,35 +222,10 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
|||||||
spec: exportableV2,
|
spec: exportableV2,
|
||||||
status: {},
|
status: {},
|
||||||
},
|
},
|
||||||
initialSaveModelVersion,
|
|
||||||
hasLibraryPanels,
|
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
|
// 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
|
// At this point we know that dashboard should be V1 or could have produced an error
|
||||||
return {
|
return {
|
||||||
@@ -276,7 +234,6 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
|||||||
'error' in exportable || !isV1ClassicDashboard(origDashboard)
|
'error' in exportable || !isV1ClassicDashboard(origDashboard)
|
||||||
? false
|
? false
|
||||||
: hasLibraryPanelsInV1Dashboard(origDashboard),
|
: hasLibraryPanelsInV1Dashboard(origDashboard),
|
||||||
initialSaveModelVersion,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -297,9 +254,11 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
|||||||
const extension = isViewingYAML ? 'yaml' : 'json';
|
const extension = isViewingYAML ? 'yaml' : 'json';
|
||||||
saveAs(blob, `${title}-${time}.${extension}`);
|
saveAs(blob, `${title}-${time}.${extension}`);
|
||||||
|
|
||||||
|
const isV2Dashboard = 'spec' in dashboard.json && 'elements' in dashboard.json.spec;
|
||||||
|
|
||||||
DashboardInteractions.exportDownloadJsonClicked({
|
DashboardInteractions.exportDownloadJsonClicked({
|
||||||
externally: isSharingExternally,
|
externally: isSharingExternally,
|
||||||
dashboard_schema_version: dashboard.initialSaveModelVersion,
|
dashboard_schema_version: isV2Dashboard ? 'v2' : 'v1',
|
||||||
has_library_panels: Boolean(dashboard.hasLibraryPanels),
|
has_library_panels: Boolean(dashboard.hasLibraryPanels),
|
||||||
format: isViewingYAML ? 'yaml' : 'json',
|
format: isViewingYAML ? 'yaml' : 'json',
|
||||||
action: 'download',
|
action: 'download',
|
||||||
@@ -310,9 +269,11 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
|||||||
const dashboard = await this.getExportableDashboardJson();
|
const dashboard = await this.getExportableDashboardJson();
|
||||||
const { isSharingExternally, isViewingYAML, exportMode } = this.state;
|
const { isSharingExternally, isViewingYAML, exportMode } = this.state;
|
||||||
|
|
||||||
|
const isV2Dashboard = 'spec' in dashboard.json && 'elements' in dashboard.json.spec;
|
||||||
|
|
||||||
DashboardInteractions.exportCopyJsonClicked({
|
DashboardInteractions.exportCopyJsonClicked({
|
||||||
externally: isSharingExternally,
|
externally: isSharingExternally,
|
||||||
dashboard_schema_version: dashboard.initialSaveModelVersion,
|
dashboard_schema_version: isV2Dashboard ? 'v2' : 'v1',
|
||||||
has_library_panels: Boolean(dashboard.hasLibraryPanels),
|
has_library_panels: Boolean(dashboard.hasLibraryPanels),
|
||||||
export_mode: exportMode || 'classic',
|
export_mode: exportMode || 'classic',
|
||||||
format: isViewingYAML ? 'yaml' : 'json',
|
format: isViewingYAML ? 'yaml' : 'json',
|
||||||
@@ -402,7 +363,7 @@ function ShareExportTabRenderer({ model }: SceneComponentProps<ShareExportTab>)
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Stack gap={2} direction="column">
|
<Stack gap={2} direction="column">
|
||||||
<Field label={exportExternallyTranslation}>
|
<Field noMargin label={exportExternallyTranslation}>
|
||||||
<Switch
|
<Switch
|
||||||
id="share-externally-toggle"
|
id="share-externally-toggle"
|
||||||
value={isSharingExternally}
|
value={isSharingExternally}
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
import { dateTimeFormat } from '@grafana/data';
|
import { DataSourceInstanceSettings, dateTimeFormat, locationUtil, TypedVariableModel } from '@grafana/data';
|
||||||
import { Trans } from '@grafana/i18n';
|
import { Trans } from '@grafana/i18n';
|
||||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
import { locationService, reportInteraction, config } from '@grafana/runtime';
|
||||||
|
import { Panel } from '@grafana/schema/dist/esm/raw/dashboard/x/dashboard_types.gen';
|
||||||
|
import { AnnotationQuery, Dashboard } from '@grafana/schema/dist/esm/veneer/dashboard.types';
|
||||||
import { Box, Legend, TextLink } from '@grafana/ui';
|
import { Box, Legend, TextLink } from '@grafana/ui';
|
||||||
import { Form } from 'app/core/components/Form/Form';
|
import { Form } from 'app/core/components/Form/Form';
|
||||||
|
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
|
||||||
|
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
|
||||||
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
|
import { addLibraryPanel } from 'app/features/library-panels/state/api';
|
||||||
import { StoreState } from 'app/types/store';
|
import { StoreState } from 'app/types/store';
|
||||||
|
|
||||||
import { clearLoadedDashboard, importDashboard } from '../state/actions';
|
import { clearLoadedDashboard, importDashboard } from '../state/actions';
|
||||||
import { DashboardSource, ImportDashboardDTO } from '../state/reducers';
|
import { DashboardSource, DataSourceInput, ImportDashboardDTO, LibraryPanelInputState } from '../state/reducers';
|
||||||
|
|
||||||
import { ImportDashboardForm } from './ImportDashboardForm';
|
import { ImportDashboardForm } from './ImportDashboardForm';
|
||||||
|
|
||||||
@@ -45,9 +51,80 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
|
|||||||
uidReset: false,
|
uidReset: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = (form: ImportDashboardDTO) => {
|
onSubmit = async (form: ImportDashboardDTO) => {
|
||||||
reportInteraction(IMPORT_FINISHED_EVENT_NAME);
|
reportInteraction(IMPORT_FINISHED_EVENT_NAME);
|
||||||
|
|
||||||
|
const { dashboard, inputs, folder } = this.props;
|
||||||
|
|
||||||
|
// when kubernetesDashboard are enabled, we bypass api/dashboard/import
|
||||||
|
// and hit the k8s dashboard API directly
|
||||||
|
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) => {
|
||||||
|
return processAnnotation(annotation, inputs, form);
|
||||||
|
});
|
||||||
|
|
||||||
|
const panels = dashboard.panels.map((panel: Panel) => {
|
||||||
|
return processPanel(panel, inputs, form);
|
||||||
|
});
|
||||||
|
|
||||||
|
const variables = dashboard.templating.list.map((variable: TypedVariableModel) => {
|
||||||
|
return processVariable(variable, inputs, form);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dashboardWithDataSources: Dashboard = {
|
||||||
|
...dashboard,
|
||||||
|
title: form.title,
|
||||||
|
annotations,
|
||||||
|
panels,
|
||||||
|
templating: {
|
||||||
|
list: variables,
|
||||||
|
},
|
||||||
|
uid: form.uid,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newLibraryPanels = inputs.libraryPanels.filter((lp) => lp.state === LibraryPanelInputState.New);
|
||||||
|
|
||||||
|
// for library panels that don't exist in the instance, we create them by hitting the library panel API
|
||||||
|
for (const lp of newLibraryPanels) {
|
||||||
|
const libPanelWithPanelModel = new PanelModel(lp.model.model);
|
||||||
|
let { scopedVars, ...panelSaveModel } = libPanelWithPanelModel.getSaveModel();
|
||||||
|
panelSaveModel = {
|
||||||
|
libraryPanel: {
|
||||||
|
name: lp.model.name,
|
||||||
|
uid: lp.model.uid,
|
||||||
|
},
|
||||||
|
...panelSaveModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addLibraryPanel(panelSaveModel, folder.uid);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding library panel during dashboard import', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashboardK8SPayload: SaveDashboardCommand<Dashboard> = {
|
||||||
|
dashboard: dashboardWithDataSources,
|
||||||
|
k8s: {
|
||||||
|
annotations: {
|
||||||
|
'grafana.app/folder': form.folder.uid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// hit v1 API directly
|
||||||
|
const result = await getDashboardAPI('v1').saveDashboard(dashboardK8SPayload);
|
||||||
|
|
||||||
|
if (result.url) {
|
||||||
|
const dashboardUrl = locationUtil.stripBaseFromUrl(result.url);
|
||||||
|
locationService.push(dashboardUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.props.importDashboard(form);
|
this.props.importDashboard(form);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,3 +203,116 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
export const ImportDashboardOverview = connector(ImportDashboardOverviewUnConnected);
|
export const ImportDashboardOverview = connector(ImportDashboardOverviewUnConnected);
|
||||||
ImportDashboardOverview.displayName = 'ImportDashboardOverview';
|
ImportDashboardOverview.displayName = 'ImportDashboardOverview';
|
||||||
|
|
||||||
|
function hasUid(query: Record<string, unknown> | {}): query is { uid: string } {
|
||||||
|
return 'uid' in query && typeof query['uid'] === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Checks whether the templateized uid matches the user prodvided datasource input
|
||||||
|
*/
|
||||||
|
function checkUserInputMatch(
|
||||||
|
templateizedUid: string,
|
||||||
|
datasourceInputs: DataSourceInput[],
|
||||||
|
userDsInputs: DataSourceInstanceSettings[]
|
||||||
|
) {
|
||||||
|
const dsName = templateizedUid.replace(/\$\{(.*)\}/, '$1');
|
||||||
|
const input = datasourceInputs?.find((ds) => ds.name === dsName);
|
||||||
|
const userInput = input && userDsInputs.find((ds) => ds.type === input.pluginId);
|
||||||
|
return userInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processAnnotation(
|
||||||
|
annotation: AnnotationQuery,
|
||||||
|
inputs: { dataSources: DataSourceInput[] },
|
||||||
|
form: ImportDashboardDTO
|
||||||
|
): AnnotationQuery {
|
||||||
|
if (annotation.datasource && annotation.datasource.uid && annotation.datasource.uid.startsWith('$')) {
|
||||||
|
const userInput = checkUserInputMatch(annotation.datasource.uid, inputs.dataSources, form.dataSources);
|
||||||
|
if (userInput) {
|
||||||
|
return {
|
||||||
|
...annotation,
|
||||||
|
datasource: {
|
||||||
|
...annotation.datasource,
|
||||||
|
uid: userInput.uid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processPanel(panel: Panel, inputs: { dataSources: DataSourceInput[] }, form: ImportDashboardDTO): Panel {
|
||||||
|
if (panel.datasource && panel.datasource.uid && panel.datasource.uid.startsWith('$')) {
|
||||||
|
const userInput = checkUserInputMatch(panel.datasource.uid, inputs.dataSources, form.dataSources);
|
||||||
|
|
||||||
|
const queries = panel.targets?.map((target) => {
|
||||||
|
if (target.datasource && hasUid(target.datasource) && target.datasource.uid.startsWith('$')) {
|
||||||
|
const userInput = checkUserInputMatch(target.datasource.uid, inputs.dataSources, form.dataSources);
|
||||||
|
if (userInput) {
|
||||||
|
return {
|
||||||
|
...target,
|
||||||
|
datasource: {
|
||||||
|
...target.datasource,
|
||||||
|
uid: userInput.uid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userInput) {
|
||||||
|
return {
|
||||||
|
...panel,
|
||||||
|
targets: queries,
|
||||||
|
datasource: {
|
||||||
|
...panel.datasource,
|
||||||
|
uid: userInput.uid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processVariable(
|
||||||
|
variable: TypedVariableModel,
|
||||||
|
inputs: { dataSources: DataSourceInput[] },
|
||||||
|
form: ImportDashboardDTO
|
||||||
|
): TypedVariableModel {
|
||||||
|
if (variable.type === 'query') {
|
||||||
|
if (variable.datasource && variable.datasource.uid?.startsWith('$')) {
|
||||||
|
const userInput = checkUserInputMatch(variable.datasource.uid, inputs.dataSources, form.dataSources);
|
||||||
|
if (userInput) {
|
||||||
|
return {
|
||||||
|
...variable,
|
||||||
|
datasource: {
|
||||||
|
...variable.datasource,
|
||||||
|
uid: userInput.uid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variable.type === 'datasource') {
|
||||||
|
if (variable.current && variable.current.value && String(variable.current.value).startsWith('$')) {
|
||||||
|
const userInput = checkUserInputMatch(String(variable.current.value), inputs.dataSources, form.dataSources);
|
||||||
|
if (userInput) {
|
||||||
|
return {
|
||||||
|
...variable,
|
||||||
|
current: {
|
||||||
|
selected: variable.current.selected,
|
||||||
|
text: userInput.name,
|
||||||
|
value: userInput.uid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return variable;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6215,7 +6215,6 @@
|
|||||||
"classic": "Classic",
|
"classic": "Classic",
|
||||||
"json": "JSON",
|
"json": "JSON",
|
||||||
"v1-resource": "V1 Resource",
|
"v1-resource": "V1 Resource",
|
||||||
"v2-resource": "V2 Resource",
|
|
||||||
"yaml": "YAML"
|
"yaml": "YAML"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user