import { GitBranch, Package, Search, Server, User, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { searchAPI } from "../utils/api"; const GlobalSearch = () => { const [query, setQuery] = useState(""); const [results, setResults] = useState(null); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1); const searchRef = useRef(null); const inputRef = useRef(null); const navigate = useNavigate(); // Debounce search const debounceTimerRef = useRef(null); const performSearch = useCallback(async (searchQuery) => { if (!searchQuery || searchQuery.trim().length === 0) { setResults(null); setIsOpen(false); return; } setIsLoading(true); try { const response = await searchAPI.global(searchQuery); setResults(response.data); setIsOpen(true); setSelectedIndex(-1); } catch (error) { console.error("Search error:", error); setResults(null); } finally { setIsLoading(false); } }, []); const handleInputChange = (e) => { const value = e.target.value; setQuery(value); // Clear previous timer if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } // Set new timer debounceTimerRef.current = setTimeout(() => { performSearch(value); }, 300); }; const handleClear = () => { // Clear debounce timer to prevent any pending searches if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } setQuery(""); setResults(null); setIsOpen(false); setSelectedIndex(-1); inputRef.current?.focus(); }; const handleResultClick = (result) => { // Navigate based on result type switch (result.type) { case "host": navigate(`/hosts/${result.id}`); break; case "package": navigate(`/packages/${result.id}`); break; case "repository": navigate(`/repositories/${result.id}`); break; case "user": // Users don't have detail pages, so navigate to settings navigate("/settings/users"); break; default: break; } // Close dropdown and clear handleClear(); }; // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event) => { if (searchRef.current && !searchRef.current.contains(event.target)) { setIsOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); // Keyboard navigation const flattenedResults = []; if (results) { if (results.hosts?.length > 0) { flattenedResults.push({ type: "header", label: "Hosts" }); flattenedResults.push(...results.hosts); } if (results.packages?.length > 0) { flattenedResults.push({ type: "header", label: "Packages" }); flattenedResults.push(...results.packages); } if (results.repositories?.length > 0) { flattenedResults.push({ type: "header", label: "Repositories" }); flattenedResults.push(...results.repositories); } if (results.users?.length > 0) { flattenedResults.push({ type: "header", label: "Users" }); flattenedResults.push(...results.users); } } const navigableResults = flattenedResults.filter((r) => r.type !== "header"); const handleKeyDown = (e) => { if (!isOpen || !results) return; switch (e.key) { case "ArrowDown": e.preventDefault(); setSelectedIndex((prev) => prev < navigableResults.length - 1 ? prev + 1 : prev, ); break; case "ArrowUp": e.preventDefault(); setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1)); break; case "Enter": e.preventDefault(); if (selectedIndex >= 0 && navigableResults[selectedIndex]) { handleResultClick(navigableResults[selectedIndex]); } break; case "Escape": e.preventDefault(); setIsOpen(false); setSelectedIndex(-1); break; default: break; } }; // Get icon for result type const getResultIcon = (type) => { switch (type) { case "host": return ; case "package": return ; case "repository": return ; case "user": return ; default: return null; } }; // Get display text for result const getResultDisplay = (result) => { switch (result.type) { case "host": return { primary: result.friendly_name || result.hostname, secondary: result.ip || result.hostname, }; case "package": return { primary: result.name, secondary: result.description || result.category, }; case "repository": return { primary: result.name, secondary: result.distribution, }; case "user": return { primary: result.username, secondary: result.email, }; default: return { primary: "", secondary: "" }; } }; const hasResults = results && (results.hosts?.length > 0 || results.packages?.length > 0 || results.repositories?.length > 0 || results.users?.length > 0); return (
{ if (query && results) setIsOpen(true); }} /> {query && ( )}
{/* Dropdown Results */} {isOpen && (
{isLoading ? (
Searching...
) : hasResults ? (
{/* Hosts */} {results.hosts?.length > 0 && (
Hosts
{results.hosts.map((host, _idx) => { const display = getResultDisplay(host); const globalIdx = navigableResults.findIndex( (r) => r.id === host.id && r.type === "host", ); return ( ); })}
)} {/* Packages */} {results.packages?.length > 0 && (
Packages
{results.packages.map((pkg, _idx) => { const display = getResultDisplay(pkg); const globalIdx = navigableResults.findIndex( (r) => r.id === pkg.id && r.type === "package", ); return ( ); })}
)} {/* Repositories */} {results.repositories?.length > 0 && (
Repositories
{results.repositories.map((repo, _idx) => { const display = getResultDisplay(repo); const globalIdx = navigableResults.findIndex( (r) => r.id === repo.id && r.type === "repository", ); return ( ); })}
)} {/* Users */} {results.users?.length > 0 && (
Users
{results.users.map((user, _idx) => { const display = getResultDisplay(user); const globalIdx = navigableResults.findIndex( (r) => r.id === user.id && r.type === "user", ); return ( ); })}
)}
) : query.trim() ? (
No results found for "{query}"
) : null}
)}
); }; export default GlobalSearch;