import React, {useState} from 'react'; import { CornerDownLeft, Hammer, Pin, Menu } from "lucide-react" import { Button } from "@/components/ui/button.tsx" import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuItem, SidebarProvider, } from "@/components/ui/sidebar.tsx" import { Separator, } from "@/components/ui/separator.tsx" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet.tsx"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion.tsx"; import {ScrollArea} from "@/components/ui/scroll-area.tsx"; import {Input} from "@/components/ui/input.tsx"; import {getSSHHosts} from "@/apps/SSH/ssh-axios"; import {Checkbox} from "@/components/ui/checkbox.tsx"; interface SSHHost { id: number; name: string; ip: string; port: number; username: string; folder: string; tags: string[]; pin: boolean; authType: string; password?: string; key?: string; keyPassword?: string; keyType?: string; enableTerminal: boolean; enableTunnel: boolean; enableConfigEditor: boolean; defaultPath: string; tunnelConnections: any[]; createdAt: string; updatedAt: string; } export interface SidebarProps { onSelectView: (view: string) => void; onHostConnect: (hostConfig: any) => void; allTabs: { id: number; title: string; terminalRef: React.RefObject }[]; runCommandOnTabs: (tabIds: number[], command: string) => void; onCloseSidebar?: () => void; onAddHostSubmit?: (data: any) => void; open?: boolean; onOpenChange?: (open: boolean) => void; } export function TerminalSidebar({ onSelectView, onHostConnect, allTabs, runCommandOnTabs, onCloseSidebar, open, onOpenChange }: SidebarProps): React.ReactElement { const [hosts, setHosts] = useState([]); const [hostsLoading, setHostsLoading] = useState(false); const [hostsError, setHostsError] = useState(null); const prevHostsRef = React.useRef([]); const fetchHosts = React.useCallback(async () => { setHostsLoading(true); setHostsError(null); try { const newHosts = await getSSHHosts(); const terminalHosts = newHosts.filter(host => host.enableTerminal); const prevHosts = prevHostsRef.current; const isSame = terminalHosts.length === prevHosts.length && terminalHosts.every((h: SSHHost, i: number) => { const prev = prevHosts[i]; if (!prev) return false; return ( h.id === prev.id && h.name === prev.name && h.folder === prev.folder && h.ip === prev.ip && h.port === prev.port && h.username === prev.username && h.password === prev.password && h.authType === prev.authType && h.key === prev.key && h.pin === prev.pin && JSON.stringify(h.tags) === JSON.stringify(prev.tags) ); }); if (!isSame) { setHosts(terminalHosts); prevHostsRef.current = terminalHosts; } } catch (err: any) { setHostsError('Failed to load hosts'); } finally { setHostsLoading(false); } }, []); React.useEffect(() => { fetchHosts(); const interval = setInterval(fetchHosts, 10000); return () => clearInterval(interval); }, [fetchHosts]); const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); React.useEffect(() => { const handler = setTimeout(() => setDebouncedSearch(search), 200); return () => clearTimeout(handler); }, [search]); const filteredHosts = React.useMemo(() => { if (!debouncedSearch.trim()) return hosts; const q = debouncedSearch.trim().toLowerCase(); return hosts.filter(h => { const searchableText = [ h.name || '', h.username, h.ip, h.folder || '', ...(h.tags || []), h.authType, h.defaultPath || '' ].join(' ').toLowerCase(); return searchableText.includes(q); }); }, [hosts, debouncedSearch]); const hostsByFolder = React.useMemo(() => { const map: Record = {}; filteredHosts.forEach(h => { const folder = h.folder && h.folder.trim() ? h.folder : 'No Folder'; if (!map[folder]) map[folder] = []; map[folder].push(h); }); return map; }, [filteredHosts]); const sortedFolders = React.useMemo(() => { const folders = Object.keys(hostsByFolder); folders.sort((a, b) => { if (a === 'No Folder') return -1; if (b === 'No Folder') return 1; return a.localeCompare(b); }); return folders; }, [hostsByFolder]); const getSortedHosts = (arr: SSHHost[]) => { const pinned = arr.filter(h => h.pin).sort((a, b) => (a.name || '').localeCompare(b.name || '')); const rest = arr.filter(h => !h.pin).sort((a, b) => (a.name || '').localeCompare(b.name || '')); return [...pinned, ...rest]; }; const [toolsSheetOpen, setToolsSheetOpen] = useState(false); const [toolsCommand, setToolsCommand] = useState(""); const [selectedTabIds, setSelectedTabIds] = useState([]); const handleTabToggle = (tabId: number) => { setSelectedTabIds(prev => prev.includes(tabId) ? prev.filter(id => id !== tabId) : [...prev, tabId]); }; const handleRunCommand = () => { if (selectedTabIds.length && toolsCommand.trim()) { let cmd = toolsCommand; if (!cmd.endsWith("\n")) cmd += "\n"; runCommandOnTabs(selectedTabIds, cmd); setToolsCommand(""); } }; function getCookie(name: string) { return document.cookie.split('; ').reduce((r, v) => { const parts = v.split('='); return parts[0] === name ? decodeURIComponent(parts[1]) : r; }, ""); } const updateRightClickCopyPaste = (checked) => { document.cookie = `rightClickCopyPaste=${checked}; expires=2147483647; path=/`; } return ( Termix / Terminal
setSearch(e.target.value)} placeholder="Search hosts by name, username, IP, folder, tags..." className="w-full h-8 text-sm bg-background border border-border rounded" autoComplete="off" />
{hostsError && (
{hostsError}
)}
0 ? sortedFolders : undefined}> {sortedFolders.map((folder, idx) => ( {folder} {getSortedHosts(hostsByFolder[folder]).map(host => (
))}
{idx < sortedFolders.length - 1 && (
)}
))}
Tools
Run multiwindow commands