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}');
});
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 = {
title: 'My dashboard',
revision: 1,
@@ -159,6 +159,10 @@ describe('dashboard exporter v1', () => {
type: 'timeseries',
title: 'My panel title',
gridPos: { x: 0, y: 0, w: 1, h: 1 },
datasource: {
type: 'prometheus',
uid: '${ds_var}',
},
},
],
templating: {
@@ -172,7 +176,7 @@ describe('dashboard exporter v1', () => {
hide: 0,
includeAll: false,
multi: false,
name: 'query1',
name: 'ds_var',
options: [],
query: 'prometheus',
refresh: 1,
@@ -188,7 +192,11 @@ describe('dashboard exporter v1', () => {
});
const exported = (await makeExportableV1(dashboardModel)) as DashboardJson;
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 () => {
@@ -19,7 +19,7 @@ import { buildPanelKind } from 'app/features/dashboard/api/ResponseTransformers'
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel, GridPos } from 'app/features/dashboard/state/PanelModel';
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 { isPanelModelLibraryPanel } from '../../../library-panels/guard';
@@ -110,6 +110,8 @@ export async function makeExportableV1(dashboard: DashboardModel) {
variableLookup[variable.name] = variable;
}
const datasourceVariableRefNameMap: { [key: string]: string } = {};
const templateizeDatasourceUsage = (obj: any, fallback?: DataSourceRef) => {
if (obj.datasource === undefined) {
obj.datasource = fallback;
@@ -120,11 +122,11 @@ export async function makeExportableV1(dashboard: DashboardModel) {
let datasourceVariable: any = null;
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) {
const varName = match[1] || match[2] || match[4];
varName = match[1] || match[2] || match[4];
datasourceVariable = variableLookup[varName];
if (datasourceVariable && datasourceVariable.current) {
datasource = datasourceVariable.current.value;
@@ -146,14 +148,10 @@ export async function makeExportableV1(dashboard: DashboardModel) {
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 libraryPanelSuffix = !!libraryPanel ? '-for-library-panel' : '';
let refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase() + libraryPanelSuffix.toUpperCase();
const templatedUid = '${' + refName + '}';
datasources[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 !== VariableRefresh.never ? variable.refresh : VariableRefresh.onDashboardLoad;
} 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') {
await templateizeDatasourceUsage(variable);
}
@@ -97,7 +97,7 @@ export const ImportDashboardFormV2 = ({
return (
<Field
label={input.label}
label={input.pluginId}
description={input.description}
key={input.pluginId}
invalid={!!errors[dataSourceOption]}
@@ -120,6 +120,7 @@ export const ImportDashboardFormV2 = ({
[input.pluginId]: {
uid: ds.uid,
type: ds.type,
name: ds.name,
},
}));
}}
@@ -98,16 +98,24 @@ export function ImportDashboardOverviewV2() {
}
}
} else if (variable.kind === 'DatasourceVariable') {
return {
...variable,
spec: {
...variable.spec,
current: {
text: '',
value: '',
},
},
};
const dsType = variable.spec.pluginId;
if (dsType) {
if (form[`datasource-${dsType}`]) {
const ds = form[`datasource-${dsType}`];
return {
...variable,
spec: {
...variable.spec,
current: {
// @ts-ignore
text: ds.name,
// @ts-ignore
value: ds.uid,
},
},
};
}
}
}
return variable;
}),
@@ -121,7 +121,7 @@ export const ImportDashboardForm = ({
const current = watchDataSources ?? [];
return (
<Field
label={input.label}
label={input.pluginId}
description={input.description}
key={dataSourceOption}
invalid={errors.dataSources && !!errors.dataSources[index]}
@@ -328,7 +328,7 @@ export async function processV2DatasourceInput(
dataSourceInput = {
name: datasource.name,
label: datasource.name,
info: `Select a ${datasource.name} data source`,
info: `Select a ${datasource.type} data source`,
value: datasource.uid,
type: InputType.DataSource,
pluginId: datasource.meta?.id,