Skip to content

Commit 5b3c90f

Browse files
committed
Add Python implementation of Grafana's DashboardExporter (WIP)
1 parent 95d9b24 commit 5b3c90f

File tree

2 files changed

+371
-0
lines changed

2 files changed

+371
-0
lines changed

grafana_client/model/__init__.py

Whitespace-only changes.

grafana_client/model/dashboard.py

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
"""
2+
About
3+
=====
4+
Python implementation of Grafana's `DashboardExporter.ts`.
5+
6+
State of the onion
7+
==================
8+
It has been started on 2022-02-15. It is a work in progress. Contributions are very much welcome!
9+
10+
Synopsis
11+
========
12+
::
13+
14+
python grafana_client/model/dashboard.py play.grafana.org 000000012 | jq
15+
16+
Parameters
17+
==========
18+
- `host`: The Grafana host name to connect to.
19+
- `dashboard uid`: The UID of the Grafana dashboard to export.
20+
21+
References
22+
==========
23+
24+
- https://community.grafana.com/t/export-dashboard-for-external-use-via-http-api/50716
25+
- https://github.com/panodata/grafana-client/issues/8
26+
- https://play.grafana.org/d/000000012
27+
"""
28+
import dataclasses
29+
import operator
30+
from typing import Any, Dict, List, Optional
31+
32+
33+
@dataclasses.dataclass
34+
class AnnotationQuery:
35+
pass
36+
37+
38+
@dataclasses.dataclass
39+
class DashboardLink:
40+
pass
41+
42+
43+
@dataclasses.dataclass
44+
class PanelModel:
45+
pass
46+
47+
48+
@dataclasses.dataclass
49+
class Subscription:
50+
pass
51+
52+
53+
@dataclasses.dataclass
54+
class DashboardModel:
55+
"""
56+
https://github.com/grafana/grafana/blob/v8.3.6/public/app/features/dashboard/state/DashboardModel.ts
57+
"""
58+
59+
id: Any
60+
uid: str
61+
title: str
62+
# autoUpdate: Any
63+
# description: Any
64+
tags: Any
65+
style: Any
66+
timezone: Any
67+
editable: Any
68+
# graphTooltip: DashboardCursorSync;
69+
graphTooltip: Any
70+
time: Any
71+
liveNow: bool
72+
# private originalTime: Any
73+
timepicker: Any
74+
templating: List[Any]
75+
# private originalTemplating: Any
76+
annotations: List[AnnotationQuery]
77+
refresh: Any
78+
# snapshot: Any
79+
schemaVersion: int
80+
version: int
81+
# revision: int
82+
links: List[DashboardLink]
83+
gnetId: Any
84+
panels: List[PanelModel]
85+
# panelInEdit?: PanelModel;
86+
# panelInView?: PanelModel;
87+
fiscalYearStartMonth: int
88+
# private panelsAffectedByVariableChange: number[] | null;
89+
# private appEventsSubscription: Subscription;
90+
# private lastRefresh: int
91+
92+
# Not in dashboard payload from API, but should be exported.
93+
weekStart: Any = ""
94+
95+
def cleanUpRepeats(self):
96+
pass
97+
98+
def getSaveModelClone(self):
99+
return self
100+
101+
def processRepeats(self):
102+
pass
103+
104+
def getVariables(self):
105+
return self.templating["list"]
106+
107+
def asdict(self):
108+
return dataclasses.asdict(self)
109+
110+
111+
@dataclasses.dataclass
112+
class DashboardExporter:
113+
"""
114+
https://github.com/grafana/grafana/blob/v8.3.6/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts
115+
"""
116+
117+
inputs: Optional[List] = None
118+
requires: Optional[Dict] = None
119+
datasources: Optional[Dict] = None
120+
promises: Optional[List[Any]] = None
121+
variableLookup: Optional[Dict[str, Any]] = None
122+
libraryPanels: Optional[Dict[str, Any]] = None
123+
124+
def makeExportable(self, dashboard: DashboardModel):
125+
126+
# clean up repeated rows and panels,
127+
# this is done on the live real dashboard instance, not on a clone
128+
# so we need to undo this
129+
# this is pretty hacky and needs to be changed
130+
dashboard.cleanUpRepeats()
131+
132+
saveModel = dashboard.getSaveModelClone()
133+
saveModel.id = None
134+
135+
# undo repeat cleanup
136+
dashboard.processRepeats()
137+
138+
self.inputs = []
139+
self.requires = {}
140+
self.datasources = {}
141+
self.promises = []
142+
self.variableLookup = {}
143+
self.libraryPanels = {}
144+
145+
for variable in saveModel.getVariables():
146+
self.variableLookup[variable.name] = variable
147+
148+
"""
149+
const templateizeDatasourceUsage = (obj: any) => {
150+
let datasource: string = obj.datasource;
151+
let datasourceVariable: any = null;
152+
153+
// ignore data source properties that contain a variable
154+
if (datasource && (datasource as any).uid) {
155+
const uid = (datasource as any).uid as string;
156+
if (uid.indexOf('$') === 0) {
157+
datasourceVariable = variableLookup[uid.substring(1)];
158+
if (datasourceVariable && datasourceVariable.current) {
159+
datasource = datasourceVariable.current.value;
160+
}
161+
}
162+
}
163+
"""
164+
165+
"""
166+
promises.push(
167+
getDataSourceSrv()
168+
.get(datasource)
169+
.then((ds) => {
170+
if (ds.meta?.builtIn) {
171+
return;
172+
}
173+
174+
// add data source type to require list
175+
requires['datasource' + ds.meta?.id] = {
176+
type: 'datasource',
177+
id: ds.meta.id,
178+
name: ds.meta.name,
179+
version: ds.meta.info.version || '1.0.0',
180+
};
181+
182+
// if used via variable we can skip templatizing usage
183+
if (datasourceVariable) {
184+
return;
185+
}
186+
187+
const refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase();
188+
datasources[refName] = {
189+
name: refName,
190+
label: ds.name,
191+
description: '',
192+
type: 'datasource',
193+
pluginId: ds.meta?.id,
194+
pluginName: ds.meta?.name,
195+
};
196+
197+
if (!obj.datasource || typeof obj.datasource === 'string') {
198+
obj.datasource = '${' + refName + '}';
199+
} else {
200+
obj.datasource.uid = '${' + refName + '}';
201+
}
202+
})
203+
);
204+
};
205+
206+
const processPanel = (panel: PanelModel) => {
207+
if (panel.datasource !== undefined && panel.datasource !== null) {
208+
templateizeDatasourceUsage(panel);
209+
}
210+
211+
if (panel.targets) {
212+
for (const target of panel.targets) {
213+
if (target.datasource !== undefined) {
214+
templateizeDatasourceUsage(target);
215+
}
216+
}
217+
}
218+
219+
const panelDef: PanelPluginMeta = config.panels[panel.type];
220+
if (panelDef) {
221+
requires['panel' + panelDef.id] = {
222+
type: 'panel',
223+
id: panelDef.id,
224+
name: panelDef.name,
225+
version: panelDef.info.version,
226+
};
227+
}
228+
};
229+
230+
const processLibraryPanels = (panel: any) => {
231+
if (isPanelModelLibraryPanel(panel)) {
232+
const { libraryPanel, ...model } = panel;
233+
const { name, uid } = libraryPanel;
234+
if (!libraryPanels.has(uid)) {
235+
libraryPanels.set(uid, { name, uid, kind: LibraryElementKind.Panel, model });
236+
}
237+
}
238+
};
239+
240+
// check up panel data sources
241+
for (const panel of saveModel.panels) {
242+
processPanel(panel);
243+
244+
// handle collapsed rows
245+
if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) {
246+
for (const rowPanel of panel.panels) {
247+
processPanel(rowPanel);
248+
}
249+
}
250+
}
251+
252+
// templatize template vars
253+
for (const variable of saveModel.getVariables()) {
254+
if (isQuery(variable)) {
255+
templateizeDatasourceUsage(variable);
256+
variable.options = [];
257+
variable.current = {} as unknown as VariableOption;
258+
variable.refresh =
259+
variable.refresh !== VariableRefresh.never ? variable.refresh : VariableRefresh.onDashboardLoad;
260+
}
261+
}
262+
263+
// templatize annotations vars
264+
for (const annotationDef of saveModel.annotations.list) {
265+
templateizeDatasourceUsage(annotationDef);
266+
}
267+
"""
268+
269+
# add grafana version
270+
self.requires["grafana"] = {
271+
"type": "grafana",
272+
"id": "grafana",
273+
"name": "Grafana",
274+
# FIXME: "version": config.buildInfo.version,
275+
"version": "8.4.0-beta1",
276+
}
277+
278+
"""
279+
return Promise.all(promises)
280+
.then(() => {
281+
each(datasources, (value: any) => {
282+
inputs.push(value);
283+
});
284+
285+
// we need to process all panels again after all the promises are resolved
286+
// so all data sources, variables and targets have been templateized when we process library panels
287+
for (const panel of saveModel.panels) {
288+
processLibraryPanels(panel);
289+
if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) {
290+
for (const rowPanel of panel.panels) {
291+
processLibraryPanels(rowPanel);
292+
}
293+
}
294+
}
295+
296+
// templatize constants
297+
for (const variable of saveModel.getVariables()) {
298+
if (isConstant(variable)) {
299+
const refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
300+
inputs.push({
301+
name: refName,
302+
type: 'constant',
303+
label: variable.label || variable.name,
304+
value: variable.query,
305+
description: '',
306+
});
307+
// update current and option
308+
variable.query = '${' + refName + '}';
309+
variable.current = {
310+
value: variable.query,
311+
text: variable.query,
312+
selected: false,
313+
};
314+
variable.options = [variable.current];
315+
}
316+
}
317+
318+
})
319+
"""
320+
321+
# make inputs and requires a top thing
322+
newObj = dict(
323+
__inputs=self.inputs,
324+
__elements=list(self.libraryPanels.values()),
325+
__requires=sorted(self.requires.values(), key=operator.itemgetter("id")),
326+
)
327+
328+
# purge some attributes.
329+
blocklist = ["gnetId"]
330+
dashboard_data = dashboard.asdict()
331+
for blockitem in blocklist:
332+
if blockitem in dashboard_data:
333+
del dashboard_data[blockitem]
334+
newObj.update(dashboard_data)
335+
336+
return newObj
337+
338+
339+
def main():
340+
import json
341+
import sys
342+
from typing import Any, Dict
343+
344+
from grafana_client import GrafanaApi
345+
from grafana_client.model.dashboard import DashboardExporter, DashboardModel
346+
347+
def jdump(data):
348+
print(json.dumps(data, indent=4, sort_keys=True))
349+
350+
grafana_host = sys.argv[1]
351+
dashboard_uid = sys.argv[2]
352+
353+
# Fetch dashboard.
354+
grafana: GrafanaApi = GrafanaApi(None, host=grafana_host)
355+
dashboard_raw: Dict = grafana.dashboard.get_dashboard(dashboard_uid)
356+
# jdump(dashboard_raw)
357+
358+
# Converge to model class.
359+
dashboard_data: Dict = dashboard_raw["dashboard"]
360+
dashboard_model: DashboardModel = DashboardModel(**dashboard_data)
361+
# jdump(dashboard_model.asdict())
362+
363+
# Represent.
364+
exporter: DashboardExporter = DashboardExporter()
365+
exported: Dict = exporter.makeExportable(dashboard_model)
366+
# assert exported.templating.list[0].datasource == '${DS_GFDB}'
367+
jdump(exported)
368+
369+
370+
if __name__ == "__main__":
371+
main()

0 commit comments

Comments
 (0)