"use client"; import { ColumnDef, flexRender, getCoreRowModel, useReactTable, getPaginationRowModel, SortingState, getSortedRowModel, ColumnFiltersState, getFilteredRowModel } from "@tanstack/react-table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, MoreHorizontal, ArrowUpRight, ShieldOff, ShieldCheck, RefreshCw } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState, useEffect } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { formatAxiosError } from "@app/lib/api"; import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import CopyToClipboard from "@app/components/CopyToClipboard"; import { Switch } from "@app/components/ui/switch"; import { AxiosResponse } from "axios"; import { UpdateResourceResponse } from "@server/routers/resource"; import { ListSitesResponse } from "@server/routers/site"; import { useTranslations } from "next-intl"; import { InfoPopup } from "@app/components/ui/info-popup"; import { Input } from "@app/components/ui/input"; import { DataTablePagination } from "@app/components/DataTablePagination"; import { Plus, Search } from "lucide-react"; 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 { useSearchParams } from "next/navigation"; import EditInternalResourceDialog from "@app/components/EditInternalResourceDialog"; import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog"; import { Alert, AlertDescription } from "@app/components/ui/alert"; export type ResourceRow = { id: number; nice: string | null; name: string; orgId: string; domain: string; authState: string; http: boolean; protocol: string; proxyPort: number | null; enabled: boolean; domainId?: string; ssl: boolean; }; export type InternalResourceRow = { id: number; name: string; orgId: string; siteName: string; protocol: string; proxyPort: number | null; siteId: number; siteNiceId: string; destinationIp: string; destinationPort: number; }; type Site = ListSitesResponse["sites"][0]; type ResourcesTableProps = { resources: ResourceRow[]; internalResources: InternalResourceRow[]; orgId: string; defaultView?: "proxy" | "internal"; defaultSort?: { id: string; desc: boolean; }; }; const STORAGE_KEYS = { PAGE_SIZE: 'datatable-page-size', getTablePageSize: (tableId?: string) => tableId ? `datatable-${tableId}-page-size` : STORAGE_KEYS.PAGE_SIZE }; 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); } }; export default function ResourcesTable({ resources, internalResources, orgId, defaultView = "proxy", defaultSort }: ResourcesTableProps) { const router = useRouter(); const searchParams = useSearchParams(); const t = useTranslations(); const { env } = useEnvContext(); const api = createApiClient({ env }); const [proxyPageSize, setProxyPageSize] = useState(() => getStoredPageSize('proxy-resources', 20) ); const [internalPageSize, setInternalPageSize] = useState(() => getStoredPageSize('internal-resources', 20) ); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedResource, setSelectedResource] = useState(); const [selectedInternalResource, setSelectedInternalResource] = useState(); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [editingResource, setEditingResource] = useState(); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [sites, setSites] = useState([]); const [proxySorting, setProxySorting] = useState( defaultSort ? [defaultSort] : [] ); const [proxyColumnFilters, setProxyColumnFilters] = useState([]); const [proxyGlobalFilter, setProxyGlobalFilter] = useState([]); const [internalSorting, setInternalSorting] = useState( defaultSort ? [defaultSort] : [] ); const [internalColumnFilters, setInternalColumnFilters] = useState([]); const [internalGlobalFilter, setInternalGlobalFilter] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); 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); } }; useEffect(() => { const fetchSites = async () => { try { const res = await api.get>( `/org/${orgId}/sites` ); setSites(res.data.data.sites); } catch (error) { console.error("Failed to fetch sites:", error); } }; if (orgId) { fetchSites(); } }, [orgId]); const handleTabChange = (value: string) => { const params = new URLSearchParams(searchParams); if (value === "internal") { params.set("view", "internal"); } else { params.delete("view"); } const newUrl = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`; router.replace(newUrl, { scroll: false }); }; const getSearchInput = () => { if (currentView === "internal") { return (
internalTable.setGlobalFilter( String(e.target.value) ) } className="w-full pl-8" />
); } return (
proxyTable.setGlobalFilter(String(e.target.value)) } className="w-full pl-8" />
); }; const getActionButton = () => { if (currentView === "internal") { return ( ); } return ( ); }; const deleteResource = (resourceId: number) => { api.delete(`/resource/${resourceId}`) .catch((e) => { console.error(t("resourceErrorDelte"), e); toast({ variant: "destructive", title: t("resourceErrorDelte"), description: formatAxiosError(e, t("resourceErrorDelte")) }); }) .then(() => { router.refresh(); setIsDeleteModalOpen(false); }); }; const deleteInternalResource = async ( resourceId: number, siteId: number ) => { try { await api.delete( `/org/${orgId}/site/${siteId}/resource/${resourceId}` ); router.refresh(); setIsDeleteModalOpen(false); } catch (e) { console.error(t("resourceErrorDelete"), e); toast({ variant: "destructive", title: t("resourceErrorDelte"), description: formatAxiosError(e, t("v")) }); } }; async function toggleResourceEnabled(val: boolean, resourceId: number) { const res = await api .post>( `resource/${resourceId}`, { enabled: val } ) .catch((e) => { toast({ variant: "destructive", title: t("resourcesErrorUpdate"), description: formatAxiosError( e, t("resourcesErrorUpdateDescription") ) }); }); } const proxyColumns: ColumnDef[] = [ { accessorKey: "name", header: ({ column }) => { return ( ); } }, { accessorKey: "nice", header: ({ column }) => { return ( ); } }, { accessorKey: "protocol", header: t("protocol"), cell: ({ row }) => { const resourceRow = row.original; return {resourceRow.http ? (resourceRow.ssl ? "HTTPS" : "HTTP") : resourceRow.protocol.toUpperCase()}; } }, { accessorKey: "domain", header: t("access"), cell: ({ row }) => { const resourceRow = row.original; return (
{!resourceRow.http ? ( ) : !resourceRow.domainId ? ( ) : ( )}
); } }, { accessorKey: "authState", header: ({ column }) => { return ( ); }, cell: ({ row }) => { const resourceRow = row.original; return (
{resourceRow.authState === "protected" ? ( {t("protected")} ) : resourceRow.authState === "not_protected" ? ( {t("notProtected")} ) : ( - )}
); } }, { accessorKey: "enabled", header: t("enabled"), cell: ({ row }) => ( toggleResourceEnabled(val, row.original.id) } /> ) }, { id: "actions", cell: ({ row }) => { const resourceRow = row.original; return (
{t("viewSettings")} { setSelectedResource(resourceRow); setIsDeleteModalOpen(true); }} > {t("delete")}
); } } ]; const internalColumns: ColumnDef[] = [ { accessorKey: "name", header: ({ column }) => { return ( ); } }, { accessorKey: "siteName", header: t("siteName"), cell: ({ row }) => { const resourceRow = row.original; return ( ); } }, { accessorKey: "protocol", header: t("protocol"), cell: ({ row }) => { const resourceRow = row.original; return {resourceRow.protocol.toUpperCase()}; } }, { accessorKey: "proxyPort", header: t("proxyPort"), cell: ({ row }) => { const resourceRow = row.original; return ( ); } }, { accessorKey: "destination", header: t("resourcesTableDestination"), cell: ({ row }) => { const resourceRow = row.original; const destination = `${resourceRow.destinationIp}:${resourceRow.destinationPort}`; return ; } }, { id: "actions", cell: ({ row }) => { const resourceRow = row.original; return (
{ setSelectedInternalResource( resourceRow ); setIsDeleteModalOpen(true); }} > {t("delete")}
); } } ]; const proxyTable = useReactTable({ data: resources, columns: proxyColumns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setProxySorting, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setProxyColumnFilters, getFilteredRowModel: getFilteredRowModel(), onGlobalFilterChange: setProxyGlobalFilter, initialState: { pagination: { pageSize: proxyPageSize, pageIndex: 0 } }, state: { sorting: proxySorting, columnFilters: proxyColumnFilters, globalFilter: proxyGlobalFilter } }); const internalTable = useReactTable({ data: internalResources, columns: internalColumns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setInternalSorting, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setInternalColumnFilters, getFilteredRowModel: getFilteredRowModel(), onGlobalFilterChange: setInternalGlobalFilter, initialState: { pagination: { pageSize: internalPageSize, pageIndex: 0 } }, state: { sorting: internalSorting, columnFilters: internalColumnFilters, globalFilter: internalGlobalFilter } }); const handleProxyPageSizeChange = (newPageSize: number) => { setProxyPageSize(newPageSize); setStoredPageSize(newPageSize, 'proxy-resources'); }; const handleInternalPageSizeChange = (newPageSize: number) => { setInternalPageSize(newPageSize); setStoredPageSize(newPageSize, 'internal-resources'); }; return ( <> {selectedResource && ( { setIsDeleteModalOpen(val); setSelectedResource(null); }} dialog={

{t("resourceQuestionRemove")}

{t("resourceMessageRemove")}

} buttonText={t("resourceDeleteConfirm")} onConfirm={async () => deleteResource(selectedResource!.id)} string={selectedResource.name} title={t("resourceDelete")} /> )} {selectedInternalResource && ( { setIsDeleteModalOpen(val); setSelectedInternalResource(null); }} dialog={

{t("resourceQuestionRemove")}

{t("resourceMessageRemove")}

} buttonText={t("resourceDeleteConfirm")} onConfirm={async () => deleteInternalResource( selectedInternalResource!.id, selectedInternalResource!.siteId ) } string={selectedInternalResource.name} title={t("resourceDelete")} /> )}
{getSearchInput()} {env.flags.enableClients && ( {t("resourcesTableProxyResources")} {t("resourcesTableClientResources")} )}
{getActionButton()}
{proxyTable .getHeaderGroups() .map((headerGroup) => ( {headerGroup.headers.map( (header) => ( {header.isPlaceholder ? null : flexRender( header .column .columnDef .header, header.getContext() )} ) )} ))} {proxyTable.getRowModel().rows ?.length ? ( proxyTable .getRowModel() .rows.map((row) => ( {row .getVisibleCells() .map((cell) => ( {flexRender( cell .column .columnDef .cell, cell.getContext() )} ))} )) ) : ( {t( "resourcesTableNoProxyResourcesFound" )} )}
{t( "resourcesTableTheseResourcesForUseWith" )}{" "} {t("resourcesTableClients")} {" "} {t( "resourcesTableAndOnlyAccessibleInternally" )}
{internalTable .getHeaderGroups() .map((headerGroup) => ( {headerGroup.headers.map( (header) => ( {header.isPlaceholder ? null : flexRender( header .column .columnDef .header, header.getContext() )} ) )} ))} {internalTable.getRowModel().rows ?.length ? ( internalTable .getRowModel() .rows.map((row) => ( {row .getVisibleCells() .map((cell) => ( {flexRender( cell .column .columnDef .cell, cell.getContext() )} ))} )) ) : ( {t( "resourcesTableNoInternalResourcesFound" )} )}
{editingResource && ( { router.refresh(); setEditingResource(null); }} /> )} { router.refresh(); }} /> ); }