import QtQuick
// Thin client for Grafana's HTTP API, authenticated with a service account
// token. Every call takes a callback(ok, dataOrMessage, network), where
// `network` is true when the server was never reached rather than when it
// answered with an error.
QtObject {
id: api
property string baseUrl: ""
property string accessToken: ""
readonly property bool configured: normalizedBase() !== "" && accessToken.trim() !== ""
function normalizedBase() {
var url = (baseUrl || "").trim()
while (url.length > 0 && url.charAt(url.length - 1) === "/") url = url.substring(0, url.length - 1)
return url
}
// QML's XMLHttpRequest ignores `timeout`, and a socket open across a
// suspend can hang without ever finishing, so every request gets its
// own watchdog. A dashboard opens with a burst of one query per panel;
// at most `maxParallel` are in flight and the rest wait their turn.
property int timeoutMs: 30000
property int maxParallel: 6
property var inFlight: []
property var queue: []
readonly property Component watchdog: Component {
Timer { repeat: false }
}
// Drop every outstanding and queued request without calling back.
function abortAll() {
queue = []
var pending = inFlight
inFlight = []
for (var i = 0; i < pending.length; i++) pending[i].cancel()
}
function request(method, path, body, callback) {
if (!configured) {
if (callback) callback(false, "Grafana URL and token must be configured", false)
return
}
queue = queue.concat([{ method: method, path: path, body: body, callback: callback }])
pump()
}
function pump() {
while (inFlight.length < maxParallel && queue.length > 0) {
var next = queue[0]
queue = queue.slice(1)
send(next.method, next.path, next.body, next.callback)
}
}
function send(method, path, body, callback) {
var xhr = new XMLHttpRequest()
xhr.open(method, normalizedBase() + path, true)
xhr.setRequestHeader("Authorization", "Bearer " + accessToken.trim())
xhr.setRequestHeader("Accept", "application/json")
if (body !== undefined && body !== null) xhr.setRequestHeader("Content-Type", "application/json")
var finished = false
var timer = watchdog.createObject(api, { interval: timeoutMs })
var entry = { cancel: function() { settle(); xhr.abort() } }
inFlight = inFlight.concat([entry])
function settle() {
if (finished) return false
finished = true
timer.stop()
timer.destroy()
inFlight = inFlight.filter(function(e) { return e !== entry })
Qt.callLater(api.pump)
return true
}
function fail(message, network) {
if (!settle()) return
console.log("Grafana API error:", method, path, message)
if (callback) callback(false, message, !!network)
}
timer.triggered.connect(function() {
fail("Timed out talking to " + normalizedBase(), true)
xhr.abort()
})
timer.start()
xhr.onreadystatechange = function() {
if (xhr.readyState !== XMLHttpRequest.DONE || finished) return
var data = null
try {
data = JSON.parse(xhr.responseText || "null")
} catch (e) {}
if (xhr.status >= 200 && xhr.status < 300) {
if (!settle()) return
if (callback) callback(true, data, false)
return
}
var message = data && (data.message || data.error) ? (data.message || data.error) : ""
// a query error still carries per-query results worth showing
if (data && data.results) {
if (!settle()) return
if (callback) callback(true, data, false)
return
}
if (xhr.status === 0) fail("Could not reach " + normalizedBase(), true)
else if (xhr.status === 401) fail(message || "Authentication failed — check the token")
else if (xhr.status === 403) fail(message || "This token isn't allowed to do that")
else if (xhr.status === 404) fail(message || "Not found")
else fail(message || ("HTTP " + xhr.status))
}
xhr.send(body !== undefined && body !== null ? JSON.stringify(body) : null)
}
function get(path, callback) { request("GET", path, null, callback) }
// ── endpoints ───────────────────────────────────────────────────────
function health(callback) { get("/api/health", callback) }
function user(callback) { get("/api/user", callback) }
// datasources by name, the default one, and buildInfo
function frontendSettings(callback) { get("/api/frontend/settings", callback) }
function search(query, callback) {
get("/api/search?type=dash-db&limit=500" + (query ? "&query=" + encodeURIComponent(query) : ""), callback)
}
// {dashboard, meta}
function dashboard(uid, callback) { get("/api/dashboards/uid/" + encodeURIComponent(uid), callback) }
function libraryPanel(uid, callback) { get("/api/library-elements/" + encodeURIComponent(uid), callback) }
// {results: {refId: {frames, error?}}}
function query(body, callback) { request("POST", "/api/ds/query", body, callback) }
// Wraps health + user into a human-readable verdict.
function testConnection(callback) {
health(function(ok, h, network) {
if (!ok) { callback(false, h); return }
user(function(ok2, u) {
if (!ok2) { callback(false, u); return }
var who = u.name || u.login || "?"
var sa = String(u.uid || "").indexOf("service-account") === 0 ? " (service account)" : ""
callback(true, "Connected as " + who + sa + " to Grafana " + (h.version || ""), u)
})
})
}
function dashboardUrl(uid, panelId) {
var url = normalizedBase() + "/d/" + encodeURIComponent(uid)
if (panelId !== undefined && panelId !== null) url += "?viewPanel=" + encodeURIComponent(String(panelId).split("#")[0])
return url
}
}
neiam
/grafana-plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
6.3 KiB · text
History
7a88816