The disks box is drives; the net box lets you pick the interface
Disks: one row per physical drive with its model, size, temperature and IO, and a usage bar built from the filesystems the sampler can attribute to it. Attribution is new — a mount's source device is resolved through sysfs to the drive behind it, following one layer of device-mapper or md and folding NVMe namespaces the way the sampler does. A pool member gets no usage bar, because one drive's share of a raidz pool is not a real number. Net: every interface that is up is listed and tappable, defaulting to the one that has actually carried traffic rather than whichever the kernel listed first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bf0bdd54af
commit
e0625be684
+41
-14
@@ -250,6 +250,7 @@ export default function Top() {
|
||||
|
||||
// the process table, and the CPU model, come from the same place the Processes page reads
|
||||
const [pt, setPt] = useState<ProcTable | null>(null)
|
||||
const [pickedIf, setPickedIf] = useState<string | null>(null) // which NIC the net box draws
|
||||
const [err, setErr] = useState('')
|
||||
useEffect(() => {
|
||||
let stop = false
|
||||
@@ -319,7 +320,10 @@ export default function Top() {
|
||||
const free = (mem as { free?: number }).free ?? Math.max(0, mem.total - mem.used - cached)
|
||||
const swap = s?.swap ?? pt?.swap
|
||||
const net = (s?.net ?? pt?.net ?? []).filter((n) => n.up && n.iface !== 'lo')
|
||||
const iface = net[0]
|
||||
// the one that has actually carried traffic, not whichever the kernel listed first — wg0
|
||||
// and a dormant wlan should not push the real NIC out of the way. Tapping a name switches.
|
||||
const busiest = net.length ? net.reduce((a, b) => (b.rx_total + b.tx_total > a.rx_total + a.tx_total ? b : a)) : undefined
|
||||
const iface = net.find((n) => n.iface === pickedIf) ?? busiest
|
||||
// the disks box is this machine's own storage: a CIFS or NFS share is somebody else's disk
|
||||
const fs = (s?.fs ?? []).filter((f) => !f.remote)
|
||||
const pct = (a: number, b: number) => (b > 0 ? (a / b) * 100 : 0)
|
||||
@@ -379,18 +383,26 @@ export default function Top() {
|
||||
|
||||
<Box n={3} name="disks" className="top-disks" right="io">
|
||||
<div className="t-disks">
|
||||
{swap && swap.total > 0 && <Disk name="swap" size={swap.total} used={swap.used} />}
|
||||
{fs.map((f) => {
|
||||
// which drive is behind a mount isn't in the sample, so only the root filesystem
|
||||
// gets an IO line, against the machine's largest drive — which is the one it is on
|
||||
const d = f.mount === '/' ? [...(s?.disks ?? [])].sort((a, b) => b.size - a.size)[0] : undefined
|
||||
return <Disk key={f.mount} name={f.mount === '/' ? 'root' : f.mount.split('/').filter(Boolean).pop() ?? f.mount} size={f.size} used={f.used} io={d ? d.util_pct : undefined} />
|
||||
{(s?.disks ?? []).map((d) => {
|
||||
// what is on this drive: the filesystems the sampler could attribute to it. A ZFS
|
||||
// pool or an md array spans several drives, so nothing is attributed and the row
|
||||
// shows what is true of the hardware — how busy it is, and how hot.
|
||||
const on = fs.filter((f) => f.disk === d.name)
|
||||
const used = on.reduce((a, f) => a + f.used, 0)
|
||||
return <Disk key={d.name} name={d.name} note={d.model} size={d.size} used={on.length ? used : undefined} io={d.util_pct} temp={d.temp} mounts={on.map((f) => f.mount)} />
|
||||
})}
|
||||
{fs.length === 0 && <div className="t-dim">no filesystems reported</div>}
|
||||
{(s?.disks ?? []).length === 0 && <div className="t-dim">no drives reported</div>}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box n={4} name="net" className="top-net" right={iface ? <span className="t-if">{iface.iface}</span> : undefined}>
|
||||
<Box n={4} name="net" className="top-net" right={net.length ? (
|
||||
<span className="t-ifs">
|
||||
{net.map((n) => (
|
||||
<button key={n.iface} className={'t-if' + (iface && n.iface === iface.iface ? ' on' : '')}
|
||||
title={`${n.iface} — ${bytes(n.rx_total)} in, ${bytes(n.tx_total)} out`} onClick={() => setPickedIf(n.iface)}>{n.iface}</button>
|
||||
))}
|
||||
</span>
|
||||
) : undefined}>
|
||||
{iface ? (
|
||||
<div className="t-net">
|
||||
<div className="t-net-graphs">
|
||||
@@ -471,14 +483,29 @@ const Rows = memo(function Rows({ procs, sel, onSel, curves, empty }: { procs: P
|
||||
)
|
||||
})
|
||||
|
||||
function Disk({ name, size, used, io }: { name: string; size: number; used: number; io?: number }) {
|
||||
const upct = size > 0 ? (used / size) * 100 : 0
|
||||
// One physical drive. `used` is the bytes of the filesystems on it, left out when none can
|
||||
// be attributed to it — a pool member's share of a pool is not a number worth inventing.
|
||||
function Disk({ name, note, size, used, io, temp, mounts }: { name: string; note?: string; size: number; used?: number; io?: number; temp?: number; mounts?: string[] }) {
|
||||
const upct = used !== undefined && size > 0 ? (used / size) * 100 : 0
|
||||
return (
|
||||
<div className="t-disk">
|
||||
<div className="t-row"><b>{name}</b><span className="spacer" />{bytes(size)}</div>
|
||||
<div className="t-row">
|
||||
<b>{name}</b>
|
||||
{note && <span className="t-dim t-model" title={note}>{note}</span>}
|
||||
<span className="spacer" />
|
||||
{temp !== undefined && temp > 0 && <span className="t-temp">{Math.round(temp)}°C</span>}
|
||||
{bytes(size)}
|
||||
</div>
|
||||
{io !== undefined && <div className="t-row t-sub"><span>IO%</span><Meter pct={io} grad="io" /></div>}
|
||||
<div className="t-row t-sub"><span>Used:</span><span className="t-pct">{Math.round(upct)}%</span><Meter pct={upct} grad="used" /><span className="t-amt">{bytes(used, 1)}</span></div>
|
||||
<div className="t-row t-sub"><span>Free:</span><span className="t-pct">{Math.round(100 - upct)}%</span><Meter pct={100 - upct} grad="free" /><span className="t-amt">{bytes(Math.max(0, size - used), 1)}</span></div>
|
||||
{used !== undefined ? (
|
||||
<>
|
||||
<div className="t-row t-sub"><span>Used:</span><span className="t-pct">{Math.round(upct)}%</span><Meter pct={upct} grad="used" /><span className="t-amt">{bytes(used, 1)}</span></div>
|
||||
<div className="t-row t-sub"><span>Free:</span><span className="t-pct">{Math.round(100 - upct)}%</span><Meter pct={100 - upct} grad="free" /><span className="t-amt">{bytes(Math.max(0, size - used), 1)}</span></div>
|
||||
{mounts && mounts.length > 0 && <div className="t-row t-sub t-dim t-mounts" title={mounts.join(' ')}>{mounts.join(' ')}</div>}
|
||||
</>
|
||||
) : (
|
||||
<div className="t-row t-sub t-dim">no filesystem of this machine is on it</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user