- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
import { useTranslation } from "@turbostarter/i18n";
|
|
import { cn } from "@turbostarter/ui";
|
|
|
|
import type { Table } from "@tanstack/react-table";
|
|
|
|
import { Button } from "#components/button";
|
|
import { Icons } from "#components/icons";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "#components/select";
|
|
|
|
interface DataTablePaginationProps<TData> extends React.ComponentProps<"div"> {
|
|
table: Table<TData>;
|
|
pageSizeOptions?: number[];
|
|
}
|
|
|
|
export function DataTablePagination<TData>({
|
|
table,
|
|
pageSizeOptions = [10, 20, 30, 40, 50],
|
|
className,
|
|
...props
|
|
}: DataTablePaginationProps<TData>) {
|
|
const { t } = useTranslation("common");
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex w-full flex-col-reverse items-center justify-between gap-4 overflow-auto p-1 sm:flex-row sm:gap-8",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{table.options.enableRowSelection && (
|
|
<div className="text-muted-foreground flex-1 text-sm whitespace-nowrap">
|
|
{t("rowsSelected", {
|
|
selected: table.getFilteredSelectedRowModel().rows.length,
|
|
total: table.getFilteredRowModel().rows.length,
|
|
})}
|
|
</div>
|
|
)}
|
|
<div className="flex flex-col-reverse items-center gap-4 sm:ml-auto sm:flex-row sm:gap-6 lg:gap-8">
|
|
<div className="flex items-center space-x-2">
|
|
<p className="text-sm font-medium whitespace-nowrap">
|
|
{t("rowsPerPage")}
|
|
</p>
|
|
<Select
|
|
value={`${table.getState().pagination.pageSize}`}
|
|
onValueChange={(value) => {
|
|
table.setPageSize(Number(value));
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 w-[4.5rem] [&[data-size]]:h-8">
|
|
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
|
</SelectTrigger>
|
|
<SelectContent side="top">
|
|
{pageSizeOptions.map((pageSize) => (
|
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
|
{pageSize}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex items-center justify-center text-sm font-medium">
|
|
{t("pageOf", {
|
|
page: table.getState().pagination.pageIndex + 1,
|
|
total: table.getPageCount() || 1,
|
|
})}
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Button
|
|
aria-label={t("first")}
|
|
variant="outline"
|
|
size="icon"
|
|
className="hidden size-8 lg:flex"
|
|
onClick={() => table.setPageIndex(0)}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
<Icons.ChevronsLeft className="size-4" />
|
|
</Button>
|
|
<Button
|
|
aria-label={t("previous")}
|
|
variant="outline"
|
|
size="icon"
|
|
className="size-8"
|
|
onClick={() => table.previousPage()}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
<Icons.ChevronLeft className="size-4" />
|
|
</Button>
|
|
<Button
|
|
aria-label={t("next")}
|
|
variant="outline"
|
|
size="icon"
|
|
className="size-8"
|
|
onClick={() => table.nextPage()}
|
|
disabled={!table.getCanNextPage()}
|
|
>
|
|
<Icons.ChevronRight className="size-4" />
|
|
</Button>
|
|
<Button
|
|
aria-label={t("last")}
|
|
variant="outline"
|
|
size="icon"
|
|
className="hidden size-8 lg:flex"
|
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
disabled={!table.getCanNextPage()}
|
|
>
|
|
<Icons.ChevronsRight className="size-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|