mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardExport: Fixes issues sharing dashboards where query data source was set wrong (#48410)
This commit is contained in:
@@ -46,116 +46,6 @@ variableAdapters.register(createDataSourceVariableAdapter());
|
||||
|
||||
it('handles a default datasource in a template variable', async () => {
|
||||
const dashboard: any = {
|
||||
annotations: {
|
||||
list: [
|
||||
{
|
||||
builtIn: 1,
|
||||
datasource: '-- Grafana --',
|
||||
enable: true,
|
||||
hide: true,
|
||||
iconColor: 'rgba(0, 211, 255, 1)',
|
||||
name: 'Annotations & Alerts',
|
||||
target: {
|
||||
limit: 100,
|
||||
matchAny: false,
|
||||
tags: [],
|
||||
type: 'dashboard',
|
||||
},
|
||||
type: 'dashboard',
|
||||
},
|
||||
],
|
||||
},
|
||||
editable: true,
|
||||
fiscalYearStartMonth: 0,
|
||||
graphTooltip: 0,
|
||||
id: 331,
|
||||
iteration: 1642157860116,
|
||||
links: [],
|
||||
liveNow: false,
|
||||
panels: [
|
||||
{
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
color: {
|
||||
mode: 'palette-classic',
|
||||
},
|
||||
custom: {
|
||||
axisLabel: '',
|
||||
axisPlacement: 'auto',
|
||||
barAlignment: 0,
|
||||
drawStyle: 'line',
|
||||
fillOpacity: 0,
|
||||
gradientMode: 'none',
|
||||
hideFrom: {
|
||||
legend: false,
|
||||
tooltip: false,
|
||||
viz: false,
|
||||
},
|
||||
lineInterpolation: 'linear',
|
||||
lineWidth: 1,
|
||||
pointSize: 5,
|
||||
scaleDistribution: {
|
||||
type: 'linear',
|
||||
},
|
||||
showPoints: 'auto',
|
||||
spanNulls: false,
|
||||
stacking: {
|
||||
group: 'A',
|
||||
mode: 'none',
|
||||
},
|
||||
thresholdsStyle: {
|
||||
mode: 'off',
|
||||
},
|
||||
},
|
||||
mappings: [],
|
||||
thresholds: {
|
||||
mode: 'absolute',
|
||||
steps: [
|
||||
{
|
||||
color: 'green',
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
value: 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
gridPos: {
|
||||
h: 9,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
id: 2,
|
||||
options: {
|
||||
legend: {
|
||||
calcs: [],
|
||||
displayMode: 'list',
|
||||
placement: 'bottom',
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'single',
|
||||
sort: 'none',
|
||||
},
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
datasource: {
|
||||
type: 'testdata',
|
||||
uid: 'PD8C576611E62080A',
|
||||
},
|
||||
expr: '{filename="/var/log/system.log"}',
|
||||
refId: 'A',
|
||||
},
|
||||
],
|
||||
title: 'Panel Title',
|
||||
type: 'timeseries',
|
||||
},
|
||||
],
|
||||
templating: {
|
||||
list: [
|
||||
{
|
||||
@@ -186,6 +76,27 @@ it('handles a default datasource in a template variable', async () => {
|
||||
expect(exported.templating.list[0].datasource.uid).toBe('${DS_GFDB}');
|
||||
});
|
||||
|
||||
it('If a panel queries has no datasource prop ignore it', async () => {
|
||||
const dashboard: any = {
|
||||
panels: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'graph',
|
||||
datasource: {
|
||||
uid: 'other',
|
||||
type: 'other',
|
||||
},
|
||||
targets: [{ refId: 'A', a: 'A' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const dashboardModel = new DashboardModel(dashboard, {}, () => []);
|
||||
const exporter = new DashboardExporter();
|
||||
const exported: any = await exporter.makeExportable(dashboardModel);
|
||||
expect(exported.panels[0].datasource).toEqual({ uid: '${DS_OTHER}', type: 'other' });
|
||||
expect(exported.panels[0].targets[0].datasource).toEqual({ uid: '${DS_OTHER}', type: 'other' });
|
||||
});
|
||||
|
||||
describe('given dashboard with repeated panels', () => {
|
||||
let dash: any, exported: any;
|
||||
|
||||
@@ -323,7 +234,7 @@ describe('given dashboard with repeated panels', () => {
|
||||
const panel = exported.panels[7];
|
||||
expect(exported.__inputs.some((ds: Record<string, string>) => ds.name === 'DS_GFDB')).toBeTruthy();
|
||||
expect(panel.datasource.uid).toBe('${DS_GFDB}');
|
||||
expect(panel.targets[0].datasource).toBe('${DS_GFDB}');
|
||||
expect(panel.targets[0].datasource).toEqual({ type: 'testdb', uid: '${DS_GFDB}' });
|
||||
});
|
||||
|
||||
it('should not include default datasource in __inputs unnecessarily', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defaults, each, sortBy } from 'lodash';
|
||||
|
||||
import { PanelPluginMeta } from '@grafana/data';
|
||||
import { DataSourceRef, PanelPluginMeta } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import config from 'app/core/config';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
@@ -47,7 +47,7 @@ export interface LibraryElementExport {
|
||||
}
|
||||
|
||||
export class DashboardExporter {
|
||||
makeExportable(dashboard: DashboardModel) {
|
||||
async makeExportable(dashboard: DashboardModel) {
|
||||
// clean up repeated rows and panels,
|
||||
// this is done on the live real dashboard instance, not on a clone
|
||||
// so we need to undo this
|
||||
@@ -63,7 +63,6 @@ export class DashboardExporter {
|
||||
const inputs: Input[] = [];
|
||||
const requires: Requires = {};
|
||||
const datasources: DataSources = {};
|
||||
const promises: Array<Promise<void>> = [];
|
||||
const variableLookup: { [key: string]: any } = {};
|
||||
const libraryPanels: Map<string, LibraryElementExport> = new Map<string, LibraryElementExport>();
|
||||
|
||||
@@ -71,7 +70,12 @@ export class DashboardExporter {
|
||||
variableLookup[variable.name] = variable;
|
||||
}
|
||||
|
||||
const templateizeDatasourceUsage = (obj: any) => {
|
||||
const templateizeDatasourceUsage = (obj: any, fallback?: DataSourceRef) => {
|
||||
if (obj.datasource === undefined) {
|
||||
obj.datasource = fallback;
|
||||
return;
|
||||
}
|
||||
|
||||
let datasource: string = obj.datasource;
|
||||
let datasourceVariable: any = null;
|
||||
|
||||
@@ -86,70 +90,59 @@ export class DashboardExporter {
|
||||
}
|
||||
}
|
||||
|
||||
promises.push(
|
||||
getDataSourceSrv()
|
||||
.get(datasource)
|
||||
.then((ds) => {
|
||||
if (ds.meta?.builtIn) {
|
||||
return;
|
||||
}
|
||||
return getDataSourceSrv()
|
||||
.get(datasource)
|
||||
.then((ds) => {
|
||||
if (ds.meta?.builtIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// add data source type to require list
|
||||
requires['datasource' + ds.meta?.id] = {
|
||||
type: 'datasource',
|
||||
id: ds.meta.id,
|
||||
name: ds.meta.name,
|
||||
version: ds.meta.info.version || '1.0.0',
|
||||
};
|
||||
// add data source type to require list
|
||||
requires['datasource' + ds.meta?.id] = {
|
||||
type: 'datasource',
|
||||
id: ds.meta.id,
|
||||
name: ds.meta.name,
|
||||
version: ds.meta.info.version || '1.0.0',
|
||||
};
|
||||
|
||||
// if used via variable we can skip templatizing usage
|
||||
if (datasourceVariable) {
|
||||
return;
|
||||
}
|
||||
// if used via variable we can skip templatizing usage
|
||||
if (datasourceVariable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase();
|
||||
datasources[refName] = {
|
||||
name: refName,
|
||||
label: ds.name,
|
||||
description: '',
|
||||
type: 'datasource',
|
||||
pluginId: ds.meta?.id,
|
||||
pluginName: ds.meta?.name,
|
||||
};
|
||||
const refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase();
|
||||
datasources[refName] = {
|
||||
name: refName,
|
||||
label: ds.name,
|
||||
description: '',
|
||||
type: 'datasource',
|
||||
pluginId: ds.meta?.id,
|
||||
pluginName: ds.meta?.name,
|
||||
};
|
||||
|
||||
if (!obj.datasource || typeof obj.datasource === 'string') {
|
||||
obj.datasource = '${' + refName + '}';
|
||||
} else {
|
||||
obj.datasource.uid = '${' + refName + '}';
|
||||
}
|
||||
})
|
||||
);
|
||||
obj.datasource = { type: ds.meta.id, uid: '${' + refName + '}' };
|
||||
});
|
||||
};
|
||||
|
||||
const processPanel = (panel: PanelModel) => {
|
||||
const isRegularPanel =
|
||||
(panel.repeatPanelId === undefined || panel.repeatPanelId === null) &&
|
||||
!('collapsed' in panel) &&
|
||||
!('panels' in panel);
|
||||
const processPanel = async (panel: PanelModel) => {
|
||||
if (panel.type !== 'row') {
|
||||
await templateizeDatasourceUsage(panel);
|
||||
|
||||
if (isRegularPanel) {
|
||||
templateizeDatasourceUsage(panel);
|
||||
}
|
||||
|
||||
if (panel.targets) {
|
||||
for (const target of panel.targets) {
|
||||
templateizeDatasourceUsage(target);
|
||||
if (panel.targets) {
|
||||
for (const target of panel.targets) {
|
||||
await templateizeDatasourceUsage(target, panel.datasource!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const panelDef: PanelPluginMeta = config.panels[panel.type];
|
||||
if (panelDef) {
|
||||
requires['panel' + panelDef.id] = {
|
||||
type: 'panel',
|
||||
id: panelDef.id,
|
||||
name: panelDef.name,
|
||||
version: panelDef.info.version,
|
||||
};
|
||||
const panelDef: PanelPluginMeta = config.panels[panel.type];
|
||||
if (panelDef) {
|
||||
requires['panel' + panelDef.id] = {
|
||||
type: 'panel',
|
||||
id: panelDef.id,
|
||||
name: panelDef.name,
|
||||
version: panelDef.info.version,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -163,95 +156,93 @@ export class DashboardExporter {
|
||||
}
|
||||
};
|
||||
|
||||
// check up panel data sources
|
||||
for (const panel of saveModel.panels) {
|
||||
processPanel(panel);
|
||||
try {
|
||||
// check up panel data sources
|
||||
for (const panel of saveModel.panels) {
|
||||
await processPanel(panel);
|
||||
|
||||
// handle collapsed rows
|
||||
if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) {
|
||||
for (const rowPanel of panel.panels) {
|
||||
processPanel(rowPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// templatize template vars
|
||||
for (const variable of saveModel.getVariables()) {
|
||||
if (isQuery(variable)) {
|
||||
templateizeDatasourceUsage(variable);
|
||||
variable.options = [];
|
||||
variable.current = {} as unknown as VariableOption;
|
||||
variable.refresh =
|
||||
variable.refresh !== VariableRefresh.never ? variable.refresh : VariableRefresh.onDashboardLoad;
|
||||
}
|
||||
}
|
||||
|
||||
// templatize annotations vars
|
||||
for (const annotationDef of saveModel.annotations.list) {
|
||||
templateizeDatasourceUsage(annotationDef);
|
||||
}
|
||||
|
||||
// add grafana version
|
||||
requires['grafana'] = {
|
||||
type: 'grafana',
|
||||
id: 'grafana',
|
||||
name: 'Grafana',
|
||||
version: config.buildInfo.version,
|
||||
};
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(() => {
|
||||
each(datasources, (value: any) => {
|
||||
inputs.push(value);
|
||||
});
|
||||
|
||||
// we need to process all panels again after all the promises are resolved
|
||||
// so all data sources, variables and targets have been templateized when we process library panels
|
||||
for (const panel of saveModel.panels) {
|
||||
processLibraryPanels(panel);
|
||||
if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) {
|
||||
for (const rowPanel of panel.panels) {
|
||||
processLibraryPanels(rowPanel);
|
||||
}
|
||||
// handle collapsed rows
|
||||
if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) {
|
||||
for (const rowPanel of panel.panels) {
|
||||
await processPanel(rowPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// templatize constants
|
||||
for (const variable of saveModel.getVariables()) {
|
||||
if (isConstant(variable)) {
|
||||
const refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
|
||||
inputs.push({
|
||||
name: refName,
|
||||
type: 'constant',
|
||||
label: variable.label || variable.name,
|
||||
value: variable.query,
|
||||
description: '',
|
||||
});
|
||||
// update current and option
|
||||
variable.query = '${' + refName + '}';
|
||||
variable.current = {
|
||||
value: variable.query,
|
||||
text: variable.query,
|
||||
selected: false,
|
||||
};
|
||||
variable.options = [variable.current];
|
||||
}
|
||||
// templatize template vars
|
||||
for (const variable of saveModel.getVariables()) {
|
||||
if (isQuery(variable)) {
|
||||
await templateizeDatasourceUsage(variable);
|
||||
variable.options = [];
|
||||
variable.current = {} as unknown as VariableOption;
|
||||
variable.refresh =
|
||||
variable.refresh !== VariableRefresh.never ? variable.refresh : VariableRefresh.onDashboardLoad;
|
||||
}
|
||||
}
|
||||
|
||||
// make inputs and requires a top thing
|
||||
const newObj: { [key: string]: {} } = {};
|
||||
newObj['__inputs'] = inputs;
|
||||
newObj['__elements'] = [...libraryPanels.values()];
|
||||
newObj['__requires'] = sortBy(requires, ['id']);
|
||||
// templatize annotations vars
|
||||
for (const annotationDef of saveModel.annotations.list) {
|
||||
await templateizeDatasourceUsage(annotationDef);
|
||||
}
|
||||
|
||||
defaults(newObj, saveModel);
|
||||
return newObj;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Export failed:', err);
|
||||
return {
|
||||
error: err,
|
||||
};
|
||||
// add grafana version
|
||||
requires['grafana'] = {
|
||||
type: 'grafana',
|
||||
id: 'grafana',
|
||||
name: 'Grafana',
|
||||
version: config.buildInfo.version,
|
||||
};
|
||||
|
||||
each(datasources, (value: any) => {
|
||||
inputs.push(value);
|
||||
});
|
||||
|
||||
// we need to process all panels again after all the promises are resolved
|
||||
// so all data sources, variables and targets have been templateized when we process library panels
|
||||
for (const panel of saveModel.panels) {
|
||||
processLibraryPanels(panel);
|
||||
if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) {
|
||||
for (const rowPanel of panel.panels) {
|
||||
processLibraryPanels(rowPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// templatize constants
|
||||
for (const variable of saveModel.getVariables()) {
|
||||
if (isConstant(variable)) {
|
||||
const refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
|
||||
inputs.push({
|
||||
name: refName,
|
||||
type: 'constant',
|
||||
label: variable.label || variable.name,
|
||||
value: variable.query,
|
||||
description: '',
|
||||
});
|
||||
// update current and option
|
||||
variable.query = '${' + refName + '}';
|
||||
variable.current = {
|
||||
value: variable.query,
|
||||
text: variable.query,
|
||||
selected: false,
|
||||
};
|
||||
variable.options = [variable.current];
|
||||
}
|
||||
}
|
||||
|
||||
// make inputs and requires a top thing
|
||||
const newObj: { [key: string]: {} } = {};
|
||||
newObj['__inputs'] = inputs;
|
||||
newObj['__elements'] = [...libraryPanels.values()];
|
||||
newObj['__requires'] = sortBy(requires, ['id']);
|
||||
|
||||
defaults(newObj, saveModel);
|
||||
return newObj;
|
||||
} catch (err) {
|
||||
console.error('Export failed:', err);
|
||||
return {
|
||||
error: err,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user