1
0
forked from baron/baron-sso

감사로그 조회 전체 새로고침 현상 수정

This commit is contained in:
2026-04-30 10:50:47 +09:00
parent 6d5a861d17
commit 894565d87e

View File

@@ -126,16 +126,26 @@ function AuditLogsPage() {
const [searchClientId, setSearchClientId] = React.useState(""); const [searchClientId, setSearchClientId] = React.useState("");
const [searchAction, setSearchAction] = React.useState(""); const [searchAction, setSearchAction] = React.useState("");
const [statusFilter, setStatusFilter] = React.useState("all"); const [statusFilter, setStatusFilter] = React.useState("all");
// Use deferred values to avoid UI lag during rapid typing
const deferredSearchClientId = React.useDeferredValue(searchClientId.trim());
const deferredSearchAction = React.useDeferredValue(searchAction.trim());
const [expandedRows, setExpandedRows] = React.useState< const [expandedRows, setExpandedRows] = React.useState<
Record<string, boolean> Record<string, boolean>
>({}); >({});
const query = useInfiniteQuery({ const query = useInfiniteQuery({
queryKey: ["dev-audit-logs", searchClientId, searchAction, statusFilter], queryKey: [
"dev-audit-logs",
deferredSearchClientId,
deferredSearchAction,
statusFilter,
],
queryFn: ({ pageParam }) => queryFn: ({ pageParam }) =>
fetchDevAuditLogs(50, pageParam, { fetchDevAuditLogs(50, pageParam, {
client_id: searchClientId.trim() || undefined, client_id: deferredSearchClientId || undefined,
action: searchAction.trim() || undefined, action: deferredSearchAction || undefined,
status: statusFilter !== "all" ? statusFilter : undefined, status: statusFilter !== "all" ? statusFilter : undefined,
}), }),
initialPageParam: undefined as string | undefined, initialPageParam: undefined as string | undefined,
@@ -160,14 +170,6 @@ function AuditLogsPage() {
downloadCsv(csv, `dev-audit-logs-${stamp}.csv`); downloadCsv(csv, `dev-audit-logs-${stamp}.csv`);
}; };
if (query.isLoading) {
return (
<div className="p-8 text-center">
{t("msg.dev.audit.loading", "Loading audit logs...")}
</div>
);
}
if (query.error) { if (query.error) {
const axiosError = query.error as AxiosError<{ error?: string }>; const axiosError = query.error as AxiosError<{ error?: string }>;
if (axiosError.response?.status === 403) { if (axiosError.response?.status === 403) {
@@ -227,7 +229,13 @@ function AuditLogsPage() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid gap-2 md:grid-cols-[1fr,1fr,180px]"> <form
onSubmit={(e) => {
e.preventDefault();
query.refetch();
}}
className="grid gap-2 md:grid-cols-[1fr,1fr,180px]"
>
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
@@ -263,142 +271,160 @@ function AuditLogsPage() {
{t("ui.common.status.failure", "Failure")} {t("ui.common.status.failure", "Failure")}
</option> </option>
</select> </select>
</div> </form>
<Table className="table-fixed"> <div
<TableHeader> className={
<TableRow> query.isFetching && !query.isFetchingNextPage
<TableHead className="w-[190px]"> ? "opacity-50 transition-opacity"
{t("ui.dev.audit.table.time", "Time")} : ""
</TableHead> }
<TableHead className="w-[180px]"> >
{t("ui.dev.audit.table.actor", "Actor")} <Table className="table-fixed">
</TableHead> <TableHeader>
<TableHead className="w-[180px]">
{t("ui.dev.audit.table.action", "Action")}
</TableHead>
<TableHead className="w-[260px]">
{t("ui.dev.audit.table.target", "Target")}
</TableHead>
<TableHead className="w-[120px]">
{t("ui.dev.audit.table.status", "Status")}
</TableHead>
<TableHead className="w-[80px]" />
</TableRow>
</TableHeader>
<TableBody>
{logs.length === 0 && (
<TableRow> <TableRow>
<TableCell <TableHead className="w-[190px]">
colSpan={6} {t("ui.dev.audit.table.time", "Time")}
className="text-center text-muted-foreground" </TableHead>
> <TableHead className="w-[180px]">
{t("msg.dev.audit.empty", "No audit logs found.")} {t("ui.dev.audit.table.actor", "Actor")}
</TableCell> </TableHead>
<TableHead className="w-[180px]">
{t("ui.dev.audit.table.action", "Action")}
</TableHead>
<TableHead className="w-[260px]">
{t("ui.dev.audit.table.target", "Target")}
</TableHead>
<TableHead className="w-[120px]">
{t("ui.dev.audit.table.status", "Status")}
</TableHead>
<TableHead className="w-[80px]" />
</TableRow> </TableRow>
)} </TableHeader>
{logs.map((row, index) => { <TableBody>
const details = parseDetails(row.details); {query.isLoading && logs.length === 0 ? (
const actionLabel = details.action || row.event_type; <TableRow>
const targetValue = details.target_id || "-"; <TableCell
const rowKey = `${row.event_id}-${row.timestamp}-${index}`; colSpan={6}
const expanded = Boolean(expandedRows[rowKey]); className="py-8 text-center text-muted-foreground"
return ( >
<React.Fragment key={rowKey}> {t("msg.dev.audit.loading", "Loading audit logs...")}
<TableRow> </TableCell>
<TableCell className="text-xs text-muted-foreground"> </TableRow>
{formatDateTime(row.timestamp)} ) : logs.length === 0 ? (
</TableCell> <TableRow>
<TableCell className="font-mono text-xs"> <TableCell
<div className="flex items-center gap-2"> colSpan={6}
<span>{row.user_id || "-"}</span> className="text-center text-muted-foreground"
{row.user_id ? ( >
{t("msg.dev.audit.empty", "No audit logs found.")}
</TableCell>
</TableRow>
) : (
logs.map((row, index) => {
const details = parseDetails(row.details);
const actionLabel = details.action || row.event_type;
const targetValue = details.target_id || "-";
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">
{formatDateTime(row.timestamp)}
</TableCell>
<TableCell className="font-mono text-xs">
<div className="flex items-center gap-2">
<span>{row.user_id || "-"}</span>
{row.user_id ? (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground"
onClick={() => handleCopy(row.user_id)}
>
<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 <Button
variant="ghost" variant="ghost"
size="icon" size="sm"
className="h-7 w-7 text-muted-foreground" onClick={() =>
onClick={() => handleCopy(row.user_id)} setExpandedRows((prev) => ({
...prev,
[rowKey]: !expanded,
}))
}
> >
<Copy className="h-3 w-3" /> {expanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button> </Button>
) : null} </TableCell>
</div> </TableRow>
</TableCell> {expanded ? (
<TableCell className="text-xs">{actionLabel}</TableCell> <TableRow className="bg-card/20">
<TableCell className="font-mono text-xs"> <TableCell
<div className="flex items-center gap-2"> colSpan={6}
<span className="break-all">{targetValue}</span> className="text-xs text-muted-foreground"
{targetValue !== "-" ? (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground"
onClick={() => handleCopy(targetValue)}
> >
<Copy className="h-3 w-3" /> <div className="grid gap-3 md:grid-cols-2">
</Button> <div className="space-y-1">
) : null} <div>
</div> Request ID: {formatValue(details.request_id)}
</TableCell> </div>
<TableCell> <div>Method: {formatValue(details.method)}</div>
<Badge <div>Path: {formatValue(details.path)}</div>
variant={ <div>
row.status === "success" ? "success" : "warning" Tenant: {formatValue(details.tenant_id)}
} </div>
> </div>
{row.status} <div className="space-y-1 break-all">
</Badge> <div>Before: {formatValue(details.before)}</div>
</TableCell> <div>After: {formatValue(details.after)}</div>
<TableCell className="text-right"> <div>Error: {formatValue(details.error)}</div>
<Button </div>
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-3 md:grid-cols-2">
<div className="space-y-1">
<div>
Request ID: {formatValue(details.request_id)}
</div> </div>
<div>Method: {formatValue(details.method)}</div> </TableCell>
<div>Path: {formatValue(details.path)}</div> </TableRow>
<div> ) : null}
Tenant: {formatValue(details.tenant_id)} </React.Fragment>
</div> );
</div> })
<div className="space-y-1 break-all"> )}
<div>Before: {formatValue(details.before)}</div> </TableBody>
<div>After: {formatValue(details.after)}</div> </Table>
<div>Error: {formatValue(details.error)}</div> </div>
</div>
</div>
</TableCell>
</TableRow>
) : null}
</React.Fragment>
);
})}
</TableBody>
</Table>
{query.hasNextPage ? ( {query.hasNextPage ? (
<div className="flex justify-center"> <div className="flex justify-center">