add pagination
This commit is contained in:
parent
5e29ac78f7
commit
3776ce6e22
78
src/components/pagination/pagination.tsx
Normal file
78
src/components/pagination/pagination.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
||||||
|
import { RowsPerPage } from "./rows-per-page";
|
||||||
|
|
||||||
|
interface PaginationProps {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalItems: number;
|
||||||
|
itemsPerPage: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
onPageSizeChange?: (pageSize: number) => void;
|
||||||
|
pageSizeOptions?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomPagination({
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
totalItems,
|
||||||
|
itemsPerPage,
|
||||||
|
onPageChange,
|
||||||
|
onPageSizeChange,
|
||||||
|
pageSizeOptions
|
||||||
|
}: PaginationProps) {
|
||||||
|
let startItem = (currentPage - 1) * itemsPerPage + 1;
|
||||||
|
let endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||||
|
if (currentPage === totalPages) {
|
||||||
|
startItem = totalItems - itemsPerPage + 1;
|
||||||
|
endItem = totalItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||||
|
{onPageSizeChange && (
|
||||||
|
<RowsPerPage
|
||||||
|
pageSize={itemsPerPage}
|
||||||
|
onPageSizeChange={onPageSizeChange}
|
||||||
|
options={pageSizeOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onPageChange(1)}
|
||||||
|
disabled={currentPage === 1}>
|
||||||
|
<ChevronsLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<span className="mx-2 text-sm">
|
||||||
|
{startItem}-{endItem} của {totalItems}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onPageChange(totalPages)}
|
||||||
|
disabled={currentPage === totalPages}>
|
||||||
|
<ChevronsRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
src/components/pagination/rows-per-page.tsx
Normal file
45
src/components/pagination/rows-per-page.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
interface RowsPerPageProps {
|
||||||
|
pageSize: number;
|
||||||
|
onPageSizeChange: (pageSize: number) => void;
|
||||||
|
options?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RowsPerPage({
|
||||||
|
pageSize,
|
||||||
|
onPageSizeChange,
|
||||||
|
options = [5, 10, 15, 20]
|
||||||
|
}: RowsPerPageProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Hiển thị</span>
|
||||||
|
<Select
|
||||||
|
value={pageSize?.toString()}
|
||||||
|
onValueChange={(value) => onPageSizeChange(Number(value))}>
|
||||||
|
<SelectTrigger className="h-8 w-[70px]">
|
||||||
|
<SelectValue placeholder={pageSize?.toString()} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{!options.includes(pageSize) && (
|
||||||
|
<SelectItem value={pageSize?.toString()} disabled>
|
||||||
|
{pageSize}
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
{options.map((option) => (
|
||||||
|
<SelectItem key={option} value={option?.toString()}>
|
||||||
|
{option}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span className="text-sm text-muted-foreground">mục</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
|
|
@ -13,7 +14,8 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useEffect } from "react";
|
import { CustomPagination } from "@/components/pagination/pagination";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface VersionTableProps<TData> {
|
interface VersionTableProps<TData> {
|
||||||
data: TData[];
|
data: TData[];
|
||||||
|
|
@ -23,6 +25,10 @@ interface VersionTableProps<TData> {
|
||||||
onRowClick?: (row: TData) => void;
|
onRowClick?: (row: TData) => void;
|
||||||
scrollable?: boolean;
|
scrollable?: boolean;
|
||||||
maxHeight?: string;
|
maxHeight?: string;
|
||||||
|
// Pagination options
|
||||||
|
enablePagination?: boolean;
|
||||||
|
defaultPageSize?: number;
|
||||||
|
pageSizeOptions?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VersionTable<TData>({
|
export function VersionTable<TData>({
|
||||||
|
|
@ -33,11 +39,24 @@ export function VersionTable<TData>({
|
||||||
onRowClick,
|
onRowClick,
|
||||||
scrollable = false,
|
scrollable = false,
|
||||||
maxHeight = "calc(100vh - 320px)",
|
maxHeight = "calc(100vh - 320px)",
|
||||||
|
enablePagination = false,
|
||||||
|
defaultPageSize = 10,
|
||||||
|
pageSizeOptions = [5, 10, 15, 20],
|
||||||
}: VersionTableProps<TData>) {
|
}: VersionTableProps<TData>) {
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: defaultPageSize,
|
||||||
|
});
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
...(enablePagination && {
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
state: { pagination },
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
}),
|
||||||
getRowId: (row: any) => row.id?.toString(),
|
getRowId: (row: any) => row.id?.toString(),
|
||||||
enableRowSelection: true,
|
enableRowSelection: true,
|
||||||
});
|
});
|
||||||
|
|
@ -96,13 +115,41 @@ export function VersionTable<TData>({
|
||||||
|
|
||||||
if (scrollable) {
|
if (scrollable) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border">
|
<div className="space-y-4">
|
||||||
<ScrollArea className="w-full" style={{ height: maxHeight }}>
|
<div className="rounded-md border">
|
||||||
{tableContent}
|
<ScrollArea className="w-full" style={{ height: maxHeight }}>
|
||||||
</ScrollArea>
|
{tableContent}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
{enablePagination && data.length > 0 && (
|
||||||
|
<CustomPagination
|
||||||
|
currentPage={table.getState().pagination.pageIndex + 1}
|
||||||
|
totalPages={table.getPageCount()}
|
||||||
|
totalItems={data.length}
|
||||||
|
itemsPerPage={table.getState().pagination.pageSize}
|
||||||
|
onPageChange={(page) => table.setPageIndex(page - 1)}
|
||||||
|
onPageSizeChange={(size) => table.setPageSize(size)}
|
||||||
|
pageSizeOptions={pageSizeOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="rounded-md border">{tableContent}</div>;
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-md border">{tableContent}</div>
|
||||||
|
{enablePagination && data.length > 0 && (
|
||||||
|
<CustomPagination
|
||||||
|
currentPage={table.getState().pagination.pageIndex + 1}
|
||||||
|
totalPages={table.getPageCount()}
|
||||||
|
totalItems={data.length}
|
||||||
|
itemsPerPage={table.getState().pagination.pageSize}
|
||||||
|
onPageChange={(page) => table.setPageIndex(page - 1)}
|
||||||
|
onPageSizeChange={(size) => table.setPageSize(size)}
|
||||||
|
pageSizeOptions={pageSizeOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,8 @@ function AgentsPage() {
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
updateLoading={updateMutation.isPending}
|
updateLoading={updateMutation.isPending}
|
||||||
rooms={roomData}
|
rooms={roomData}
|
||||||
|
enablePagination
|
||||||
|
defaultPageSize={10}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,8 @@ function AppsComponent() {
|
||||||
addToRequiredLoading={addRequiredFileMutation.isPending}
|
addToRequiredLoading={addRequiredFileMutation.isPending}
|
||||||
onTableInit={setTable}
|
onTableInit={setTable}
|
||||||
rooms={roomData}
|
rooms={roomData}
|
||||||
|
enablePagination
|
||||||
|
defaultPageSize={10}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,8 @@ function BlacklistComponent() {
|
||||||
onAdd={handleAddNewBlacklist}
|
onAdd={handleAddNewBlacklist}
|
||||||
onDelete={handleDeleteBlacklist}
|
onDelete={handleDeleteBlacklist}
|
||||||
onUpdate={handleUpdateDevice}
|
onUpdate={handleUpdateDevice}
|
||||||
|
enablePagination
|
||||||
|
defaultPageSize={10}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,8 @@ function CommandPage() {
|
||||||
onRowClick={(row) => setDetailPanelCommand(row)}
|
onRowClick={(row) => setDetailPanelCommand(row)}
|
||||||
scrollable={true}
|
scrollable={true}
|
||||||
maxHeight="500px"
|
maxHeight="500px"
|
||||||
|
enablePagination
|
||||||
|
defaultPageSize={10}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Detail Dialog Popup */}
|
{/* Detail Dialog Popup */}
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,8 @@ function RoleComponent() {
|
||||||
tableDescription="Các vai trò trong hệ thống và quyền hạn tương ứng"
|
tableDescription="Các vai trò trong hệ thống và quyền hạn tương ứng"
|
||||||
createButtonLabel="Tạo role mới"
|
createButtonLabel="Tạo role mới"
|
||||||
createLink="/role/create"
|
createLink="/role/create"
|
||||||
|
enablePagination
|
||||||
|
defaultPageSize={10}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ function RoomComponent() {
|
||||||
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate({
|
navigate({
|
||||||
to: "/room/$roomName",
|
to: "/rooms/$roomName",
|
||||||
params: { roomName: row.original.name },
|
params: { roomName: row.original.name },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ interface AppManagerTemplateProps<TData> {
|
||||||
onTableInit?: (table: any) => void;
|
onTableInit?: (table: any) => void;
|
||||||
rooms?: Room[];
|
rooms?: Room[];
|
||||||
devices?: string[];
|
devices?: string[];
|
||||||
|
// Pagination options
|
||||||
|
enablePagination?: boolean;
|
||||||
|
defaultPageSize?: number;
|
||||||
|
pageSizeOptions?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppManagerTemplate<TData>({
|
export function AppManagerTemplate<TData>({
|
||||||
|
|
@ -69,6 +73,9 @@ export function AppManagerTemplate<TData>({
|
||||||
onTableInit,
|
onTableInit,
|
||||||
rooms = [],
|
rooms = [],
|
||||||
devices = [],
|
devices = [],
|
||||||
|
enablePagination = false,
|
||||||
|
defaultPageSize = 10,
|
||||||
|
pageSizeOptions = [5, 10, 15, 20],
|
||||||
}: AppManagerTemplateProps<TData>) {
|
}: AppManagerTemplateProps<TData>) {
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [dialogType, setDialogType] = useState<"room" | "device" | "download-room" | "download-device" | null>(null);
|
const [dialogType, setDialogType] = useState<"room" | "device" | "download-room" | "download-device" | null>(null);
|
||||||
|
|
@ -146,6 +153,9 @@ export function AppManagerTemplate<TData>({
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onTableInit={onTableInit}
|
onTableInit={onTableInit}
|
||||||
|
enablePagination={enablePagination}
|
||||||
|
defaultPageSize={defaultPageSize}
|
||||||
|
pageSizeOptions={pageSizeOptions}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,11 @@ interface CommandSubmitTemplateProps<T extends { id: number }> {
|
||||||
onRowClick?: (row: T) => void;
|
onRowClick?: (row: T) => void;
|
||||||
scrollable?: boolean;
|
scrollable?: boolean;
|
||||||
maxHeight?: string;
|
maxHeight?: string;
|
||||||
|
|
||||||
|
// Pagination options
|
||||||
|
enablePagination?: boolean;
|
||||||
|
defaultPageSize?: number;
|
||||||
|
pageSizeOptions?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommandSubmitTemplate<T extends { id: number }>({
|
export function CommandSubmitTemplate<T extends { id: number }>({
|
||||||
|
|
@ -86,6 +91,9 @@ export function CommandSubmitTemplate<T extends { id: number }>({
|
||||||
onRowClick,
|
onRowClick,
|
||||||
scrollable = true,
|
scrollable = true,
|
||||||
maxHeight = "calc(100vh - 320px)",
|
maxHeight = "calc(100vh - 320px)",
|
||||||
|
enablePagination = false,
|
||||||
|
defaultPageSize = 10,
|
||||||
|
pageSizeOptions = [5, 10, 15, 20],
|
||||||
}: CommandSubmitTemplateProps<T>) {
|
}: CommandSubmitTemplateProps<T>) {
|
||||||
const [activeTab, setActiveTab] = useState<"list" | "execute">("list");
|
const [activeTab, setActiveTab] = useState<"list" | "execute">("list");
|
||||||
const [customCommand, setCustomCommand] = useState("");
|
const [customCommand, setCustomCommand] = useState("");
|
||||||
|
|
@ -252,6 +260,9 @@ export function CommandSubmitTemplate<T extends { id: number }>({
|
||||||
onRowClick={onRowClick}
|
onRowClick={onRowClick}
|
||||||
scrollable={scrollable}
|
scrollable={scrollable}
|
||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
|
enablePagination={enablePagination}
|
||||||
|
defaultPageSize={defaultPageSize}
|
||||||
|
pageSizeOptions={pageSizeOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ interface RoleManagerTemplateProps<TData> {
|
||||||
createButtonLabel?: string;
|
createButtonLabel?: string;
|
||||||
createLink?: string;
|
createLink?: string;
|
||||||
headerActions?: ReactNode;
|
headerActions?: ReactNode;
|
||||||
|
// Pagination options
|
||||||
|
enablePagination?: boolean;
|
||||||
|
defaultPageSize?: number;
|
||||||
|
pageSizeOptions?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RoleManagerTemplate<TData>({
|
export function RoleManagerTemplate<TData>({
|
||||||
|
|
@ -41,6 +45,9 @@ export function RoleManagerTemplate<TData>({
|
||||||
createButtonLabel = "Tạo mới",
|
createButtonLabel = "Tạo mới",
|
||||||
createLink,
|
createLink,
|
||||||
headerActions,
|
headerActions,
|
||||||
|
enablePagination = false,
|
||||||
|
defaultPageSize = 10,
|
||||||
|
pageSizeOptions = [5, 10, 15, 20],
|
||||||
}: RoleManagerTemplateProps<TData>) {
|
}: RoleManagerTemplateProps<TData>) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-6 space-y-4">
|
<div className="w-full px-6 space-y-4">
|
||||||
|
|
@ -79,6 +86,9 @@ export function RoleManagerTemplate<TData>({
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onTableInit={onTableInit}
|
onTableInit={onTableInit}
|
||||||
|
enablePagination={enablePagination}
|
||||||
|
defaultPageSize={defaultPageSize}
|
||||||
|
pageSizeOptions={pageSizeOptions}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ interface BlackListManagerTemplateProps<TData> {
|
||||||
updateLoading?: boolean;
|
updateLoading?: boolean;
|
||||||
onTableInit?: (table: any) => void;
|
onTableInit?: (table: any) => void;
|
||||||
rooms: Room[];
|
rooms: Room[];
|
||||||
|
// Pagination options
|
||||||
|
enablePagination?: boolean;
|
||||||
|
defaultPageSize?: number;
|
||||||
|
pageSizeOptions?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BlackListManagerTemplate<TData>({
|
export function BlackListManagerTemplate<TData>({
|
||||||
|
|
@ -45,6 +49,9 @@ export function BlackListManagerTemplate<TData>({
|
||||||
updateLoading,
|
updateLoading,
|
||||||
onTableInit,
|
onTableInit,
|
||||||
rooms = [],
|
rooms = [],
|
||||||
|
enablePagination = false,
|
||||||
|
defaultPageSize = 10,
|
||||||
|
pageSizeOptions = [5, 10, 15, 20],
|
||||||
}: BlackListManagerTemplateProps<TData>) {
|
}: BlackListManagerTemplateProps<TData>) {
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [dialogType, setDialogType] = useState<"room" | "device" | null>(null);
|
const [dialogType, setDialogType] = useState<"room" | "device" | null>(null);
|
||||||
|
|
@ -102,6 +109,9 @@ export function BlackListManagerTemplate<TData>({
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onTableInit={onTableInit}
|
onTableInit={onTableInit}
|
||||||
|
enablePagination={enablePagination}
|
||||||
|
defaultPageSize={defaultPageSize}
|
||||||
|
pageSizeOptions={pageSizeOptions}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user