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:
@@ -6,17 +6,17 @@ describe("navEntries", () => {
|
||||
expect(configuredNavEntries(new Set())).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns Media when jellyfin is configured", () => {
|
||||
it("returns Jellyfin when jellyfin is configured", () => {
|
||||
const entries = configuredNavEntries(new Set(["jellyfin"]));
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0].label).toBe("Media");
|
||||
expect(entries[0].label).toBe("Jellyfin");
|
||||
expect(entries[0].path).toBe("/services/jellyfin");
|
||||
});
|
||||
|
||||
it("returns Files + Actions when ssh_tasks is configured", () => {
|
||||
it("returns one SSH Tasks entry when ssh_tasks is configured", () => {
|
||||
const entries = configuredNavEntries(new Set(["ssh_tasks"]));
|
||||
expect(entries).toHaveLength(2);
|
||||
expect(entries.map((e) => e.label)).toEqual(["Files", "Actions"]);
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0].label).toBe("SSH Tasks");
|
||||
});
|
||||
|
||||
it("returns all observability entries", () => {
|
||||
@@ -24,15 +24,15 @@ describe("navEntries", () => {
|
||||
new Set(["alertmanager", "grafana", "prometheus"]),
|
||||
);
|
||||
expect(entries.map((e) => e.label)).toEqual([
|
||||
"Alerts",
|
||||
"Alertmanager",
|
||||
"Grafana",
|
||||
"Prometheus",
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns Backups + Users when configured", () => {
|
||||
it("returns Backups + Authentik when configured", () => {
|
||||
const entries = configuredNavEntries(new Set(["backups", "authentik"]));
|
||||
expect(entries.map((e) => e.label)).toEqual(["Backups", "Users"]);
|
||||
expect(entries.map((e) => e.label)).toEqual(["Backups", "Authentik"]);
|
||||
});
|
||||
|
||||
it("nextcloud has no nav entries in the static map", () => {
|
||||
@@ -46,10 +46,9 @@ describe("navEntries", () => {
|
||||
new Set(["authentik", "ssh_tasks", "jellyfin"]),
|
||||
);
|
||||
expect(entries.map((e) => e.label)).toEqual([
|
||||
"Media",
|
||||
"Files",
|
||||
"Actions",
|
||||
"Users",
|
||||
"Jellyfin",
|
||||
"SSH Tasks",
|
||||
"Authentik",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
/**
|
||||
* Service-type → conditional nav-entry map.
|
||||
*
|
||||
* Each configured service type contributes one or more top-level nav entries
|
||||
* that appear only when at least one enabled instance of that type exists.
|
||||
* See OpenSpec change `services-as-hub-ia`, spec R1.2.
|
||||
* Each configured service type contributes ONE top-level nav entry that
|
||||
* appears only when at least one enabled instance of that type exists. The
|
||||
* label is the service TYPE name (Jellyfin, SSH Tasks), not a conceptual
|
||||
* name (Media, Files) — the service page's content tabs surface the concepts.
|
||||
*/
|
||||
import {
|
||||
Activity,
|
||||
DatabaseBackup,
|
||||
FolderOpen,
|
||||
GanttChartSquare,
|
||||
Link2,
|
||||
Monitor,
|
||||
Server,
|
||||
Users,
|
||||
Zap,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -26,31 +26,26 @@ export interface NavEntry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mapping from service type to its conditional nav entries.
|
||||
* `nextcloud` has no entries (no operational content).
|
||||
* Static mapping from service type to its conditional nav entry.
|
||||
* Uses the service type's display name. One entry per type.
|
||||
* `nextcloud` has no entry (no operational content tabs).
|
||||
*/
|
||||
export const SERVICE_TYPE_NAV_ENTRIES: NavEntry[] = [
|
||||
{
|
||||
serviceType: "jellyfin",
|
||||
label: "Media",
|
||||
label: "Jellyfin",
|
||||
icon: Monitor,
|
||||
path: "/services/jellyfin",
|
||||
},
|
||||
{
|
||||
serviceType: "ssh_tasks",
|
||||
label: "Files",
|
||||
icon: FolderOpen,
|
||||
path: "/services/ssh_tasks",
|
||||
},
|
||||
{
|
||||
serviceType: "ssh_tasks",
|
||||
label: "Actions",
|
||||
icon: Zap,
|
||||
label: "SSH Tasks",
|
||||
icon: Server,
|
||||
path: "/services/ssh_tasks",
|
||||
},
|
||||
{
|
||||
serviceType: "alertmanager",
|
||||
label: "Alerts",
|
||||
label: "Alertmanager",
|
||||
icon: Activity,
|
||||
path: "/services/alertmanager",
|
||||
},
|
||||
@@ -74,7 +69,7 @@ export const SERVICE_TYPE_NAV_ENTRIES: NavEntry[] = [
|
||||
},
|
||||
{
|
||||
serviceType: "authentik",
|
||||
label: "Users",
|
||||
label: "Authentik",
|
||||
icon: Users,
|
||||
path: "/services/authentik",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user