Implemented admin pane + user permission
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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<Col, string> = {
|
||||
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<SignalValue>(DEFAULT_VALUE);
|
||||
const [meta, setMeta] = useState<SignalMeta | null>(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 (
|
||||
<tr>
|
||||
{cols.map(c => {
|
||||
switch (c) {
|
||||
case 'name':
|
||||
return <td key={c} class="tw-name" title={`${sig.ds}:${sig.name}`}>{label}</td>;
|
||||
case 'value':
|
||||
return <td key={c} class="tw-value">{valStr()}</td>;
|
||||
case 'unit':
|
||||
return <td key={c} class="tw-unit">{unit}</td>;
|
||||
case 'quality':
|
||||
return (
|
||||
<td key={c} class="tw-quality">
|
||||
<span class="quality-dot" style={`background:${qualityColor(sv.quality)};`} title={`Quality: ${sv.quality}`} />
|
||||
</td>
|
||||
);
|
||||
case 'time':
|
||||
return <td key={c} class="tw-time">{time}</td>;
|
||||
}
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
class="table-widget"
|
||||
style={`left:${widget.x}px;top:${widget.y}px;width:${widget.w}px;height:${widget.h}px;`}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
{title && <div class="tw-title">{title}</div>}
|
||||
<div class="tw-scroll">
|
||||
<table class="tw-table">
|
||||
{showHeader && (
|
||||
<thead>
|
||||
<tr>{cols.map(c => <th key={c} class={`tw-th-${c}`}>{COL_LABELS[c]}</th>)}</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody>
|
||||
{widget.signals.length === 0 ? (
|
||||
<tr><td class="tw-empty" colSpan={cols.length}>No signals — drop signals here</td></tr>
|
||||
) : (
|
||||
widget.signals.map((s, i) => (
|
||||
<TableRow
|
||||
key={`${s.ds}:${s.name}`}
|
||||
sig={s}
|
||||
label={labels[i]?.trim() || s.name}
|
||||
cols={cols}
|
||||
fmt={fmt}
|
||||
unitOverride={unitOverride}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user