import { h } from 'preact'; import { useState, useEffect } from 'preact/hooks'; import { getSignalStore, getMetaStore } from '../lib/stores'; import { formatValue } from '../lib/format'; import type { Widget, SignalRef, SignalValue, SignalMeta } from '../lib/types'; const DEFAULT_VALUE: SignalValue = { value: null, quality: 'unknown', ts: null }; const COLS = ['name', 'value', 'unit', 'quality', 'time'] as const; type Col = typeof COLS[number]; const COL_LABELS: Record = { name: 'Name', value: 'Value', unit: 'Unit', quality: 'Status', time: 'Time', }; function qualityColor(q: string): string { switch (q) { case 'good': return '#4ade80'; case 'uncertain': return '#fbbf24'; case 'bad': return '#f87171'; default: return '#6b7280'; } } function isCol(c: string): c is Col { return (COLS as readonly string[]).includes(c); } interface RowProps { sig: SignalRef; label: string; cols: Col[]; fmt: string; unitOverride: string; } // One subscribed row. Each row owns its own value/meta subscription so the // table can carry an arbitrary number of unrelated signals. function TableRow({ sig, label, cols, fmt, unitOverride }: RowProps) { const [sv, setSv] = useState(DEFAULT_VALUE); const [meta, setMeta] = useState(null); useEffect(() => { const uv = getSignalStore(sig).subscribe(setSv); const um = getMetaStore(sig).subscribe(setMeta); return () => { uv(); um(); }; }, [sig.ds, sig.name]); function valStr(): string { const v = sv.value; if (v === null || v === undefined) return '---'; if (typeof v === 'number') return formatValue(v, fmt); if (Array.isArray(v)) return `[${v.length}]`; return String(v); } const unit = unitOverride === 'none' ? '' : (unitOverride || meta?.unit || ''); const time = sv.ts ? new Date(sv.ts).toLocaleTimeString() : ''; return ( {cols.map(c => { switch (c) { case 'name': return {label}; case 'value': return {valStr()}; case 'unit': return {unit}; case 'quality': return ( ); case 'time': return {time}; } })} ); } interface Props { widget: Widget; onContextMenu?: (e: MouseEvent) => void; } // A tabular multi-signal readout: each bound signal is one row, with a // configurable set of columns (name / value / unit / status / time). export default function TableWidget({ widget, onContextMenu }: Props) { const o = widget.options; const parsed = (o['columns'] ?? 'name,value,unit') .split(',').map(s => s.trim()).filter(isCol); const cols: Col[] = parsed.length ? parsed : ['name', 'value']; const showHeader = o['header'] !== 'false'; const title = o['title'] ?? ''; const fmt = o['format'] ?? ''; const unitOverride = o['unit'] ?? ''; const labels = (o['labels'] ?? '').split(','); return (
{title &&
{title}
}
{showHeader && ( {cols.map(c => )} )} {widget.signals.length === 0 ? ( ) : ( widget.signals.map((s, i) => ( )) )}
{COL_LABELS[c]}
No signals — drop signals here
); }