import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Key, X, ExternalLink, Loader2 } from "lucide-react"; import * as cmd from "../../lib/commands"; import type { QueryResult } from "../../lib/types"; import { useDbViewerStore } from "../../stores/dbViewerStore"; import { DataTypeIcon } from "../ui/DataTypeIcon"; interface FkPreviewPopoverProps { connectionId: string; schema: string; table: string; column: string; value: string; anchorRect: DOMRect | null; onClose: () => void; } export function FkPreviewPopover({ connectionId, schema, table, column, value, anchorRect, onClose, }: FkPreviewPopoverProps) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const popoverRef = useRef(null); const openTab = useDbViewerStore((s) => s.openTab); const setColumnFilter = useDbViewerStore((s) => s.setColumnFilter); const handleOpen = () => { openTab(schema, table); // Find the newly created tab and apply the column filter const newTab = useDbViewerStore.getState().tabs.find( (t) => t.schema === schema && t.table === table, ); if (newTab) { setColumnFilter(newTab.id, column, value); } onClose(); }; // Fetch the referenced row useEffect(() => { let cancelled = false; setLoading(true); setError(null); cmd .getFkPreview(connectionId, schema, table, column, value) .then((result) => { if (!cancelled) { setData(result); setLoading(false); } }) .catch((e: any) => { if (!cancelled) { setError(String(e)); setLoading(false); } }); return () => { cancelled = true; }; }, [connectionId, schema, table, column, value]); // Close on Escape useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [onClose]); // Close on outside click useEffect(() => { const onClick = (e: MouseEvent) => { if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { onClose(); } }; // Delay to avoid closing immediately from the same click that opened it const id = setTimeout(() => document.addEventListener("mousedown", onClick), 0); return () => { clearTimeout(id); document.removeEventListener("mousedown", onClick); }; }, [onClose]); if (!anchorRect) return null; // Compute position to keep popover within viewport const popoverWidth = 360; const popoverMaxHeight = 320; const gap = 8; let left = anchorRect.left; let top = anchorRect.bottom + gap; // Flip horizontally if off-screen if (left + popoverWidth > window.innerWidth - 16) { left = Math.max(16, window.innerWidth - popoverWidth - 16); } // Flip vertically if not enough space below if (top + popoverMaxHeight > window.innerHeight - 16) { top = anchorRect.top - popoverMaxHeight - gap; if (top < 16) top = 16; } return createPortal(
{/* Header */}
{schema}.{table}
{/* Body */}
{loading && (
Loading...
)} {error && (
{error}
)} {data && data.rows.length === 0 && !loading && (
No matching row found
)} {data && data.rows.length > 0 && ( {data.columns.map((col, ci) => { const cell = data.rows[0][ci]; const isNull = cell === null || cell === undefined; return ( ); })}
{col.is_pk && } {col.is_fk && } {col.name}
{isNull ? ( NULL ) : ( {String(cell)} )}
)}
, document.body, ); }