@@ -32,6 +32,9 @@ import {
3232 Plus ,
3333 Search ,
3434 ChevronDown ,
35+ Clock ,
36+ Wifi ,
37+ WifiOff ,
3538} from "lucide-react" ;
3639import Link from "next/link" ;
3740import { useRouter } from "next/navigation" ;
@@ -70,6 +73,15 @@ import EditInternalResourceDialog from "@app/components/EditInternalResourceDial
7073import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog" ;
7174import { Alert , AlertDescription } from "@app/components/ui/alert" ;
7275
76+
77+ export type TargetHealth = {
78+ targetId : number ;
79+ ip : string ;
80+ port : number ;
81+ enabled : boolean ;
82+ healthStatus ?: 'healthy' | 'unhealthy' | 'unknown' ;
83+ } ;
84+
7385export type ResourceRow = {
7486 id : number ;
7587 nice : string | null ;
@@ -85,8 +97,52 @@ export type ResourceRow = {
8597 ssl : boolean ;
8698 targetHost ?: string ;
8799 targetPort ?: number ;
100+ targets ?: TargetHealth [ ] ;
88101} ;
89102
103+
104+ function getOverallHealthStatus ( targets ?: TargetHealth [ ] ) : 'online' | 'degraded' | 'offline' | 'unknown' {
105+ if ( ! targets || targets . length === 0 ) {
106+ return 'unknown' ;
107+ }
108+
109+ const monitoredTargets = targets . filter ( t => t . enabled && t . healthStatus && t . healthStatus !== 'unknown' ) ;
110+
111+ if ( monitoredTargets . length === 0 ) {
112+ return 'unknown' ;
113+ }
114+
115+ const healthyCount = monitoredTargets . filter ( t => t . healthStatus === 'healthy' ) . length ;
116+ const unhealthyCount = monitoredTargets . filter ( t => t . healthStatus === 'unhealthy' ) . length ;
117+
118+ if ( healthyCount === monitoredTargets . length ) {
119+ return 'online' ;
120+ } else if ( unhealthyCount === monitoredTargets . length ) {
121+ return 'offline' ;
122+ } else {
123+ return 'degraded' ;
124+ }
125+ }
126+
127+ function StatusIcon ( { status, className = "" } : {
128+ status : 'online' | 'degraded' | 'offline' | 'unknown' ;
129+ className ?: string ;
130+ } ) {
131+ const iconClass = `h-4 w-4 ${ className } ` ;
132+
133+ switch ( status ) {
134+ case 'online' :
135+ return < Wifi className = { `${ iconClass } text-green-500` } /> ;
136+ case 'degraded' :
137+ return < Wifi className = { `${ iconClass } text-yellow-500` } /> ;
138+ case 'offline' :
139+ return < WifiOff className = { `${ iconClass } text-red-500` } /> ;
140+ case 'unknown' :
141+ return < Clock className = { `${ iconClass } text-gray-400` } /> ;
142+ default :
143+ return null ;
144+ }
145+ }
90146export type InternalResourceRow = {
91147 id : number ;
92148 name : string ;
@@ -150,6 +206,7 @@ const setStoredPageSize = (pageSize: number, tableId?: string): void => {
150206} ;
151207
152208
209+
153210export default function ResourcesTable ( {
154211 resources,
155212 internalResources,
@@ -361,6 +418,76 @@ export default function ResourcesTable({
361418 } ) ;
362419 }
363420
421+ function TargetStatusCell ( { targets } : { targets ?: TargetHealth [ ] } ) {
422+ const overallStatus = getOverallHealthStatus ( targets ) ;
423+
424+ if ( ! targets || targets . length === 0 ) {
425+ return (
426+ < div className = "flex items-center gap-2" >
427+ < StatusIcon status = "unknown" />
428+ < span className = "text-sm text-muted-foreground" > No targets</ span >
429+ </ div >
430+ ) ;
431+ }
432+
433+ const monitoredTargets = targets . filter ( t => t . enabled && t . healthStatus && t . healthStatus !== 'unknown' ) ;
434+ const unknownTargets = targets . filter ( t => ! t . enabled || ! t . healthStatus || t . healthStatus === 'unknown' ) ;
435+
436+ return (
437+ < DropdownMenu >
438+ < DropdownMenuTrigger asChild >
439+ < Button variant = "ghost" size = "sm" className = "flex items-center gap-2 h-8" >
440+ < StatusIcon status = { overallStatus } />
441+ < span className = "text-sm" >
442+ { overallStatus === 'online' && 'Healthy' }
443+ { overallStatus === 'degraded' && 'Degraded' }
444+ { overallStatus === 'offline' && 'Offline' }
445+ { overallStatus === 'unknown' && 'Unknown' }
446+ </ span >
447+ < ChevronDown className = "h-3 w-3" />
448+ </ Button >
449+ </ DropdownMenuTrigger >
450+ < DropdownMenuContent align = "start" className = "min-w-[280px]" >
451+ { monitoredTargets . length > 0 && (
452+ < >
453+ { monitoredTargets . map ( ( target ) => (
454+ < DropdownMenuItem key = { target . targetId } className = "flex items-center justify-between gap-4" >
455+ < div className = "flex items-center gap-2" >
456+ < StatusIcon
457+ status = { target . healthStatus === 'healthy' ? 'online' : 'offline' }
458+ className = "h-3 w-3"
459+ />
460+ < CopyToClipboard text = { `${ target . ip } :${ target . port } ` } />
461+ </ div >
462+ < span className = { `text-xs capitalize ${ target . healthStatus === 'healthy' ? 'text-green-600' : 'text-red-600'
463+ } `} >
464+ { target . healthStatus }
465+ </ span >
466+ </ DropdownMenuItem >
467+ ) ) }
468+ </ >
469+ ) }
470+ { unknownTargets . length > 0 && (
471+ < >
472+ { unknownTargets . map ( ( target ) => (
473+ < DropdownMenuItem key = { target . targetId } className = "flex items-center justify-between gap-4" >
474+ < div className = "flex items-center gap-2" >
475+ < StatusIcon status = "unknown" className = "h-3 w-3" />
476+ < CopyToClipboard text = { `${ target . ip } :${ target . port } ` } />
477+ </ div >
478+ < span className = "text-xs text-gray-500" >
479+ { ! target . enabled ? 'Disabled' : 'Not monitored' }
480+ </ span >
481+ </ DropdownMenuItem >
482+ ) ) }
483+ </ >
484+ ) }
485+ </ DropdownMenuContent >
486+ </ DropdownMenu >
487+ ) ;
488+ }
489+
490+
364491 const proxyColumns : ColumnDef < ResourceRow > [ ] = [
365492 {
366493 accessorKey : "name" ,
@@ -403,8 +530,8 @@ export default function ResourcesTable({
403530 }
404531 } ,
405532 {
406- id : "target " ,
407- accessorKey : "target " ,
533+ id : "status " ,
534+ accessorKey : "status " ,
408535 header : ( { column } ) => {
409536 return (
410537 < Button
@@ -413,52 +540,21 @@ export default function ResourcesTable({
413540 column . toggleSorting ( column . getIsSorted ( ) === "asc" )
414541 }
415542 >
416- { t ( "target " ) }
543+ { t ( "status " ) }
417544 < ArrowUpDown className = "ml-2 h-4 w-4" />
418545 </ Button >
419546 ) ;
420547 } ,
421548 cell : ( { row } ) => {
422- const resourceRow = row . original as ResourceRow & {
423- targets ?: { ip : string ; port : number } [ ] ;
424- } ;
425-
426- const targets = resourceRow . targets ?? [ ] ;
427-
428- if ( targets . length === 0 ) {
429- return < span className = "text-muted-foreground" > -</ span > ;
430- }
431-
432- const count = targets . length ;
433-
434- return (
435- < DropdownMenu >
436- < DropdownMenuTrigger asChild >
437- < Button
438- variant = "ghost"
439- size = "sm"
440- className = "flex items-center"
441- >
442- < ChevronDown className = "h-4 w-4 mr-1" />
443- { `${ count } Configurations` }
444- </ Button >
445- </ DropdownMenuTrigger >
446-
447- < DropdownMenuContent align = "start" className = "min-w-[200px]" >
448- { targets . map ( ( target , idx ) => {
449- return (
450- < DropdownMenuItem key = { idx } className = "flex items-center gap-2" >
451- < CopyToClipboard
452- text = { `${ target . ip } :${ target . port } ` }
453- isLink = { false }
454- />
455- </ DropdownMenuItem >
456- ) ;
457- } ) }
458- </ DropdownMenuContent >
459- </ DropdownMenu >
460- ) ;
549+ const resourceRow = row . original ;
550+ return < TargetStatusCell targets = { resourceRow . targets } /> ;
461551 } ,
552+ sortingFn : ( rowA , rowB ) => {
553+ const statusA = getOverallHealthStatus ( rowA . original . targets ) ;
554+ const statusB = getOverallHealthStatus ( rowB . original . targets ) ;
555+ const statusOrder = { online : 3 , degraded : 2 , offline : 1 , unknown : 0 } ;
556+ return statusOrder [ statusA ] - statusOrder [ statusB ] ;
557+ }
462558 } ,
463559 {
464560 accessorKey : "domain" ,
0 commit comments