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