import { useEffect, useRef, useState } from "react"; import { Search } from "lucide-react"; import { useDbViewerStore } from "../../stores/dbViewerStore"; import * as cmd from "../../lib/commands"; import type { ObjectSearchHit, ObjectType } from "../../lib/types"; const TYPE_TO_OBJECTS: Record = { FUNCTION: "functions", PROCEDURE: "procedures", TRIGGER: "triggers", SEQUENCE: "sequences", ENUM: "enums", EXTENSION: "extensions", INDEX: "indexes", CONSTRAINT: "constraints", }; interface ObjectSearchPaletteProps { connectionId: string; } export function ObjectSearchPalette({ connectionId }: ObjectSearchPaletteProps) { const open = useDbViewerStore((s) => s.objectSearchOpen); const setOpen = useDbViewerStore((s) => s.setObjectSearchOpen); const storeSchema = useDbViewerStore((s) => s.currentSchema); const currentSchema = storeSchema ?? "public"; const openTab = useDbViewerStore((s) => s.openTab); const setCurrentSchema = useDbViewerStore((s) => s.setCurrentSchema); const setSelectedObjectType = useDbViewerStore((s) => s.setSelectedObjectType); const [query, setQuery] = useState(""); const [hits, setHits] = useState([]); const [loading, setLoading] = useState(false); const timerRef = useRef | null>(null); // Clear the transient state whenever the palette is closed so it reopens // with an empty search and no stale results. useEffect(() => { if (!open) { setQuery(""); setHits([]); } }, [open]); // Debounced search against the current schema. useEffect(() => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } if (!query.trim()) { setHits([]); setLoading(false); return; } timerRef.current = setTimeout(async () => { setLoading(true); try { const results = await cmd.searchObjects( connectionId, currentSchema, query, ); setHits(results); } catch { setHits([]); } finally { setLoading(false); } }, 150); return () => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } }; }, [query, connectionId, currentSchema]); // Esc closes the palette. useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { setOpen(false); } }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [setOpen]); if (!open) return null; const grouped = hits.reduce>((acc, hit) => { const list = acc[hit.object_type] ?? []; list.push(hit); acc[hit.object_type] = list; return acc; }, {}); const handleSelect = (hit: ObjectSearchHit) => { if ( hit.object_type === "TABLE" || hit.object_type === "VIEW" || hit.object_type === "MATERIALIZED VIEW" ) { openTab(hit.schema, hit.name); } else { setCurrentSchema(hit.schema); setSelectedObjectType( TYPE_TO_OBJECTS[hit.object_type] ?? "functions", ); } setOpen(false); }; return (
setOpen(false)} role="dialog" aria-label="Search objects" >
e.stopPropagation()} >
setQuery(e.target.value)} placeholder="Search objects in current schema…" className="flex-1 bg-transparent text-sm text-text outline-none placeholder:text-text-muted" /> {loading && ( loading )}
{Object.entries(grouped).map(([type, list]) => (
{type}
{list.map((hit) => ( ))}
))} {!loading && query.trim() && hits.length === 0 && (
No matches
)}
); }