forked from baron/baron-sso
감사로그 조회 전체 새로고침 현상 수정
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user