SPB Git

Repository Public

NodeLive.tsx 3.9 KB 69 lines tsx

1"use client";23import { getDict, type Locale } from "@/lib/i18n";4import { Meter, StatusDot } from "./bits";5import { fmtNum, fmtPct, fmtRate, fmtUptime } from "./format";6import { HistoryCard } from "./HistoryCard";7import { useAge, useLive } from "./store";89/** Live panel of one node record: current readings and their history. */10export function NodeLivePanel({ id, locale }: { id: string; locale: Locale }) {11  const d = getDict(locale);12  const t = d.live;13  const { ready, snap } = useLive();14  const age = useAge(snap?.ts);15  const live = snap?.nodes.find((n) => n.id === id);16  const on = live?.online ?? false;17  const facts: [string, string][] = on && live ? [18    [t.nodes.load, fmtNum(live.load1, locale, 2)],19    [t.nodes.uptime, fmtUptime(live.uptimeS, t.units)],20    [t.node.netIn, fmtRate(live.netInKBs, locale, t.units)],21    [t.node.netOut, fmtRate(live.netOutKBs, locale, t.units)],22    ...(live.thermal ? [[t.nodes.thermal, t.thermal[live.thermal as keyof typeof t.thermal] ?? live.thermal] as [string, string]] : []),23    ...(live.battery !== null ? [[t.nodes.battery, fmtPct(live.battery, locale)] as [string, string]] : []),24  ] : [];25  return (26    <div className="grid gap-3 lg:grid-cols-12" data-testid="node-live" data-state={snap ? (on ? "online" : "offline") : ready ? "unavailable" : "loading"}>27      <section aria-labelledby="live-h" className="card p-5 md:p-7 lg:col-span-5">28        <div className="flex flex-wrap items-center justify-between gap-3">29          <h2 id="live-h" className="text-lg font-semibold tracking-[-0.02em]">{t.node.title}</h2>30          <p className="flex items-center gap-2 text-[0.8125rem] text-body" role="status">31            <StatusDot state={on ? "on" : "off"} />32            <span className="font-medium text-fg">{!snap ? (ready ? t.unavailable.short : t.loading) : on ? t.nodes.online : t.nodes.offline}</span>33            {snap && age !== null ? <span className="num text-mute">· {t.updated(age)}</span> : null}34          </p>35        </div>36        {on && live ? (37          <>38            <div className="mt-6 space-y-4">39              <Meter label={t.chart.cpu} value={live.cpu} text={fmtPct(live.cpu, locale, 1)} />40              <Meter label={t.chart.memory} value={live.memPct} text={`${fmtNum(live.memUsedGB, locale, 1)} / ${live.memTotalGB} ${d.common.gb} · ${fmtPct(live.memPct, locale)}`} severity />41              <Meter label={t.chart.gpu} value={live.gpu} text={live.gpu !== null ? fmtPct(live.gpu, locale, 1) : "—"} muted={live.gpu === null} />42            </div>43            <dl className="num mt-6 grid grid-cols-2 gap-x-6 gap-y-4 border-t border-line pt-5 sm:grid-cols-3 lg:grid-cols-2 xl:grid-cols-3">44              {facts.map(([k, v]) => <div key={k}><dt className="text-[0.75rem] text-mute">{k}</dt><dd className="mt-0.5 font-mono text-[0.875rem] text-fg">{v}</dd></div>)}45            </dl>46          </>47        ) : (48          <p className="mt-6 rounded-xl bg-surface-2 p-5 text-[0.9375rem] leading-relaxed text-body">{!snap ? (ready ? t.unavailable.body : t.loading) : t.node.offlineBody}</p>49        )}50      </section>51      <div className="lg:col-span-7"><HistoryCard target={id} locale={locale} title={t.node.history} headingId="node-history-h" /></div>52    </div>53  );54}5556/** Small live reading for inventory cards: status dot and CPU, or nothing when telemetry is unavailable. */57export function NodeLiveInline({ id, locale }: { id: string; locale: Locale }) {58  const t = getDict(locale).live;59  const { snap } = useLive();60  const live = snap?.nodes.find((n) => n.id === id);61  if (!snap || !live) return null;62  return (63    <span className="inline-flex items-center gap-1.5 whitespace-nowrap font-mono text-[0.6875rem] text-body" data-testid="node-live-inline">64      <StatusDot state={live.online ? "on" : "off"} />65      {live.online ? <span className="num">{t.chart.cpu} {fmtPct(live.cpu, locale)}</span> : <span>{t.nodes.offline}</span>}66    </span>67  );68}69
Files main
Menu

Theme