mirror of
https://github.com/KLogicHQ/nats-console.git
synced 2026-10-11 23:41:41 +00:00
206 lines
6.9 KiB
TypeScript
206 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
ColumnDef,
|
|
ColumnFiltersState,
|
|
SortingState,
|
|
VisibilityState,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getFilteredRowModel,
|
|
getPaginationRowModel,
|
|
getSortedRowModel,
|
|
useReactTable,
|
|
} from '@tanstack/react-table';
|
|
import { ChevronDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Search } from 'lucide-react';
|
|
import { Button } from './button';
|
|
import { Input } from './input';
|
|
|
|
interface DataTableProps<TData, TValue> {
|
|
columns: ColumnDef<TData, TValue>[];
|
|
data: TData[];
|
|
searchColumn?: string;
|
|
searchPlaceholder?: string;
|
|
pageSize?: number;
|
|
showPagination?: boolean;
|
|
onRowClick?: (row: TData) => void;
|
|
}
|
|
|
|
export function DataTable<TData, TValue>({
|
|
columns,
|
|
data,
|
|
searchColumn,
|
|
searchPlaceholder = 'Search...',
|
|
pageSize = 10,
|
|
showPagination = true,
|
|
onRowClick,
|
|
}: DataTableProps<TData, TValue>) {
|
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
|
const [rowSelection, setRowSelection] = useState({});
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns,
|
|
onSortingChange: setSorting,
|
|
onColumnFiltersChange: setColumnFilters,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
onColumnVisibilityChange: setColumnVisibility,
|
|
onRowSelectionChange: setRowSelection,
|
|
initialState: {
|
|
pagination: { pageSize },
|
|
},
|
|
state: {
|
|
sorting,
|
|
columnFilters,
|
|
columnVisibility,
|
|
rowSelection,
|
|
},
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{searchColumn && (
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative flex-1 max-w-sm">
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
placeholder={searchPlaceholder}
|
|
value={(table.getColumn(searchColumn)?.getFilterValue() as string) ?? ''}
|
|
onChange={(event) =>
|
|
table.getColumn(searchColumn)?.setFilterValue(event.target.value)
|
|
}
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="border">
|
|
<table className="w-full">
|
|
<thead className="bg-muted/50">
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<tr key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => (
|
|
<th
|
|
key={header.id}
|
|
className="text-left p-4 font-medium text-sm"
|
|
style={{ width: header.getSize() !== 150 ? header.getSize() : undefined }}
|
|
>
|
|
{header.isPlaceholder ? null : (
|
|
<div
|
|
className={
|
|
header.column.getCanSort()
|
|
? 'flex items-center gap-2 cursor-pointer select-none hover:text-foreground'
|
|
: ''
|
|
}
|
|
onClick={header.column.getToggleSortingHandler()}
|
|
>
|
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
{header.column.getCanSort() && (
|
|
<ChevronDown
|
|
className={`h-4 w-4 transition-transform ${
|
|
header.column.getIsSorted() === 'asc' ? 'rotate-180' : ''
|
|
} ${header.column.getIsSorted() ? 'opacity-100' : 'opacity-0'}`}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</thead>
|
|
<tbody>
|
|
{table.getRowModel().rows?.length ? (
|
|
table.getRowModel().rows.map((row) => (
|
|
<tr
|
|
key={row.id}
|
|
data-state={row.getIsSelected() && 'selected'}
|
|
className={`border-t hover:bg-muted/30 ${
|
|
onRowClick ? 'cursor-pointer' : ''
|
|
}`}
|
|
onClick={() => onRowClick?.(row.original)}
|
|
>
|
|
{row.getVisibleCells().map((cell) => (
|
|
<td key={cell.id} className="p-4">
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))
|
|
) : (
|
|
<tr>
|
|
<td colSpan={columns.length} className="h-24 text-center text-muted-foreground">
|
|
No results.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{showPagination && (
|
|
<div className="flex items-center justify-between px-2">
|
|
<div className="text-sm text-muted-foreground">
|
|
{table.getFilteredSelectedRowModel().rows.length > 0 && (
|
|
<span>
|
|
{table.getFilteredSelectedRowModel().rows.length} of{' '}
|
|
{table.getFilteredRowModel().rows.length} row(s) selected.
|
|
</span>
|
|
)}
|
|
{table.getFilteredSelectedRowModel().rows.length === 0 && (
|
|
<span>
|
|
Showing {table.getRowModel().rows.length} of{' '}
|
|
{table.getFilteredRowModel().rows.length} row(s).
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.setPageIndex(0)}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
<ChevronsLeft className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.previousPage()}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</Button>
|
|
<span className="text-sm">
|
|
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
|
</span>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.nextPage()}
|
|
disabled={!table.getCanNextPage()}
|
|
>
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
disabled={!table.getCanNextPage()}
|
|
>
|
|
<ChevronsRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|