wip
This commit is contained in:
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user