forked from baron/baron-sso
feat: apply sticky header and inner scroll to audit logs page
This commit is contained in:
@@ -158,8 +158,8 @@ function AuditLogsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
|
||||||
<header className="flex flex-wrap items-start justify-between gap-4">
|
<header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
||||||
<span>{t("ui.admin.audit.breadcrumb.section", "Audit")}</span>
|
<span>{t("ui.admin.audit.breadcrumb.section", "Audit")}</span>
|
||||||
@@ -194,409 +194,421 @@ function AuditLogsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<Card className="glass-panel flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||||
<Card className="glass-panel">
|
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<div>
|
||||||
<div>
|
<CardTitle>
|
||||||
<CardTitle>
|
{t("ui.admin.audit.registry.title", "Audit registry")}
|
||||||
{t("ui.admin.audit.registry.title", "Audit registry")}
|
</CardTitle>
|
||||||
</CardTitle>
|
<CardDescription>
|
||||||
<CardDescription>
|
{t("msg.admin.audit.registry.count", "로드된 로그 {{count}}건", {
|
||||||
{t(
|
count: logs.length,
|
||||||
"msg.admin.audit.registry.count",
|
})}
|
||||||
"로드된 로그 {{count}}건",
|
</CardDescription>
|
||||||
{ count: logs.length },
|
</div>
|
||||||
|
<Badge variant="muted">
|
||||||
|
{t("ui.common.badge.command_only", "Command only")}
|
||||||
|
</Badge>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
|
||||||
|
<div className="mb-4 flex flex-wrap items-center gap-2 flex-shrink-0">
|
||||||
|
<div className="flex flex-1 items-center gap-2 rounded-full border border-[var(--color-border)] bg-[rgba(255,255,255,0.02)] px-4 py-2 text-[var(--color-muted)]">
|
||||||
|
<Search size={14} />
|
||||||
|
<input
|
||||||
|
value={filterDraft}
|
||||||
|
onChange={(event) => setFilterDraft(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
handleAddFilter();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={t(
|
||||||
|
"ui.admin.audit.filters.placeholder",
|
||||||
|
"필터 추가 (예: status:failure)",
|
||||||
)}
|
)}
|
||||||
</CardDescription>
|
className="w-full bg-transparent text-sm text-foreground outline-none"
|
||||||
|
/>
|
||||||
|
<Button size="sm" variant="outline" onClick={handleAddFilter}>
|
||||||
|
{t("ui.common.add", "추가")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="muted">
|
{filters.length === 0 ? (
|
||||||
{t("ui.common.badge.command_only", "Command only")}
|
<span className="text-xs text-[var(--color-muted)]">
|
||||||
</Badge>
|
{t("msg.admin.audit.filters.empty", "필터 없음")}
|
||||||
</CardHeader>
|
</span>
|
||||||
<CardContent>
|
) : (
|
||||||
<div className="mb-4 flex flex-wrap items-center gap-2">
|
filters.map((filter) => (
|
||||||
<div className="flex flex-1 items-center gap-2 rounded-full border border-[var(--color-border)] bg-[rgba(255,255,255,0.02)] px-4 py-2 text-[var(--color-muted)]">
|
<span
|
||||||
<Search size={14} />
|
key={filter}
|
||||||
<input
|
className="inline-flex items-center gap-2 rounded-full border border-[var(--color-border)] bg-[rgba(255,255,255,0.04)] px-3 py-1 text-xs text-[var(--color-muted)]"
|
||||||
value={filterDraft}
|
>
|
||||||
onChange={(event) => setFilterDraft(event.target.value)}
|
<Terminal size={12} />
|
||||||
onKeyDown={(event) => {
|
{filter}
|
||||||
if (event.key === "Enter") {
|
<button
|
||||||
handleAddFilter();
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters((prev) =>
|
||||||
|
prev.filter((item) => item !== filter),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}}
|
className="inline-flex h-5 w-5 items-center justify-center rounded-full border border-[var(--color-border)] text-[10px] text-[var(--color-muted)]"
|
||||||
placeholder={t(
|
aria-label={t(
|
||||||
"ui.admin.audit.filters.placeholder",
|
"ui.admin.audit.filters.remove",
|
||||||
"필터 추가 (예: status:failure)",
|
"{{filter}} 필터 제거",
|
||||||
)}
|
{ filter },
|
||||||
className="w-full bg-transparent text-sm text-foreground outline-none"
|
)}
|
||||||
/>
|
|
||||||
<Button size="sm" variant="outline" onClick={handleAddFilter}>
|
|
||||||
{t("ui.common.add", "추가")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{filters.length === 0 ? (
|
|
||||||
<span className="text-xs text-[var(--color-muted)]">
|
|
||||||
{t("msg.admin.audit.filters.empty", "필터 없음")}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
filters.map((filter) => (
|
|
||||||
<span
|
|
||||||
key={filter}
|
|
||||||
className="inline-flex items-center gap-2 rounded-full border border-[var(--color-border)] bg-[rgba(255,255,255,0.04)] px-3 py-1 text-xs text-[var(--color-muted)]"
|
|
||||||
>
|
>
|
||||||
<Terminal size={12} />
|
×
|
||||||
{filter}
|
</button>
|
||||||
<button
|
</span>
|
||||||
type="button"
|
))
|
||||||
onClick={() =>
|
)}
|
||||||
setFilters((prev) =>
|
</div>
|
||||||
prev.filter((item) => item !== filter),
|
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
|
||||||
)
|
<div className="flex-1 overflow-auto relative custom-scrollbar">
|
||||||
}
|
<Table className="table-fixed">
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded-full border border-[var(--color-border)] text-[10px] text-[var(--color-muted)]"
|
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
|
||||||
aria-label={t(
|
|
||||||
"ui.admin.audit.filters.remove",
|
|
||||||
"{{filter}} 필터 제거",
|
|
||||||
{ filter },
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Table className="table-fixed">
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead className="w-[140px]">
|
|
||||||
{t("ui.admin.audit.table.time", "TIME")}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="w-[160px]">
|
|
||||||
{t("ui.admin.audit.table.actor", "ACTOR (ID)")}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>
|
|
||||||
{t("ui.admin.audit.table.request", "REQUEST")}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>
|
|
||||||
{t("ui.admin.audit.table.path", "PATH")}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="w-[120px]">
|
|
||||||
{t("ui.admin.audit.table.status", "STATUS")}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>
|
|
||||||
{t("ui.admin.audit.table.action_target", "Action / Target")}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="w-[80px]" />
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{isLoading && (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={7}>
|
<TableHead className="w-[140px]">
|
||||||
{t("msg.common.loading", "로딩 중...")}
|
{t("ui.admin.audit.table.time", "TIME")}
|
||||||
</TableCell>
|
</TableHead>
|
||||||
</TableRow>
|
<TableHead className="w-[160px]">
|
||||||
)}
|
{t("ui.admin.audit.table.actor", "ACTOR (ID)")}
|
||||||
{!isLoading && logs.length === 0 && (
|
</TableHead>
|
||||||
<TableRow>
|
<TableHead>
|
||||||
<TableCell colSpan={7}>
|
{t("ui.admin.audit.table.request", "REQUEST")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
{t("ui.admin.audit.table.path", "PATH")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="w-[120px]">
|
||||||
|
{t("ui.admin.audit.table.status", "STATUS")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
{t(
|
{t(
|
||||||
"msg.admin.audit.empty",
|
"ui.admin.audit.table.action_target",
|
||||||
"아직 수집된 감사 로그가 없습니다.",
|
"Action / Target",
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableHead>
|
||||||
|
<TableHead className="w-[80px]" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
</TableHeader>
|
||||||
{logs.map((row, index) => {
|
<TableBody>
|
||||||
const details = parseDetails(row.details);
|
{isLoading && (
|
||||||
const actionLabel =
|
<TableRow>
|
||||||
details.action ||
|
<TableCell colSpan={7}>
|
||||||
(details.method && details.path
|
{t("msg.common.loading", "로딩 중...")}
|
||||||
? `${details.method} ${details.path}`
|
</TableCell>
|
||||||
: row.event_type);
|
</TableRow>
|
||||||
const rowKey = `${row.event_id}-${row.timestamp}-${index}`;
|
)}
|
||||||
const isExpanded = Boolean(expandedRows[rowKey]);
|
{!isLoading && logs.length === 0 && (
|
||||||
return (
|
<TableRow>
|
||||||
<React.Fragment key={rowKey}>
|
<TableCell colSpan={7}>
|
||||||
<TableRow className="bg-card/40">
|
{t(
|
||||||
<TableCell className="text-xs text-[var(--color-muted)]">
|
"msg.admin.audit.empty",
|
||||||
{(() => {
|
"아직 수집된 감사 로그가 없습니다.",
|
||||||
const { date, time } = formatIsoDateTime(
|
)}
|
||||||
row.timestamp,
|
</TableCell>
|
||||||
);
|
</TableRow>
|
||||||
return (
|
)}
|
||||||
<div className="space-y-1">
|
{logs.map((row, index) => {
|
||||||
<div>{date}</div>
|
const details = parseDetails(row.details);
|
||||||
<div>{time}</div>
|
const actionLabel =
|
||||||
</div>
|
details.action ||
|
||||||
);
|
(details.method && details.path
|
||||||
})()}
|
? `${details.method} ${details.path}`
|
||||||
</TableCell>
|
: row.event_type);
|
||||||
<TableCell>
|
const rowKey = `${row.event_id}-${row.timestamp}-${index}`;
|
||||||
<div className="flex items-center gap-2">
|
const isExpanded = Boolean(expandedRows[rowKey]);
|
||||||
<code className="rounded-md bg-secondary/60 px-2 py-1 text-xs text-muted-foreground">
|
return (
|
||||||
{row.user_id || details.actor_id || "-"}
|
<React.Fragment key={rowKey}>
|
||||||
</code>
|
<TableRow className="bg-card/40">
|
||||||
{(row.user_id || details.actor_id) && (
|
<TableCell className="text-xs text-[var(--color-muted)]">
|
||||||
<Button
|
{(() => {
|
||||||
variant="ghost"
|
const { date, time } = formatIsoDateTime(
|
||||||
size="icon"
|
row.timestamp,
|
||||||
className="h-7 w-7 text-muted-foreground hover:text-primary"
|
);
|
||||||
aria-label={t(
|
return (
|
||||||
"ui.admin.audit.copy.actor_id",
|
<div className="space-y-1">
|
||||||
"Copy actor id",
|
<div>{date}</div>
|
||||||
)}
|
<div>{time}</div>
|
||||||
onClick={() =>
|
</div>
|
||||||
handleCopy(
|
);
|
||||||
row.user_id || details.actor_id || "",
|
})()}
|
||||||
)
|
</TableCell>
|
||||||
}
|
<TableCell>
|
||||||
>
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-xs text-[var(--color-muted)]">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<span className="break-all">
|
|
||||||
{formatCellValue(details.request_id)}
|
|
||||||
</span>
|
|
||||||
{details.request_id && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-7 w-7 text-muted-foreground hover:text-primary"
|
|
||||||
aria-label={t(
|
|
||||||
"ui.admin.audit.copy.request_id",
|
|
||||||
"Copy request id",
|
|
||||||
)}
|
|
||||||
onClick={() =>
|
|
||||||
handleCopy(details.request_id || "")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-xs text-[var(--color-muted)]">
|
|
||||||
<div className="font-semibold text-foreground">
|
|
||||||
{formatCellValue(details.method)}
|
|
||||||
</div>
|
|
||||||
<div className="break-all">
|
|
||||||
{formatCellValue(details.path)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
row.status === "success" || row.status === "ok"
|
|
||||||
? "success"
|
|
||||||
: "warning"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{row.status}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-xs text-[var(--color-muted)]">
|
|
||||||
<div className="font-semibold text-foreground">
|
|
||||||
{actionLabel}
|
|
||||||
</div>
|
|
||||||
{details.target && (
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="break-all">
|
<code className="rounded-md bg-secondary/60 px-2 py-1 text-xs text-muted-foreground">
|
||||||
{t(
|
{row.user_id || details.actor_id || "-"}
|
||||||
"ui.admin.audit.target",
|
</code>
|
||||||
"Target · {{target}}",
|
{(row.user_id || details.actor_id) && (
|
||||||
{
|
<Button
|
||||||
target: details.target,
|
variant="ghost"
|
||||||
},
|
size="icon"
|
||||||
)}
|
className="h-7 w-7 text-muted-foreground hover:text-primary"
|
||||||
</span>
|
aria-label={t(
|
||||||
<Button
|
"ui.admin.audit.copy.actor_id",
|
||||||
variant="ghost"
|
"Copy actor id",
|
||||||
size="icon"
|
|
||||||
className="h-7 w-7 text-muted-foreground hover:text-primary"
|
|
||||||
aria-label={t(
|
|
||||||
"ui.admin.audit.copy.target",
|
|
||||||
"Copy target",
|
|
||||||
)}
|
|
||||||
onClick={() => handleCopy(details.target || "")}
|
|
||||||
>
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
setExpandedRows((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[rowKey]: !isExpanded,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isExpanded ? (
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{isExpanded && (
|
|
||||||
<TableRow className="bg-card/20">
|
|
||||||
<TableCell colSpan={7} className="text-xs">
|
|
||||||
<div className="grid gap-4 text-[var(--color-muted)] md:grid-cols-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="uppercase tracking-[0.16em]">
|
|
||||||
{t(
|
|
||||||
"ui.admin.audit.details.request",
|
|
||||||
"Request",
|
|
||||||
)}
|
)}
|
||||||
</div>
|
onClick={() =>
|
||||||
<div className="break-all">
|
handleCopy(
|
||||||
{t(
|
row.user_id || details.actor_id || "",
|
||||||
"ui.admin.audit.details.request_id",
|
)
|
||||||
"Request ID · {{value}}",
|
}
|
||||||
{
|
>
|
||||||
value: formatCellValue(
|
<Copy className="h-3 w-3" />
|
||||||
details.request_id,
|
</Button>
|
||||||
),
|
)}
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="break-all">
|
|
||||||
{t(
|
|
||||||
"ui.admin.audit.details.event_id",
|
|
||||||
"Event ID · {{value}}",
|
|
||||||
{
|
|
||||||
value: formatCellValue(row.event_id),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{t(
|
|
||||||
"ui.admin.audit.details.ip",
|
|
||||||
"IP · {{value}}",
|
|
||||||
{
|
|
||||||
value: formatCellValue(row.ip_address),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{t(
|
|
||||||
"ui.admin.audit.details.latency",
|
|
||||||
"Latency · {{value}}",
|
|
||||||
{
|
|
||||||
value:
|
|
||||||
details.latency_ms !== undefined
|
|
||||||
? `${details.latency_ms}ms`
|
|
||||||
: "-",
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="uppercase tracking-[0.16em]">
|
|
||||||
{t("ui.admin.audit.details.actor", "Actor")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{t(
|
|
||||||
"ui.admin.audit.details.actor_id",
|
|
||||||
"Actor ID · {{value}}",
|
|
||||||
{
|
|
||||||
value:
|
|
||||||
row.user_id || details.actor_id || "-",
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{t(
|
|
||||||
"ui.admin.audit.details.tenant",
|
|
||||||
"Tenant · {{value}}",
|
|
||||||
{
|
|
||||||
value: formatCellValue(details.tenant_id),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{t(
|
|
||||||
"ui.admin.audit.details.device",
|
|
||||||
"Device · {{value}}",
|
|
||||||
{
|
|
||||||
value: formatCellValue(row.device_id),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="uppercase tracking-[0.16em]">
|
|
||||||
{t("ui.admin.audit.details.result", "Result")}
|
|
||||||
</div>
|
|
||||||
<div className="break-all">
|
|
||||||
{t(
|
|
||||||
"ui.admin.audit.details.error",
|
|
||||||
"Error · {{value}}",
|
|
||||||
{
|
|
||||||
value: formatCellValue(details.error),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="break-all">
|
|
||||||
{t(
|
|
||||||
"ui.admin.audit.details.before",
|
|
||||||
"Before · {{value}}",
|
|
||||||
{
|
|
||||||
value: formatCellValue(details.before),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="break-all">
|
|
||||||
{t(
|
|
||||||
"ui.admin.audit.details.after",
|
|
||||||
"After · {{value}}",
|
|
||||||
{
|
|
||||||
value: formatCellValue(details.after),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="text-xs text-[var(--color-muted)]">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="break-all">
|
||||||
|
{formatCellValue(details.request_id)}
|
||||||
|
</span>
|
||||||
|
{details.request_id && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 text-muted-foreground hover:text-primary"
|
||||||
|
aria-label={t(
|
||||||
|
"ui.admin.audit.copy.request_id",
|
||||||
|
"Copy request id",
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
handleCopy(details.request_id || "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs text-[var(--color-muted)]">
|
||||||
|
<div className="font-semibold text-foreground">
|
||||||
|
{formatCellValue(details.method)}
|
||||||
|
</div>
|
||||||
|
<div className="break-all">
|
||||||
|
{formatCellValue(details.path)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
row.status === "success" || row.status === "ok"
|
||||||
|
? "success"
|
||||||
|
: "warning"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs text-[var(--color-muted)]">
|
||||||
|
<div className="font-semibold text-foreground">
|
||||||
|
{actionLabel}
|
||||||
|
</div>
|
||||||
|
{details.target && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="break-all">
|
||||||
|
{t(
|
||||||
|
"ui.admin.audit.target",
|
||||||
|
"Target · {{target}}",
|
||||||
|
{
|
||||||
|
target: details.target,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 text-muted-foreground hover:text-primary"
|
||||||
|
aria-label={t(
|
||||||
|
"ui.admin.audit.copy.target",
|
||||||
|
"Copy target",
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
handleCopy(details.target || "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setExpandedRows((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[rowKey]: !isExpanded,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
{isExpanded && (
|
||||||
</React.Fragment>
|
<TableRow className="bg-card/20">
|
||||||
);
|
<TableCell colSpan={7} className="text-xs">
|
||||||
})}
|
<div className="grid gap-4 text-[var(--color-muted)] md:grid-cols-3">
|
||||||
</TableBody>
|
<div className="space-y-1">
|
||||||
</Table>
|
<div className="uppercase tracking-[0.16em]">
|
||||||
<div className="pt-4 text-center">
|
{t(
|
||||||
{hasNextPage ? (
|
"ui.admin.audit.details.request",
|
||||||
<Button
|
"Request",
|
||||||
variant="outline"
|
)}
|
||||||
onClick={() => fetchNextPage()}
|
</div>
|
||||||
disabled={isFetchingNextPage}
|
<div className="break-all">
|
||||||
>
|
{t(
|
||||||
{isFetchingNextPage
|
"ui.admin.audit.details.request_id",
|
||||||
? t("msg.common.loading", "Loading...")
|
"Request ID · {{value}}",
|
||||||
: t("ui.admin.audit.load_more", "Load more")}
|
{
|
||||||
</Button>
|
value: formatCellValue(
|
||||||
) : (
|
details.request_id,
|
||||||
<span className="text-xs text-[var(--color-muted)]">
|
),
|
||||||
{t("msg.admin.audit.end", "End of audit feed")}
|
},
|
||||||
</span>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
<div className="break-all">
|
||||||
|
{t(
|
||||||
|
"ui.admin.audit.details.event_id",
|
||||||
|
"Event ID · {{value}}",
|
||||||
|
{
|
||||||
|
value: formatCellValue(row.event_id),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{t(
|
||||||
|
"ui.admin.audit.details.ip",
|
||||||
|
"IP · {{value}}",
|
||||||
|
{
|
||||||
|
value: formatCellValue(row.ip_address),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{t(
|
||||||
|
"ui.admin.audit.details.latency",
|
||||||
|
"Latency · {{value}}",
|
||||||
|
{
|
||||||
|
value:
|
||||||
|
details.latency_ms !== undefined
|
||||||
|
? `${details.latency_ms}ms`
|
||||||
|
: "-",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="uppercase tracking-[0.16em]">
|
||||||
|
{t("ui.admin.audit.details.actor", "Actor")}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{t(
|
||||||
|
"ui.admin.audit.details.actor_id",
|
||||||
|
"Actor ID · {{value}}",
|
||||||
|
{
|
||||||
|
value:
|
||||||
|
row.user_id ||
|
||||||
|
details.actor_id ||
|
||||||
|
"-",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{t(
|
||||||
|
"ui.admin.audit.details.tenant",
|
||||||
|
"Tenant · {{value}}",
|
||||||
|
{
|
||||||
|
value: formatCellValue(
|
||||||
|
details.tenant_id,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{t(
|
||||||
|
"ui.admin.audit.details.device",
|
||||||
|
"Device · {{value}}",
|
||||||
|
{
|
||||||
|
value: formatCellValue(row.device_id),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="uppercase tracking-[0.16em]">
|
||||||
|
{t(
|
||||||
|
"ui.admin.audit.details.result",
|
||||||
|
"Result",
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="break-all">
|
||||||
|
{t(
|
||||||
|
"ui.admin.audit.details.error",
|
||||||
|
"Error · {{value}}",
|
||||||
|
{
|
||||||
|
value: formatCellValue(details.error),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="break-all">
|
||||||
|
{t(
|
||||||
|
"ui.admin.audit.details.before",
|
||||||
|
"Before · {{value}}",
|
||||||
|
{
|
||||||
|
value: formatCellValue(details.before),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="break-all">
|
||||||
|
{t(
|
||||||
|
"ui.admin.audit.details.after",
|
||||||
|
"After · {{value}}",
|
||||||
|
{
|
||||||
|
value: formatCellValue(details.after),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
<div className="pt-4 text-center flex-shrink-0">
|
||||||
</div>
|
{hasNextPage ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
disabled={isFetchingNextPage}
|
||||||
|
>
|
||||||
|
{isFetchingNextPage
|
||||||
|
? t("msg.common.loading", "Loading...")
|
||||||
|
: t("ui.admin.audit.load_more", "Load more")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-[var(--color-muted)]">
|
||||||
|
{t("msg.admin.audit.end", "End of audit feed")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user