feat(prometheus): the Prometheus dashboard widget, target + template (#5691)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m14s
CI / unit (push) Successful in 1m29s
CI / publish (push) Successful in 1m12s

#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:
2026-10-14 07:05:15 -04:00
co-authored by Claude Opus 5.5
parent 4d95c5d0c2
commit 5ca13f9278
11 changed files with 551 additions and 8 deletions
+5 -3
View File
@@ -6,7 +6,9 @@
# "params" is a list of configurable parameters for the widget. Each param:
# key - form field name (stored in config_json)
# label - human-readable label
# type - "select" (only type currently supported)
# type - "select", or "host" for a live list of hosts
# options_from - async (db) -> options, read when the picker opens (a plugin's
# targets, its templates); "empty" is said when it returns none
# default - default value (int or str determines how it's parsed from the form)
# options - list of {value, label} dicts
#
@@ -353,9 +355,9 @@ def recommended_layout(plugins_cfg: dict) -> list[dict]:
# Group each widget for the picker (Core monitors / Monitoring capabilities /
# Integrations), mirroring the Settings → Plugins taxonomy. Capability plugins
# are host/monitoring facets; traefik/unifi are discrete vendor integrations.
# are host/monitoring facets; traefik/unifi/prometheus are discrete integrations.
_CORE_WIDGETS = {"monitors", "status_overview", "alerts", "hosts_overview"}
_INTEGRATION_PLUGINS = {"traefik", "unifi"}
_INTEGRATION_PLUGINS = {"traefik", "unifi", "prometheus"}
GROUP_LABELS = {
"core": "Core monitors",
"capability": "Monitoring capabilities",
+33 -3
View File
@@ -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()
@@ -45,8 +45,12 @@
{% for p in w.params %}
<div>
<label style="font-size:0.74rem;color:var(--text-muted);display:block;margin-bottom:0.2rem;">{{ p.label }}</label>
{% if p.type == "select" %}
<select name="param_{{ p.key }}" class="input">
{% if p.type == "select" and p.options_from and not p.options %}
{# A list read when the picker opens (a plugin's targets, its templates) that is empty. #}
<p class="field-hint" style="margin:0;">{{ p.empty or "Nothing to choose from yet." }}</p>
{% elif p.type == "select" %}
<select name="param_{{ p.key }}" class="input"{% if p.options_from %} required{% endif %}>
{% if p.options_from and p.options|length > 1 %}<option value="">— choose —</option>{% endif %}
{% for opt in p.options %}
<option value="{{ opt.value }}" {% if opt.value == p.default %}selected{% endif %}>{{ opt.label }}</option>
{% endfor %}