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:
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Configurable per-service Overview tab.
|
||||
*
|
||||
* Each service instance manages its own set of widgets on this tab. The
|
||||
* widget system is reused from the main Dashboard: widget instances with
|
||||
* a `service_id` matching this instance are fetched and rendered in a
|
||||
* responsive grid. An edit button opens the WidgetConfigDialog (same one
|
||||
* the Dashboard uses) for add/remove/reorder/enable/disable.
|
||||
*/
|
||||
import { useMemo, useState } from "react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Settings2 } from "lucide-react";
|
||||
import { useWidgetInstances } from "../../hooks/useWidgets";
|
||||
import { WidgetInstanceCard } from "../../components/WidgetInstance";
|
||||
import { WidgetConfigDialog } from "../../components/WidgetConfigDialog";
|
||||
import type { ServiceInstance } from "../../types";
|
||||
|
||||
export function OverviewTab({ instance }: { instance: ServiceInstance }) {
|
||||
const { data: widgets = [] } = useWidgetInstances(instance.id);
|
||||
const [configOpen, setConfigOpen] = useState(false);
|
||||
|
||||
const visibleWidgets = useMemo(
|
||||
() =>
|
||||
widgets
|
||||
.filter((w) => w.enabled)
|
||||
.sort((a, b) => a.sort_order - b.sort_order),
|
||||
[widgets],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground">
|
||||
{instance.name} overview
|
||||
</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mobile-touch-target"
|
||||
onClick={() => setConfigOpen(true)}
|
||||
>
|
||||
<Settings2 className="size-4" />
|
||||
Edit widgets
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{visibleWidgets.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{visibleWidgets.map((widget) => (
|
||||
<WidgetInstanceCard key={widget.id} widget={widget} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Alert>
|
||||
<AlertDescription className="flex flex-col gap-3">
|
||||
<span>
|
||||
No widgets on this overview yet. Add widgets to show key metrics
|
||||
and information for {instance.name}.
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-fit mobile-touch-target"
|
||||
onClick={() => setConfigOpen(true)}
|
||||
>
|
||||
Add widgets
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<WidgetConfigDialog
|
||||
open={configOpen}
|
||||
onClose={() => setConfigOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user