This commit is contained in:
Martino Ferrari
2026-04-25 22:56:09 +02:00
parent 8b548ba1c2
commit 986f6cd6d8
85 changed files with 11479 additions and 5050 deletions
+127
View File
@@ -0,0 +1,127 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import type { SignalRef } from './lib/types';
interface SignalInfo {
name: string;
type: string;
unit?: string;
description?: string;
writable: boolean;
}
interface DsGroup {
ds: string;
signals: SignalInfo[];
expanded: boolean;
}
interface Props {
onDragStart?: (sig: SignalRef) => void;
}
export default function SignalTree({ onDragStart }: Props) {
const [collapsed, setCollapsed] = useState(false);
const [groups, setGroups] = useState<DsGroup[]>([]);
const [filter, setFilter] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
async function load() {
try {
const res = await fetch('/api/v1/datasources');
if (!res.ok) return;
const dsList: { name: string }[] = await res.json();
const loaded: DsGroup[] = await Promise.all(
dsList.map(async ({ name }) => {
try {
const r = await fetch(`/api/v1/signals?ds=${encodeURIComponent(name)}`);
const sigs: SignalInfo[] = r.ok ? await r.json() : [];
return { ds: name, signals: sigs, expanded: true };
} catch {
return { ds: name, signals: [], expanded: true };
}
})
);
setGroups(loaded);
} catch (e) {
console.error('Failed to load signals:', e);
} finally {
setLoading(false);
}
}
load();
}, []);
function toggleGroup(ds: string) {
setGroups(gs => gs.map(g => g.ds === ds ? { ...g, expanded: !g.expanded } : g));
}
const lf = filter.toLowerCase();
const filtered = groups.map(g => ({
...g,
signals: lf ? g.signals.filter(s => s.name.toLowerCase().includes(lf) || (s.description ?? '').toLowerCase().includes(lf)) : g.signals,
})).filter(g => g.signals.length > 0 || !lf);
return (
<aside class={`panel signal-tree-panel${collapsed ? ' collapsed' : ''}`}>
<div class="panel-header">
{!collapsed && <span class="panel-title">Signals</span>}
<button
class="icon-btn"
onClick={() => setCollapsed(c => !c)}
title={collapsed ? 'Expand' : 'Collapse'}
>
{collapsed ? '▶' : '◀'}
</button>
</div>
{!collapsed && (
<div class="signal-tree-body">
<div class="signal-tree-search">
<input
class="signal-filter"
type="search"
placeholder="Filter signals…"
value={filter}
onInput={(e) => setFilter((e.target as HTMLInputElement).value)}
/>
</div>
<div class="panel-list signal-list">
{loading ? (
<p class="hint">Loading signals</p>
) : filtered.length === 0 ? (
<p class="hint">No signals found.</p>
) : (
filtered.map(group => (
<div key={group.ds} class="signal-group">
<div class="signal-group-header" onClick={() => toggleGroup(group.ds)}>
<span class="signal-group-arrow">{group.expanded ? '▾' : '▸'}</span>
<span class="signal-group-name">{group.ds}</span>
<span class="signal-group-count">{group.signals.length}</span>
</div>
{group.expanded && group.signals.map(sig => (
<div
key={sig.name}
class="signal-item"
draggable
title={`${group.ds}:${sig.name}${sig.unit ? ` [${sig.unit}]` : ''}${sig.description ? `${sig.description}` : ''}`}
onDragStart={(e: DragEvent) => {
const ref: SignalRef = { ds: group.ds, name: sig.name };
e.dataTransfer?.setData('application/json', JSON.stringify(ref));
onDragStart?.(ref);
}}
>
<span class="signal-name">{sig.name}</span>
{sig.unit && <span class="signal-unit">{sig.unit}</span>}
</div>
))}
</div>
))
)}
</div>
</div>
)}
</aside>
);
}