Group monitoring-app resources into one Node Status tile per integration
Integrations whose resources represent many sub-items (Uptime Kuma's monitors) now collapse into a single tile using the Uptime Kuma CDN icon, instead of flooding Node Status with one tile per monitor. Selecting that tile lists every underlying monitor's status in a scrollable Node Detail panel, so hundreds of monitors stay manageable. Also drops the temporary debug logging added while diagnosing the listener-timing bug, now that real monitor/heartbeat data confirmed coming through.
This commit is contained in:
parent
0b9acd32a5
commit
d50ec0076e
2 changed files with 98 additions and 13 deletions
|
|
@ -99,11 +99,6 @@ export const uptimeKuma: IntegrationAdapter = {
|
|||
// single "done" signal, so give it a short window to arrive.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2500))
|
||||
|
||||
console.log(`[uptimeKuma] received ${monitors.size} monitor(s), ${lastHeartbeat.size} heartbeat(s)`)
|
||||
for (const m of monitors.values()) {
|
||||
console.log(`[uptimeKuma] monitor ${m.id} "${m.name}" active=${m.active} heartbeat=${JSON.stringify(lastHeartbeat.get(m.id))}`)
|
||||
}
|
||||
|
||||
const resources: Resource[] = []
|
||||
for (const monitor of monitors.values()) {
|
||||
if (!monitor.active) continue
|
||||
|
|
|
|||
|
|
@ -68,6 +68,25 @@ const nodeStatusColor: Record<string, string> = {
|
|||
|
||||
const integrationPalette = ['#C8A434', '#E67E22', '#2ECC71', '#7A7D85', '#3B82F6', '#8B5E3C']
|
||||
|
||||
// Kinds that represent a monitoring/aggregator app with many sub-items (e.g. Uptime
|
||||
// Kuma's individual monitors) collapse into one tile per integration on Node Status,
|
||||
// with the underlying items shown in Node Detail once selected.
|
||||
const groupedKinds = new Set(['app'])
|
||||
|
||||
const cdnIconByIntegrationKind: Record<string, string> = {
|
||||
app: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/uptime-kuma.png',
|
||||
}
|
||||
|
||||
interface NodeGroup {
|
||||
isGroup: true
|
||||
integration: string
|
||||
kind: string
|
||||
status: Resource['status']
|
||||
members: Resource[]
|
||||
}
|
||||
|
||||
type NodeTile = Resource | NodeGroup
|
||||
|
||||
const kindIcon: Record<string, LucideIcon> = {
|
||||
vm: MonitorSmartphone,
|
||||
container: Box,
|
||||
|
|
@ -116,7 +135,7 @@ export default function Infrastructure() {
|
|||
const [resources, setResources] = useState<Resource[] | null>(null)
|
||||
const [integrations, setIntegrations] = useState<Integration[] | null>(null)
|
||||
const [events, setEvents] = useState<Event[] | null>(null)
|
||||
const [selectedNode, setSelectedNode] = useState<Resource | null>(null)
|
||||
const [selectedNode, setSelectedNode] = useState<NodeTile | null>(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -144,6 +163,37 @@ export default function Infrastructure() {
|
|||
return Array.from(byIntegration.entries()).map(([name, value], i) => ({ name, value, color: integrationPalette[i % integrationPalette.length] }))
|
||||
}, [resources])
|
||||
|
||||
// Kinds like Uptime Kuma's monitors can number in the hundreds — collapse them into
|
||||
// one tile per integration on Node Status, with members listed in Node Detail.
|
||||
const nodeTiles = useMemo<NodeTile[]>(() => {
|
||||
if (!resources) return []
|
||||
const groups = new Map<string, Resource[]>()
|
||||
const singles: Resource[] = []
|
||||
for (const r of resources) {
|
||||
if (r.kind && groupedKinds.has(r.kind)) {
|
||||
const arr = groups.get(r.integration) ?? []
|
||||
arr.push(r)
|
||||
groups.set(r.integration, arr)
|
||||
} else {
|
||||
singles.push(r)
|
||||
}
|
||||
}
|
||||
const groupTiles: NodeGroup[] = Array.from(groups.entries()).map(([integration, members]) => ({
|
||||
isGroup: true,
|
||||
integration,
|
||||
kind: members[0]?.kind ?? '',
|
||||
status: members.some((m) => m.status === 'critical')
|
||||
? 'critical'
|
||||
: members.some((m) => m.status === 'warning')
|
||||
? 'warning'
|
||||
: members.every((m) => m.status === 'healthy')
|
||||
? 'healthy'
|
||||
: 'unknown',
|
||||
members,
|
||||
}))
|
||||
return [...singles, ...groupTiles]
|
||||
}, [resources])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Sub-tabs + Add Resource — hero banner is rendered at the layout level behind this */}
|
||||
|
|
@ -254,14 +304,17 @@ export default function Infrastructure() {
|
|||
<div style={cardVignette} />
|
||||
<div className="relative z-10 flex min-h-0 flex-1 flex-col">
|
||||
<h3 style={sectionTitle}>Node Status</h3>
|
||||
{resources && resources.length > 0 ? (
|
||||
{nodeTiles.length > 0 ? (
|
||||
<div className="scrollbar-ghost grid min-h-0 flex-1 grid-cols-5 content-start gap-2" style={{ overflowY: 'auto' }}>
|
||||
{resources.map((node, i) => {
|
||||
{nodeTiles.map((node, i) => {
|
||||
const cdnIcon = cdnIconByIntegrationKind[node.kind ?? '']
|
||||
const NodeIcon = kindIcon[node.kind ?? ''] ?? Server
|
||||
const label = 'isGroup' in node ? node.integration : node.name
|
||||
const tooltip = 'isGroup' in node ? `${node.integration}: ${node.members.length} monitored` : `${node.name}: ${node.detail ?? node.status}`
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
title={`${node.name}: ${node.detail ?? node.status}`}
|
||||
title={tooltip}
|
||||
onClick={() => setSelectedNode(node)}
|
||||
className="cursor-pointer transition-colors"
|
||||
style={{
|
||||
|
|
@ -276,9 +329,13 @@ export default function Infrastructure() {
|
|||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span style={{ width: '7px', height: '7px', borderRadius: '50%', backgroundColor: nodeStatusColor[node.status], boxShadow: `0 0 6px ${nodeStatusColor[node.status]}` }} />
|
||||
{cdnIcon ? (
|
||||
<img src={cdnIcon} width={12} height={12} style={{ borderRadius: '2px' }} alt="" />
|
||||
) : (
|
||||
<NodeIcon size={12} style={{ color: '#7A7D85' }} />
|
||||
)}
|
||||
</div>
|
||||
<span style={{ fontSize: '11px', color: '#E8E6E0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{node.name}</span>
|
||||
<span style={{ fontSize: '11px', color: '#E8E6E0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{label}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
|
@ -322,9 +379,42 @@ export default function Infrastructure() {
|
|||
|
||||
{/* Node Detail — shows the node selected up in Node Status */}
|
||||
<div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
|
||||
<div className="relative z-10">
|
||||
<div className="relative z-10 flex flex-col" style={{ maxHeight: '220px' }}>
|
||||
<h3 style={sectionTitle}>Node Detail</h3>
|
||||
{selectedNode ? (
|
||||
{selectedNode && 'isGroup' in selectedNode ? (
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: nodeStatusColor[selectedNode.status],
|
||||
boxShadow: `0 0 6px ${nodeStatusColor[selectedNode.status]}`,
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '14px', fontWeight: 600, color: '#E8E6E0' }}>{selectedNode.integration}</span>
|
||||
<span style={{ fontSize: '11px', color: '#7A7D85' }}>({selectedNode.members.length} monitored)</span>
|
||||
</div>
|
||||
<div className="scrollbar-ghost flex flex-col gap-1.5" style={{ overflowY: 'auto' }}>
|
||||
{selectedNode.members.map((m, i) => (
|
||||
<div key={i} className="flex items-center gap-2" style={{ fontSize: '11.5px' }}>
|
||||
<span
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: nodeStatusColor[m.status],
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: '#E8E6E0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{m.name}</span>
|
||||
<span style={{ color: '#7A7D85', marginLeft: 'auto', flexShrink: 0 }}>{m.detail ?? m.status}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : selectedNode ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue