import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ScrollText } from "lucide-react"; import { useTranslation } from "react-i18next"; import { managerGet, managerGetSub } from "@/main-axios"; import { extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { useAdaptivePolling } from "@/hooks/use-adaptive-polling"; import { Select2 } from "@/components/select2"; interface LogFiles { common: string[]; files: string[]; } type Mode = "file" | "unit"; export function LogViewerCard({ hostId }: { hostId: number | null }) { const { t } = useTranslation(); const [mode, setMode] = useState("file"); const [path, setPath] = useState("/var/log/syslog"); const [unit, setUnit] = useState(""); const [customPath, setCustomPath] = useState(""); const [lines, setLines] = useState(300); const [content, setContent] = useState(""); const [grep, setGrep] = useState(""); const [follow, setFollow] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [files, setFiles] = useState([]); const bodyRef = useRef(null); const contentRef = useRef(content); contentRef.current = content; // Load the host's actual log files once. useEffect(() => { if (hostId == null) return; managerGetSub(hostId, "logs", "files") .then((res) => { const merged = Array.from( new Set([...(res.common ?? []), ...(res.files ?? [])]), ); setFiles(merged); if (merged.length && !merged.includes(path)) setPath(merged[0]); }) .catch(() => {}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [hostId]); const fetchLog = useCallback(async () => { if (hostId == null) return false; const params: Record = { lines }; if (mode === "unit") { if (!unit.trim()) return false; params.unit = unit.trim(); } else { const p = customPath.trim() || path; if (!p) return false; params.path = p; } setLoading(true); setError(null); try { const res = await managerGet<{ content: string }>(hostId, "logs", params); const next = res.content || ""; const changed = next !== contentRef.current; if (changed) { contentRef.current = next; setContent(next); requestAnimationFrame(() => { if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight; }); } return changed; } catch (e) { setError(extractError(e).message); } finally { setLoading(false); } }, [customPath, hostId, lines, mode, path, unit]); useEffect(() => { void fetchLog(); // Custom paths are applied on blur/manual refresh, not on every keystroke. // eslint-disable-next-line react-hooks/exhaustive-deps }, [hostId, mode, path, unit, lines]); useAdaptivePolling( fetchLog, { minIntervalMs: 3_000, maxIntervalMs: 18_000, stablePollsPerStep: 2, maxRequestDutyCycle: 0.2, }, follow, { runImmediately: false }, ); const shown = useMemo(() => { if (!grep) return content; const needle = grep.toLowerCase(); return content .split("\n") .filter((l) => l.toLowerCase().includes(needle)) .join("\n"); }, [content, grep]); return ( } loading={loading} error={error ? { message: error } : null} onRefresh={fetchLog} headerExtra={ } >
setMode(e.target.value as Mode)} className="h-7 border border-border bg-background px-1 text-[11px]" > {mode === "file" ? ( { setCustomPath(""); setPath(e.target.value); }} className="h-7 flex-1 border border-border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-ring" > {files.map((f) => ( ))} ) : ( setUnit(e.target.value)} onBlur={fetchLog} placeholder="nginx.service" className="h-7 flex-1 border border-border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-ring" /> )} setLines(Number(e.target.value))} className="h-7 border border-border bg-background px-1 text-[11px]" > {[100, 300, 500, 1000, 2000].map((n) => ( ))}
{mode === "file" && ( setCustomPath(e.target.value)} onBlur={() => customPath && fetchLog()} placeholder={t("hostMetrics.managers.logCustomPath")} className="mb-2 h-7 w-full border border-border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-ring" /> )} setGrep(e.target.value)} placeholder={t("hostMetrics.managers.logGrep")} className="mb-2 h-7 w-full border border-border bg-background px-2 text-[11px] outline-none focus:ring-1 focus:ring-ring" />
        {shown || t("hostMetrics.managers.noLogData")}
      
); }