import { h } from 'preact'; import { useState, useMemo, useEffect } from 'preact/hooks'; import InterfaceList from './InterfaceList'; import Canvas from './Canvas'; import { wsClient } from './lib/ws'; import { parseInterface } from './lib/xml'; import { newPlotPanel } from './lib/templates'; import { useAuth, canWrite } from './lib/auth'; import type { Interface } from './lib/types'; import HelpModal from './HelpModal'; import ZoomControl from './ZoomControl'; import ControlLogicEditor from './ControlLogicEditor'; import AuditViewer from './AuditViewer'; import ConfigManager from './ConfigManager'; interface Props { onEdit?: (iface?: Interface) => void; initialInterface?: Interface | null; /** Notifies the parent which interface is currently shown (for edit return). */ onView?: (iface: Interface | null) => void; } /** Format a Date as a datetime-local input value (YYYY-MM-DDTHH:MM) */ function toLocalInput(d: Date): string { const pad = (n: number) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; } export default function ViewMode({ onEdit, initialInterface, onView }: Props) { const [currentInterface, setCurrentInterface] = useState(initialInterface ?? null); const [parseError, setParseError] = useState(null); const [wsStatus, setWsStatus] = useState('connecting'); const [showHelp, setShowHelp] = useState(false); const [helpSection, setHelpSection] = useState('start'); const [showTimeNav, setShowTimeNav] = useState(false); const [showControlLogic, setShowControlLogic] = useState(false); const [showAudit, setShowAudit] = useState(false); const [showConfig, setShowConfig] = useState(false); const [leftW, setLeftW] = useState(220); const [listCollapsed, setListCollapsed] = useState(false); const me = useAuth(); const writable = canWrite(me.level); function startResize(e: MouseEvent) { e.preventDefault(); const startX = e.clientX; const startW = leftW; function onMove(mv: MouseEvent) { setLeftW(Math.max(140, startW + mv.clientX - startX)); } function onUp() { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); } window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); } function openHelp(section = 'start') { setHelpSection(section); setShowHelp(true); } // Historical time range — null means live const now = useMemo(() => new Date(), []); const [startInput, setStartInput] = useState(() => toLocalInput(new Date(now.getTime() - 3600_000))); const [endInput, setEndInput] = useState(() => toLocalInput(now)); const [timeRange, setTimeRange] = useState<{ start: string; end: string } | null>(null); useEffect(() => { const unsub = wsClient.status.subscribe(setWsStatus); return unsub; }, []); // When returning from edit mode, show the edited interface useEffect(() => { if (initialInterface) setCurrentInterface(initialInterface); }, [initialInterface]); // Auto-hide the interface-selection pane whenever a panel is opened, and keep // the parent informed of what's shown (so closing the editor can return here). useEffect(() => { if (currentInterface) setListCollapsed(true); onView?.(currentInterface); }, [currentInterface]); function handleLoad(xml: string) { try { setParseError(null); setCurrentInterface(parseInterface(xml)); } catch (err) { setParseError(err instanceof Error ? err.message : 'Failed to parse interface file.'); } } async function handleNavigate(interfaceId: string) { try { const res = await fetch(`/api/v1/interfaces/${encodeURIComponent(interfaceId)}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); handleLoad(await res.text()); } catch (err) { setParseError(`Failed to load interface "${interfaceId}": ${err instanceof Error ? err.message : err}`); } } /** Load interface by ID and pass parsed object to onEdit */ async function handleEditById(id: string) { try { const res = await fetch(`/api/v1/interfaces/${encodeURIComponent(id)}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const iface = parseInterface(await res.text()); onEdit?.(iface); } catch (err) { setParseError(`Failed to load interface for editing: ${err instanceof Error ? err.message : err}`); } } function handleLoadHistory() { if (!startInput || !endInput) return; const start = new Date(startInput).toISOString(); const end = new Date(endInput).toISOString(); setTimeRange({ start, end }); } function handleGoLive() { setTimeRange(null); } const isLive = timeRange === null; return (
uopi {currentInterface && ( {currentInterface.name} )}
{parseError && ( Parse error — check console )}
{wsStatus === 'connected' && me.user ? `connected as ${me.user}` : wsStatus} {me.user && !writable && ' (read-only)'}
{writable && me.canEditLogic && ( )} {me.canViewAudit && ( )} {writable && ( )} {writable && ( )}
{/* Historical time range bar — hidden by default */} {showTimeNav && (
setStartInput((e.target as HTMLInputElement).value)} title="History start" /> setEndInput((e.target as HTMLInputElement).value)} title="History end" />
)}
onEdit?.(newPlotPanel())} onEditId={handleEditById} width={leftW} collapsed={listCollapsed} onToggleCollapse={() => setListCollapsed(c => !c)} onSelect={async (id) => { try { const res = await fetch(`/api/v1/interfaces/${encodeURIComponent(id)}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); handleLoad(await res.text()); } catch (err) { setParseError(`Failed to load interface "${id}": ${err instanceof Error ? err.message : err}`); } }} />
{showHelp && ( setShowHelp(false)} /> )} {showControlLogic && ( setShowControlLogic(false)} /> )} {showAudit && ( setShowAudit(false)} /> )} {showConfig && ( setShowConfig(false)} /> )}
); }