feat(widgets): add frontend widget runtime (types, API, hooks, registry, components)

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
This commit is contained in:
Developer
2026-06-21 16:24:44 +00:00
parent e6d333ef7b
commit e1356b20f1
13 changed files with 700 additions and 1 deletions
@@ -0,0 +1,48 @@
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { ExternalLink } from "lucide-react";
import { SectionCard } from "../components/SectionCard";
import { useWidgetData } from "../hooks/useWidgets";
import type { WidgetInstance } from "../types";
import { getWidgetDefinition } from "./registry";
interface Props {
widget: WidgetInstance;
}
export function GrafanaLinkWidget({ widget }: Props) {
const def = getWidgetDefinition(widget.widget_type);
const { data, isLoading } = useWidgetData(
widget.id,
def?.refreshInterval ?? 0,
);
const url = data?.data?.url as string | undefined;
return (
<SectionCard title={widget.title} description={def?.description}>
{isLoading && !data ? (
<Skeleton className="h-10 w-48" />
) : data?.error ? (
<Alert variant="destructive">
<AlertDescription>{data.error}</AlertDescription>
</Alert>
) : url ? (
<Button asChild>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
>
Open Grafana
<ExternalLink className="ml-2 h-4 w-4" />
</a>
</Button>
) : (
<Alert>
<AlertDescription>No Grafana URL configured.</AlertDescription>
</Alert>
)}
</SectionCard>
);
}