import React from "react"; import {SSHTunnelObject} from "./SSHTunnelObject.tsx"; import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion.tsx"; import {Separator} from "@/components/ui/separator.tsx"; import {Input} from "@/components/ui/input.tsx"; import {Search} from "lucide-react"; interface TunnelConnection { sourcePort: number; endpointPort: number; endpointHost: string; maxRetries: number; retryInterval: number; autoStart: boolean; } interface SSHHost { id: number; name: string; ip: string; port: number; username: string; folder: string; tags: string[]; pin: boolean; authType: string; enableTerminal: boolean; enableTunnel: boolean; enableConfigEditor: boolean; defaultPath: string; tunnelConnections: TunnelConnection[]; createdAt: string; updatedAt: string; } interface TunnelStatus { status: string; reason?: string; errorType?: string; retryCount?: number; maxRetries?: number; nextRetryIn?: number; retryExhausted?: boolean; } interface SSHTunnelViewerProps { hosts: SSHHost[]; tunnelStatuses: Record; tunnelActions: Record; onTunnelAction: (action: 'connect' | 'disconnect' | 'cancel', host: SSHHost, tunnelIndex: number) => Promise; } export function SSHTunnelViewer({ hosts = [], tunnelStatuses = {}, tunnelActions = {}, onTunnelAction }: SSHTunnelViewerProps): React.ReactElement { const [searchQuery, setSearchQuery] = React.useState(""); const [debouncedSearch, setDebouncedSearch] = React.useState(""); React.useEffect(() => { const handler = setTimeout(() => setDebouncedSearch(searchQuery), 200); return () => clearTimeout(handler); }, [searchQuery]); const filteredHosts = React.useMemo(() => { if (!debouncedSearch.trim()) return hosts; const query = debouncedSearch.trim().toLowerCase(); return hosts.filter(host => { const searchableText = [ host.name || '', host.username, host.ip, host.folder || '', ...(host.tags || []), host.authType, host.defaultPath || '' ].join(' ').toLowerCase(); return searchableText.includes(query); }); }, [hosts, debouncedSearch]); const tunnelHosts = React.useMemo(() => { return filteredHosts.filter(host => host.enableTunnel && host.tunnelConnections && host.tunnelConnections.length > 0 ); }, [filteredHosts]); const hostsByFolder = React.useMemo(() => { const map: Record = {}; tunnelHosts.forEach(host => { const folder = host.folder && host.folder.trim() ? host.folder : 'Uncategorized'; if (!map[folder]) map[folder] = []; map[folder].push(host); }); return map; }, [tunnelHosts]); const sortedFolders = React.useMemo(() => { const folders = Object.keys(hostsByFolder); folders.sort((a, b) => { if (a === 'Uncategorized') return -1; if (b === 'Uncategorized') 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]; }; return (

SSH Tunnels

Manage your SSH tunnel connections

setSearchQuery(e.target.value)} className="pl-10" />
{tunnelHosts.length === 0 ? (

No SSH Tunnels

{searchQuery.trim() ? "No hosts match your search criteria." : "Create your first SSH tunnel to get started. Use the SSH Manager to add hosts with tunnel connections." }

) : ( {sortedFolders.map((folder, idx) => ( {folder}
{getSortedHosts(hostsByFolder[folder]).map((host, hostIndex) => (
))}
))}
)}
); }