import { useState, useMemo, useCallback } from 'react' import { useQuery } from '@tanstack/react-query' import { getTableList, getTableSchema, getTableData } from '@/api/lightrag' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/Dialog' import DataTable from '@/components/ui/DataTable' import { ColumnDef } from '@tanstack/react-table' import Button from '@/components/ui/Button' import { ChevronLeftIcon, ChevronRightIcon, RefreshCwIcon, CopyIcon, CheckIcon } from 'lucide-react' import { toast } from 'sonner' const HIDDEN_COLUMNS = ['meta'] // Truncate long values for display function truncateValue(value: any, maxLength = 50): string { if (value === null || value === undefined) return '' let strValue: string if (typeof value === 'object') { strValue = JSON.stringify(value) } else { strValue = String(value) } if (strValue.length <= maxLength) return strValue return strValue.slice(0, maxLength) + '...' } // Format value for display in modal function formatValue(value: any): string { if (value === null) return 'null' if (value === undefined) return 'undefined' if (typeof value === 'object') { try { return JSON.stringify(value, null, 2) } catch { return String(value) } } return String(value) } // Check if value is JSON-like (object or array) function isJsonLike(value: any): boolean { return typeof value === 'object' && value !== null } // Copy to clipboard helper async function copyToClipboard(text: string): Promise { if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(text) return true } catch { // Fall through to legacy approach } } // Fallback for older browsers const textarea = document.createElement('textarea') textarea.value = text textarea.style.position = 'fixed' textarea.style.opacity = '0' try { document.body.appendChild(textarea) textarea.select() document.execCommand('copy') return true } catch { return false } finally { if (textarea.parentNode) { document.body.removeChild(textarea) } } } // Copy button component with feedback function CopyButton({ text, label }: { text: string; label?: string }) { const [copied, setCopied] = useState(false) const handleCopy = async () => { const success = await copyToClipboard(text) if (success) { setCopied(true) toast.success(label ? `${label} copied` : 'Copied to clipboard') setTimeout(() => setCopied(false), 2000) } else { toast.error('Failed to copy') } } return ( ) } // Row Detail Modal function RowDetailModal({ row, open, onOpenChange }: { row: Record | null open: boolean onOpenChange: (open: boolean) => void }) { const entries = useMemo(() => (row ? Object.entries(row) : []), [row]) const fullRowJson = useMemo(() => { try { return JSON.stringify(row, null, 2) } catch { return '[Unable to serialize row]' } }, [row]) if (!row) return null return ( Row Details Click the copy icon next to any field to copy its value
{entries.map(([key, value]) => (
{key}
{isJsonLike(value) ? (
                    {formatValue(value)}
                  
) : (
{formatValue(value)}
)}
))}
) } export default function TableExplorer() { const [selectedTable, setSelectedTable] = useState('') const [page, setPage] = useState(1) const [selectedRow, setSelectedRow] = useState | null>(null) const [modalOpen, setModalOpen] = useState(false) const pageSize = 20 // Fetch table list const { data: tableList } = useQuery({ queryKey: ['tables', 'list'], queryFn: getTableList, }) // Derive effective selection: use state if set, otherwise default to first table const effectiveSelectedTable = selectedTable || (tableList?.[0] ?? '') // Reset page when table changes const handleTableChange = (value: string) => { setSelectedTable(value) setPage(1) } // Fetch schema const { data: schema } = useQuery({ queryKey: ['tables', effectiveSelectedTable, 'schema'], queryFn: () => getTableSchema(effectiveSelectedTable), enabled: !!effectiveSelectedTable, }) // Fetch data const { data: tableData, isLoading, isError, error, refetch } = useQuery({ queryKey: ['tables', effectiveSelectedTable, 'data', page], queryFn: () => getTableData(effectiveSelectedTable, page, pageSize), enabled: !!effectiveSelectedTable, }) // Handle row click const handleRowClick = useCallback((row: Record) => { setSelectedRow(row) setModalOpen(true) }, []) // Generate columns dynamically from data const columns = useMemo[]>(() => { const cols: ColumnDef[] = [] if (tableData?.data && tableData.data.length > 0) { const allKeys = new Set() tableData.data.forEach((row: any) => { Object.keys(row).forEach(key => allKeys.add(key)) }) Array.from(allKeys).sort().forEach((key) => { if (HIDDEN_COLUMNS.includes(key)) return // Skip hidden columns cols.push({ accessorKey: key, header: () => (
{key}
), cell: ({ row }) => { const value = row.getValue(key) const displayValue = truncateValue(value, 50) const isLong = typeof value === 'object' || (typeof value === 'string' && value.length > 50) return (
{displayValue}
) }, }) }) } return cols }, [tableData?.data]) const totalPages = tableData?.total_pages || 0 return (
Table Explorer
{schema && (
Show Schema (DDL)
                {schema.ddl}
              
)}
{isLoading ? (
) : isError ? (

Failed to load table data

{error instanceof Error ? error.message : 'Unknown error'}

) : (
)}
{tableData?.total ? ( <> Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, tableData.total)} of {tableData.total} rows ) : ( 'No results' )}
{page} / {totalPages || 1}
) }