import { useSendToAgentChat } from "@agent-native/core/client/agent-chat"; import { useActionQuery, useActionMutation, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { IconBook2, IconPencil, IconPlus, IconTrash, IconSearch, IconExternalLink, } from "@tabler/icons-react"; import { useMemo, useState } from "react"; import { useSetHeaderActions } from "@/components/layout/HeaderActions"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; interface DictionaryEntry { id: string; metric: string; definition: string; department?: string; table?: string; columnsUsed?: string; cuts?: string; queryTemplate?: string; exampleOutput?: string; joinPattern?: string; updateFrequency?: string; dataLag?: string; dependencies?: string; validDateRange?: string; commonQuestions?: string; knownGotchas?: string; exampleUseCase?: string; owner?: string; approved?: boolean; aiGenerated?: boolean; sourceUrl?: string; updatedAt?: string; } function safeHttpUrl(value?: string): string | null { if (!value) return null; try { const url = new URL(value.trim()); return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null; } catch { return null; } } const EMPTY_ENTRY: Partial = { metric: "", definition: "", department: "", table: "", columnsUsed: "", cuts: "", queryTemplate: "", exampleOutput: "", joinPattern: "", updateFrequency: "", dataLag: "", dependencies: "", validDateRange: "", commonQuestions: "", knownGotchas: "", exampleUseCase: "", owner: "", approved: true, aiGenerated: false, }; const DEPARTMENT_BADGE: Record = { Sales: "bg-green-500/10 text-green-600 dark:text-green-400", Marketing: "bg-blue-500/10 text-blue-600 dark:text-blue-400", Product: "bg-orange-500/10 text-orange-600 dark:text-orange-400", Data: "bg-amber-500/10 text-amber-600 dark:text-amber-400", Finance: "bg-rose-500/10 text-rose-600 dark:text-rose-400", Engineering: "bg-cyan-500/10 text-cyan-600 dark:text-cyan-400", }; const ENTRY_BADGE_CLASS = "max-w-full min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[10px] px-1.5 py-0"; function deptClass(dept?: string): string { if (!dept) return "bg-muted text-muted-foreground"; return DEPARTMENT_BADGE[dept] ?? "bg-muted text-muted-foreground"; } function DictionaryBadge({ children, tooltip, className, }: { children: React.ReactNode; tooltip: string; className?: string; }) { return ( {children} {tooltip} ); } export default function DataDictionary() { const t = useT(); const [search, setSearch] = useState(""); const [editing, setEditing] = useState | null>(null); const [toDelete, setToDelete] = useState(null); const { data: entries, isLoading } = useActionQuery( "list-data-dictionary", search ? { search } : undefined, { staleTime: 30_000 }, ); const { send } = useSendToAgentChat(); const save = useActionMutation("save-data-dictionary-entry"); const remove = useActionMutation("delete-data-dictionary-entry"); const list = useMemo( () => (entries as DictionaryEntry[] | undefined) ?? [], [entries], ); useSetHeaderActions( , ); return (

{t("dataDictionary.intro")}

setSearch(e.target.value)} placeholder={t("dataDictionary.searchPlaceholder")} className="pl-9" />
{isLoading ? (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) : list.length === 0 ? (

{t("dataDictionary.noEntriesTitle")}

{t("dataDictionary.noEntriesDescription")}

) : (
{list.map((e) => (
{e.metric}
{e.definition && ( {e.definition} )}
{e.department && ( {e.department} )} {e.table && ( {e.table} )} {e.approved ? ( {t("dataDictionary.approved")} ) : e.aiGenerated ? ( {t("dataDictionary.suggestion")} ) : ( {t("dataDictionary.unreviewed")} )} {e.aiGenerated && e.approved && ( {t("dataDictionary.ai")} )}
{safeHttpUrl(e.sourceUrl) && ( {t("dataDictionary.source")} )}
))}
)} setEditing(null)} onSave={async (entry) => { const metric = entry.metric?.trim(); const definition = entry.definition?.trim(); if (!metric || !definition) return; await save.mutateAsync({ ...entry, metric, definition }); setEditing(null); }} saving={save.isPending} /> !open && setToDelete(null)} > {t("dataDictionary.deleteTitle", { metric: toDelete?.metric ?? "", })} {t("dataDictionary.deleteDescription")} {t("sidebar.cancel")} { if (toDelete) { await remove.mutateAsync({ id: toDelete.id }); setToDelete(null); } }} > {t("sidebar.delete")}
); } interface EditEntryDialogProps { entry: Partial | null; onClose: () => void; onSave: (entry: Partial) => Promise; saving: boolean; } function EditEntryDialog({ entry, onClose, onSave, saving, }: EditEntryDialogProps) { const t = useT(); const [draft, setDraft] = useState>({}); // Reset the form when a new entry is opened const currentId = entry?.id ?? "__new__"; const [lastId, setLastId] = useState(""); if (entry && currentId !== lastId) { setDraft({ ...entry }); setLastId(currentId); } if (!entry && lastId) setLastId(""); const set = ( key: K, value: DictionaryEntry[K], ) => setDraft((d) => ({ ...d, [key]: value })); return ( !open && onClose()}> {entry?.id ? t("dataDictionary.editEntry") : t("dataDictionary.newDictionaryEntry")} {t("dataDictionary.dialogDescription")}
set("metric", e.target.value)} placeholder={t("dataDictionary.metricPlaceholder")} />