refactor(quality): 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✓ (17.34s), go build=0, 17/17 测试通过
This commit is contained in:
1 parent
195b6945b4
commit
6c828c5992
6 files changed
+241
-256
No files matched your search
@@ -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 🟡 中优先级
|
||||
|
||||
|
||||
@@ -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** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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<string, unknown>): 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<OperationLog | null>(null);
|
||||
|
||||
// Statistics modal
|
||||
const [statsOpen, setStatsOpen] = useState(false);
|
||||
const [statistics, setStatistics] = useState<AuditStatistics | null>(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 */}
|
||||
<LogDetailDialog log={detailLog} onClose={() => setDetailLog(null)} />
|
||||
|
||||
{/* Statistics Modal */}
|
||||
<StatisticsDialog
|
||||
open={statsOpen}
|
||||
onClose={() => setStatsOpen(false)}
|
||||
loading={statsLoading}
|
||||
stats={statistics}
|
||||
/>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Log Detail Dialog
|
||||
// =========================================================================
|
||||
|
||||
function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
if (!log) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
||||
<div className="relative dg-card rounded-lg p-6 max-w-2xl w-full mx-4 shadow-2xl max-h-[85vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--dg-text-primary)]">{t('audit.operations.detailTitle')}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 dg-text-secondary hover:text-[var(--dg-text-primary)]"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-sm">
|
||||
<DetailRow label="ID" value={log.id} />
|
||||
<DetailRow label={t('audit.operations.col.time')} value={log.createdAt ? new Date(log.createdAt).toLocaleString() : '--'} />
|
||||
<DetailRow label={t('audit.operations.col.user')} value={log.username || '--'} />
|
||||
<DetailRow label={t('audit.operations.detailCol.userId')} value={log.userId || '--'} />
|
||||
<DetailRow label={t('audit.operations.col.module')} value={log.module || '--'} />
|
||||
<DetailRow label={t('audit.operations.col.actionType')} value={log.action || '--'} />
|
||||
<DetailRow label={t('audit.operations.col.resource')} value={log.resource || '--'} />
|
||||
<DetailRow label={t('audit.operations.detailCol.ipAddress')} value={log.ip || '--'} />
|
||||
<DetailRow label={t('audit.operations.detailCol.userAgent')} value={log.userAgent || '--'} />
|
||||
<DetailRow
|
||||
label={t('audit.operations.col.result')}
|
||||
value={
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
log.result === 'success'
|
||||
? 'border-emerald-500/30 dg-text-green bg-emerald-500/10'
|
||||
: 'border-red-500/30 dg-text-red bg-red-500/10'
|
||||
}
|
||||
>
|
||||
{log.result === 'success' ? t('common.success') : t('common.failed')}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
{log.request && <DetailRow label={t('audit.operations.detailCol.request', { defaultValue: '请求参数' })} value={log.request} mono />}
|
||||
{log.response && <DetailRow label={t('audit.operations.detailCol.response', { defaultValue: '执行输出' })} value={log.response} mono />}
|
||||
{log.error && <DetailRow label={t('audit.operations.detailCol.error', { defaultValue: '错误信息' })} value={log.error} mono />}
|
||||
{!log.request && !log.response && log.message && (
|
||||
<DetailRow label={t('audit.operations.detailCol.message')} value={log.message} mono />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="dg-btn-secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailRow({ label, value, mono }: { label: string; value: React.ReactNode; mono?: boolean }) {
|
||||
return (
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="dg-text-muted w-20 flex-shrink-0">{label}</span>
|
||||
<span className={mono ? 'text-[var(--dg-text-secondary)] font-mono text-xs break-all' : 'text-[var(--dg-text-primary)] break-all'}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
||||
<div className="relative dg-card rounded-lg p-6 max-w-md w-full mx-4 shadow-2xl">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--dg-text-primary)]">{t('audit.operations.statsTitle')}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 dg-text-secondary hover:text-[var(--dg-text-primary)]"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-10">
|
||||
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : stats ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<StatCard label={t('audit.operations.stats.totalLogs')} value={String(stats.total_logs ?? '--')} color="blue" />
|
||||
<StatCard label={t('audit.operations.stats.todayLogs')} value={String(stats.today_count ?? '--')} color="emerald" />
|
||||
<StatCard label={t('audit.operations.stats.successCount')} value={String(stats.success_count ?? '--')} color="emerald" />
|
||||
<StatCard label={t('audit.operations.stats.failureCount')} value={String(stats.failure_count ?? '--')} color="red" />
|
||||
</div>
|
||||
|
||||
{stats.top_modules && stats.top_modules.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold dg-text-secondary mb-2">{t('audit.operations.topModules')}</h4>
|
||||
<div className="space-y-1">
|
||||
{stats.top_modules.slice(0, 5).map((m, i) => (
|
||||
<div key={i} className="flex justify-between text-xs">
|
||||
<span className="text-[var(--dg-text-secondary)]">{m.module}</span>
|
||||
<span className="dg-text-muted">{m.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stats.top_users && stats.top_users.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold dg-text-secondary mb-2">{t('audit.operations.topUsers')}</h4>
|
||||
<div className="space-y-1">
|
||||
{stats.top_users.slice(0, 5).map((u, i) => (
|
||||
<div key={i} className="flex justify-between text-xs">
|
||||
<span className="text-[var(--dg-text-secondary)]">{u.username}</span>
|
||||
<span className="dg-text-muted">{u.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="dg-text-muted text-center py-10 text-sm">{t('audit.operations.noStats')}</p>
|
||||
)}
|
||||
|
||||
<div className="mt-5 flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="dg-btn-secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={`rounded-lg border p-3 ${colorMap[color]}`}>
|
||||
<div className="text-xs dg-text-muted">{label}</div>
|
||||
<div className="text-xl font-bold mt-0.5">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
||||
<div className="relative dg-card rounded-lg p-6 max-w-2xl w-full mx-4 shadow-2xl max-h-[85vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--dg-text-primary)]">{t('audit.operations.detailTitle')}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 dg-text-secondary hover:text-[var(--dg-text-primary)]"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-sm">
|
||||
<DetailRow label="ID" value={log.id} />
|
||||
<DetailRow label={t('audit.operations.col.time')} value={log.createdAt ? new Date(log.createdAt).toLocaleString() : '--'} />
|
||||
<DetailRow label={t('audit.operations.col.user')} value={log.username || '--'} />
|
||||
<DetailRow label={t('audit.operations.detailCol.userId')} value={log.userId || '--'} />
|
||||
<DetailRow label={t('audit.operations.col.module')} value={log.module || '--'} />
|
||||
<DetailRow label={t('audit.operations.col.actionType')} value={log.action || '--'} />
|
||||
<DetailRow label={t('audit.operations.col.resource')} value={log.resource || '--'} />
|
||||
<DetailRow label={t('audit.operations.detailCol.ipAddress')} value={log.ip || '--'} />
|
||||
<DetailRow label={t('audit.operations.detailCol.userAgent')} value={log.userAgent || '--'} />
|
||||
<DetailRow
|
||||
label={t('audit.operations.col.result')}
|
||||
value={
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
log.result === 'success'
|
||||
? 'border-emerald-500/30 dg-text-green bg-emerald-500/10'
|
||||
: 'border-red-500/30 dg-text-red bg-red-500/10'
|
||||
}
|
||||
>
|
||||
{log.result === 'success' ? t('common.success') : t('common.failed')}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
{log.request && <DetailRow label={t('audit.operations.detailCol.request', { defaultValue: '请求参数' })} value={log.request} mono />}
|
||||
{log.response && <DetailRow label={t('audit.operations.detailCol.response', { defaultValue: '执行输出' })} value={log.response} mono />}
|
||||
{log.error && <DetailRow label={t('audit.operations.detailCol.error', { defaultValue: '错误信息' })} value={log.error} mono />}
|
||||
{!log.request && !log.response && log.message && (
|
||||
<DetailRow label={t('audit.operations.detailCol.message')} value={log.message} mono />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="dg-btn-secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailRow({ label, value, mono }: { label: string; value: ReactNode; mono?: boolean }) {
|
||||
return (
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="dg-text-muted w-20 flex-shrink-0">{label}</span>
|
||||
<span className={mono ? 'text-[var(--dg-text-secondary)] font-mono text-xs break-all' : 'text-[var(--dg-text-primary)] break-all'}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
||||
<div className="relative dg-card rounded-lg p-6 max-w-md w-full mx-4 shadow-2xl">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--dg-text-primary)]">{t('audit.operations.statsTitle')}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 dg-text-secondary hover:text-[var(--dg-text-primary)]"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-10">
|
||||
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : stats ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<StatCard label={t('audit.operations.stats.totalLogs')} value={String(stats.total_logs ?? '--')} color="blue" />
|
||||
<StatCard label={t('audit.operations.stats.todayLogs')} value={String(stats.today_count ?? '--')} color="emerald" />
|
||||
<StatCard label={t('audit.operations.stats.successCount')} value={String(stats.success_count ?? '--')} color="emerald" />
|
||||
<StatCard label={t('audit.operations.stats.failureCount')} value={String(stats.failure_count ?? '--')} color="red" />
|
||||
</div>
|
||||
|
||||
{stats.top_modules && stats.top_modules.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold dg-text-secondary mb-2">{t('audit.operations.topModules')}</h4>
|
||||
<div className="space-y-1">
|
||||
{stats.top_modules.slice(0, 5).map((m, i) => (
|
||||
<div key={i} className="flex justify-between text-xs">
|
||||
<span className="text-[var(--dg-text-secondary)]">{m.module}</span>
|
||||
<span className="dg-text-muted">{m.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stats.top_users && stats.top_users.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold dg-text-secondary mb-2">{t('audit.operations.topUsers')}</h4>
|
||||
<div className="space-y-1">
|
||||
{stats.top_users.slice(0, 5).map((u, i) => (
|
||||
<div key={i} className="flex justify-between text-xs">
|
||||
<span className="text-[var(--dg-text-secondary)]">{u.username}</span>
|
||||
<span className="dg-text-muted">{u.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="dg-text-muted text-center py-10 text-sm">{t('audit.operations.noStats')}</p>
|
||||
)}
|
||||
|
||||
<div className="mt-5 flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="dg-btn-secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={`rounded-lg border p-3 ${colorMap[color]}`}>
|
||||
<div className="text-xs dg-text-muted">{label}</div>
|
||||
<div className="text-xl font-bold mt-0.5">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
|
||||
/** 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 }[];
|
||||
}
|
||||
Reference in new issue
Block a user