From 27ea2caebcf0e4cf8225931e0da6846a85c80776 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 2 May 2026 09:58:38 +0200 Subject: [PATCH] refactor: use tanstack table for filtering and sorting --- .../components/data-table-sort-header.tsx | 54 ++++ app/client/components/status-dot.tsx | 2 +- .../repositories/routes/repositories.tsx | 269 ++++++++---------- app/client/modules/volumes/routes/volumes.tsx | 231 +++++++-------- bun.lock | 5 + package.json | 1 + 6 files changed, 284 insertions(+), 278 deletions(-) create mode 100644 app/client/components/data-table-sort-header.tsx diff --git a/app/client/components/data-table-sort-header.tsx b/app/client/components/data-table-sort-header.tsx new file mode 100644 index 00000000..0a888e49 --- /dev/null +++ b/app/client/components/data-table-sort-header.tsx @@ -0,0 +1,54 @@ +import type { Column } from "@tanstack/react-table"; +import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react"; +import { Button } from "~/client/components/ui/button"; +import { cn } from "~/client/lib/utils"; + +export function DataTableSortHeader({ + column, + title, + sortDirection, + center = false, +}: { + column: Column; + title: string; + sortDirection: false | "asc" | "desc"; + center?: boolean; +}) { + const icon = + sortDirection === "desc" ? ( + + ) : sortDirection === "asc" ? ( + + ) : ( + + ); + const iconVisibility = sortDirection ? "" : "lg:invisible lg:group-hover/sort:visible"; + + if (center) { + return ( + + ); + } + + return ( + + ); +} diff --git a/app/client/components/status-dot.tsx b/app/client/components/status-dot.tsx index 5a9c8544..b1118967 100644 --- a/app/client/components/status-dot.tsx +++ b/app/client/components/status-dot.tsx @@ -54,7 +54,7 @@ export const StatusDot = ({ variant, label, animated }: StatusDotProps) => { -

{label}

+

{label}

); diff --git a/app/client/modules/repositories/routes/repositories.tsx b/app/client/modules/repositories/routes/repositories.tsx index 2e5f40db..a52cbb6b 100644 --- a/app/client/modules/repositories/routes/repositories.tsx +++ b/app/client/modules/repositories/routes/repositories.tsx @@ -1,7 +1,18 @@ import { useSuspenseQuery } from "@tanstack/react-query"; -import { ArrowDown, ArrowUp, ArrowUpDown, Database, Plus, RotateCcw } from "lucide-react"; +import { + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + type ColumnDef, + type ColumnFiltersState, + type SortingState, + useReactTable, +} from "@tanstack/react-table"; +import { Database, Plus, RotateCcw } from "lucide-react"; import { useState } from "react"; import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen"; +import { DataTableSortHeader } from "~/client/components/data-table-sort-header"; import { RepositoryIcon } from "~/client/components/repository-icon"; import { Button } from "~/client/components/ui/button"; import { Card } from "~/client/components/ui/card"; @@ -14,8 +25,6 @@ import { EmptyState } from "~/client/components/empty-state"; import { useNavigate } from "@tanstack/react-router"; import type { RepositoryBackend } from "@zerobyte/core/restic"; -type SortColumn = "name" | "backend" | "status" | "compression"; -type SortDirection = "asc" | "desc"; type RepositoryRow = { id: string; shortId: string; @@ -25,31 +34,62 @@ type RepositoryRow = { compressionMode?: string | null; }; -const getSortValue = (column: SortColumn, repository: RepositoryRow) => { - switch (column) { - case "name": - return repository.name; - case "backend": - return repository.type; - case "status": - return repository.status || ""; - case "compression": - return repository.compressionMode || ""; - } -}; +const repositoryColumns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => , + cell: ({ row }) => ( +
+ {row.original.name} +
+ ), + }, + { + accessorKey: "type", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + + {row.original.type} + + ), + filterFn: (row, id, value) => row.getValue(id) === value, + }, + { + accessorFn: (row) => row.compressionMode || "off", + id: "compressionMode", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.compressionMode || "off"} + + ), + sortingFn: "alphanumeric", + }, + { + accessorFn: (row) => row.status || "unknown", + id: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + sortingFn: "alphanumeric", + filterFn: (row, id, value) => row.getValue(id) === value, + }, +]; export function RepositoriesPage() { - const [searchQuery, setSearchQuery] = useState(""); - const [statusFilter, setStatusFilter] = useState(""); - const [backendFilter, setBackendFilter] = useState(""); - const [sortColumn, setSortColumn] = useState("name"); - const [sortDirection, setSortDirection] = useState("asc"); - - const clearFilters = () => { - setSearchQuery(""); - setStatusFilter(""); - setBackendFilter(""); - }; + const [columnFilters, setColumnFilters] = useState([]); + const [sorting, setSorting] = useState([{ id: "name", desc: false }]); const navigate = useNavigate(); @@ -59,45 +99,23 @@ export function RepositoriesPage() { const repositories = data as RepositoryRow[]; - const toggleSort = (column: SortColumn) => { - if (sortColumn === column) { - setSortDirection((prev) => (prev === "asc" ? "desc" : "asc")); - return; - } - - setSortColumn(column); - setSortDirection("asc"); - }; - - const renderSortIcon = (column: SortColumn) => { - if (sortColumn !== column) { - return ; - } - - return sortDirection === "asc" ? ( - - ) : ( - - ); - }; - - const filteredRepositories = repositories.filter((repository) => { - const matchesSearch = repository.name.toLowerCase().includes(searchQuery.toLowerCase()); - const matchesStatus = !statusFilter || repository.status === statusFilter; - const matchesBackend = !backendFilter || repository.type === backendFilter; - return matchesSearch && matchesStatus && matchesBackend; + const table = useReactTable({ + data: repositories, + columns: repositoryColumns, + state: { columnFilters, sorting }, + onColumnFiltersChange: setColumnFilters, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), }); + const rows = table.getRowModel().rows; + const hasFilters = columnFilters.length > 0; - const sortedFilteredRepositories = [...filteredRepositories].sort((a, b) => { - const valueA = getSortValue(sortColumn, a).toLowerCase(); - const valueB = getSortValue(sortColumn, b).toLowerCase(); - const result = valueA.localeCompare(valueB); - - return sortDirection === "asc" ? result : -result; - }); + const clearFilters = () => table.resetColumnFilters(); const hasNoRepositories = repositories.length === 0; - const hasNoFilteredRepositories = sortedFilteredRepositories.length === 0 && !hasNoRepositories; + const hasNoFilteredRepositories = rows.length === 0 && !hasNoRepositories; if (hasNoRepositories) { return ( @@ -115,6 +133,10 @@ export function RepositoriesPage() { ); } + const search = (table.getColumn("name")?.getFilterValue() as string) ?? ""; + const type = (table.getColumn("type")?.getFilterValue() as string) ?? ""; + const status = (table.getColumn("status")?.getFilterValue() as string) ?? ""; + return (
@@ -122,10 +144,10 @@ export function RepositoriesPage() { setSearchQuery(e.target.value)} + value={search} + onChange={(e) => table.getColumn("name")?.setFilterValue(e.target.value)} /> - table.getColumn("status")?.setFilterValue(value)}> @@ -135,7 +157,7 @@ export function RepositoriesPage() { Unknown - table.getColumn("type")?.setFilterValue(value)}> @@ -146,7 +168,7 @@ export function RepositoriesPage() { Google Cloud Storage - {(searchQuery || statusFilter || backendFilter) && ( + {hasFilters && (