e1356b20f1
PR 3 of 4 for configurable dashboard widgets. - Add TypeScript widget interfaces (WidgetInstance, WidgetInstanceInput, WidgetTypeInfo, WidgetDataResponse). - Create widget API client for CRUD, registry metadata, and per-widget data. - Create TanStack Query hooks for instances, data, sources, types, and mutations. - Create closed frontend widget registry with metadata, source type, refresh intervals, and config fields. - Add six shadcn/ui-based widget components: Jellyfin, Backups, Grafana link, Prometheus metric, SSH task output, and static text. - Add Vitest unit tests for registry metadata. Verification: - backend ruff clean; pytest 200 passed - frontend npm run lint: 0 errors - frontend npm run build: success - frontend npm run test -- src/widgets/registry.test.ts: 3 passed
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
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 { BackupDashboardSummary } from "../types/backups";
|
|
import type { WidgetInstance } from "../types";
|
|
import { getWidgetDefinition } from "./registry";
|
|
|
|
interface Props {
|
|
widget: WidgetInstance;
|
|
}
|
|
|
|
export function BackupsWidget({ widget }: Props) {
|
|
const def = getWidgetDefinition(widget.widget_type);
|
|
const { data, isLoading } = useWidgetData(
|
|
widget.id,
|
|
def?.refreshInterval ?? 0,
|
|
);
|
|
const summary = data?.data as BackupDashboardSummary | undefined;
|
|
|
|
return (
|
|
<SectionCard title={widget.title} description={def?.description}>
|
|
{isLoading && !data ? (
|
|
<div className="flex flex-row flex-wrap gap-6">
|
|
<Skeleton className="h-10 w-20" />
|
|
<Skeleton className="h-10 w-20" />
|
|
<Skeleton className="h-10 w-20" />
|
|
</div>
|
|
) : data?.error ? (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>{data.error}</AlertDescription>
|
|
</Alert>
|
|
) : summary ? (
|
|
<div className="flex flex-row flex-wrap gap-6">
|
|
<div>
|
|
<div className="text-2xl font-semibold">{summary.total_jobs}</div>
|
|
<div className="text-xs text-muted-foreground">Jobs</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-semibold">
|
|
{summary.success_rate_24h}%
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">24h Success</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-semibold">
|
|
{summary.active_alerts > 0 ? (
|
|
<Badge variant="destructive">{summary.active_alerts}</Badge>
|
|
) : (
|
|
0
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">Alerts</div>
|
|
</div>
|
|
{summary.last_failed_at ? (
|
|
<div className="self-center text-xs text-destructive">
|
|
Last failed:{" "}
|
|
{new Date(summary.last_failed_at * 1000).toLocaleString()}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
</SectionCard>
|
|
);
|
|
}
|