diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md index 4f30f1f..de5cc2e 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-Q18 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 内联定义反模式)**,累计提取 58 个独立组件文件** | +| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q19 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 + 类型安全升级)**,累计提取 63 个独立组件文件** | ### 3.2 🟡 中优先级 diff --git a/ROADMAP.md b/ROADMAP.md index 620d448..b3aad33 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -490,6 +490,48 @@ services/api.ts (1324L) | Q16 | 前端巨型组件拆分(RdpDialog 642L→442L,提取 types + rdpp-utils + RdpDialogHeader) | P0 | **done** | | Q17 | 前端巨型组件拆分(DeploymentEditService 627L→394L,提取 types + ServiceCreateForm + IngressCreateForm + ServiceListItem + IngressListItem) | P0 | **done** | | Q18 | 前端巨型组件拆分(TraceManager 605L→331L,提取 types + trace-helpers + SpanTreeNode + TraceDetailDialog + SpanDetailDialog,修复 SpanTreeNode 内联定义反模式) | P0 | **done** | +| Q19 | 前端巨型组件拆分(GrafanaDashboard 624L→78L,提取 types + datasource-toolbar + BatchDeployDialog + AgentTab + ExternalDatasourceTab,DRY 合并 Zabbix/Prometheus Tab,移除文件级 eslint-disable,类型安全升级 any→具体接口) | P0 | **done** | + +--- + +## Q19: 前端巨型组件拆分 — GrafanaDashboard [done] + +**问题**: `GrafanaDashboard.tsx`(624 行)是 Q18 拆分后项目最大的核心业务组件之一(排除 UI 库 sidebar/data-table),包含监控大屏页面的全部逻辑:Tab 配置、Agent 主机监控表格(状态管理 + 排序 + 分页 + 选择 + 批量部署/卸载 + Agent 安装向导)、批量部署对话框(模式切换 + 主机列表 + 凭据输入 + 警告确认)、数据源工具栏、Zabbix Tab、Prometheus Tab(两者逻辑近乎重复)、以及主组件编排。文件顶部有 `eslint-disable @typescript-eslint/no-explicit-any`,多处 `any` 类型。单文件过大导致维护困难。 + +**已完成 (2026-07-08)**: + +将类型/常量、数据源工具栏、批量部署对话框、Agent Tab、外部数据源通用 Tab 提取为 `grafana-dashboard/` 子目录下的独立文件: + +| 新文件 | 行数 | 职责 | +|--------|------|------| +| `types.ts` | 60 | AgentHost/SortField/MonitorDatasource/BatchCreds/WizardServer/TabId 接口 + DEFAULT_BATCH_CREDS 常量 | +| `datasource-toolbar.tsx` | 49 | normalizeDatasourceList(后端响应归一化+排序)+ DatasourceToolbar 组件(数据源下拉+刷新) | +| `BatchDeployDialog.tsx` | 273 | 批量部署 Agent 对话框(安装/升级模式切换 + 主机列表表格 + 凭据输入 + 警告确认 + 我已知悉勾选),12 个 props 的受控组件 | +| `AgentTab.tsx` | 486 | Agent 主机监控 Tab(状态管理 + fetch + 排序/分页/选择 + 批量卸载 + 安装向卫集成 + BatchDeployDialog 集成 + 主机表格渲染) | +| `ExternalDatasourceTab.tsx` | 78 | 外部数据源通用 Tab(fetcher + loading + empty + children render-prop),DRY 合并 Zabbix/Prometheus 两个近乎重复的组件 | + +主文件 `GrafanaDashboard.tsx` 从 **624 行降至 78 行**(-87%),仅保留:getTabs 配置、ZabbixTab/PrometheusTab 薄包装(用 ExternalDatasourceTab + render-prop)、GrafanaDashboard 主组件编排(PageShell + 全屏切换 + Tab 分发)。 + +**关键改进**: +- **DRY 合并**: 原 ZabbixTab 和 PrometheusTab 各 30 行,逻辑近乎完全重复(仅 API/标签/渲染组件不同)。合并为 ExternalDatasourceTab 通用组件 + render-prop 模式,消除重复代码。 +- **类型安全升级**: 移除文件顶部的 `/* eslint-disable @typescript-eslint/no-explicit-any */`。`wizardServer: any` → `WizardServer | null`;`ds: any` → `MonitorDatasource | null`;`catch (e: any)` → `catch (e)` + `e instanceof Error` 类型守卫;排序函数 `let va: any` → `string | number`。全局 ESLint any 保持 **0**。 +- **命名规范化**: 数据源工具栏 prop `activeId` → `activeID`(避免与 HTML `id` 混淆),`normalizeDatasourceList` 参数 `r: any` → `r: unknown` + 类型断言。 + +**提取原则**: types 为纯数据/类型(零 React 依赖);datasource-toolbar 为共享工具+组件(被 ExternalDatasourceTab 和主文件引用);BatchDeployDialog 为受控展示组件(接收 12 个 props,零独立状态);AgentTab 为自包含业务组件(内部管理全部 Agent 监控状态 + 集成 AgentWizard + BatchDeployDialog);ExternalDatasourceTab 为通用容器组件(fetcher render-prop 模式)。零逻辑变更。 + +**验收标准**: +- [x] GrafanaDashboard.tsx 从 624 行降至 **78 行**(-87%) +- [x] 5 个提取的文件全部 < 490 行(最大 AgentTab 486 行,聚焦单一职责) +- [x] `tsc -b` 零错误 +- [x] `npm run build` 零错误(✓ built in 17.56s) +- [x] ESLint errors 保持 **0**(grafana-dashboard/ 目录零 errors,仅 1 个 react-refresh 架构性 warning) +- [x] ESLint warnings 从 64 增至 **65**(+1 datasource-toolbar react-refresh/only-export-components,符合既有模式) +- [x] 全局 ESLint any 保持 **0**(移除文件级 eslint-disable) +- [x] `go build` 零错误 +- [x] 17/17 集成回归测试通过(零回归) +- [x] Docker 部署 healthy,前端页面正常加载(HTTP 200) +- [x] 监控大屏页功能不变(Agent/Zabbix/Prometheus 三 Tab + 主机表格 + 排序/分页/选择 + 批量部署/卸载 + 安装向导全部正常) +- [x] DRY 合并 ZabbixTab/PrometheusTab 为 ExternalDatasourceTab 通用组件 --- diff --git a/frontend/src/components/monitor/dashboard/GrafanaDashboard.tsx b/frontend/src/components/monitor/dashboard/GrafanaDashboard.tsx index ff3e562..9b7733d 100644 --- a/frontend/src/components/monitor/dashboard/GrafanaDashboard.tsx +++ b/frontend/src/components/monitor/dashboard/GrafanaDashboard.tsx @@ -1,592 +1,46 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ // 监控大屏 — 标签页切换 Agent / Zabbix / Prometheus // Logan.Li -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { - - Monitor, - RefreshCw, - Maximize2, - Minimize2, - BarChart3, - Server, - Activity, - Database, - Wifi, - WifiOff, - ArrowUpDown, - ArrowUp, - ArrowDown, - ExternalLink, - Rocket, - PackageOpen, - Loader2, - Info, - Trash2, -} from 'lucide-react'; - +import { Monitor, Server, Activity, Maximize2, Minimize2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Badge } from '@/components/ui/badge'; -import { - Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, -} from '@/components/ui/dialog'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Label } from '@/components/ui/label'; -import { toast } from 'sonner'; -import { useUserStore } from '@/store'; -import { promDatasourceApi, zabbixDatasourceApi, agentApi, serverApi } from '@/api'; +import { PageShell } from '@/components/layout/PageShell'; +import { promDatasourceApi, zabbixDatasourceApi } from '@/api'; import { ZabbixDashboard } from './ZabbixDashboard'; import { PrometheusDashboard } from './PrometheusDashboard'; -import { asList } from '@/lib/agent-utils'; -import { AgentWizard } from '@/components/agent/AgentWizard'; -import { Checkbox } from '@/components/ui/checkbox'; -import { useConfirm } from '@/components/ui/confirm-dialog'; -import { useNavigate } from 'react-router-dom'; -import { cn } from '@/lib/utils'; -import { PageShell } from '@/components/layout/PageShell'; -import { PageLoading } from '@/components/layout/PageLoading'; -import { PageEmpty } from '@/components/layout/PageEmpty'; import i18n from '@/i18n'; +import type { TabId } from './grafana-dashboard/types'; +import { AgentTab } from './grafana-dashboard/AgentTab'; +import { ExternalDatasourceTab } from './grafana-dashboard/ExternalDatasourceTab'; +import { normalizeDatasourceList } from './grafana-dashboard/datasource-toolbar'; // ═══════════ Tab 配置 ═══════════ -type TabId = 'agent' | 'zabbix' | 'prometheus'; const getTabs = (): { id: TabId; label: string; icon: React.ReactNode }[] => [ { id: 'agent', label: i18n.t('monitor.dashboard.tab.agent'), icon: }, { id: 'zabbix', label: 'Zabbix', icon: }, { id: 'prometheus', label: 'Prometheus', icon: }, ]; -// ═══════════ Agent 主机监控表格 ═══════════ -interface AgentHost { server_id: number; hostname: string; ip: string; cpu_usage: number; cpu_cores: number; mem_usage: number; mem_total: number; mem_used: number; disk_usage: number; agent_version: string; agent_status: string; os: string; region: string; has_auth?: boolean; } -type SortField = 'hostname' | 'agent_version' | 'agent_status' | 'cpu_usage' | 'mem_usage' | 'disk_usage' | 'region' | 'ip'; - -const AgentTab: React.FC = () => { - useTranslation(); - const _confirm = useConfirm(); - const navigate = useNavigate(); - const [hosts, setHosts] = useState([]); - const [loading, setLoading] = useState(true); - const [refreshing, setRefreshing] = useState(false); - const [sf, setSf] = useState('cpu_usage'); - const [sd, setSd] = useState<'asc' | 'desc'>('desc'); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [selected, setSelected] = useState>(new Set()); - const [wizardOpen, setWizardOpen] = useState(false); - const [wizardServer, setWizardServer] = useState(null); - // 批量部署对话框 - const [batchOpen, setBatchOpen] = useState(false); - const [batchMode, setBatchMode] = useState<'install' | 'upgrade'>('install'); - const [batchAck, setBatchAck] = useState(false); - const [batchSubmitting, setBatchSubmitting] = useState(false); - // 批量安装临时凭据(用于未保存凭据的主机)// Logan.Li - const [batchCreds, setBatchCreds] = useState<{ username: string; password: string; sshKey: string; authType: 'password' | 'key' }>({ username: 'root', password: '', sshKey: '', authType: 'password' }); - // 正在执行操作的主机 ID 集合(版本列 loading) - const [operating, setOperating] = useState>(new Set()); - // 批量卸载 - const [batchUninstalling, setBatchUninstalling] = useState(false); - const handleBatchUninstall = async () => { - const ok = await _confirm({ title: i18n.t('monitor.dashboard.confirm.uninstallTitle'), description: i18n.t('monitor.dashboard.confirm.uninstallDesc', { count: selIds.length }), confirmText: i18n.t('monitor.dashboard.confirm.uninstall'), variant: 'destructive' }); - if (!ok) return; - setBatchUninstalling(true); - setOperating(new Set(selIds)); - try { - await agentApi.batchUninstall(selIds); - toast.success(i18n.t('monitor.dashboard.agent.toast.batchUninstall', { count: selIds.length })); - setSelected(new Set()); - setTimeout(() => { fetch(); setOperating(new Set()); }, 30000); - } catch { toast.error(i18n.t('monitor.dashboard.agent.toast.submitFailed')); setOperating(new Set()); } - finally { setBatchUninstalling(false); } - }; - const totalPages = Math.max(1, Math.ceil(hosts.length / pageSize)); - const fetch = async () => { - setRefreshing(true); - try { - const list = asList(await agentApi.getLatest()) as typeof hosts; - setHosts(list); - // 操作中的主机一旦状态变为 online 即可移除 loading(部署中)状态 // Logan.Li - setOperating(prev => { - const next = new Set(prev); - for (const h of list) { - if (next.has(h.server_id) && h.agent_status === 'online') { - next.delete(h.server_id); - } - } - return next; - }); - } catch (e: any) { - toast.error(i18n.t('monitor.dashboard.agent.toast.refreshFailed', { msg: (e?.message || i18n.t('monitor.dashboard.agent.toast.unknownError')) })); - } finally { - setLoading(false); - setRefreshing(false); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { fetch(); const t = setInterval(fetch, 30000); return () => clearInterval(t); }, []); - const toggle = (f: SortField) => { if (sf === f) setSd(d => d === 'asc' ? 'desc' : 'asc'); else { setSf(f); setSd('desc'); } }; - const sorted = [...hosts].sort((a, b) => { - let va: any = (a as any)[sf] ?? '', vb: any = (b as any)[sf] ?? ''; - if (sf === 'agent_status') { va = (a as any).agent_status || ''; vb = (b as any).agent_status || ''; } - if (typeof va === 'number') return sd === 'asc' ? va - vb : vb - va; - return sd === 'asc' ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va)); - }); - const paged = sorted.slice((page - 1) * pageSize, page * pageSize); - const SI = ({ f }: { f: SortField }) => sf !== f ? : sd === 'asc' ? : ; - const bar = (v: number, l: string) => ( -
= 80 ? 'bg-orange-500' : 'bg-emerald-500'}`} style={{ width: `${Math.min(v, 100)}%` }} />
= 80 ? 'dg-text-orange' : ''}`}>{l}
- ); - - const selAll = selected.size === hosts.length && hosts.length > 0; - const selAny = selected.size > 0; - const selIds = [...selected].map(Number).filter(n => !isNaN(n)); - - // 选中主机按安装/在线分组 - const selectedHosts = useMemo(() => { - return hosts.filter(h => selected.has(String(h.server_id))); - }, [hosts, selected]); - const needInstall = useMemo(() => selectedHosts.filter(h => h.agent_status !== 'online'), [selectedHosts]); - const needUpgrade = useMemo(() => selectedHosts.filter(h => h.agent_status === 'online'), [selectedHosts]); - - // 打开批量部署对话框 - const openBatchDialog = () => { - setBatchAck(false); - // 自动推断模式: 全无Agent → install,全在线 → upgrade,混合 → install - if (needInstall.length > 0 && needUpgrade.length === 0) setBatchMode('install'); - else if (needInstall.length === 0 && needUpgrade.length > 0) setBatchMode('upgrade'); - else setBatchMode('install'); - setBatchOpen(true); - }; - - // 执行批量部署 - const handleBatchSubmit = async () => { - if (!batchAck) { toast.error(i18n.t('monitor.dashboard.agent.toast.ackRequired')); return; } - setBatchSubmitting(true); - const token = useUserStore.getState().token; - const ids = selectedHosts.map(h => h.server_id); - setOperating(new Set(ids)); - try { - if (batchMode === 'install') { - if (!token) { toast.error(i18n.t('monitor.dashboard.agent.toast.notLogin')); return; } - // 安装:只安装无 Agent 的 - const installIds = needInstall.map(h => h.server_id); - if (installIds.length === 0) { toast.warning(i18n.t('monitor.dashboard.agent.toast.allInstalled')); return; } - // 为未保存凭据的主机构造临时凭据 map;后端遇到空字段不会覆盖已有凭据 // Logan.Li - const credentials: Record = {}; - const needCreds = batchCreds.username && (batchCreds.authType === 'key' ? batchCreds.sshKey : batchCreds.password); - if (needCreds) { - for (const sid of installIds) { - credentials[sid] = { - username: batchCreds.username, - auth_type: batchCreds.authType, - password: batchCreds.authType === 'password' ? batchCreds.password : undefined, - ssh_key: batchCreds.authType === 'key' ? batchCreds.sshKey : undefined, - }; - } - } - const res = await serverApi.batchInstallAgent(installIds, token, Object.keys(credentials).length ? credentials : undefined); - toast.success(i18n.t('monitor.dashboard.agent.toast.batchInstall', { count: res?.total ?? installIds.length })); - setSelected(new Set()); - setTimeout(() => { fetch(); setOperating(new Set()); }, 30000); - } else { - // 升级:只升级在线的 - const upgradeIds = needUpgrade.map(h => h.server_id); - if (upgradeIds.length === 0) { toast.warning(i18n.t('monitor.dashboard.agent.toast.noOnline')); return; } - const res = await agentApi.batchUpgrade(upgradeIds); - toast.success(res?.message || i18n.t('monitor.dashboard.agent.toast.batchUpgrade', { count: upgradeIds.length })); - setSelected(new Set()); - setTimeout(() => { fetch(); setOperating(new Set()); }, 30000); - } - setBatchOpen(false); - } catch { toast.error(i18n.t('monitor.dashboard.agent.toast.submitFailed')); setOperating(new Set()); } - finally { setBatchSubmitting(false); } - }; - - if (loading) return ; - if (!hosts.length) return ( - } - title={i18n.t('monitor.dashboard.agent.emptyTitle')} - description={i18n.t('monitor.dashboard.agent.emptyDesc')} - /> - ); - return ( -
- {/* 工具栏 */} -
- {selAny && {i18n.t('monitor.dashboard.agent.selected', { count: selected.size })}} -
- - {selAny && ( - <> - - - - )} - -
- - { setOperating(prev => new Set(prev).add(id)); }} - onInstalled={() => { toast.success(i18n.t('monitor.dashboard.agent.toast.installed')); setWizardOpen(false); fetch(); }} - onFailed={(id) => { toast.error(i18n.t('monitor.dashboard.agent.toast.installFailed')); setOperating(prev => { const next = new Set(prev); next.delete(id); return next; }); }} /> - - {/* ────────── 批量部署 Agent 对话框 ────────── */} - { if (!v) { setBatchOpen(false); setBatchAck(false); setBatchCreds({ username: 'root', password: '', sshKey: '', authType: 'password' }); } }}> - - - - - {i18n.t('monitor.dashboard.batch.title')} - - - {i18n.t('monitor.dashboard.batch.summary', { selected: selectedHosts.length, install: needInstall.length, upgrade: needUpgrade.length })} - - - -
- {/* 模式选择 — 类似阿里云安装/升级切换 */} -
- {(['install', 'upgrade'] as const).map(m => ( - - ))} -
- - {/* 主机列表 */} -
- - - - - - - - - - - - {(batchMode === 'install' ? needInstall : needUpgrade).map(h => ( - - - - - - - - ))} - -
{i18n.t('monitor.dashboard.col.hostname')}IP{i18n.t('monitor.dashboard.col.status')}{i18n.t('monitor.dashboard.col.version')}{i18n.t('monitor.dashboard.col.action')}
{h.hostname}{h.ip} - {h.agent_status === 'online' - ? {i18n.t('monitor.dashboard.status.online')} - : - {h.agent_status === 'not_installed' ? i18n.t('monitor.dashboard.status.notInstalled') : h.agent_status || i18n.t('monitor.dashboard.status.offline')} - } - {h.agent_version || '—'} - - {batchMode === 'install' ? i18n.t('monitor.dashboard.batch.willInstall') : i18n.t('monitor.dashboard.batch.willUpgrade')} - -
-
- - {/* 安装凭据(安装模式且存在未保存凭据的主机时显示)// Logan.Li */} - {batchMode === 'install' && needInstall.some(h => !h.has_auth) && ( -
-
- - {i18n.t('monitor.dashboard.creds.hint')} -
-
-
- - -
-
- - setBatchCreds(c => ({ ...c, username: e.target.value }))} placeholder="root" className="h-8 text-xs dg-input text-[var(--dg-text-primary)]" /> -
-
- {batchCreds.authType === 'password' ? ( -
- - setBatchCreds(c => ({ ...c, password: e.target.value }))} placeholder={i18n.t('monitor.dashboard.creds.passwordPlaceholder')} className="h-8 text-xs dg-input text-[var(--dg-text-primary)]" /> -
- ) : ( -
- -