mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-11 14:35:17 +00:00
Merge pull request #3159 from quochuydev/feat/add-domains-grid-table-toggle
feat: add grid/table view toggle for domains page
This commit is contained in:
@@ -0,0 +1,303 @@
|
|||||||
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
ArrowUpDown,
|
||||||
|
CheckCircle2,
|
||||||
|
ExternalLink,
|
||||||
|
Loader2,
|
||||||
|
PenBoxIcon,
|
||||||
|
RefreshCw,
|
||||||
|
Server,
|
||||||
|
Trash2,
|
||||||
|
XCircle,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import type { RouterOutputs } from "@/utils/api";
|
||||||
|
import type { ValidationStates } from "./show-domains";
|
||||||
|
import { AddDomain } from "./handle-domain";
|
||||||
|
import { DnsHelperModal } from "./dns-helper-modal";
|
||||||
|
|
||||||
|
export type Domain =
|
||||||
|
| RouterOutputs["domain"]["byApplicationId"][0]
|
||||||
|
| RouterOutputs["domain"]["byComposeId"][0];
|
||||||
|
|
||||||
|
interface ColumnsProps {
|
||||||
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
|
validationStates: ValidationStates;
|
||||||
|
handleValidateDomain: (host: string) => Promise<void>;
|
||||||
|
handleDeleteDomain: (domainId: string) => Promise<void>;
|
||||||
|
isDeleting: boolean;
|
||||||
|
serverIp?: string;
|
||||||
|
canCreateDomain: boolean;
|
||||||
|
canDeleteDomain: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createColumns = ({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
validationStates,
|
||||||
|
handleValidateDomain,
|
||||||
|
handleDeleteDomain,
|
||||||
|
isDeleting,
|
||||||
|
serverIp,
|
||||||
|
canCreateDomain,
|
||||||
|
canDeleteDomain,
|
||||||
|
}: ColumnsProps): ColumnDef<Domain>[] => [
|
||||||
|
...(type === "compose"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
accessorKey: "serviceName",
|
||||||
|
header: "Service",
|
||||||
|
cell: ({ row }: { row: { getValue: (key: string) => unknown } }) => {
|
||||||
|
const serviceName = row.getValue("serviceName") as string | null;
|
||||||
|
if (!serviceName) return null;
|
||||||
|
return (
|
||||||
|
<Badge variant="outline">
|
||||||
|
<Server className="size-3 mr-1" />
|
||||||
|
{serviceName}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} satisfies ColumnDef<Domain>,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
accessorKey: "host",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Host
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const domain = row.original;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className="flex items-center gap-2 font-medium hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||||
|
>
|
||||||
|
{domain.host}
|
||||||
|
<ExternalLink className="size-3" />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "path",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Path
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const path = row.getValue("path") as string;
|
||||||
|
return <div className="font-mono text-sm">{path || "/"}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "port",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Port
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const port = row.getValue("port") as number;
|
||||||
|
return <Badge variant="secondary">{port}</Badge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "customEntrypoint",
|
||||||
|
header: "Entrypoint",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const entrypoint = row.getValue("customEntrypoint") as string | null;
|
||||||
|
if (!entrypoint) return <span className="text-muted-foreground">-</span>;
|
||||||
|
return <div className="font-mono text-sm">{entrypoint}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "https",
|
||||||
|
header: "Protocol",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const https = row.getValue("https") as boolean;
|
||||||
|
return (
|
||||||
|
<Badge variant={https ? "outline" : "secondary"}>
|
||||||
|
{https ? "HTTPS" : "HTTP"}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "certificate",
|
||||||
|
header: "Certificate",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const domain = row.original;
|
||||||
|
const validationState = validationStates[domain.host];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{domain.certificateType && (
|
||||||
|
<Badge variant="outline" className="capitalize">
|
||||||
|
{domain.certificateType}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{!domain.host.includes("traefik.me") && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={
|
||||||
|
validationState?.isValid
|
||||||
|
? "bg-green-500/10 text-green-500 cursor-pointer"
|
||||||
|
: validationState?.error
|
||||||
|
? "bg-red-500/10 text-red-500 cursor-pointer"
|
||||||
|
: "bg-yellow-500/10 text-yellow-500 cursor-pointer"
|
||||||
|
}
|
||||||
|
onClick={() => handleValidateDomain(domain.host)}
|
||||||
|
>
|
||||||
|
{validationState?.isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="size-3 mr-1 animate-spin" />
|
||||||
|
Checking...
|
||||||
|
</>
|
||||||
|
) : validationState?.isValid ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle2 className="size-3 mr-1" />
|
||||||
|
{validationState.message && validationState.cdnProvider
|
||||||
|
? `${validationState.cdnProvider}`
|
||||||
|
: "Valid"}
|
||||||
|
</>
|
||||||
|
) : validationState?.error ? (
|
||||||
|
<>
|
||||||
|
<XCircle className="size-3 mr-1" />
|
||||||
|
Invalid
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="size-3 mr-1" />
|
||||||
|
Validate
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-xs">
|
||||||
|
{validationState?.error ? (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="font-medium text-red-500">Error:</p>
|
||||||
|
<p>{validationState.error}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Click to validate DNS configuration"
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "createdAt",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Created
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const createdAt = row.getValue("createdAt") as string;
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{new Date(createdAt).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: "Actions",
|
||||||
|
enableHiding: false,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const domain = row.original;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{!domain.host.includes("traefik.me") && (
|
||||||
|
<DnsHelperModal
|
||||||
|
domain={{
|
||||||
|
host: domain.host,
|
||||||
|
https: domain.https,
|
||||||
|
path: domain.path || undefined,
|
||||||
|
}}
|
||||||
|
serverIp={serverIp}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{canCreateDomain && (
|
||||||
|
<AddDomain id={id} type={type} domainId={domain.domainId}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 h-8 w-8"
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
</AddDomain>
|
||||||
|
)}
|
||||||
|
{canDeleteDomain && (
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Domain"
|
||||||
|
description="Are you sure you want to delete this domain?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await handleDeleteDomain(domain.domainId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 h-8 w-8"
|
||||||
|
isLoading={isDeleting}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,8 +1,22 @@
|
|||||||
|
import {
|
||||||
|
type ColumnFiltersState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
type SortingState,
|
||||||
|
useReactTable,
|
||||||
|
type VisibilityState,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
ChevronDown,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
|
LayoutGrid,
|
||||||
|
LayoutList,
|
||||||
Loader2,
|
Loader2,
|
||||||
PenBoxIcon,
|
PenBoxIcon,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -23,6 +37,21 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -30,6 +59,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { createColumns } from "./columns";
|
||||||
import { DnsHelperModal } from "./dns-helper-modal";
|
import { DnsHelperModal } from "./dns-helper-modal";
|
||||||
import { AddDomain } from "./handle-domain";
|
import { AddDomain } from "./handle-domain";
|
||||||
|
|
||||||
@@ -74,6 +104,19 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
const [viewMode, setViewMode] = useState<"grid" | "table">(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
return (
|
||||||
|
(localStorage.getItem("domains-view-mode") as "grid" | "table") ??
|
||||||
|
"grid"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return "grid";
|
||||||
|
});
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||||
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -103,6 +146,16 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
const { mutateAsync: deleteDomain, isPending: isRemoving } =
|
const { mutateAsync: deleteDomain, isPending: isRemoving } =
|
||||||
api.domain.delete.useMutation();
|
api.domain.delete.useMutation();
|
||||||
|
|
||||||
|
const handleDeleteDomain = async (domainId: string) => {
|
||||||
|
try {
|
||||||
|
await deleteDomain({ domainId });
|
||||||
|
refetch();
|
||||||
|
toast.success("Domain deleted successfully");
|
||||||
|
} catch {
|
||||||
|
toast.error("Error deleting domain");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleValidateDomain = async (host: string) => {
|
const handleValidateDomain = async (host: string) => {
|
||||||
setValidationStates((prev) => ({
|
setValidationStates((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -140,6 +193,37 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const columns = createColumns({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
validationStates,
|
||||||
|
handleValidateDomain,
|
||||||
|
handleDeleteDomain,
|
||||||
|
isDeleting: isRemoving,
|
||||||
|
serverIp: application?.server?.ipAddress?.toString() || ip?.toString(),
|
||||||
|
canCreateDomain,
|
||||||
|
canDeleteDomain,
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: data ?? [],
|
||||||
|
columns,
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnFilters,
|
||||||
|
columnVisibility,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -151,13 +235,32 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-2 flex-wrap">
|
||||||
{canCreateDomain && data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<AddDomain id={id} type={type}>
|
<>
|
||||||
<Button>
|
<Button
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const next = viewMode === "grid" ? "table" : "grid";
|
||||||
|
localStorage.setItem("domains-view-mode", next);
|
||||||
|
setViewMode(next);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{viewMode === "grid" ? (
|
||||||
|
<LayoutList className="size-4" />
|
||||||
|
) : (
|
||||||
|
<LayoutGrid className="size-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</AddDomain>
|
{canCreateDomain && (
|
||||||
|
<AddDomain id={id} type={type}>
|
||||||
|
<Button>
|
||||||
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
|
</Button>
|
||||||
|
</AddDomain>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -186,6 +289,122 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
) : viewMode === "table" ? (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter by host..."
|
||||||
|
value={
|
||||||
|
(table.getColumn("host")?.getFilterValue() as string) ?? ""
|
||||||
|
}
|
||||||
|
onChange={(event) =>
|
||||||
|
table.getColumn("host")?.setFilterValue(event.target.value)
|
||||||
|
}
|
||||||
|
className="md:max-w-sm"
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="sm:ml-auto max-sm:w-full"
|
||||||
|
>
|
||||||
|
Columns <ChevronDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter((column) => column.getCanHide())
|
||||||
|
.map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.id}
|
||||||
|
className="capitalize"
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
column.toggleVisibility(!!value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table?.getRowModel()?.rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
{data && data?.length > 0 && (
|
||||||
|
<div className="flex items-center justify-end space-x-2 py-4">
|
||||||
|
<div className="space-x-2 flex flex-wrap">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
||||||
{data?.map((item) => {
|
{data?.map((item) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user