import QtQuick
import QtQuick.Layouts
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore
import org.kde.kirigami as Kirigami
import "Grafana.js" as G
PlasmoidItem {
id: root
switchWidth: Kirigami.Units.gridUnit * 24
switchHeight: Kirigami.Units.gridUnit * 18
// In a panel always show the pinned strip; on the desktop the
// dashboard itself once there's room for it.
preferredRepresentation: Plasmoid.formFactor === PlasmaCore.Types.Horizontal
|| Plasmoid.formFactor === PlasmaCore.Types.Vertical
? compactRepresentation : null
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground | PlasmaCore.Types.ConfigurableBackground
toolTipMainText: ""
toolTipSubText: ""
readonly property url logoSource: Qt.resolvedUrl("../icons/dashboards.svg")
property string instanceUrl: Plasmoid.configuration.instanceUrl
property string accessToken: Plasmoid.configuration.accessToken
readonly property var dashboardList: parseJson(Plasmoid.configuration.dashboards, [])
readonly property var pinnedList: parseJson(Plasmoid.configuration.pinned, [])
readonly property string timeOverride: Plasmoid.configuration.timeRange
readonly property int refreshOverride: Plasmoid.configuration.refreshSeconds
readonly property int pinnedSeconds: Math.max(10, Plasmoid.configuration.pinnedSeconds)
readonly property int rowHeight: Math.max(16, Plasmoid.configuration.rowHeight)
readonly property bool pinnedLabels: Plasmoid.configuration.pinnedLabels
readonly property var selections: parseJson(Plasmoid.configuration.selections, {})
readonly property bool hasValidConfig: api.configured
readonly property bool hasDashboards: dashboardList.length > 0
// the dashboard on screen; falls back to the first configured one
property string currentUid: ""
readonly property string shownUid: {
for (var i = 0; i < dashboardList.length; i++) if (dashboardList[i].uid === currentUid) return currentUid
return dashboardList.length ? dashboardList[0].uid : ""
}
// a desktop widget showing its full representation is always "open"
readonly property bool showing: expanded || (Plasmoid.formFactor !== PlasmaCore.Types.Horizontal
&& Plasmoid.formFactor !== PlasmaCore.Types.Vertical)
// ── server state ────────────────────────────────────────────────────
property var settings: null // /api/frontend/settings
property var dashboards: ({}) // uid -> {dashboard, meta, at}
property var varOptions: ({}) // uid -> {name -> [{text, value}]}
property var results: ({}) // "uid/panelId" -> {results, error, at, range, interval}
property var ranges: ({}) // uid -> {from, to} of the last refresh
property var loadingPanels: ({}) // "uid/panelId" -> true
property int dataRev: 0
property int dashRev: 0
property var folded: ({}) // uid -> {rowId -> bool}
property string lastError: ""
property bool offline: false
GrafanaAPI {
id: grafanaApi
baseUrl: root.instanceUrl
accessToken: root.accessToken
}
property alias api: grafanaApi
function parseJson(text, fallback) {
try {
var v = JSON.parse(text)
return v === null || v === undefined ? fallback : v
} catch (e) {
return fallback
}
}
Component.onCompleted: {
currentUid = Plasmoid.configuration.currentDashboard
Qt.callLater(start)
}
onInstanceUrlChanged: resetAndStart()
onAccessTokenChanged: resetAndStart()
onDashboardListChanged: Qt.callLater(start)
onPinnedListChanged: Qt.callLater(refreshPinned)
onTimeOverrideChanged: Qt.callLater(refreshShown)
onShownUidChanged: {
if (shownUid && shownUid !== Plasmoid.configuration.currentDashboard) Plasmoid.configuration.currentDashboard = shownUid
Qt.callLater(refreshShown)
}
onShowingChanged: if (showing) Qt.callLater(refreshShown)
function resetAndStart() {
api.abortAll()
settings = null
dashboards = {}
varOptions = {}
results = {}
ranges = {}
loadingPanels = {}
dataRev++
dashRev++
Qt.callLater(start)
}
function start() {
if (!hasValidConfig) return
ensureSettings(function() {
refreshPinned()
refreshShown()
})
}
function setKey(obj, key, value) {
var copy = Object.assign({}, obj)
if (value === undefined) delete copy[key]
else copy[key] = value
return copy
}
// ── loading ─────────────────────────────────────────────────────────
function ensureSettings(cb) {
if (settings) { cb(); return }
api.frontendSettings(function(ok, data, network) {
offline = !ok && network
if (!ok) { lastError = data; return }
settings = data
lastError = ""
cb()
})
}
// Fetches a dashboard (unless it's fresh), resolves library panels, then
// evaluates its query variables in order, each seeing the ones before.
function loadDashboard(uid, force, cb) {
var cached = dashboards[uid]
if (cached && !force && Date.now() - cached.at < 600000) { cb(true); return }
api.dashboard(uid, function(ok, data, network) {
offline = !ok && network
if (!ok) { lastError = uid + ": " + data; cb(false); return }
resolveLibraryPanels(data.dashboard, function() {
dashboards = setKey(dashboards, uid, { dashboard: data.dashboard, meta: data.meta, at: Date.now() })
loadVariables(uid, function() {
dashRev++
cb(true)
})
})
})
}
function resolveLibraryPanels(dashboard, cb) {
var refs = []
function walk(list) {
for (var i = 0; i < (list || []).length; i++) {
if (list[i].libraryPanel && list[i].libraryPanel.uid && !list[i].targets) refs.push(list[i])
walk(list[i].panels)
}
}
walk(dashboard.panels)
if (refs.length === 0) { cb(); return }
var pending = refs.length
refs.forEach(function(p) {
api.libraryPanel(p.libraryPanel.uid, function(ok, data) {
if (ok && data && data.result && data.result.model) {
var model = data.result.model
for (var k in model) if (k !== "gridPos" && k !== "id") p[k] = model[k]
}
if (--pending === 0) cb()
})
})
}
function loadVariables(uid, cb) {
var dash = dashboards[uid].dashboard
var list = (dash.templating && dash.templating.list) || []
var fetched = {}
var range = rangeFor(uid)
var i = 0
function next() {
if (i >= list.length) {
varOptions = setKey(varOptions, uid, fetched)
cb()
return
}
var v = list[i++]
var vars = G.variables(dash, settings.datasources, selections[uid], fetched)
var req = G.varRequest(v, vars, settings.datasources, range)
if (!req) { next(); return }
api.get(req.path, function(ok, data) {
if (ok) fetched[v.name] = G.varOptions(v, req, data)
next()
})
}
next()
}
function variablesFor(uid) {
var entry = dashboards[uid]
if (!entry || !settings) return {}
return G.variables(entry.dashboard, settings.datasources, selections[uid], (varOptions[uid] || {}))
}
function rangeSpec(uid) {
if (timeOverride) return { from: timeOverride, to: "now" }
var entry = dashboards[uid]
return entry && entry.dashboard.time ? entry.dashboard.time : { from: "now-6h", to: "now" }
}
function rangeFor(uid) {
return G.resolveRange(rangeSpec(uid), Date.now())
}
function refreshIntervalFor(uid) {
if (refreshOverride > 0) return Math.max(10, refreshOverride) * 1000
var entry = dashboards[uid]
var r = entry && entry.dashboard.refresh
var ms = 0
var m = /^(\d+)([smhd])$/.exec(r || "")
if (m) ms = parseInt(m[1]) * { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]]
return Math.max(10000, ms || 300000)
}
// ── querying ────────────────────────────────────────────────────────
function panelKey(uid, panelId) { return uid + "/" + panelId }
// Every visible panel of a dashboard, against one shared time range so
// they line up. Folded rows stay unqueried until opened.
function refreshDashboard(uid, force, onlyMissing) {
if (!uid || !hasValidConfig) return
ensureSettings(function() {
loadDashboard(uid, force, function(ok) {
if (!ok) return
var dash = dashboards[uid].dashboard
var vars = variablesFor(uid)
var range = onlyMissing && ranges[uid] ? ranges[uid] : rangeFor(uid)
ranges = setKey(ranges, uid, range)
var items = G.layout(dash, folded[uid] || {}, vars)
for (var i = 0; i < items.length; i++) {
if (items[i].kind !== "panel") continue
var p = items[i].panel
if (onlyMissing && results[panelKey(uid, p.id)]) continue
queryPanel(uid, p, vars, range, Math.max(100, Math.min(1000, Math.round(items[i].w * 40))))
}
})
})
}
function refreshShown() {
if (showing && shownUid) refreshDashboard(shownUid, false, false)
}
function queryPanel(uid, panel, vars, range, maxDataPoints) {
var key = panelKey(uid, panel.id)
if (panel.type === "text" || panel.type === "row") return
if (!G.supported(panel.type)) return
var q = G.buildQuery(panel, dashboards[uid].dashboard, vars, settings.datasources, range, maxDataPoints)
if (q.error) {
setResult(key, { error: q.error, at: Date.now(), range: range })
return
}
loadingPanels = setKey(loadingPanels, key, true)
api.query(q.body, function(ok, data, network) {
loadingPanels = setKey(loadingPanels, key, undefined)
if (!ok) {
offline = network
var prev = results[key]
// keep the last picture, say why it's stale
setResult(key, { results: prev ? prev.results : null, error: data, at: Date.now(), range: prev ? prev.range : range })
return
}
offline = false
var errs = G.resultErrors(data.results)
setResult(key, { results: data.results, error: errs.join("; "), at: Date.now(), range: range, interval: q.interval })
})
}
function setResult(key, value) {
results[key] = value
dataRev++
}
function resultFor(uid, panelId) {
return results[panelKey(uid, panelId)] || null
}
function isLoading(uid, panelId) {
return !!loadingPanels[panelKey(uid, panelId)]
}
readonly property bool anyLoading: Object.keys(loadingPanels).length > 0
// ── pinned ──────────────────────────────────────────────────────────
function refreshPinned() {
if (!hasValidConfig || pinnedList.length === 0) return
ensureSettings(function() {
var uids = {}
for (var i = 0; i < pinnedList.length; i++) uids[pinnedList[i].uid] = true
Object.keys(uids).forEach(function(uid) {
loadDashboard(uid, false, function(ok) {
if (!ok) return
var dash = dashboards[uid].dashboard
var vars = variablesFor(uid)
var range = rangeFor(uid)
for (var j = 0; j < pinnedList.length; j++) {
if (pinnedList[j].uid !== uid) continue
var p = G.findPanel(dash, pinnedList[j].panelId)
if (p) queryPanel(uid, p, vars, range, 100)
}
})
})
})
}
// What a pinned panel shows: its first series reduced the way the panel
// reduces it, formatted with its unit, in its threshold colour.
function pinnedValue(pin) {
var entry = dashboards[pin.uid]
var res = resultFor(pin.uid, pin.panelId)
var out = { text: "…", color: "", series: [], error: res ? res.error : "", title: pin.title }
if (!entry || !res) return out
var panel = G.findPanel(entry.dashboard, pin.panelId)
if (!panel) { out.text = "?"; out.error = "panel no longer exists"; return out }
out.title = G.panelTitle(panel, variablesFor(pin.uid))
var series = G.seriesFromResults(res.results || {}, panel)
var calc = ((panel.options && panel.options.reduceOptions && panel.options.reduceOptions.calcs) || ["lastNotNull"])[0]
for (var i = 0; i < series.length; i++) {
var cfg = G.fieldConfig(panel, series[i])
var v = G.reduce(series[i].values, calc)
out.series.push({ name: G.displayName(series[i], cfg), text: G.format(v, cfg.unit, cfg.decimals),
color: G.seriesColor(cfg, i, v, { min: cfg.min, max: cfg.max }, "") })
}
if (out.series.length === 0) { out.text = res.error ? "!" : "–"; return out }
out.text = out.series[0].text
out.color = out.series[0].color
return out
}
// ── refreshing ──────────────────────────────────────────────────────
Timer {
interval: root.refreshIntervalFor(root.shownUid)
running: root.showing && root.hasValidConfig && root.shownUid !== ""
repeat: true
onTriggered: root.refreshDashboard(root.shownUid, false, false)
}
Timer {
interval: root.pinnedSeconds * 1000
running: root.hasValidConfig && root.pinnedList.length > 0
repeat: true
onTriggered: root.refreshPinned()
}
// Offline (just woke, Wi-Fi not back): retry soon rather than sitting
// on an error for a whole interval.
Timer {
interval: 10000
running: root.offline && root.hasValidConfig
repeat: true
onTriggered: { root.settings ? (root.refreshPinned(), root.refreshShown()) : root.start() }
}
// Wall-clock watchdog: timers don't fire during suspend, so a tick far
// later than scheduled means the machine slept and anything in flight
// is suspect.
Timer {
property double lastTick: Date.now()
interval: 5000
running: true
repeat: true
onTriggered: {
var now = Date.now()
var gap = now - lastTick
lastTick = now
if (gap > interval * 6) {
console.log("Grafana: resumed after", Math.round(gap / 1000), "s")
root.api.abortAll()
root.loadingPanels = {}
root.refreshPinned()
root.refreshShown()
}
}
}
// ── view actions ────────────────────────────────────────────────────
function selectDashboard(uid) {
currentUid = uid
}
function toggleRow(uid, rowId, collapsedNow) {
var f = Object.assign({}, folded[uid] || {})
f[rowId] = !collapsedNow
folded = setKey(folded, uid, f)
dashRev++
if (collapsedNow) refreshDashboard(uid, false, true)
}
function setVariable(uid, name, value) {
var all = Object.assign({}, selections)
var mine = Object.assign({}, all[uid] || {})
mine[name] = value
all[uid] = mine
Plasmoid.configuration.selections = JSON.stringify(all)
// a variable can feed the next one's query, so re-evaluate them all
if (dashboards[uid]) {
loadVariables(uid, function() {
dashRev++
refreshDashboard(uid, false, false)
})
}
}
function openAll() {
root.expanded = !root.expanded
}
function openDashboard(uid) {
if (root.expanded && shownUid === uid) {
root.expanded = false
} else {
currentUid = uid
root.expanded = true
}
}
// ── representations ─────────────────────────────────────────────────
compactRepresentation: CompactStrip {
store: root
}
fullRepresentation: FullView {
store: root
}
}
neiam
/grafana-plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
17.2 KiB · text
History
7a88816