"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 { InfoPopup } from "@app/components/ui/info-popup"; import { Input } from "@app/components/ui/input"; import { Switch } from "@app/components/ui/switch"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@app/components/ui/table"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useStoredColumnVisibility } from "@app/hooks/useStoredColumnVisibility"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { UpdateResourceResponse } from "@server/routers/resource"; import { ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table"; import { AxiosResponse } from "axios"; import { ArrowRight, ArrowUpDown, CheckCircle2, ChevronDown, Clock, Columns, MoreHorizontal, Plus, RefreshCw, Search, ShieldCheck, ShieldOff, XCircle } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState, useTransition } from "react"; 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[]; }; function getOverallHealthStatus( targets?: TargetHealth[] ): "online" | "degraded" | "offline" | "unknown" { if (!targets || targets.length === 0) { return "unknown"; } const monitoredTargets = targets.filter( (t) => t.enabled && t.healthStatus && t.healthStatus !== "unknown" ); if (monitoredTargets.length === 0) { return "unknown"; } const healthyCount = monitoredTargets.filter( (t) => t.healthStatus === "healthy" ).length; const unhealthyCount = monitoredTargets.filter( (t) => t.healthStatus === "unhealthy" ).length; if (healthyCount === monitoredTargets.length) { return "online"; } else if (unhealthyCount === monitoredTargets.length) { return "offline"; } else { return "degraded"; } } function StatusIcon({ status, className = "" }: { status: "online" | "degraded" | "offline" | "unknown"; className?: string; }) { const iconClass = `h-4 w-4 ${className}`; switch (status) { case "online": return ; case "degraded": return ; case "offline": return ; case "unknown": return ; default: return null; } } type ProxyResourcesTableProps = { resources: ResourceRow[]; orgId: string; defaultSort?: { id: string; desc: boolean; }; }; export default function ProxyResourcesTable({ resources, orgId, defaultSort }: ProxyResourcesTableProps) { const router = useRouter(); const t = useTranslations(); const { env } = useEnvContext(); const api = createApiClient({ env }); const [proxyPageSize, setProxyPageSize] = useStoredPageSize( "proxy-resources", 20 ); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedResource, setSelectedResource] = useState(); const [proxySorting, setProxySorting] = useState( defaultSort ? [defaultSort] : [] ); const [proxyColumnFilters, setProxyColumnFilters] = useState([]); const [proxyGlobalFilter, setProxyGlobalFilter] = useState([]); const [isRefreshing, startTransition] = useTransition(); const [proxyColumnVisibility, setProxyColumnVisibility] = useStoredColumnVisibility("proxy-resources", {}); const refreshData = () => { try { router.refresh(); } catch (error) { toast({ title: t("error"), description: t("refreshError"), variant: "destructive" }); } }; 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(() => { startTransition(() => { router.refresh(); setIsDeleteModalOpen(false); }); }); }; async function toggleResourceEnabled(val: boolean, resourceId: number) { await api .post>( `resource/${resourceId}`, { enabled: val } ) .catch((e) => { toast({ variant: "destructive", title: t("resourcesErrorUpdate"), description: formatAxiosError( e, t("resourcesErrorUpdateDescription") ) }); }); } function TargetStatusCell({ targets }: { targets?: TargetHealth[] }) { const overallStatus = getOverallHealthStatus(targets); if (!targets || targets.length === 0) { return ( {t("resourcesTableNoTargets")} ); } const monitoredTargets = targets.filter( (t) => t.enabled && t.healthStatus && t.healthStatus !== "unknown" ); const unknownTargets = targets.filter( (t) => !t.enabled || !t.healthStatus || t.healthStatus === "unknown" ); return ( {overallStatus === "online" && t("resourcesTableHealthy")} {overallStatus === "degraded" && t("resourcesTableDegraded")} {overallStatus === "offline" && t("resourcesTableOffline")} {overallStatus === "unknown" && t("resourcesTableUnknown")} {monitoredTargets.length > 0 && ( <> {monitoredTargets.map((target) => ( {`${target.ip}:${target.port}`} {target.healthStatus} ))} > )} {unknownTargets.length > 0 && ( <> {unknownTargets.map((target) => ( {`${target.ip}:${target.port}`} {!target.enabled ? t("disabled") : t("resourcesTableNotMonitored")} ))} > )} ); } const proxyColumns: ExtendedColumnDef[] = [ { accessorKey: "name", enableHiding: false, friendlyName: t("name"), header: ({ column }) => { return ( column.toggleSorting(column.getIsSorted() === "asc") } > {t("name")} ); } }, { accessorKey: "nice", friendlyName: t("resource"), enableHiding: true, header: ({ column }) => { return ( column.toggleSorting(column.getIsSorted() === "asc") } > {t("resource")} ); } }, { accessorKey: "protocol", friendlyName: t("protocol"), header: () => {t("protocol")}, cell: ({ row }) => { const resourceRow = row.original; return ( {resourceRow.http ? resourceRow.ssl ? "HTTPS" : "HTTP" : resourceRow.protocol.toUpperCase()} ); } }, { id: "status", accessorKey: "status", friendlyName: t("status"), header: ({ column }) => { return ( column.toggleSorting(column.getIsSorted() === "asc") } > {t("status")} ); }, cell: ({ row }) => { const resourceRow = row.original; return ; }, sortingFn: (rowA, rowB) => { const statusA = getOverallHealthStatus(rowA.original.targets); const statusB = getOverallHealthStatus(rowB.original.targets); const statusOrder = { online: 3, degraded: 2, offline: 1, unknown: 0 }; return statusOrder[statusA] - statusOrder[statusB]; } }, { accessorKey: "domain", friendlyName: t("access"), header: () => {t("access")}, cell: ({ row }) => { const resourceRow = row.original; return ( {!resourceRow.http ? ( ) : !resourceRow.domainId ? ( ) : ( )} ); } }, { accessorKey: "authState", friendlyName: t("authentication"), header: ({ column }) => { return ( column.toggleSorting(column.getIsSorted() === "asc") } > {t("authentication")} ); }, cell: ({ row }) => { const resourceRow = row.original; return ( {resourceRow.authState === "protected" ? ( {t("protected")} ) : resourceRow.authState === "not_protected" ? ( {t("notProtected")} ) : ( - )} ); } }, { accessorKey: "enabled", friendlyName: t("enabled"), header: () => {t("enabled")}, cell: ({ row }) => ( toggleResourceEnabled(val, row.original.id) } /> ) }, { id: "actions", enableHiding: false, header: ({ table }) => { const hasHideableColumns = table .getAllColumns() .some((column) => column.getCanHide()); if (!hasHideableColumns) { return ; } return ( {t("columns") || "Columns"} {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 ( {t("openMenu")} {t("viewSettings")} { setSelectedResource(resourceRow); setIsDeleteModalOpen(true); }} > {t("delete")} {t("edit")} ); } } ]; const proxyTable = useReactTable({ data: resources, columns: proxyColumns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setProxySorting, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setProxyColumnFilters, getFilteredRowModel: getFilteredRowModel(), onGlobalFilterChange: setProxyGlobalFilter, onColumnVisibilityChange: setProxyColumnVisibility, initialState: { pagination: { pageSize: proxyPageSize, pageIndex: 0 }, columnVisibility: proxyColumnVisibility }, state: { sorting: proxySorting, columnFilters: proxyColumnFilters, globalFilter: proxyGlobalFilter, columnVisibility: proxyColumnVisibility } }); 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")} /> )} proxyTable.setGlobalFilter( String(e.target.value) ) } className="w-full pl-8" /> startTransition(refreshData)} disabled={isRefreshing} > {t("refresh")} router.push( `/${orgId}/settings/resources/create` ) } > {t("resourceAdd")} {proxyTable .getHeaderGroups() .map((headerGroup) => ( {headerGroup.headers .filter((header) => header.column.getIsVisible() ) .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("resourceQuestionRemove")}
{t("resourceMessageRemove")}