import { useState } from "react" import { useWidgets, useCreateWidget, useUpdateWidget, useDeleteWidget, } from "@/api/widgets" import { useDataSources } from "@/api/data-sources" import type { Widget, DisplayHint, KeyMapping } from "@/api/types" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Badge } from "@/components/ui/badge" import { Plus, Pencil, Trash2, X } from "lucide-react" import { toast } from "sonner" const DISPLAY_HINTS: DisplayHint[] = ["icon_value", "text_block", "key_value"] const EMPTY: Widget = { id: 0, name: "", display_hint: "icon_value", data_source_id: 0, mappings: [], max_data_size: 2048, } export function WidgetsPage() { const { data: widgets = [], isLoading } = useWidgets() const { data: sources = [] } = useDataSources() const create = useCreateWidget() const update = useUpdateWidget() const del = useDeleteWidget() const [editing, setEditing] = useState(null) const [deleting, setDeleting] = useState(null) function openNew() { const nextId = widgets.length > 0 ? Math.max(...widgets.map((w) => w.id)) + 1 : 1 setEditing({ ...EMPTY, id: nextId, data_source_id: sources[0]?.id ?? 0, }) } async function save() { if (!editing) return const isNew = !widgets.some((w) => w.id === editing.id) try { if (isNew) { await create.mutateAsync(editing) toast.success("Widget created") } else { await update.mutateAsync(editing) toast.success("Widget updated") } setEditing(null) } catch (e) { toast.error(String(e)) } } async function confirmDelete() { if (deleting == null) return try { await del.mutateAsync(deleting) toast.success("Widget deleted") } catch (e) { toast.error(String(e)) } setDeleting(null) } const sourceName = (id: number) => sources.find((s) => s.id === id)?.name ?? `#${id}` if (isLoading) return
Loading…
return (

Widgets

Display primitives bound to data sources

{widgets.length === 0 ? (

No widgets configured yet.

) : (
{widgets.map((w) => (
{w.name} {w.display_hint} source: {sourceName(w.data_source_id)} {w.mappings.length} mapping(s)
))}
)} !o && setEditing(null)}> {editing && widgets.some((w) => w.id === editing.id) ? "Edit Widget" : "New Widget"} {editing && ( )} !o && setDeleting(null)} > Delete widget? This will permanently remove this widget. Layout references will become dangling. Cancel Delete
) } function WidgetForm({ value, onChange, sources, }: { value: Widget onChange: (w: Widget) => void sources: { id: number; name: string }[] }) { const set = (k: K, v: Widget[K]) => onChange({ ...value, [k]: v }) function addMapping() { set("mappings", [...value.mappings, { source_path: "", target_key: "" }]) } function updateMapping(i: number, m: KeyMapping) { const next = [...value.mappings] next[i] = m set("mappings", next) } function removeMapping(i: number) { set( "mappings", value.mappings.filter((_, idx) => idx !== i), ) } return (
set("name", e.target.value)} placeholder="e.g. weather" />
set("max_data_size", Number(e.target.value))} min={1} />
{value.mappings.map((m, i) => (
updateMapping(i, { ...m, source_path: e.target.value }) } placeholder="$.path.to.value" className="flex-1" /> → updateMapping(i, { ...m, target_key: e.target.value }) } placeholder="target_key" className="flex-1" />
))}
) }