diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts index 038812d89c6..5c344340666 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts @@ -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) => 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 () => { diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts index 6fea11bd39f..dcef2dfc6b6 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts @@ -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> = []; const variableLookup: { [key: string]: any } = {}; const libraryPanels: Map = new Map(); @@ -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, + }; + } } }