import { useEffect, useRef, useState } from "react"; import { useOptionalConnectionLog } from "@/ssh/connection-log/ConnectionLogContext.tsx"; import { useTranslation } from "react-i18next"; import { copyToClipboard } from "@/lib/clipboard.ts"; import { Button } from "@/components/button.tsx"; import { cn } from "@/lib/utils.ts"; import { Check, ChevronDown, ChevronUp, Copy } from "lucide-react"; import { toast } from "sonner"; interface ConnectionLogPanelProps { isConnecting: boolean; isConnected: boolean; hasConnectionError: boolean; /** Kept for older callers. The log always sits at the bottom. */ position?: "top" | "bottom"; className?: string; } const COLLAPSED_HEIGHT = "h-[136px]"; const EXPANDED_HEIGHT = "h-[46%] min-h-[220px]"; /** A dot per line, so the log reads as one column of text. */ const DOT: Record = { info: "bg-muted-foreground/50", success: "bg-accent-brand", warning: "bg-warning", error: "bg-destructive", }; const TEXT: Record = { info: "text-foreground-secondary", success: "text-foreground", warning: "text-warning", error: "text-destructive", }; export function ConnectionLogPanel({ isConnecting, isConnected, hasConnectionError, className, }: ConnectionLogPanelProps) { const { t } = useTranslation(); const connectionLog = useOptionalConnectionLog(); const { logs, clearLogs, isExpanded, toggleExpanded, setIsExpanded } = connectionLog ?? {}; const lastLogRef = useRef(null); const [manuallyCollapsed, setManuallyCollapsed] = useState(false); const [copied, setCopied] = useState(false); useEffect(() => { if (hasConnectionError && setIsExpanded) { setManuallyCollapsed(false); setIsExpanded(true); } }, [hasConnectionError, setIsExpanded]); useEffect(() => { if (isConnected && !hasConnectionError && !isConnecting && clearLogs) { clearLogs(); setManuallyCollapsed(false); } }, [isConnected, hasConnectionError, isConnecting, clearLogs]); useEffect(() => { if (lastLogRef.current) { lastLogRef.current.scrollIntoView?.({ block: "end" }); } }, [logs]); const shouldShow = !!connectionLog && !isConnected && (isConnecting || hasConnectionError || logs.length > 0); if (!shouldShow) { return null; } const expanded = isExpanded && !manuallyCollapsed; const handleToggle = () => { if (hasConnectionError) { setManuallyCollapsed((prev) => !prev); return; } toggleExpanded(); }; const copyLogsToClipboard = async () => { const logsText = logs .map((log) => { const time = log.timestamp.toLocaleTimeString(); return `[${time}] [${log.type.toUpperCase()}] ${log.message}`; }) .join("\n"); const ok = await copyToClipboard(logsText); if (ok) { setCopied(true); setTimeout(() => setCopied(false), 1400); toast.success(t("sshAuth.connectionLogCopied")); } else { toast.error(t("sshAuth.connectionLogCopyFailed")); } }; return (
{logs.length} {logs.length > 0 && ( )}
{logs.length === 0 ? (

{isConnecting ? t("sshAuth.connectionLogWaiting") : t("sshAuth.connectionLogEmpty")}

) : (
{logs.map((log, index) => (
{log.timestamp.toLocaleTimeString([], { hour12: false })} {log.message}
))}
)}
); }