Skip to content

[WIP] Complete dashboard export #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 256 additions & 0 deletions grafana_client/export/DashboardExporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
// https://github.com/grafana/grafana/blob/v8.3.6/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts
// License: AGPL-3.0

import { defaults, each, sortBy } from 'lodash';

import config from 'app/core/config';
import { DashboardModel } from '../../state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state';
import { PanelPluginMeta } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { VariableOption, VariableRefresh } from '../../../variables/types';
import { isConstant, isQuery } from '../../../variables/guard';
import { LibraryElementKind } from '../../../library-panels/types';
import { isPanelModelLibraryPanel } from '../../../library-panels/guard';

interface Input {
name: string;
type: string;
label: string;
value: any;
description: string;
}

interface Requires {
[key: string]: {
type: string;
id: string;
name: string;
version: string;
};
}

interface DataSources {
[key: string]: {
name: string;
label: string;
description: string;
type: string;
pluginId: string;
pluginName: string;
};
}

export interface LibraryElementExport {
name: string;
uid: string;
model: any;
kind: LibraryElementKind;
}

export class DashboardExporter {
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
// this is pretty hacky and needs to be changed
dashboard.cleanUpRepeats();

const saveModel = dashboard.getSaveModelClone();
saveModel.id = null;

// undo repeat cleanup
dashboard.processRepeats();

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>();

for (const variable of saveModel.getVariables()) {
variableLookup[variable.name] = variable;
}

const templateizeDatasourceUsage = (obj: any) => {
let datasource: string = obj.datasource;
let datasourceVariable: any = null;

// ignore data source properties that contain a variable
if (datasource && (datasource as any).uid) {
const uid = (datasource as any).uid as string;
if (uid.indexOf('$') === 0) {
datasourceVariable = variableLookup[uid.substring(1)];
if (datasourceVariable && datasourceVariable.current) {
datasource = datasourceVariable.current.value;
}
}
}

promises.push(
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',
};

// 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,
};

if (!obj.datasource || typeof obj.datasource === 'string') {
obj.datasource = '${' + refName + '}';
} else {
obj.datasource.uid = '${' + refName + '}';
}
})
);
};

const processPanel = (panel: PanelModel) => {
if (panel.datasource !== undefined && panel.datasource !== null) {
templateizeDatasourceUsage(panel);
}

if (panel.targets) {
for (const target of panel.targets) {
if (target.datasource !== undefined) {
templateizeDatasourceUsage(target);
}
}
}

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 processLibraryPanels = (panel: any) => {
if (isPanelModelLibraryPanel(panel)) {
const { libraryPanel, ...model } = panel;
const { name, uid } = libraryPanel;
if (!libraryPanels.has(uid)) {
libraryPanels.set(uid, { name, uid, kind: LibraryElementKind.Panel, model });
}
}
};

// check up panel data sources
for (const panel of saveModel.panels) {
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);
}
}
}

// 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,
};
});
}
}
Empty file.
Loading