diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -279,3 +279,14 @@ getToken ) } + +export function deleteRecord( + getToken: () => Promise, + uri: string +) { + return apiFetch( + `/admin/records?${new URLSearchParams({ uri })}`, + getToken, + { method: "DELETE" } + ) +} diff --git a/web/src/lib/data-table.ts b/web/src/lib/data-table.ts --- a/web/src/lib/data-table.ts +++ b/web/src/lib/data-table.ts @@ -23,10 +23,8 @@ : undefined, left: isPinned === "left" ? `${column.getStart("left")}px` : undefined, right: isPinned === "right" ? `${column.getAfter("right")}px` : undefined, - opacity: isPinned ? 0.97 : 1, position: isPinned ? "sticky" : "relative", - background: isPinned ? "var(--background)" : undefined, - width: column.getSize(), + width: isPinned ? "1%" : column.getSize(), zIndex: isPinned ? 1 : undefined, }; } diff --git a/web/src/components/data-table/data-table.tsx b/web/src/components/data-table/data-table.tsx --- a/web/src/components/data-table/data-table.tsx +++ b/web/src/components/data-table/data-table.tsx @@ -37,7 +37,7 @@ {...props} > {children} -
+
{table.getHeaderGroups().map((headerGroup) => ( @@ -46,6 +46,7 @@ onRowClick(row.original) - : undefined + onRowClick ? () => onRowClick(row.original) : undefined } > {row.getVisibleCells().map((cell) => ( ) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogAction({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + variant = "outline", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +} diff --git a/web/src/components/ui/table.tsx b/web/src/components/ui/table.tsx --- a/web/src/components/ui/table.tsx +++ b/web/src/components/ui/table.tsx @@ -1,8 +1,8 @@ -"use client" +"use client"; -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Table({ className, ...props }: React.ComponentProps<"table">) { return ( @@ -16,7 +16,7 @@ {...props} />
- ) + ); } function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { @@ -26,7 +26,7 @@ className={cn("[&_tr]:border-b", className)} {...props} /> - ) + ); } function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { @@ -36,7 +36,7 @@ className={cn("[&_tr:last-child]:border-0", className)} {...props} /> - ) + ); } function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { @@ -45,11 +45,11 @@ data-slot="table-footer" className={cn( "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", - className + className, )} {...props} /> - ) + ); } function TableRow({ className, ...props }: React.ComponentProps<"tr">) { @@ -57,12 +57,12 @@
- ) + ); } function TableHead({ className, ...props }: React.ComponentProps<"th">) { @@ -71,11 +71,11 @@ data-slot="table-head" className={cn( "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", - className + className, )} {...props} /> - ) + ); } function TableCell({ className, ...props }: React.ComponentProps<"td">) { @@ -84,11 +84,11 @@ data-slot="table-cell" className={cn( "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", - className + className, )} {...props} /> - ) + ); } function TableCaption({ @@ -101,7 +101,7 @@ className={cn("text-muted-foreground mt-4 text-sm", className)} {...props} /> - ) + ); } export { @@ -113,4 +113,4 @@ TableRow, TableCell, TableCaption, -} +}; diff --git a/web/src/app/(dashboard)/records/page.tsx b/web/src/app/(dashboard)/records/page.tsx --- a/web/src/app/(dashboard)/records/page.tsx +++ b/web/src/app/(dashboard)/records/page.tsx @@ -12,9 +12,20 @@ import { getStats, getAdminRecords, + deleteRecord, type CollectionStat, type AdminRecord, } from "@/lib/api"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { CodeBlock } from "@/components/code-block"; import { DataTable } from "@/components/data-table/data-table"; import { DataTableViewOptions } from "@/components/data-table/data-table-view-options"; @@ -33,7 +44,7 @@ SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { ChevronLeft, ChevronRight } from "lucide-react"; +import { ChevronLeft, ChevronRight, Trash2 } from "lucide-react"; function parseAtUri(uri: string): { did: string; rkey: string } { const parts = uri.replace("at://", "").split("/"); @@ -58,6 +69,8 @@ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [viewRecord, setViewRecord] = useState(null); + const [deleting, setDeleting] = useState(false); + const [deleteUri, setDeleteUri] = useState(null); const [columnVisibility, setColumnVisibility] = useState({}); @@ -84,6 +97,29 @@ } }, [getToken], + ); + + const handleDeleteRecord = useCallback( + async (uri: string) => { + setDeleting(true); + try { + await deleteRecord(getToken, uri); + setDeleteUri(null); + setViewRecord(null); + if (selectedCollection) { + const currentCursor = + cursorStack.length > 0 + ? cursorStack[cursorStack.length - 1] + : undefined; + fetchRecords(selectedCollection, currentCursor); + } + } catch (e: unknown) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setDeleting(false); + } + }, + [getToken, selectedCollection, cursorStack, fetchRecords], ); // Build columns dynamically from the union of all record keys @@ -146,14 +182,36 @@ }); } + cols.push({ + id: "actions", + header: "", + enableSorting: false, + enableHiding: false, + cell: ({ row }) => ( + + ), + }); + return cols; - }, [records]); + }, [records, deleting]); const table = useReactTable({ data: records, columns, state: { columnVisibility, + columnPinning: { right: ["actions"] }, }, onColumnVisibilityChange: setColumnVisibility, getCoreRowModel: getCoreRowModel(), @@ -253,9 +311,52 @@ +
+ +
)} + + { + 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"} + + + + );