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:
@@ -0,0 +1,61 @@
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SectionCard } from "../components/SectionCard";
|
||||
import { useWidgetData } from "../hooks/useWidgets";
|
||||
import type { WidgetInstance } from "../types";
|
||||
import { getWidgetDefinition } from "./registry";
|
||||
|
||||
interface Props {
|
||||
widget: WidgetInstance;
|
||||
}
|
||||
|
||||
type PromQLResult = {
|
||||
resultType?: string;
|
||||
result?: unknown;
|
||||
};
|
||||
|
||||
type PromQLVectorSample = {
|
||||
metric?: Record<string, string>;
|
||||
value?: [number, string];
|
||||
};
|
||||
|
||||
function formatPrometheusValue(result: PromQLResult | undefined): string {
|
||||
if (!result) return "No data";
|
||||
if (result.resultType === "scalar" && Array.isArray(result.result)) {
|
||||
return String(result.result[1] ?? "No data");
|
||||
}
|
||||
if (
|
||||
result.resultType === "vector" &&
|
||||
Array.isArray(result.result) &&
|
||||
result.result.length > 0
|
||||
) {
|
||||
const first = result.result[0] as PromQLVectorSample;
|
||||
if (first.value) return String(first.value[1]);
|
||||
}
|
||||
return JSON.stringify(result, null, 2);
|
||||
}
|
||||
|
||||
export function PrometheusMetricWidget({ widget }: Props) {
|
||||
const def = getWidgetDefinition(widget.widget_type);
|
||||
const { data, isLoading } = useWidgetData(
|
||||
widget.id,
|
||||
def?.refreshInterval ?? 0,
|
||||
);
|
||||
const result = data?.data?.result as PromQLResult | undefined;
|
||||
|
||||
return (
|
||||
<SectionCard title={widget.title} description={def?.description}>
|
||||
{isLoading && !data ? (
|
||||
<Skeleton className="h-10 w-32" />
|
||||
) : data?.error ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{data.error}</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap text-sm">
|
||||
{formatPrometheusValue(result)}
|
||||
</pre>
|
||||
)}
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user