8d2e4c9bfd
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.
79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
/**
|
|
* 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>
|
|
);
|
|
}
|