Dashboard Export: Fix datasource variable templating in dashboard export (#109764)

* wip investigation

* improve

* cleanup

* betterer

* Improve v2 import
This commit is contained in:
Haris Rozajac
2025-09-17 18:55:03 +00:00
committed by GitHub
parent 14b6e60f31
commit e34b90a024
6 changed files with 58 additions and 27 deletions
@@ -144,7 +144,7 @@ describe('dashboard exporter v1', () => {
expect(exported.templating.list[0].datasource.uid).toBe('${DS_GFDB}'); expect(exported.templating.list[0].datasource.uid).toBe('${DS_GFDB}');
}); });
it('do not expose datasource name and id in a in a template variable of type datasource', async () => { it('templateize datasource uid in a datasource variable that is used in a panel', async () => {
const dashboard: Dashboard = { const dashboard: Dashboard = {
title: 'My dashboard', title: 'My dashboard',
revision: 1, revision: 1,
@@ -159,6 +159,10 @@ describe('dashboard exporter v1', () => {
type: 'timeseries', type: 'timeseries',
title: 'My panel title', title: 'My panel title',
gridPos: { x: 0, y: 0, w: 1, h: 1 }, gridPos: { x: 0, y: 0, w: 1, h: 1 },
datasource: {
type: 'prometheus',
uid: '${ds_var}',
},
}, },
], ],
templating: { templating: {
@@ -172,7 +176,7 @@ describe('dashboard exporter v1', () => {
hide: 0, hide: 0,
includeAll: false, includeAll: false,
multi: false, multi: false,
name: 'query1', name: 'ds_var',
options: [], options: [],
query: 'prometheus', query: 'prometheus',
refresh: 1, refresh: 1,
@@ -188,7 +192,11 @@ describe('dashboard exporter v1', () => {
}); });
const exported = (await makeExportableV1(dashboardModel)) as DashboardJson; const exported = (await makeExportableV1(dashboardModel)) as DashboardJson;
const value = exported?.templating?.list ? exported?.templating?.list[0].current : ''; const value = exported?.templating?.list ? exported?.templating?.list[0].current : '';
expect(value).toEqual({}); expect(value).toEqual({
selected: true,
text: '',
value: '${DS_GFDB}',
});
}); });
it('replaces datasource ref in library panel', async () => { it('replaces datasource ref in library panel', async () => {
@@ -19,7 +19,7 @@ import { buildPanelKind } from 'app/features/dashboard/api/ResponseTransformers'
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel, GridPos } from 'app/features/dashboard/state/PanelModel'; import { PanelModel, GridPos } from 'app/features/dashboard/state/PanelModel';
import { getLibraryPanel } from 'app/features/library-panels/state/api'; import { getLibraryPanel } from 'app/features/library-panels/state/api';
import { variableRegex } from 'app/features/variables/utils'; import { variableRegexExec } from 'app/features/variables/utils';
import { dispatch } from 'app/store/store'; import { dispatch } from 'app/store/store';
import { isPanelModelLibraryPanel } from '../../../library-panels/guard'; import { isPanelModelLibraryPanel } from '../../../library-panels/guard';
@@ -110,6 +110,8 @@ export async function makeExportableV1(dashboard: DashboardModel) {
variableLookup[variable.name] = variable; variableLookup[variable.name] = variable;
} }
const datasourceVariableRefNameMap: { [key: string]: string } = {};
const templateizeDatasourceUsage = (obj: any, fallback?: DataSourceRef) => { const templateizeDatasourceUsage = (obj: any, fallback?: DataSourceRef) => {
if (obj.datasource === undefined) { if (obj.datasource === undefined) {
obj.datasource = fallback; obj.datasource = fallback;
@@ -120,11 +122,11 @@ export async function makeExportableV1(dashboard: DashboardModel) {
let datasourceVariable: any = null; let datasourceVariable: any = null;
const datasourceUid: string | undefined = datasource?.uid; const datasourceUid: string | undefined = datasource?.uid;
const match = datasourceUid && variableRegex.exec(datasourceUid); const match = datasourceUid && variableRegexExec(datasourceUid);
let varName: string | undefined;
// ignore data source properties that contain a variable
if (match) { if (match) {
const varName = match[1] || match[2] || match[4]; varName = match[1] || match[2] || match[4];
datasourceVariable = variableLookup[varName]; datasourceVariable = variableLookup[varName];
if (datasourceVariable && datasourceVariable.current) { if (datasourceVariable && datasourceVariable.current) {
datasource = datasourceVariable.current.value; datasource = datasourceVariable.current.value;
@@ -146,14 +148,10 @@ export async function makeExportableV1(dashboard: DashboardModel) {
version: ds.meta.info.version || '1.0.0', version: ds.meta.info.version || '1.0.0',
}; };
// if used via variable we can skip templatizing usage
if (datasourceVariable) {
return;
}
const libraryPanel = obj.libraryPanel; const libraryPanel = obj.libraryPanel;
const libraryPanelSuffix = !!libraryPanel ? '-for-library-panel' : ''; const libraryPanelSuffix = !!libraryPanel ? '-for-library-panel' : '';
let refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase() + libraryPanelSuffix.toUpperCase(); let refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase() + libraryPanelSuffix.toUpperCase();
const templatedUid = '${' + refName + '}';
datasources[refName] = { datasources[refName] = {
name: refName, name: refName,
@@ -174,7 +172,14 @@ export async function makeExportableV1(dashboard: DashboardModel) {
}; };
} }
obj.datasource = { type: ds.meta.id, uid: '${' + refName + '}' }; // if it panel or query is relying on a datasource variable
// skip templating datasource uid but save the reference so we can set datasource variable's current prop
if (datasourceVariable && varName) {
datasourceVariableRefNameMap[varName] = '${' + refName + '}';
return;
}
obj.datasource = { type: ds.meta.id, uid: templatedUid };
}); });
}; };
@@ -240,7 +245,16 @@ export async function makeExportableV1(dashboard: DashboardModel) {
variable.refresh = variable.refresh =
variable.refresh !== VariableRefresh.never ? variable.refresh : VariableRefresh.onDashboardLoad; variable.refresh !== VariableRefresh.never ? variable.refresh : VariableRefresh.onDashboardLoad;
} else if (variable.type === 'datasource') { } else if (variable.type === 'datasource') {
variable.current = {}; const templateizedUID = datasourceVariableRefNameMap[variable.name];
if (templateizedUID) {
variable.current = {
text: '',
value: templateizedUID,
selected: true,
};
} else {
variable.current = {};
}
} else if (variable.type === 'adhoc') { } else if (variable.type === 'adhoc') {
await templateizeDatasourceUsage(variable); await templateizeDatasourceUsage(variable);
} }
@@ -97,7 +97,7 @@ export const ImportDashboardFormV2 = ({
return ( return (
<Field <Field
label={input.label} label={input.pluginId}
description={input.description} description={input.description}
key={input.pluginId} key={input.pluginId}
invalid={!!errors[dataSourceOption]} invalid={!!errors[dataSourceOption]}
@@ -120,6 +120,7 @@ export const ImportDashboardFormV2 = ({
[input.pluginId]: { [input.pluginId]: {
uid: ds.uid, uid: ds.uid,
type: ds.type, type: ds.type,
name: ds.name,
}, },
})); }));
}} }}
@@ -98,16 +98,24 @@ export function ImportDashboardOverviewV2() {
} }
} }
} else if (variable.kind === 'DatasourceVariable') { } else if (variable.kind === 'DatasourceVariable') {
return { const dsType = variable.spec.pluginId;
...variable, if (dsType) {
spec: { if (form[`datasource-${dsType}`]) {
...variable.spec, const ds = form[`datasource-${dsType}`];
current: { return {
text: '', ...variable,
value: '', spec: {
}, ...variable.spec,
}, current: {
}; // @ts-ignore
text: ds.name,
// @ts-ignore
value: ds.uid,
},
},
};
}
}
} }
return variable; return variable;
}), }),
@@ -121,7 +121,7 @@ export const ImportDashboardForm = ({
const current = watchDataSources ?? []; const current = watchDataSources ?? [];
return ( return (
<Field <Field
label={input.label} label={input.pluginId}
description={input.description} description={input.description}
key={dataSourceOption} key={dataSourceOption}
invalid={errors.dataSources && !!errors.dataSources[index]} invalid={errors.dataSources && !!errors.dataSources[index]}
@@ -328,7 +328,7 @@ export async function processV2DatasourceInput(
dataSourceInput = { dataSourceInput = {
name: datasource.name, name: datasource.name,
label: datasource.name, label: datasource.name,
info: `Select a ${datasource.name} data source`, info: `Select a ${datasource.type} data source`,
value: datasource.uid, value: datasource.uid,
type: InputType.DataSource, type: InputType.DataSource,
pluginId: datasource.meta?.id, pluginId: datasource.meta?.id,