import { useState, useEffect, useCallback } from "react"; import { Save, X, Loader2, HelpCircle } from "lucide-react"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { Label } from "./ui/label"; import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; import { Checkbox } from "./ui/checkbox"; import { toast } from "sonner"; import { getDataCatalogSource, saveDataDictionary } from "../../services/orchestrationApi"; export interface DataDictionaryColumn { column_id: string; name: string; data_type: string; nullable: boolean; pii_flag: boolean; sample_values?: any[]; stats?: { min?: any; max?: any; mean?: number | null; median?: number | null; distinct_count?: number; }; description?: string; aggregation?: string; // sum, avg, min, max, count classify?: string; // dimension or measure business_meaning?: string; data_classification?: string; is_key?: boolean; } export interface DataDictionaryTable { table_id: string; name: string; row_count?: number; columns: DataDictionaryColumn[]; } export interface DataDictionarySource { source_id: string; source_type: "schema" | "tabular" | "unstructured"; name: string; tables: DataDictionaryTable[]; } interface DataDictionaryFormProps { open: boolean; onClose: () => void; sourceId: string; sourceName: string; userId: string; selectedTableId?: string; onTableChange?: (tableId: string) => void; } const AGGREGATION_OPTIONS = [ { value: "none", label: "None" }, { value: "sum", label: "Sum" }, { value: "avg", label: "Average" }, { value: "min", label: "Min" }, { value: "max", label: "Max" }, { value: "count", label: "Count" }, ]; const CLASSIFY_OPTIONS = [ { value: "none", label: "Select..." }, { value: "dimension", label: "Dimension" }, { value: "measure", label: "Measure" }, ]; export default function DataDictionaryForm({ open, onClose, sourceId, sourceName, userId, selectedTableId, onTableChange, }: DataDictionaryFormProps) { const [sources, setSources] = useState([]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [activeTableId, setActiveTableId] = useState(selectedTableId); const [editedColumns, setEditedColumns] = useState>>({}); useEffect(() => { if (open) { loadCatalog(); } }, [open, sourceId, userId]); useEffect(() => { if (selectedTableId) { setActiveTableId(selectedTableId); } }, [selectedTableId]); const loadCatalog = async () => { setLoading(true); try { const source = await getDataCatalogSource(userId, sourceId); if (source) { // Transform to our type - source now contains full tables/columns const transformed: DataDictionarySource = { source_id: source.source_id, source_type: source.source_type as "schema" | "tabular" | "unstructured", name: source.name, tables: (source.tables || []).map((t: any) => ({ table_id: t.table_id || t.name, name: t.name, row_count: t.row_count, columns: (t.columns || []).map((c: any) => ({ column_id: c.column_id, name: c.name, data_type: c.data_type, nullable: c.nullable, pii_flag: c.pii_flag, sample_values: c.sample_values, stats: c.stats, description: c.description, business_meaning: c.business_meaning, data_classification: c.data_classification, is_key: c.is_key, })), })), }; setSources([transformed]); // Auto-select first table if none selected if (!activeTableId && transformed.tables.length > 0) { setActiveTableId(transformed.tables[0].table_id); onTableChange?.(transformed.tables[0].table_id); } } } catch (err) { console.error("Failed to load catalog:", err); toast.error("Failed to load data dictionary"); } finally { setLoading(false); } }; const handleColumnChange = useCallback( (columnId: string, field: keyof DataDictionaryColumn, value: any) => { setEditedColumns((prev) => ({ ...prev, [columnId]: { ...prev[columnId], [field]: value, }, })); }, [] ); const handleSave = async () => { if (saving) return; setSaving(true); try { const source = sources[0]; if (!source) throw new Error("Source not found"); // Prepare updated tables - apply edits to ALL tables const updatedTables = source.tables.map((table) => { const updatedColumns = table.columns.map((col) => { const edits = editedColumns[col.column_id] || {}; return { ...col, ...edits, }; }); return { ...table, columns: updatedColumns }; }); // Update the source in catalog const updatedSource = { ...source, tables: updatedTables, }; await saveDataDictionary(userId, sourceId, updatedSource); toast.success("Data dictionary saved successfully"); onClose(); } catch (err) { console.error("Failed to save data dictionary:", err); toast.error(err instanceof Error ? err.message : "Failed to save data dictionary"); } finally { setSaving(false); } }; if (!open) return null; const source = sources[0]; const activeTable = source?.tables.find((t) => t.table_id === activeTableId); return (
{/* Header */}

Data Dictionary

{sourceName}

{/* Content */}
{loading ? (
) : source ? (
{/* Table selector for multi-table sources */} {source.tables.length > 1 && (
)} {/* Columns Table */} {activeTable && ( {activeTable.name} ({activeTable.columns.length} columns) {activeTable.row_count && ( {activeTable.row_count.toLocaleString()} rows )}
{activeTable.columns.map((col, index) => { const edits = editedColumns[col.column_id] || {}; const classify = (edits.classify ?? col.classify ?? "") as string; const isMeasure = classify === "measure"; return ( ); })}
# Column Type Nullable Description Classify Aggregation
{index + 1}
{col.name}
{col.data_type} handleColumnChange(col.column_id, "description", e.target.value)} placeholder="Add description..." className="text-xs h-7 px-2" />
)} {!source && (

No catalog data found for this source.

)}
) : (

No catalog data found for this source.

)}
{/* Footer */}
); } export { DataDictionaryForm };