From 03a84049fb123d42119130badd654669820c80e7 Mon Sep 17 00:00:00 2001 From: attacker Date: Thu, 9 Jul 2026 03:37:35 +0800 Subject: [PATCH] =?UTF-8?q?refactor(quality):=20Q26=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=B7=A8=E5=9E=8B=E7=BB=84=E4=BB=B6=E6=8B=86=E5=88=86=20?= =?UTF-8?q?=E2=80=94=20PodLogsViewer=20553L=E2=86=92331L=20(-40%),=20?= =?UTF-8?q?=E6=8F=90=E5=8F=96=20pod-logs-viewer/=205=20=E4=B8=AA=E6=96=87?= =?UTF-8?q?=E4=BB=B6=20(types.tsx(highlightKeyword=20=E7=BA=AF=E5=87=BD?= =?UTF-8?q?=E6=95=B0)=20+=20LogsHeader(66L)=20+=20ControlBar(178L)=20+=20L?= =?UTF-8?q?ogContentArea(64L)=20+=20BottomStatusBar(42L)),=20renderLine=20?= =?UTF-8?q?=E5=86=85=E8=81=94=E5=87=BD=E6=95=B0=E6=8F=90=E5=8F=96=E4=B8=BA?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E7=BA=A7=20highlightKeyword=20=E7=BA=AF?= =?UTF-8?q?=E5=87=BD=E6=95=B0,=20catch=20=E7=B1=BB=E5=9E=8B=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E5=8D=87=E7=BA=A7=20(e=3F.message=E2=86=92e=20instanc?= =?UTF-8?q?eof=20Error),=20=E9=9B=B6=E9=80=BB=E8=BE=91=E5=8F=98=E6=9B=B4,?= =?UTF-8?q?=20ESLint=20errors=3D0/warnings=3D65/any=3D0,=20tsc=20-b=3D0,?= =?UTF-8?q?=20npm=20build=E2=9C=93=20(17.34s),=2017/17=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_EVALUATION.md | 2 +- ROADMAP.md | 39 ++ TDD_PLAN.md | 17 + .../components/kubernetes/PodLogsViewer.tsx | 378 ++++-------------- .../pod-logs-viewer/BottomStatusBar.tsx | 42 ++ .../kubernetes/pod-logs-viewer/ControlBar.tsx | 178 +++++++++ .../pod-logs-viewer/LogContentArea.tsx | 64 +++ .../kubernetes/pod-logs-viewer/LogsHeader.tsx | 66 +++ .../kubernetes/pod-logs-viewer/types.tsx | 57 +++ 9 files changed, 542 insertions(+), 301 deletions(-) create mode 100644 frontend/src/components/kubernetes/pod-logs-viewer/BottomStatusBar.tsx create mode 100644 frontend/src/components/kubernetes/pod-logs-viewer/ControlBar.tsx create mode 100644 frontend/src/components/kubernetes/pod-logs-viewer/LogContentArea.tsx create mode 100644 frontend/src/components/kubernetes/pod-logs-viewer/LogsHeader.tsx create mode 100644 frontend/src/components/kubernetes/pod-logs-viewer/types.tsx diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md index 1a22afa..96bb0e8 100644 --- a/PROJECT_EVALUATION.md +++ b/PROJECT_EVALUATION.md @@ -129,7 +129,7 @@ | ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done | | 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) | | ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done | -| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q22 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + **TopologyView 596L→472L (-21%,提取 TopologyCanvas/NodeDetailDialog/EditNodeDialog,移除文件级 eslint-disable + any→ClusterInfo 具体接口)** + **DeploymentEditAdvanced 590L→289L (-51%,提取 types(parser+builder 纯函数) + UpdateStrategyCard/TolerationsCard/PodMiscCard/PodSecurityCard/HostSettingsCard 5 个 Card 组件)** + **WorkloadViewPage 572L→348L (-39%,提取 types(parseWorkloadStats 纯函数) + shared-components(StatCard/LabelBadges/ContainerInfo) + UpdateStrategySelectorCards + PodInstancesTable + ScaleDialog)**,累计提取 78 个独立组件文件** | +| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q26 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + **TopologyView 596L→472L (-21%,提取 TopologyCanvas/NodeDetailDialog/EditNodeDialog,移除文件级 eslint-disable + any→ClusterInfo 具体接口)** + **DeploymentEditAdvanced 590L→289L (-51%,提取 types(parser+builder 纯函数) + UpdateStrategyCard/TolerationsCard/PodMiscCard/PodSecurityCard/HostSettingsCard 5 个 Card 组件)** + **WorkloadViewPage 572L→348L (-39%,提取 types(parseWorkloadStats 纯函数) + shared-components(StatCard/LabelBadges/ContainerInfo) + UpdateStrategySelectorCards + PodInstancesTable + ScaleDialog)** + **CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L** + **PodLogsViewer 553L→331L (-40%,提取 types(highlightKeyword 纯函数) + LogsHeader + ControlBar + LogContentArea + BottomStatusBar)**,累计提取 83 个独立组件文件** | ### 3.2 🟡 中优先级 diff --git a/ROADMAP.md b/ROADMAP.md index d056b83..e0b2f58 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -497,6 +497,45 @@ services/api.ts (1324L) | Q23 | 前端巨型组件拆分(CloudAccountManager 572L→308L,提取 cloud-account/types(AccountFormData/AccountStats + 6 个纯函数) + AccountCard(178L) + AccountFormDialog(162L)) | P0 | **done** | | Q24 | 前端巨型组件拆分(ExcelServerImport 560L→385L,提取 excel-import/types(6 个 CSV 纯函数) + StepIndicator + PreviewTable) | P0 | **done** | | Q25 | 前端巨型组件拆分(PipelineCreateDialog 559L→498L,提取 pipeline-create/types(PRESETS/DEFAULT_TARGET) + AdvancedSettingsPanel(172L 受控展开面板)) | P0 | **done** | +| Q26 | 前端巨型组件拆分(PodLogsViewer 553L→331L,提取 pod-logs-viewer/types(highlightKeyword 纯函数) + LogsHeader + ControlBar + LogContentArea + BottomStatusBar) | P0 | **done** | + +--- + +## Q26: 前端巨型组件拆分 — PodLogsViewer [done] + +**问题**: `PodLogsViewer.tsx`(553 行)是 Q25 拆分后项目最大的 K8s 业务组件之一,包含 Pod 日志查看器的全部逻辑:~25 个 useState/useRef 状态管理、fetch ReadableStream 实时流式日志(tail -f 效果)、10000 行环形缓冲、一次性日志拉取、关键字过滤+高亮渲染、多容器选择、行数限制、previous 日志、时间范围筛选、下载导出、暂停/继续、全屏切换、以及 4 个内联 JSX 区块(头部工具栏/控制栏/日志内容区/底部状态条)。单文件过大导致维护困难。 + +**已完成 (2026-07-09)**: + +将类型/常量/纯函数、头部工具栏、控制栏、日志内容区、底部状态条提取为 `pod-logs-viewer/` 子目录下的独立文件: + +| 新文件 | 行数 | 职责 | +|--------|------|------| +| `types.tsx` | 57 | PodLogsViewerProps/LogLine 接口 + RING_BUFFER_MAX/TAIL_OPTIONS 常量 + `highlightKeyword(text, keyword)` 关键字高亮纯函数(返回 ReactNode,用 `` 包裹匹配片段) | +| `LogsHeader.tsx` | 66 | 头部工具栏(状态指示灯 streaming/paused/idle + ScrollText 图标 + Pod 名称 + 行数统计 + 全屏/关闭按钮),接收 11 个 props 的受控展示组件 | +| `ControlBar.tsx` | 178 | 控制栏(容器选择 + 行数限制 + previous 切换 + sinceTime 时间 + 关键字过滤 + 下载 + 刷新 + 实时流/暂停/停止按钮),接收 23 个 props 的受控展示组件 | +| `LogContentArea.tsx` | 64 | 日志内容可滚动区(空状态 + 加载状态 + 关键字高亮行渲染 + onScroll 自动滚动检测),接收 9 个 props 的受控展示组件 | +| `BottomStatusBar.tsx` | 42 | 底部状态条(缓冲计数 + 匹配数 + 容器/previous 标签 + 自动滚动开关),接收 8 个 props 的受控展示组件 | + +主文件 `PodLogsViewer.tsx` 从 **553 行降至 331 行**(-40%),仅保留:状态管理(fullscreen/container/tailLines/previous/sinceTime/keyword/autoScroll/streaming/paused/loading + lines + 7 个 ref)、refs 同步 effects、环形缓冲 `appendLines`、`stopStream`/`fetchLogs`/`startStream` 回调、自动拉取/参数变化 effects、`handleClose`/`handleDownload` 回调、`filteredLines` useMemo、以及 Dialog 外壳 + 5 个子组件编排。 + +**关键改进**: +- **纯函数提取**: 原 `renderLine(line)` 内联函数(25 行关键字高亮 JSX 拆分逻辑)提取为 `highlightKeyword(text, keyword)` 模块级纯函数,消除每次 render 重建闭包。 +- **类型安全**: catch 块从 `e?.message` 改为 `e instanceof Error ? e.message : String(e)` 类型安全访问;`e?.name === 'AbortError'` 改为 `(e as { name?: string })?.name === 'AbortError'`。 +- **受控组件架构**: 5 个子组件全部为受控展示组件(接收状态 + setter/回调 props),无独立状态。所有状态管理和流式逻辑保留在主组件,拆分为纯机械操作(移动 JSX + 添加 props 透传),零逻辑变更。 + +**验收标准**: +- [x] PodLogsViewer.tsx 从 553 行降至 **331 行**(-40%) +- [x] 5 个提取的文件全部 < 180 行(最大 ControlBar 178 行) +- [x] `tsc -b` 零错误 +- [x] `npm run build` 零错误(✓ built in 17.34s) +- [x] ESLint errors 保持 **0**(PodLogsViewer + pod-logs-viewer/ 目录零 errors/warnings) +- [x] ESLint warnings 保持 **65**(无新增) +- [x] 全局 ESLint any 保持 **0** +- [x] `go build` 零错误 +- [x] 17/17 集成回归测试通过(零回归) +- [x] Docker 部署 healthy,前端页面正常加载(HTTP 200) +- [x] Pod 日志查看器功能不变(容器选择/行数限制/previous/sinceTime/关键字过滤+高亮/下载/刷新/实时流/暂停继续/全屏切换全部正常) --- diff --git a/TDD_PLAN.md b/TDD_PLAN.md index 140e765..8914461 100644 --- a/TDD_PLAN.md +++ b/TDD_PLAN.md @@ -1485,3 +1485,20 @@ curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer ${TOKEN}" "http - [x] 链路追踪管理页功能不变(Traces/Services/Metrics 三 Tab + Trace 详情 Span 树 + Span 详情标签/日志全部正常) - [x] SpanTreeNode 内联定义反模式已修复(提取为模块级稳定组件引用) +--- + +## Q26: 前端巨型组件拆分 — PodLogsViewer [done] + +### 通过标准 +- [x] PodLogsViewer.tsx 从 553 行降至 **331 行**(-40%) +- [x] 5 个提取的文件全部 < 180 行(types.tsx 57L, LogsHeader 66L, ControlBar 178L, LogContentArea 64L, BottomStatusBar 42L) +- [x] `tsc -b` 零错误 +- [x] `npm run build` 零错误(✓ built in 17.34s) +- [x] ESLint errors 保持 **0**(PodLogsViewer + pod-logs-viewer/ 目录零 errors/warnings) +- [x] ESLint warnings 保持 **65**(无新增) +- [x] 全局 ESLint any 保持 **0** +- [x] `go build` 零错误 +- [x] 17/17 集成回归测试通过(零回归) +- [x] Docker 部署 healthy,前端页面正常加载(HTTP 200) +- [x] Pod 日志查看器功能不变(容器选择/行数限制/previous/sinceTime/关键字过滤+高亮/下载/刷新/实时流/暂停继续/全屏切换全部正常) + diff --git a/frontend/src/components/kubernetes/PodLogsViewer.tsx b/frontend/src/components/kubernetes/PodLogsViewer.tsx index 73ea447..c70c0cf 100644 --- a/frontend/src/components/kubernetes/PodLogsViewer.tsx +++ b/frontend/src/components/kubernetes/PodLogsViewer.tsx @@ -5,67 +5,27 @@ import { DialogTitle, DialogDescription, } from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { - ScrollText, Maximize2, Minimize2, X, Play, Pause, Download, Search, - RefreshCw, Loader2, Box, History, Clock, Filter, AlertCircle, -} from 'lucide-react'; 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 的日志体验 * - * 功能: - * - 容器选择下拉(多容器 Pod) - * - 实时跟踪开关(tail -f 效果,用 fetch stream) - * - 行数限制选择(100 / 500 / 1000 / 全部) - * - previous 容器日志(上一次崩溃的日志) - * - 时间范围筛选(sinceTime) - * - 关键字过滤(前端过滤,高亮) - * - 下载按钮(导出当前日志为文件) - * - 暂停 / 继续(流式时) - * - 10000 行环形缓冲(借鉴 opsx-agent) - * - * @author Logan + * 功能:容器选择 / 实时跟踪(fetch stream) / 行数限制 / previous 日志 / + * 时间范围 / 关键字过滤(高亮)/ 下载 / 暂停-继续 / 10000 行环形缓冲 */ -interface PodLogsViewerProps { - open: boolean; - onOpenChange: (open: boolean) => void; - clusterCode: string; - namespace: string; - podName: string; - /** 可选:Pod 容器名列表 */ - containers?: string[]; - initialContainer?: string; -} - -const TAIL_OPTIONS = [ - { value: '100', label: i18n.t('kubernetes.podlogsviewerts.s58b02a7a') }, - { value: '500', label: i18n.t('kubernetes.podlogsviewerts.s9a036801') }, - { value: '1000', label: i18n.t('kubernetes.podlogsviewerts.sf89a330c') }, - { value: '0', label: i18n.t('system.auditAll') }, -]; - -// 环形缓冲大小(借鉴 opsx-agent) -const RING_BUFFER_MAX = 10000; - -interface LogLine { - id: number; - text: string; -} - export function PodLogsViewer({ open, onOpenChange, @@ -90,7 +50,7 @@ export function PodLogsViewer({ const [paused, setPaused] = useState(false); const [loading, setLoading] = useState(false); - // 环形缓冲(使用 state 触发渲染,ref 持久存储) + // 环形缓冲(state 触发渲染,ref 持久存储) const [lines, setLines] = useState([]); const linesRef = useRef([]); const lineIdRef = useRef(0); @@ -121,7 +81,6 @@ export function PodLogsViewer({ 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 { @@ -165,8 +124,9 @@ export function PodLogsViewer({ const arr = typeof raw === 'string' ? raw.split('\n') : (Array.isArray(raw) ? raw : String(raw).split('\n')); appendLines(arr); } catch (e) { - appendLines([`\x1b[31m[获取日志失败: ${e?.message || String(e)}]\x1b[0m`]); - toast({ title: i18n.t('k8s.podLogs.loadFailed'), description: e?.message || String(e), variant: 'destructive' }); + 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); } @@ -207,7 +167,6 @@ export function PodLogsViewer({ const flush = () => { if (!buffer) return; - // 按行切分,保留最后未结束的行 const parts = buffer.split('\n'); buffer = parts.pop() ?? ''; if (parts.length > 0) appendLines(parts); @@ -216,22 +175,22 @@ export function PodLogsViewer({ while (true) { const { done, value } = await reader.read(); if (done) break; - // 暂停时累积但不写入缓冲(丢弃,保持实时性);这里改为暂停时不 append buffer += decoder.decode(value, { stream: true }); if (!pausedRef.current) { flush(); } else { - // 暂停期间丢弃缓冲内容,避免堆积 buffer = ''; } } - flush(); // 收尾 + flush(); appendLines([i18n.t('kubernetes.podlogsviewerts.s4f0e17cd')]); } catch (e) { - if (e?.name === 'AbortError') { + const errName = (e as { name?: string })?.name; + if (errName === 'AbortError') { appendLines([i18n.t('kubernetes.podlogsviewerts.s1ca9010d')]); } else { - appendLines([`\x1b[31m[流式连接中断: ${e?.message || String(e)}]\x1b[0m`]); + const msg = e instanceof Error ? e.message : String(e); + appendLines([`\x1b[31m[流式连接中断: ${msg}]\x1b[0m`]); } } finally { setStreaming(false); @@ -250,17 +209,15 @@ export function PodLogsViewer({ // 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); @@ -287,33 +244,6 @@ export function PodLogsViewer({ return lines.filter((l) => l.text.toLowerCase().includes(kw)); }, [lines, keyword]); - // 高亮关键字渲染 - const renderLine = (line: LogLine) => { - const text = line.text; - if (!keyword.trim()) return {text}; - const kw = keyword.trim(); - const lower = text.toLowerCase(); - const kwLower = kw.toLowerCase(); - const parts: React.ReactNode[] = []; - let idx = 0; - let key = 0; - while (idx < text.length) { - const found = lower.indexOf(kwLower, idx); - if (found === -1) { - parts.push({text.slice(idx)}); - break; - } - if (found > idx) parts.push({text.slice(idx, found)}); - parts.push( - - {text.slice(found, found + kw.length)} - - ); - idx = found + kw.length; - } - return <>{parts}; - }; - const lineCount = lines.length; const matchedCount = filteredLines.length; @@ -334,219 +264,67 @@ export function PodLogsViewer({ 日志 — {podName} Pod 容器日志查看 - {/* 头部工具栏 */} -
-
-
- - 日志 — {podName} - {loading && } - - {streaming ? (paused ? i18n.t('kubernetes.deployment.paused') : i18n.t('kubernetes.podlogsviewerts.sd9c71f71')) : i18n.t('agent.agentServiceStopped')} · {matchedCount}{keyword ? `/${lineCount}` : ''} 行 - {lineCount >= RING_BUFFER_MAX && i18n.t('kubernetes.podlogsviewerts.sf3280b73')} - -
+ 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)} + /> - {/* 控制栏 */} -
- {/* 容器 */} - {containers.length > 0 && ( - - )} + setAutoScroll(atBottom)} + /> - {/* 行数 */} - - - {/* previous */} - - - {/* sinceTime */} -
- - { - const v = e.target.value; - setSinceTime(v ? new Date(v).toISOString() : ''); - }} - className="h-8 w-[170px] text-xs bg-[var(--dg-bg-hover)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" - title={i18n.t('kubernetes.podlogsviewerts.s127892fd')} - /> -
- -
- - {/* 关键字过滤 */} -
- - setKeyword(e.target.value)} - placeholder={i18n.t('kubernetes.podlogsviewerts.s7558f175')} - className="h-8 w-[160px] pl-7 text-xs bg-[var(--dg-bg-hover)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" - /> -
- - {/* 下载 */} - - - {/* 刷新(一次性) */} - - - {/* 实时跟踪 / 暂停 / 继续 */} - {!streaming ? ( - - ) : ( - <> - - - - )} -
- - {/* 日志内容 */} -
{ - const el = e.currentTarget; - // 用户主动向上滚时关闭自动跟随 - const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40; - if (!atBottom && autoScrollRef.current) setAutoScroll(false); - if (atBottom && !autoScrollRef.current) setAutoScroll(true); + { + setAutoScroll(!autoScroll); + if (!autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } }} - className={cn( - 'overflow-auto bg-[#0f172a] font-mono text-xs leading-relaxed p-3', - 'text-[var(--dg-text-secondary)]' - )} - style={{ height: fullscreen ? 'calc(100vh - 98px)' : '520px' }} - > - {lineCount === 0 && !loading && ( -
- - {i18n.t('pipeline.noLog')} -
- )} - {loading && lineCount === 0 && ( -
- 加载中… -
- )} - {filteredLines.map((line) => ( -
- {renderLine(line)} -
- ))} -
- - {/* 底部状态条 */} -
-
- 缓冲 {lineCount}/{RING_BUFFER_MAX} - {keyword && 匹配 {matchedCount}} - {container && {container}} - {previous && previous} -
- -
+ /> ); diff --git a/frontend/src/components/kubernetes/pod-logs-viewer/BottomStatusBar.tsx b/frontend/src/components/kubernetes/pod-logs-viewer/BottomStatusBar.tsx new file mode 100644 index 0000000..c525350 --- /dev/null +++ b/frontend/src/components/kubernetes/pod-logs-viewer/BottomStatusBar.tsx @@ -0,0 +1,42 @@ +import { Filter, Box, History, Maximize2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import i18n from '@/i18n'; +import { RING_BUFFER_MAX } from './types'; + +interface BottomStatusBarProps { + lineCount: number; + matchedCount: number; + keyword: string; + container: string; + previous: boolean; + autoScroll: boolean; + onToggleAutoScroll: () => void; +} + +/** 底部状态条 — 缓冲/匹配/容器/previous 标签 + 自动滚动开关 */ +export function BottomStatusBar({ + lineCount, + matchedCount, + keyword, + container, + previous, + autoScroll, + onToggleAutoScroll, +}: BottomStatusBarProps) { + return ( +
+
+ 缓冲 {lineCount}/{RING_BUFFER_MAX} + {keyword && 匹配 {matchedCount}} + {container && {container}} + {previous && previous} +
+ +
+ ); +} diff --git a/frontend/src/components/kubernetes/pod-logs-viewer/ControlBar.tsx b/frontend/src/components/kubernetes/pod-logs-viewer/ControlBar.tsx new file mode 100644 index 0000000..bb90564 --- /dev/null +++ b/frontend/src/components/kubernetes/pod-logs-viewer/ControlBar.tsx @@ -0,0 +1,178 @@ +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Play, Pause, Download, Search, RefreshCw, Box, History, Clock, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import i18n from '@/i18n'; +import { TAIL_OPTIONS } from './types'; + +interface ControlBarProps { + containers: string[]; + container: string; + onContainerChange: (v: string) => void; + tailLines: number; + onTailLinesChange: (v: number) => void; + previous: boolean; + onPreviousToggle: () => void; + sinceTime: string; + onSinceTimeChange: (v: string) => void; + keyword: string; + onKeywordChange: (v: string) => void; + streaming: boolean; + paused: boolean; + loading: boolean; + filteredLineCount: number; + onDownload: () => void; + onRefresh: () => void; + onStartStream: () => void; + onStopStream: () => void; + onPauseToggle: () => void; +} + +/** 控制栏 — 容器/行数/previous/时间/关键字/下载/刷新/实时流控制 */ +export function ControlBar(props: ControlBarProps) { + const { + containers, container, onContainerChange, + tailLines, onTailLinesChange, + previous, onPreviousToggle, + sinceTime, onSinceTimeChange, + keyword, onKeywordChange, + streaming, paused, loading, + filteredLineCount, + onDownload, onRefresh, onStartStream, onStopStream, onPauseToggle, + } = props; + + return ( +
+ {/* 容器 */} + {containers.length > 0 && ( + + )} + + {/* 行数 */} + + + {/* previous */} + + + {/* sinceTime */} +
+ + { + const v = e.target.value; + onSinceTimeChange(v ? new Date(v).toISOString() : ''); + }} + className="h-8 w-[170px] text-xs bg-[var(--dg-bg-hover)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" + title={i18n.t('kubernetes.podlogsviewerts.s127892fd')} + /> +
+ +
+ + {/* 关键字过滤 */} +
+ + onKeywordChange(e.target.value)} + placeholder={i18n.t('kubernetes.podlogsviewerts.s7558f175')} + className="h-8 w-[160px] pl-7 text-xs bg-[var(--dg-bg-hover)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" + /> +
+ + {/* 下载 */} + + + {/* 刷新(一次性) */} + + + {/* 实时跟踪 / 暂停 / 继续 */} + {!streaming ? ( + + ) : ( + <> + + + + )} +
+ ); +} diff --git a/frontend/src/components/kubernetes/pod-logs-viewer/LogContentArea.tsx b/frontend/src/components/kubernetes/pod-logs-viewer/LogContentArea.tsx new file mode 100644 index 0000000..0bc7e14 --- /dev/null +++ b/frontend/src/components/kubernetes/pod-logs-viewer/LogContentArea.tsx @@ -0,0 +1,64 @@ +import type { RefObject } from 'react'; +import { Loader2, AlertCircle } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import i18n from '@/i18n'; +import type { LogLine } from './types'; +import { highlightKeyword } from './types'; + +interface LogContentAreaProps { + filteredLines: LogLine[]; + lineCount: number; + loading: boolean; + keyword: string; + fullscreen: boolean; + scrollRef: RefObject; + autoScrollRef: RefObject; + /** 用户滚动时通知父组件当前是否在底部(用于自动滚动开关同步) */ + onScrollAt: (atBottom: boolean) => void; +} + +/** 日志内容区 — 可滚动区域 + 空状态/加载状态 + 关键字高亮渲染 */ +export function LogContentArea({ + filteredLines, + lineCount, + loading, + keyword, + fullscreen, + scrollRef, + autoScrollRef, + onScrollAt, +}: LogContentAreaProps) { + return ( +
{ + const el = e.currentTarget; + const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40; + if (!atBottom && autoScrollRef.current) onScrollAt(false); + if (atBottom && !autoScrollRef.current) onScrollAt(true); + }} + className={cn( + 'overflow-auto bg-[#0f172a] font-mono text-xs leading-relaxed p-3', + 'text-[var(--dg-text-secondary)]' + )} + style={{ height: fullscreen ? 'calc(100vh - 98px)' : '520px' }} + > + {lineCount === 0 && !loading && ( +
+ + {i18n.t('pipeline.noLog')} +
+ )} + {loading && lineCount === 0 && ( +
+ 加载中… +
+ )} + {filteredLines.map((line) => ( +
+ {highlightKeyword(line.text, keyword)} +
+ ))} +
+ ); +} diff --git a/frontend/src/components/kubernetes/pod-logs-viewer/LogsHeader.tsx b/frontend/src/components/kubernetes/pod-logs-viewer/LogsHeader.tsx new file mode 100644 index 0000000..7a79de7 --- /dev/null +++ b/frontend/src/components/kubernetes/pod-logs-viewer/LogsHeader.tsx @@ -0,0 +1,66 @@ +import { ScrollText, Maximize2, Minimize2, X, Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import i18n from '@/i18n'; +import { RING_BUFFER_MAX } from './types'; + +interface LogsHeaderProps { + podName: string; + streaming: boolean; + paused: boolean; + loading: boolean; + fullscreen: boolean; + lineCount: number; + matchedCount: number; + keyword: string; + onToggleFullscreen: () => void; + onClose: () => void; +} + +/** 头部工具栏 — 状态指示灯 + 标题 + 行数统计 + 全屏/关闭按钮 */ +export function LogsHeader({ + podName, + streaming, + paused, + loading, + fullscreen, + lineCount, + matchedCount, + keyword, + onToggleFullscreen, + onClose, +}: LogsHeaderProps) { + return ( +
+
+
+ + 日志 — {podName} + {loading && } + + {streaming ? (paused ? i18n.t('kubernetes.deployment.paused') : i18n.t('kubernetes.podlogsviewerts.sd9c71f71')) : i18n.t('agent.agentServiceStopped')} · {matchedCount}{keyword ? `/${lineCount}` : ''} 行 + {lineCount >= RING_BUFFER_MAX && i18n.t('kubernetes.podlogsviewerts.sf3280b73')} + +
+ +
+ + +
+
+ ); +} diff --git a/frontend/src/components/kubernetes/pod-logs-viewer/types.tsx b/frontend/src/components/kubernetes/pod-logs-viewer/types.tsx new file mode 100644 index 0000000..4e5f5dc --- /dev/null +++ b/frontend/src/components/kubernetes/pod-logs-viewer/types.tsx @@ -0,0 +1,57 @@ +import type { ReactNode } from 'react'; +import i18n from '@/i18n'; + +export interface PodLogsViewerProps { + open: boolean; + onOpenChange: (open: boolean) => void; + clusterCode: string; + namespace: string; + podName: string; + /** 可选:Pod 容器名列表 */ + containers?: string[]; + initialContainer?: string; +} + +export interface LogLine { + id: number; + text: string; +} + +/** 环形缓冲大小(借鉴 opsx-agent) */ +export const RING_BUFFER_MAX = 10000; + +export const TAIL_OPTIONS = [ + { value: '100', label: i18n.t('kubernetes.podlogsviewerts.s58b02a7a') }, + { value: '500', label: i18n.t('kubernetes.podlogsviewerts.s9a036801') }, + { value: '1000', label: i18n.t('kubernetes.podlogsviewerts.sf89a330c') }, + { value: '0', label: i18n.t('system.auditAll') }, +]; + +/** + * 高亮关键字渲染纯函数 — 将一行文本中匹配关键字的片段用 包裹。 + * 无关键字时直接返回文本 span。 + */ +export function highlightKeyword(text: string, keyword: string): ReactNode { + if (!keyword.trim()) return {text}; + const kw = keyword.trim(); + const lower = text.toLowerCase(); + const kwLower = kw.toLowerCase(); + const parts: ReactNode[] = []; + let idx = 0; + let key = 0; + while (idx < text.length) { + const found = lower.indexOf(kwLower, idx); + if (found === -1) { + parts.push({text.slice(idx)}); + break; + } + if (found > idx) parts.push({text.slice(idx, found)}); + parts.push( + + {text.slice(found, found + kw.length)} + + ); + idx = found + kw.length; + } + return <>{parts}; +}