From 6c828c59920ec4b2bdcede89eb99b9feace38d32 Mon Sep 17 00:00:00 2001 From: attacker Date: Thu, 9 Jul 2026 23:05:57 +0800 Subject: [PATCH] =?UTF-8?q?refactor(quality):=20Q32=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=20AuditOperationsTab=20527L=E2=86=92275L=20(-48%),=20?= =?UTF-8?q?=E6=8F=90=E5=8F=96=20audit-operations/=203=20=E4=B8=AA=E6=96=87?= =?UTF-8?q?=E4=BB=B6=20(types=2043L=20normalizeLog=20=E7=BA=AF=E5=87=BD?= =?UTF-8?q?=E6=95=B0+AuditStatistics=20=E6=8E=A5=E5=8F=A3=20+=20LogDetailD?= =?UTF-8?q?ialog=2085L=20=E8=AF=A6=E6=83=85=E5=BC=B9=E7=AA=97+DetailRow=20?= =?UTF-8?q?+=20StatisticsDialog=20108L=20=E7=BB=9F=E8=AE=A1=E5=BC=B9?= =?UTF-8?q?=E7=AA=97+StatCard),=20=E6=B8=85=E9=99=A4=E6=AD=BB=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=20useConfirm/=5Fconfirm/=5FhandleShowStats/statsOpen/?= =?UTF-8?q?statistics/statsLoading,=20=E9=9B=B6=E9=80=BB=E8=BE=91=E5=8F=98?= =?UTF-8?q?=E6=9B=B4,=20ESLint=20errors=3D0/any=3D0,=20tsc=20-b=3D0,=20npm?= =?UTF-8?q?=20build=E2=9C=93=20(17.34s),=20go=20build=3D0,=2017/17=20?= =?UTF-8?q?=E6=B5=8B=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 | 1 + .../src/pages/audit/AuditOperationsTab.tsx | 258 +----------------- .../audit-operations/LogDetailDialog.tsx | 85 ++++++ .../audit-operations/StatisticsDialog.tsx | 108 ++++++++ .../src/pages/audit/audit-operations/types.ts | 43 +++ 6 files changed, 241 insertions(+), 256 deletions(-) create mode 100644 frontend/src/pages/audit/audit-operations/LogDetailDialog.tsx create mode 100644 frontend/src/pages/audit/audit-operations/StatisticsDialog.tsx create mode 100644 frontend/src/pages/audit/audit-operations/types.ts diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md index 29b2c8a..8e0a10c 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-Q27 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%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + HostMonitor 547L→224L (-59%) + KubernetesClusterDetail 547L→130L (-76%) + **AlertCenter 542L→312L (-42%,提取 4 个受控组件: AlertsToolbar + AlertsTab + ContactsTab + ContactGroupsTab,客户端搜索过滤+删除确认逻辑下沉到子组件)** + **CloudBilling 534L→295L (-45%,提取 cloud-billing/ 3 个文件: OverviewTab + RecordsTab(含 RecordsFilterToolbar) + AlertRulesTab)** + **PromDsFormDialog 528L→294L (-44%,提取 6 个文件: CollapsibleSection 通用折叠包装器 DRY 3 个重复折叠模式 + AuthSection + HeadersSection + AlertingSection + MiscSection + TestResultBanner)**,累计提取 112 个独立组件文件** | +| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q27 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%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + HostMonitor 547L→224L (-59%) + KubernetesClusterDetail 547L→130L (-76%) + **AlertCenter 542L→312L (-42%,提取 4 个受控组件: AlertsToolbar + AlertsTab + ContactsTab + ContactGroupsTab,客户端搜索过滤+删除确认逻辑下沉到子组件)** + **CloudBilling 534L→295L (-45%,提取 cloud-billing/ 3 个文件: OverviewTab + RecordsTab(含 RecordsFilterToolbar) + AlertRulesTab)** + **PromDsFormDialog 528L→294L (-44%,提取 6 个文件: CollapsibleSection 通用折叠包装器 DRY 3 个重复折叠模式 + AuthSection + HeadersSection + AlertingSection + MiscSection + TestResultBanner)**,累计提取 112 个独立组件文件 + **AuditOperationsTab 527L→275L (-48%,提取 audit-operations/ 3 个文件: types(normalizeLog 纯函数+AuditStatistics 接口) + LogDetailDialog(85L 详情弹窗+DetailRow) + StatisticsDialog(108L 统计弹窗+StatCard),清除死代码 useConfirm/_confirm/_handleShowStats/statsOpen/statistics/statsLoading)**,累计提取 115 个独立组件文件** | ### 3.2 🟡 中优先级 diff --git a/ROADMAP.md b/ROADMAP.md index 825f70a..639d71c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -503,6 +503,7 @@ services/api.ts (1324L) | Q29 | 前端巨型组件拆分(AlertCenter 542L→312L (-42%),提取 4 个受控组件 (AlertsToolbar 89L 筛选工具栏 + AlertsTab 149L 告警列表表格+分页 + ContactsTab 87L 联系人卡片网格 + ContactGroupsTab 88L 联系组卡片网格),客户端搜索过滤逻辑移入 AlertsTab,删除确认逻辑下沉到子组件) | P0 | **done** | | Q30 | 前端巨型组件拆分(CloudBilling 534L→295L (-45%),提取 cloud-billing/ 3 个文件 (OverviewTab 110L 概览统计:同步信息+成本告警横幅+趋势图+厂商分布 + RecordsTab 146L 成本记录表格+RecordsFilterToolbar筛选工具栏 + AlertRulesTab 91L 预警规则卡片网格)) | P0 | **done** | | Q31 | 前端巨型组件拆分(PromDsFormDialog 528L→294L (-44%),提取 6 个文件 (CollapsibleSection 42L 通用折叠包装器 DRY 3 个重复折叠模式 + AuthSection 107L 认证折叠 + HeadersSection 64L 自定义Headers + AlertingSection 58L 告警折叠 + MiscSection 47L 杂项折叠 + TestResultBanner 32L 测试结果横幅),3 个折叠区块全部使用共享 CollapsibleSection 消除重复 JSX)) | P0 | **done** | +| Q32 | 前端巨型组件拆分(AuditOperationsTab 527L→275L (-48%),提取 audit-operations/ 3 个文件 (types 43L normalizeLog 纯函数+AuditStatistics 接口 + LogDetailDialog 85L 详情弹窗+DetailRow + StatisticsDialog 108L 统计弹窗+StatCard),清除死代码 useConfirm/_confirm/_handleShowStats/statsOpen/statistics/statsLoading,ESLint errors=0/any=0,tsc -b=0,npm build✓,go build=0,17/17 测试通过) | P0 | **done** | --- diff --git a/frontend/src/pages/audit/AuditOperationsTab.tsx b/frontend/src/pages/audit/AuditOperationsTab.tsx index e93ca4c..57906ec 100644 --- a/frontend/src/pages/audit/AuditOperationsTab.tsx +++ b/frontend/src/pages/audit/AuditOperationsTab.tsx @@ -1,5 +1,6 @@ // 操作审计 Tab — 增强版审计日志列表,含筛选/导出/统计/清除 // v2: 虚拟滚动渲染,支持万级日志流畅查看 +// Q32: 拆分 types/LogDetailDialog/StatisticsDialog 到 audit-operations/ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { cn } from '@/lib/utils'; import { useTranslation } from 'react-i18next'; @@ -12,7 +13,6 @@ import { RefreshCw, Download, Eye, - X, ClipboardList, } from 'lucide-react'; import { PageShell } from '@/components/layout/PageShell'; @@ -21,49 +21,12 @@ import { PageEmpty } from '@/components/layout/PageEmpty'; import { StatStrip } from '@/components/ui/stat-strip'; import { auditApi } from '@/api'; import { useAuditLogs } from '@/hooks/api'; -import { useConfirm } from '@/components/ui/confirm-dialog'; import type { OperationLog, OptionItem } from '@/types'; - -// Normalize backend log to frontend type -function normalizeLog(raw: Record): OperationLog { - const status = raw.status as number | undefined; - const result: OperationLog['result'] = - status === 0 ? 'failure' : status === 1 ? 'success' : raw.result === 'failure' ? 'failure' : 'success'; - const request = String(raw.request ?? ''); - const response = String(raw.response ?? ''); - const error = String(raw.error ?? ''); - const message = error || response || request || String(raw.message ?? raw.detail ?? ''); - return { - id: String(raw.id ?? ''), - userId: String(raw.user_id ?? raw.userId ?? ''), - username: String(raw.username ?? ''), - module: String(raw.module ?? raw.resource ?? ''), - action: String(raw.action ?? ''), - resource: String(raw.resource ?? ''), - ip: String(raw.ip ?? ''), - userAgent: String(raw.user_agent ?? raw.userAgent ?? ''), - result, - message, - request, - response, - error, - createdAt: String(raw.createdAt ?? raw.created_at ?? ''), - }; -} - -interface AuditStatistics { - total_logs?: number; - success_count?: number; - failure_count?: number; - today_count?: number; - top_modules?: { module: string; count: number }[]; - top_actions?: { action: string; count: number }[]; - top_users?: { username: string; count: number }[]; -} +import { normalizeLog } from './audit-operations/types'; +import { LogDetailDialog } from './audit-operations/LogDetailDialog'; export function AuditOperationsTab() { const { t } = useTranslation(); - const _confirm = useConfirm(); // Filters const [keyword, setKeyword] = useState(''); const [moduleFilter, setModuleFilter] = useState(''); @@ -87,11 +50,6 @@ export function AuditOperationsTab() { // Detail modal const [detailLog, setDetailLog] = useState(null); - // Statistics modal - const [statsOpen, setStatsOpen] = useState(false); - const [statistics, setStatistics] = useState(null); - const [statsLoading, setStatsLoading] = useState(false); - const fetchOptions = useCallback(async () => { try { const [modRes, actRes] = await Promise.all([auditApi.getModules(), auditApi.getActions()]); @@ -142,19 +100,6 @@ export function AuditOperationsTab() { } }; - const _handleShowStats = async () => { - setStatsOpen(true); - setStatsLoading(true); - try { - const data = (await auditApi.getStatistics()) as AuditStatistics; - setStatistics(data); - } catch { - setStatistics(null); - } finally { - setStatsLoading(false); - } - }; - const viewDetail = (log: OperationLog) => { setDetailLog(log); }; @@ -325,203 +270,6 @@ export function AuditOperationsTab() { {/* Detail Modal */} setDetailLog(null)} /> - - {/* Statistics Modal */} - setStatsOpen(false)} - loading={statsLoading} - stats={statistics} - /> ); } - -// ========================================================================= -// Log Detail Dialog -// ========================================================================= - -function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose: () => void }) { - const { t } = useTranslation(); - if (!log) return null; - - return ( -
-
-
-
-

