The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/features/dashboard-scene/scene/export/exporters.ts

414 lines
12 KiB

import { defaults, each, sortBy } from 'lodash';
import { DataSourceRef, PanelPluginMeta, VariableOption, VariableRefresh } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import {
Spec as DashboardV2Spec,
PanelKind,
PanelQueryKind,
AnnotationQueryKind,
QueryVariableKind,
LibraryPanelRef,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import config from 'app/core/config';
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 { isPanelModelLibraryPanel } from '../../../library-panels/guard';
import { LibraryElementKind } from '../../../library-panels/types';
import { DashboardJson } from '../../../manage-dashboards/types';
import { isConstant } from '../../../variables/guard';
import { removePanelRefFromLayout } from './utils';
export interface InputUsage {
libraryPanels?: LibraryPanelRef[];
}
export interface Input {
name: string;
type: string;
label: string;
value: any;
description: string;
usage?: InputUsage;
}
interface Requires {
[key: string]: {
type: string;
id: string;
name: string;
version: string;
};
}
export interface ExternalDashboard {
__inputs?: Input[];
__elements?: Record<string, LibraryElementExport>;
__requires?: Array<Requires[string]>;
panels: Array<PanelModel | PanelWithExportableLibraryPanel>;
}
interface PanelWithExportableLibraryPanel {
gridPos: GridPos;
id: number;
libraryPanel: LibraryPanelRef;
}
function isExportableLibraryPanel(
p: PanelModel | PanelWithExportableLibraryPanel
): p is PanelWithExportableLibraryPanel {
return Boolean(p.libraryPanel?.name && p.libraryPanel?.uid);
}
interface DataSources {
[key: string]: {
name: string;
label: string;
description: string;
type: string;
pluginId: string;
pluginName: string;
usage?: InputUsage;
};
}
export interface LibraryElementExport {
name: string;
uid: string;
model: any;
kind: LibraryElementKind;
}
export async function makeExportableV1(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.getSaveModelCloneOld();
saveModel.id = null;
// undo repeat cleanup
dashboard.processRepeats();
const inputs: Input[] = [];
const requires: Requires = {};
const datasources: DataSources = {};
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, fallback?: DataSourceRef) => {
if (obj.datasource === undefined) {
obj.datasource = fallback;
return;
}
let datasource = obj.datasource;
let datasourceVariable: any = null;
const datasourceUid: string | undefined = datasource?.uid;
const match = datasourceUid && variableRegex.exec(datasourceUid);
// ignore data source properties that contain a variable
if (match) {
const varName = match[1] || match[2] || match[4];
datasourceVariable = variableLookup[varName];
if (datasourceVariable && datasourceVariable.current) {
datasource = datasourceVariable.current.value;
}
}
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',
};
// 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();
datasources[refName] = {
name: refName,
label: ds.name,
description: '',
type: 'datasource',
pluginId: ds.meta?.id,
pluginName: ds.meta?.name,
usage: datasources[refName]?.usage,
};
if (!!libraryPanel) {
const libPanels = datasources[refName]?.usage?.libraryPanels || [];
libPanels.push({ name: libraryPanel.name, uid: libraryPanel.uid });
datasources[refName].usage = {
libraryPanels: libPanels,
};
}
obj.datasource = { type: ds.meta.id, uid: '${' + refName + '}' };
});
};
const processPanel = async (panel: PanelModel) => {
if (panel.type !== 'row') {
await templateizeDatasourceUsage(panel);
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 processLibraryPanels = async (panel: PanelModel) => {
if (isPanelModelLibraryPanel(panel)) {
const { name, uid } = panel.libraryPanel;
let model = panel.libraryPanel.model;
if (!model) {
const libPanel = await getLibraryPanel(uid, true);
model = libPanel.model;
}
await templateizeDatasourceUsage(model);
const { gridPos, id, ...rest } = model as any;
if (!libraryPanels.has(uid)) {
libraryPanels.set(uid, { name, uid, kind: LibraryElementKind.Panel, model: rest });
}
}
};
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) {
await processPanel(rowPanel);
}
}
}
// templatize template vars
for (const variable of saveModel.getVariables()) {
if (variable.type === 'query') {
await templateizeDatasourceUsage(variable);
variable.options = [];
variable.current = {} as unknown as VariableOption;
variable.refresh =
variable.refresh !== VariableRefresh.never ? variable.refresh : VariableRefresh.onDashboardLoad;
} else if (variable.type === 'datasource') {
variable.current = {};
} else if (variable.type === 'adhoc') {
await templateizeDatasourceUsage(variable);
}
}
// templatize annotations vars
for (const annotationDef of saveModel.annotations.list) {
await templateizeDatasourceUsage(annotationDef);
}
// add grafana version
requires['grafana'] = {
type: 'grafana',
id: 'grafana',
name: 'Grafana',
version: config.buildInfo.version,
};
// 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) {
await processLibraryPanels(panel);
if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) {
for (const rowPanel of panel.panels) {
await processLibraryPanels(rowPanel);
}
}
}
each(datasources, (value: any) => {
inputs.push(value);
});
// 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];
}
}
const __elements = [...libraryPanels.entries()].reduce<Record<string, LibraryElementExport>>(
(prev, [curKey, curLibPanel]) => {
prev[curKey] = curLibPanel;
return prev;
},
{}
);
// make inputs and requires a top thing
const newObj: DashboardJson = defaults(
{
__inputs: inputs,
__elements,
__requires: sortBy(requires, ['id']),
},
saveModel
);
// Remove extraneous props from library panels
for (let i = 0; i < newObj.panels.length; i++) {
const libPanel = newObj.panels[i];
if (isExportableLibraryPanel(libPanel)) {
newObj.panels[i] = {
gridPos: libPanel.gridPos,
id: libPanel.id,
libraryPanel: { uid: libPanel.libraryPanel.uid, name: libPanel.libraryPanel.name },
};
}
}
return newObj;
} catch (err) {
console.error('Export failed:', err);
return {
error: err,
};
}
}
export async function makeExportableV2(dashboard: DashboardV2Spec) {
const variableLookup: { [key: string]: any } = {};
// get all datasource variables
const datasourceVariables = dashboard.variables.filter((v) => v.kind === 'DatasourceVariable');
for (const variable of dashboard.variables) {
variableLookup[variable.spec.name] = variable.spec;
}
const removeDataSourceRefs = (
obj: AnnotationQueryKind['spec'] | QueryVariableKind['spec'] | PanelQueryKind['spec']
) => {
const datasourceUid = obj.datasource?.uid;
if (datasourceUid?.startsWith('${') && datasourceUid?.endsWith('}')) {
const varName = datasourceUid.slice(2, -1);
// if there's a match we don't want to remove the datasource ref
const match = datasourceVariables.find((v) => v.spec.name === varName);
if (match) {
return;
}
}
obj.datasource = undefined;
};
const processPanel = (panel: PanelKind) => {
if (panel.spec.data.spec.queries) {
for (const query of panel.spec.data.spec.queries) {
removeDataSourceRefs(query.spec);
}
}
};
try {
const elements = dashboard.elements;
const layout = dashboard.layout;
// process elements
for (const [key, element] of Object.entries(elements)) {
if (element.kind === 'Panel') {
processPanel(element);
} else if (element.kind === 'LibraryPanel') {
// just remove the library panel
delete elements[key];
// remove reference from layout
removePanelRefFromLayout(layout, key);
}
}
// process template variables
for (const variable of dashboard.variables) {
if (variable.kind === 'QueryVariable') {
removeDataSourceRefs(variable.spec);
variable.spec.options = [];
variable.spec.current = {
text: '',
value: '',
};
} else if (variable.kind === 'DatasourceVariable') {
variable.spec.current = {
text: '',
value: '',
};
}
}
// process annotations vars
for (const annotation of dashboard.annotations) {
removeDataSourceRefs(annotation.spec);
}
return dashboard;
} catch (err) {
console.error('Export failed:', err);
return {
error: err,
};
}
}