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:
@@ -110,7 +110,7 @@ export function BenchmarksClient() {
|
|||||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
|
||||||
const [actionLoading, setActionLoading] = useState<Record<string, boolean>>({})
|
const [actionLoading, setActionLoading] = useState<Record<string, boolean>>({})
|
||||||
const [confirmAction, setConfirmAction] = useState<
|
const [confirmAction, setConfirmAction] = useState<
|
||||||
| { type: "verify" | "remove" | "restore"; entry: PerformanceEntry }
|
| { type: "verify" | "remove" | "restore" | "hardDelete"; entry: PerformanceEntry }
|
||||||
| null
|
| null
|
||||||
>(null)
|
>(null)
|
||||||
const [removeReason, setRemoveReason] = useState("")
|
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) => {
|
const handleRestore = async (entry: PerformanceEntry) => {
|
||||||
setActionLoading((prev) => ({ ...prev, [entry.id]: true }))
|
setActionLoading((prev) => ({ ...prev, [entry.id]: true }))
|
||||||
try {
|
try {
|
||||||
@@ -281,7 +297,8 @@ export function BenchmarksClient() {
|
|||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="rounded-xl border border-border overflow-hidden">
|
<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]">
|
<thead className="bg-text/[0.03]">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left px-4 py-3 text-xs font-medium uppercase tracking-wider text-text/50">
|
<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
|
Restore
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -459,6 +485,7 @@ export function BenchmarksClient() {
|
|||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
@@ -596,6 +623,37 @@ export function BenchmarksClient() {
|
|||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -226,6 +226,36 @@ export const adminPerformanceRoutes = new Elysia({ prefix: "/admin" })
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.delete(
|
||||||
|
"/performance/:id/hard-delete",
|
||||||
|
async ({ params, request, set }) => {
|
||||||
|
const guard = await requireAdmin(request.headers)
|
||||||
|
if (!guard.ok) {
|
||||||
|
set.status = guard.status
|
||||||
|
return { error: guard.error }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [entry] = await db
|
||||||
|
.select()
|
||||||
|
.from(performanceEntries)
|
||||||
|
.where(eq(performanceEntries.id, params.id))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
set.status = 404
|
||||||
|
return { error: "Performance entry not found" }
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(performanceEntries)
|
||||||
|
.where(eq(performanceEntries.id, params.id))
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: t.Object({ id: t.String() }),
|
||||||
|
},
|
||||||
|
)
|
||||||
.patch(
|
.patch(
|
||||||
"/performance/:id/restore",
|
"/performance/:id/restore",
|
||||||
async ({ params, request, set }) => {
|
async ({ params, request, set }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user