import { useInfiniteQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { Download, RefreshCw, Search } from "lucide-react"; import * as React from "react"; import { ForbiddenMessage } from "../../components/common/ForbiddenMessage"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"; import { Input } from "../../components/ui/input"; import { parseAuditDetails, } 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"; import { fetchDevAuditLogs } from "../../lib/devApi"; import { t } from "../../lib/i18n"; 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 = parseAuditDetails(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 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 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.common.audit.load_error", "Error loading logs: {{error}}", { error: errMsg, })}
); } return (
{t("msg.common.audit.registry.count", "총 {{count}}개 로그", { count: logs.length, })} } />
{t("ui.common.audit.registry.title", "Audit registry")}
{ e.preventDefault(); query.refetch(); }} className="grid flex-1 gap-2 md:grid-cols-[1fr,1fr,180px]" >
setSearchClientId(e.target.value)} placeholder={t( "ui.common.audit.filters.client_id", "Filter by Client ID", )} />
setSearchAction(e.target.value.toUpperCase()) } placeholder={t( "ui.common.audit.filters.action", "Filter by Action (e.g. ROTATE_SECRET)", )} /> } />
query.fetchNextPage()} />
); } export default AuditLogsPage;