import React, { useCallback, useEffect, useRef, useState } from "react"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { Download, Search, Square, Trash2, Play } from "lucide-react"; import { Button, Input } from "@/components/ui"; import { streamPodLogsCmd, stopLogStreamCmd } from "@/lib/tauriCommands"; export interface LogsTabData { clusterId: string; namespace: string; podName: string; containers: string[]; } interface LogsTabProps { data: LogsTabData; } const MAX_LINES = 5000; /** * In-dock pod log viewer. Mirrors the structure of LogStreamPanel but renders * inline (no Dialog) and at the dock's available height. */ export function LogsTab({ data }: LogsTabProps) { const { clusterId, namespace, podName, containers } = data; const [selectedContainer, setSelectedContainer] = useState( containers[0] ?? "" ); const [follow, setFollow] = useState(true); const [timestamps, setTimestamps] = useState(false); const [tailLines, setTailLines] = useState(100); const [lines, setLines] = useState([]); const [streaming, setStreaming] = useState(false); const [search, setSearch] = useState(""); const [error, setError] = useState(null); const streamIdRef = useRef(null); const unlistenRef = useRef(null); const bottomRef = useRef(null); const stopStream = useCallback(async () => { if (unlistenRef.current) { unlistenRef.current(); unlistenRef.current = null; } if (streamIdRef.current) { try { await stopLogStreamCmd(streamIdRef.current); } catch { // best-effort } streamIdRef.current = null; } setStreaming(false); }, []); useEffect(() => { return () => { void stopStream(); }; }, [stopStream]); useEffect(() => { if (follow && streaming && bottomRef.current) { bottomRef.current.scrollIntoView({ behavior: "smooth" }); } }, [lines, follow, streaming]); const startStream = async () => { if (streaming) return; setError(null); setLines([]); try { const streamId = await streamPodLogsCmd({ cluster_id: clusterId, namespace, pod_name: podName, container_name: selectedContainer, follow, timestamps, tail_lines: tailLines, }); streamIdRef.current = streamId; const unlisten = await listen<{ stream_id: string; line: string }>( "pod-log-line", (event) => { if (event.payload.stream_id !== streamId) return; setLines((prev) => { const next = [...prev, event.payload.line]; return next.length > MAX_LINES ? next.slice(next.length - MAX_LINES) : next; }); } ); unlistenRef.current = unlisten; setStreaming(true); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }; const handleDownload = () => { const content = lines.join("\n"); const blob = new Blob([content], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${podName}-${selectedContainer}-logs.txt`; a.click(); URL.revokeObjectURL(url); }; const handleClear = () => setLines([]); const filteredLines = search.trim() === "" ? lines : lines.filter((l) => l.includes(search)); return (
Tail: setTailLines(Math.min(10000, Math.max(10, Number(e.target.value)))) } className="h-8 w-20 rounded-md border border-input bg-background px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50" />
{!streaming ? ( ) : ( )}
setSearch(e.target.value)} className="pl-7 h-8 text-xs" />
{error && (
{error}
)}
{filteredLines.length === 0 ? ( {streaming ? "Waiting for log data..." : "No logs to display. Press Stream to begin."} ) : ( <> {filteredLines.map((line, i) => (
{line}
))}
)}
{lines.length.toLocaleString()} line{lines.length !== 1 ? "s" : ""} {search.trim() !== "" && ` — ${filteredLines.length.toLocaleString()} matching`}
); }