153 lines
5.5 KiB
TypeScript
153 lines
5.5 KiB
TypeScript
import { h } from 'preact';
|
|
import { useEffect, useState } from 'preact/hooks';
|
|
import { getMetaStore, getSignalStore } from './lib/stores';
|
|
import type { SignalRef, SignalMeta, SignalValue } from './lib/types';
|
|
|
|
interface Props {
|
|
signal: SignalRef;
|
|
onClose: () => void;
|
|
}
|
|
|
|
function qualityColor(q: string): string {
|
|
switch (q) {
|
|
case 'good': return '#4ade80';
|
|
case 'uncertain': return '#fbbf24';
|
|
case 'bad': return '#f87171';
|
|
default: return '#6b7280';
|
|
}
|
|
}
|
|
|
|
function fmt(v: any): string {
|
|
if (v === null || v === undefined) return '—';
|
|
if (typeof v === 'number') return Number.isFinite(v) ? v.toPrecision(6).replace(/\.?0+$/, '') : String(v);
|
|
if (Array.isArray(v)) return `[${v.slice(0, 8).map((x: any) => fmt(x)).join(', ')}${v.length > 8 ? ', …' : ''}]`;
|
|
return String(v);
|
|
}
|
|
|
|
function fmtTs(ts: string | null): string {
|
|
if (!ts) return '—';
|
|
try { return new Date(ts).toLocaleString(); } catch { return ts; }
|
|
}
|
|
|
|
export default function InfoPanel({ signal, onClose }: Props) {
|
|
const [meta, setMeta] = useState<SignalMeta | null>(null);
|
|
const [sv, setSv] = useState<SignalValue>({ value: null, quality: 'unknown', ts: null });
|
|
|
|
useEffect(() => {
|
|
const unsubM = getMetaStore(signal).subscribe(setMeta);
|
|
const unsubV = getSignalStore(signal).subscribe(setSv);
|
|
return () => { unsubM(); unsubV(); };
|
|
}, [signal.ds, signal.name]);
|
|
|
|
return (
|
|
<div class="info-panel">
|
|
<div class="info-panel-header">
|
|
<span class="info-panel-title">Signal Info</span>
|
|
<button class="info-panel-close" onClick={onClose} title="Close">✕</button>
|
|
</div>
|
|
<div class="info-panel-body">
|
|
<table class="info-table">
|
|
<tbody>
|
|
<tr><td class="info-key">Datasource</td><td class="info-val info-mono">{signal.ds}</td></tr>
|
|
<tr><td class="info-key">Name</td><td class="info-val info-mono">{signal.name}</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="info-sep" />
|
|
|
|
<table class="info-table">
|
|
<tbody>
|
|
<tr>
|
|
<td class="info-key">Value</td>
|
|
<td class="info-val">
|
|
<span
|
|
class="quality-dot"
|
|
style={`background:${qualityColor(sv.quality)};display:inline-block;margin-right:5px;vertical-align:middle;`}
|
|
/>
|
|
{fmt(sv.value)}{meta?.unit ? ` ${meta.unit}` : ''}
|
|
</td>
|
|
</tr>
|
|
<tr><td class="info-key">Quality</td><td class="info-val">{sv.quality}</td></tr>
|
|
<tr><td class="info-key">Timestamp</td><td class="info-val">{fmtTs(sv.ts)}</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
{meta && (
|
|
<div>
|
|
<div class="info-sep" />
|
|
<table class="info-table">
|
|
<tbody>
|
|
<tr><td class="info-key">Type</td><td class="info-val">{meta.type}</td></tr>
|
|
{meta.unit && <tr><td class="info-key">Unit</td><td class="info-val">{meta.unit}</td></tr>}
|
|
<tr><td class="info-key">Writable</td><td class="info-val">{meta.writable ? 'Yes' : 'No'}</td></tr>
|
|
{(meta.displayLow !== 0 || meta.displayHigh !== 0) && (
|
|
<tr>
|
|
<td class="info-key">Display range</td>
|
|
<td class="info-val">{meta.displayLow} … {meta.displayHigh}</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
|
|
{meta.tags && meta.tags.length > 0 && (
|
|
<div>
|
|
<div class="info-sep" />
|
|
<div class="info-section-hdr">Tags</div>
|
|
<div style="display: flex; gap: 4px; flex-wrap: wrap; padding: 4px 0.75rem;">
|
|
{meta.tags.map(t => (
|
|
<span key={t} style="font-size: 0.7rem; background: #1e293b; color: #94a3b8; padding: 1px 6px; border-radius: 4px; border: 1px solid #334155;">
|
|
{t}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{meta.properties && Object.keys(meta.properties).length > 0 && (
|
|
<div>
|
|
<div class="info-sep" />
|
|
<div class="info-section-hdr">Properties</div>
|
|
<table class="info-table">
|
|
<tbody>
|
|
{Object.entries(meta.properties).map(([k, v]) => (
|
|
<tr key={k}>
|
|
<td class="info-key">{k}</td>
|
|
<td class="info-val">{v}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{meta.enumStrings && meta.enumStrings.length > 0 && (
|
|
<div>
|
|
<div class="info-sep" />
|
|
<div class="info-section-hdr">States</div>
|
|
<table class="info-table">
|
|
<tbody>
|
|
{meta.enumStrings.map((s, i) => (
|
|
<tr key={i}>
|
|
<td class="info-key">{i}</td>
|
|
<td class="info-val info-mono">{s}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{meta.description && (
|
|
<div>
|
|
<div class="info-sep" />
|
|
<div class="info-section-hdr">Description</div>
|
|
<div class="info-desc">{meta.description}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|