--ADDED CPU,MEMORY and DISK METRICS
This commit is contained in:
george committed 2026-09-23 12:40:21 +05:30
1 parent 3b8e1dca3e
commit 08d79770cf
3 files changed
+165 -8

No files matched your search

+41
View File
@@ -755,3 +755,44 @@
grid-template-columns: 1fr;
}
}
.hardware-dashboard { max-width: 1200px; }
.hardware-nodes { display: grid; gap: 24px; margin-top: 24px; }
.hardware-node {
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg);
padding: 20px;
min-width: 0;
}
.hardware-node-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
overflow-wrap: anywhere;
}
.hardware-node-header h2, .hardware-node-header p { margin: 0 0 6px; }
.hardware-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
gap: 16px;
}
.hardware-metric {
min-width: 0;
padding: 18px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
}
.hardware-metric h3 { font-size: 15px; margin: 0 0 12px; }
.hardware-metric strong {
display: block;
color: var(--text-h);
font-size: 30px;
font-variant-numeric: tabular-nums;
}
.hardware-metric meter { width: 100%; height: 16px; margin-top: 12px; }
.hardware-metric p, .hardware-updated { font-size: 13px; margin: 12px 0 0; }
+15 -8
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { Link, Navigate, Route, Routes, useMatch, useNavigate, useParams } from 'react-router-dom'
import './App.css'
import NodeConnections from './NodeConnections'
import NodeHardware from './NodeHardware'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
@@ -379,7 +380,7 @@ function Workspace({ page: activePage }: { page: PageId | 'node-details' }) {
if (activePage === 'clusters') {
void loadClusters()
}
if (activePage === 'nodes' || activePage === 'clusters') {
if (activePage === 'dashboard' || activePage === 'nodes' || activePage === 'clusters') {
void loadNodeInterfaces()
}
}, [activePage])
@@ -654,14 +655,20 @@ function Workspace({ page: activePage }: { page: PageId | 'node-details' }) {
)}
{activePage === 'dashboard' && (
<section className="content-panel" id="dashboard">
<section className="content-panel hardware-dashboard" id="dashboard">
<p className="eyebrow">Dashboard</p>
<h1>Welcome back</h1>
<p>
Your main workspace is ready. The left navigation can collapse
now, and more sections can be added to the nav list as the app
grows.
</p>
<h1>Hardware usage</h1>
<p>Live CPU, memory, and disk usage for each registered node.</p>
{(nodesStatus === 'idle' || nodesStatus === 'loading') && <p role="status">Loading nodes…</p>}
{nodesStatus === 'error' && <div>
<p className="status-message error" role="alert">{nodesError}</p>
<button className="secondary-button" onClick={() => void loadNodeInterfaces()}>Retry loading nodes</button>
</div>}
{nodesStatus === 'success' && (nodes.length === 0
? <p>No registered nodes. Add a node from the Nodes page to monitor hardware usage.</p>
: <div className="hardware-nodes">
{nodes.map((node) => <NodeHardware key={`${node.uuid}:${node.host}`} name={node.if_name} host={node.host} />)}
</div>)}
</section>
)}
+109
View File
@@ -0,0 +1,109 @@
import { useEffect, useState } from 'react'
const fields = [
'cpu_usage_percent', 'memory_usage_percent', 'memory_total_mb',
'memory_used_mb', 'memory_available_mb', 'disk_usage_percent',
'disk_total_gb', 'disk_used_gb', 'disk_free_gb',
] as const
type HardwareStatus = Record<(typeof fields)[number], number>
function isHardwareStatus(value: unknown): value is HardwareStatus {
if (!value || typeof value !== 'object') return false
const data = value as Record<string, unknown>
return fields.every((field) => {
const number = data[field]
return typeof number === 'number' && Number.isFinite(number) && number >= 0
&& (!field.endsWith('_percent') || number <= 100)
})
}
function Metric({ label, value, detail }: { label: string; value?: number; detail?: string }) {
return (
<div className="hardware-metric">
<h3>{label}</h3>
<strong>{value === undefined ? '—' : `${value.toFixed(1)}%`}</strong>
{value !== undefined && <meter aria-label={`${label} usage`} min={0} max={100} value={value} />}
<p>{detail ?? 'Awaiting live data'}</p>
</div>
)
}
export default function NodeHardware({ name, host }: { name: string; host: string }) {
const [data, setData] = useState<HardwareStatus | null>(null)
const [status, setStatus] = useState('Connecting…')
const [updated, setUpdated] = useState<Date | null>(null)
const [attempt, setAttempt] = useState(0)
useEffect(() => {
let active = true
let socket: WebSocket | undefined
let timeout: ReturnType<typeof setTimeout> | undefined
const armTimeout = () => {
clearTimeout(timeout)
timeout = setTimeout(() => {
if (!active) return
setData(null)
setStatus('No recent hardware data. Reconnect to try again.')
}, 30000)
}
try {
socket = new WebSocket(`ws://${host}:9100/ws/hardware/status`)
armTimeout()
socket.onopen = () => {
if (active) setStatus('Waiting for hardware data…')
}
socket.onmessage = (event) => {
if (!active) return
try {
const message: unknown = JSON.parse(event.data)
if (!isHardwareStatus(message)) throw new Error('Invalid hardware data')
setData(message)
setUpdated(new Date())
setStatus('Live')
armTimeout()
} catch {
setData(null)
setStatus('Invalid hardware data received.')
}
}
const disconnected = () => {
if (!active) return
clearTimeout(timeout)
setData(null)
setStatus('Hardware feed disconnected. Reconnect to try again.')
}
socket.onerror = disconnected
socket.onclose = disconnected
} catch {
setStatus('Unable to connect to the hardware feed.')
}
return () => {
active = false
clearTimeout(timeout)
socket?.close()
}
}, [host, attempt])
return (
<article className="hardware-node">
<header className="hardware-node-header">
<div><h2>{name}</h2><p>{host}</p></div>
<div>
<p role="status" className={data ? 'connection-status connected' : ''}>{status}</p>
{!data && <button className="secondary-button" onClick={() => {
setData(null)
setUpdated(null)
setStatus('Connecting…')
setAttempt((value) => value + 1)
}}>Reconnect</button>}
</div>
</header>
<div className="hardware-metrics">
<Metric label="CPU" value={data?.cpu_usage_percent} detail={data ? 'Current processor usage' : undefined} />
<Metric label="Memory" value={data?.memory_usage_percent} detail={data ? `${data.memory_used_mb.toFixed(2)} / ${data.memory_total_mb.toFixed(2)} MB used · ${data.memory_available_mb.toFixed(2)} MB available` : undefined} />
<Metric label="Disk" value={data?.disk_usage_percent} detail={data ? `${data.disk_used_gb.toFixed(2)} / ${data.disk_total_gb.toFixed(2)} GB used · ${data.disk_free_gb.toFixed(2)} GB free` : undefined} />
</div>
{updated && <p className="hardware-updated">Last received: {updated.toLocaleTimeString()}</p>}
</article>
)
}