"use client"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { Badge } from "@app/components/ui/badge"; import { Button } from "@app/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; 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, ArrowRight, ArrowUp10Icon, ArrowUpRight, ChevronsUpDownIcon, MoreHorizontal } 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"; export type SiteRow = { id: number; nice: string; name: string; mbIn: string; mbOut: string; orgId: string; type: "newt" | "wireguard"; newtVersion?: string; newtUpdateAvailable?: boolean; online: boolean; address?: string; exitNodeName?: string; exitNodeEndpoint?: string; remoteExitNodeId?: string; }; type SitesTableProps = { sites: SiteRow[]; pagination: PaginationState; orgId: string; rowCount: number; }; export default function SitesTable({ sites, orgId, pagination, rowCount }: SitesTableProps) { const router = useRouter(); const pathname = usePathname(); const { navigate: filter, isNavigating: isFiltering, searchParams } = useNavigationContext(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedSite, setSelectedSite] = useState(null); const [isRefreshing, startTransition] = useTransition(); const [isNavigatingToAddPage, startNavigation] = useTransition(); 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" }); } }); } function deleteSite(siteId: number) { startTransition(async () => { await api .delete(`/site/${siteId}`) .catch((e) => { console.error(t("siteErrorDelete"), e); toast({ variant: "destructive", title: t("siteErrorDelete"), description: formatAxiosError(e, t("siteErrorDelete")) }); }) .then(() => { router.refresh(); setIsDeleteModalOpen(false); }); }); } const columns: ExtendedColumnDef[] = [ { accessorKey: "name", enableHiding: false, header: () => { return {t("name")}; } }, { 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} ); } // Self-hosted node if (originalRow.remoteExitNodeId) { return ( ); } // Fallback if no remoteExitNodeId 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; return (
{t("viewSettings")} { setSelectedSite(siteRow); setIsDeleteModalOpen(true); }} > {t("delete")}
); } } ]; 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 ( <> {selectedSite && ( { setIsDeleteModalOpen(val); setSelectedSite(null); }} dialog={

{t("siteQuestionRemove")}

{t("siteMessageRemove")}

} buttonText={t("siteConfirmDelete")} onConfirm={async () => startTransition(() => deleteSite(selectedSite!.id)) } string={selectedSite.name} title={t("siteDelete")} /> )} startNavigation(() => router.push(`/${orgId}/settings/sites/create`) ) } isNavigatingToAddPage={isNavigatingToAddPage} searchQuery={searchParams.get("query")?.toString()} onSearch={handleSearchChange} addButtonText={t("siteAdd")} onRefresh={refreshData} isRefreshing={isRefreshing || isFiltering} rowCount={rowCount} columnVisibility={{ niceId: false, nice: false, exitNode: false, address: false }} enableColumnVisibility stickyLeftColumn="name" stickyRightColumn="actions" /> ); }