From ddb05c81cff1e9acf0b79643d3c8c3c462aeba0f Mon Sep 17 00:00:00 2001 From: Trezy Date: Tue, 17 Feb 2026 15:00:28 +0000 Subject: [PATCH] feat: add column visibility and better scrolling to records table --- web/src/app/(dashboard)/records/page.tsx | 282 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------------------------------------------------------------------------- web/src/components/data-table/data-table-toolbar.tsx | 5 +---- web/src/components/data-table/data-table-view-options.tsx | 16 ++++++---------- web/src/components/data-table/data-table.tsx | 24 ++++++++++++++++++------ web/src/components/ui/sidebar.tsx | 2 +- 5 file(s) changed, 149 insertion(s)(+), 180 deletion(s)(-) 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 @@ -1,133 +1,137 @@ -"use client" +"use client"; -import { useCallback, useEffect, useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useState } from "react"; import { type ColumnDef, - flexRender, + type VisibilityState, getCoreRowModel, useReactTable, -} from "@tanstack/react-table" +} from "@tanstack/react-table"; -import { useAuth } from "@/lib/auth-context" +import { useAuth } from "@/lib/auth-context"; import { getStats, getAdminRecords, type CollectionStat, type AdminRecord, -} from "@/lib/api" -import { SiteHeader } from "@/components/site-header" -import { Button } from "@/components/ui/button" +} from "@/lib/api"; +import { DataTable } from "@/components/data-table/data-table"; +import { DataTableViewOptions } from "@/components/data-table/data-table-view-options"; +import { SiteHeader } from "@/components/site-header"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, -} from "@/components/ui/dialog" +} from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" +} from "@/components/ui/select"; +import { ChevronLeft, ChevronRight } from "lucide-react"; function parseAtUri(uri: string): { did: string; rkey: string } { - const parts = uri.replace("at://", "").split("/") - return { did: parts[0] ?? "", rkey: parts[2] ?? "" } + 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 (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) + return String(value); + return JSON.stringify(value); } export default function RecordsPage() { - const { getToken } = useAuth() - 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 [error, setError] = useState(null) - const [viewRecord, setViewRecord] = useState(null) + const { getToken } = useAuth(); + 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 [error, setError] = useState(null); + const [viewRecord, setViewRecord] = useState(null); + + const [columnVisibility, setColumnVisibility] = useState({}); useEffect(() => { getStats(getToken) .then((stats) => setCollections(stats.collections)) - .catch((e) => setError(e.message)) - }, [getToken]) + .catch((e) => setError(e.message)); + }, [getToken]); const fetchRecords = useCallback( async (collection: string, cursor?: string) => { - setLoading(true) - setError(null) + setLoading(true); + setError(null); try { - const data = await getAdminRecords(getToken, collection, 20, cursor) - setRecords(data.records) - setNextCursor(data.cursor) + const data = await getAdminRecords(getToken, collection, 20, cursor); + setRecords(data.records); + setNextCursor(data.cursor); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)) - setRecords([]) - setNextCursor(undefined) + setError(e instanceof Error ? e.message : String(e)); + setRecords([]); + setNextCursor(undefined); } finally { - setLoading(false) + setLoading(false); } }, - [getToken] - ) + [getToken], + ); // Build columns dynamically from the union of all record keys const columns = useMemo[]>(() => { - const keySet = new Set() + const keySet = new Set(); for (const r of records) { for (const key of Object.keys(r.record)) { - keySet.add(key) + keySet.add(key); } } const cols: ColumnDef[] = [ { id: "did", + accessorFn: (row) => parseAtUri(row.uri).did, header: "DID", - accessorFn: (row) => parseAtUri(row.uri).did, cell: ({ getValue }) => ( {getValue()} ), + enableSorting: false, + enableHiding: false, + meta: { label: "DID" }, }, { id: "rkey", - header: "Rkey", accessorFn: (row) => parseAtUri(row.uri).rkey, + header: "Record Key", cell: ({ getValue }) => ( {getValue()} ), + enableSorting: false, + enableHiding: false, + meta: { label: "Record Key" }, }, - ] + ]; for (const key of keySet) { cols.push({ id: key, - header: key, accessorFn: (row) => row.record[key], + header: key, + enableSorting: false, cell: ({ getValue }) => { - const val = getValue() - const str = formatCellValue(val) + const val = getValue(); + const str = formatCellValue(val); return ( {str} - ) + ); }, - }) + meta: { label: key }, + }); } - return cols - }, [records]) + return cols; + }, [records]); const table = useReactTable({ data: records, columns, + state: { + columnVisibility, + }, + onColumnVisibilityChange: setColumnVisibility, getCoreRowModel: getCoreRowModel(), getRowId: (row) => row.uri, - }) + }); function handleSelectCollection(collection: string) { - setSelectedCollection(collection) - setCursorStack([]) - setNextCursor(undefined) - fetchRecords(collection) + setSelectedCollection(collection); + setCursorStack([]); + setNextCursor(undefined); + setColumnVisibility({}); + fetchRecords(collection); } function handleNext() { - if (!nextCursor || !selectedCollection) return - setCursorStack((prev) => [...prev, nextCursor]) - fetchRecords(selectedCollection, nextCursor) + 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) + 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 ( @@ -178,105 +188,59 @@
{error &&

{error}

} -
- -
+ +
+ + +
+
{selectedCollection && ( - <> -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ))} - - ))} - - - {loading && ( - - - Loading... - - - )} - {!loading && table.getRowModel().rows.length === 0 && ( - - - No records found. - - - )} - {!loading && - table.getRowModel().rows.map((row) => ( - setViewRecord(row.original)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
-
- -
+
+

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

+
- +
)} {viewRecord && ( @@ -295,5 +259,5 @@ )}
- ) + ); } diff --git a/web/src/components/data-table/data-table-toolbar.tsx b/web/src/components/data-table/data-table-toolbar.tsx --- a/web/src/components/data-table/data-table-toolbar.tsx +++ b/web/src/components/data-table/data-table-toolbar.tsx @@ -25,10 +25,7 @@ ...props }: DataTableToolbarProps) { const isFiltered = table.getState().columnFilters.length > 0; - const columns = React.useMemo( - () => table.getAllColumns().filter((column) => column.getCanFilter()), - [table], - ); + const columns = table.getAllColumns().filter((column) => column.getCanFilter()); const onReset = React.useCallback(() => { table.resetColumnFilters(); diff --git a/web/src/components/data-table/data-table-view-options.tsx b/web/src/components/data-table/data-table-view-options.tsx --- a/web/src/components/data-table/data-table-view-options.tsx +++ b/web/src/components/data-table/data-table-view-options.tsx @@ -31,16 +31,12 @@ table, disabled, ...props }: DataTableViewOptionsProps) { - const columns = React.useMemo( - () => - table - .getAllColumns() - .filter( - (column) => - typeof column.accessorFn !== "undefined" && column.getCanHide(), - ), - [table], - ); + const columns = table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide(), + ); return ( 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 @@ -18,11 +18,15 @@ interface DataTableProps extends React.ComponentProps<"div"> { table: TanstackTable; actionBar?: React.ReactNode; + showPagination?: boolean; + onRowClick?: (row: TData) => void; } export function DataTable({ table, actionBar, + showPagination = true, + onRowClick, children, className, ...props @@ -63,6 +67,12 @@ table.getRowModel().rows.map((row) => ( onRowClick(row.original) + : undefined + } > {row.getVisibleCells().map((cell) => (
-
- - {actionBar && - table.getFilteredSelectedRowModel().rows.length > 0 && - actionBar} -
+ {showPagination && ( +
+ + {actionBar && + table.getFilteredSelectedRowModel().rows.length > 0 && + actionBar} +
+ )} ); } diff --git a/web/src/components/ui/sidebar.tsx b/web/src/components/ui/sidebar.tsx --- a/web/src/components/ui/sidebar.tsx +++ b/web/src/components/ui/sidebar.tsx @@ -309,7 +309,7 @@ return (