"use client"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import CopyToClipboard from "@app/components/CopyToClipboard"; import { DataTablePagination } from "@app/components/DataTablePagination"; import { Button } from "@app/components/ui/button"; import { Card, CardContent, CardHeader } from "@app/components/ui/card"; import { ExtendedColumnDef } from "@app/components/ui/data-table"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { Input } from "@app/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@app/components/ui/table"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table"; import { ArrowUpDown, ArrowUpRight, Columns, MoreHorizontal, Plus, RefreshCw, Search } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState, useTransition } from "react"; import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog"; import EditInternalResourceDialog from "@app/components/EditInternalResourceDialog"; import { useStoredColumnVisibility } from "@app/hooks/useStoredColumnVisibility"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { orgQueries, siteQueries } from "@app/lib/queries"; import { useQuery } from "@tanstack/react-query"; export type TargetHealth = { targetId: number; ip: string; port: number; enabled: boolean; healthStatus?: "healthy" | "unhealthy" | "unknown"; }; 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; targetHost?: string; targetPort?: number; targets?: TargetHealth[]; }; export type InternalResourceRow = { id: number; name: string; orgId: string; siteName: string; siteAddress: string | null; // mode: "host" | "cidr" | "port"; mode: "host" | "cidr"; // protocol: string | null; // proxyPort: number | null; siteId: number; siteNiceId: string; destination: string; // destinationPort: number | null; alias: string | null; }; type ClientResourcesTableProps = { internalResources: InternalResourceRow[]; orgId: string; defaultSort?: { id: string; desc: boolean; }; }; export default function ClientResourcesTable({ internalResources, orgId, defaultSort }: ClientResourcesTableProps) { const router = useRouter(); const t = useTranslations(); const { env } = useEnvContext(); const api = createApiClient({ env }); const [internalPageSize, setInternalPageSize] = useStoredPageSize( "internal-resources", 20 ); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedInternalResource, setSelectedInternalResource] = useState(); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [editingResource, setEditingResource] = useState(); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const { data: sites = [] } = useQuery(orgQueries.sites({ orgId, api })); const [internalSorting, setInternalSorting] = useState( defaultSort ? [defaultSort] : [] ); const [internalColumnFilters, setInternalColumnFilters] = useState([]); const [internalGlobalFilter, setInternalGlobalFilter] = useState([]); const [isRefreshing, startTransition] = useTransition(); const [internalColumnVisibility, setInternalColumnVisibility] = useStoredColumnVisibility("internal-resources", {}); const refreshData = async () => { try { router.refresh(); console.log("Data refreshed"); } catch (error) { toast({ title: t("error"), description: t("refreshError"), variant: "destructive" }); } }; const deleteInternalResource = async ( resourceId: number, siteId: number ) => { try { await api .delete(`/org/${orgId}/site/${siteId}/resource/${resourceId}`) .then(() => { startTransition(() => { router.refresh(); setIsDeleteModalOpen(false); }); }); } catch (e) { console.error(t("resourceErrorDelete"), e); toast({ variant: "destructive", title: t("resourceErrorDelte"), description: formatAxiosError(e, t("v")) }); } }; const internalColumns: ExtendedColumnDef[] = [ { accessorKey: "name", enableHiding: false, friendlyName: t("name"), header: ({ column }) => { return ( ); } }, { accessorKey: "siteName", friendlyName: t("siteName"), header: () => {t("siteName")}, cell: ({ row }) => { const resourceRow = row.original; return ( ); } }, { accessorKey: "mode", friendlyName: t("editInternalResourceDialogMode"), header: () => ( {t("editInternalResourceDialogMode")} ), cell: ({ row }) => { const resourceRow = row.original; const modeLabels: Record<"host" | "cidr" | "port", string> = { host: t("editInternalResourceDialogModeHost"), cidr: t("editInternalResourceDialogModeCidr"), port: t("editInternalResourceDialogModePort") }; return {modeLabels[resourceRow.mode]}; } }, { accessorKey: "destination", friendlyName: t("resourcesTableDestination"), header: () => ( {t("resourcesTableDestination")} ), cell: ({ row }) => { const resourceRow = row.original; let displayText: string; let copyText: string; // if ( // resourceRow.mode === "port" && // // resourceRow.protocol && // // resourceRow.proxyPort && // // resourceRow.destinationPort // ) { // // const protocol = resourceRow.protocol.toUpperCase(); // // For port mode: site part uses alias or site address, destination part uses destination IP // // If site address has CIDR notation, extract just the IP address // let siteAddress = resourceRow.siteAddress; // if (siteAddress && siteAddress.includes("/")) { // siteAddress = siteAddress.split("/")[0]; // } // const siteDisplay = resourceRow.alias || siteAddress; // // displayText = `${protocol} ${siteDisplay}:${resourceRow.proxyPort} -> ${resourceRow.destination}:${resourceRow.destinationPort}`; // // copyText = `${siteDisplay}:${resourceRow.proxyPort}`; // } else if (resourceRow.mode === "host") { if (resourceRow.mode === "host") { // For host mode: use alias if available, otherwise use destination const destinationDisplay = resourceRow.alias || resourceRow.destination; displayText = destinationDisplay; copyText = destinationDisplay; } else if (resourceRow.mode === "cidr") { displayText = resourceRow.destination; copyText = resourceRow.destination; } else { const destinationDisplay = resourceRow.alias || resourceRow.destination; displayText = destinationDisplay; copyText = destinationDisplay; } return ( ); } }, { id: "actions", enableHiding: false, header: ({ table }) => { const hasHideableColumns = table .getAllColumns() .some((column) => column.getCanHide()); if (!hasHideableColumns) { return ; } return (
{t("toggleColumns") || "Toggle columns"} {table .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { const columnDef = column.columnDef as any; const friendlyName = columnDef.friendlyName; const displayName = friendlyName || (typeof columnDef.header === "string" ? columnDef.header : column.id); return ( column.toggleVisibility( !!value ) } onSelect={(e) => e.preventDefault() } > {displayName} ); })}
); }, cell: ({ row }) => { const resourceRow = row.original; return (
{ setSelectedInternalResource( resourceRow ); setIsDeleteModalOpen(true); }} > {t("delete")}
); } } ]; const internalTable = useReactTable({ data: internalResources, columns: internalColumns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setInternalSorting, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setInternalColumnFilters, getFilteredRowModel: getFilteredRowModel(), onGlobalFilterChange: setInternalGlobalFilter, onColumnVisibilityChange: setInternalColumnVisibility, initialState: { pagination: { pageSize: internalPageSize, pageIndex: 0 }, columnVisibility: internalColumnVisibility }, state: { sorting: internalSorting, columnFilters: internalColumnFilters, globalFilter: internalGlobalFilter, columnVisibility: internalColumnVisibility } }); return ( <> {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")} /> )}
internalTable.setGlobalFilter( String(e.target.value) ) } className="w-full pl-8" />
{" "}
{internalTable .getHeaderGroups() .map((headerGroup) => ( {headerGroup.headers .filter((header) => header.column.getIsVisible() ) .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(); }} /> ); }