Major changes: logic add to panel, local variables, panel histor, users management...

This commit is contained in:
Martino Ferrari
2026-06-18 17:37:04 +02:00
parent 71430bc3b0
commit aba394b84d
54 changed files with 6104 additions and 1166 deletions
+132 -7
View File
@@ -1,6 +1,6 @@
import { h, Fragment } from 'preact';
import { useState, useEffect, useRef, useMemo } from 'preact/hooks';
import type { SignalRef } from './lib/types';
import type { SignalRef, StateVar } from './lib/types';
import SyntheticWizard from './SyntheticWizard';
import SyntheticEditor from './SyntheticEditor';
import GroupsTree from './GroupsTree';
@@ -46,9 +46,14 @@ function saveCustom(signals: Array<{ ds: string; name: string }>) {
interface Props {
onDragStart?: (sig: SignalRef) => void;
width?: number;
// Current interface id — scopes panel-visibility synthetic signals.
panelId?: string;
// Panel-local state variables and a setter (edit mode only).
statevars?: StateVar[];
onStateVarsChange?: (vars: StateVar[]) => void;
}
export default function SignalTree({ onDragStart, width }: Props) {
export default function SignalTree({ onDragStart, width, panelId, statevars, onStateVarsChange }: Props) {
const [treeTab, setTreeTab] = useState<TreeTab>('sources');
const [groupBy, setGroupBy] = useState<GroupBy>('datasource');
const [collapsed, setCollapsed] = useState(false);
@@ -79,9 +84,14 @@ export default function SignalTree({ onDragStart, width }: Props) {
await Promise.all(
dsList.map(async ({ name }) => {
try {
const r = await fetch(`/api/v1/signals?ds=${encodeURIComponent(name)}`);
// Synthetic signals are filtered server-side by panel scope.
const url = name === 'synthetic' && panelId
? `/api/v1/signals?ds=synthetic&panel=${encodeURIComponent(panelId)}`
: `/api/v1/signals?ds=${encodeURIComponent(name)}`;
const r = await fetch(url);
if (r.ok) {
const sigs: SignalInfo[] = await r.json();
const data = await r.json();
const sigs = Array.isArray(data) ? data : (data && typeof data === 'object' && Array.isArray(data.signals) ? data.signals : []);
all.push(...sigs.map(s => ({ ...s, ds: name })));
}
} catch (e) { console.error(`Failed to load ${name}:`, e); }
@@ -90,8 +100,10 @@ export default function SignalTree({ onDragStart, width }: Props) {
setRawSignals(all);
// Check if Channel Finder is available
const cf = await fetch('/api/v1/channel-finder?q=').catch(() => null);
setCfAvailable(cf?.status === 200);
const cf = await fetch('/api/v1/channel-finder?q=')
.then(r => (r.ok ? r.json() : null))
.catch(() => null);
setCfAvailable(!!cf?.available);
} catch (e) {
console.error('Failed to load signals:', e);
} finally {
@@ -99,7 +111,41 @@ export default function SignalTree({ onDragStart, width }: Props) {
}
}
useEffect(() => { loadAllSignals(); }, []);
useEffect(() => { loadAllSignals(); }, [panelId]);
// ── Local state variables (edit mode) ──────────────────────────────────────
const [showAddLocal, setShowAddLocal] = useState(false);
const [localOpen, setLocalOpen] = useState(true);
const [lvName, setLvName] = useState('');
const [lvType, setLvType] = useState<'number' | 'bool' | 'string'>('number');
const [lvInitial, setLvInitial] = useState('0');
function addLocal() {
const name = lvName.trim();
if (!name || !onStateVarsChange) return;
const next = (statevars ?? []).filter(v => v.name !== name);
next.push({ name, type: lvType, initial: lvInitial });
onStateVarsChange(next);
setLvName('');
setLvInitial('0');
setShowAddLocal(false);
}
function removeLocal(name: string) {
if (!onStateVarsChange) return;
onStateVarsChange((statevars ?? []).filter(v => v.name !== name));
}
function makeLocalDraggable(name: string) {
return {
draggable: true as const,
onDragStart: (e: DragEvent) => {
const ref: SignalRef = { ds: 'local', name };
e.dataTransfer?.setData('application/json', JSON.stringify(ref));
onDragStart?.(ref);
},
};
}
// Merge custom signals that might not be in ListSignals yet
const allSignals = useMemo(() => {
@@ -243,6 +289,84 @@ export default function SignalTree({ onDragStart, width }: Props) {
<GroupsTree onDragStart={onDragStart} />
)}
{treeTab === 'sources' && onStateVarsChange && (
<div class="signal-group local-state-group">
<div class="signal-group-header">
<span class="signal-group-arrow" onClick={() => setLocalOpen(o => !o)}>
{localOpen ? '▾' : '▸'}
</span>
<span class="signal-group-name" title="Panel-local state variables" onClick={() => setLocalOpen(o => !o)}>
Local State
</span>
<span class="signal-group-count">{(statevars ?? []).length}</span>
<button
class="icon-btn signal-add-btn"
title="Add local state variable"
onClick={(e) => { e.stopPropagation(); setShowAddLocal(s => !s); setLocalOpen(true); }}
>+</button>
</div>
{localOpen && (
<div>
{showAddLocal && (
<div class="signal-add-row" style="flex-wrap: wrap; gap: 4px;">
<input
class="signal-add-input"
placeholder="Variable name…"
autoFocus
value={lvName}
onInput={(e) => setLvName((e.target as HTMLInputElement).value)}
onKeyDown={(e: KeyboardEvent) => {
if (e.key === 'Enter') addLocal();
if (e.key === 'Escape') setShowAddLocal(false);
}}
/>
<select
class="prop-select"
style="font-size: 0.7rem; height: 1.6rem; padding: 0 4px;"
value={lvType}
onChange={(e) => setLvType((e.target as HTMLSelectElement).value as any)}
>
<option value="number">number</option>
<option value="bool">bool</option>
<option value="string">string</option>
</select>
<input
class="signal-add-input"
style="flex: 1;"
placeholder="initial"
value={lvInitial}
onInput={(e) => setLvInitial((e.target as HTMLInputElement).value)}
onKeyDown={(e: KeyboardEvent) => { if (e.key === 'Enter') addLocal(); }}
/>
<button class="icon-btn" title="Add" onClick={addLocal}></button>
<button class="icon-btn" title="Cancel" onClick={() => setShowAddLocal(false)}></button>
</div>
)}
{(statevars ?? []).length === 0 && !showAddLocal && (
<p class="hint" style="padding: 0.25rem 0.5rem;">No local variables.</p>
)}
{(statevars ?? []).map(v => (
<div
key={v.name}
class="signal-item signal-custom"
title={`local:${v.name} (${v.type ?? 'number'}, initial ${v.initial})`}
{...makeLocalDraggable(v.name)}
>
<span class="signal-name">{v.name}</span>
<span class="signal-unit">{v.type ?? 'number'}</span>
<button
class="icon-btn signal-remove-btn"
title="Remove local variable"
onMouseDown={(e: MouseEvent) => e.stopPropagation()}
onClick={(e: MouseEvent) => { e.stopPropagation(); removeLocal(v.name); }}
></button>
</div>
))}
</div>
)}
</div>
)}
{treeTab === 'sources' && (
<Fragment>
<div class="signal-tree-search">
@@ -368,6 +492,7 @@ export default function SignalTree({ onDragStart, width }: Props) {
{showWizard && (
<SyntheticWizard
currentIfaceId={panelId}
onClose={() => setShowWizard(false)}
onCreated={loadAllSignals}
/>