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:
@@ -0,0 +1,71 @@
|
||||
"""The widget templates a Prometheus widget can draw with.
|
||||
|
||||
Bundled templates ship in plugins/prometheus/widgets/*.yaml, read once and
|
||||
read-only; their key is "bundled:<file name>". Templates an admin copies in
|
||||
join them with the template library (#5692). Steward fetches none from
|
||||
anywhere (System 181's ruling).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from .models import PrometheusTarget
|
||||
from .template_format import Problem, Template, TemplateError, check
|
||||
|
||||
BUNDLED = Path(__file__).parent / "widgets"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Entry:
|
||||
key: str
|
||||
name: str
|
||||
bundled: bool
|
||||
text: str
|
||||
template: Template | None = None
|
||||
problems: list[Problem] = field(default_factory=list)
|
||||
|
||||
|
||||
def _entry(key: str, fallback_name: str, text: str, bundled: bool) -> Entry:
|
||||
try:
|
||||
template = check(text)
|
||||
except TemplateError as exc:
|
||||
return Entry(key, fallback_name, bundled, text, None, exc.problems)
|
||||
return Entry(key, template.name, bundled, text, template)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def bundled() -> tuple[Entry, ...]:
|
||||
return tuple(_entry(f"bundled:{p.stem}", p.stem, p.read_text(encoding="utf-8"), True)
|
||||
for p in sorted(BUNDLED.glob("*.yaml")))
|
||||
|
||||
|
||||
async def entries(db) -> list[Entry]:
|
||||
"""Every template, by name."""
|
||||
return sorted(bundled(), key=lambda e: e.name.lower())
|
||||
|
||||
|
||||
async def find(db, key: str) -> Entry | None:
|
||||
return next((e for e in await entries(db) if e.key == key), None)
|
||||
|
||||
|
||||
# ── The widget's settings ────────────────────────────────────────────────────
|
||||
|
||||
async def target_options(db) -> list[dict]:
|
||||
rows = (await db.execute(select(PrometheusTarget.id, PrometheusTarget.name)
|
||||
.order_by(PrometheusTarget.name))).all()
|
||||
return [{"value": tid, "label": name} for tid, name in rows]
|
||||
|
||||
|
||||
async def template_options(db) -> list[dict]:
|
||||
return [{"value": e.key, "label": f"{e.name} (built in)" if e.bundled else e.name}
|
||||
for e in await entries(db) if e.template is not None]
|
||||
|
||||
|
||||
async def default_title(db, config: dict) -> str | None:
|
||||
"""A new widget is named after its template: "Fabled Archives"."""
|
||||
entry = await find(db, str(config.get("template") or ""))
|
||||
return entry.name if entry and entry.template else None
|
||||
Reference in New Issue
Block a user