|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import { Badge } from "@/components/ui/badge"; |
| 4 | +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; |
| 5 | +import { |
| 6 | + Table, |
| 7 | + TableBody, |
| 8 | + TableCell, |
| 9 | + TableHead, |
| 10 | + TableHeader, |
| 11 | + TableRow, |
| 12 | +} from "@/components/ui/table"; |
| 13 | +import { formatBytes, formatCount, formatUptime } from "@/lib/format"; |
| 14 | +import type { NodeInfo } from "@/lib/types"; |
| 15 | + |
| 16 | +interface ClusterNodesTableProps { |
| 17 | + nodes: NodeInfo[]; |
| 18 | + selectedNodeId: string | null; |
| 19 | + onSelectNode: (id: string | null) => void; |
| 20 | +} |
| 21 | + |
| 22 | +export function ClusterNodesTable({ |
| 23 | + nodes, |
| 24 | + selectedNodeId, |
| 25 | + onSelectNode, |
| 26 | +}: ClusterNodesTableProps) { |
| 27 | + if (nodes.length === 0) return null; |
| 28 | + |
| 29 | + return ( |
| 30 | + <Card className="border-flux-card-border bg-flux-card"> |
| 31 | + <CardHeader className="pb-3"> |
| 32 | + <div className="flex items-center justify-between"> |
| 33 | + <div> |
| 34 | + <CardTitle className="text-base text-flux-text"> |
| 35 | + Cluster Nodes |
| 36 | + </CardTitle> |
| 37 | + <p className="text-xs text-flux-text-muted mt-0.5"> |
| 38 | + {nodes.length} node{nodes.length !== 1 ? "s" : ""} |
| 39 | + </p> |
| 40 | + </div> |
| 41 | + </div> |
| 42 | + </CardHeader> |
| 43 | + <CardContent className="p-0"> |
| 44 | + <div className="overflow-x-auto"> |
| 45 | + <Table> |
| 46 | + <TableHeader> |
| 47 | + <TableRow className="border-flux-card-border hover:bg-transparent"> |
| 48 | + <TableHead className="pl-6">Node</TableHead> |
| 49 | + <TableHead>Address</TableHead> |
| 50 | + <TableHead className="text-right">Sessions</TableHead> |
| 51 | + <TableHead className="text-right">Subscriptions</TableHead> |
| 52 | + <TableHead className="text-right">Msgs In</TableHead> |
| 53 | + <TableHead className="text-right">Msgs Out</TableHead> |
| 54 | + <TableHead className="text-right">Bytes In</TableHead> |
| 55 | + <TableHead className="text-right pr-6">Uptime</TableHead> |
| 56 | + </TableRow> |
| 57 | + </TableHeader> |
| 58 | + <TableBody> |
| 59 | + {nodes.map((node) => { |
| 60 | + const isSelected = selectedNodeId === node.node_id; |
| 61 | + return ( |
| 62 | + <TableRow |
| 63 | + key={node.node_id} |
| 64 | + className={`border-flux-card-border transition-colors ${ |
| 65 | + isSelected |
| 66 | + ? "bg-flux-blue/10 hover:bg-flux-blue/15" |
| 67 | + : "hover:bg-flux-hover" |
| 68 | + }`} |
| 69 | + > |
| 70 | + <TableCell className="pl-6 py-4"> |
| 71 | + <button |
| 72 | + type="button" |
| 73 | + aria-pressed={isSelected} |
| 74 | + aria-label={`Inspect node ${node.node_id}`} |
| 75 | + onClick={() => |
| 76 | + onSelectNode(isSelected ? null : node.node_id) |
| 77 | + } |
| 78 | + className="flex w-full items-center gap-2 rounded text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-flux-blue" |
| 79 | + > |
| 80 | + <span className="inline-block w-2 h-2 rounded-full bg-flux-green shrink-0" /> |
| 81 | + <span className="text-flux-text font-medium text-sm"> |
| 82 | + {node.node_id} |
| 83 | + </span> |
| 84 | + {node.is_leader && ( |
| 85 | + <Badge |
| 86 | + variant="outline" |
| 87 | + className="text-xs bg-flux-blue/10 text-flux-blue border-flux-blue/20" |
| 88 | + > |
| 89 | + Leader |
| 90 | + </Badge> |
| 91 | + )} |
| 92 | + </button> |
| 93 | + </TableCell> |
| 94 | + <TableCell className="text-flux-text-muted font-mono text-xs py-4"> |
| 95 | + {node.addr} |
| 96 | + </TableCell> |
| 97 | + <TableCell className="text-flux-text text-sm text-right py-4"> |
| 98 | + {node.sessions !== undefined ? ( |
| 99 | + formatCount(node.sessions) |
| 100 | + ) : ( |
| 101 | + <span className="text-flux-text-muted">—</span> |
| 102 | + )} |
| 103 | + </TableCell> |
| 104 | + <TableCell className="text-flux-text text-sm text-right py-4"> |
| 105 | + {node.subscriptions !== undefined ? ( |
| 106 | + formatCount(node.subscriptions) |
| 107 | + ) : ( |
| 108 | + <span className="text-flux-text-muted">—</span> |
| 109 | + )} |
| 110 | + </TableCell> |
| 111 | + <TableCell className="text-flux-text text-sm text-right py-4"> |
| 112 | + {node.messages_received !== undefined ? ( |
| 113 | + formatCount(node.messages_received) |
| 114 | + ) : ( |
| 115 | + <span className="text-flux-text-muted">—</span> |
| 116 | + )} |
| 117 | + </TableCell> |
| 118 | + <TableCell className="text-flux-text text-sm text-right py-4"> |
| 119 | + {node.messages_sent !== undefined ? ( |
| 120 | + formatCount(node.messages_sent) |
| 121 | + ) : ( |
| 122 | + <span className="text-flux-text-muted">—</span> |
| 123 | + )} |
| 124 | + </TableCell> |
| 125 | + <TableCell className="text-flux-text text-sm text-right py-4"> |
| 126 | + {node.bytes_received !== undefined ? ( |
| 127 | + formatBytes(node.bytes_received) |
| 128 | + ) : ( |
| 129 | + <span className="text-flux-text-muted">—</span> |
| 130 | + )} |
| 131 | + </TableCell> |
| 132 | + <TableCell className="text-flux-text-muted text-sm text-right pr-6 py-4"> |
| 133 | + {formatUptime(node.uptime_seconds)} |
| 134 | + </TableCell> |
| 135 | + </TableRow> |
| 136 | + ); |
| 137 | + })} |
| 138 | + </TableBody> |
| 139 | + </Table> |
| 140 | + </div> |
| 141 | + </CardContent> |
| 142 | + </Card> |
| 143 | + ); |
| 144 | +} |
0 commit comments