import React, { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import { Kbd } from "@/components/kbd"; import { Command, CommandItem, CommandList, CommandGroup, CommandSeparator, } from "@/components/command"; import { Server, Settings, Terminal, FolderOpen, FolderSearch, Box, Globe, Plus, MessagesSquare, LifeBuoy, Search, Activity, Network, User, KeyRound, LayoutDashboard, Monitor, Clock, Folder, Pencil, } from "lucide-react"; import { getRecentActivity, type RecentActivityItem } from "@/main-axios"; import type { Host } from "@/types/ui-types"; interface CommandPaletteProps { isOpen: boolean; setIsOpen: (isOpen: boolean) => void; hosts: Host[]; onOpenTab: (type: any, label?: string, pendingEvent?: string) => void; } const ACTIVITY_ICONS: Record = { terminal: , file_manager: , server_stats: , tunnel: , docker: , telnet: , vnc: , rdp: , }; const ACTIVITY_TAB_TYPE: Record = { terminal: "terminal", file_manager: "files", server_stats: "stats", tunnel: "tunnel", docker: "docker", telnet: "telnet", vnc: "vnc", rdp: "rdp", }; function getSshActions(host: Host) { const metricsEnabled = host.statsConfig?.metricsEnabled !== false; return [ host.enableTerminal !== false && { type: "terminal", icon: Terminal, label: "Terminal", }, host.enableFileManager && { type: "files", icon: FolderSearch, label: "Files", }, host.enableDocker && { type: "docker", icon: Box, label: "Docker" }, host.enableTunnel && { type: "tunnel", icon: Network, label: "Tunnels" }, metricsEnabled && { type: "stats", icon: Activity, label: "Stats" }, ].filter(Boolean) as { type: string; icon: React.ElementType; label: string; }[]; } export function CommandPalette({ isOpen, setIsOpen, hosts, onOpenTab, }: CommandPaletteProps) { const inputRef = useRef(null); const [search, setSearch] = useState(""); const [recentActivity, setRecentActivity] = useState( [], ); useEffect(() => { if (isOpen) { setTimeout(() => inputRef.current?.focus(), 50); setSearch(""); getRecentActivity(5) .then(setRecentActivity) .catch(() => {}); } }, [isOpen]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { setIsOpen(false); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [setIsOpen]); const filteredHosts = hosts.filter( (h) => h.name.toLowerCase().includes(search.toLowerCase()) || h.ip.toLowerCase().includes(search.toLowerCase()) || h.username.toLowerCase().includes(search.toLowerCase()), ); // Group hosts by folder; ungrouped hosts appear first under an implicit root group const groupedHosts: { folder: string | null; hosts: Host[] }[] = []; const folderMap = new Map(); const ungrouped: Host[] = []; for (const h of filteredHosts) { if (h.folder) { if (!folderMap.has(h.folder)) folderMap.set(h.folder, []); folderMap.get(h.folder)!.push(h); } else { ungrouped.push(h); } } if (ungrouped.length > 0) groupedHosts.push({ folder: null, hosts: ungrouped }); for (const [folder, fhosts] of folderMap) { groupedHosts.push({ folder, hosts: fhosts }); } const handleAction = (action: () => void) => { action(); setIsOpen(false); }; if (!isOpen) return null; return (
setIsOpen(false)} >
e.stopPropagation()} >
setSearch(e.target.value)} placeholder="Search hosts, commands, or settings..." className="flex-1 h-12 bg-transparent text-sm outline-none placeholder:text-muted-foreground" />
ESC
handleAction(() => onOpenTab("host-manager"))} className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer" >
Host Manager Manage, add, or edit hosts
handleAction(() => onOpenTab( "host-manager", undefined, "host-manager:add-host", ), ) } className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer" >
Add New Host Register a new host
handleAction(() => onOpenTab("admin-settings"))} className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer" >
Admin Settings Configure system preferences and users
handleAction(() => onOpenTab("user-profile"))} className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer" >
User Profile Manage your account and preferences
handleAction(() => onOpenTab( "host-manager", undefined, "host-manager:add-credential", ), ) } className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer" >
Add Credential Store SSH keys or passwords
{recentActivity.length > 0 && ( <> {recentActivity.map((item) => ( handleAction(() => onOpenTab( ACTIVITY_TAB_TYPE[item.type] as any, item.hostName, ), ) } className="group flex items-center gap-3 px-3 py-2 rounded-none hover:bg-accent-brand/10 cursor-pointer" >
{ACTIVITY_ICONS[item.type]}
{item.hostName} {item.type.replace("_", " ")}
{new Date(item.timestamp).toLocaleDateString()}
))}
)} {filteredHosts.length > 0 ? ( groupedHosts.map(({ folder, hosts: groupHosts }) => (
{folder && (
{folder}
)} {groupHosts.map((host, i) => ( handleAction(() => onOpenTab("terminal", host.name)) } className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer" >
{host.name} {host.online && ( )}
{host.username}@{host.ip}
{host.enableSsh && getSshActions(host).map( ({ type, icon: Icon, label }) => ( ), )} {host.enableSsh && (host.enableRdp || host.enableVnc || host.enableTelnet) && (
)} {host.enableRdp && ( )} {host.enableVnc && ( )} {host.enableTelnet && ( )}
))}
)) ) : (
No hosts found matching “{search}”
)}
window.open( "https://github.com/Termix-SSH/Termix", "_blank", ) } className="flex items-center gap-3 px-3 py-2 rounded-none hover:bg-accent-brand/10 cursor-pointer" > GitHub window.open( "https://discord.com/invite/jVQGdvHDrf", "_blank", ) } className="flex items-center gap-3 px-3 py-2 rounded-none hover:bg-accent-brand/10 cursor-pointer" > Discord window.open( "https://github.com/Termix-SSH/Support/issues/new", "_blank", ) } className="flex items-center gap-3 px-3 py-2 rounded-none hover:bg-accent-brand/10 cursor-pointer" > Support
↑↓ Navigate
ENTER Select
Toggle with Shift + Shift
); }