Phase 6
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user