Service IA refinement: nav naming, instance tabs, config to Settings, configurable Overview
Four coupled changes to the services-as-hub IA:
1. Nav entries use service TYPE names (Jellyfin, SSH Tasks, Alertmanager,
Grafana, Prometheus, Backups, Authentik) instead of conceptual names
(Media, Files, Actions, Alerts, Users). ssh_tasks collapses to one
entry ('SSH Tasks') instead of two. The content tabs inside each
service page surface the concepts (Files, Actions).
2. Service page gains a two-level tab structure when multiple enabled
instances of the same type exist: instance tabs on top ([Main Jellyfin]
[Backup Jellyfin]), content tabs below ([Overview] [Media] [Requests]
[Widgets]). Clicking an instance tab navigates to the sibling's route.
Single instance: no instance tabs. Replaces the dropdown switcher.
3. Config tab (connection fields, secrets, enable/disable, delete) moves
from the service page to Settings > Services tab. The service page
becomes a PURE operational view (Overview + content tabs + Widgets) --
no save/delete/config state. Settings gains a 4th tab 'Services' with
ServiceConfigEditor per instance (schema-driven config fields, secrets
with leave-blank-to-keep semantics, ConfirmDialog on delete).
4. Overview tab is now a configurable widget grid per service instance.
Each instance manages its own set of widgets on its Overview. Backend
widget list endpoints gain ?service_id= and ?scope= (dashboard|service)
filter params; the main Dashboard uses scope=dashboard to exclude
service-scoped widgets. The OverviewTab reuses WidgetInstanceCard +
WidgetConfigDialog. Empty state CTA for instances with no widgets.
All service-tab stubs are replaced; stubs.tsx deleted.
272 backend tests pass (+1 widget filter); 121 frontend tests pass (+3
instance-tabs + OverviewTab); lint/build green both sides.
This commit is contained in:
@@ -90,6 +90,44 @@ def test_create_backups_widget(client):
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
def test_widget_filtering_by_service_id_and_scope(client):
|
||||
"""Test ?service_id= and ?scope= query params on GET /api/widgets/instances."""
|
||||
service = _make_grafana_service(client)
|
||||
# Create a dashboard-scoped (built-in) widget + a service-scoped widget.
|
||||
client.post(
|
||||
"/api/widgets/instances",
|
||||
json={"widget_kind": "static", "title": "Note", "config": {"text": "hi"}},
|
||||
)
|
||||
client.post(
|
||||
"/api/widgets/instances",
|
||||
json={
|
||||
"service_id": service["id"],
|
||||
"widget_kind": "link",
|
||||
"title": "Dash",
|
||||
"config": {"dashboard_uid": "o"},
|
||||
},
|
||||
)
|
||||
|
||||
# No filter: both widgets.
|
||||
all_widgets = client.get("/api/widgets/instances").json()
|
||||
assert len(all_widgets) == 2
|
||||
|
||||
# Filter by service_id: only the service-scoped one.
|
||||
by_service = client.get(f"/api/widgets/instances?service_id={service['id']}").json()
|
||||
assert len(by_service) == 1
|
||||
assert by_service[0]["service_id"] == service["id"]
|
||||
|
||||
# scope=dashboard: only the built-in (NULL service_id).
|
||||
dashboard_scope = client.get("/api/widgets/instances?scope=dashboard").json()
|
||||
assert len(dashboard_scope) == 1
|
||||
assert dashboard_scope[0]["service_id"] is None
|
||||
|
||||
# scope=service: only the non-null service_id widget.
|
||||
service_scope = client.get("/api/widgets/instances?scope=service").json()
|
||||
assert len(service_scope) == 1
|
||||
assert service_scope[0]["service_id"] == service["id"]
|
||||
|
||||
|
||||
def test_unknown_builtin_kind_rejected(client):
|
||||
response = client.post(
|
||||
"/api/widgets/instances",
|
||||
|
||||
Reference in New Issue
Block a user