Implemented confi snapshots

This commit is contained in:
Martino Ferrari
2026-06-21 23:50:03 +02:00
parent 04d31a15c4
commit 113e5a0fe8
51 changed files with 4921 additions and 241 deletions
+80
View File
@@ -0,0 +1,80 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { wsClient } from '../lib/ws';
import { getSignalStore } from '../lib/stores';
import type { Widget, SignalValue } from '../lib/types';
interface InstanceMeta { id: string; name: string; setId?: string; }
interface Props { widget: Widget; onContextMenu?: (e: MouseEvent) => void; }
// A combo that lists the config instances of a chosen set and writes the
// selected instance id (a string) to a panel-local variable. The output var can
// then drive the apply/read/write config logic nodes (set to "From variable"),
// letting an operator pick which configuration the flow operates on at run time.
export default function ConfigSelect({ widget, onContextMenu }: Props) {
const setId = widget.options['set'] || '';
const output = widget.options['output'] || '';
const label = widget.options['label'] || 'Config';
// Optional comma-separated allow-list of instance ids; empty shows them all.
const subset = (widget.options['instances'] || '')
.split(',').map(s => s.trim()).filter(Boolean);
const [instances, setInstances] = useState<InstanceMeta[]>([]);
const [selected, setSelected] = useState('');
// Mirror the output var so external writers (e.g. a create-config node) keep
// the combo in sync.
useEffect(() => {
if (!output) return;
const unsub = getSignalStore({ ds: 'local', name: output }).subscribe((v: SignalValue) => {
const id = v.value == null ? '' : String(v.value);
setSelected(prev => (id && id !== prev ? id : prev));
});
return unsub;
}, [output]);
useEffect(() => {
let cancelled = false;
fetch('/api/v1/config/instances')
.then(r => r.ok ? r.json() : [])
.then((list: InstanceMeta[]) => {
if (cancelled) return;
let filtered = (list ?? []).filter(ci => !setId || ci.setId === setId);
if (subset.length > 0) filtered = filtered.filter(ci => subset.includes(ci.id));
setInstances(filtered);
// Default the output to the first instance when nothing is chosen yet.
setSelected(prev => {
if (prev && filtered.some(ci => ci.id === prev)) return prev;
const first = filtered[0]?.id ?? '';
if (first && output) wsClient.write({ ds: 'local', name: output }, first);
return first;
});
})
.catch(() => {});
return () => { cancelled = true; };
}, [setId, widget.options['instances']]);
function onChange(id: string) {
setSelected(id);
if (output) wsClient.write({ ds: 'local', name: output }, id);
}
return (
<div
class="configselect"
style={`left:${widget.x}px;top:${widget.y}px;width:${widget.w}px;height:${widget.h}px;`}
onContextMenu={onContextMenu}
>
<span class="cs-label">{label}:</span>
<select
class="cs-select"
value={selected}
onChange={(e: Event) => onChange((e.target as HTMLSelectElement).value)}
>
{instances.length === 0 && <option value="">no instances</option>}
{instances.map(ci => <option key={ci.id} value={ci.id}>{ci.name}</option>)}
</select>
</div>
);
}