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:
@@ -103,10 +103,18 @@ def list_builtin_kinds() -> list[BuiltinWidgetKindInfo]:
|
||||
|
||||
@router.get("/instances")
|
||||
def list_instances(
|
||||
service_id: str | None = None,
|
||||
scope: str | None = None,
|
||||
store: SettingsStore = Depends(get_settings_store),
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return all persisted widget instances."""
|
||||
return [WidgetInstance(**widget).model_dump() for widget in store.list_widgets()]
|
||||
"""Return widget instances, optionally filtered.
|
||||
|
||||
- ``?service_id=X``: only widgets for service X.
|
||||
- ``?scope=dashboard``: only widgets with NULL service_id.
|
||||
- ``?scope=service``: only widgets with a non-null service_id.
|
||||
- No params: all widgets (backward-compatible).
|
||||
"""
|
||||
return [WidgetInstance(**widget).model_dump() for widget in store.list_widgets(service_id=service_id, scope=scope)]
|
||||
|
||||
|
||||
@router.post("/instances", status_code=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -1403,10 +1403,36 @@ class SettingsStore:
|
||||
"sort_order": sort_order,
|
||||
}
|
||||
|
||||
def list_widgets(self) -> list[dict[str, Any]]:
|
||||
def list_widgets(
|
||||
self,
|
||||
service_id: str | None = None,
|
||||
*,
|
||||
scope: str | None = None,
|
||||
all_widgets: bool = True,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""List widget instances, optionally filtered.
|
||||
|
||||
- ``service_id=X``: only widgets for service X.
|
||||
- ``scope="dashboard"``: only widgets with NULL service_id.
|
||||
- ``scope="service"``: only widgets with a non-null service_id.
|
||||
- ``all_widgets=True, service_id=None, scope=None``: all widgets.
|
||||
"""
|
||||
self.init_schema()
|
||||
clauses: list[str] = []
|
||||
params: list[Any] = []
|
||||
if service_id is not None:
|
||||
clauses.append("service_id = ?")
|
||||
params.append(service_id)
|
||||
if scope == "dashboard":
|
||||
clauses.append("service_id IS NULL")
|
||||
elif scope == "service":
|
||||
clauses.append("service_id IS NOT NULL")
|
||||
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
with self.connect() as conn:
|
||||
rows = conn.execute("SELECT * FROM dashboard_widgets ORDER BY sort_order ASC, created_at ASC").fetchall()
|
||||
rows = conn.execute(
|
||||
f"SELECT * FROM dashboard_widgets{where} ORDER BY sort_order ASC, created_at ASC",
|
||||
params,
|
||||
).fetchall()
|
||||
return [self._row_to_widget(row) for row in rows]
|
||||
|
||||
def get_widget(self, widget_id: str | None) -> dict[str, Any] | None:
|
||||
|
||||
Reference in New Issue
Block a user