import React from "react"; import {Button} from "@/components/ui/button.tsx"; import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card.tsx"; import {Separator} from "@/components/ui/separator.tsx"; import { Loader2, Pin, Terminal, Network, FileEdit, Tag, Play, Square, AlertCircle, Clock, Wifi, WifiOff, Zap, X } from "lucide-react"; import {Badge} from "@/components/ui/badge.tsx"; const CONNECTION_STATES = { DISCONNECTED: "disconnected", CONNECTING: "connecting", CONNECTED: "connected", VERIFYING: "verifying", FAILED: "failed", UNSTABLE: "unstable", RETRYING: "retrying", WAITING: "waiting", DISCONNECTING: "disconnecting" }; 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; enableFileManager: 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 SSHTunnelObjectProps { host: SSHHost; tunnelStatuses: Record; tunnelActions: Record; onTunnelAction: (action: 'connect' | 'disconnect' | 'cancel', host: SSHHost, tunnelIndex: number) => Promise; compact?: boolean; bare?: boolean; } export function TunnelObject({ host, tunnelStatuses, tunnelActions, onTunnelAction, compact = false, bare = false }: SSHTunnelObjectProps): React.ReactElement { const getTunnelStatus = (tunnelIndex: number): TunnelStatus | undefined => { const tunnel = host.tunnelConnections[tunnelIndex]; const tunnelName = `${host.name || `${host.username}@${host.ip}`}_${tunnel.sourcePort}_${tunnel.endpointPort}`; return tunnelStatuses[tunnelName]; }; const getTunnelStatusDisplay = (status: TunnelStatus | undefined) => { if (!status) return { icon: , text: 'Unknown', color: 'text-muted-foreground', bgColor: 'bg-muted/50', borderColor: 'border-border' }; const statusValue = status.status || 'DISCONNECTED'; switch (statusValue.toUpperCase()) { case 'CONNECTED': return { icon: , text: 'Connected', color: 'text-green-600 dark:text-green-400', bgColor: 'bg-green-500/10 dark:bg-green-400/10', borderColor: 'border-green-500/20 dark:border-green-400/20' }; case 'CONNECTING': return { icon: , text: 'Connecting...', color: 'text-blue-600 dark:text-blue-400', bgColor: 'bg-blue-500/10 dark:bg-blue-400/10', borderColor: 'border-blue-500/20 dark:border-blue-400/20' }; case 'DISCONNECTING': return { icon: , text: 'Disconnecting...', color: 'text-orange-600 dark:text-orange-400', bgColor: 'bg-orange-500/10 dark:bg-orange-400/10', borderColor: 'border-orange-500/20 dark:border-orange-400/20' }; case 'DISCONNECTED': return { icon: , text: 'Disconnected', color: 'text-muted-foreground', bgColor: 'bg-muted/30', borderColor: 'border-border' }; case 'WAITING': return { icon: , color: 'text-blue-600 dark:text-blue-400', bgColor: 'bg-blue-500/10 dark:bg-blue-400/10', borderColor: 'border-blue-500/20 dark:border-blue-400/20' }; case 'ERROR': case 'FAILED': return { icon: , text: status.reason || 'Error', color: 'text-red-600 dark:text-red-400', bgColor: 'bg-red-500/10 dark:bg-red-400/10', borderColor: 'border-red-500/20 dark:border-red-400/20' }; default: return { icon: , text: statusValue, color: 'text-muted-foreground', bgColor: 'bg-muted/30', borderColor: 'border-border' }; } }; if (bare) { return (
{host.tunnelConnections && host.tunnelConnections.length > 0 ? (
{host.tunnelConnections.map((tunnel, tunnelIndex) => { const status = getTunnelStatus(tunnelIndex); const statusDisplay = getTunnelStatusDisplay(status); const tunnelName = `${host.name || `${host.username}@${host.ip}`}_${tunnel.sourcePort}_${tunnel.endpointPort}`; const isActionLoading = tunnelActions[tunnelName]; const statusValue = status?.status?.toUpperCase() || 'DISCONNECTED'; const isConnected = statusValue === 'CONNECTED'; const isConnecting = statusValue === 'CONNECTING'; const isDisconnecting = statusValue === 'DISCONNECTING'; const isRetrying = statusValue === 'RETRYING'; const isWaiting = statusValue === 'WAITING'; return (
{statusDisplay.icon}
Port {tunnel.sourcePort} → {tunnel.endpointHost}:{tunnel.endpointPort}
{statusDisplay.text}
{!isActionLoading ? (
{isConnected ? ( <> ) : isRetrying || isWaiting ? ( ) : ( )}
) : ( )}
{(statusValue === 'ERROR' || statusValue === 'FAILED') && status?.reason && (
Error:
{status.reason} {status.reason && status.reason.includes('Max retries exhausted') && ( <>
Check your Docker logs for the error reason, join the Discord or create a GitHub issue for help.
)}
)} {(statusValue === 'RETRYING' || statusValue === 'WAITING') && status?.retryCount && status?.maxRetries && (
{statusValue === 'WAITING' ? 'Waiting for retry' : 'Retrying connection'}
Attempt {status.retryCount} of {status.maxRetries} {status.nextRetryIn && ( • Next retry in {status.nextRetryIn} seconds )}
)}
); })}
) : (

No tunnel connections configured

)}
); } return (
{!compact && (
{host.pin && }

{host.name || `${host.username}@${host.ip}`}

{host.ip}:{host.port} • {host.username}

)} {!compact && host.tags && host.tags.length > 0 && (
{host.tags.slice(0, 3).map((tag, index) => ( {tag} ))} {host.tags.length > 3 && ( +{host.tags.length - 3} )}
)} {!compact && }
{!compact && (

Tunnel Connections ({host.tunnelConnections.length})

)} {host.tunnelConnections && host.tunnelConnections.length > 0 ? (
{host.tunnelConnections.map((tunnel, tunnelIndex) => { const status = getTunnelStatus(tunnelIndex); const statusDisplay = getTunnelStatusDisplay(status); const tunnelName = `${host.name || `${host.username}@${host.ip}`}_${tunnel.sourcePort}_${tunnel.endpointPort}`; const isActionLoading = tunnelActions[tunnelName]; const statusValue = status?.status?.toUpperCase() || 'DISCONNECTED'; const isConnected = statusValue === 'CONNECTED'; const isConnecting = statusValue === 'CONNECTING'; const isDisconnecting = statusValue === 'DISCONNECTING'; const isRetrying = statusValue === 'RETRYING'; const isWaiting = statusValue === 'WAITING'; return (
{statusDisplay.icon}
Port {tunnel.sourcePort} → {tunnel.endpointHost}:{tunnel.endpointPort}
{statusDisplay.text}
{!isActionLoading && (
{isConnected ? ( <> ) : isRetrying || isWaiting ? ( ) : ( )}
)} {isActionLoading && ( )}
{(statusValue === 'ERROR' || statusValue === 'FAILED') && status?.reason && (
Error:
{status.reason} {status.reason && status.reason.includes('Max retries exhausted') && ( <>
Check your Docker logs for the error reason, join the Discord or create a GitHub issue for help.
)}
)} {(statusValue === 'RETRYING' || statusValue === 'WAITING') && status?.retryCount && status?.maxRetries && (
{statusValue === 'WAITING' ? 'Waiting for retry' : 'Retrying connection'}
Attempt {status.retryCount} of {status.maxRetries} {status.nextRetryIn && ( • Next retry in {status.nextRetryIn} seconds )}
)}
); })}
) : (

No tunnel connections configured

)}
); }