118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { Download } from "lucide-react"
|
|
import { SettingsSectionHeader } from "@/components/gmail/settings/settings-section-header"
|
|
import { SettingsSyncBanner } from "@/components/gmail/settings/settings-sync-banner"
|
|
import { useAdminAuditLogs } from "@/lib/api/hooks/use-admin-queries"
|
|
import { apiClient } from "@/lib/api/client"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table"
|
|
|
|
export function AuditSection() {
|
|
const [page, setPage] = useState(1)
|
|
const { data, isFetching, isError, refetch } = useAdminAuditLogs({ page, page_size: 50 })
|
|
const logs = data?.logs ?? []
|
|
const total = data?.pagination.total ?? 0
|
|
const pageSize = data?.pagination.page_size ?? 50
|
|
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
|
|
|
async function exportLogs(format: "csv" | "ndjson") {
|
|
const blob = await apiClient.getBlob(`/admin/audit/export?format=${format}&limit=5000`)
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement("a")
|
|
a.href = url
|
|
a.download = `audit-export.${format === "csv" ? "csv" : "ndjson"}`
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<SettingsSectionHeader
|
|
title="Journal d'audit"
|
|
description="Historique des actions administratives sur la plateforme."
|
|
/>
|
|
<SettingsSyncBanner isFetching={isFetching} isError={isError} onRetry={() => refetch()} />
|
|
|
|
<div className="mb-4 flex flex-wrap gap-2">
|
|
<Button variant="outline" size="sm" onClick={() => void exportLogs("csv")}>
|
|
<Download className="mr-2 size-4" />
|
|
Export CSV
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => void exportLogs("ndjson")}>
|
|
<Download className="mr-2 size-4" />
|
|
Export NDJSON
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto rounded-lg border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Date</TableHead>
|
|
<TableHead>Acteur</TableHead>
|
|
<TableHead>Action</TableHead>
|
|
<TableHead className="hidden lg:table-cell">Détails</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{logs.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={4} className="text-center text-muted-foreground">
|
|
Aucun événement.
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
logs.map((log) => (
|
|
<TableRow key={log.id}>
|
|
<TableCell className="whitespace-nowrap text-xs">
|
|
{new Date(log.created_at).toLocaleString("fr-FR")}
|
|
</TableCell>
|
|
<TableCell className="max-w-[140px] truncate font-mono text-xs">
|
|
{log.actor}
|
|
</TableCell>
|
|
<TableCell className="text-sm">{log.action}</TableCell>
|
|
<TableCell className="hidden max-w-md truncate font-mono text-xs lg:table-cell">
|
|
{typeof log.details === "string"
|
|
? log.details
|
|
: JSON.stringify(log.details)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{totalPages > 1 ? (
|
|
<div className="mt-4 flex justify-end gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={page <= 1}
|
|
onClick={() => setPage((p) => p - 1)}
|
|
>
|
|
Précédent
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={page >= totalPages}
|
|
onClick={() => setPage((p) => p + 1)}
|
|
>
|
|
Suivant
|
|
</Button>
|
|
</div>
|
|
) : null}
|
|
</>
|
|
)
|
|
}
|