import React, {useState, useEffect, useCallback} from "react"; import {SSHTunnelViewer} from "@/ui/SSH/Tunnel/SSHTunnelViewer.tsx"; import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/ui/SSH/ssh-axios"; 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; password?: string; key?: string; keyPassword?: string; keyType?: 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 SSHTunnelProps { filterHostKey?: string; } export function SSHTunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement { // Keep full list for endpoint lookups; keep a separate visible list for UI const [allHosts, setAllHosts] = useState([]); const [visibleHosts, setVisibleHosts] = useState([]); const [tunnelStatuses, setTunnelStatuses] = useState>({}); const [tunnelActions, setTunnelActions] = useState>({}); const prevVisibleHostRef = React.useRef(null); const haveTunnelConnectionsChanged = (a: TunnelConnection[] = [], b: TunnelConnection[] = []): boolean => { if (a.length !== b.length) return true; for (let i = 0; i < a.length; i++) { const x = a[i]; const y = b[i]; if ( x.sourcePort !== y.sourcePort || x.endpointPort !== y.endpointPort || x.endpointHost !== y.endpointHost || x.maxRetries !== y.maxRetries || x.retryInterval !== y.retryInterval || x.autoStart !== y.autoStart ) { return true; } } return false; }; const fetchHosts = useCallback(async () => { const hostsData = await getSSHHosts(); setAllHosts(hostsData); const nextVisible = filterHostKey ? hostsData.filter(h => { const key = (h.name && h.name.trim() !== '') ? h.name : `${h.username}@${h.ip}`; return key === filterHostKey; }) : hostsData; // Silent update: only set state if meaningful changes const prev = prevVisibleHostRef.current; const curr = nextVisible[0] ?? null; let changed = false; if (!prev && curr) changed = true; else if (prev && !curr) changed = true; else if (prev && curr) { if ( prev.id !== curr.id || prev.name !== curr.name || prev.ip !== curr.ip || prev.port !== curr.port || prev.username !== curr.username || haveTunnelConnectionsChanged(prev.tunnelConnections, curr.tunnelConnections) ) { changed = true; } } if (changed) { setVisibleHosts(nextVisible); prevVisibleHostRef.current = curr; } }, [filterHostKey]); const fetchTunnelStatuses = useCallback(async () => { const statusData = await getTunnelStatuses(); setTunnelStatuses(statusData); }, []); useEffect(() => { fetchHosts(); const interval = setInterval(fetchHosts, 5000); // Refresh immediately when hosts are changed elsewhere (e.g., SSH Manager) const handleHostsChanged = () => { fetchHosts(); }; window.addEventListener('ssh-hosts:changed', handleHostsChanged as EventListener); return () => { clearInterval(interval); window.removeEventListener('ssh-hosts:changed', handleHostsChanged as EventListener); }; }, [fetchHosts]); useEffect(() => { fetchTunnelStatuses(); const interval = setInterval(fetchTunnelStatuses, 500); return () => clearInterval(interval); }, [fetchTunnelStatuses]); const handleTunnelAction = async (action: 'connect' | 'disconnect' | 'cancel', host: SSHHost, tunnelIndex: number) => { const tunnel = host.tunnelConnections[tunnelIndex]; const tunnelName = `${host.name || `${host.username}@${host.ip}`}_${tunnel.sourcePort}_${tunnel.endpointPort}`; setTunnelActions(prev => ({...prev, [tunnelName]: true})); try { if (action === 'connect') { const endpointHost = allHosts.find(h => h.name === tunnel.endpointHost || `${h.username}@${h.ip}` === tunnel.endpointHost ); if (!endpointHost) { throw new Error('Endpoint host not found'); } const tunnelConfig = { name: tunnelName, hostName: host.name || `${host.username}@${host.ip}`, sourceIP: host.ip, sourceSSHPort: host.port, sourceUsername: host.username, sourcePassword: host.authType === 'password' ? host.password : undefined, sourceAuthMethod: host.authType, sourceSSHKey: host.authType === 'key' ? host.key : undefined, sourceKeyPassword: host.authType === 'key' ? host.keyPassword : undefined, sourceKeyType: host.authType === 'key' ? host.keyType : undefined, endpointIP: endpointHost.ip, endpointSSHPort: endpointHost.port, endpointUsername: endpointHost.username, endpointPassword: endpointHost.authType === 'password' ? endpointHost.password : undefined, endpointAuthMethod: endpointHost.authType, endpointSSHKey: endpointHost.authType === 'key' ? endpointHost.key : undefined, endpointKeyPassword: endpointHost.authType === 'key' ? endpointHost.keyPassword : undefined, endpointKeyType: endpointHost.authType === 'key' ? endpointHost.keyType : undefined, sourcePort: tunnel.sourcePort, endpointPort: tunnel.endpointPort, maxRetries: tunnel.maxRetries, retryInterval: tunnel.retryInterval * 1000, autoStart: tunnel.autoStart, isPinned: host.pin }; await connectTunnel(tunnelConfig); } else if (action === 'disconnect') { await disconnectTunnel(tunnelName); } else if (action === 'cancel') { await cancelTunnel(tunnelName); } await fetchTunnelStatuses(); } catch (err) { } finally { setTunnelActions(prev => ({...prev, [tunnelName]: false})); } }; return ( ); }