diff --git a/messages/en-US.json b/messages/en-US.json index f3ae34a7..3b1c43a4 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1151,6 +1151,8 @@ "sidebarHome": "Home", "sidebarSites": "Sites", "sidebarResources": "Resources", + "sidebarProxyResources": "Proxy Resources", + "sidebarClientResources": "Client Resources", "sidebarAccessControl": "Access Control", "sidebarUsers": "Users", "sidebarInvitations": "Invitations", diff --git a/src/app/[orgId]/settings/resources/client/page.tsx b/src/app/[orgId]/settings/resources/client/page.tsx new file mode 100644 index 00000000..a02f0ae0 --- /dev/null +++ b/src/app/[orgId]/settings/resources/client/page.tsx @@ -0,0 +1,113 @@ +import ClientResourcesTable from "@app/components/ClientResourcesTable"; +import type { InternalResourceRow } from "@app/components/ProxyResourcesTable"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { pullEnv } from "@app/lib/pullEnv"; +import OrgProvider from "@app/providers/OrgProvider"; +import type { GetOrgResponse } from "@server/routers/org"; +import type { ListResourcesResponse } from "@server/routers/resource"; +import type { ListAllSiteResourcesByOrgResponse } from "@server/routers/siteResource"; +import type { AxiosResponse } from "axios"; +import { getTranslations } from "next-intl/server"; +import { redirect } from "next/navigation"; +import { cache } from "react"; + +export interface ClientResourcesPageProps { + params: Promise<{ orgId: string }>; + searchParams: Promise<{ view?: string }>; +} + +export default async function ClientResourcesPage( + props: ClientResourcesPageProps +) { + const params = await props.params; + const searchParams = await props.searchParams; + const t = await getTranslations(); + + const env = pullEnv(); + + // Default to 'proxy' view, or use the query param if provided + let defaultView: "proxy" | "internal" = "proxy"; + if (env.flags.enableClients) { + defaultView = searchParams.view === "internal" ? "internal" : "proxy"; + } + + let resources: ListResourcesResponse["resources"] = []; + try { + const res = await internal.get>( + `/org/${params.orgId}/resources`, + await authCookieHeader() + ); + resources = res.data.data.resources; + } catch (e) {} + + let siteResources: ListAllSiteResourcesByOrgResponse["siteResources"] = []; + try { + const res = await internal.get< + AxiosResponse + >(`/org/${params.orgId}/site-resources`, await authCookieHeader()); + siteResources = res.data.data.siteResources; + } catch (e) {} + + let org = null; + try { + const getOrg = cache(async () => + internal.get>( + `/org/${params.orgId}`, + await authCookieHeader() + ) + ); + const res = await getOrg(); + org = res.data.data; + } catch { + redirect(`/${params.orgId}/settings/resources`); + } + + if (!org) { + redirect(`/${params.orgId}/settings/resources`); + } + + const internalResourceRows: InternalResourceRow[] = siteResources.map( + (siteResource) => { + return { + id: siteResource.siteResourceId, + name: siteResource.name, + orgId: params.orgId, + siteName: siteResource.siteName, + siteAddress: siteResource.siteAddress || null, + mode: siteResource.mode || ("port" as any), + // protocol: siteResource.protocol, + // proxyPort: siteResource.proxyPort, + siteId: siteResource.siteId, + destination: siteResource.destination, + // destinationPort: siteResource.destinationPort, + alias: siteResource.alias || null, + siteNiceId: siteResource.siteNiceId + }; + } + ); + return ( + <> + + + + + + + ); +} diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index b6c8b232..954b966a 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -1,149 +1,10 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import ResourcesTable, { - ResourceRow, - InternalResourceRow -} from "../../../../components/ResourcesTable"; -import { AxiosResponse } from "axios"; -import { ListResourcesResponse } from "@server/routers/resource"; -import { ListAllSiteResourcesByOrgResponse } from "@server/routers/siteResource"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { redirect } from "next/navigation"; -import { cache } from "react"; -import { GetOrgResponse } from "@server/routers/org"; -import OrgProvider from "@app/providers/OrgProvider"; -import { getTranslations } from "next-intl/server"; -import { pullEnv } from "@app/lib/pullEnv"; -import { toUnicode } from "punycode"; -type ResourcesPageProps = { +export interface ResourcesPageProps { params: Promise<{ orgId: string }>; - searchParams: Promise<{ view?: string }>; -}; - -export const dynamic = "force-dynamic"; +} export default async function ResourcesPage(props: ResourcesPageProps) { const params = await props.params; - const searchParams = await props.searchParams; - const t = await getTranslations(); - - const env = pullEnv(); - - // Default to 'proxy' view, or use the query param if provided - let defaultView: "proxy" | "internal" = "proxy"; - if (env.flags.enableClients) { - defaultView = searchParams.view === "internal" ? "internal" : "proxy"; - } - - let resources: ListResourcesResponse["resources"] = []; - try { - const res = await internal.get>( - `/org/${params.orgId}/resources`, - await authCookieHeader() - ); - resources = res.data.data.resources; - } catch (e) {} - - let siteResources: ListAllSiteResourcesByOrgResponse["siteResources"] = []; - try { - const res = await internal.get< - AxiosResponse - >(`/org/${params.orgId}/site-resources`, await authCookieHeader()); - siteResources = res.data.data.siteResources; - } catch (e) {} - - let org = null; - try { - const getOrg = cache(async () => - internal.get>( - `/org/${params.orgId}`, - await authCookieHeader() - ) - ); - const res = await getOrg(); - org = res.data.data; - } catch { - redirect(`/${params.orgId}/settings/resources`); - } - - if (!org) { - redirect(`/${params.orgId}/settings/resources`); - } - - const resourceRows: ResourceRow[] = resources.map((resource) => { - return { - id: resource.resourceId, - name: resource.name, - orgId: params.orgId, - nice: resource.niceId, - domain: `${resource.ssl ? "https://" : "http://"}${toUnicode(resource.fullDomain || "")}`, - protocol: resource.protocol, - proxyPort: resource.proxyPort, - http: resource.http, - authState: !resource.http - ? "none" - : resource.sso || - resource.pincodeId !== null || - resource.passwordId !== null || - resource.whitelist || - resource.headerAuthId - ? "protected" - : "not_protected", - enabled: resource.enabled, - domainId: resource.domainId || undefined, - ssl: resource.ssl, - targets: resource.targets?.map((target) => ({ - targetId: target.targetId, - ip: target.ip, - port: target.port, - enabled: target.enabled, - healthStatus: target.healthStatus - })) - }; - }); - - const internalResourceRows: InternalResourceRow[] = siteResources.map( - (siteResource) => { - return { - id: siteResource.siteResourceId, - name: siteResource.name, - orgId: params.orgId, - siteName: siteResource.siteName, - siteAddress: siteResource.siteAddress || null, - mode: siteResource.mode || ("port" as any), - // protocol: siteResource.protocol, - // proxyPort: siteResource.proxyPort, - siteId: siteResource.siteId, - destination: siteResource.destination, - // destinationPort: siteResource.destinationPort, - alias: siteResource.alias || null, - siteNiceId: siteResource.siteNiceId - }; - } - ); - - return ( - <> - - - - - - - ); + redirect(`/${params.orgId}/settings/resources/proxy`); } diff --git a/src/app/[orgId]/settings/resources/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/page.tsx new file mode 100644 index 00000000..edbe5ca0 --- /dev/null +++ b/src/app/[orgId]/settings/resources/proxy/page.tsx @@ -0,0 +1,122 @@ +import type { ResourceRow } from "@app/components/ProxyResourcesTable"; +import ProxyResourcesTable from "@app/components/ProxyResourcesTable"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { pullEnv } from "@app/lib/pullEnv"; +import OrgProvider from "@app/providers/OrgProvider"; +import type { GetOrgResponse } from "@server/routers/org"; +import type { ListResourcesResponse } from "@server/routers/resource"; +import type { ListAllSiteResourcesByOrgResponse } from "@server/routers/siteResource"; +import type { AxiosResponse } from "axios"; +import { getTranslations } from "next-intl/server"; +import { redirect } from "next/navigation"; +import { toUnicode } from "punycode"; +import { cache } from "react"; + +export interface ProxyResourcesPageProps { + params: Promise<{ orgId: string }>; + searchParams: Promise<{ view?: string }>; +} + +export default async function ProxyResourcesPage( + props: ProxyResourcesPageProps +) { + const params = await props.params; + const searchParams = await props.searchParams; + const t = await getTranslations(); + + const env = pullEnv(); + + // Default to 'proxy' view, or use the query param if provided + let defaultView: "proxy" | "internal" = "proxy"; + if (env.flags.enableClients) { + defaultView = searchParams.view === "internal" ? "internal" : "proxy"; + } + + let resources: ListResourcesResponse["resources"] = []; + try { + const res = await internal.get>( + `/org/${params.orgId}/resources`, + await authCookieHeader() + ); + resources = res.data.data.resources; + } catch (e) {} + + let siteResources: ListAllSiteResourcesByOrgResponse["siteResources"] = []; + try { + const res = await internal.get< + AxiosResponse + >(`/org/${params.orgId}/site-resources`, await authCookieHeader()); + siteResources = res.data.data.siteResources; + } catch (e) {} + + let org = null; + try { + const getOrg = cache(async () => + internal.get>( + `/org/${params.orgId}`, + await authCookieHeader() + ) + ); + const res = await getOrg(); + org = res.data.data; + } catch { + redirect(`/${params.orgId}/settings/resources`); + } + + if (!org) { + redirect(`/${params.orgId}/settings/resources`); + } + + const resourceRows: ResourceRow[] = resources.map((resource) => { + return { + id: resource.resourceId, + name: resource.name, + orgId: params.orgId, + nice: resource.niceId, + domain: `${resource.ssl ? "https://" : "http://"}${toUnicode(resource.fullDomain || "")}`, + protocol: resource.protocol, + proxyPort: resource.proxyPort, + http: resource.http, + authState: !resource.http + ? "none" + : resource.sso || + resource.pincodeId !== null || + resource.passwordId !== null || + resource.whitelist || + resource.headerAuthId + ? "protected" + : "not_protected", + enabled: resource.enabled, + domainId: resource.domainId || undefined, + ssl: resource.ssl, + targets: resource.targets?.map((target) => ({ + targetId: target.targetId, + ip: target.ip, + port: target.port, + enabled: target.enabled, + healthStatus: target.healthStatus + })) + }; + }); + return ( + <> + + + + + + + ); +} diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index debf9b74..ceefa8f0 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -17,7 +17,8 @@ import { CreditCard, Logs, SquareMousePointer, - ScanEye + ScanEye, + GlobeLock } from "lucide-react"; export type SidebarNavSection = { @@ -31,7 +32,7 @@ export const orgLangingNavItems: SidebarNavItem[] = [ { title: "sidebarAccount", href: "/{orgId}", - icon: + icon: } ]; @@ -44,19 +45,36 @@ export const orgNavSections = ( { title: "sidebarSites", href: "/{orgId}/settings/sites", - icon: + icon: }, { title: "sidebarResources", - href: "/{orgId}/settings/resources", - icon: + icon: , + items: [ + { + title: "sidebarProxyResources", + href: "/{orgId}/settings/resources/proxy", + icon: + }, + ...(enableClients + ? [ + { + title: "sidebarClientResources", + href: "/{orgId}/settings/resources/client", + icon: ( + + ) + } + ] + : []) + ] }, ...(enableClients ? [ { title: "sidebarClients", href: "/{orgId}/settings/clients", - icon: , + icon: , isBeta: true } ] @@ -66,7 +84,7 @@ export const orgNavSections = ( { title: "sidebarRemoteExitNodes", href: "/{orgId}/settings/remote-exit-nodes", - icon: , + icon: , showEE: true } ] @@ -74,12 +92,12 @@ export const orgNavSections = ( { title: "sidebarDomains", href: "/{orgId}/settings/domains", - icon: + icon: }, { title: "sidebarBluePrints", href: "/{orgId}/settings/blueprints", - icon: + icon: } ] }, @@ -88,31 +106,31 @@ export const orgNavSections = ( items: [ { title: "sidebarUsers", - icon: , + icon: , items: [ { title: "sidebarUsers", href: "/{orgId}/settings/access/users", - icon: + icon: }, { title: "sidebarInvitations", href: "/{orgId}/settings/access/invitations", - icon: + icon: } ] }, { title: "sidebarRoles", href: "/{orgId}/settings/access/roles", - icon: + icon: }, ...(build == "saas" ? [ { title: "sidebarIdentityProviders", href: "/{orgId}/settings/idp", - icon: , + icon: , showEE: true } ] @@ -120,7 +138,7 @@ export const orgNavSections = ( { title: "sidebarShareableLinks", href: "/{orgId}/settings/share-links", - icon: + icon: } ] }, @@ -131,19 +149,19 @@ export const orgNavSections = ( { title: "sidebarLogsRequest", href: "/{orgId}/settings/logs/request", - icon: + icon: }, ...(build != "oss" ? [ { title: "sidebarLogsAccess", href: "/{orgId}/settings/logs/access", - icon: + icon: }, { title: "sidebarLogsAction", href: "/{orgId}/settings/logs/action", - icon: + icon: } ] : []) @@ -158,7 +176,7 @@ export const orgNavSections = ( return [ { title: "sidebarLogs", - icon: , + icon: , items: logItems } ]; @@ -170,14 +188,14 @@ export const orgNavSections = ( { title: "sidebarApiKeys", href: "/{orgId}/settings/api-keys", - icon: + icon: }, ...(build == "saas" ? [ { title: "sidebarBilling", href: "/{orgId}/settings/billing", - icon: + icon: } ] : []), @@ -186,14 +204,14 @@ export const orgNavSections = ( { title: "sidebarEnterpriseLicenses", href: "/{orgId}/settings/license", - icon: + icon: } ] : []), { title: "sidebarSettings", href: "/{orgId}/settings/general", - icon: + icon: } ] } @@ -206,24 +224,24 @@ export const adminNavSections: SidebarNavSection[] = [ { title: "sidebarAllUsers", href: "/admin/users", - icon: + icon: }, { title: "sidebarApiKeys", href: "/admin/api-keys", - icon: + icon: }, { title: "sidebarIdentityProviders", href: "/admin/idp", - icon: + icon: }, ...(build == "enterprise" ? [ { title: "sidebarLicense", href: "/admin/license", - icon: + icon: } ] : []) diff --git a/src/components/ResourcesTable.tsx b/src/components/ClientResourcesTable.tsx similarity index 99% rename from src/components/ResourcesTable.tsx rename to src/components/ClientResourcesTable.tsx index 4a28c586..b37f3eb1 100644 --- a/src/components/ResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -177,7 +177,7 @@ export type InternalResourceRow = { type Site = ListSitesResponse["sites"][0]; -type ResourcesTableProps = { +type ClientResourcesTableProps = { resources: ResourceRow[]; internalResources: InternalResourceRow[]; orgId: string; @@ -270,13 +270,13 @@ const setStoredColumnVisibility = ( } }; -export default function ResourcesTable({ +export default function ClientResourcesTable({ resources, internalResources, orgId, defaultView = "proxy", defaultSort -}: ResourcesTableProps) { +}: ClientResourcesTableProps) { const router = useRouter(); const searchParams = useSearchParams(); const t = useTranslations(); diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx new file mode 100644 index 00000000..14830840 --- /dev/null +++ b/src/components/ProxyResourcesTable.tsx @@ -0,0 +1,924 @@ +"use client"; + +import { + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + SortingState, + getSortedRowModel, + ColumnFiltersState, + getFilteredRowModel, + VisibilityState +} from "@tanstack/react-table"; +import { ExtendedColumnDef } from "@app/components/ui/data-table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuCheckboxItem +} from "@app/components/ui/dropdown-menu"; +import { Button } from "@app/components/ui/button"; +import { + ArrowRight, + ArrowUpDown, + MoreHorizontal, + ShieldOff, + ShieldCheck, + RefreshCw, + Columns, + Plus, + Search, + ChevronDown, + Clock, + CheckCircle2, + XCircle +} from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState, useEffect, useTransition } 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 { Card, CardContent, CardHeader } from "@app/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@app/components/ui/table"; + +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; + }; +}; + +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 ProxyResourcesTable({ + resources, + orgId, + defaultSort +}: ProxyResourcesTableProps) { + const router = useRouter(); + const t = useTranslations(); + + const { env } = useEnvContext(); + + const api = createApiClient({ env }); + + const [proxyPageSize, setProxyPageSize] = useState(() => + getStoredPageSize("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] = + useState(() => + getStoredColumnVisibility("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(() => { + 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 ( + + + + + + {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 ( + + ); + } + }, + { + accessorKey: "nice", + friendlyName: t("resource"), + enableHiding: true, + header: ({ column }) => { + return ( + + ); + } + }, + { + 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 ( + + ); + }, + 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 ( + + ); + }, + 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("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("viewSettings")} + + + { + setSelectedResource(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, + onColumnVisibilityChange: setProxyColumnVisibility, + initialState: { + pagination: { + pageSize: proxyPageSize, + pageIndex: 0 + }, + columnVisibility: proxyColumnVisibility + }, + state: { + sorting: proxySorting, + columnFilters: proxyColumnFilters, + globalFilter: proxyGlobalFilter, + columnVisibility: proxyColumnVisibility + } + }); + + const handleProxyPageSizeChange = (newPageSize: number) => { + setProxyPageSize(newPageSize); + setStoredPageSize(newPageSize, "proxy-resources"); + }; + + // Persist column visibility changes to localStorage + useEffect(() => { + setStoredColumnVisibility(proxyColumnVisibility, "proxy-resources"); + }, [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" + /> + +
+
+
+
+ +
+
+ +
+
+
+ +
+ + + {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" + )} + + + )} + +
+
+
+ +
+
+
+
+ + ); +}