"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type ColumnDef, type RowSelectionState, type VisibilityState, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import { toast } from "sonner"; import { useSearchParams } from "next/navigation"; import { useCurrentUser } from "@/hooks/use-current-user"; import { toastError } from "@/lib/format"; import { getCollections, getAdminRecords, deleteRecord, deleteCollectionRecords, } from "@/lib/api"; import type { AdminRecord } from "@/types/records"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { ResponsiveDialog, ResponsiveDialogClose, ResponsiveDialogContent, ResponsiveDialogDescription, ResponsiveDialogFooter, ResponsiveDialogHeader, ResponsiveDialogTitle, } from "@/components/ui/responsive-dialog"; import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; import { Checkbox } from "@/components/ui/checkbox"; import { CodeBlock } from "@/components/code-block"; import { DataTable } from "@/components/data-table/data-table"; import { DataTableViewOptions } from "@/components/data-table/data-table-view-options"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { LabelBadges } from "@/components/label-badges"; import { SiteHeader } from "@/components/site-header"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { ChevronLeft, ChevronRight, Trash2 } from "lucide-react"; import { Field, FieldContent, FieldDescription, FieldLabel, FieldTitle, } from "@/components/ui/field"; function parseAtUri(uri: string): { did: string; rkey: string } { const parts = uri.replace("at://", "").split("/"); return { did: parts[0] ?? "", rkey: parts[2] ?? "" }; } function formatCellValue(value: unknown): string { if (value === null || value === undefined) return ""; if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return String(value); return JSON.stringify(value); } export default function RecordsPage() { const { hasPermission } = useCurrentUser(); const searchParams = useSearchParams(); const initialCollection = searchParams.get("collection") ?? ""; const appliedInitial = useRef(false); const [collections, setCollections] = useState([]); const [selectedCollection, setSelectedCollection] = useState(""); const [records, setRecords] = useState([]); const [cursorStack, setCursorStack] = useState([]); const [nextCursor, setNextCursor] = useState(); const [loading, setLoading] = useState(false); const [viewRecord, setViewRecord] = useState(null); const [deleting, setDeleting] = useState(false); const [deleteUri, setDeleteUri] = useState(null); const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false); const [bulkDeleteMode, setBulkDeleteMode] = useState<"selected" | "all">( "selected", ); const [bulkDeleteConfirm, setBulkDeleteConfirm] = useState(""); const [deletingAll, setDeletingAll] = useState(false); const [columnVisibility, setColumnVisibility] = useState({}); const [rowSelection, setRowSelection] = useState({}); useEffect(() => { getCollections() .then((data) => setCollections(data.collections)) .catch((e) => toastError("Failed to load collections", e)); }, []); useEffect(() => { if (Object.keys(rowSelection).length === 0) return; const validUris = new Set(records.map((r) => r.uri)); const pruned: RowSelectionState = {}; let changed = false; for (const [uri, selected] of Object.entries(rowSelection)) { if (validUris.has(uri)) { pruned[uri] = selected; } else { changed = true; } } if (changed) setRowSelection(pruned); }, [records]); // Auto-select collection from URL search param on initial load. useEffect(() => { if (appliedInitial.current || !initialCollection || collections.length === 0) return; if (collections.includes(initialCollection)) { appliedInitial.current = true; handleSelectCollection(initialCollection); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [collections, initialCollection]); const fetchRecords = useCallback( async (collection: string, cursor?: string) => { setLoading(true); try { const data = await getAdminRecords(collection, 20, cursor); setRecords(data.records); setNextCursor(data.cursor); } catch (e: unknown) { toastError("Failed to load records", e); setRecords([]); setNextCursor(undefined); } finally { setLoading(false); } }, [], ); const handleDeleteRecord = useCallback( async (uri: string) => { setDeleting(true); try { await deleteRecord(uri); setDeleteUri(null); setViewRecord(null); toast.success("Record deleted"); if (selectedCollection) { const currentCursor = cursorStack.length > 0 ? cursorStack[cursorStack.length - 1] : undefined; fetchRecords(selectedCollection, currentCursor); } } catch (e: unknown) { toastError("Failed to delete record", e); } finally { setDeleting(false); } }, [selectedCollection, cursorStack, fetchRecords], ); const handleDeleteAll = useCallback(async () => { if (!selectedCollection) return; setDeletingAll(true); try { await deleteCollectionRecords(selectedCollection); setBulkDeleteOpen(false); setBulkDeleteMode("selected"); setBulkDeleteConfirm(""); setRowSelection({}); toast.success("All records deleted"); const data = await getCollections(); setCollections(data.collections); setCursorStack([]); setNextCursor(undefined); setRecords([]); setSelectedCollection(""); } catch (e: unknown) { toastError("Failed to delete collection", e); } finally { setDeletingAll(false); } }, [selectedCollection]); const handleBulkDelete = useCallback(async () => { setDeleting(true); const selectedUris = Object.keys(rowSelection); const results = await Promise.allSettled(selectedUris.map((uri) => deleteRecord(uri))); const succeeded = selectedUris.filter((_, i) => results[i].status === "fulfilled"); const failed = selectedUris.length - succeeded.length; if (failed === 0) { toast.success(`Deleted ${succeeded.length} ${succeeded.length === 1 ? "record" : "records"}`); } else if (succeeded.length === 0) { toast.error("Failed to delete records"); } else { toast.warning(`Deleted ${succeeded.length} of ${selectedUris.length} records`, { description: `${failed} ${failed === 1 ? "record" : "records"} failed to delete.`, }); } setRowSelection({}); setBulkDeleteOpen(false); if (selectedCollection) { const currentCursor = cursorStack.length > 0 ? cursorStack[cursorStack.length - 1] : undefined; fetchRecords(selectedCollection, currentCursor); } setDeleting(false); }, [rowSelection, selectedCollection, cursorStack, fetchRecords]); // Build columns dynamically from the union of all record keys const columns = useMemo[]>(() => { const keySet = new Set(); for (const r of records) { for (const key of Object.keys(r.record)) { keySet.add(key); } } const cols: ColumnDef[] = [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value) } aria-label="Select all" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} onClick={(e) => e.stopPropagation()} aria-label="Select row" /> ), enableSorting: false, enableHiding: false, }, { id: "did", accessorFn: (row) => parseAtUri(row.uri).did, header: "DID", cell: ({ getValue }) => ( {getValue()} ), enableSorting: false, enableHiding: false, meta: { label: "DID" }, }, { id: "rkey", accessorFn: (row) => parseAtUri(row.uri).rkey, header: "Record Key", cell: ({ getValue }) => ( {getValue()} ), enableSorting: false, enableHiding: false, meta: { label: "Record Key" }, }, { id: "labels", accessorFn: (row) => row.labels, header: "Labels", enableSorting: false, cell: ({ row }) => ( ), meta: { label: "Labels" }, }, ]; for (const key of keySet) { cols.push({ id: key, accessorFn: (row) => row.record[key], header: key, enableSorting: false, cell: ({ getValue }) => { const val = getValue(); const str = formatCellValue(val); return ( {str} ); }, meta: { label: key }, }); } return cols; }, [records]); const table = useReactTable({ data: records, columns, state: { columnVisibility, columnPinning: { left: ["select"] }, rowSelection, }, onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: setRowSelection, enableRowSelection: true, getCoreRowModel: getCoreRowModel(), getRowId: (row) => row.uri, }); function handleSelectCollection(collection: string) { setSelectedCollection(collection); setCursorStack([]); setNextCursor(undefined); setColumnVisibility({}); setRowSelection({}); fetchRecords(collection); } function handleNext() { if (!nextCursor || !selectedCollection) return; setCursorStack((prev) => [...prev, nextCursor]); fetchRecords(selectedCollection, nextCursor); } function handlePrevious() { if (cursorStack.length === 0 || !selectedCollection) return; const stack = [...cursorStack]; stack.pop(); const prevCursor = stack.length > 0 ? stack[stack.length - 1] : undefined; setCursorStack(stack); fetchRecords(selectedCollection, prevCursor); } return ( <>
{hasPermission("records:delete") && ( setBulkDeleteOpen(true)} > Delete )}
{selectedCollection && (

{records.length} record(s) on this page.

)} { if (!open) setViewRecord(null); }} > {viewRecord && ( <> Record Detail
URI

{viewRecord.uri}

DID

{viewRecord.did}

Collection

{viewRecord.collection}

Record Key

{viewRecord.rkey}

CID

{viewRecord.cid}

{viewRecord.indexed_at && (
Indexed

{new Date(viewRecord.indexed_at).toLocaleString()}

)} {viewRecord.labels.length > 0 && (
Labels
{viewRecord.labels.map((l, i) => ( {l.val} ))}
)}
Record
{hasPermission("records:delete") && (
)} )}
{ if (!open) setDeleteUri(null); }}> Delete record? This will permanently delete the record. This action cannot be undone. {deleteUri && ( {deleteUri} )} Cancel { if (deleteUri) handleDeleteRecord(deleteUri); }} > {deleting ? "Deleting..." : "Delete"} { if (!open) { setBulkDeleteOpen(false); setBulkDeleteMode("selected"); setBulkDeleteConfirm(""); } }} > {(() => { const selectedCount = Object.keys(rowSelection).length; if (!table.getIsAllPageRowsSelected()) { return ( <> Delete {selectedCount} record(s)? This action cannot be undone. ); } return ( <> Delete records? This action cannot be undone. { setBulkDeleteMode(v as "selected" | "all"); setBulkDeleteConfirm(""); }} > Delete selected only {`${selectedCount} items selected`} Delete all records {selectedCollection} {bulkDeleteMode === "all" && (
setBulkDeleteConfirm(e.target.value)} placeholder={selectedCollection} />
)} ); })()}
); }