import { AuditAction, type AuditEvent } from "@umt/contracts"; import { useEffect, useState } from "react"; import { useSearchParams } from "react-router"; import { useAuditEvents } from "~/api/audit.ts"; import { DataTable } from "~/components/console/data-table.tsx"; import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx"; import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx"; import { JsonView } from "~/components/console/json-view.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { PageHeader } from "~/components/console/page-header.tsx"; import { formatRelative } from "~/lib/labels.ts"; import { formatDateTime } from "~/lib/utils.ts"; export const meta = () => [{ title: "审计日志 · UHD 媒体自动化控制台" }]; const ACTOR_TYPES = [ { value: "user", label: "管理员" }, { value: "api_key", label: "API Key" }, { value: "agent", label: "Agent" }, { value: "system", label: "系统" }, ]; const actorLabel = (t: string) => ACTOR_TYPES.find((a) => a.value === t)?.label ?? t; const ACTION_LABEL: Record = { "admin.setup": "初始化管理员", "admin.login_success": "登录", "admin.logout": "登出", "admin.password_change": "修改密码", "admin.totp_enable": "启用二次验证", "admin.totp_disable": "停用二次验证", "api_key.create": "创建 API Key", "api_key.update": "修改 API Key", "api_key.revoke": "撤销 API Key", "settings.update": "修改设置", "secret.create": "新建凭证", "secret.rotate": "轮换凭证", "secret.delete": "删除凭证", "site_account.credentials_set": "录入站点凭证", "engine.run_cancel": "取消引擎 run", }; const VERB: Record = { create: "新建", update: "修改", delete: "删除", test: "测试", verify: "校验" }; const NOUN: Record = { library_root: "库根", downloader_instance: "下载器", media_server_instance: "媒体服务器", path_mapping: "路径映射", }; function actionLabel(a: string): string { if (ACTION_LABEL[a]) return ACTION_LABEL[a]; const [noun, verb] = a.split("."); if (noun && verb && NOUN[noun] && VERB[verb]) return `${VERB[verb]}${NOUN[noun]}`; return a; } // 前缀组 + 具体动作 const ACTION_OPTIONS = [ ...[ "admin", "api_key", "settings", "secret", "library_root", "downloader_instance", "media_server_instance", "path_mapping", ].map((p) => ({ value: `${p}.*`, label: `${p}.*(全部)` })), ...AuditAction.options.map((a) => ({ value: a, label: `${actionLabel(a)} · ${a}` })), ]; export default function AuditPage() { const [params, setParams] = useSearchParams(); const actorType = params.get("actorType") ?? ""; const action = params.get("action") ?? ""; const [target, setTarget] = useState(params.get("targetType") ?? ""); const [targetType, setTargetType] = useState(target); const [selected, setSelected] = useState(null); useEffect(() => { const t = setTimeout(() => { const v = target.trim(); setTargetType(v); setParams( (p) => { if (v) p.set("targetType", v); else p.delete("targetType"); return p; }, { replace: true }, ); }, 350); return () => clearTimeout(t); }, [target, setParams]); const setParam = (k: string, v: string) => setParams( (p) => { if (v) p.set(k, v); else p.delete(k); return p; }, { replace: true }, ); const q = useAuditEvents({ actorType: actorType || undefined, action: action || undefined, targetType: targetType || undefined, }); const rows = q.data?.pages.flatMap((p) => p.items); const filtered = !!(actorType || action || targetType); return ( <> { setTarget(""); setParams({}, { replace: true }); } : undefined } > setParam("actorType", v)} options={ACTOR_TYPES} /> setParam("action", v)} options={ACTION_OPTIONS} /> rows={rows} rowKey={(r) => r.id} loading={q.isPending} error={q.error} onRetry={() => q.refetch()} empty={{ title: filtered ? "没有符合条件的审计事件" : "还没有审计事件", description: filtered ? "换个筛选条件试试。" : undefined, }} onRowClick={setSelected} selectedKey={selected?.id ?? null} hasMore={!!q.hasNextPage} onLoadMore={() => q.fetchNextPage()} loadingMore={q.isFetchingNextPage} columns={[ { key: "at", header: "时间", cell: (r) => ( {formatRelative(r.at)} ), }, { key: "action", header: "动作", cell: (r) => (

{actionLabel(r.action)}

{r.action}

), }, { key: "actor", header: "操作者", hideBelow: "sm", cell: (r) => actorLabel(r.actorType) }, { key: "target", header: "对象", hideBelow: "md", cell: (r) => r.targetType ? (

{r.targetType}

{r.targetId}

) : ( "—" ), }, ]} /> !o && setSelected(null)} title={selected ? actionLabel(selected.action) : ""} description={selected ? formatDateTime(selected.at) : undefined} > {selected ? ( <> ) : null} ); }