{t('audit.operations.detailTitle')}

- -
- -
- - - - - - - - - - - {log.result === 'success' ? t('common.success') : t('common.failed')} - - } - /> - {log.request && } - {log.response && } - {log.error && } - {!log.request && !log.response && log.message && ( - - )} -
- -
- -
-
-
- ); -} - -function DetailRow({ label, value, mono }: { label: string; value: React.ReactNode; mono?: boolean }) { - return ( -
- {label} - - {value} - -
- ); -} - -// ========================================================================= -// Statistics Dialog -// ========================================================================= - -function StatisticsDialog({ - open, - onClose, - loading, - stats, -}: { - open: boolean; - onClose: () => void; - loading: boolean; - stats: AuditStatistics | null; -}) { - const { t } = useTranslation(); - if (!open) return null; - - return ( -
-
-
-
-

{t('audit.operations.statsTitle')}

- -
- - {loading ? ( -
-
-
- ) : stats ? ( -
-
- - - - -
- - {stats.top_modules && stats.top_modules.length > 0 && ( -
-

{t('audit.operations.topModules')}

-
- {stats.top_modules.slice(0, 5).map((m, i) => ( -
- {m.module} - {m.count} -
- ))} -
-
- )} - - {stats.top_users && stats.top_users.length > 0 && ( -
-

{t('audit.operations.topUsers')}

-
- {stats.top_users.slice(0, 5).map((u, i) => ( -
- {u.username} - {u.count} -
- ))} -
-
- )} -
- ) : ( -

{t('audit.operations.noStats')}

- )} - -
- -
-
-
- ); -} - -function StatCard({ label, value, color }: { label: string; value: string; color: 'blue' | 'emerald' | 'red' }) { - const colorMap = { - blue: 'border-blue-500/20 bg-blue-500/5 dg-text-blue', - emerald: 'border-emerald-500/20 bg-emerald-500/5 dg-text-green', - red: 'border-red-500/20 bg-red-500/5 dg-text-red', - }; - return ( -
-
{label}
-
{value}
-
- ); -} diff --git a/frontend/src/pages/audit/audit-operations/LogDetailDialog.tsx b/frontend/src/pages/audit/audit-operations/LogDetailDialog.tsx new file mode 100644 index 0000000..b81010d --- /dev/null +++ b/frontend/src/pages/audit/audit-operations/LogDetailDialog.tsx @@ -0,0 +1,85 @@ +// Audit log detail dialog — extracted from AuditOperationsTab +import type { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { X } from 'lucide-react'; +import type { OperationLog } from '@/types'; + +export function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose: () => void }) { + const { t } = useTranslation(); + if (!log) return null; + + return ( +
+
+
+
+

