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 }[]; +}