import React from "react"; import { useSidebar } from "@/components/ui/sidebar.tsx"; import { Separator } from "@/components/ui/separator.tsx"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs.tsx"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import type { SSHHost, DockerContainer, DockerValidation } from "@/types"; import { connectDockerSession, disconnectDockerSession, listDockerContainers, validateDockerAvailability, keepaliveDockerSession, verifyDockerTOTP, logActivity, } from "@/ui/main-axios.ts"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; import { AlertCircle } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert.tsx"; import { ContainerList } from "./components/ContainerList.tsx"; import { ContainerDetail } from "./components/ContainerDetail.tsx"; import { TOTPDialog } from "@/ui/desktop/navigation/TOTPDialog.tsx"; import { SSHAuthDialog } from "@/ui/desktop/navigation/SSHAuthDialog.tsx"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; interface DockerManagerProps { hostConfig?: SSHHost; title?: string; isVisible?: boolean; isTopbarOpen?: boolean; embedded?: boolean; onClose?: () => void; } interface TabData { id: number; type: string; [key: string]: unknown; } export function DockerManager({ hostConfig, title, isVisible = true, isTopbarOpen = true, embedded = false, onClose, }: DockerManagerProps): React.ReactElement { const { t } = useTranslation(); const { state: sidebarState } = useSidebar(); const { currentTab, removeTab } = useTabs() as { currentTab: number | null; removeTab: (tabId: number) => void; }; const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig); const [sessionId, setSessionId] = React.useState(null); const [containers, setContainers] = React.useState([]); const [selectedContainer, setSelectedContainer] = React.useState< string | null >(null); const [isConnecting, setIsConnecting] = React.useState(false); const [activeTab, setActiveTab] = React.useState("containers"); const [dockerValidation, setDockerValidation] = React.useState(null); const [isValidating, setIsValidating] = React.useState(false); const [viewMode, setViewMode] = React.useState<"list" | "detail">("list"); const [isLoadingContainers, setIsLoadingContainers] = React.useState(false); const [totpRequired, setTotpRequired] = React.useState(false); const [totpSessionId, setTotpSessionId] = React.useState(null); const [totpPrompt, setTotpPrompt] = React.useState(""); const [showAuthDialog, setShowAuthDialog] = React.useState(false); const [authReason, setAuthReason] = React.useState< "no_keyboard" | "auth_failed" | "timeout" >("no_keyboard"); const activityLoggedRef = React.useRef(false); const activityLoggingRef = React.useRef(false); const logDockerActivity = async () => { if ( !currentHostConfig?.id || activityLoggedRef.current || activityLoggingRef.current ) { return; } activityLoggingRef.current = true; activityLoggedRef.current = true; try { const hostName = currentHostConfig.name || `${currentHostConfig.username}@${currentHostConfig.ip}`; await logActivity("docker", currentHostConfig.id, hostName); } catch (err) { console.warn("Failed to log docker activity:", err); activityLoggedRef.current = false; } finally { activityLoggingRef.current = false; } }; React.useEffect(() => { if (hostConfig?.id !== currentHostConfig?.id) { setCurrentHostConfig(hostConfig); setContainers([]); setSelectedContainer(null); setSessionId(null); setDockerValidation(null); setViewMode("list"); } }, [hostConfig?.id]); React.useEffect(() => { const fetchLatestHostConfig = async () => { if (hostConfig?.id) { try { const { getSSHHosts } = await import("@/ui/main-axios.ts"); const hosts = await getSSHHosts(); const updatedHost = hosts.find((h) => h.id === hostConfig.id); if (updatedHost) { setCurrentHostConfig(updatedHost); } } catch { // Silently handle error } } }; fetchLatestHostConfig(); const handleHostsChanged = async () => { if (hostConfig?.id) { try { const { getSSHHosts } = await import("@/ui/main-axios.ts"); const hosts = await getSSHHosts(); const updatedHost = hosts.find((h) => h.id === hostConfig.id); if (updatedHost) { setCurrentHostConfig(updatedHost); } } catch { // Silently handle error } } }; window.addEventListener("ssh-hosts:changed", handleHostsChanged); return () => window.removeEventListener("ssh-hosts:changed", handleHostsChanged); }, [hostConfig?.id]); const initializingRef = React.useRef(false); React.useEffect(() => { const initSession = async () => { if (!currentHostConfig?.id || !currentHostConfig.enableDocker) { return; } if (initializingRef.current) return; initializingRef.current = true; if (sessionId) { initializingRef.current = false; return; } setIsConnecting(true); const sid = `docker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; try { const result = await connectDockerSession(sid, currentHostConfig.id, { useSocks5: currentHostConfig.useSocks5, socks5Host: currentHostConfig.socks5Host, socks5Port: currentHostConfig.socks5Port, socks5Username: currentHostConfig.socks5Username, socks5Password: currentHostConfig.socks5Password, socks5ProxyChain: currentHostConfig.socks5ProxyChain, }); if (result?.requires_totp) { setTotpRequired(true); setTotpSessionId(sid); setTotpPrompt(result.prompt || t("docker.verificationCodePrompt")); setIsConnecting(false); return; } if (result?.status === "auth_required") { setShowAuthDialog(true); setAuthReason( result.reason === "no_keyboard" ? "no_keyboard" : "auth_failed", ); setIsConnecting(false); return; } setSessionId(sid); setIsValidating(true); const validation = await validateDockerAvailability(sid); setDockerValidation(validation); setIsValidating(false); if (!validation.available) { toast.error( validation.error || "Docker is not available on this host", ); } else { logDockerActivity(); } } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to connect to host", ); setIsConnecting(false); setIsValidating(false); onClose?.(); } finally { setIsConnecting(false); } }; initSession(); return () => { initializingRef.current = false; if (sessionId) { disconnectDockerSession(sessionId).catch(() => { // Silently handle disconnect errors }); } }; }, [currentHostConfig?.id, currentHostConfig?.enableDocker]); React.useEffect(() => { if (!sessionId || !isVisible) return; const keepalive = setInterval( () => { keepaliveDockerSession(sessionId).catch(() => { // Silently handle keepalive errors }); }, 10 * 60 * 1000, ); return () => clearInterval(keepalive); }, [sessionId, isVisible]); const refreshContainers = React.useCallback(async () => { if (!sessionId) return; try { const data = await listDockerContainers(sessionId, true); setContainers(data); } catch (error) { // Silently handle polling errors } }, [sessionId]); React.useEffect(() => { if (!sessionId || !isVisible || !dockerValidation?.available) return; let cancelled = false; const pollContainers = async () => { try { setIsLoadingContainers(true); const data = await listDockerContainers(sessionId, true); if (!cancelled) { setContainers(data); } } catch (error) { // Silently handle polling errors } finally { if (!cancelled) { setIsLoadingContainers(false); } } }; pollContainers(); const interval = setInterval(pollContainers, 5000); return () => { cancelled = true; clearInterval(interval); }; }, [sessionId, isVisible, dockerValidation?.available]); const handleBack = React.useCallback(() => { setViewMode("list"); setSelectedContainer(null); }, []); const handleTotpSubmit = async (code: string) => { if (!totpSessionId || !code) return; try { setIsConnecting(true); const result = await verifyDockerTOTP(totpSessionId, code); if (result?.status === "success") { setTotpRequired(false); setTotpPrompt(""); setSessionId(totpSessionId); setTotpSessionId(null); setIsValidating(true); const validation = await validateDockerAvailability(totpSessionId); setDockerValidation(validation); setIsValidating(false); if (!validation.available) { toast.error( validation.error || "Docker is not available on this host", ); } else { logDockerActivity(); } } } catch (error) { console.error("TOTP verification failed:", error); toast.error(t("docker.totpVerificationFailed")); } finally { setIsConnecting(false); } }; const handleTotpCancel = () => { setTotpRequired(false); setTotpSessionId(null); setTotpPrompt(""); setIsConnecting(false); if (currentTab !== null) { removeTab(currentTab); } }; const handleAuthSubmit = async (credentials: { password?: string; sshKey?: string; keyPassword?: string; }) => { if (!currentHostConfig?.id) return; setShowAuthDialog(false); setIsConnecting(true); const sid = `docker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; try { const result = await connectDockerSession(sid, currentHostConfig.id, { userProvidedPassword: credentials.password, userProvidedSshKey: credentials.sshKey, userProvidedKeyPassword: credentials.keyPassword, useSocks5: currentHostConfig.useSocks5, socks5Host: currentHostConfig.socks5Host, socks5Port: currentHostConfig.socks5Port, socks5Username: currentHostConfig.socks5Username, socks5Password: currentHostConfig.socks5Password, socks5ProxyChain: currentHostConfig.socks5ProxyChain, }); if (result?.requires_totp) { setTotpRequired(true); setTotpSessionId(sid); setTotpPrompt(result.prompt || t("docker.verificationCodePrompt")); setIsConnecting(false); return; } if (result?.status === "auth_required") { setShowAuthDialog(true); setAuthReason("auth_failed"); setIsConnecting(false); return; } setSessionId(sid); setIsValidating(true); const validation = await validateDockerAvailability(sid); setDockerValidation(validation); setIsValidating(false); if (!validation.available) { toast.error(validation.error || "Docker is not available on this host"); } else { logDockerActivity(); } } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to connect"); setIsConnecting(false); setIsValidating(false); onClose?.(); } finally { setIsConnecting(false); } }; const handleAuthCancel = () => { setShowAuthDialog(false); setIsConnecting(false); onClose?.(); }; const topMarginPx = isTopbarOpen ? 74 : 16; const leftMarginPx = sidebarState === "collapsed" ? 16 : 8; const bottomMarginPx = 8; const wrapperStyle: React.CSSProperties = embedded ? { opacity: isVisible ? 1 : 0, height: "100%", width: "100%" } : { opacity: isVisible ? 1 : 0, marginLeft: leftMarginPx, marginRight: 17, marginTop: topMarginPx, marginBottom: bottomMarginPx, height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`, }; const containerClass = embedded ? "h-full w-full text-foreground overflow-hidden bg-transparent" : "bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"; if (!currentHostConfig?.enableDocker) { return (

{currentHostConfig?.folder} / {title}

{t("docker.notEnabled")}
); } if (isConnecting || isValidating) { return (

{currentHostConfig?.folder} / {title}

); } if (dockerValidation && !dockerValidation.available) { return (

{currentHostConfig?.folder} / {title}

{t("docker.error")}
{dockerValidation.error}
{dockerValidation.code && (
{t("docker.errorCode", { code: dockerValidation.code })}
)}
); } return (

{currentHostConfig?.folder} / {title}

{dockerValidation?.version && (

{t("docker.version", { version: dockerValidation.version })}

)}
{viewMode === "list" ? (
{sessionId ? ( isLoadingContainers && containers.length === 0 ? ( ) : ( { setSelectedContainer(id); setViewMode("detail"); }} selectedContainerId={selectedContainer} onRefresh={refreshContainers} /> ) ) : (

No session available

)}
) : sessionId && selectedContainer && currentHostConfig ? ( ) : (

Select a container to view details

)}
{currentHostConfig && ( )}
); }