Major changes: logic add to panel, local variables, panel histor, users management...
This commit is contained in:
+35
-62
@@ -2,11 +2,11 @@ import { h } from 'preact';
|
||||
import { useState, useMemo, useEffect } from 'preact/hooks';
|
||||
import InterfaceList from './InterfaceList';
|
||||
import Canvas from './Canvas';
|
||||
import PlotPanel from './PlotPanel';
|
||||
import type { PlotSignal, PlotSignalStyle } from './PlotPanel';
|
||||
import { wsClient } from './lib/ws';
|
||||
import { parseInterface } from './lib/xml';
|
||||
import type { Interface, SignalRef } from './lib/types';
|
||||
import { newPlotPanel } from './lib/templates';
|
||||
import { useAuth, canWrite } from './lib/auth';
|
||||
import type { Interface } from './lib/types';
|
||||
import ContextualHelp from './ContextualHelp';
|
||||
import HelpModal from './HelpModal';
|
||||
import ZoomControl from './ZoomControl';
|
||||
@@ -14,6 +14,8 @@ import ZoomControl from './ZoomControl';
|
||||
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) */
|
||||
@@ -22,20 +24,17 @@ function toLocalInput(d: Date): string {
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
const PLOT_COLORS = ['#4a9eff', '#22c55e', '#f59e0b', '#ef4444', '#a78bfa', '#f472b6', '#34d399', '#fb923c'];
|
||||
|
||||
type ViewTab = 'hmi' | 'plot';
|
||||
|
||||
export default function ViewMode({ onEdit, initialInterface }: Props) {
|
||||
export default function ViewMode({ onEdit, initialInterface, onView }: Props) {
|
||||
const [currentInterface, setCurrentInterface] = useState<Interface | null>(initialInterface ?? null);
|
||||
const [parseError, setParseError] = useState<string | null>(null);
|
||||
const [wsStatus, setWsStatus] = useState('connecting');
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const [helpSection, setHelpSection] = useState('start');
|
||||
const [showTimeNav, setShowTimeNav] = useState(false);
|
||||
const [viewTab, setViewTab] = useState<ViewTab>('hmi');
|
||||
const [plotSignals, setPlotSignals] = useState<PlotSignal[]>([]);
|
||||
const [leftW, setLeftW] = useState(220);
|
||||
const [listCollapsed, setListCollapsed] = useState(false);
|
||||
const me = useAuth();
|
||||
const writable = canWrite(me.level);
|
||||
|
||||
function startResize(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
@@ -70,6 +69,13 @@ export default function ViewMode({ onEdit, initialInterface }: Props) {
|
||||
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);
|
||||
@@ -112,27 +118,6 @@ export default function ViewMode({ onEdit, initialInterface }: Props) {
|
||||
setTimeRange(null);
|
||||
}
|
||||
|
||||
function addToPlot(ref: SignalRef) {
|
||||
const key = `${ref.ds}\0${ref.name}`;
|
||||
const already = plotSignals.some(s => `${s.ref.ds}\0${s.ref.name}` === key);
|
||||
if (!already) {
|
||||
const color = PLOT_COLORS[plotSignals.length % PLOT_COLORS.length];
|
||||
const style: PlotSignalStyle = { color, lineWidth: 1.5, lineDash: [], markerSize: 5 };
|
||||
setPlotSignals(prev => [...prev, { ref, style }]);
|
||||
}
|
||||
setViewTab('plot');
|
||||
}
|
||||
|
||||
function removeFromPlot(ref: SignalRef) {
|
||||
setPlotSignals(prev => prev.filter(s => !(s.ref.ds === ref.ds && s.ref.name === ref.name)));
|
||||
}
|
||||
|
||||
function updateSignalStyle(ref: SignalRef, style: PlotSignalStyle) {
|
||||
setPlotSignals(prev => prev.map(s =>
|
||||
s.ref.ds === ref.ds && s.ref.name === ref.name ? { ...s, style } : s
|
||||
));
|
||||
}
|
||||
|
||||
const isLive = timeRange === null;
|
||||
|
||||
return (
|
||||
@@ -152,7 +137,7 @@ export default function ViewMode({ onEdit, initialInterface }: Props) {
|
||||
<div class="toolbar-right">
|
||||
<div class={`status-chip ${wsStatus}`}>
|
||||
<span class="status-dot"></span>
|
||||
{wsStatus}
|
||||
{wsStatus === 'connected' && me.user ? `connected as ${me.user}` : wsStatus}
|
||||
</div>
|
||||
<button
|
||||
class={`toolbar-btn${showTimeNav ? ' toolbar-btn-active' : ''}`}
|
||||
@@ -170,13 +155,20 @@ export default function ViewMode({ onEdit, initialInterface }: Props) {
|
||||
📖
|
||||
</button>
|
||||
<ZoomControl />
|
||||
<button
|
||||
class="btn-edit"
|
||||
onClick={() => onEdit?.(currentInterface ?? undefined)}
|
||||
title="Switch to Edit mode"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
{writable && (
|
||||
<button
|
||||
class="btn-edit"
|
||||
onClick={() => onEdit?.(currentInterface ?? undefined)}
|
||||
title="Switch to Edit mode"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
{me.user && (
|
||||
<span class="user-chip" title={`Signed in as ${me.user}${writable ? '' : ' (read-only)'}`}>
|
||||
{me.user}{!writable && ' (read-only)'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -219,14 +211,16 @@ export default function ViewMode({ onEdit, initialInterface }: Props) {
|
||||
<InterfaceList
|
||||
onLoad={handleLoad}
|
||||
onEdit={onEdit}
|
||||
onNewPlot={() => 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());
|
||||
setViewTab('hmi');
|
||||
} catch (err) {
|
||||
setParseError(`Failed to load interface "${id}": ${err instanceof Error ? err.message : err}`);
|
||||
}
|
||||
@@ -235,34 +229,13 @@ export default function ViewMode({ onEdit, initialInterface }: Props) {
|
||||
<div class="panel-resize-handle" onMouseDown={startResize} />
|
||||
|
||||
<div class="view-content-area">
|
||||
{/* Tab bar */}
|
||||
<div class="view-tabs">
|
||||
<button
|
||||
class={`view-tab${viewTab === 'hmi' ? ' view-tab-active' : ''}`}
|
||||
onClick={() => setViewTab('hmi')}
|
||||
>
|
||||
HMI
|
||||
</button>
|
||||
<button
|
||||
class={`view-tab${viewTab === 'plot' ? ' view-tab-active' : ''}`}
|
||||
onClick={() => setViewTab('plot')}
|
||||
>
|
||||
Plot{plotSignals.length > 0 ? ` (${plotSignals.length})` : ''}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Keep both panels mounted so ring-buffer data is never lost on tab switch */}
|
||||
<div style={`display:${viewTab === 'hmi' ? 'contents' : 'none'}`}>
|
||||
<div class="view-panel-container active">
|
||||
<Canvas
|
||||
iface={currentInterface}
|
||||
onNavigate={handleNavigate}
|
||||
timeRange={timeRange}
|
||||
onPlot={addToPlot}
|
||||
/>
|
||||
</div>
|
||||
<div style={`display:${viewTab === 'plot' ? 'contents' : 'none'}`}>
|
||||
<PlotPanel signals={plotSignals} onRemove={removeFromPlot} onStyleChange={updateSignalStyle} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user