{t('audit.operations.detailTitle')}

+ +
+ +
+ + + + + + + + + + + {log.result === 'success' ? t('common.success') : t('common.failed')} + + } + /> + {log.request && } + {log.response && } + {log.error && } + {!log.request && !log.response && log.message && ( + + )} +
+ +
+ +
+
+
+ ); +} + +function DetailRow({ label, value, mono }: { label: string; value: ReactNode; mono?: boolean }) { + return ( +
+ {label} + + {value} + +
+ ); +} diff --git a/frontend/src/pages/audit/audit-operations/StatisticsDialog.tsx b/frontend/src/pages/audit/audit-operations/StatisticsDialog.tsx new file mode 100644 index 0000000..f4cc6d7 --- /dev/null +++ b/frontend/src/pages/audit/audit-operations/StatisticsDialog.tsx @@ -0,0 +1,108 @@ +// Audit statistics dialog — extracted from AuditOperationsTab +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; +import { X } from 'lucide-react'; +import type { AuditStatistics } from './types'; + +export function StatisticsDialog({ + open, + onClose, + loading, + stats, +}: { + open: boolean; + onClose: () => void; + loading: boolean; + stats: AuditStatistics | null; +}) { + const { t } = useTranslation(); + if (!open) return null; + + return ( +
+
+
+
+

