Skip to content

Commit 9edd0b5

Browse files
add status column in resource table
1 parent ad59e06 commit 9edd0b5

File tree

3 files changed

+165
-50
lines changed

3 files changed

+165
-50
lines changed

server/routers/resource/listResources.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
resourcePassword,
99
resourcePincode,
1010
targets,
11+
targetHealthCheck,
1112
} from "@server/db";
1213
import response from "@server/lib/response";
1314
import HttpCode from "@server/types/HttpCode";
@@ -63,6 +64,9 @@ type JoinedRow = {
6364
targetIp: string | null;
6465
targetPort: number | null;
6566
targetEnabled: boolean | null;
67+
68+
hcHealth: string | null;
69+
hcEnabled: boolean | null;
6670
};
6771

6872
// grouped by resource with targets[])
@@ -87,6 +91,7 @@ export type ResourceWithTargets = {
8791
ip: string;
8892
port: number;
8993
enabled: boolean;
94+
healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
9095
}>;
9196
};
9297

@@ -114,6 +119,8 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
114119
targetPort: targets.port,
115120
targetEnabled: targets.enabled,
116121

122+
hcHealth: targetHealthCheck.hcHealth,
123+
hcEnabled: targetHealthCheck.hcEnabled,
117124
})
118125
.from(resources)
119126
.leftJoin(
@@ -129,6 +136,10 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
129136
eq(resourceHeaderAuth.resourceId, resources.resourceId)
130137
)
131138
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
139+
.leftJoin(
140+
targetHealthCheck,
141+
eq(targetHealthCheck.targetId, targets.targetId)
142+
)
132143
.where(
133144
and(
134145
inArray(resources.resourceId, accessibleResourceIds),
@@ -269,18 +280,19 @@ export async function listResources(
269280
map.set(row.resourceId, entry);
270281
}
271282

272-
// Push target if present
273-
if (
274-
row.targetId != null &&
275-
row.targetIp &&
276-
row.targetPort != null &&
277-
row.targetEnabled != null
278-
) {
283+
if (row.targetId != null && row.targetIp && row.targetPort != null && row.targetEnabled != null) {
284+
let healthStatus: 'healthy' | 'unhealthy' | 'unknown' = 'unknown';
285+
286+
if (row.hcEnabled && row.hcHealth) {
287+
healthStatus = row.hcHealth as 'healthy' | 'unhealthy' | 'unknown';
288+
}
289+
279290
entry.targets.push({
280291
targetId: row.targetId,
281292
ip: row.targetIp,
282293
port: row.targetPort,
283294
enabled: row.targetEnabled,
295+
healthStatus: healthStatus,
284296
});
285297
}
286298
}

src/app/[orgId]/settings/resources/page.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,14 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
9292
: "not_protected",
9393
enabled: resource.enabled,
9494
domainId: resource.domainId || undefined,
95-
ssl: resource.ssl
95+
ssl: resource.ssl,
96+
targets: resource.targets?.map(target => ({
97+
targetId: target.targetId,
98+
ip: target.ip,
99+
port: target.port,
100+
enabled: target.enabled,
101+
healthStatus: target.healthStatus
102+
}))
96103
};
97104
});
98105

src/components/ResourcesTable.tsx

Lines changed: 138 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ import {
3232
Plus,
3333
Search,
3434
ChevronDown,
35+
Clock,
36+
Wifi,
37+
WifiOff,
3538
} from "lucide-react";
3639
import Link from "next/link";
3740
import { useRouter } from "next/navigation";
@@ -70,6 +73,15 @@ import EditInternalResourceDialog from "@app/components/EditInternalResourceDial
7073
import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog";
7174
import { 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+
7385
export 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+
}
90146
export type InternalResourceRow = {
91147
id: number;
92148
name: string;
@@ -150,6 +206,7 @@ const setStoredPageSize = (pageSize: number, tableId?: string): void => {
150206
};
151207

152208

209+
153210
export 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

Comments
 (0)