feat(prometheus-direct-charting): slice 1 — prom range query + chart rebrand

Add PrometheusWidgetSource._fetch_chart hitting /api/v1/query_range directly
(SC-101..104). New shared helpers in widgets/prometheus_range.py:
step_for_window (window preset -> step, ~200pts) and normalize_prometheus_matrix
(extracted label/dedup rule, retargeted at Prom matrix, robust to malformed
data). Chart widget kind moved grafana->prometheus in both registries;
GrafanaChartWidget renamed -> PrometheusChartWidget (git mv, recharts body
preserved). Grafana binding/service untouched (removed in slice 3).

Backend: 298 pytest pass, ruff clean. Frontend: 128 vitest pass, build+lint green.
This commit is contained in:
Developer
2026-07-08 21:51:49 +00:00
parent d906b0392b
commit 5dad98231f
12 changed files with 383 additions and 56 deletions
@@ -45,61 +45,61 @@ This ordering ensures the chart capability is proven against Prometheus before t
**Satisfies:** SC-101, SC-102, SC-103, SC-104, SC-105, SC-106, SC-107, SC-108.
- [ ] **1.1 Create shared Prometheus range helpers module**
- [x] **1.1 Create shared Prometheus range helpers module**
- Files: `backend/src/media_library_viewer_api/widgets/prometheus_range.py` (new)
- Lines: ~60
- Dependencies: none
- Details: Implement `WINDOW_PRESETS = {"1h": 3600, "6h": 21600, "24h": 86400, "7d": 604800}`, `step_for_window(window_seconds, target_points=200) -> int` returning `max(15, round(window_seconds / target_points))`, and `normalize_prometheus_matrix(result: list[dict]) -> list[dict]` that converts a Prometheus `/api/v1/query_range` `data.result` matrix into `{label, points:[{t:int, v:float|None}]}` series. Label rule: drop `__name__` from metric labels; join remaining as `k=v`; fall back to `"value"`; dedup collisions with `(n)` suffix. Null handling: `"NaN"`, `"+Inf"`, `"-Inf"`, `None``v: None`.
- [ ] **1.2 Add backend unit tests for range helpers**
- [x] **1.2 Add backend unit tests for range helpers**
- Files: `backend/tests/test_prometheus_range.py` (new)
- Lines: ~70
- Dependencies: 1.1
- Details: Test `step_for_window` for all four presets asserts result yields 100300 points. Test `normalize_prometheus_matrix`: feed a two-entry sample matrix (one with `__name__`, one colliding label) → assert `{series}` shape, label dedup `(1)` suffix, null handling for `"NaN"` string.
- [ ] **1.3 Add `_fetch_chart` + shared range-query plumbing to `PrometheusWidgetSource`**
- [x] **1.3 Add `_fetch_chart` + shared range-query plumbing to `PrometheusWidgetSource`**
- Files: `backend/src/media_library_viewer_api/widgets/sources.py` (modify)
- Lines: ~55
- Dependencies: 1.1
- Details: Import `WINDOW_PRESETS`, `step_for_window`, `normalize_prometheus_matrix` from `prometheus_range`. Add a private `_range_query(base_url, timeout, promql, window) -> dict` returning `{"matrix": result}` or `{"error": ...}` (shared by chart now and mean in S2). Add `_fetch_chart(self, base_url, timeout, config)` calling `_range_query` and returning `{"series": normalize_prometheus_matrix(matrix)}`. Dispatch `widget_kind == "chart"` in `.fetch()`. Existing `metric` path stays byte-for-byte unchanged. Errors (timeout, `RequestException`) → `{"error": str}`, never raise.
- [ ] **1.4 Declare `chart` widget kind in Prometheus integration**
- [x] **1.4 Declare `chart` widget kind in Prometheus integration**
- Files: `backend/src/media_library_viewer_api/integrations/prometheus.py` (modify)
- Lines: ~15
- Dependencies: 1.3
- Details: Add `PrometheusChartWidgetConfig(WidgetConfigBase)` with `promql: str` and `window: str = "1h"`. Add a `widget_kind(...)` entry for `chart` (refresh 60s, default config `{"promql": "", "window": "1h"}`). Leave `metric` kind untouched.
- [ ] **1.5 Rename `GrafanaChartWidget` → `PrometheusChartWidget`**
- [x] **1.5 Rename `GrafanaChartWidget` → `PrometheusChartWidget`**
- Files: `frontend/src/widgets/GrafanaChartWidget.tsx``frontend/src/widgets/PrometheusChartWidget.tsx` (git mv)
- Lines: ~5 changed (rename export, fix empty-state copy)
- Dependencies: none
- Details: `git mv` to preserve history. Rename exported function `GrafanaChartWidget``PrometheusChartWidget`. The recharts body (`LineChart`, `Line`, `XAxis`, `YAxis`, `CartesianGrid`, `Tooltip`, `ResponsiveContainer`, `mergeSeries`, `CHART_COLORS`, `formatTime`) is preserved unchanged (SC-106). Fix empty-state copy: "Check your query and datasource_uid" → "Check your PromQL query and window."
- [ ] **1.6 Rename chart widget test**
- [x] **1.6 Rename chart widget test**
- Files: `frontend/src/widgets/__tests__/GrafanaChartWidget.test.tsx``frontend/src/widgets/__tests__/PrometheusChartWidget.test.tsx` (git mv)
- Lines: ~10 changed (import path, component name, error-message assertion)
- Dependencies: 1.5
- Details: `git mv`. Update import to `PrometheusChartWidget`. Update error-state assertion: the old Grafana-specific error string (`"Grafana api_key is required"`) → a Prom error string (e.g. `"promql is required"`). Keep loading + rendered-data test cases.
- [ ] **1.7 Rebind `chart` from grafana to prometheus in frontend registry**
- [x] **1.7 Rebind `chart` from grafana to prometheus in frontend registry**
- Files: `frontend/src/integrations/registry.ts` (modify)
- Lines: ~30
- Dependencies: 1.5
- Details: Import `PrometheusChartWidget`. Add a `chart` entry to the `prometheus` binding's `widgets` array (kind `chart`, refresh 60s, configSchema with `promql` + `window`). Remove the `chart` entry from the `grafana` binding's `widgets` array (leave `link` intact). Do NOT delete the `grafana` binding itself.
- [ ] **1.8 Update frontend widgets barrel export**
- [x] **1.8 Update frontend widgets barrel export**
- Files: `frontend/src/widgets/index.ts` (modify)
- Lines: ~2
- Dependencies: 1.5
- Details: Rename the `GrafanaChartWidget` export to `PrometheusChartWidget`. Leave `GrafanaLinkWidget` export intact.
- [ ] **1.9 Update registry tests for chart rebind**
- [x] **1.9 Update registry tests for chart rebind**
- Files: `frontend/src/integrations/registry.test.ts` (modify)
- Lines: ~15
- Dependencies: 1.7
- Details: Assert `prometheus` binding has `metric` + `chart` kinds. Assert `grafana` binding has only `link` (no `chart`).
- [ ] **1.10 Verify Slice 1 (build + lint + test)**
- [x] **1.10 Verify Slice 1 (build + lint + test)**
- Run: `cd backend && PYTHONPATH=src pytest tests/test_prometheus_range.py tests/test_widgets.py && cd ../frontend && npm run build && npm run lint`
- Verify: helpers tests pass; existing widget tests pass (grafana link adapter still wired); frontend typechecks and lints; `prometheus/chart` widget resolves to `PrometheusChartWidget`.