feat(prometheus-direct-charting): slice 3 — remove grafana + config rewrite + changelog

Remove the entire Grafana surface: integrations/grafana.py, GrafanaWidgetSource
(+ _fetch_chart, now redundant since prometheus chart exists), GrafanaLinkWidget,
LinksTab, get_grafana_status endpoint, useGrafanaStatus hook, GrafanaStatus type,
fetchGrafanaStatus client fn, registry/nav/tab entries (FE+BE). Rewrite
config.yaml thin-dashboard rule to match reality (recharts is sanctioned for
Prometheus-backed series). CHANGELOG migration note added.

Backend: 293 pytest pass, ruff clean. Frontend: build+lint green (0 errors).
SC-115/116 grep-clean (only prometheus_range.py migration comments + Dashboard.test
shortcut fixture remain — both spec-allowed).
This commit is contained in:
Developer
2026-07-08 22:33:44 +00:00
parent 65bae95e3c
commit 67ca0fc3bc
27 changed files with 134 additions and 937 deletions
+45 -50
View File
@@ -57,9 +57,8 @@ def client(tmp_path):
# ---------------------------------------------------------------------------
def test_registry_contains_eight_service_types():
def test_registry_contains_seven_service_types():
assert set(SERVICE_DEFINITIONS) == {
"grafana",
"prometheus",
"alertmanager",
"jellyfin",
@@ -99,7 +98,6 @@ def test_authentik_service_definition():
def test_definitions_declare_widget_kinds():
assert {wk.kind for wk in get_service_definition("grafana").widget_kinds} == {"link", "chart"}
assert {wk.kind for wk in get_service_definition("prometheus").widget_kinds} == {"metric", "chart", "gauge", "mean"}
assert {wk.kind for wk in get_service_definition("alertmanager").widget_kinds} == {"active_alerts"}
assert {wk.kind for wk in get_service_definition("jellyfin").widget_kinds} == {"activity", "now_playing"}
@@ -110,13 +108,13 @@ def test_definitions_declare_widget_kinds():
def test_widget_kind_lookup():
assert get_widget_kind("grafana", "link") is not None
assert get_widget_kind("grafana", "missing") is None
assert get_widget_kind("unknown", "link") is None
assert get_widget_kind("prometheus", "metric") is not None
assert get_widget_kind("prometheus", "missing") is None
assert get_widget_kind("unknown", "metric") is None
def test_service_config_schema_is_json_schema():
schema = get_service_definition("grafana").config_schema
schema = get_service_definition("prometheus").config_schema
assert schema["type"] == "object"
assert "base_url" in schema["properties"]
@@ -172,7 +170,6 @@ def test_list_service_types(client):
"alertmanager",
"authentik",
"backups",
"grafana",
"jellyfin",
"nextcloud",
"prometheus",
@@ -182,9 +179,9 @@ def test_list_service_types(client):
def test_service_type_includes_secret_and_widget_metadata(client):
response = client.get("/api/services/types")
grafana = next(item for item in response.json() if item["service_type"] == "grafana")
assert [sf["key"] for sf in grafana["secret_fields"]] == ["api_key"]
assert [wk["kind"] for wk in grafana["widget_kinds"]] == ["link", "chart"]
prom = next(item for item in response.json() if item["service_type"] == "prometheus")
assert [sf["key"] for sf in prom["secret_fields"]] == ["api_key"]
assert set(wk["kind"] for wk in prom["widget_kinds"]) == {"metric", "chart", "gauge", "mean"}
# ---------------------------------------------------------------------------
@@ -192,11 +189,11 @@ def test_service_type_includes_secret_and_widget_metadata(client):
# ---------------------------------------------------------------------------
def _grafana_payload(**overrides):
def _prometheus_payload(**overrides):
payload = {
"service_type": "grafana",
"name": "Production Grafana",
"config": {"base_url": "https://grafana.example.com"},
"service_type": "prometheus",
"name": "Production Prometheus",
"config": {"base_url": "https://prometheus.example.com"},
"secrets": {"api_key": "secret-token"},
"enabled": True,
}
@@ -205,11 +202,11 @@ def _grafana_payload(**overrides):
def test_create_and_list_service(client):
response = client.post("/api/services/instances", json=_grafana_payload())
response = client.post("/api/services/instances", json=_prometheus_payload())
assert response.status_code == 201
created = response.json()
assert created["service_type"] == "grafana"
assert created["config"]["base_url"] == "https://grafana.example.com"
assert created["service_type"] == "prometheus"
assert created["config"]["base_url"] == "https://prometheus.example.com"
# Plaintext secrets are never returned.
assert "secrets" not in created
assert created["secrets_set"] == {"api_key": True}
@@ -220,44 +217,44 @@ def test_create_and_list_service(client):
def test_list_instances_filters_by_type(client):
client.post("/api/services/instances", json=_grafana_payload())
client.post("/api/services/instances", json=_prometheus_payload())
client.post(
"/api/services/instances",
json={
"service_type": "prometheus",
"name": "Prom",
"config": {"base_url": "http://prometheus:9090"},
"service_type": "alertmanager",
"name": "AM",
"config": {"base_url": "http://am:9093"},
},
)
response = client.get("/api/services/instances?service_type=grafana")
response = client.get("/api/services/instances?service_type=prometheus")
assert response.status_code == 200
assert len(response.json()) == 1
assert response.json()[0]["service_type"] == "grafana"
assert response.json()[0]["service_type"] == "prometheus"
def test_update_service_preserves_unsent_secrets(client):
created = client.post("/api/services/instances", json=_grafana_payload()).json()
created = client.post("/api/services/instances", json=_prometheus_payload()).json()
# Update without sending secrets; the existing key should remain set.
updated = client.put(
f"/api/services/instances/{created['id']}",
json={
"service_type": "grafana",
"name": "Renamed Grafana",
"config": {"base_url": "https://grafana.example.com", "timeout_seconds": 10},
"service_type": "prometheus",
"name": "Renamed Prometheus",
"config": {"base_url": "https://prometheus.example.com", "timeout_seconds": 10},
},
).json()
assert updated["name"] == "Renamed Grafana"
assert updated["name"] == "Renamed Prometheus"
assert updated["secrets_set"] == {"api_key": True}
def test_update_service_can_clear_secret(client):
created = client.post("/api/services/instances", json=_grafana_payload()).json()
created = client.post("/api/services/instances", json=_prometheus_payload()).json()
updated = client.put(
f"/api/services/instances/{created['id']}",
json={
"service_type": "grafana",
"name": "Production Grafana",
"config": {"base_url": "https://grafana.example.com"},
"service_type": "prometheus",
"name": "Production Prometheus",
"config": {"base_url": "https://prometheus.example.com"},
"secrets": {"api_key": ""},
},
).json()
@@ -275,30 +272,28 @@ def test_unknown_service_type_rejected(client):
def test_invalid_config_rejected(client):
response = client.post(
"/api/services/instances",
json={"service_type": "grafana", "name": "x", "config": {"base_url": ""}},
json={"service_type": "prometheus", "name": "x", "config": {"base_url": ""}},
)
assert response.status_code == 422
# Force a real validation error via bad type.
response = client.post(
"/api/services/instances",
json={"service_type": "grafana", "name": "x", "config": {"timeout_seconds": "fast"}},
json={"service_type": "prometheus", "name": "x", "config": {"timeout_seconds": "fast"}},
)
assert response.status_code == 422
@pytest.mark.parametrize(
"bad_url", ["grafana.example.com", "localhost:3000", "//grafana.example.com", "ftp://grafana.example.com"]
"bad_url", ["prometheus.example.com", "localhost:3000", "//bad.example.com", "ftp://bad.example.com"]
)
def test_service_base_url_requires_http_schema(bad_url):
"""Every service base_url must include an http:// or https:// schema."""
model = get_service_definition("grafana").config_model
model = get_service_definition("prometheus").config_model
with pytest.raises(ValidationError):
model.model_validate({"base_url": bad_url, "timeout_seconds": 5})
@pytest.mark.parametrize(
"service_type", ["grafana", "prometheus", "alertmanager", "jellyfin", "authentik", "nextcloud"]
)
@pytest.mark.parametrize("service_type", ["prometheus", "alertmanager", "jellyfin", "authentik", "nextcloud"])
def test_service_base_url_accepts_absolute_urls(service_type):
model = get_service_definition(service_type).config_model
instance = model.model_validate({"base_url": "https://example.com"})
@@ -309,9 +304,9 @@ def test_unknown_secret_field_rejected(client):
response = client.post(
"/api/services/instances",
json={
"service_type": "grafana",
"service_type": "prometheus",
"name": "x",
"config": {"base_url": "https://grafana.example.com"},
"config": {"base_url": "https://prometheus.example.com"},
"secrets": {"password": "leak"},
},
)
@@ -322,9 +317,9 @@ def test_credential_key_in_config_rejected(client):
response = client.post(
"/api/services/instances",
json={
"service_type": "grafana",
"service_type": "prometheus",
"name": "x",
"config": {"base_url": "https://grafana.example.com", "api_key": "leak"},
"config": {"base_url": "https://prometheus.example.com", "api_key": "leak"},
},
)
assert response.status_code == 422
@@ -333,22 +328,22 @@ def test_credential_key_in_config_rejected(client):
def test_update_nonexistent_returns_404(client):
response = client.put(
"/api/services/instances/missing",
json=_grafana_payload(id="missing"),
json=_prometheus_payload(id="missing"),
)
assert response.status_code == 404
def test_update_id_mismatch_returns_400(client):
created = client.post("/api/services/instances", json=_grafana_payload()).json()
created = client.post("/api/services/instances", json=_prometheus_payload()).json()
response = client.put(
f"/api/services/instances/{created['id']}",
json=_grafana_payload(id="other-id"),
json=_prometheus_payload(id="other-id"),
)
assert response.status_code == 400
def test_delete_service(client):
created = client.post("/api/services/instances", json=_grafana_payload()).json()
created = client.post("/api/services/instances", json=_prometheus_payload()).json()
response = client.delete(f"/api/services/instances/{created['id']}")
assert response.status_code == 200
assert client.get("/api/services/instances").json() == []
@@ -372,7 +367,7 @@ def test_delete_service_cascades_to_widgets(client, tmp_path):
"""
store = app.dependency_overrides[get_settings_store]()
service = store.upsert_service(
{"service_type": "grafana", "name": "Grafana", "config": {"base_url": "u"}, "enabled": True}
{"service_type": "prometheus", "name": "Prometheus", "config": {"base_url": "u"}, "enabled": True}
)
# Ensure the service_id column exists and seed a referencing widget.
@@ -386,7 +381,7 @@ def test_delete_service_cascades_to_widgets(client, tmp_path):
enabled, sort_order, created_at, updated_at, service_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
("w1", "grafana", "grafana.link", "Link", "{}", 1, 0, 1, 1, service["id"]),
("w1", "prometheus", "prometheus.metric", "Link", "{}", 1, 0, 1, 1, service["id"]),
)
store.delete_service(service["id"])