mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(data-table): Introduce comprehensive DataTable component with full feature set
This commit is contained in:
40
packages/ui-common/src/components/data-table/error.tsx
Normal file
40
packages/ui-common/src/components/data-table/error.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Table as TanstackTable } from "@tanstack/react-table";
|
||||
|
||||
import { TableCell, TableRow } from "#components/table";
|
||||
import { Button } from "#components/button.tsx";
|
||||
|
||||
export function DataTableError({
|
||||
table,
|
||||
message,
|
||||
description,
|
||||
onRetry,
|
||||
}: {
|
||||
table: TanstackTable<any>;
|
||||
message?: string;
|
||||
description?: string;
|
||||
onRetry?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell colSpan={table.getVisibleFlatColumns().length}>
|
||||
<div className="flex items-center my-6 justify-center">
|
||||
<div className="text-center">
|
||||
<div className="my-4">
|
||||
<h2 className="text-xl font-semibold ">
|
||||
{message ?? "No results"}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-secondary-foreground">
|
||||
{description ?? "No results found"}
|
||||
</p>
|
||||
</div>
|
||||
{onRetry && (
|
||||
<Button variant={"outline"} onClick={onRetry}>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
119
packages/ui-common/src/components/data-table/footer.tsx
Normal file
119
packages/ui-common/src/components/data-table/footer.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Button } from "../button";
|
||||
import { DataTableFooterProps } from "./types";
|
||||
|
||||
export interface DataTableFooterOptions {
|
||||
pageSizeOptions?: number[];
|
||||
showPageSizeSelector?: boolean;
|
||||
showRowCount?: boolean;
|
||||
showPagination?: boolean;
|
||||
labels?: {
|
||||
rowsPerPage?: string;
|
||||
page?: string;
|
||||
of?: string;
|
||||
showing?: string;
|
||||
ofLabel?: string;
|
||||
items?: string;
|
||||
previous?: string;
|
||||
next?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DataTableFooterComponentProps<
|
||||
TData,
|
||||
> extends DataTableFooterProps<TData> {
|
||||
options?: DataTableFooterOptions;
|
||||
}
|
||||
|
||||
const defaultOptions: DataTableFooterOptions = {
|
||||
pageSizeOptions: [5, 10, 25, 50],
|
||||
showPageSizeSelector: true,
|
||||
showRowCount: true,
|
||||
showPagination: true,
|
||||
labels: {
|
||||
rowsPerPage: "Rows per page",
|
||||
page: "Page",
|
||||
of: "of",
|
||||
showing: "Showing",
|
||||
ofLabel: "of",
|
||||
items: "items",
|
||||
previous: "Previous",
|
||||
next: "Next",
|
||||
},
|
||||
};
|
||||
|
||||
export function DataTableFooter<TData>({
|
||||
table,
|
||||
pagination,
|
||||
options = {},
|
||||
}: DataTableFooterComponentProps<TData>) {
|
||||
const opts = { ...defaultOptions, ...options };
|
||||
const labels = { ...defaultOptions.labels, ...options.labels };
|
||||
|
||||
const pageIndex = pagination.pageIndex ?? 0;
|
||||
const pageSize = pagination.pageSize ?? 10;
|
||||
const totalCount = pagination.totalCount ?? 0;
|
||||
|
||||
const start = totalCount === 0 ? 0 : pageIndex * pageSize + 1;
|
||||
const end = Math.min((pageIndex + 1) * pageSize, totalCount);
|
||||
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
table?.setPageSize(newPageSize);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 p-4 max-sm:flex-col max-sm:items-start">
|
||||
{(opts.showPageSizeSelector || opts.showRowCount) && (
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-500">
|
||||
{opts.showPageSizeSelector && (
|
||||
<>
|
||||
<label htmlFor="page-size" className="font-medium text-slate-700">
|
||||
{labels.rowsPerPage}
|
||||
</label>
|
||||
<select
|
||||
id="page-size"
|
||||
value={pageSize}
|
||||
onChange={(e) => handlePageSizeChange(Number(e.target.value))}
|
||||
className="h-9 rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-700 outline-none transition focus:border-[#33578D]/50 focus:ring-2 focus:ring-[#33578D]/20"
|
||||
>
|
||||
{opts.pageSizeOptions?.map((size) => (
|
||||
<option key={size} value={size}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
{opts.showRowCount && (
|
||||
<span>
|
||||
{labels.showing} {start}–{end} {labels.ofLabel} {totalCount}{" "}
|
||||
{labels.items}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{opts.showPagination && (
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table?.previousPage()}
|
||||
disabled={!table?.getCanPreviousPage()}
|
||||
>
|
||||
{labels.previous}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table?.nextPage()}
|
||||
disabled={!table?.getCanNextPage()}
|
||||
>
|
||||
{labels.next}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
packages/ui-common/src/components/data-table/hooks.ts
Normal file
25
packages/ui-common/src/components/data-table/hooks.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { PaginationState } from "@tanstack/react-table";
|
||||
import { useState } from "react";
|
||||
|
||||
export const usePagination = ({
|
||||
pageSize,
|
||||
pageIndex,
|
||||
}: {
|
||||
pageSize?: number;
|
||||
pageIndex?: number;
|
||||
} = {}) => {
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: pageIndex ?? 0,
|
||||
pageSize: pageSize ?? 10,
|
||||
});
|
||||
|
||||
const setPage = (pageIndex: number) => {
|
||||
setPagination((prev) => ({ ...prev, pageIndex }));
|
||||
};
|
||||
|
||||
return {
|
||||
pagination,
|
||||
setPagination,
|
||||
setPage,
|
||||
};
|
||||
};
|
||||
12
packages/ui-common/src/components/data-table/index.ts
Normal file
12
packages/ui-common/src/components/data-table/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { DataTableError } from "./error";
|
||||
export { DataTableSkeleton } from "./skeleton";
|
||||
export { DataTableFooter, type DataTableFooterOptions } from "./footer";
|
||||
export type {
|
||||
DataTableProps,
|
||||
DataTablePagination,
|
||||
DataTableFooterProps,
|
||||
DataTableFooterComponent,
|
||||
} from "./types";
|
||||
export { usePagination } from "./hooks";
|
||||
export { DataTable } from "./table";
|
||||
export * from "@tanstack/react-table";
|
||||
16
packages/ui-common/src/components/data-table/skeleton.tsx
Normal file
16
packages/ui-common/src/components/data-table/skeleton.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Table as TanstackTable } from "@tanstack/react-table";
|
||||
|
||||
import { Skeleton } from "../skeleton";
|
||||
import { TableCell, TableRow } from "#components/table";
|
||||
|
||||
export function DataTableSkeleton({ table }: { table: TanstackTable<any> }) {
|
||||
return Array.from({ length: 10 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{table.getAllColumns().map((column) => (
|
||||
<TableCell key={column.id}>
|
||||
<Skeleton className="h-8" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
));
|
||||
}
|
||||
149
packages/ui-common/src/components/data-table/table.tsx
Normal file
149
packages/ui-common/src/components/data-table/table.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "#components/table";
|
||||
import { DataTableProps } from "./types";
|
||||
import { DataTableSkeleton } from "./skeleton";
|
||||
import { DataTableError } from "./error";
|
||||
import { DataTableFooter } from "./footer";
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
status,
|
||||
onRowClick,
|
||||
tableOptions,
|
||||
pagination,
|
||||
footer,
|
||||
footerClassName,
|
||||
containerClassName,
|
||||
error,
|
||||
emptyMessage,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const { state, ...otherOptions } = tableOptions ?? {};
|
||||
const baseState = state ?? {};
|
||||
if (pagination) {
|
||||
baseState.pagination = {
|
||||
pageIndex: pagination.pageIndex ?? 0,
|
||||
pageSize: pagination.pageSize ?? 1,
|
||||
};
|
||||
}
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
|
||||
manualPagination: true,
|
||||
...(pagination && {
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
pageCount: pagination.pageCount,
|
||||
}),
|
||||
state: baseState,
|
||||
...otherOptions,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={containerClassName}>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className={
|
||||
(header.column.columnDef.meta as Record<string, any>)
|
||||
?.headerClassName ?? ""
|
||||
}
|
||||
style={{ width: `${header.getSize()}px` }}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{status === "loading" && <DataTableSkeleton table={table} />}
|
||||
{status === "error" && (
|
||||
<DataTableError
|
||||
table={table}
|
||||
message={error?.message ?? "Something went wrong"}
|
||||
description={error?.description ?? "Please try again"}
|
||||
onRetry={error?.onRetry}
|
||||
/>
|
||||
)}
|
||||
{status === "success" &&
|
||||
(table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
role={onRowClick ? "button" : ""}
|
||||
className={
|
||||
onRowClick
|
||||
? "cursor-pointer hover:bg-accent hover:text-foreground "
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={
|
||||
(cell.column.columnDef.meta as Record<string, any>)
|
||||
?.cellClassName ?? ""
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={table.getVisibleFlatColumns().length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{emptyMessage ?? "No data"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{pagination && (
|
||||
<div className={footerClassName}>
|
||||
{footer ? (
|
||||
footer({ table, pagination })
|
||||
) : (
|
||||
<DataTableFooter table={table} pagination={pagination} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
packages/ui-common/src/components/data-table/types.ts
Normal file
38
packages/ui-common/src/components/data-table/types.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Table as TanstackTable, TableOptions, ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
export interface DataTablePagination {
|
||||
pageSize?: number;
|
||||
pageIndex?: number;
|
||||
pageCount?: number;
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
export interface DataTableFooterProps<TData> {
|
||||
table: TanstackTable<TData>;
|
||||
pagination: DataTablePagination;
|
||||
}
|
||||
|
||||
export type DataTableFooterComponent<TData> = (
|
||||
props: DataTableFooterProps<TData>
|
||||
) => React.ReactNode;
|
||||
|
||||
export interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
status?: "loading" | "error" | "success";
|
||||
onRowClick?: (row: TData) => void;
|
||||
tableOptions?: Omit<
|
||||
TableOptions<TData>,
|
||||
"data" | "columns" | "getCoreRowModel"
|
||||
>;
|
||||
pagination?: DataTablePagination;
|
||||
footer?: DataTableFooterComponent<TData>;
|
||||
footerClassName?: string;
|
||||
containerClassName?: string;
|
||||
emptyMessage?: string;
|
||||
error?: {
|
||||
message: string;
|
||||
description?: string;
|
||||
onRetry?: () => void;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user