import { useState, useEffect, useRef, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { ExternalLink, Plug, Search, X } from "lucide-react"; import { getActiveSessions, deleteOpenTab, type ActiveSessionInfo, type OpenTabRecord } from "@/main-axios"; import { tabIcon } from "@/shell/tabUtils"; import type { Tab, TabType } from "@/types/ui-types"; import { Badge } from "@/components/badge"; import { Input } from "@/components/input"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/tooltip"; const CONNECTION_TAB_TYPES: TabType[] = [ "terminal", "rdp", "vnc", "telnet", "files", "docker", "stats", "tunnel", ]; const TYPE_LABELS: Record = { terminal: "SSH", rdp: "RDP", vnc: "VNC", telnet: "Telnet", files: "Files", docker: "Docker", stats: "Stats", tunnel: "Tunnel", }; function formatDuration(ms: number): string { const s = Math.max(0, Math.floor(ms / 1000)); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); if (m < 60) return `${m}m ${s % 60}s`; const h = Math.floor(m / 60); return `${h}h ${m % 60}m`; } function formatExpiry(updatedAt: string): string { const TTL_MS = 30 * 60 * 1000; const elapsed = Date.now() - new Date(updatedAt).getTime(); const remaining = TTL_MS - elapsed; if (remaining <= 0) return "expiring"; return formatDuration(remaining); } function ConnectionRow({ isActive, isLive, tabType, name, subLabel, icon, onSwitch, onClose, switchTitle, faded, }: { isActive?: boolean; isLive: boolean; tabType: string; name: string; subLabel: string; icon: React.ReactNode; onSwitch?: () => void; onClose: () => void; switchTitle?: string; faded?: boolean; }) { return (
e.key === "Enter" && onSwitch?.()} className={`group flex items-center gap-2.5 px-3 py-2.5 border-b border-border/40 transition-colors last:border-b-0 ${ faded ? "opacity-60" : "" } ${ isActive ? "bg-accent-brand/8 cursor-pointer border-l-2 border-l-accent-brand" : onSwitch ? "hover:bg-muted/40 cursor-pointer" : "" }`} >
{icon}
{name} {TYPE_LABELS[tabType] ?? tabType}
{subLabel}
{switchTitle && onSwitch && ( {switchTitle} )}
); } function SectionHeader({ label, count }: { label: string; count: number }) { return (
{label} {count}
); } export function ConnectionsPanel({ tabs, activeTabId, allHosts, backgroundTabRecords, onSwitchToTab, onCloseTab, onReopenTab, onForgetBackground, }: { tabs: Tab[]; activeTabId: string; allHosts: { id: string; name: string }[]; backgroundTabRecords: OpenTabRecord[]; onSwitchToTab: (tabId: string) => void; onCloseTab: (tabId: string) => void; onReopenTab: (record: OpenTabRecord, restoredSessionId: string | null) => void; onForgetBackground: (recordId: string) => void; }) { const { t } = useTranslation(); const [now, setNow] = useState(Date.now()); const [activeSessions, setActiveSessions] = useState([]); const [search, setSearch] = useState(""); const pollTimerRef = useRef | null>(null); const openTabs = tabs.filter((tab) => CONNECTION_TAB_TYPES.includes(tab.type)); // Filter background records to only those not already open in the tab bar const openInstanceIds = new Set(tabs.map((t) => t.instanceId).filter(Boolean)); const backgroundTabs = backgroundTabRecords.filter((r) => !openInstanceIds.has(r.id)); const q = search.trim().toLowerCase(); const filteredOpenTabs = q ? openTabs.filter((tab) => (tab.host?.name ?? tab.label).toLowerCase().includes(q)) : openTabs; const filteredBackgroundTabs = q ? backgroundTabs.filter((r) => { const host = allHosts.find((h) => h.id === String(r.hostId)); return (host?.name ?? r.label).toLowerCase().includes(q); }) : backgroundTabs; useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, []); const refresh = useCallback(async () => { try { const sessions = await getActiveSessions(); setActiveSessions((prev) => { const next = Array.isArray(sessions) ? sessions : []; if (JSON.stringify(prev) === JSON.stringify(next)) return prev; return next; }); } catch { // silently ignore } }, []); useEffect(() => { refresh(); pollTimerRef.current = setInterval(refresh, 5000); return () => { if (pollTimerRef.current) clearInterval(pollTimerRef.current); }; }, [refresh]); const sessionByInstanceId = new Map(activeSessions.map((s) => [s.tabInstanceId, s])); const hasAnything = openTabs.length > 0 || backgroundTabs.length > 0; const hasResults = filteredOpenTabs.length > 0 || filteredBackgroundTabs.length > 0; if (!hasAnything) { return (
{t("connections.noConnections")} {t("connections.noConnectionsDesc")}
); } return (
setSearch(e.target.value)} className="pl-8 h-7 text-xs" />
{!hasResults && (
{t("connections.noSearchResults")}
)} {filteredOpenTabs.length > 0 && (
{filteredOpenTabs.map((tab) => { const isActive = tab.id === activeTabId; const liveSession = tab.instanceId ? sessionByInstanceId.get(tab.instanceId) : undefined; const isLive = tab.type === "terminal" ? (liveSession?.isConnected ?? false) : true; const duration = liveSession?.createdAt ? formatDuration(now - liveSession.createdAt) : formatDuration(now - tab.openedAt); return ( onSwitchToTab(tab.id)} onClose={() => onCloseTab(tab.id)} /> ); })}
)} {filteredBackgroundTabs.length > 0 && (
0 ? "mt-2" : ""}`}>
{t("connections.backgroundDesc")}
{filteredBackgroundTabs.map((record) => { const host = record.hostId ? allHosts.find((h) => h.id === String(record.hostId)) : undefined; const expiresIn = formatExpiry(record.updatedAt); return ( { const liveSession = sessionByInstanceId.get(record.id); onReopenTab(record, liveSession?.sessionId ?? null); }} onClose={async () => { await deleteOpenTab(record.id).catch(() => {}); onForgetBackground(record.id); }} switchTitle={t("connections.reconnect")} /> ); })}
)}
); }