fix: add horizontal scroll to benchmarks table, add hard delete functionality

- Wrap table in overflow-x-auto container with min-w-[800px] to prevent overflow on smaller screens
- Add hard delete (purge) button to actions column for each benchmark entry
- Add confirmation dialog for permanent deletion with warning
- Add handleHardDelete handler that calls DELETE /api/admin/performance/:id/hard-delete
- Add DELETE endpoint for hard-deleting performance entries from the database
- Expand confirmAction state type to include 'hardDelete'
This commit is contained in:
2026-04-29 01:38:29 +08:00
parent 2fed5a3964
commit b64e55157e
2 changed files with 90 additions and 2 deletions
@@ -110,7 +110,7 @@ export function BenchmarksClient() {
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
const [actionLoading, setActionLoading] = useState<Record<string, boolean>>({})
const [confirmAction, setConfirmAction] = useState<
| { type: "verify" | "remove" | "restore"; entry: PerformanceEntry }
| { type: "verify" | "remove" | "restore" | "hardDelete"; entry: PerformanceEntry }
| null
>(null)
const [removeReason, setRemoveReason] = useState("")
@@ -224,6 +224,22 @@ export function BenchmarksClient() {
}
}
const handleHardDelete = async (entry: PerformanceEntry) => {
setActionLoading((prev) => ({ ...prev, [entry.id]: true }))
try {
const res = await fetch(`/api/admin/performance/${entry.id}/hard-delete`, {
method: "DELETE",
})
if (res.ok) {
setEntries((prev) => prev.filter((e) => e.id !== entry.id))
setTotal((prev) => Math.max(0, prev - 1))
setConfirmAction(null)
}
} finally {
setActionLoading((prev) => ({ ...prev, [entry.id]: false }))
}
}
const handleRestore = async (entry: PerformanceEntry) => {
setActionLoading((prev) => ({ ...prev, [entry.id]: true }))
try {
@@ -281,7 +297,8 @@ export function BenchmarksClient() {
{/* Table */}
<div className="rounded-xl border border-border overflow-hidden">
<table className="w-full text-sm">
<div className="overflow-x-auto">
<table className="w-full text-sm min-w-[800px]">
<thead className="bg-text/[0.03]">
<tr>
<th className="text-left px-4 py-3 text-xs font-medium uppercase tracking-wider text-text/50">
@@ -452,6 +469,15 @@ export function BenchmarksClient() {
Restore
</button>
)}
<button
onClick={() => setConfirmAction({ type: "hardDelete", entry })}
disabled={actionLoading[entry.id]}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium bg-red-600/10 text-red-500 hover:bg-red-600/20 transition-colors cursor-pointer disabled:opacity-50"
title="Permanently delete"
>
<TrashIcon className="h-3.5 w-3.5" />
Purge
</button>
</div>
</td>
</tr>
@@ -459,6 +485,7 @@ export function BenchmarksClient() {
)}
</tbody>
</table>
</div>
</div>
{/* Pagination */}
@@ -596,6 +623,37 @@ export function BenchmarksClient() {
</div>
</>
)}
{confirmAction.type === "hardDelete" && (
<>
<h2 className="text-base font-semibold text-text">
Permanent Delete
</h2>
<p className="text-sm text-text/70">
This will permanently delete this benchmark entry. This action cannot be undone.
</p>
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setConfirmAction(null)}
className="px-3 py-1.5 rounded-md text-xs font-medium bg-text/5 text-text hover:bg-text/10 transition-colors cursor-pointer"
>
Cancel
</button>
<button
onClick={() => handleHardDelete(confirmAction.entry)}
disabled={actionLoading[confirmAction.entry.id]}
className="px-3 py-1.5 rounded-md text-xs font-medium bg-red-600/10 text-red-500 hover:bg-red-600/20 transition-colors cursor-pointer disabled:opacity-50 flex items-center gap-1.5"
>
{actionLoading[confirmAction.entry.id] ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<TrashIcon className="h-3.5 w-3.5" />
)}
Delete Forever
</button>
</div>
</>
)}
</div>
</div>
)}