1
0
forked from baron/baron-sso

감사 로그 테이블 헤더 및 검색창 문구 수정

This commit is contained in:
2026-05-15 14:46:58 +09:00
parent 974af01d34
commit 9df69f22e8
7 changed files with 563 additions and 698 deletions

View File

@@ -1,43 +1,20 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import {
ChevronDown,
ChevronUp,
Copy,
Download,
RefreshCw,
Search,
} from "lucide-react";
import { Download, RefreshCw, Search } from "lucide-react";
import * as React from "react";
import {
commonStickyTableHeaderClass,
commonTableShellClass,
commonTableViewportClass,
} from "../../../../common/ui/table";
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
Card,
CardContent,
} from "../../components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card";
import { Input } from "../../components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
import {
formatAuditDateParts,
formatAuditValue,
parseAuditDetails,
resolveAuditAction,
resolveAuditActor,
resolveAuditTarget,
} from "../../../../common/core/audit";
import { AuditLogTable } from "../../../../common/core/components/audit";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { PageHeader } from "../../../../common/core/components/page";
import type { DevAuditLog } from "../../lib/devApi";
@@ -96,10 +73,6 @@ function AuditLogsPage() {
const deferredSearchClientId = React.useDeferredValue(searchClientId.trim());
const deferredSearchAction = React.useDeferredValue(searchAction.trim());
const [expandedRows, setExpandedRows] = React.useState<
Record<string, boolean>
>({});
const query = useInfiniteQuery({
queryKey: [
"dev-audit-logs",
@@ -122,13 +95,6 @@ function AuditLogsPage() {
page.items.filter((item): item is DevAuditLog => Boolean(item)),
) ?? [];
const handleCopy = (value: string) => {
if (!value) {
return;
}
navigator.clipboard.writeText(value);
};
const handleExportCsv = () => {
const csv = toCsv(logs);
const stamp = new Date().toISOString().replaceAll(":", "-");
@@ -155,7 +121,6 @@ function AuditLogsPage() {
return (
<div className="space-y-6">
<PageHeader
eyebrow={t("ui.common.audit.registry.title", "Audit registry")}
title={t("ui.common.audit.title", "Audit Logs")}
description={t(
"msg.dev.audit.subtitle",
@@ -164,7 +129,7 @@ function AuditLogsPage() {
actions={
<>
<Badge variant="muted">
{t("msg.dev.audit.loaded_count", "Loaded {{count}} rows", {
{t("msg.common.audit.registry.count", " {{count}}개 로그", {
count: logs.length,
})}
</Badge>
@@ -188,7 +153,14 @@ function AuditLogsPage() {
/>
<Card className="glass-panel">
<CardContent className="space-y-4 pt-6">
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>
{t("ui.common.audit.registry.title", "Audit registry")}
</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-4 pt-0">
<SearchFilterBar
primary={
<form
@@ -205,7 +177,7 @@ function AuditLogsPage() {
value={searchClientId}
onChange={(e) => setSearchClientId(e.target.value)}
placeholder={t(
"ui.dev.audit.filter.client_id",
"ui.common.audit.filters.client_id",
"Filter by Client ID",
)}
/>
@@ -216,7 +188,7 @@ function AuditLogsPage() {
setSearchAction(e.target.value.toUpperCase())
}
placeholder={t(
"ui.dev.audit.filter.action",
"ui.common.audit.filters.action",
"Filter by Action (e.g. ROTATE_SECRET)",
)}
/>
@@ -226,7 +198,7 @@ function AuditLogsPage() {
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.audit.filter.status_all", "All Status")}
{t("ui.common.audit.filters.status_all", "All Status")}
</option>
<option value="success">
{t("ui.common.status.success", "Success")}
@@ -246,230 +218,15 @@ function AuditLogsPage() {
: ""
}
>
<div className={commonTableShellClass}>
<div className={commonTableViewportClass}>
<Table className="table-fixed">
<TableHeader className={commonStickyTableHeaderClass}>
<TableRow>
<TableHead className="w-[190px]">
{t("ui.common.audit.table.time", "Time")}
</TableHead>
<TableHead className="w-[180px]">
{t("ui.common.audit.table.actor", "Actor")}
</TableHead>
<TableHead className="w-[180px]">
{t("ui.common.audit.table.action", "Action")}
</TableHead>
<TableHead className="w-[260px]">
{t("ui.common.audit.table.target", "Target")}
</TableHead>
<TableHead className="w-[120px]">
{t("ui.common.audit.table.status", "Status")}
</TableHead>
<TableHead className="w-[80px]" />
</TableRow>
</TableHeader>
<TableBody>
{query.isLoading && logs.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
className="py-8 text-center text-muted-foreground"
>
{t("msg.common.audit.loading", "Loading audit logs...")}
</TableCell>
</TableRow>
) : logs.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
className="text-center text-muted-foreground"
>
{t("msg.common.audit.empty", "No audit logs found.")}
</TableCell>
</TableRow>
) : (
logs.map((row, index) => {
const details = parseAuditDetails(row.details);
const actionLabel = resolveAuditAction(row, details);
const actorLabel = resolveAuditActor(row, details);
const targetValue = resolveAuditTarget(details);
const rowKey = `${row.event_id}-${row.timestamp}-${index}`;
const expanded = Boolean(expandedRows[rowKey]);
return (
<React.Fragment key={rowKey}>
<TableRow>
<TableCell className="text-xs text-muted-foreground">
{(() => {
const { date, time } = formatAuditDateParts(
row.timestamp,
);
return (
<div className="space-y-1">
<div>{date}</div>
<div>{time}</div>
</div>
);
})()}
</TableCell>
<TableCell className="font-mono text-xs">
<div className="flex items-center gap-2">
<span>{actorLabel}</span>
{actorLabel !== "-" ? (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground"
onClick={() => handleCopy(actorLabel)}
>
<Copy className="h-3 w-3" />
</Button>
) : null}
</div>
</TableCell>
<TableCell className="text-xs">
{actionLabel}
</TableCell>
<TableCell className="font-mono text-xs">
<div className="flex items-center gap-2">
<span className="break-all">
{targetValue}
</span>
{targetValue !== "-" ? (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground"
onClick={() => handleCopy(targetValue)}
>
<Copy className="h-3 w-3" />
</Button>
) : null}
</div>
</TableCell>
<TableCell>
<Badge
variant={
row.status === "success"
? "success"
: "warning"
}
>
{row.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="sm"
onClick={() =>
setExpandedRows((prev) => ({
...prev,
[rowKey]: !expanded,
}))
}
>
{expanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</TableCell>
</TableRow>
{expanded ? (
<TableRow className="bg-card/20">
<TableCell
colSpan={6}
className="text-xs text-muted-foreground"
>
<div className="grid gap-4 md:grid-cols-3">
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
Request
</div>
<div className="break-all">
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>
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>
Device:{" "}
{formatAuditValue(row.device_id)}
</div>
<div className="break-all">
Target: {targetValue}
</div>
</div>
<div className="space-y-1 break-all">
<div className="uppercase tracking-[0.16em]">
Result
</div>
<div>
Error:{" "}
{formatAuditValue(details.error)}
</div>
<div>
Before:{" "}
{formatAuditValue(details.before)}
</div>
<div>
After: {formatAuditValue(details.after)}
</div>
</div>
</div>
</TableCell>
</TableRow>
) : null}
</React.Fragment>
);
})
)}
</TableBody>
</Table>
</div>
</div>
<AuditLogTable
logs={logs}
t={t}
loading={query.isLoading}
hasNextPage={Boolean(query.hasNextPage)}
isFetchingNextPage={query.isFetchingNextPage}
onLoadMore={() => query.fetchNextPage()}
/>
</div>
{query.hasNextPage ? (
<div className="flex justify-center">
<Button
variant="outline"
onClick={() => query.fetchNextPage()}
disabled={query.isFetchingNextPage}
>
{query.isFetchingNextPage
? t("msg.common.loading", "Loading...")
: t("ui.common.audit.load_more", "Load more")}
</Button>
</div>
) : null}
</CardContent>
</Card>
</div>