From 54f7525f1bf55ee2f288821301a860225e4fe43f Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Tue, 21 Oct 2025 11:55:14 +0530 Subject: [PATCH] add status column in resource table --- server/routers/resource/listResources.ts | 26 ++- src/app/[orgId]/settings/resources/page.tsx | 9 +- src/components/ResourcesTable.tsx | 180 +++++++++++++++----- 3 files changed, 165 insertions(+), 50 deletions(-) diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index de2158c6..e612d5ec 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -8,6 +8,7 @@ import { resourcePassword, resourcePincode, targets, + targetHealthCheck, } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -63,6 +64,9 @@ type JoinedRow = { targetIp: string | null; targetPort: number | null; targetEnabled: boolean | null; + + hcHealth: string | null; + hcEnabled: boolean | null; }; // grouped by resource with targets[]) @@ -87,6 +91,7 @@ export type ResourceWithTargets = { ip: string; port: number; enabled: boolean; + healthStatus?: 'healthy' | 'unhealthy' | 'unknown'; }>; }; @@ -114,6 +119,8 @@ function queryResources(accessibleResourceIds: number[], orgId: string) { targetPort: targets.port, targetEnabled: targets.enabled, + hcHealth: targetHealthCheck.hcHealth, + hcEnabled: targetHealthCheck.hcEnabled, }) .from(resources) .leftJoin( @@ -129,6 +136,10 @@ function queryResources(accessibleResourceIds: number[], orgId: string) { eq(resourceHeaderAuth.resourceId, resources.resourceId) ) .leftJoin(targets, eq(targets.resourceId, resources.resourceId)) + .leftJoin( + targetHealthCheck, + eq(targetHealthCheck.targetId, targets.targetId) + ) .where( and( inArray(resources.resourceId, accessibleResourceIds), @@ -269,18 +280,19 @@ export async function listResources( map.set(row.resourceId, entry); } - // Push target if present - if ( - row.targetId != null && - row.targetIp && - row.targetPort != null && - row.targetEnabled != null - ) { + if (row.targetId != null && row.targetIp && row.targetPort != null && row.targetEnabled != null) { + let healthStatus: 'healthy' | 'unhealthy' | 'unknown' = 'unknown'; + + if (row.hcEnabled && row.hcHealth) { + healthStatus = row.hcHealth as 'healthy' | 'unhealthy' | 'unknown'; + } + entry.targets.push({ targetId: row.targetId, ip: row.targetIp, port: row.targetPort, enabled: row.targetEnabled, + healthStatus: healthStatus, }); } } diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index 0f8ee262..5b18c3c5 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -92,7 +92,14 @@ export default async function ResourcesPage(props: ResourcesPageProps) { : "not_protected", enabled: resource.enabled, domainId: resource.domainId || undefined, - ssl: resource.ssl + ssl: resource.ssl, + targets: resource.targets?.map(target => ({ + targetId: target.targetId, + ip: target.ip, + port: target.port, + enabled: target.enabled, + healthStatus: target.healthStatus + })) }; }); diff --git a/src/components/ResourcesTable.tsx b/src/components/ResourcesTable.tsx index a717ae1b..9faee629 100644 --- a/src/components/ResourcesTable.tsx +++ b/src/components/ResourcesTable.tsx @@ -32,6 +32,9 @@ import { Plus, Search, ChevronDown, + Clock, + Wifi, + WifiOff, } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -70,6 +73,15 @@ import EditInternalResourceDialog from "@app/components/EditInternalResourceDial import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog"; import { Alert, AlertDescription } from "@app/components/ui/alert"; + +export type TargetHealth = { + targetId: number; + ip: string; + port: number; + enabled: boolean; + healthStatus?: 'healthy' | 'unhealthy' | 'unknown'; +}; + export type ResourceRow = { id: number; nice: string | null; @@ -85,8 +97,52 @@ export type ResourceRow = { 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; + } +} export type InternalResourceRow = { id: number; name: string; @@ -150,6 +206,7 @@ const setStoredPageSize = (pageSize: number, tableId?: string): void => { }; + export default function ResourcesTable({ resources, internalResources, @@ -361,6 +418,76 @@ export default function ResourcesTable({ }); } + function TargetStatusCell({ targets }: { targets?: TargetHealth[] }) { + const overallStatus = getOverallHealthStatus(targets); + + if (!targets || targets.length === 0) { + return ( +
+ + No targets +
+ ); + } + + 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.healthStatus} + +
+ ))} + + )} + {unknownTargets.length > 0 && ( + <> + {unknownTargets.map((target) => ( + +
+ + +
+ + {!target.enabled ? 'Disabled' : 'Not monitored'} + +
+ ))} + + )} +
+
+ ); + } + + const proxyColumns: ColumnDef[] = [ { accessorKey: "name", @@ -403,8 +530,8 @@ export default function ResourcesTable({ } }, { - id: "target", - accessorKey: "target", + id: "status", + accessorKey: "status", header: ({ column }) => { return ( ); }, cell: ({ row }) => { - const resourceRow = row.original as ResourceRow & { - targets?: { ip: string; port: number }[]; - }; - - const targets = resourceRow.targets ?? []; - - if (targets.length === 0) { - return -; - } - - const count = targets.length; - - return ( - - - - - - - {targets.map((target, idx) => { - return ( - - - - ); - })} - - - ); + 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",