feat(prometheus): the Prometheus dashboard widget, target + template (#5691)
#5691 "Generic Prometheus dashboard widget: pick a target and a template", step 3 of milestone 508 "Prometheus widget templates". - /plugins/prometheus/widget draws one target with one template in widget shape: the verdict (led by "isn't answering" when the target is down), then each block with problems first, figures beside a share bar or the last 24h of bars, and the healthy rest folding to "+N more healthy" past six. A missing target or template, or one that no longer checks, says so. - library.py: bundled templates from plugins/prometheus/widgets/*.yaml, the picker's target and template lists, and the default title. - widgets/fabled_archives.yaml: the first bundled template, against the series Archives reports (refined in #5693). - Dashboard: a widget setting may take `options_from` (read when the picker opens) with an `empty` sentence; `default_title`; a Details link may name settings ("/plugins/prometheus/targets/{target}"). Prometheus is grouped with the integrations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import hashlib
|
||||
import json
|
||||
import secrets
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import quote, urlencode
|
||||
from quart import Blueprint, render_template, current_app, abort, redirect, request, session
|
||||
from sqlalchemy import select, func, update
|
||||
from steward.auth.middleware import public_route, require_role, current_user_id
|
||||
@@ -127,13 +127,41 @@ def _resolve_widgets(widgets: list[DashboardWidget]) -> list[dict]:
|
||||
hx_url = defn["hx_url"] + "?" + urlencode(url_params)
|
||||
out.append({
|
||||
"db": w,
|
||||
"defn": defn,
|
||||
"defn": {**defn, "detail_url": detail_url(defn, config)},
|
||||
"hx_url": hx_url,
|
||||
"title": w.title or defn["label"],
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def detail_url(defn: dict, config: dict) -> str:
|
||||
"""The widget's Details link. A plugin widget may name its settings in
|
||||
it, "/plugins/prometheus/targets/{target}"; when one isn't set, the link
|
||||
goes to the section above, "/plugins/prometheus/"."""
|
||||
url = defn["detail_url"]
|
||||
if "{" not in url:
|
||||
return url
|
||||
try:
|
||||
return url.format(**{k: quote(str(v), safe="") for k, v in config.items()})
|
||||
except (KeyError, IndexError, ValueError):
|
||||
return url[:url.index("{")].rsplit("/", 2)[0] + "/"
|
||||
|
||||
|
||||
async def with_options(db, widget: dict) -> dict:
|
||||
"""A widget definition for the picker, its `options_from` settings
|
||||
filled in now: a plugin's targets, templates and the like, read when
|
||||
the picker opens rather than fixed in the registry."""
|
||||
params = widget.get("params", [])
|
||||
if not any(p.get("options_from") for p in params):
|
||||
return widget
|
||||
filled = []
|
||||
for p in params:
|
||||
if p.get("options_from"):
|
||||
p = {**p, "options": await p["options_from"](db)}
|
||||
filled.append(p)
|
||||
return {**widget, "params": filled}
|
||||
|
||||
|
||||
# ── Edit panel helpers ────────────────────────────────────────────────────────
|
||||
|
||||
# ── Root redirect ─────────────────────────────────────────────────────────────
|
||||
@@ -173,7 +201,7 @@ async def view(dash_id: int):
|
||||
# viewers/shared-readers don't, so skip the extra queries for them.
|
||||
if can_edit:
|
||||
plugins_cfg = current_app.config.get("PLUGINS", {})
|
||||
addable = get_available_widgets(plugins_cfg)
|
||||
addable = [await with_options(db, w) for w in get_available_widgets(plugins_cfg)]
|
||||
hosts = list((await db.execute(select(Host).order_by(Host.name))).scalars())
|
||||
else:
|
||||
addable, hosts = [], []
|
||||
@@ -339,6 +367,8 @@ async def add_widget(dash_id: int, widget_key: str):
|
||||
config[p["key"]] = val
|
||||
config_json = json.dumps(config) if config else None
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
if title is None and defn.get("default_title"):
|
||||
title = await defn["default_title"](db, config)
|
||||
async with db.begin():
|
||||
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
||||
dash = result.scalar_one_or_none()
|
||||
|
||||
Reference in New Issue
Block a user