"use client"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import CopyToClipboard from "@app/components/CopyToClipboard"; import { DataTable } from "@app/components/ui/data-table"; import { ExtendedColumnDef } from "@app/components/ui/data-table"; 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 { Switch } from "@app/components/ui/switch"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { UpdateResourceResponse } from "@server/routers/resource"; import { AxiosResponse } from "axios"; import { ArrowRight, ArrowUpDown, CheckCircle2, ChevronDown, Clock, MoreHorizontal, 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 [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedResource, setSelectedResource] = useState(); const [isRefreshing, startTransition] = useTransition(); const refreshData = () => { startTransition(() => { 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")} ); } }, { id: "niceId", accessorKey: "nice", friendlyName: t("niceId"), enableHiding: true, header: ({ column }) => { return ( column.toggleSorting(column.getIsSorted() === "asc") } > {t("niceId")} ); }, cell: ({ row }) => { return {row.original.nice || "-"}; } }, { 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: () => , cell: ({ row }) => { const resourceRow = row.original; return ( {t("openMenu")} {t("viewSettings")} { setSelectedResource(resourceRow); setIsDeleteModalOpen(true); }} > {t("delete")} {t("edit")} ); } } ]; 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")} /> )} router.push(`/${orgId}/settings/resources/proxy/create`) } addButtonText={t("resourceAdd")} onRefresh={refreshData} isRefreshing={isRefreshing} defaultSort={defaultSort} enableColumnVisibility={true} persistColumnVisibility="proxy-resources" columnVisibility={{ niceId: false }} stickyLeftColumn="name" stickyRightColumn="actions" /> > ); }
{t("resourceQuestionRemove")}
{t("resourceMessageRemove")}