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
@@ -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