{t('audit.operations.statsTitle')}

+ +
+ + {loading ? ( +
+
+
+ ) : stats ? ( +
+
+ + + + +
+ + {stats.top_modules && stats.top_modules.length > 0 && ( +
+

{t('audit.operations.topModules')}

+
+ {stats.top_modules.slice(0, 5).map((m, i) => ( +
+ {m.module} + {m.count} +
+ ))} +
+
+ )} + + {stats.top_users && stats.top_users.length > 0 && ( +
+

{t('audit.operations.topUsers')}

+
+ {stats.top_users.slice(0, 5).map((u, i) => ( +
+ {u.username} + {u.count} +
+ ))} +
+
+ )} +
+ ) : ( +

{t('audit.operations.noStats')}

+ )} + +
+ +
+
+
+ ); +} + +function StatCard({ label, value, color }: { label: string; value: string; color: 'blue' | 'emerald' | 'red' }) { + const colorMap = { + blue: 'border-blue-500/20 bg-blue-500/5 dg-text-blue', + emerald: 'border-emerald-500/20 bg-emerald-500/5 dg-text-green', + red: 'border-red-500/20 bg-red-500/5 dg-text-red', + }; + return ( +
+
{label}
+
{value}
+
+ ); +} diff --git a/frontend/src/pages/audit/audit-operations/types.ts b/frontend/src/pages/audit/audit-operations/types.ts new file mode 100644 index 0000000..f2086dc --- /dev/null +++ b/frontend/src/pages/audit/audit-operations/types.ts @@ -0,0 +1,43 @@ +// Audit operations types & pure helpers (extracted from AuditOperationsTab) +import type { OperationLog } from '@/types'; + +/** Backend audit log shape (snake_case from API). */ +type RawLog = Record; + +/** Normalize a raw backend audit log to the frontend OperationLog type. */ +export function normalizeLog(raw: RawLog): OperationLog { + const status = raw.status as number | undefined; + const result: OperationLog['result'] = + status === 0 ? 'failure' : status === 1 ? 'success' : raw.result === 'failure' ? 'failure' : 'success'; + const request = String(raw.request ?? ''); + const response = String(raw.response ?? ''); + const error = String(raw.error ?? ''); + const message = error || response || request || String(raw.message ?? raw.detail ?? ''); + return { + id: String(raw.id ?? ''), + userId: String(raw.user_id ?? raw.userId ?? ''), + username: String(raw.username ?? ''), + module: String(raw.module ?? raw.resource ?? ''), + action: String(raw.action ?? ''), + resource: String(raw.resource ?? ''), + ip: String(raw.ip ?? ''), + userAgent: String(raw.user_agent ?? raw.userAgent ?? ''), + result, + message, + request, + response, + error, + createdAt: String(raw.createdAt ?? raw.created_at ?? ''), + }; +} + +/** Audit statistics shape from `auditApi.getStatistics()`. */ +export interface AuditStatistics { + total_logs?: number; + success_count?: number; + failure_count?: number; + today_count?: number; + top_modules?: { module: string; count: number }[]; + top_actions?: { action: string; count: number }[]; + top_users?: { username: string; count: number }[]; +}