feat(service-storage-harness): slice 2 — qbit widgets + LineSeriesChart extract

This commit is contained in:
Developer
2026-07-09 08:46:20 +00:00
parent e7bd0afdd1
commit 1fb12b8a0a
14 changed files with 831 additions and 81 deletions
@@ -0,0 +1,51 @@
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { SectionCard } from "../components/SectionCard";
import { useWidgetData } from "../hooks/useWidgets";
import type { WidgetInstance } from "../types";
interface Props {
widget: WidgetInstance;
refreshIntervalMs: number;
description?: string;
}
export function QbittorrentTotalsWidget({
widget,
refreshIntervalMs,
description,
}: Props) {
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
const payload = data?.data as
| { total?: number; by_state?: Record<string, number> }
| undefined;
return (
<SectionCard title={widget.title} description={description}>
{isLoading && !data ? (
<Skeleton className="h-20 w-full" />
) : data?.error ? (
<Alert variant="destructive">
<AlertDescription>{data.error}</AlertDescription>
</Alert>
) : payload ? (
<div className="space-y-3">
<div>
<div className="text-3xl font-semibold">{payload.total ?? 0}</div>
<div className="text-xs text-muted-foreground">Total torrents</div>
</div>
{payload.by_state && Object.keys(payload.by_state).length > 0 ? (
<div className="flex flex-wrap gap-1.5">
{Object.entries(payload.by_state).map(([state, count]) => (
<Badge key={state} variant="secondary">
{state}: {count}
</Badge>
))}
</div>
) : null}
</div>
) : null}
</SectionCard>
);
}