import React from "react"; import { Button } from "@/components/button.tsx"; import { Input } from "@/components/input.tsx"; import { Separator } from "@/components/separator.tsx"; import { Download, RefreshCw, Search, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import type { DockerLogOptions } from "@/types"; import { getContainerLogs, downloadContainerLogs } from "@/main-axios.ts"; interface LogViewerProps { sessionId: string; containerId: string; containerName: string; } function AdminToggle({ on, onToggle, label, }: { on: boolean; onToggle: () => void; label: string; }) { return (
{label}
); } function getLogColor(line: string): string { if (line.includes(" WARN") || line.includes(" warn")) return "text-yellow-400/90"; if (line.includes(" ERROR") || line.includes(" error")) return "text-destructive"; if (line.includes(" DEBUG") || line.includes(" debug")) return "text-muted-foreground/60"; return "text-foreground/90"; } export function LogViewer({ sessionId, containerId, containerName, }: LogViewerProps): React.ReactElement { const { t } = useTranslation(); const [rawLogs, setRawLogs] = React.useState([]); const [isLoading, setIsLoading] = React.useState(false); const [isDownloading, setIsDownloading] = React.useState(false); const [tailLines, setTailLines] = React.useState("100"); const [showTimestamps, setShowTimestamps] = React.useState(false); const [autoRefresh, setAutoRefresh] = React.useState(false); const [logSearch, setLogSearch] = React.useState(""); const logsEndRef = React.useRef(null); const fetchLogs = React.useCallback(async () => { setIsLoading(true); try { const options: DockerLogOptions = { tail: tailLines === "all" ? undefined : parseInt(tailLines, 10), timestamps: showTimestamps, }; const data = await getContainerLogs(sessionId, containerId, options); setRawLogs(data.logs.split("\n").filter(Boolean)); } catch (error) { toast.error( `Failed to fetch logs: ${error instanceof Error ? error.message : "Unknown error"}`, ); } finally { setIsLoading(false); } }, [sessionId, containerId, tailLines, showTimestamps]); React.useEffect(() => { fetchLogs(); }, [fetchLogs]); React.useEffect(() => { if (!autoRefresh) return; const interval = setInterval(fetchLogs, 3000); return () => clearInterval(interval); }, [autoRefresh, fetchLogs]); React.useEffect(() => { if (autoRefresh && logsEndRef.current) { logsEndRef.current.scrollIntoView({ behavior: "smooth" }); } }, [rawLogs, autoRefresh]); const handleDownload = async () => { setIsDownloading(true); try { const blob = await downloadContainerLogs(sessionId, containerId, { timestamps: showTimestamps, }); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${containerName.replace(/[^a-z0-9]/gi, "_")}_logs.txt`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); toast.success(t("docker.logsDownloaded")); } catch (error) { toast.error( `Failed to download logs: ${error instanceof Error ? error.message : "Unknown error"}`, ); } finally { setIsDownloading(false); } }; const filteredLogs = logSearch ? rawLogs.filter((l) => l.toLowerCase().includes(logSearch.toLowerCase())) : rawLogs; return (
setAutoRefresh(!autoRefresh)} label={t("docker.autoRefresh")} /> setShowTimestamps(!showTimestamps)} label={t("docker.timestamps")} /> {filteredLogs.length}/{rawLogs.length} {t("docker.lines")}
setLogSearch(e.target.value)} className="pl-7 h-7 text-xs bg-background border-border rounded-none" />
{filteredLogs.length > 0 ? ( filteredLogs.map((line, i) => { const tsEnd = line.indexOf(" ", 1); const maybeTs = tsEnd > 10 ? line.substring(0, tsEnd) : null; const rest = maybeTs ? line.substring(tsEnd) : line; return (
{maybeTs && ( {maybeTs} )} {rest}
); }) ) : ( {logSearch ? t("docker.noLogsMatching", { query: logSearch }) : t("docker.noLogsAvailable")} )}
); }