Quality of life: in-app dialogs, selections in the URL, editable widgets, token dialog, gallery search, status history, stream errors that say why, tab reordering and shortcuts, log out everywhere
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
8ee2a2d293
commit
d7ed4d8549
+10
-5
@@ -35,6 +35,7 @@ export default function Gallery({ demoUrl }: { demoUrl?: string }) {
|
||||
const [cat, setCat] = useState<Sensor[] | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
const rate = useRefreshMs()
|
||||
const [find, setFind] = useState('')
|
||||
useEffect(() => { fetchCatalog().then(setCat).catch((e) => setErr(e instanceof Error ? e.message : 'failed')) }, [])
|
||||
// Values only, at most once a second: the catalog already holds everything that doesn't change.
|
||||
const live = useSensorStream<[Record<string, Sensor['value']>]>(['*'], { full: false, everyMs: Math.max(1000, rate) })
|
||||
@@ -47,7 +48,9 @@ export default function Gallery({ demoUrl }: { demoUrl?: string }) {
|
||||
|
||||
const byId = new Map(sensors.map((s) => [s.id, s]))
|
||||
const families = new Map<string, Sensor[]>()
|
||||
for (const s of sensors) { const f = s.id.split('.')[0]; families.set(f, [...(families.get(f) ?? []), s]) }
|
||||
const needle = find.trim().toLowerCase()
|
||||
const shown = needle ? sensors.filter((s) => s.id.includes(needle) || s.name.toLowerCase().includes(needle) || Object.values(s.labels ?? {}).some((l) => l.toLowerCase().includes(needle))) : sensors
|
||||
for (const s of shown) { const f = s.id.split('.')[0]; families.set(f, [...(families.get(f) ?? []), s]) }
|
||||
const names = [...families.keys()].sort((a, b) => (ORDER.indexOf(a) + 1 || 99) - (ORDER.indexOf(b) + 1 || 99) || a.localeCompare(b))
|
||||
const demo = byId.get('cpu.usage')
|
||||
const cores = Object.fromEntries(sensors.filter((s) => /^cpu\.core\.\d+\.usage$/.test(s.id)).map((s) => [s.id, s]))
|
||||
@@ -55,16 +58,18 @@ export default function Gallery({ demoUrl }: { demoUrl?: string }) {
|
||||
return (
|
||||
<div className="dash gallery">
|
||||
<div className="dash-bar">
|
||||
<span className="hint">{sensors.length} sensors on this machine · live · tap a tile to copy its ID</span>
|
||||
<span className="hint">{needle ? `${shown.length} of ${sensors.length}` : sensors.length} sensors on this machine · live · tap a tile to copy its ID</span>
|
||||
<input className="gallery-find" value={find} onChange={(e) => setFind(e.target.value)} placeholder="find — gitea, temp, enp3s0…" spellCheck={false} autoCapitalize="off" />
|
||||
<span className="spacer" />
|
||||
{demoUrl && <a className="btn" href={demoUrl} target="_blank" rel="noreferrer" title="a page outside Ham Lab, built on the same sensor API">External demo ↗</a>}
|
||||
</div>
|
||||
|
||||
<h2 className="section">Widget types</h2>
|
||||
<div className="kpis">
|
||||
{!needle && <h2 className="section">Widget types</h2>}
|
||||
{!needle && <div className="kpis">
|
||||
{demo && WIDGET_TYPES.filter((t) => !t.many).map((t) => <Tile key={t.id} spec={{ id: t.id, type: t.id, sensor: 'cpu.usage', title: t.label }} data={demo} note={`type=${t.id}`} />)}
|
||||
<Tile spec={{ id: 'list', type: 'list', sensor: 'cpu.core.*.usage', title: 'List' }} data={cores} note="type=list · cpu.core.*.usage" />
|
||||
</div>
|
||||
</div>}
|
||||
{needle && shown.length === 0 && <div className="muted">No sensor matches “{find}”.</div>}
|
||||
|
||||
{names.map((f) => (
|
||||
<section key={f}>
|
||||
|
||||
Reference in New Issue
Block a user