1
0
forked from baron/baron-sso

감사 로그 테이블 공통 컬럼 통일

This commit is contained in:
2026-05-15 13:26:08 +09:00
parent 94f33a0a64
commit 055a804f7f
3 changed files with 255 additions and 254 deletions

View File

@@ -32,6 +32,14 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import {
formatAuditDateParts,
formatAuditValue,
parseAuditDetails,
resolveAuditAction,
resolveAuditActor,
resolveAuditTarget,
} from "../../../../common/core/audit";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar"; import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { PageHeader } from "../../../../common/core/components/page"; import { PageHeader } from "../../../../common/core/components/page";
import type { AuditLog } from "../../lib/adminApi"; import type { AuditLog } from "../../lib/adminApi";
@@ -44,61 +52,6 @@ const defaultAuditFilters = [
"latency_ms:>1000", "latency_ms:>1000",
]; ];
type AuditDetails = {
request_id?: string;
method?: string;
path?: string;
status?: number;
latency_ms?: number;
error?: string;
tenant_id?: string;
actor_id?: string;
action?: string;
target?: string;
before?: unknown;
after?: unknown;
};
function parseDetails(details?: string): AuditDetails {
if (!details) {
return {};
}
try {
const parsed = JSON.parse(details);
if (parsed && typeof parsed === "object") {
return parsed as AuditDetails;
}
} catch {}
return {};
}
function formatCellValue(value: unknown) {
if (value === null || value === undefined || value === "") {
return "-";
}
if (typeof value === "string") {
return value;
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function formatIsoDateTime(value: string) {
if (!value) {
return { date: "-", time: "-" };
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return { date: value, time: "-" };
}
const date = parsed.toISOString().slice(0, 10);
const time = parsed.toLocaleTimeString("ko-KR", { hour12: false });
return { date, time };
}
function AuditLogsPage() { function AuditLogsPage() {
const [filters, setFilters] = React.useState(defaultAuditFilters); const [filters, setFilters] = React.useState(defaultAuditFilters);
const [filterDraft, setFilterDraft] = React.useState(""); const [filterDraft, setFilterDraft] = React.useState("");
@@ -146,7 +99,7 @@ function AuditLogsPage() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="p-8 text-center"> <div className="p-8 text-center">
{t("msg.admin.audit.loading", "Loading audit logs...")} {t("msg.common.audit.loading", "Loading audit logs...")}
</div> </div>
); );
} }
@@ -157,7 +110,7 @@ function AuditLogsPage() {
(error as Error).message; (error as Error).message;
return ( return (
<div className="p-8 text-center text-red-500"> <div className="p-8 text-center text-red-500">
{t("msg.admin.audit.load_error", "Error loading logs: {{error}}", { {t("msg.common.audit.load_error", "Error loading logs: {{error}}", {
error: errMsg, error: errMsg,
})} })}
</div> </div>
@@ -169,7 +122,7 @@ function AuditLogsPage() {
<PageHeader <PageHeader
sticky sticky
titleAs="h2" titleAs="h2"
title={t("ui.admin.audit.title", "감사 로그")} title={t("ui.common.audit.title", "감사 로그")}
description={t( description={t(
"msg.admin.audit.subtitle", "msg.admin.audit.subtitle",
"Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.", "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.",
@@ -196,7 +149,7 @@ function AuditLogsPage() {
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0"> <CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
<div> <div>
<CardTitle> <CardTitle>
{t("ui.admin.audit.registry.title", "Log Registry")} {t("ui.common.audit.registry.title", "Audit registry")}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
{t("msg.admin.audit.registry.count", "총 {{count}}개 로그", { {t("msg.admin.audit.registry.count", "총 {{count}}개 로그", {
@@ -274,25 +227,15 @@ function AuditLogsPage() {
<TableHeader className={commonStickyTableHeaderClass}> <TableHeader className={commonStickyTableHeaderClass}>
<TableRow> <TableRow>
<TableHead className="w-[140px]"> <TableHead className="w-[140px]">
{t("ui.admin.audit.table.time", "TIME")} {t("ui.common.audit.table.time", "시간")}
</TableHead> </TableHead>
<TableHead className="w-[160px]"> <TableHead className="w-[160px]">
{t("ui.admin.audit.table.actor", "ACTOR (ID)")} {t("ui.common.audit.table.actor", "수행자")}
</TableHead>
<TableHead>
{t("ui.admin.audit.table.request", "REQUEST")}
</TableHead>
<TableHead>
{t("ui.admin.audit.table.path", "PATH")}
</TableHead> </TableHead>
<TableHead>{t("ui.common.audit.table.action", "액션")}</TableHead>
<TableHead>{t("ui.common.audit.table.target", "대상")}</TableHead>
<TableHead className="w-[120px]"> <TableHead className="w-[120px]">
{t("ui.admin.audit.table.status", "STATUS")} {t("ui.common.audit.table.status", "상태")}
</TableHead>
<TableHead>
{t(
"ui.admin.audit.table.action_target",
"Action / Target",
)}
</TableHead> </TableHead>
<TableHead className="w-[80px]" /> <TableHead className="w-[80px]" />
</TableRow> </TableRow>
@@ -300,28 +243,26 @@ function AuditLogsPage() {
<TableBody> <TableBody>
{isLoading && ( {isLoading && (
<TableRow> <TableRow>
<TableCell colSpan={7}> <TableCell colSpan={6}>
{t("msg.common.loading", "로딩 중...")} {t("msg.common.loading", "로딩 중...")}
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
{!isLoading && logs.length === 0 && ( {!isLoading && logs.length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={7}> <TableCell colSpan={6}>
{t( {t(
"msg.admin.audit.empty", "msg.common.audit.empty",
"아직 수집된 감사 로그가 없습니다.", "아직 수집된 감사 로그가 없습니다.",
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
{logs.map((row, index) => { {logs.map((row, index) => {
const details = parseDetails(row.details); const details = parseAuditDetails(row.details);
const actionLabel = const actionLabel = resolveAuditAction(row, details);
details.action || const actorLabel = resolveAuditActor(row, details);
(details.method && details.path const targetLabel = resolveAuditTarget(details);
? `${details.method} ${details.path}`
: row.event_type);
const rowKey = `${row.event_id}-${row.timestamp}-${index}`; const rowKey = `${row.event_id}-${row.timestamp}-${index}`;
const isExpanded = Boolean(expandedRows[rowKey]); const isExpanded = Boolean(expandedRows[rowKey]);
return ( return (
@@ -329,7 +270,7 @@ function AuditLogsPage() {
<TableRow className="bg-card/40"> <TableRow className="bg-card/40">
<TableCell className="text-xs text-[var(--color-muted)]"> <TableCell className="text-xs text-[var(--color-muted)]">
{(() => { {(() => {
const { date, time } = formatIsoDateTime( const { date, time } = formatAuditDateParts(
row.timestamp, row.timestamp,
); );
return ( return (
@@ -343,45 +284,18 @@ function AuditLogsPage() {
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="rounded-md bg-secondary/60 px-2 py-1 text-xs text-muted-foreground"> <code className="rounded-md bg-secondary/60 px-2 py-1 text-xs text-muted-foreground">
{row.user_id || details.actor_id || "-"} {actorLabel}
</code> </code>
{(row.user_id || details.actor_id) && ( {actorLabel !== "-" && (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-muted-foreground hover:text-primary" className="h-7 w-7 text-muted-foreground hover:text-primary"
aria-label={t( aria-label={t(
"ui.admin.audit.copy.actor_id", "ui.common.audit.copy.actor_id",
"Copy actor id", "Copy actor id",
)} )}
onClick={() => onClick={() => handleCopy(actorLabel)}
handleCopy(
row.user_id || details.actor_id || "",
)
}
>
<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" /> <Copy className="h-3 w-3" />
</Button> </Button>
@@ -390,10 +304,26 @@ function AuditLogsPage() {
</TableCell> </TableCell>
<TableCell className="text-xs text-[var(--color-muted)]"> <TableCell className="text-xs text-[var(--color-muted)]">
<div className="font-semibold text-foreground"> <div className="font-semibold text-foreground">
{formatCellValue(details.method)} {actionLabel}
</div> </div>
<div className="break-all"> </TableCell>
{formatCellValue(details.path)} <TableCell className="text-xs text-[var(--color-muted)]">
<div className="flex items-center gap-2">
<span className="break-all">{targetLabel}</span>
{targetLabel !== "-" && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground hover:text-primary"
aria-label={t(
"ui.common.audit.copy.target",
"Copy target",
)}
onClick={() => handleCopy(targetLabel)}
>
<Copy className="h-3 w-3" />
</Button>
)}
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -407,38 +337,6 @@ function AuditLogsPage() {
{row.status} {row.status}
</Badge> </Badge>
</TableCell> </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"> <TableCell className="text-right">
<Button <Button
variant="ghost" variant="ghost"
@@ -460,21 +358,21 @@ function AuditLogsPage() {
</TableRow> </TableRow>
{isExpanded && ( {isExpanded && (
<TableRow className="bg-card/20"> <TableRow className="bg-card/20">
<TableCell colSpan={7} className="text-xs"> <TableCell colSpan={6} className="text-xs">
<div className="grid gap-4 text-[var(--color-muted)] md:grid-cols-3"> <div className="grid gap-4 text-[var(--color-muted)] md:grid-cols-3">
<div className="space-y-1"> <div className="space-y-1">
<div className="uppercase tracking-[0.16em]"> <div className="uppercase tracking-[0.16em]">
{t( {t(
"ui.admin.audit.details.request", "ui.common.audit.details.request",
"Request", "Request",
)} )}
</div> </div>
<div className="break-all"> <div className="break-all">
{t( {t(
"ui.admin.audit.details.request_id", "ui.common.audit.details.request_id",
"Request ID · {{value}}", "Request ID · {{value}}",
{ {
value: formatCellValue( value: formatAuditValue(
details.request_id, details.request_id,
), ),
}, },
@@ -482,25 +380,31 @@ function AuditLogsPage() {
</div> </div>
<div className="break-all"> <div className="break-all">
{t( {t(
"ui.admin.audit.details.event_id", "ui.common.audit.details.event_id",
"Event ID · {{value}}", "Event ID · {{value}}",
{ {
value: formatCellValue(row.event_id), value: formatAuditValue(row.event_id),
}, },
)} )}
</div> </div>
<div> <div>
{t( {t(
"ui.admin.audit.details.ip", "ui.common.audit.details.ip",
"IP · {{value}}", "IP · {{value}}",
{ {
value: formatCellValue(row.ip_address), value: formatAuditValue(row.ip_address),
}, },
)} )}
</div> </div>
<div className="break-all">
Method · {formatAuditValue(details.method)}
</div>
<div className="break-all">
Path · {formatAuditValue(details.path)}
</div>
<div> <div>
{t( {t(
"ui.admin.audit.details.latency", "ui.common.audit.details.latency",
"Latency · {{value}}", "Latency · {{value}}",
{ {
value: value:
@@ -513,26 +417,23 @@ function AuditLogsPage() {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="uppercase tracking-[0.16em]"> <div className="uppercase tracking-[0.16em]">
{t("ui.admin.audit.details.actor", "Actor")} {t("ui.common.audit.details.actor", "Actor")}
</div> </div>
<div> <div>
{t( {t(
"ui.admin.audit.details.actor_id", "ui.common.audit.details.actor_id",
"Actor ID · {{value}}", "Actor ID · {{value}}",
{ {
value: value: actorLabel,
row.user_id ||
details.actor_id ||
"-",
}, },
)} )}
</div> </div>
<div> <div>
{t( {t(
"ui.admin.audit.details.tenant", "ui.common.audit.details.tenant",
"Tenant · {{value}}", "Tenant · {{value}}",
{ {
value: formatCellValue( value: formatAuditValue(
details.tenant_id, details.tenant_id,
), ),
}, },
@@ -540,45 +441,48 @@ function AuditLogsPage() {
</div> </div>
<div> <div>
{t( {t(
"ui.admin.audit.details.device", "ui.common.audit.details.device",
"Device · {{value}}", "Device · {{value}}",
{ {
value: formatCellValue(row.device_id), value: formatAuditValue(row.device_id),
}, },
)} )}
</div> </div>
<div className="break-all">
Target · {targetLabel}
</div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="uppercase tracking-[0.16em]"> <div className="uppercase tracking-[0.16em]">
{t( {t(
"ui.admin.audit.details.result", "ui.common.audit.details.result",
"Result", "Result",
)} )}
</div> </div>
<div className="break-all"> <div className="break-all">
{t( {t(
"ui.admin.audit.details.error", "ui.common.audit.details.error",
"Error · {{value}}", "Error · {{value}}",
{ {
value: formatCellValue(details.error), value: formatAuditValue(details.error),
}, },
)} )}
</div> </div>
<div className="break-all"> <div className="break-all">
{t( {t(
"ui.admin.audit.details.before", "ui.common.audit.details.before",
"Before · {{value}}", "Before · {{value}}",
{ {
value: formatCellValue(details.before), value: formatAuditValue(details.before),
}, },
)} )}
</div> </div>
<div className="break-all"> <div className="break-all">
{t( {t(
"ui.admin.audit.details.after", "ui.common.audit.details.after",
"After · {{value}}", "After · {{value}}",
{ {
value: formatCellValue(details.after), value: formatAuditValue(details.after),
}, },
)} )}
</div> </div>
@@ -603,7 +507,7 @@ function AuditLogsPage() {
> >
{isFetchingNextPage {isFetchingNextPage
? t("msg.common.loading", "Loading...") ? t("msg.common.loading", "Loading...")
: t("ui.admin.audit.load_more", "Load more")} : t("ui.common.audit.load_more", "더 보기")}
</Button> </Button>
) : ( ) : (
<span className="text-xs text-[var(--color-muted)]"> <span className="text-xs text-[var(--color-muted)]">

View File

@@ -0,0 +1,92 @@
export type CommonAuditLog = {
event_id: string;
timestamp: string;
user_id: string;
event_type: string;
status: string;
ip_address: string;
user_agent: string;
device_id?: string;
details?: string;
};
export type AuditDetails = {
request_id?: string;
method?: string;
path?: string;
status?: number;
latency_ms?: number;
error?: string;
tenant_id?: string;
actor_id?: string;
action?: string;
target?: string;
target_id?: string;
before?: unknown;
after?: unknown;
};
export function parseAuditDetails(details?: string): AuditDetails {
if (!details) {
return {};
}
try {
const parsed = JSON.parse(details);
if (parsed && typeof parsed === "object") {
return parsed as AuditDetails;
}
} catch {}
return {};
}
export function formatAuditValue(value: unknown) {
if (value === null || value === undefined || value === "") {
return "-";
}
if (typeof value === "string") {
return value;
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
export function formatAuditDateParts(value: string) {
if (!value) {
return { date: "-", time: "-" };
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return { date: value, time: "-" };
}
return {
date: parsed.toISOString().slice(0, 10),
time: parsed.toLocaleTimeString("ko-KR", { hour12: false }),
};
}
export function resolveAuditActor(
log: Pick<CommonAuditLog, "user_id">,
details: AuditDetails,
) {
return log.user_id || details.actor_id || "-";
}
export function resolveAuditAction(
log: Pick<CommonAuditLog, "event_type">,
details: AuditDetails,
) {
if (details.action) {
return details.action;
}
if (details.method && details.path) {
return `${details.method} ${details.path}`;
}
return log.event_type;
}
export function resolveAuditTarget(details: AuditDetails) {
return details.target || details.target_id || "-";
}

View File

@@ -30,59 +30,20 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import {
formatAuditDateParts,
formatAuditValue,
parseAuditDetails,
resolveAuditAction,
resolveAuditActor,
resolveAuditTarget,
} from "../../../../common/core/audit";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar"; import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { PageHeader } from "../../../../common/core/components/page"; import { PageHeader } from "../../../../common/core/components/page";
import type { DevAuditLog } from "../../lib/devApi"; import type { DevAuditLog } from "../../lib/devApi";
import { fetchDevAuditLogs } from "../../lib/devApi"; import { fetchDevAuditLogs } from "../../lib/devApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
type AuditDetails = {
request_id?: string;
method?: string;
path?: string;
tenant_id?: string;
action?: string;
target_id?: string;
before?: unknown;
after?: unknown;
error?: string;
};
function parseDetails(details?: string): AuditDetails {
if (!details) {
return {};
}
try {
const parsed = JSON.parse(details);
if (parsed && typeof parsed === "object") {
return parsed as AuditDetails;
}
} catch {}
return {};
}
function formatValue(value: unknown): string {
if (value === null || value === undefined || value === "") {
return "-";
}
if (typeof value === "string") {
return value;
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function formatDateTime(value: string): string {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return value;
}
return parsed.toLocaleString("ko-KR");
}
function toCsv(logs: DevAuditLog[]) { function toCsv(logs: DevAuditLog[]) {
const header = [ const header = [
"timestamp", "timestamp",
@@ -95,7 +56,7 @@ function toCsv(logs: DevAuditLog[]) {
"request_id", "request_id",
]; ];
const rows = logs.map((logItem) => { const rows = logs.map((logItem) => {
const details = parseDetails(logItem.details); const details = parseAuditDetails(logItem.details);
return [ return [
logItem.timestamp, logItem.timestamp,
logItem.user_id || "", logItem.user_id || "",
@@ -184,7 +145,7 @@ function AuditLogsPage() {
axiosError.response?.data?.error ?? (query.error as Error).message; axiosError.response?.data?.error ?? (query.error as Error).message;
return ( return (
<div className="p-8 text-center text-red-500"> <div className="p-8 text-center text-red-500">
{t("msg.dev.audit.load_error", "Error loading logs: {{error}}", { {t("msg.common.audit.load_error", "Error loading logs: {{error}}", {
error: errMsg, error: errMsg,
})} })}
</div> </div>
@@ -194,8 +155,8 @@ function AuditLogsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader <PageHeader
eyebrow={t("ui.dev.audit.registry.title", "Audit registry")} eyebrow={t("ui.common.audit.registry.title", "Audit registry")}
title={t("ui.dev.audit.title", "Audit Logs")} title={t("ui.common.audit.title", "Audit Logs")}
description={t( description={t(
"msg.dev.audit.subtitle", "msg.dev.audit.subtitle",
"Shows DevFront activity history within current tenant/app scope.", "Shows DevFront activity history within current tenant/app scope.",
@@ -291,19 +252,19 @@ function AuditLogsPage() {
<TableHeader className={commonStickyTableHeaderClass}> <TableHeader className={commonStickyTableHeaderClass}>
<TableRow> <TableRow>
<TableHead className="w-[190px]"> <TableHead className="w-[190px]">
{t("ui.dev.audit.table.time", "Time")} {t("ui.common.audit.table.time", "Time")}
</TableHead> </TableHead>
<TableHead className="w-[180px]"> <TableHead className="w-[180px]">
{t("ui.dev.audit.table.actor", "Actor")} {t("ui.common.audit.table.actor", "Actor")}
</TableHead> </TableHead>
<TableHead className="w-[180px]"> <TableHead className="w-[180px]">
{t("ui.dev.audit.table.action", "Action")} {t("ui.common.audit.table.action", "Action")}
</TableHead> </TableHead>
<TableHead className="w-[260px]"> <TableHead className="w-[260px]">
{t("ui.dev.audit.table.target", "Target")} {t("ui.common.audit.table.target", "Target")}
</TableHead> </TableHead>
<TableHead className="w-[120px]"> <TableHead className="w-[120px]">
{t("ui.dev.audit.table.status", "Status")} {t("ui.common.audit.table.status", "Status")}
</TableHead> </TableHead>
<TableHead className="w-[80px]" /> <TableHead className="w-[80px]" />
</TableRow> </TableRow>
@@ -315,7 +276,7 @@ function AuditLogsPage() {
colSpan={6} colSpan={6}
className="py-8 text-center text-muted-foreground" className="py-8 text-center text-muted-foreground"
> >
{t("msg.dev.audit.loading", "Loading audit logs...")} {t("msg.common.audit.loading", "Loading audit logs...")}
</TableCell> </TableCell>
</TableRow> </TableRow>
) : logs.length === 0 ? ( ) : logs.length === 0 ? (
@@ -324,31 +285,42 @@ function AuditLogsPage() {
colSpan={6} colSpan={6}
className="text-center text-muted-foreground" className="text-center text-muted-foreground"
> >
{t("msg.dev.audit.empty", "No audit logs found.")} {t("msg.common.audit.empty", "No audit logs found.")}
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
logs.map((row, index) => { logs.map((row, index) => {
const details = parseDetails(row.details); const details = parseAuditDetails(row.details);
const actionLabel = details.action || row.event_type; const actionLabel = resolveAuditAction(row, details);
const targetValue = details.target_id || "-"; const actorLabel = resolveAuditActor(row, details);
const targetValue = resolveAuditTarget(details);
const rowKey = `${row.event_id}-${row.timestamp}-${index}`; const rowKey = `${row.event_id}-${row.timestamp}-${index}`;
const expanded = Boolean(expandedRows[rowKey]); const expanded = Boolean(expandedRows[rowKey]);
return ( return (
<React.Fragment key={rowKey}> <React.Fragment key={rowKey}>
<TableRow> <TableRow>
<TableCell className="text-xs text-muted-foreground"> <TableCell className="text-xs text-muted-foreground">
{formatDateTime(row.timestamp)} {(() => {
const { date, time } = formatAuditDateParts(
row.timestamp,
);
return (
<div className="space-y-1">
<div>{date}</div>
<div>{time}</div>
</div>
);
})()}
</TableCell> </TableCell>
<TableCell className="font-mono text-xs"> <TableCell className="font-mono text-xs">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>{row.user_id || "-"}</span> <span>{actorLabel}</span>
{row.user_id ? ( {actorLabel !== "-" ? (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-muted-foreground" className="h-7 w-7 text-muted-foreground"
onClick={() => handleCopy(row.user_id)} onClick={() => handleCopy(actorLabel)}
> >
<Copy className="h-3 w-3" /> <Copy className="h-3 w-3" />
</Button> </Button>
@@ -411,31 +383,64 @@ function AuditLogsPage() {
colSpan={6} colSpan={6}
className="text-xs text-muted-foreground" className="text-xs text-muted-foreground"
> >
<div className="grid gap-3 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-3">
<div className="space-y-1"> <div className="space-y-1">
<div> <div className="uppercase tracking-[0.16em]">
Request
</div>
<div className="break-all">
Request ID:{" "} Request ID:{" "}
{formatValue(details.request_id)} {formatAuditValue(details.request_id)}
</div>
<div className="break-all">
Event ID:{" "}
{formatAuditValue(row.event_id)}
</div>
<div>IP: {formatAuditValue(row.ip_address)}</div>
<div className="break-all">
Method: {formatAuditValue(details.method)}
</div>
<div className="break-all">
Path: {formatAuditValue(details.path)}
</div> </div>
<div> <div>
Method: {formatValue(details.method)} Latency:{" "}
{details.latency_ms !== undefined
? `${details.latency_ms}ms`
: "-"}
</div>
</div>
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
Actor
</div>
<div>Actor ID: {actorLabel}</div>
<div>
Tenant:{" "}
{formatAuditValue(details.tenant_id)}
</div> </div>
<div> <div>
Path: {formatValue(details.path)} Device:{" "}
{formatAuditValue(row.device_id)}
</div> </div>
<div> <div className="break-all">
Tenant: {formatValue(details.tenant_id)} Target: {targetValue}
</div> </div>
</div> </div>
<div className="space-y-1 break-all"> <div className="space-y-1 break-all">
<div> <div className="uppercase tracking-[0.16em]">
Before: {formatValue(details.before)} Result
</div> </div>
<div> <div>
After: {formatValue(details.after)} Error:{" "}
{formatAuditValue(details.error)}
</div> </div>
<div> <div>
Error: {formatValue(details.error)} Before:{" "}
{formatAuditValue(details.before)}
</div>
<div>
After: {formatAuditValue(details.after)}
</div> </div>
</div> </div>
</div> </div>
@@ -461,7 +466,7 @@ function AuditLogsPage() {
> >
{query.isFetchingNextPage {query.isFetchingNextPage
? t("msg.common.loading", "Loading...") ? t("msg.common.loading", "Loading...")
: t("ui.dev.audit.load_more", "Load more")} : t("ui.common.audit.load_more", "Load more")}
</Button> </Button>
</div> </div>
) : null} ) : null}