import { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { Dialog, DialogContent, DialogTitle, DialogDescription, } from '@/components/ui/dialog'; import { cn } from '@/lib/utils'; import { kubernetesApi } from '@/api'; import { useToast } from '@/hooks/use-toast'; import type { JsonData } from '@/types/common'; import i18n from '@/i18n'; import { type PodLogsViewerProps, type LogLine, RING_BUFFER_MAX, } from './pod-logs-viewer/types'; // types.tsx — 含 highlightKeyword JSX 纯函数 import { LogsHeader } from './pod-logs-viewer/LogsHeader'; import { ControlBar } from './pod-logs-viewer/ControlBar'; import { LogContentArea } from './pod-logs-viewer/LogContentArea'; import { BottomStatusBar } from './pod-logs-viewer/BottomStatusBar'; /** * 增强版 Pod 日志查看器 — 增强 DeploymentDetail 的日志体验 * * 功能:容器选择 / 实时跟踪(fetch stream) / 行数限制 / previous 日志 / * 时间范围 / 关键字过滤(高亮)/ 下载 / 暂停-继续 / 10000 行环形缓冲 */ export function PodLogsViewer({ open, onOpenChange, clusterCode, namespace, podName, containers = [], initialContainer, }: PodLogsViewerProps) { const { toast } = useToast(); const [fullscreen, setFullscreen] = useState(false); const [container, setContainer] = useState(initialContainer || containers[0] || ''); const [tailLines, setTailLines] = useState(500); const [previous, setPrevious] = useState(false); const [sinceTime, setSinceTime] = useState(''); const [keyword, setKeyword] = useState(''); const [autoScroll, setAutoScroll] = useState(true); // 流式状态 const [streaming, setStreaming] = useState(false); const [paused, setPaused] = useState(false); const [loading, setLoading] = useState(false); // 环形缓冲(state 触发渲染,ref 持久存储) const [lines, setLines] = useState([]); const linesRef = useRef([]); const lineIdRef = useRef(0); const abortRef = useRef(null); const pausedRef = useRef(false); const autoScrollRef = useRef(true); const scrollRef = useRef(null); useEffect(() => { pausedRef.current = paused; }, [paused]); useEffect(() => { autoScrollRef.current = autoScroll; }, [autoScroll]); // 重置全屏状态 useEffect(() => { if (!open) setFullscreen(false); }, [open]); // 容器列表变化时同步默认值 useEffect(() => { if (open && !container && containers.length > 0) { setContainer(initialContainer || containers[0]); } }, [open, containers, initialContainer, container]); // 追加行到环形缓冲(批量更新) const appendLines = useCallback((newTexts: string[]) => { if (newTexts.length === 0) return; const next = [...linesRef.current]; for (const t of newTexts) { next.push({ id: ++lineIdRef.current, text: t }); } if (next.length > RING_BUFFER_MAX) { linesRef.current = next.slice(next.length - RING_BUFFER_MAX); } else { linesRef.current = next; } setLines(linesRef.current); }, []); // 自动滚动到底部 useEffect(() => { if (autoScrollRef.current && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [lines]); // 停止流 const stopStream = useCallback(() => { if (abortRef.current) { abortRef.current.abort(); abortRef.current = null; } setStreaming(false); setPaused(false); }, []); // 一次性拉取日志 const fetchLogs = useCallback(async () => { if (!podName) return; stopStream(); setLoading(true); linesRef.current = []; setLines([]); try { const res: JsonData = await kubernetesApi.getPodLogsEnhanced(clusterCode, namespace, podName, { container: container || undefined, tailLines: tailLines, previous, sinceTime: sinceTime || undefined, }); const raw = res?.logs ?? res?.data?.logs ?? (typeof res === 'string' ? res : '') ?? ''; const arr = typeof raw === 'string' ? raw.split('\n') : (Array.isArray(raw) ? raw : String(raw).split('\n')); appendLines(arr); } catch (e) { const msg = e instanceof Error ? e.message : String(e); appendLines([`\x1b[31m[获取日志失败: ${msg}]\x1b[0m`]); toast({ title: i18n.t('k8s.podLogs.loadFailed'), description: msg, variant: 'destructive' }); } finally { setLoading(false); } }, [clusterCode, namespace, podName, container, tailLines, previous, sinceTime, appendLines, stopStream, toast]); // 启动实时流(fetch ReadableStream,tail -f 效果) const startStream = useCallback(async () => { if (!podName) return; stopStream(); linesRef.current = []; setLines([]); setLoading(true); setStreaming(true); setPaused(false); const controller = new AbortController(); abortRef.current = controller; const url = kubernetesApi.buildPodLogsStreamUrl(clusterCode, namespace, podName, { container: container || undefined, tailLines: tailLines, previous, sinceTime: sinceTime || undefined, }); const token = (await import('@/store')).useUserStore.getState().token || ''; try { const resp = await fetch(url, { signal: controller.signal, headers: token ? { Authorization: `Bearer ${token}` } : undefined, }); if (!resp.ok || !resp.body) { throw new Error(`HTTP ${resp.status}`); } setLoading(false); const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; const flush = () => { if (!buffer) return; const parts = buffer.split('\n'); buffer = parts.pop() ?? ''; if (parts.length > 0) appendLines(parts); }; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); if (!pausedRef.current) { flush(); } else { buffer = ''; } } flush(); appendLines([i18n.t('\\x1b[33m— 流式结束 —\\x1b[0m')]); } catch (e) { const errName = (e as { name?: string })?.name; if (errName === 'AbortError') { appendLines([i18n.t('\\x1b[33m— 已停止 —\\x1b[0m')]); } else { const msg = e instanceof Error ? e.message : String(e); appendLines([`\x1b[31m[流式连接中断: ${msg}]\x1b[0m`]); } } finally { setStreaming(false); setPaused(false); setLoading(false); abortRef.current = null; } }, [clusterCode, namespace, podName, container, tailLines, previous, sinceTime, appendLines, stopStream]); // 打开时自动拉取一次 useEffect(() => { if (open && podName) { fetchLogs(); } return () => { stopStream(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, podName]); // 参数变化时若正在流则重启 useEffect(() => { if (!open) return; if (streaming) { startStream(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [container, tailLines, previous, sinceTime]); const handleClose = useCallback(() => { stopStream(); onOpenChange(false); }, [stopStream, onOpenChange]); // 下载当前(过滤后)日志 const handleDownload = useCallback(() => { const content = filteredLines.map((l) => l.text).join('\n'); const filename = `${podName}${container ? '-' + container : ''}-${previous ? 'previous-' : ''}${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '')}.log`; const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); toast({ title: i18n.t('monitor.logPanel.exported'), description: filename }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [podName, container, previous, toast]); // 关键字过滤 const filteredLines = useMemo(() => { if (!keyword.trim()) return lines; const kw = keyword.trim().toLowerCase(); return lines.filter((l) => l.text.toLowerCase().includes(kw)); }, [lines, keyword]); const lineCount = lines.length; const matchedCount = filteredLines.length; return ( { if (!o) handleClose(); }}> e.preventDefault()} className={cn( 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0', '!duration-0 !transition-none', fullscreen ? '!max-w-none !w-screen !h-screen !rounded-none !translate-x-0 !translate-y-0 !left-0 !top-0' : '' )} style={fullscreen ? { maxWidth: '100vw', width: '100vw', height: '100vh' } : { maxWidth: '1100px', width: '94vw' }} > 日志 — {podName} Pod 容器日志查看 setFullscreen(!fullscreen)} onClose={handleClose} /> setPrevious(!previous)} sinceTime={sinceTime} onSinceTimeChange={setSinceTime} keyword={keyword} onKeywordChange={setKeyword} streaming={streaming} paused={paused} loading={loading} filteredLineCount={filteredLines.length} onDownload={handleDownload} onRefresh={fetchLogs} onStartStream={startStream} onStopStream={stopStream} onPauseToggle={() => setPaused(!paused)} /> setAutoScroll(atBottom)} /> { setAutoScroll(!autoScroll); if (!autoScroll && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }} /> ); }