Major changes: logic add to panel, local variables, panel histor, users management...
This commit is contained in:
+132
-7
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user