"use client"; import { ColumnDef, flexRender, getCoreRowModel, useReactTable, getPaginationRowModel, SortingState, getSortedRowModel, ColumnFiltersState, getFilteredRowModel, VisibilityState } from "@tanstack/react-table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuCheckboxItem, DropdownMenuLabel, DropdownMenuSeparator } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, ArrowUpRight, Check, MoreHorizontal, X, RefreshCw, Columns, Search, Plus } from "lucide-react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { useState, useEffect } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; import { Badge } from "./ui/badge"; import { InfoPopup } from "./ui/info-popup"; import { Input } from "@app/components/ui/input"; import { DataTablePagination } from "@app/components/DataTablePagination"; import { Card, CardContent, CardHeader } from "@app/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@app/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@app/components/ui/tabs"; import { Alert, AlertDescription } from "@app/components/ui/alert"; export type ClientRow = { id: number; name: string; subnet: string; // siteIds: string; mbIn: string; mbOut: string; orgId: string; online: boolean; olmVersion?: string; olmUpdateAvailable: boolean; userId: string | null; username: string | null; userEmail: string | null; }; type ClientTableProps = { userClients: ClientRow[]; machineClients: ClientRow[]; orgId: string; defaultView?: "user" | "machine"; }; const STORAGE_KEYS = { PAGE_SIZE: "datatable-page-size", COLUMN_VISIBILITY: "datatable-column-visibility", getTablePageSize: (tableId?: string) => tableId ? `datatable-${tableId}-page-size` : STORAGE_KEYS.PAGE_SIZE, getTableColumnVisibility: (tableId?: string) => tableId ? `datatable-${tableId}-column-visibility` : STORAGE_KEYS.COLUMN_VISIBILITY }; const getStoredPageSize = (tableId?: string, defaultSize = 20): number => { if (typeof window === "undefined") return defaultSize; try { const key = STORAGE_KEYS.getTablePageSize(tableId); const stored = localStorage.getItem(key); if (stored) { const parsed = parseInt(stored, 10); if (parsed > 0 && parsed <= 1000) { return parsed; } } } catch (error) { console.warn("Failed to read page size from localStorage:", error); } return defaultSize; }; const setStoredPageSize = (pageSize: number, tableId?: string): void => { if (typeof window === "undefined") return; try { const key = STORAGE_KEYS.getTablePageSize(tableId); localStorage.setItem(key, pageSize.toString()); } catch (error) { console.warn("Failed to save page size to localStorage:", error); } }; const getStoredColumnVisibility = ( tableId?: string, defaultVisibility?: Record ): Record => { if (typeof window === "undefined") return defaultVisibility || {}; try { const key = STORAGE_KEYS.getTableColumnVisibility(tableId); const stored = localStorage.getItem(key); if (stored) { const parsed = JSON.parse(stored); // Validate that it's an object if (typeof parsed === "object" && parsed !== null) { return parsed; } } } catch (error) { console.warn( "Failed to read column visibility from localStorage:", error ); } return defaultVisibility || {}; }; const setStoredColumnVisibility = ( visibility: Record, tableId?: string ): void => { if (typeof window === "undefined") return; try { const key = STORAGE_KEYS.getTableColumnVisibility(tableId); localStorage.setItem(key, JSON.stringify(visibility)); } catch (error) { console.warn( "Failed to save column visibility to localStorage:", error ); } }; export default function ClientsTable({ userClients, machineClients, orgId, defaultView = "user" }: ClientTableProps) { const router = useRouter(); const searchParams = useSearchParams(); const t = useTranslations(); const [userPageSize, setUserPageSize] = useState(() => getStoredPageSize("user-clients", 20) ); const [machinePageSize, setMachinePageSize] = useState(() => getStoredPageSize("machine-clients", 20) ); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedClient, setSelectedClient] = useState( null ); const api = createApiClient(useEnvContext()); const [isRefreshing, setIsRefreshing] = useState(false); const [userSorting, setUserSorting] = useState([]); const [userColumnFilters, setUserColumnFilters] = useState([]); const [userGlobalFilter, setUserGlobalFilter] = useState([]); const [machineSorting, setMachineSorting] = useState([]); const [machineColumnFilters, setMachineColumnFilters] = useState([]); const [machineGlobalFilter, setMachineGlobalFilter] = useState([]); const defaultUserColumnVisibility = { client: false, subnet: false }; const defaultMachineColumnVisibility = { client: false, subnet: false, userId: false }; const [userColumnVisibility, setUserColumnVisibility] = useState( () => getStoredColumnVisibility("user-clients", defaultUserColumnVisibility) ); const [machineColumnVisibility, setMachineColumnVisibility] = useState( () => getStoredColumnVisibility("machine-clients", defaultMachineColumnVisibility) ); const currentView = searchParams.get("view") || defaultView; const refreshData = async () => { console.log("Data refreshed"); setIsRefreshing(true); try { await new Promise((resolve) => setTimeout(resolve, 200)); router.refresh(); } catch (error) { toast({ title: t("error"), description: t("refreshError"), variant: "destructive" }); } finally { setIsRefreshing(false); } }; const handleTabChange = (value: string) => { const params = new URLSearchParams(searchParams); if (value === "machine") { params.set("view", "machine"); } else { params.delete("view"); } const newUrl = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`; router.replace(newUrl, { scroll: false }); }; const deleteClient = (clientId: number) => { api.delete(`/client/${clientId}`) .catch((e) => { console.error("Error deleting client", e); toast({ variant: "destructive", title: "Error deleting client", description: formatAxiosError(e, "Error deleting client") }); }) .then(() => { router.refresh(); setIsDeleteModalOpen(false); }); }; const getSearchInput = () => { if (currentView === "machine") { return (
machineTable.setGlobalFilter( String(e.target.value) ) } className="w-full pl-8" />
); } return (
userTable.setGlobalFilter(String(e.target.value)) } className="w-full pl-8" />
); }; const getActionButton = () => { // Only show create button on machine clients tab if (currentView === "machine") { return ( ); } return null; }; const columns: ColumnDef[] = [ { accessorKey: "name", header: ({ column }) => { return ( ); } }, { accessorKey: "userId", header: ({ column }) => { return ( ); }, cell: ({ row }) => { const r = row.original; return r.userId ? ( ) : ( "-" ); } }, // { // accessorKey: "siteName", // header: ({ column }) => { // return ( // // ); // }, // cell: ({ row }) => { // const r = row.original; // return ( // // // // ); // } // }, { accessorKey: "online", header: ({ column }) => { return ( ); }, cell: ({ row }) => { const originalRow = row.original; if (originalRow.online) { return (
Connected
); } else { return (
Disconnected
); } } }, { accessorKey: "mbIn", header: ({ column }) => { return ( ); } }, { accessorKey: "mbOut", header: ({ column }) => { return ( ); } }, { accessorKey: "client", header: ({ column }) => { return ( ); }, cell: ({ row }) => { const originalRow = row.original; return (
Olm {originalRow.olmVersion && ( v{originalRow.olmVersion} )}
{originalRow.olmUpdateAvailable && ( )}
); } }, { accessorKey: "subnet", header: ({ column }) => { return ( ); } }, { id: "actions", header: () => ({t("actions")}), cell: ({ row }) => { const clientRow = row.original; return (
{/* */} {/* */} {/* View settings */} {/* */} {/* */} { setSelectedClient(clientRow); setIsDeleteModalOpen(true); }} > Delete
); } } ]; const userTable = useReactTable({ data: userClients || [], columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setUserSorting, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setUserColumnFilters, getFilteredRowModel: getFilteredRowModel(), onGlobalFilterChange: setUserGlobalFilter, onColumnVisibilityChange: setUserColumnVisibility, initialState: { pagination: { pageSize: userPageSize, pageIndex: 0 }, columnVisibility: userColumnVisibility }, state: { sorting: userSorting, columnFilters: userColumnFilters, globalFilter: userGlobalFilter, columnVisibility: userColumnVisibility } }); const machineTable = useReactTable({ data: machineClients || [], columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setMachineSorting, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setMachineColumnFilters, getFilteredRowModel: getFilteredRowModel(), onGlobalFilterChange: setMachineGlobalFilter, onColumnVisibilityChange: setMachineColumnVisibility, initialState: { pagination: { pageSize: machinePageSize, pageIndex: 0 }, columnVisibility: machineColumnVisibility }, state: { sorting: machineSorting, columnFilters: machineColumnFilters, globalFilter: machineGlobalFilter, columnVisibility: machineColumnVisibility } }); const handleUserPageSizeChange = (newPageSize: number) => { setUserPageSize(newPageSize); setStoredPageSize(newPageSize, "user-clients"); }; const handleMachinePageSizeChange = (newPageSize: number) => { setMachinePageSize(newPageSize); setStoredPageSize(newPageSize, "machine-clients"); }; // Persist column visibility changes to localStorage useEffect(() => { setStoredColumnVisibility(userColumnVisibility, "user-clients"); }, [userColumnVisibility]); useEffect(() => { setStoredColumnVisibility(machineColumnVisibility, "machine-clients"); }, [machineColumnVisibility]); return ( <> {selectedClient && ( { setIsDeleteModalOpen(val); setSelectedClient(null); }} dialog={

