"use client"; import { Badge } from "@app/components/ui/badge"; import { Button } from "@app/components/ui/button"; import { InfoPopup } from "@app/components/ui/info-popup"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { build } from "@server/build"; import { type PaginationState } from "@tanstack/react-table"; import { ArrowDown01Icon, ArrowUp10Icon, ArrowUpRight, Check, ChevronsUpDownIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { useState, useTransition } from "react"; import { useDebouncedCallback } from "use-debounce"; import z from "zod"; import { ColumnFilterButton } from "./ColumnFilterButton"; import { ControlledDataTable, type ExtendedColumnDef } from "./ui/controlled-data-table"; import { SiteRow } from "./SitesTable"; type PendingSitesTableProps = { sites: SiteRow[]; pagination: PaginationState; orgId: string; rowCount: number; }; export default function PendingSitesTable({ sites, orgId, pagination, rowCount }: PendingSitesTableProps) { const router = useRouter(); const pathname = usePathname(); const { navigate: filter, isNavigating: isFiltering, searchParams } = useNavigationContext(); const [isRefreshing, startTransition] = useTransition(); const [approvingIds, setApprovingIds] = useState>(new Set()); const api = createApiClient(useEnvContext()); const t = useTranslations(); const booleanSearchFilterSchema = z .enum(["true", "false"]) .optional() .catch(undefined); function handleFilterChange( column: string, value: string | undefined | null ) { const sp = new URLSearchParams(searchParams); sp.delete(column); sp.delete("page"); if (value) { sp.set(column, value); } startTransition(() => router.push(`${pathname}?${sp.toString()}`)); } function refreshData() { startTransition(async () => { try { router.refresh(); } catch (error) { toast({ title: t("error"), description: t("refreshError"), variant: "destructive" }); } }); } async function approveSite(siteId: number) { setApprovingIds((prev) => new Set(prev).add(siteId)); try { await api.post(`/site/${siteId}`, { status: "approved" }); toast({ title: t("success"), description: t("siteApproveSuccess"), variant: "default" }); router.refresh(); } catch (e) { toast({ variant: "destructive", title: t("siteApproveError"), description: formatAxiosError(e, t("siteApproveError")) }); } finally { setApprovingIds((prev) => { const next = new Set(prev); next.delete(siteId); return next; }); } } const columns: ExtendedColumnDef[] = [ { accessorKey: "name", enableHiding: false, header: () => { const nameOrder = getSortDirection("name", searchParams); const Icon = nameOrder === "asc" ? ArrowDown01Icon : nameOrder === "desc" ? ArrowUp10Icon : ChevronsUpDownIcon; return ( ); } }, { id: "niceId", accessorKey: "nice", friendlyName: t("identifier"), enableHiding: true, header: () => { return {t("identifier")}; }, cell: ({ row }) => { return {row.original.nice || "-"}; } }, { accessorKey: "online", friendlyName: t("online"), header: () => { return ( handleFilterChange("online", value) } searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} label={t("online")} className="p-3" /> ); }, cell: ({ row }) => { const originalRow = row.original; if ( originalRow.type == "newt" || originalRow.type == "wireguard" ) { if (originalRow.online) { return (
{t("online")}
); } else { return (
{t("offline")}
); } } else { return -; } } }, { accessorKey: "mbIn", friendlyName: t("dataIn"), header: () => { const dataInOrder = getSortDirection( "megabytesIn", searchParams ); const Icon = dataInOrder === "asc" ? ArrowDown01Icon : dataInOrder === "desc" ? ArrowUp10Icon : ChevronsUpDownIcon; return ( ); } }, { accessorKey: "mbOut", friendlyName: t("dataOut"), header: () => { const dataOutOrder = getSortDirection( "megabytesOut", searchParams ); const Icon = dataOutOrder === "asc" ? ArrowDown01Icon : dataOutOrder === "desc" ? ArrowUp10Icon : ChevronsUpDownIcon; return ( ); } }, { accessorKey: "type", friendlyName: t("type"), header: () => { return {t("type")}; }, cell: ({ row }) => { const originalRow = row.original; if (originalRow.type === "newt") { return (
Newt {originalRow.newtVersion && ( v{originalRow.newtVersion} )}
{originalRow.newtUpdateAvailable && ( )}
); } if (originalRow.type === "wireguard") { return (
WireGuard
); } if (originalRow.type === "local") { return (
Local
); } } }, { accessorKey: "exitNode", friendlyName: t("exitNode"), header: () => { return {t("exitNode")}; }, cell: ({ row }) => { const originalRow = row.original; if (!originalRow.exitNodeName) { return "-"; } const isCloudNode = build == "saas" && originalRow.exitNodeName && [ "mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune" ].includes(originalRow.exitNodeName.toLowerCase()); if (isCloudNode) { const capitalizedName = originalRow.exitNodeName.charAt(0).toUpperCase() + originalRow.exitNodeName.slice(1).toLowerCase(); return ( Pangolin {capitalizedName} ); } if (originalRow.remoteExitNodeId) { return ( ); } return {originalRow.exitNodeName}; } }, { accessorKey: "address", header: () => { return {t("address")}; }, cell: ({ row }: { row: any }) => { const originalRow = row.original; return originalRow.address ? (
{originalRow.address}
) : ( "-" ); } }, { id: "actions", enableHiding: false, header: () => , cell: ({ row }) => { const siteRow = row.original; const isApproving = approvingIds.has(siteRow.id); return (
); } } ]; function toggleSort(column: string) { const newSearch = getNextSortOrder(column, searchParams); filter({ searchParams: newSearch }); } const handlePaginationChange = (newPage: PaginationState) => { searchParams.set("page", (newPage.pageIndex + 1).toString()); searchParams.set("pageSize", newPage.pageSize.toString()); filter({ searchParams }); }; const handleSearchChange = useDebouncedCallback((query: string) => { searchParams.set("query", query); searchParams.delete("page"); filter({ searchParams }); }, 300); return ( ); }