"use client"; import { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { Filter, ChevronDown, ChevronLeft, ChevronRight, X, ArrowUpDown, Search, ExternalLink } from 'lucide-react'; import type { Node } from '@/types/database'; type SortOrder = 'updated' | 'edges' | 'created' | 'event_date'; const SORT_LABELS: Record = { updated: 'Recently Updated', edges: 'Most Edges', created: 'Creation Date', event_date: 'Event Date', }; const PAGE_SIZE = 50; interface DimensionSummary { dimension: string; count: number; isPriority: boolean; } interface DatabaseTableViewProps { onNodeClick: (nodeId: number) => void; refreshToken?: number; toolbarHost?: HTMLDivElement | null; } function formatRelativeTime(dateStr: string): string { const now = Date.now(); const then = new Date(dateStr).getTime(); const diff = now - then; const minutes = Math.floor(diff / 60000); if (minutes < 1) return 'just now'; if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 30) return `${days}d ago`; const months = Math.floor(days / 30); if (months < 12) return `${months}mo ago`; return `${Math.floor(months / 12)}y ago`; } function formatDate(dateStr: string | null | undefined): string { if (!dateStr) return '\u2014'; try { return dateStr.slice(0, 10); } catch { return '\u2014'; } } export default function DatabaseTableView({ onNodeClick, refreshToken = 0, toolbarHost }: DatabaseTableViewProps) { const [nodes, setNodes] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); const [sortOrder, setSortOrder] = useState('updated'); const [searchQuery, setSearchQuery] = useState(''); const [activeSearch, setActiveSearch] = useState(''); const [selectedFilters, setSelectedFilters] = useState([]); const [dimensions, setDimensions] = useState([]); const [showFilterPicker, setShowFilterPicker] = useState(false); const [filterSearchQuery, setFilterSearchQuery] = useState(''); const [showSortDropdown, setShowSortDropdown] = useState(false); const [hoveredRow, setHoveredRow] = useState(null); const filterPickerRef = useRef(null); const sortDropdownRef = useRef(null); const sortedDimensions = useMemo(() => { return [...dimensions].sort((a, b) => { if (a.isPriority && !b.isPriority) return -1; if (!a.isPriority && b.isPriority) return 1; return a.dimension.localeCompare(b.dimension); }); }, [dimensions]); const filterPickerDimensions = sortedDimensions.filter(d => d.dimension.toLowerCase().includes(filterSearchQuery.toLowerCase()) ); // Fetch dimensions useEffect(() => { (async () => { try { const res = await fetch('/api/dimensions'); const data = await res.json(); if (data.success) setDimensions(data.data || []); } catch (e) { console.error('Error fetching dimensions:', e); } })(); }, [refreshToken]); // Fetch nodes const fetchNodes = useCallback(async () => { setLoading(true); try { const params = new URLSearchParams({ limit: String(PAGE_SIZE), offset: String((page - 1) * PAGE_SIZE), sortBy: sortOrder, }); if (activeSearch) params.set('search', activeSearch); if (selectedFilters.length > 0) { params.set('dimensions', selectedFilters.join(',')); params.set('dimensionsMatch', 'all'); } const res = await fetch(`/api/nodes?${params}`); const data = await res.json(); if (data.success) { setNodes(data.data || []); setTotal(data.total ?? data.count ?? 0); } } catch (e) { console.error('Error fetching nodes:', e); } finally { setLoading(false); } }, [page, sortOrder, activeSearch, selectedFilters]); useEffect(() => { fetchNodes(); }, [fetchNodes, refreshToken]); // Reset to page 1 when filters/sort/search change const filtersKey = selectedFilters.join(','); useEffect(() => { setPage(1); }, [sortOrder, activeSearch, filtersKey]); // Close dropdowns on outside click useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (showFilterPicker && filterPickerRef.current && !filterPickerRef.current.contains(e.target as HTMLElement)) { setShowFilterPicker(false); setFilterSearchQuery(''); } if (showSortDropdown && sortDropdownRef.current && !sortDropdownRef.current.contains(e.target as HTMLElement)) { setShowSortDropdown(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [showFilterPicker, showSortDropdown]); const handleSearchSubmit = (e: React.FormEvent) => { e.preventDefault(); setActiveSearch(searchQuery); }; const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); const startItem = (page - 1) * PAGE_SIZE + 1; const endItem = Math.min(page * PAGE_SIZE, total); const toolbar = (
setSearchQuery(e.target.value)} placeholder="Search nodes..." style={{ background: 'transparent', border: 'none', color: 'var(var(--rah-text-active))', fontSize: '12px', padding: '5px 0', outline: 'none', width: '140px', }} /> {activeSearch && ( )}
{/* Filter chips + button */}
{selectedFilters.map(f => (
{f}
))}
{showFilterPicker && (
setFilterSearchQuery(e.target.value)} placeholder="Search dimensions..." autoFocus style={{ width: '100%', padding: '7px 10px', background: 'var(var(--rah-bg-surface))', border: '1px solid transparent', borderRadius: '6px', color: 'var(var(--rah-text-active))', fontSize: '12px', marginBottom: '4px', outline: 'none', }} /> {filterPickerDimensions.length === 0 ? (
No matching dimensions
) : ( filterPickerDimensions.map(d => ( )) )}
)}
{selectedFilters.length > 0 && ( )}
{/* Sort dropdown */}
{showSortDropdown && (
{(Object.keys(SORT_LABELS) as SortOrder[]).map(key => ( ))}
)}
{/* Pagination */}
{total > 0 ? `${startItem}-${endItem} of ${total}` : '0 nodes'}
); return (
{toolbarHost ? createPortal(toolbar, toolbarHost) : (
{toolbar}
)} {/* Table */}
{loading ? (
Loading...
) : nodes.length === 0 ? (
{activeSearch || selectedFilters.length > 0 ? 'No nodes match your filters.' : 'No nodes yet.'}
) : ( {nodes.map((node, i) => { const metaStr = node.metadata ? (typeof node.metadata === 'string' ? node.metadata : JSON.stringify(node.metadata)) : ''; return ( onNodeClick(node.id)} onMouseEnter={() => setHoveredRow(node.id)} onMouseLeave={() => setHoveredRow(null)} style={{ height: '44px', cursor: 'pointer', background: hoveredRow === node.id ? '#141414' : i % 2 === 0 ? '#080808' : '#0d0d0d', transition: 'background 0.1s ease', }} > {/* Title */} {/* ID */} {/* Description */} {/* Notes */} {/* Link */} {/* Dimensions */} {/* Edges */} {/* Event Date */} {/* Updated */} {/* Created */} {/* Metadata */} {/* Chunk */} {/* Chunk Status */} {/* Embedding Updated */} ); })}
TITLE ID DESCRIPTION NOTES LINK DIMENSIONS EDGES EVENT UPDATED CREATED METADATA CHUNK CHUNK STATUS EMB UPDATED
{node.title || 'Untitled'}
{node.id}
{node.description || '\u2014'}
{node.notes ? node.notes.slice(0, 120) : '\u2014'}
{node.link ? ( {node.link.replace(/^https?:\/\/(www\.)?/, '')} ) : ( {'\u2014'} )}
{node.dimensions && node.dimensions.length > 0 ? ( <> {node.dimensions.slice(0, 3).map(d => ( {d} ))} {node.dimensions.length > 3 && ( +{node.dimensions.length - 3} )} ) : ( {'\u2014'} )}
{node.edge_count ?? 0} {formatDate(node.event_date)} {formatRelativeTime(node.updated_at)} {formatRelativeTime(node.created_at)}
{metaStr || '\u2014'}
{node.chunk ? node.chunk.slice(0, 100) : '\u2014'}
{node.chunk_status || '\u2014'} {node.embedding_updated_at ? formatRelativeTime(node.embedding_updated_at) : '\u2014'}
)}
); } function thStyle(extra: React.CSSProperties = {}): React.CSSProperties { return { position: 'sticky' as const, top: 0, background: 'var(var(--rah-bg-base))', padding: '8px 12px', fontSize: '10px', fontWeight: 500, color: 'var(var(--rah-text-muted))', textAlign: 'left', letterSpacing: '0.05em', whiteSpace: 'nowrap', borderBottom: '1px solid rgba(255,255,255,0.06)', zIndex: 1, ...extra, }; } function tdStyle(extra: React.CSSProperties = {}): React.CSSProperties { return { padding: '0 12px', verticalAlign: 'middle', borderBottom: '1px solid #111', overflow: 'hidden', ...extra, }; } const truncCell: React.CSSProperties = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', };