{t("deleteClientQuestion")}

{t("clientMessageRemove")}

} buttonText="Confirm Delete Client" onConfirm={async () => deleteClient(selectedClient!.id)} string={selectedClient.name} title="Delete Client" /> )}
{getSearchInput()} {t("clientsTableUserClients")} {t("clientsTableMachineClients")}
{currentView === "user" && userTable.getAllColumns().some((column) => column.getCanHide()) && ( {t("toggleColumns") || "Toggle columns"} {userTable .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { return ( column.toggleVisibility(!!value) } > {typeof column.columnDef.header === "string" ? column.columnDef.header : column.id} ); })} )} {currentView === "machine" && machineTable.getAllColumns().some((column) => column.getCanHide()) && ( {t("toggleColumns") || "Toggle columns"} {machineTable .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { return ( column.toggleVisibility(!!value) } > {typeof column.columnDef.header === "string" ? column.columnDef.header : column.id} ); })} )}
{getActionButton()}
{t("clientsTableUserClientsDescription")}
{userTable .getHeaderGroups() .map((headerGroup) => ( {headerGroup.headers .filter((header) => header.column.getIsVisible()) .map( (header) => ( {header.isPlaceholder ? null : flexRender( header .column .columnDef .header, header.getContext() )} ) )} ))} {userTable.getRowModel().rows ?.length ? ( userTable .getRowModel() .rows.map((row) => ( {row .getVisibleCells() .map((cell) => ( {flexRender( cell .column .columnDef .cell, cell.getContext() )} ))} )) ) : ( {t("noResults")} )}
{t("clientsTableMachineClientsDescription")}
{machineTable .getHeaderGroups() .map((headerGroup) => ( {headerGroup.headers .filter((header) => header.column.getIsVisible()) .map( (header) => ( {header.isPlaceholder ? null : flexRender( header .column .columnDef .header, header.getContext() )} ) )} ))} {machineTable.getRowModel().rows ?.length ? ( machineTable .getRowModel() .rows.map((row) => ( {row .getVisibleCells() .map((cell) => ( {flexRender( cell .column .columnDef .cell, cell.getContext() )} ))} )) ) : ( {t("noResults")} )}
); }