Files
rustfs-console/components/data-table/data-table.tsx
T

174 lines
6.4 KiB
TypeScript

"use client"
import * as React from "react"
import { flexRender, type Column, type Table } from "@tanstack/react-table"
import { RiArrowUpDownLine, RiArrowUpSLine, RiArrowDownSLine } from "@remixicon/react"
import { ScrollArea } from "@/components/ui/scroll-area"
import { TableBody, TableCell, TableHead, TableHeader, TableRow, Table as UiTable } from "@/components/ui/table"
import { Spinner } from "@/components/ui/spinner"
import { EmptyState } from "@/components/empty-state"
import { cn } from "@/lib/utils"
interface DataTableProps<TData> {
table: Table<TData>
isLoading?: boolean
emptyTitle?: string
emptyDescription?: string
caption?: string
className?: string
tableClass?: string
stickyHeader?: boolean
bodyHeight?: string
}
function getColumnStyles<TData>(column: Column<TData, unknown>) {
const meta = column.columnDef.meta as
| { width?: number | string; minWidth?: number | string; maxWidth?: number | string }
| undefined
if (!meta) return undefined
const styles: React.CSSProperties = {}
if (meta.width != null) {
styles.width = typeof meta.width === "number" ? `${meta.width}px` : meta.width
}
if (meta.minWidth != null) {
styles.minWidth = typeof meta.minWidth === "number" ? `${meta.minWidth}px` : meta.minWidth
}
if (meta.maxWidth != null) {
styles.maxWidth = typeof meta.maxWidth === "number" ? `${meta.maxWidth}px` : meta.maxWidth
}
return Object.keys(styles).length > 0 ? styles : undefined
}
function canSort<TData>(column: Column<TData, unknown>) {
const def = column.columnDef
if (def.enableSorting === false) return false
const hasAccessorKey =
Object.prototype.hasOwnProperty.call(def, "accessorKey") &&
typeof (def as { accessorKey?: unknown }).accessorKey === "string" &&
((def as { accessorKey: string }).accessorKey?.length ?? 0) > 0
const hasAccessorFn =
Object.prototype.hasOwnProperty.call(def, "accessorFn") &&
typeof (def as { accessorFn?: unknown }).accessorFn === "function"
return hasAccessorKey || hasAccessorFn
}
function getAriaSort<TData>(column: Column<TData, unknown>): React.AriaAttributes["aria-sort"] {
if (!canSort(column)) return undefined
const direction = column.getIsSorted()
if (direction === "asc") return "ascending"
if (direction === "desc") return "descending"
return "none"
}
export function DataTable<TData>({
table,
isLoading = false,
emptyTitle = "No data",
emptyDescription = "There is nothing to display yet.",
caption,
className,
tableClass,
stickyHeader = false,
bodyHeight,
}: DataTableProps<TData>) {
const visibleColumnCount = table.getVisibleLeafColumns().length
const hasRows = table.getRowModel().rows.length > 0
const tableContent = (
<UiTable className={cn("min-w-full tabular-nums", tableClass)} aria-busy={isLoading}>
{caption ? <caption className="sr-only">{caption}</caption> : null}
<TableHeader className={stickyHeader ? "sticky top-0 z-10 bg-muted/40 backdrop-blur" : "bg-muted/20"}>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow
key={headerGroup.id}
className={stickyHeader ? "bg-muted/40 backdrop-blur" : "bg-muted/20 hover:bg-muted/30"}
>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
scope="col"
aria-sort={getAriaSort(header.column)}
className={bodyHeight ? undefined : "py-2"}
style={getColumnStyles(header.column)}
>
{!header.isPlaceholder && (
<>
{canSort(header.column) ? (
<button
type="button"
className="flex cursor-pointer items-center gap-2 select-none rounded-none outline-none hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring/50"
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() === "asc" ? (
<RiArrowUpSLine className="size-4" aria-hidden />
) : header.column.getIsSorted() === "desc" ? (
<RiArrowDownSLine className="size-4" aria-hidden />
) : (
<RiArrowUpDownLine className="size-4 text-muted-foreground opacity-50" aria-hidden />
)}
</button>
) : (
flexRender(header.column.columnDef.header, header.getContext())
)}
</>
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={visibleColumnCount} className="h-48 text-center align-middle">
<div className="flex flex-col items-center gap-2">
<Spinner className="size-6" />
<span className="text-sm text-muted-foreground">Loading…</span>
</div>
</TableCell>
</TableRow>
) : hasRows ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() ? "selected" : undefined}
className="transition-colors hover:bg-muted/40"
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={bodyHeight ? undefined : "py-2"}
style={getColumnStyles(cell.column)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={visibleColumnCount} className="h-48">
<EmptyState title={emptyTitle} description={emptyDescription} />
</TableCell>
</TableRow>
)}
</TableBody>
</UiTable>
)
return (
<div className={cn("flex flex-col gap-4", className)}>
{bodyHeight ? (
<ScrollArea className={cn("border", bodyHeight)}>{tableContent}</ScrollArea>
) : (
<div className="w-full border">{tableContent}</div>
)}
</div>
)
}