import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectSeparator } from "@/components/ui/select"; import { api } from "@/utils/api"; import { Download as DownloadIcon, Loader2 } from "lucide-react"; import React, { useEffect, useRef } from "react"; import { TerminalLine } from "./terminal-line"; import { type LogLine, getLogType, parseLogs } from "./utils"; import { StatusLogsFilter } from "./status-logs-filter"; interface Props { containerId: string; serverId?: string | null; } type TimeFilter = "all" | "timestamp" | "1h" | "6h" | "24h" | "168h" | "720h"; export const priorities = [ { label: "Info", value: "info", }, { label: "Success", value: "success", }, { label: "Warning", value: "warning", }, { label: "Debug", value: "debug", }, { label: "Error", value: "error", }, ]; export const DockerLogsId: React.FC = ({ containerId, serverId }) => { const { data } = api.docker.getConfig.useQuery( { containerId, serverId: serverId ?? undefined, }, { enabled: !!containerId, } ); const [rawLogs, setRawLogs] = React.useState(""); const [filteredLogs, setFilteredLogs] = React.useState([]); const [autoScroll, setAutoScroll] = React.useState(true); const [lines, setLines] = React.useState(100); const [search, setSearch] = React.useState(""); const [showTimestamp, setShowTimestamp] = React.useState(true); const [since, setSince] = React.useState("all"); const [typeFilter, setTypeFilter] = React.useState([]); const scrollRef = useRef(null); const [isLoading, setIsLoading] = React.useState(false); const scrollToBottom = () => { if (autoScroll && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }; const handleScroll = () => { if (!scrollRef.current) return; const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; setAutoScroll(isAtBottom); }; const handleSearch = (e: React.ChangeEvent) => { setSearch(e.target.value || ""); }; const handleLines = (e: React.ChangeEvent) => { setRawLogs(""); setFilteredLogs([]); setLines(Number(e.target.value) || 1); }; const handleSince = (value: TimeFilter) => { if (value === "timestamp") { setShowTimestamp(!showTimestamp); } else { setRawLogs(""); setFilteredLogs([]); setSince(value); } }; useEffect(() => { if (!containerId) return; let isCurrentConnection = true; let noDataTimeout: NodeJS.Timeout; setIsLoading(true); setRawLogs(""); setFilteredLogs([]); const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const params = new globalThis.URLSearchParams({ containerId, tail: lines.toString(), since, search, }); if (serverId) { params.append("serverId", serverId); } const wsUrl = `${protocol}//${ window.location.host }/docker-container-logs?${params.toString()}`; console.log("Connecting to WebSocket:", wsUrl); const ws = new WebSocket(wsUrl); const resetNoDataTimeout = () => { if (noDataTimeout) clearTimeout(noDataTimeout); noDataTimeout = setTimeout(() => { if (isCurrentConnection) { setIsLoading(false); } }, 2000); // Wait 2 seconds for data before showing "No logs found" }; ws.onopen = () => { if (!isCurrentConnection) { ws.close(); return; } console.log("WebSocket connected"); resetNoDataTimeout(); }; ws.onmessage = (e) => { if (!isCurrentConnection) return; setRawLogs((prev) => prev + e.data); setIsLoading(false); if (noDataTimeout) clearTimeout(noDataTimeout); }; ws.onerror = (error) => { if (!isCurrentConnection) return; console.error("WebSocket error:", error); setIsLoading(false); if (noDataTimeout) clearTimeout(noDataTimeout); }; ws.onclose = (e) => { if (!isCurrentConnection) return; console.log("WebSocket closed:", e.reason); setIsLoading(false); if (noDataTimeout) clearTimeout(noDataTimeout); }; return () => { isCurrentConnection = false; if (noDataTimeout) clearTimeout(noDataTimeout); if (ws.readyState === WebSocket.OPEN) { ws.close(); } }; }, [containerId, serverId, lines, search, since]); const handleDownload = () => { const logContent = filteredLogs .map( ({ timestamp, message }: { timestamp: Date | null; message: string }) => `${timestamp?.toISOString() || "No timestamp"} ${message}` ) .join("\n"); const blob = new Blob([logContent], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); const appName = data.Name.replace("/", "") || "app"; const isoDate = new Date().toISOString(); a.href = url; a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate .slice(11, 19) .replace(/:/g, "")}.log.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const handleFilter = (logs: LogLine[]) => { return logs.filter((log) => { const logType = getLogType(log.message).type; if (typeFilter.length === 0) { return true; } return typeFilter.includes(logType); }); }; useEffect(() => { setRawLogs(""); setFilteredLogs([]); }, [containerId]); useEffect(() => { const logs = parseLogs(rawLogs); const filtered = handleFilter(logs); setFilteredLogs(filtered); }, [rawLogs, search, lines, since, typeFilter]); useEffect(() => { scrollToBottom(); if (autoScroll && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [filteredLogs, autoScroll]); return (
{filteredLogs.length > 0 ? ( filteredLogs.map((filteredLog: LogLine, index: number) => ( )) ) : isLoading ? (
) : (
No logs found
)}
); };