Wworking on improving the tool
This commit is contained in:
+260
-209
@@ -1,11 +1,13 @@
|
||||
import { h, Fragment } from 'preact';
|
||||
import { useState, useEffect, useRef } from 'preact/hooks';
|
||||
import { useState, useEffect, useRef, useMemo } from 'preact/hooks';
|
||||
import type { SignalRef } from './lib/types';
|
||||
import SyntheticWizard from './SyntheticWizard';
|
||||
import SyntheticEditor from './SyntheticEditor';
|
||||
import GroupsTree from './GroupsTree';
|
||||
import ChannelFinderModal from './ChannelFinderModal';
|
||||
|
||||
type TreeTab = 'sources' | 'groups';
|
||||
type GroupBy = 'datasource' | 'area' | 'system' | 'ioc';
|
||||
|
||||
interface SignalInfo {
|
||||
name: string;
|
||||
@@ -13,14 +15,17 @@ interface SignalInfo {
|
||||
unit?: string;
|
||||
description?: string;
|
||||
writable?: boolean;
|
||||
properties?: Record<string, string>;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
interface DsGroup {
|
||||
ds: string;
|
||||
signals: SignalInfo[];
|
||||
interface DisplayGroup {
|
||||
id: string;
|
||||
label: string;
|
||||
signals: Array<SignalInfo & { ds: string }>;
|
||||
expanded: boolean;
|
||||
addingSignal: boolean; // show add-PV input
|
||||
addValue: string;
|
||||
addingSignal?: boolean;
|
||||
addValue?: string;
|
||||
}
|
||||
|
||||
// Custom signals persisted in localStorage
|
||||
@@ -45,35 +50,44 @@ interface Props {
|
||||
|
||||
export default function SignalTree({ onDragStart, width }: Props) {
|
||||
const [treeTab, setTreeTab] = useState<TreeTab>('sources');
|
||||
const [groupBy, setGroupBy] = useState<GroupBy>('datasource');
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [groups, setGroups] = useState<DsGroup[]>([]);
|
||||
const [rawSignals, setRawSignals] = useState<Array<SignalInfo & { ds: string }>>([]);
|
||||
const [customSignals, setCustomSignals] = useState<Array<{ ds: string; name: string }>>(loadCustom);
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||
const [filter, setFilter] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [cfAvailable, setCfAvailable] = useState(false);
|
||||
const [cfSearching, setCfSearching] = useState(false);
|
||||
const [showWizard, setShowWizard] = useState(false);
|
||||
const [showCfModal, setShowCfModal] = useState(false);
|
||||
const [showManualAdd, setShowManualAdd] = useState(false);
|
||||
const [manualDs, setManualDs] = useState('epics');
|
||||
const [manualName, setManualName] = useState('');
|
||||
const [addingTo, setAddingTo] = useState<string | null>(null);
|
||||
const [addValue, setAddValue] = useState('');
|
||||
const [editSynthetic, setEditSynthetic] = useState<string | null>(null);
|
||||
const csvRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
async function loadGroups() {
|
||||
async function loadAllSignals() {
|
||||
setLoading(true);
|
||||
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(
|
||||
|
||||
const all: Array<SignalInfo & { ds: string }> = [];
|
||||
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, addingSignal: false, addValue: '' };
|
||||
} catch {
|
||||
return { ds: name, signals: [], expanded: true, addingSignal: false, addValue: '' };
|
||||
}
|
||||
if (r.ok) {
|
||||
const sigs: SignalInfo[] = await r.json();
|
||||
all.push(...sigs.map(s => ({ ...s, ds: name })));
|
||||
}
|
||||
} catch (e) { console.error(`Failed to load ${name}:`, e); }
|
||||
})
|
||||
);
|
||||
setGroups(loaded);
|
||||
setRawSignals(all);
|
||||
|
||||
// Check if Channel Finder is available
|
||||
const cf = await fetch('/api/v1/channel-finder?q=').catch(() => null);
|
||||
@@ -85,53 +99,66 @@ export default function SignalTree({ onDragStart, width }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { loadGroups(); }, []);
|
||||
useEffect(() => { loadAllSignals(); }, []);
|
||||
|
||||
// Merge custom signals into groups
|
||||
const allGroups = groups.map(g => {
|
||||
const extra = customSignals.filter(c => c.ds === g.ds);
|
||||
const existing = new Set(g.signals.map(s => s.name));
|
||||
const merged = [
|
||||
...g.signals,
|
||||
...extra.filter(c => !existing.has(c.name)).map(c => ({ name: c.name, type: 'custom' } as SignalInfo)),
|
||||
];
|
||||
return { ...g, signals: merged };
|
||||
});
|
||||
// Merge custom signals that might not be in ListSignals yet
|
||||
const allSignals = useMemo(() => {
|
||||
const existing = new Set(rawSignals.map(s => `${s.ds}\0${s.name}`));
|
||||
const extra = customSignals
|
||||
.filter(c => !existing.has(`${c.ds}\0${c.name}`))
|
||||
.map(c => ({ name: c.name, ds: c.ds, type: 'custom' } as SignalInfo & { ds: string }));
|
||||
return [...rawSignals, ...extra];
|
||||
}, [rawSignals, customSignals]);
|
||||
|
||||
// Custom signals with no matching DS group get their own group
|
||||
const knownDs = new Set(groups.map(g => g.ds));
|
||||
const orphanCustom = customSignals.filter(c => !knownDs.has(c.ds));
|
||||
const orphanGroups: DsGroup[] = Object.entries(
|
||||
orphanCustom.reduce<Record<string, string[]>>((acc, { ds, name }) => {
|
||||
(acc[ds] ??= []).push(name);
|
||||
return acc;
|
||||
}, {})
|
||||
).map(([ds, names]) => ({
|
||||
ds, signals: names.map(name => ({ name, type: 'custom' } as SignalInfo)),
|
||||
expanded: true, addingSignal: false, addValue: '',
|
||||
}));
|
||||
// Group signals based on selected criteria
|
||||
const visibleGroups = useMemo(() => {
|
||||
const lf = filter.toLowerCase();
|
||||
const filtered = allSignals.filter(s =>
|
||||
s.name.toLowerCase().includes(lf) ||
|
||||
(s.description ?? '').toLowerCase().includes(lf)
|
||||
);
|
||||
|
||||
const visibleGroups = [...allGroups, ...orphanGroups];
|
||||
const groups: Record<string, Array<SignalInfo & { ds: string }>> = {};
|
||||
|
||||
function toggleGroup(ds: string) {
|
||||
setGroups(gs => gs.map(g => g.ds === ds ? { ...g, expanded: !g.expanded } : g));
|
||||
filtered.forEach(s => {
|
||||
let key = 'unknown';
|
||||
if (groupBy === 'datasource') {
|
||||
key = s.ds;
|
||||
} else if (groupBy === 'area') {
|
||||
key = s.properties?.area || 'no-area';
|
||||
} else if (groupBy === 'system') {
|
||||
key = s.properties?.system || 'no-system';
|
||||
} else if (groupBy === 'ioc') {
|
||||
key = s.properties?.iocName || 'no-ioc';
|
||||
}
|
||||
(groups[key] ??= []).push(s);
|
||||
});
|
||||
|
||||
return Object.entries(groups).map(([id, signals]) => ({
|
||||
id,
|
||||
label: id,
|
||||
signals: signals.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
expanded: expandedGroups[id] ?? true,
|
||||
})).sort((a, b) => a.label.localeCompare(b.label));
|
||||
}, [allSignals, groupBy, expandedGroups, filter]);
|
||||
|
||||
function toggleGroup(id: string) {
|
||||
setExpandedGroups(prev => ({ ...prev, [id]: !(prev[id] ?? true) }));
|
||||
}
|
||||
|
||||
function setAdding(ds: string, val: boolean) {
|
||||
setGroups(gs => gs.map(g => g.ds === ds ? { ...g, addingSignal: val, addValue: '' } : g));
|
||||
function startAdding(ds: string) {
|
||||
setAddingTo(ds);
|
||||
setAddValue('');
|
||||
setExpandedGroups(prev => ({ ...prev, [ds]: true }));
|
||||
}
|
||||
|
||||
function setAddValue(ds: string, val: string) {
|
||||
setGroups(gs => gs.map(g => g.ds === ds ? { ...g, addValue: val } : g));
|
||||
}
|
||||
|
||||
function commitAdd(ds: string, name: string) {
|
||||
name = name.trim();
|
||||
if (!name) { setAdding(ds, false); return; }
|
||||
const next = [...customSignals, { ds, name }];
|
||||
function commitAddManually() {
|
||||
const name = addValue.trim();
|
||||
if (!name || !addingTo) { setAddingTo(null); return; }
|
||||
const next = [...customSignals, { ds: addingTo, name }];
|
||||
setCustomSignals(next);
|
||||
saveCustom(next);
|
||||
setAdding(ds, false);
|
||||
setAddingTo(null);
|
||||
}
|
||||
|
||||
function removeCustom(ds: string, name: string) {
|
||||
@@ -140,26 +167,21 @@ export default function SignalTree({ onDragStart, width }: Props) {
|
||||
saveCustom(next);
|
||||
}
|
||||
|
||||
// ── Channel Finder search ────────────────────────────────────────────────
|
||||
function handleAddCfSignals(refs: SignalRef[]) {
|
||||
const existing = new Set(customSignals.map(c => `${c.ds}\0${c.name}`));
|
||||
const fresh = refs.filter(r => !existing.has(`${r.ds}\0${r.name}`));
|
||||
if (fresh.length === 0) return;
|
||||
const next = [...customSignals, ...fresh];
|
||||
setCustomSignals(next);
|
||||
saveCustom(next);
|
||||
loadAllSignals(); // Refresh to fetch metadata for new ones
|
||||
}
|
||||
|
||||
async function handleCfSearch() {
|
||||
if (!filter) return;
|
||||
setCfSearching(true);
|
||||
try {
|
||||
const res = await fetch(`/api/v1/channel-finder?q=${encodeURIComponent(filter)}`);
|
||||
if (!res.ok) return;
|
||||
const names: string[] = await res.json();
|
||||
const next = [
|
||||
...customSignals,
|
||||
...names
|
||||
.filter(n => !customSignals.some(c => c.ds === 'epics' && c.name === n))
|
||||
.map(n => ({ ds: 'epics', name: n })),
|
||||
];
|
||||
setCustomSignals(next);
|
||||
saveCustom(next);
|
||||
} finally {
|
||||
setCfSearching(false);
|
||||
}
|
||||
function handleManualAdd() {
|
||||
if (!manualName.trim()) return;
|
||||
handleAddCfSignals([{ ds: manualDs, name: manualName.trim() }]);
|
||||
setManualName('');
|
||||
setShowManualAdd(false);
|
||||
}
|
||||
|
||||
// ── CSV import ────────────────────────────────────────────────────────────
|
||||
@@ -176,39 +198,19 @@ export default function SignalTree({ onDragStart, width }: Props) {
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
const parts = line.split(',').map(p => p.trim());
|
||||
if (parts.length === 1 && parts[0]) {
|
||||
// bare PV name → default to epics
|
||||
added.push({ ds: 'epics', name: parts[0] });
|
||||
} else if (parts.length >= 2 && parts[0] && parts[1]) {
|
||||
added.push({ ds: parts[0], name: parts[1] });
|
||||
}
|
||||
}
|
||||
const existing = new Set(customSignals.map(c => `${c.ds}\0${c.name}`));
|
||||
const fresh = added.filter(a => !existing.has(`${a.ds}\0${a.name}`));
|
||||
const next = [...customSignals, ...fresh];
|
||||
setCustomSignals(next);
|
||||
saveCustom(next);
|
||||
handleAddCfSignals(added);
|
||||
}
|
||||
|
||||
// ── Filter ────────────────────────────────────────────────────────────────
|
||||
|
||||
const lf = filter.toLowerCase();
|
||||
const filtered = visibleGroups
|
||||
.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 || g.addingSignal || !lf);
|
||||
|
||||
function makeDraggable(ds: string, sig: SignalInfo) {
|
||||
function makeDraggable(sig: SignalInfo & { ds: string }) {
|
||||
return {
|
||||
draggable: true as const,
|
||||
onDragStart: (e: DragEvent) => {
|
||||
const ref: SignalRef = { ds, name: sig.name };
|
||||
const ref: SignalRef = { ds: sig.ds, name: sig.name };
|
||||
e.dataTransfer?.setData('application/json', JSON.stringify(ref));
|
||||
onDragStart?.(ref);
|
||||
},
|
||||
@@ -226,7 +228,6 @@ export default function SignalTree({ onDragStart, width }: Props) {
|
||||
|
||||
{!collapsed && (
|
||||
<div class="signal-tree-body">
|
||||
{/* Tab bar: Sources | Groups */}
|
||||
<div class="signal-tree-tabs">
|
||||
<button
|
||||
class={`stab${treeTab === 'sources' ? ' stab-active' : ''}`}
|
||||
@@ -238,132 +239,137 @@ export default function SignalTree({ onDragStart, width }: Props) {
|
||||
>Groups</button>
|
||||
</div>
|
||||
|
||||
{/* Groups tab */}
|
||||
{treeTab === 'groups' && (
|
||||
<GroupsTree onDragStart={onDragStart} />
|
||||
)}
|
||||
|
||||
{/* Sources tab contents */}
|
||||
{treeTab === 'sources' && <>
|
||||
{treeTab === 'sources' && (
|
||||
<Fragment>
|
||||
<div class="signal-tree-search">
|
||||
<div class="signal-search-row">
|
||||
<input
|
||||
class="signal-filter"
|
||||
type="search"
|
||||
placeholder="Filter signals…"
|
||||
value={filter}
|
||||
onInput={(e) => setFilter((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
{cfAvailable && (
|
||||
<button class="icon-btn" title="Advanced Search" onClick={() => setShowCfModal(true)}>
|
||||
🔍
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search / filter bar */}
|
||||
<div class="signal-tree-search">
|
||||
<div class="signal-search-row">
|
||||
<input
|
||||
class="signal-filter"
|
||||
type="search"
|
||||
placeholder="Filter signals…"
|
||||
value={filter}
|
||||
onInput={(e) => setFilter((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
{cfAvailable && filter && (
|
||||
<button class="icon-btn" title="Search Channel Finder" onClick={handleCfSearch} disabled={cfSearching}>
|
||||
{cfSearching ? '…' : '🔍'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="signal-tree-toolbar" style="gap: 4px;">
|
||||
<select
|
||||
class="prop-select"
|
||||
style="font-size: 0.7rem; height: 1.6rem; padding: 0 4px; flex: 1;"
|
||||
value={groupBy}
|
||||
onChange={e => setGroupBy((e.target as HTMLSelectElement).value as any)}
|
||||
>
|
||||
<option value="datasource">By Source</option>
|
||||
<option value="area">By Area</option>
|
||||
<option value="system">By System</option>
|
||||
<option value="ioc">By IOC</option>
|
||||
</select>
|
||||
<button class="panel-btn icon-only" title="Add Signal Manually" onClick={() => setShowManualAdd(true)}>+</button>
|
||||
<button class="panel-btn icon-only" title="Create synthetic signal" onClick={() => setShowWizard(true)}>Σ</button>
|
||||
<button class="panel-btn icon-only" title="Import CSV" onClick={() => csvRef.current?.click()}>CSV</button>
|
||||
<button class="panel-btn icon-only" title="Refresh signal list" onClick={loadAllSignals}>↺</button>
|
||||
<input ref={csvRef} type="file" accept=".csv,text/csv" style="display:none" onChange={handleCsvImport} />
|
||||
</div>
|
||||
|
||||
{/* Toolbar: New Synthetic | Import CSV | Refresh */}
|
||||
<div class="signal-tree-toolbar">
|
||||
<button class="panel-btn" title="Create synthetic signal" onClick={() => setShowWizard(true)}>+ Synthetic</button>
|
||||
<button class="panel-btn" title="Import CSV (ds,name or bare PV names)" onClick={() => csvRef.current?.click()}>CSV</button>
|
||||
<button class="panel-btn" title="Refresh signal list" onClick={loadGroups}>↺</button>
|
||||
<input ref={csvRef} type="file" accept=".csv,text/csv" style="display:none" onChange={handleCsvImport} />
|
||||
</div>
|
||||
|
||||
{/* Signal groups */}
|
||||
<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 => {
|
||||
const groupInGroups = groups.find(g => g.ds === group.ds);
|
||||
return (
|
||||
<div key={group.ds} class="signal-group">
|
||||
<div class="signal-group-header">
|
||||
<span class="signal-group-arrow" onClick={() => toggleGroup(group.ds)}>
|
||||
{group.expanded ? '▾' : '▸'}
|
||||
</span>
|
||||
<span class="signal-group-name" onClick={() => toggleGroup(group.ds)}>
|
||||
{group.ds}
|
||||
</span>
|
||||
<span class="signal-group-count">{group.signals.length}</span>
|
||||
<button
|
||||
class="icon-btn signal-add-btn"
|
||||
title={`Add custom signal to ${group.ds}`}
|
||||
onClick={() => setAdding(group.ds, true)}
|
||||
>+</button>
|
||||
</div>
|
||||
|
||||
{group.expanded && (
|
||||
<div>
|
||||
{group.signals.map(sig => {
|
||||
const isCustom = !groups.find(g => g.ds === group.ds)?.signals.some(s => s.name === sig.name);
|
||||
return (
|
||||
<div
|
||||
key={sig.name}
|
||||
class={`signal-item${isCustom ? ' signal-custom' : ''}`}
|
||||
title={`${group.ds}:${sig.name}${sig.unit ? ` [${sig.unit}]` : ''}${sig.description ? ` — ${sig.description}` : ''}`}
|
||||
{...makeDraggable(group.ds, sig)}
|
||||
>
|
||||
<span class="signal-name">{sig.name}</span>
|
||||
{sig.unit && <span class="signal-unit">{sig.unit}</span>}
|
||||
{group.ds === 'synthetic' && (
|
||||
<button
|
||||
class="icon-btn signal-edit-btn"
|
||||
title="Edit pipeline"
|
||||
onMouseDown={(e: MouseEvent) => e.stopPropagation()}
|
||||
onClick={(e: MouseEvent) => { e.stopPropagation(); setEditSynthetic(sig.name); }}
|
||||
>✎</button>
|
||||
)}
|
||||
{isCustom && (
|
||||
<button
|
||||
class="icon-btn signal-remove-btn"
|
||||
title="Remove custom signal"
|
||||
onMouseDown={(e: MouseEvent) => e.stopPropagation()}
|
||||
onClick={(e: MouseEvent) => { e.stopPropagation(); removeCustom(group.ds, sig.name); }}
|
||||
>✕</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Add-signal input row */}
|
||||
{(groupInGroups?.addingSignal) && (
|
||||
<div class="signal-add-row">
|
||||
<input
|
||||
class="signal-add-input"
|
||||
placeholder="Signal / PV name…"
|
||||
autoFocus
|
||||
value={groupInGroups?.addValue ?? ''}
|
||||
onInput={(e) => setAddValue(group.ds, (e.target as HTMLInputElement).value)}
|
||||
onKeyDown={(e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') commitAdd(group.ds, (e.target as HTMLInputElement).value);
|
||||
if (e.key === 'Escape') setAdding(group.ds, false);
|
||||
}}
|
||||
/>
|
||||
<button class="icon-btn" onClick={() => commitAdd(group.ds, groupInGroups?.addValue ?? '')}>✓</button>
|
||||
<button class="icon-btn" onClick={() => setAdding(group.ds, false)}>✕</button>
|
||||
</div>
|
||||
<div class="panel-list signal-list">
|
||||
{loading ? (
|
||||
<p class="hint">Loading signals…</p>
|
||||
) : visibleGroups.length === 0 ? (
|
||||
<p class="hint">No signals found.</p>
|
||||
) : (
|
||||
visibleGroups.map(group => (
|
||||
<div key={group.id} class="signal-group">
|
||||
<div class="signal-group-header">
|
||||
<span class="signal-group-arrow" onClick={() => toggleGroup(group.id)}>
|
||||
{group.expanded ? '▾' : '▸'}
|
||||
</span>
|
||||
<span class="signal-group-name" title={group.label} onClick={() => toggleGroup(group.id)}>
|
||||
{group.label}
|
||||
</span>
|
||||
<span class="signal-group-count">{group.signals.length}</span>
|
||||
{groupBy === 'datasource' && group.id !== 'synthetic' && (
|
||||
<button
|
||||
class="icon-btn signal-add-btn"
|
||||
title={`Add custom signal to ${group.id}`}
|
||||
onClick={(e) => { e.stopPropagation(); startAdding(group.id); }}
|
||||
>+</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</>}
|
||||
|
||||
{group.expanded && (
|
||||
<div>
|
||||
{addingTo === group.id && (
|
||||
<div class="signal-add-row">
|
||||
<input
|
||||
class="signal-add-input"
|
||||
placeholder="Signal / PV name…"
|
||||
autoFocus
|
||||
value={addValue}
|
||||
onInput={(e) => setAddValue((e.target as HTMLInputElement).value)}
|
||||
onKeyDown={(e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') commitAddManually();
|
||||
if (e.key === 'Escape') setAddingTo(null);
|
||||
}}
|
||||
/>
|
||||
<button class="icon-btn" onClick={commitAddManually}>✓</button>
|
||||
<button class="icon-btn" onClick={() => setAddingTo(null)}>✕</button>
|
||||
</div>
|
||||
)}
|
||||
{group.signals.map(sig => {
|
||||
const isCustom = sig.type === 'custom' || !rawSignals.some(rs => rs.ds === sig.ds && rs.name === sig.name);
|
||||
return (
|
||||
<div
|
||||
key={`${sig.ds}:${sig.name}`}
|
||||
class={`signal-item${isCustom ? ' signal-custom' : ''}`}
|
||||
title={`${sig.ds}:${sig.name}${sig.unit ? ` [${sig.unit}]` : ''}${sig.description ? ` — ${sig.description}` : ''}`}
|
||||
{...makeDraggable(sig)}
|
||||
>
|
||||
<span class="signal-name">{sig.name}</span>
|
||||
{sig.unit && <span class="signal-unit">{sig.unit}</span>}
|
||||
{sig.ds === 'synthetic' && (
|
||||
<button
|
||||
class="icon-btn signal-edit-btn"
|
||||
title="Edit pipeline"
|
||||
onMouseDown={(e: MouseEvent) => e.stopPropagation()}
|
||||
onClick={(e: MouseEvent) => { e.stopPropagation(); setEditSynthetic(sig.name); }}
|
||||
>✎</button>
|
||||
)}
|
||||
{isCustom && (
|
||||
<button
|
||||
class="icon-btn signal-remove-btn"
|
||||
title="Remove custom signal"
|
||||
onMouseDown={(e: MouseEvent) => e.stopPropagation()}
|
||||
onClick={(e: MouseEvent) => { e.stopPropagation(); removeCustom(sig.ds, sig.name); }}
|
||||
>✕</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showWizard && (
|
||||
<SyntheticWizard
|
||||
onClose={() => setShowWizard(false)}
|
||||
onCreated={loadGroups}
|
||||
onCreated={loadAllSignals}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -371,9 +377,54 @@ export default function SignalTree({ onDragStart, width }: Props) {
|
||||
<SyntheticEditor
|
||||
name={editSynthetic}
|
||||
onClose={() => setEditSynthetic(null)}
|
||||
onSaved={loadGroups}
|
||||
onSaved={loadAllSignals}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCfModal && (
|
||||
<ChannelFinderModal
|
||||
cfURL="" // fetched by backend API
|
||||
onClose={() => setShowCfModal(false)}
|
||||
onAddSignals={handleAddCfSignals}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showManualAdd && (
|
||||
<div class="wizard-backdrop" onClick={() => setShowManualAdd(false)}>
|
||||
<div class="wizard" onClick={e => e.stopPropagation()} style="max-width: 400px;">
|
||||
<div class="wizard-header">
|
||||
<span>Add Signal Manually</span>
|
||||
<button class="icon-btn" onClick={() => setShowManualAdd(false)}>✕</button>
|
||||
</div>
|
||||
<div class="wizard-body">
|
||||
<div class="wizard-field">
|
||||
<label>Data Source</label>
|
||||
<select class="prop-select" value={manualDs} onChange={e => setManualDs((e.target as HTMLSelectElement).value)}>
|
||||
{Array.from(new Set(allSignals.map(s => s.ds))).map(ds => (
|
||||
<option key={ds} value={ds}>{ds}</option>
|
||||
))}
|
||||
{!allSignals.some(s => s.ds === 'epics') && <option value="epics">epics</option>}
|
||||
</select>
|
||||
</div>
|
||||
<div class="wizard-field">
|
||||
<label>Signal / PV Name</label>
|
||||
<input
|
||||
class="prop-input"
|
||||
autoFocus
|
||||
placeholder="e.g. MY:PV:NAME"
|
||||
value={manualName}
|
||||
onInput={e => setManualName((e.target as HTMLInputElement).value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleManualAdd()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-footer">
|
||||
<button class="toolbar-btn" onClick={() => setShowManualAdd(false)}>Cancel</button>
|
||||
<button class="toolbar-btn toolbar-btn-primary" onClick={handleManualAdd}>Add Signal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user