import { useInfiniteQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { ChevronDown, ChevronUp, Copy, Download, RefreshCw, Search, } from "lucide-react"; import * as React from "react"; import { 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, CardDescription, CardHeader, CardTitle, } from "../../components/ui/card"; import { Input } from "../../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/ui/table"; import type { DevAuditLog } from "../../lib/devApi"; import { fetchDevAuditLogs } from "../../lib/devApi"; 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[]) { const header = [ "timestamp", "user_id", "status", "event_type", "action", "target_id", "tenant_id", "request_id", ]; const rows = logs.map((logItem) => { const details = parseDetails(logItem.details); return [ logItem.timestamp, logItem.user_id || "", logItem.status, logItem.event_type, details.action || "", details.target_id || "", details.tenant_id || "", details.request_id || "", ]; }); return [header, ...rows] .map((line) => line.map((cell) => `"${String(cell).replaceAll('"', '""')}"`).join(","), ) .join("\n"); } function downloadCsv(content: string, filename: string) { const blob = new Blob([content], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); URL.revokeObjectURL(url); } function AuditLogsPage() { const [searchClientId, setSearchClientId] = React.useState(""); const [searchAction, setSearchAction] = React.useState(""); 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< Record >({}); const query = useInfiniteQuery({ queryKey: [ "dev-audit-logs", deferredSearchClientId, deferredSearchAction, statusFilter, ], queryFn: ({ pageParam }) => fetchDevAuditLogs(50, pageParam, { client_id: deferredSearchClientId || undefined, action: deferredSearchAction || undefined, status: statusFilter !== "all" ? statusFilter : undefined, }), initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.next_cursor || undefined, }); const logs = query.data?.pages.flatMap((page) => 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(":", "-"); downloadCsv(csv, `dev-audit-logs-${stamp}.csv`); }; if (query.error) { const axiosError = query.error as AxiosError<{ error?: string }>; if (axiosError.response?.status === 403) { return ; } const errMsg = axiosError.response?.data?.error ?? (query.error as Error).message; return (
{t("msg.dev.audit.load_error", "Error loading logs: {{error}}", { error: errMsg, })}
); } return (

{t("ui.dev.audit.registry.title", "Audit registry")}

{t("ui.dev.audit.title", "Audit Logs")} {t( "msg.dev.audit.subtitle", "Shows DevFront activity history within current tenant/app scope.", )}
{t("msg.dev.audit.loaded_count", "Loaded {{count}} rows", { count: logs.length, })}
{ e.preventDefault(); query.refetch(); }} className="grid gap-2 md:grid-cols-[1fr,1fr,180px]" >
setSearchClientId(e.target.value)} placeholder={t( "ui.dev.audit.filter.client_id", "Filter by Client ID", )} />
setSearchAction(e.target.value.toUpperCase())} placeholder={t( "ui.dev.audit.filter.action", "Filter by Action (e.g. ROTATE_SECRET)", )} />
{t("ui.dev.audit.table.time", "Time")} {t("ui.dev.audit.table.actor", "Actor")} {t("ui.dev.audit.table.action", "Action")} {t("ui.dev.audit.table.target", "Target")} {t("ui.dev.audit.table.status", "Status")} {query.isLoading && logs.length === 0 ? ( {t("msg.dev.audit.loading", "Loading audit logs...")} ) : logs.length === 0 ? ( {t("msg.dev.audit.empty", "No audit logs found.")} ) : ( 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 ( {formatDateTime(row.timestamp)}
{row.user_id || "-"} {row.user_id ? ( ) : null}
{actionLabel}
{targetValue} {targetValue !== "-" ? ( ) : null}
{row.status}
{expanded ? (
Request ID:{" "} {formatValue(details.request_id)}
Method: {formatValue(details.method)}
Path: {formatValue(details.path)}
Tenant: {formatValue(details.tenant_id)}
Before: {formatValue(details.before)}
After: {formatValue(details.after)}
Error: {formatValue(details.error)}
) : null}
); }) )}
{query.hasNextPage ? (
) : null}
); } export default AuditLogsPage;