1027 lines
51 KiB
TypeScript
1027 lines
51 KiB
TypeScript
import { h } from 'preact';
|
|
import { useState } from 'preact/hooks';
|
|
|
|
interface Props {
|
|
initialSection?: string;
|
|
onClose: () => void;
|
|
}
|
|
|
|
// ── SVG Diagrams ─────────────────────────────────────────────────────────────
|
|
|
|
function DiagramViewMode() {
|
|
return (
|
|
<svg viewBox="0 0 580 230" class="help-diagram" role="img" aria-label="View mode layout diagram">
|
|
{/* outer bg */}
|
|
<rect width="580" height="230" rx="8" fill="#0f1117" />
|
|
|
|
{/* toolbar */}
|
|
<rect width="580" height="38" rx="0" fill="#1a1f2e" />
|
|
<rect y="37" width="580" height="1" fill="#2d3748" />
|
|
<text x="12" y="24" fill="#4a9eff" font-size="12" font-weight="bold">uopi</text>
|
|
<text x="54" y="24" fill="#e2e8f0" font-size="11">Control Room</text>
|
|
{/* live chip */}
|
|
<rect x="488" y="10" width="80" height="18" rx="9" fill="#1e3a1e" />
|
|
<circle cx="500" cy="19" r="4" fill="#22c55e" />
|
|
<text x="509" y="23" fill="#22c55e" font-size="9">live</text>
|
|
<rect x="454" y="10" width="30" height="18" rx="4" fill="#2d3748" />
|
|
<text x="469" y="23" fill="#e2e8f0" font-size="9" text-anchor="middle">Edit</text>
|
|
|
|
{/* interface list panel */}
|
|
<rect x="0" y="38" width="148" height="192" fill="#151b2e" />
|
|
<rect x="0" y="38" width="148" height="192" fill="none" stroke="#2d3748" />
|
|
<text x="10" y="57" fill="#64748b" font-size="8">INTERFACES</text>
|
|
<rect x="6" y="63" width="136" height="22" rx="4" fill="#2d3748" />
|
|
<text x="14" y="79" fill="#4a9eff" font-size="10">Control Room</text>
|
|
<rect x="6" y="89" width="136" height="22" rx="4" fill="none" />
|
|
<text x="14" y="105" fill="#94a3b8" font-size="10">Vacuum System</text>
|
|
<rect x="6" y="115" width="136" height="22" rx="4" fill="none" />
|
|
<text x="14" y="131" fill="#94a3b8" font-size="10">RF Status</text>
|
|
|
|
{/* canvas area */}
|
|
<rect x="148" y="38" width="432" height="192" fill="#0f1117" />
|
|
|
|
{/* gauge widget */}
|
|
<rect x="162" y="52" width="108" height="88" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<text x="216" y="69" fill="#64748b" font-size="8" text-anchor="middle">Temperature</text>
|
|
<circle cx="216" cy="108" r="26" fill="none" stroke="#2d3748" stroke-width="5" />
|
|
<circle cx="216" cy="108" r="26" fill="none" stroke="#4a9eff" stroke-width="5"
|
|
stroke-dasharray="55 108" stroke-dashoffset="27" stroke-linecap="round" />
|
|
<text x="216" y="113" fill="#e2e8f0" font-size="10" text-anchor="middle" font-weight="bold">67°C</text>
|
|
|
|
{/* LED */}
|
|
<rect x="282" y="52" width="90" height="38" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<circle cx="300" cy="71" r="8" fill="#22c55e" />
|
|
<text x="314" y="75" fill="#94a3b8" font-size="9">Beam ON</text>
|
|
|
|
{/* text view */}
|
|
<rect x="282" y="98" width="90" height="30" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<text x="327" y="118" fill="#e2e8f0" font-size="12" text-anchor="middle" font-weight="bold">1.23 mA</text>
|
|
|
|
{/* bar */}
|
|
<rect x="162" y="150" width="108" height="68" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<text x="216" y="167" fill="#64748b" font-size="8" text-anchor="middle">Pressure</text>
|
|
<rect x="172" y="174" width="88" height="14" rx="3" fill="#2d3748" />
|
|
<rect x="172" y="174" width="57" height="14" rx="3" fill="#4a9eff" />
|
|
<text x="216" y="204" fill="#94a3b8" font-size="8" text-anchor="middle">65 %</text>
|
|
|
|
{/* plot */}
|
|
<rect x="386" y="52" width="182" height="166" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<text x="477" y="69" fill="#64748b" font-size="8" text-anchor="middle">Beam Current</text>
|
|
<polyline points="398,195 418,180 438,170 458,175 478,160 498,155 518,158 548,145"
|
|
fill="none" stroke="#4a9eff" stroke-width="1.5" />
|
|
<line x1="398" y1="75" x2="398" y2="200" stroke="#2d3748" stroke-width="1" />
|
|
<line x1="398" y1="200" x2="560" y2="200" stroke="#2d3748" stroke-width="1" />
|
|
|
|
{/* Annotations */}
|
|
{/* arrow to interface list */}
|
|
<line x1="74" y1="218" x2="74" y2="170" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#arr)" />
|
|
<text x="74" y="228" fill="#fbbf24" font-size="9" text-anchor="middle">① Interface list</text>
|
|
|
|
{/* arrow to canvas */}
|
|
<line x1="360" y1="218" x2="360" y2="180" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#arr)" />
|
|
<text x="360" y="228" fill="#fbbf24" font-size="9" text-anchor="middle">② Live canvas</text>
|
|
|
|
<defs>
|
|
<marker id="arr" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto">
|
|
<path d="M0,0 L6,3 L0,6 Z" fill="#fbbf24" />
|
|
</marker>
|
|
</defs>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function DiagramEditMode() {
|
|
return (
|
|
<svg viewBox="0 0 580 230" class="help-diagram" role="img" aria-label="Edit mode layout diagram">
|
|
<rect width="580" height="230" rx="8" fill="#0f1117" />
|
|
{/* toolbar */}
|
|
<rect width="580" height="38" fill="#1a1f2e" />
|
|
<rect y="37" width="580" height="1" fill="#2d3748" />
|
|
<text x="12" y="24" fill="#4a9eff" font-size="12" font-weight="bold">uopi</text>
|
|
<rect x="50" y="12" width="26" height="14" rx="3" fill="#1e40af" />
|
|
<text x="63" y="23" fill="#93c5fd" font-size="8" text-anchor="middle">Edit</text>
|
|
<text x="86" y="24" fill="#e2e8f0" font-size="11">My Panel</text>
|
|
{/* toolbar buttons */}
|
|
<rect x="350" y="11" width="28" height="16" rx="3" fill="#2d3748" />
|
|
<text x="364" y="23" fill="#e2e8f0" font-size="9" text-anchor="middle">↩</text>
|
|
<rect x="382" y="11" width="28" height="16" rx="3" fill="#2d3748" />
|
|
<text x="396" y="23" fill="#e2e8f0" font-size="9" text-anchor="middle">↪</text>
|
|
<rect x="480" y="11" width="36" height="16" rx="3" fill="#2563eb" />
|
|
<text x="498" y="23" fill="#fff" font-size="9" text-anchor="middle">Save</text>
|
|
<rect x="520" y="11" width="40" height="16" rx="3" fill="#2d3748" />
|
|
<text x="540" y="23" fill="#e2e8f0" font-size="9" text-anchor="middle">✕ Close</text>
|
|
|
|
{/* signal tree */}
|
|
<rect x="0" y="38" width="140" height="192" fill="#151b2e" />
|
|
<rect x="0" y="38" width="140" height="192" fill="none" stroke="#2d3748" />
|
|
<text x="10" y="57" fill="#64748b" font-size="8">SIGNALS</text>
|
|
<rect x="4" y="63" width="132" height="20" rx="3" fill="#1a2236" stroke="#2d3748" />
|
|
<text x="10" y="78" fill="#64748b" font-size="9">Filter signals…</text>
|
|
<text x="10" y="100" fill="#94a3b8" font-size="9">▾ epics</text>
|
|
<text x="20" y="115" fill="#cbd5e1" font-size="9"> CURRENT</text>
|
|
<text x="20" y="129" fill="#cbd5e1" font-size="9"> PRESSURE</text>
|
|
<text x="10" y="148" fill="#94a3b8" font-size="9">▾ stub</text>
|
|
<text x="20" y="163" fill="#cbd5e1" font-size="9"> sine_1hz</text>
|
|
<text x="20" y="177" fill="#cbd5e1" font-size="9"> ramp_10s</text>
|
|
|
|
{/* canvas */}
|
|
<rect x="140" y="38" width="288" height="192" fill="#0f1117" />
|
|
{/* grid dots */}
|
|
{[0,1,2,3,4,5,6,7,8].map(col =>
|
|
[0,1,2,3,4,5,6].map(row => (
|
|
<circle key={`${col}-${row}`} cx={140 + 10 + col * 30} cy={38 + 10 + row * 26}
|
|
r="0.8" fill="#2d3748" />
|
|
))
|
|
)}
|
|
{/* selected widget with handles */}
|
|
<rect x="185" y="75" width="120" height="60" rx="4" fill="#1a1f2e" stroke="#4a9eff" stroke-width="1.5" />
|
|
<text x="245" y="110" fill="#e2e8f0" font-size="11" text-anchor="middle" font-weight="bold">42.3°C</text>
|
|
<text x="245" y="90" fill="#64748b" font-size="8" text-anchor="middle">Temperature</text>
|
|
{/* resize handles */}
|
|
<rect x="182" y="72" width="8" height="8" rx="1" fill="#4a9eff" />
|
|
<rect x="301" y="72" width="8" height="8" rx="1" fill="#4a9eff" />
|
|
<rect x="182" y="131" width="8" height="8" rx="1" fill="#4a9eff" />
|
|
<rect x="301" y="131" width="8" height="8" rx="1" fill="#4a9eff" />
|
|
{/* another widget */}
|
|
<rect x="220" y="155" width="80" height="30" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<rect x="230" y="163" width="60" height="10" rx="2" fill="#2d3748" />
|
|
<rect x="230" y="163" width="38" height="10" rx="2" fill="#22c55e" />
|
|
|
|
{/* properties pane */}
|
|
<rect x="428" y="38" width="152" height="192" fill="#1a1f2e" />
|
|
<rect x="428" y="38" width="152" height="192" fill="none" stroke="#2d3748" />
|
|
<text x="438" y="56" fill="#64748b" font-size="8">PROPERTIES</text>
|
|
<line x1="428" y1="61" x2="580" y2="61" stroke="#2d3748" />
|
|
<text x="438" y="78" fill="#94a3b8" font-size="8">Signal</text>
|
|
<rect x="438" y="82" width="132" height="16" rx="3" fill="#1a2236" stroke="#2d3748" />
|
|
<text x="444" y="94" fill="#e2e8f0" font-size="9">TEMPERATURE</text>
|
|
<text x="438" y="112" fill="#94a3b8" font-size="8">X</text>
|
|
<rect x="438" y="116" width="58" height="14" rx="3" fill="#1a2236" stroke="#2d3748" />
|
|
<text x="444" y="127" fill="#e2e8f0" font-size="9">185</text>
|
|
<text x="504" y="112" fill="#94a3b8" font-size="8">Y</text>
|
|
<rect x="504" y="116" width="58" height="14" rx="3" fill="#1a2236" stroke="#2d3748" />
|
|
<text x="510" y="127" fill="#e2e8f0" font-size="9">75</text>
|
|
<text x="438" y="145" fill="#94a3b8" font-size="8">Width</text>
|
|
<rect x="438" y="149" width="58" height="14" rx="3" fill="#1a2236" stroke="#2d3748" />
|
|
<text x="444" y="160" fill="#e2e8f0" font-size="9">120</text>
|
|
<text x="504" y="145" fill="#94a3b8" font-size="8">Height</text>
|
|
<rect x="504" y="149" width="58" height="14" rx="3" fill="#1a2236" stroke="#2d3748" />
|
|
<text x="510" y="160" fill="#e2e8f0" font-size="9">60</text>
|
|
|
|
{/* Annotations */}
|
|
<text x="70" y="218" fill="#fbbf24" font-size="9" text-anchor="middle">① Signal tree</text>
|
|
<text x="284" y="218" fill="#fbbf24" font-size="9" text-anchor="middle">② Canvas</text>
|
|
<text x="504" y="218" fill="#fbbf24" font-size="9" text-anchor="middle">③ Properties</text>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function DiagramDragDrop() {
|
|
return (
|
|
<svg viewBox="0 0 580 200" class="help-diagram" role="img" aria-label="Drag and drop workflow">
|
|
<rect width="580" height="200" rx="8" fill="#0f1117" />
|
|
|
|
{/* Step 1 */}
|
|
<rect x="10" y="10" width="160" height="180" rx="6" fill="#151b2e" stroke="#2d3748" />
|
|
<text x="90" y="30" fill="#64748b" font-size="9" text-anchor="middle">① SIGNAL TREE</text>
|
|
<text x="20" y="55" fill="#94a3b8" font-size="9">▾ epics</text>
|
|
<rect x="16" y="62" width="148" height="22" rx="4" fill="#1e3a5f" stroke="#4a9eff" />
|
|
<text x="26" y="78" fill="#4a9eff" font-size="10">TEMPERATURE</text>
|
|
<text x="26" y="78" fill="none" stroke="#4a9eff" stroke-width="0" />
|
|
<text x="20" y="100" fill="#94a3b8" font-size="9"> PRESSURE</text>
|
|
<text x="20" y="115" fill="#94a3b8" font-size="9"> CURRENT</text>
|
|
{/* drag cursor hint */}
|
|
<text x="90" y="175" fill="#64748b" font-size="8" text-anchor="middle">drag signal →</text>
|
|
|
|
{/* Arrow 1→2 */}
|
|
<line x1="178" y1="100" x2="218" y2="100" stroke="#fbbf24" stroke-width="2"
|
|
marker-end="url(#arr2)" stroke-dasharray="4 2" />
|
|
|
|
{/* Step 2 */}
|
|
<rect x="220" y="10" width="160" height="180" rx="6" fill="#0f1117" stroke="#4a9eff" stroke-dasharray="4 3" />
|
|
<text x="300" y="30" fill="#64748b" font-size="9" text-anchor="middle">② DROP ON CANVAS</text>
|
|
{/* ghost drag indicator */}
|
|
<rect x="235" y="75" width="130" height="50" rx="4" fill="#1e3a5f" stroke="#4a9eff" stroke-width="1.5" stroke-dasharray="4 2" />
|
|
<text x="300" y="105" fill="#4a9eff" font-size="10" text-anchor="middle">TEMPERATURE</text>
|
|
<text x="300" y="175" fill="#64748b" font-size="8" text-anchor="middle">drop here</text>
|
|
|
|
{/* Arrow 2→3 */}
|
|
<line x1="388" y1="100" x2="408" y2="100" stroke="#fbbf24" stroke-width="2"
|
|
marker-end="url(#arr2)" stroke-dasharray="4 2" />
|
|
|
|
{/* Step 3: widget picker */}
|
|
<rect x="410" y="10" width="160" height="180" rx="6" fill="#1a1f2e" stroke="#2d3748" />
|
|
<text x="490" y="30" fill="#64748b" font-size="9" text-anchor="middle">③ PICK WIDGET TYPE</text>
|
|
<rect x="420" y="40" width="140" height="24" rx="4" fill="#2d3748" />
|
|
<text x="490" y="57" fill="#e2e8f0" font-size="10" text-anchor="middle">Gauge</text>
|
|
<rect x="420" y="68" width="140" height="24" rx="4" fill="#2d3748" />
|
|
<text x="490" y="85" fill="#e2e8f0" font-size="10" text-anchor="middle">Bar (horizontal)</text>
|
|
<rect x="420" y="96" width="140" height="24" rx="4" fill="#2d3748" />
|
|
<text x="490" y="113" fill="#e2e8f0" font-size="10" text-anchor="middle">Text View</text>
|
|
<rect x="420" y="124" width="140" height="24" rx="4" fill="#1e40af" />
|
|
<text x="490" y="141" fill="#93c5fd" font-size="10" text-anchor="middle">Plot</text>
|
|
<rect x="420" y="152" width="140" height="24" rx="4" fill="#2d3748" />
|
|
<text x="490" y="169" fill="#e2e8f0" font-size="10" text-anchor="middle">LED</text>
|
|
|
|
<defs>
|
|
<marker id="arr2" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
|
|
<path d="M0,0 L6,3 L0,6 Z" fill="#fbbf24" />
|
|
</marker>
|
|
</defs>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function DiagramWidgets() {
|
|
return (
|
|
<svg viewBox="0 0 580 220" class="help-diagram" role="img" aria-label="Widget types overview">
|
|
<rect width="580" height="220" rx="8" fill="#0f1117" />
|
|
{/* Text View */}
|
|
<g>
|
|
<rect x="20" y="20" width="110" height="56" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<text x="75" y="50" fill="#e2e8f0" font-size="14" text-anchor="middle" font-weight="bold">42.7 °C</text>
|
|
<text x="75" y="66" fill="#64748b" font-size="8" text-anchor="middle">Text View</text>
|
|
</g>
|
|
{/* Gauge */}
|
|
<g>
|
|
<rect x="150" y="20" width="110" height="90" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<circle cx="205" cy="82" r="26" fill="none" stroke="#2d3748" stroke-width="5" />
|
|
<circle cx="205" cy="82" r="26" fill="none" stroke="#4a9eff" stroke-width="5"
|
|
stroke-dasharray="52 108" stroke-dashoffset="27" stroke-linecap="round" />
|
|
<text x="205" y="87" fill="#e2e8f0" font-size="10" text-anchor="middle">67%</text>
|
|
<text x="205" y="34" fill="#64748b" font-size="8" text-anchor="middle">Gauge</text>
|
|
</g>
|
|
{/* Bar H */}
|
|
<g>
|
|
<rect x="280" y="20" width="130" height="56" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<text x="345" y="36" fill="#64748b" font-size="8" text-anchor="middle">Bar (horiz.)</text>
|
|
<rect x="290" y="44" width="110" height="18" rx="3" fill="#2d3748" />
|
|
<rect x="290" y="44" width="71" height="18" rx="3" fill="#4a9eff" />
|
|
<text x="345" y="66" fill="#94a3b8" font-size="8" text-anchor="middle">65 %</text>
|
|
</g>
|
|
{/* LED */}
|
|
<g>
|
|
<rect x="430" y="20" width="130" height="56" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<circle cx="460" cy="48" r="12" fill="#22c55e" />
|
|
<text x="477" y="46" fill="#94a3b8" font-size="9">Beam ON</text>
|
|
<text x="477" y="59" fill="#64748b" font-size="8">LED</text>
|
|
</g>
|
|
{/* Plot */}
|
|
<g>
|
|
<rect x="20" y="130" width="260" height="80" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<text x="150" y="148" fill="#64748b" font-size="8" text-anchor="middle">Time-series Plot</text>
|
|
<line x1="30" y1="152" x2="30" y2="200" stroke="#2d3748" />
|
|
<line x1="30" y1="200" x2="270" y2="200" stroke="#2d3748" />
|
|
<polyline points="32,195 55,182 78,175 101,180 124,165 147,160 170,163 193,150 216,155 239,142 262,138"
|
|
fill="none" stroke="#4a9eff" stroke-width="1.5" />
|
|
<polyline points="32,190 55,188 78,192 101,185 124,179 147,183 170,175 193,170 216,175 239,168 262,162"
|
|
fill="none" stroke="#22c55e" stroke-width="1.5" />
|
|
</g>
|
|
{/* Set Value */}
|
|
<g>
|
|
<rect x="300" y="130" width="130" height="80" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<text x="365" y="150" fill="#64748b" font-size="8" text-anchor="middle">Set-point Control</text>
|
|
<rect x="310" y="157" width="110" height="22" rx="4" fill="#1a2236" stroke="#2d3748" />
|
|
<text x="365" y="173" fill="#e2e8f0" font-size="11" text-anchor="middle">100.0</text>
|
|
<rect x="320" y="185" width="90" height="18" rx="4" fill="#2563eb" />
|
|
<text x="365" y="198" fill="#fff" font-size="9" text-anchor="middle">Set</text>
|
|
</g>
|
|
{/* Button */}
|
|
<g>
|
|
<rect x="450" y="130" width="110" height="80" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<text x="505" y="150" fill="#64748b" font-size="8" text-anchor="middle">Button</text>
|
|
<rect x="460" y="158" width="90" height="30" rx="6" fill="#2563eb" />
|
|
<text x="505" y="178" fill="#fff" font-size="11" text-anchor="middle">RESET</text>
|
|
</g>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function DiagramHistory() {
|
|
return (
|
|
<svg viewBox="0 0 580 130" class="help-diagram" role="img" aria-label="Historical data toolbar">
|
|
<rect width="580" height="130" rx="8" fill="#0f1117" />
|
|
|
|
{/* toolbar */}
|
|
<rect width="580" height="44" fill="#1a1f2e" />
|
|
<rect y="43" width="580" height="1" fill="#2d3748" />
|
|
|
|
{/* Live button (inactive) */}
|
|
<rect x="14" y="12" width="50" height="20" rx="10" fill="#2d3748" />
|
|
<circle cx="26" cy="22" r="4" fill="#64748b" />
|
|
<text x="42" y="27" fill="#94a3b8" font-size="9" text-anchor="middle">Live</text>
|
|
|
|
{/* start input */}
|
|
<rect x="72" y="12" width="148" height="20" rx="4" fill="#1a2236" stroke="#2d3748" />
|
|
<text x="80" y="26" fill="#e2e8f0" font-size="9">2026-01-15 09:00</text>
|
|
|
|
<text x="228" y="26" fill="#475569" font-size="11" text-anchor="middle">→</text>
|
|
|
|
{/* end input */}
|
|
<rect x="242" y="12" width="148" height="20" rx="4" fill="#1a2236" stroke="#63b3ed" />
|
|
<text x="250" y="26" fill="#e2e8f0" font-size="9">2026-01-15 10:00</text>
|
|
|
|
{/* Load button */}
|
|
<rect x="398" y="12" width="44" height="20" rx="4" fill="#2d3748" />
|
|
<text x="420" y="26" fill="#e2e8f0" font-size="9" text-anchor="middle">Load</text>
|
|
|
|
{/* Plot showing historical data */}
|
|
<rect x="10" y="54" width="560" height="66" rx="4" fill="#1a1f2e" stroke="#2d3748" />
|
|
<text x="290" y="70" fill="#64748b" font-size="8" text-anchor="middle">Loaded 3 600 historical points · 09:00 → 10:00</text>
|
|
<line x1="22" y1="75" x2="22" y2="112" stroke="#2d3748" />
|
|
<line x1="22" y1="112" x2="560" y2="112" stroke="#2d3748" />
|
|
<polyline
|
|
points="24,108 70,100 116,92 162,96 208,85 254,80 300,84 346,75 392,78 438,70 484,73 530,65 558,62"
|
|
fill="none" stroke="#4a9eff" stroke-width="1.5" />
|
|
|
|
{/* annotations */}
|
|
<line x1="39" y1="54" x2="39" y2="44" stroke="#fbbf24" stroke-width="1.5" />
|
|
<text x="39" y="122" fill="#fbbf24" font-size="8" text-anchor="middle">① Live</text>
|
|
<line x1="146" y1="54" x2="146" y2="44" stroke="#fbbf24" stroke-width="1.5" />
|
|
<text x="146" y="122" fill="#fbbf24" font-size="8" text-anchor="middle">② Start time</text>
|
|
<line x1="316" y1="54" x2="316" y2="44" stroke="#fbbf24" stroke-width="1.5" />
|
|
<text x="316" y="122" fill="#fbbf24" font-size="8" text-anchor="middle">③ End time</text>
|
|
<line x1="420" y1="54" x2="420" y2="44" stroke="#fbbf24" stroke-width="1.5" />
|
|
<text x="420" y="122" fill="#fbbf24" font-size="8" text-anchor="middle">④ Load</text>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function DiagramSignalTree() {
|
|
return (
|
|
<svg viewBox="0 0 280 280" class="help-diagram help-diagram-narrow" role="img" aria-label="Signal tree">
|
|
<rect width="280" height="280" rx="8" fill="#151b2e" />
|
|
|
|
{/* header */}
|
|
<text x="14" y="22" fill="#e2e8f0" font-size="11" font-weight="bold">Signals</text>
|
|
<rect x="240" y="8" width="28" height="20" rx="4" fill="#2d3748" />
|
|
<text x="254" y="23" fill="#e2e8f0" font-size="10" text-anchor="middle">◀</text>
|
|
<line x1="0" y1="32" x2="280" y2="32" stroke="#2d3748" />
|
|
|
|
{/* search */}
|
|
<rect x="8" y="38" width="194" height="20" rx="4" fill="#1a2236" stroke="#2d3748" />
|
|
<text x="16" y="53" fill="#64748b" font-size="9">Filter signals…</text>
|
|
<rect x="208" y="38" width="64" height="20" rx="4" fill="#2d3748" />
|
|
<text x="240" y="53" fill="#e2e8f0" font-size="8" text-anchor="middle">🔍 CF</text>
|
|
<line x1="0" y1="63" x2="280" y2="63" stroke="#2d3748" />
|
|
|
|
{/* toolbar */}
|
|
<rect x="8" y="68" width="80" height="18" rx="4" fill="#2d3748" />
|
|
<text x="48" y="81" fill="#e2e8f0" font-size="8" text-anchor="middle">+ Synthetic</text>
|
|
<rect x="94" y="68" width="38" height="18" rx="4" fill="#2d3748" />
|
|
<text x="113" y="81" fill="#e2e8f0" font-size="8" text-anchor="middle">CSV</text>
|
|
<rect x="138" y="68" width="28" height="18" rx="4" fill="#2d3748" />
|
|
<text x="152" y="81" fill="#e2e8f0" font-size="9" text-anchor="middle">↺</text>
|
|
<line x1="0" y1="90" x2="280" y2="90" stroke="#2d3748" />
|
|
|
|
{/* epics group */}
|
|
<text x="10" y="110" fill="#94a3b8" font-size="10">▾ epics</text>
|
|
<rect x="250" y="100" width="22" height="16" rx="3" fill="none" />
|
|
<text x="261" y="112" fill="#64748b" font-size="9" text-anchor="middle">+</text>
|
|
<rect x="8" y="118" width="264" height="20" rx="3" fill="none" />
|
|
<text x="22" y="133" fill="#cbd5e1" font-size="10" style="cursor:grab">TEMPERATURE</text>
|
|
<text x="22" y="152" fill="#cbd5e1" font-size="10">PRESSURE</text>
|
|
<text x="22" y="170" fill="#cbd5e1" font-size="10">BEAM_CURRENT</text>
|
|
|
|
{/* stub group */}
|
|
<line x1="0" y1="180" x2="280" y2="180" stroke="#2d3748" stroke-dasharray="2 2" />
|
|
<text x="10" y="198" fill="#94a3b8" font-size="10">▾ stub</text>
|
|
<text x="22" y="217" fill="#cbd5e1" font-size="10">sine_1hz</text>
|
|
<text x="22" y="235" fill="#cbd5e1" font-size="10">ramp_10s</text>
|
|
<text x="22" y="253" fill="#94a3b8" font-size="9" font-style="italic">my_custom_pv</text>
|
|
<rect x="250" y="243" width="22" height="14" rx="3" fill="none" />
|
|
<text x="261" y="254" fill="#ef4444" font-size="9" text-anchor="middle">✕</text>
|
|
|
|
{/* label custom */}
|
|
<rect x="100" y="246" width="52" height="12" rx="3" fill="#2d3748" />
|
|
<text x="126" y="256" fill="#fbbf24" font-size="7" text-anchor="middle">custom</text>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
// ── Section content ───────────────────────────────────────────────────────────
|
|
|
|
const SECTIONS = [
|
|
{ id: 'start', label: 'Getting Started' },
|
|
{ id: 'tips', label: 'Quick Tips' },
|
|
{ id: 'view', label: 'View Mode' },
|
|
{ id: 'edit', label: 'Edit Mode' },
|
|
{ id: 'widgets', label: 'Widgets' },
|
|
{ id: 'plots', label: 'Plot Panels' },
|
|
{ id: 'signals', label: 'Signals' },
|
|
{ id: 'logic', label: 'Panel Logic' },
|
|
{ id: 'control', label: 'Control Logic' },
|
|
{ id: 'sharing', label: 'Sharing & Access' },
|
|
{ id: 'audit', label: 'Audit Log' },
|
|
{ id: 'history', label: 'Historical Data' },
|
|
{ id: 'shortcuts', label: 'Keyboard Shortcuts' },
|
|
];
|
|
|
|
// Quick tips, grouped by the mode they apply to. Each links to the manual
|
|
// section that covers it in depth. (Previously surfaced via a floating
|
|
// contextual-help popover; now consolidated here in the manual.)
|
|
const TIPS: Record<'view' | 'edit', Array<{ text: string; section: string }>> = {
|
|
view: [
|
|
{ text: 'Click any interface in the left panel to load it on the canvas.', section: 'view' },
|
|
{ text: 'Right-click a widget to view signal info or export data as CSV.', section: 'view' },
|
|
{ text: 'The ● Live button shows live streaming is active. Use the date pickers to load historical data.', section: 'history' },
|
|
{ text: 'Click Edit (top-right) to open the drag-and-drop panel builder.', section: 'edit' },
|
|
{ text: 'Use the Share button on a panel to grant access to users or groups.', section: 'sharing' },
|
|
{ text: 'The ⚙ Control logic button opens server-side automation that runs even with no panel open.', section: 'control' },
|
|
{ text: 'The 🛡 Audit button (for permitted users) opens the log of system-affecting actions.', section: 'audit' },
|
|
{ text: 'The status chip turns green and shows your username when the WebSocket is connected.', section: 'view' },
|
|
],
|
|
edit: [
|
|
{ text: 'Drag a signal from the left tree onto the canvas to create a widget.', section: 'edit' },
|
|
{ text: 'Click a widget to select it, then adjust its properties on the right.', section: 'edit' },
|
|
{ text: 'Ctrl+click to select multiple widgets; then use the align toolbar.', section: 'edit' },
|
|
{ text: 'Ctrl+Z to undo, Ctrl+Y to redo. Up to 50 steps are remembered.', section: 'shortcuts' },
|
|
{ text: 'Use "+ Synthetic" in the signal tree to create computed/filtered signals.', section: 'signals' },
|
|
{ text: 'Switch to the Logic tab to add interactive behaviour with a node-graph flow editor.', section: 'logic' },
|
|
{ text: 'Add Local variables for set-points and counters that live inside the panel.', section: 'edit' },
|
|
{ text: 'Arrow keys nudge the selected widget by 1 px (Shift for grid size).', section: 'shortcuts' },
|
|
],
|
|
};
|
|
|
|
function SectionTips({ onNavigate }: { onNavigate: (section: string) => void }) {
|
|
const groups: Array<['view' | 'edit', string]> = [
|
|
['view', 'In View mode'],
|
|
['edit', 'In Edit mode'],
|
|
];
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
A handful of quick pointers to get the most out of uopi. Click any tip to jump to the
|
|
section that explains it in detail.
|
|
</p>
|
|
{groups.map(([mode, title]) => (
|
|
<div key={mode}>
|
|
<h3 class="help-h3">{title}</h3>
|
|
<ul class="help-tips-list">
|
|
{TIPS[mode].map(tip => (
|
|
<li key={tip.text}>
|
|
<button class="help-tip-item" onClick={() => onNavigate(tip.section)}>
|
|
<span class="help-tip-icon">💡</span>
|
|
<span class="help-tip-text">{tip.text}</span>
|
|
<span class="help-tip-go">↗</span>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionStart() {
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
<strong>uopi</strong> is a web-based HMI for monitoring and controlling EPICS-based scientific and industrial systems.
|
|
It runs as a single binary — open a browser, no installation required.
|
|
</p>
|
|
|
|
<h3 class="help-h3">How it works</h3>
|
|
<ol class="help-list">
|
|
<li>The server connects to your control system (EPICS, synthetic signals).</li>
|
|
<li>Open <code>http://<host>:8080</code> in any browser.</li>
|
|
<li>Pick an HMI panel from the left sidebar to view live data.</li>
|
|
<li>Switch to <strong>Edit mode</strong> to build or modify panels.</li>
|
|
</ol>
|
|
|
|
<h3 class="help-h3">Two modes</h3>
|
|
<div class="help-cards">
|
|
<div class="help-card">
|
|
<div class="help-card-title">👁 View Mode</div>
|
|
<p>Monitor live signals in read-only HMI panels. Right-click any widget for signal info or to export data as CSV.</p>
|
|
</div>
|
|
<div class="help-card">
|
|
<div class="help-card-title">✏ Edit Mode</div>
|
|
<p>Drag signals from the tree onto the canvas, resize and configure widgets, then save your panel to the server.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 class="help-h3">Quick navigation</h3>
|
|
<ul class="help-list">
|
|
<li>Click an interface in the left list to open it in view mode.</li>
|
|
<li>Click <strong>Edit</strong> (top-right) to switch to the editor.</li>
|
|
<li>Click <strong>✕ Close</strong> in Edit mode to return to View mode.</li>
|
|
<li>Press <kbd>?</kbd> anywhere to open this help.</li>
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionView() {
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
View mode displays a live HMI panel. Widget values update in real time via WebSocket.
|
|
</p>
|
|
|
|
<DiagramViewMode />
|
|
<p class="help-caption">View mode layout: ① interface list · ② live canvas · WebSocket status top-right.</p>
|
|
|
|
<h3 class="help-h3">Interface list</h3>
|
|
<ul class="help-list">
|
|
<li>Click any panel name to load it on the canvas.</li>
|
|
<li>Use the action buttons (edit ✎, clone ⧉, delete ✕) that appear on hover.</li>
|
|
<li>Collapse the list by clicking the <strong>◀</strong> button at the top.</li>
|
|
</ul>
|
|
|
|
<h3 class="help-h3">Context menu</h3>
|
|
<p>Right-click any widget to:</p>
|
|
<ul class="help-list">
|
|
<li>View the signal name and current value.</li>
|
|
<li>Copy the signal name to the clipboard.</li>
|
|
<li>Export the last 1 000 samples as a CSV file.</li>
|
|
</ul>
|
|
|
|
<h3 class="help-h3">Connection status</h3>
|
|
<p>
|
|
The coloured chip in the top-right shows the WebSocket connection state. When connected it
|
|
reads <em>"connected as <your username>"</em>, so you can confirm the identity your
|
|
actions are attributed to in the audit log. A red <em>"disconnected"</em> banner appears at
|
|
the top while reconnecting — live values will resume automatically.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionEdit() {
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
Edit mode gives you a drag-and-drop canvas, a signal tree on the left, and a properties pane on the right.
|
|
</p>
|
|
|
|
<DiagramEditMode />
|
|
<p class="help-caption">Edit mode layout: ① signal tree · ② canvas with snap grid · ③ properties pane.</p>
|
|
|
|
<h3 class="help-h3">Adding widgets</h3>
|
|
<DiagramDragDrop />
|
|
<p class="help-caption">Drag a signal from the tree → drop on canvas → choose a widget type.</p>
|
|
|
|
<h3 class="help-h3">Selecting and moving</h3>
|
|
<ul class="help-list">
|
|
<li>Click a widget to select it — resize handles appear at the corners and edges.</li>
|
|
<li>Drag the widget body to move it; drag a handle to resize.</li>
|
|
<li><kbd>Ctrl</kbd>+click to add/remove widgets from the selection.</li>
|
|
<li>Drag on empty canvas to rubber-band select multiple widgets.</li>
|
|
<li>Arrow keys nudge the selection by 1 px (or grid size with <kbd>Shift</kbd>).</li>
|
|
</ul>
|
|
|
|
<h3 class="help-h3">Align & distribute</h3>
|
|
<p>When 2+ widgets are selected the alignment toolbar appears. Buttons align edges or centres; with 3+ selected you can distribute spacing evenly.</p>
|
|
|
|
<h3 class="help-h3">Layout & Logic tabs</h3>
|
|
<p>The centre of the editor has two tabs:</p>
|
|
<ul class="help-list">
|
|
<li><strong>Layout</strong> — the drag-and-drop widget canvas described above.</li>
|
|
<li><strong>Logic</strong> — a visual flow editor that adds interactive behaviour to the
|
|
panel (buttons that run actions, thresholds that pop up dialogs, timers, …). See the
|
|
<em>Panel Logic</em> section. The tab is hidden if you are not permitted to edit logic.</li>
|
|
</ul>
|
|
|
|
<h3 class="help-h3">Local variables</h3>
|
|
<p>
|
|
Panels can define their own <strong>local variables</strong> — lightweight values that live
|
|
only inside the panel (no data source needed). Add them from the signal tree's
|
|
<em>Local</em> group (or the Logic palette). They are written by Set-value widgets, buttons
|
|
and logic actions, and referenced anywhere a signal is, making them handy for set-points,
|
|
toggles and counters used by panel logic.
|
|
</p>
|
|
|
|
<h3 class="help-h3">Saving</h3>
|
|
<ul class="help-list">
|
|
<li><strong>Save</strong> — stores the panel on the server (XML). A <span style="color:#f59e0b">●</span> indicates unsaved changes.</li>
|
|
<li><strong>Export</strong> — downloads the XML file locally.</li>
|
|
<li><strong>Import</strong> — loads an XML file from disk (replaces the current canvas).</li>
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionPlots() {
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
A <strong>plot panel</strong> is a special interface dedicated to charts. Instead of
|
|
free-form boxes, plots <em>fill the viewport</em> and you split the space between them like
|
|
panes in a tiling window manager (tmux / IDE style).
|
|
</p>
|
|
|
|
<h3 class="help-h3">Creating one</h3>
|
|
<ul class="help-list">
|
|
<li>Click <strong>+ Plot</strong> in the interface list to create a plot panel — it opens
|
|
with one full-viewport empty plot.</li>
|
|
</ul>
|
|
|
|
<h3 class="help-h3">Splitting & arranging</h3>
|
|
<ul class="help-list">
|
|
<li>Hover a pane and use its split buttons (<strong>⬌</strong> vertical / <strong>⬍</strong>
|
|
horizontal) to divide it — a new empty plot appears in the freed half.</li>
|
|
<li>Drag the divider between two panes to resize them; nesting is unlimited.</li>
|
|
<li>Click a pane to select it, then configure its plot in the Properties pane (plot type,
|
|
time window, range, legend, per-signal colour).</li>
|
|
<li>Drag a signal from the tree onto a pane to add it to that pane's plot.</li>
|
|
<li>Use a pane's <strong>✕</strong> to remove it; the layout collapses onto its sibling.</li>
|
|
</ul>
|
|
<p>In view mode the saved split layout fills the screen with live, streaming plots.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionLogic() {
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
The <strong>Logic</strong> tab in the panel editor is a node-graph (Node-RED style) flow
|
|
editor. You wire <em>trigger</em> nodes to <em>action</em> nodes to give a panel interactive
|
|
behaviour. Logic runs entirely client-side while the panel is open in view mode.
|
|
</p>
|
|
|
|
<h3 class="help-h3">Building a flow</h3>
|
|
<ul class="help-list">
|
|
<li>Drag blocks from the left palette onto the canvas (or click to add), then drag from a
|
|
node's output port to another node's input port to connect them.</li>
|
|
<li>Select a node to edit its parameters in the right inspector.</li>
|
|
<li>Expression fields accept arithmetic/logic over signals — reference a signal as
|
|
<code>{'{ds:name}'}</code> and a panel-local variable by its bare name.</li>
|
|
<li>The editor has its own undo/redo and copy/paste (see Keyboard Shortcuts).</li>
|
|
</ul>
|
|
|
|
<h3 class="help-h3">Node types</h3>
|
|
<div class="help-widget-table">
|
|
{[
|
|
{ type: 'Triggers', desc: 'Button press, threshold crossing, value change, timer/interval, panel loop, and On-open / On-close lifecycle.' },
|
|
{ type: 'Logic', desc: 'AND gate, If (then/else branches), and Loop (count or while).' },
|
|
{ type: 'Actions', desc: 'Write a value to a signal/variable, Delay, Log (debug), and Accumulate / Export-CSV / Clear for in-memory data arrays.' },
|
|
{ type: 'Dialogs', desc: 'Info and Error pop-ups, and a Set-point prompt that asks the user for a number and writes it to a target.' },
|
|
].map(w => (
|
|
<div key={w.type} class="help-widget-row">
|
|
<span class="help-widget-name">{w.type}</span>
|
|
<span class="help-widget-desc">{w.desc}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<h3 class="help-h3">System helpers</h3>
|
|
<ul class="help-list">
|
|
<li><code>{'{sys:time}'}</code> — current time in epoch seconds.</li>
|
|
<li><code>{'{sys:dt}'}</code> — seconds since the firing trigger last fired (useful for rates).</li>
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionControl() {
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
<strong>Control logic</strong> is server-side automation. Unlike panel logic (which only
|
|
runs while a panel is open in a browser), control-logic graphs run continuously on the
|
|
server, independent of any client. Open them with the <strong>⚙ Control logic</strong>
|
|
button in the view-mode toolbar.
|
|
</p>
|
|
|
|
<h3 class="help-h3">What it can do</h3>
|
|
<ul class="help-list">
|
|
<li>React to <em>cron</em> schedules and signal <em>alarm</em>/threshold conditions.</li>
|
|
<li>Run a <strong>Lua</strong> block for custom logic and compute writes back to signals.</li>
|
|
<li>Each graph can be enabled/disabled independently; changes reload the engine live.</li>
|
|
</ul>
|
|
|
|
<p class="help-callout">
|
|
Editing both panel logic and control logic can be restricted to an allowlist of users or
|
|
groups (<code>server.logic_editors</code> in the config). When restricted, users not on the
|
|
list keep full access to everything else but cannot add or change logic — the Logic tab and
|
|
the ⚙ Control logic button are hidden for them.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionSharing() {
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
uopi has graduated, server-enforced access control. Identity comes from a trusted
|
|
reverse-proxy header (with a configurable default for unproxied/LAN use) — there is no login
|
|
page in the app itself.
|
|
</p>
|
|
|
|
<h3 class="help-h3">Global access levels</h3>
|
|
<ul class="help-list">
|
|
<li>Every user is trusted with full <strong>write</strong> access by default.</li>
|
|
<li>A config blacklist can downgrade specific users to <strong>read-only</strong> or
|
|
<strong>no access</strong>. Read-only users see panels but cannot edit or write signals.</li>
|
|
<li>Named <strong>groups</strong> are defined in the config and used by panel sharing.</li>
|
|
</ul>
|
|
|
|
<h3 class="help-h3">Panel ownership & sharing</h3>
|
|
<ul class="help-list">
|
|
<li>New panels are <strong>private</strong> to their owner by default.</li>
|
|
<li>Use the <strong>Share</strong> button on a panel (in the interface list) to grant
|
|
read or write to specific users/groups, or make it public.</li>
|
|
<li>Your global level always caps the per-panel permission (read-only stays read-only).</li>
|
|
</ul>
|
|
|
|
<h3 class="help-h3">Folders</h3>
|
|
<ul class="help-list">
|
|
<li>Organise panels into nested <strong>folders</strong> in the interface list.</li>
|
|
<li>Permissions inherit down the folder chain.</li>
|
|
<li>Drag panels to reorder them or move them between folders.</li>
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionAudit() {
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
When the server is configured with <code>[audit] enabled = true</code>, every
|
|
<strong> system-affecting action</strong> is appended to an append-only log so it can be
|
|
reviewed later. Permitted users open it with the <strong>🛡 Audit</strong> button in the
|
|
view-mode toolbar.
|
|
</p>
|
|
|
|
<h3 class="help-h3">What gets recorded</h3>
|
|
<ul class="help-list">
|
|
<li><strong>Signal writes</strong> — both user-initiated (set-value widgets, buttons, panel
|
|
logic) and automated writes from the control-logic engine.</li>
|
|
<li><strong>Interface changes</strong> — panels created, updated, or deleted.</li>
|
|
<li><strong>Control-logic changes</strong> — graphs created, updated, or deleted.</li>
|
|
</ul>
|
|
<p>Each entry records the time, the actor (username, or the graph name for automated
|
|
actions), the action, the affected data source / signal and value, the client address, and
|
|
whether it succeeded.</p>
|
|
|
|
<h3 class="help-h3">Viewing the log</h3>
|
|
<ul class="help-list">
|
|
<li>The viewer lists the most recent events first.</li>
|
|
<li>Filter by user, action, signal, or a start/end time range.</li>
|
|
<li>System (automated) actions are tagged distinctly from user actions.</li>
|
|
</ul>
|
|
|
|
<p class="help-callout">
|
|
Access to the audit log is gated by an allowlist (<code>audit.readers</code> in the config),
|
|
following the same model as logic editing: an empty list means every user may view it,
|
|
otherwise only the listed users or groups can. The 🛡 Audit button is hidden for users who
|
|
are not permitted.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionWidgets() {
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
Each widget is configured in the Properties pane on the right.
|
|
The available options depend on the widget type.
|
|
</p>
|
|
|
|
<DiagramWidgets />
|
|
<p class="help-caption">Widget type overview — drag a signal from the tree to place one.</p>
|
|
|
|
<div class="help-widget-table">
|
|
{[
|
|
{ type: 'Text View', desc: 'Shows the current numeric or string value. Supports alarm colouring.' },
|
|
{ type: 'Gauge', desc: 'Circular arc gauge with configurable range and colour zones.' },
|
|
{ type: 'Bar (H / V)', desc: 'Horizontal or vertical filled bar, great for percentages or levels.' },
|
|
{ type: 'LED', desc: 'Coloured indicator. Turns green/red based on a threshold or enum value.' },
|
|
{ type: 'Multi-LED', desc: 'Bitset indicator — shows individual bits of an integer signal as LEDs.' },
|
|
{ type: 'Set Value', desc: 'Text input that writes a value back to a writable signal on Enter or click.' },
|
|
{ type: 'Button', desc: 'Sends a fixed value to a signal when clicked.' },
|
|
{ type: 'Plot', desc: 'Multi-signal chart. Supports time-series (uPlot), FFT, waterfall, histogram, bar chart, logic analyser.' },
|
|
{ type: 'Text Label', desc: 'Static annotation text — no signal required.' },
|
|
{ type: 'Image', desc: 'Static image loaded from a URL.' },
|
|
{ type: 'Link', desc: 'Navigates to another interface when clicked.' },
|
|
].map(w => (
|
|
<div key={w.type} class="help-widget-row">
|
|
<span class="help-widget-name">{w.type}</span>
|
|
<span class="help-widget-desc">{w.desc}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<h3 class="help-h3">Plot widget details</h3>
|
|
<ul class="help-list">
|
|
<li>Add multiple signals via the <em>Signals</em> section in the Properties pane.</li>
|
|
<li>Choose plot type: <em>timeseries</em>, <em>FFT</em>, <em>waterfall</em>, <em>histogram</em>, <em>bar</em>, or <em>logic</em>.</li>
|
|
<li>Set <em>Time window</em> to control how many seconds of live data are shown.</li>
|
|
<li>In Historical mode the entire requested range is loaded at once.</li>
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionSignals() {
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
The signal tree (left pane in Edit mode) lets you browse, search, and manage the signals available from each data source.
|
|
</p>
|
|
|
|
<div class="help-two-col">
|
|
<div>
|
|
<h3 class="help-h3">Finding signals</h3>
|
|
<ul class="help-list">
|
|
<li>Type in the <em>Filter signals…</em> box to search by name or description.</li>
|
|
<li>Click a group header to collapse or expand that data source.</li>
|
|
<li>Drag any signal onto the canvas to create a widget.</li>
|
|
</ul>
|
|
|
|
<h3 class="help-h3">Adding custom signals</h3>
|
|
<ul class="help-list">
|
|
<li>Click <strong>+</strong> next to a group name to type a PV name manually.</li>
|
|
<li>Custom signals are saved in your browser (localStorage) and persist across reloads.</li>
|
|
<li>Click <strong>✕</strong> on a custom signal to remove it.</li>
|
|
</ul>
|
|
|
|
<h3 class="help-h3">CSV import</h3>
|
|
<p>Click <strong>CSV</strong> to import a list of signals from a file. Each line should be either:</p>
|
|
<ul class="help-list">
|
|
<li><code>PV_NAME</code> — added to the <em>epics</em> data source.</li>
|
|
<li><code>ds,PV_NAME</code> — added to the specified data source.</li>
|
|
</ul>
|
|
<p>Lines starting with <code>#</code> are treated as comments.</p>
|
|
|
|
<h3 class="help-h3">Channel Finder</h3>
|
|
<p>
|
|
If your server is configured with an EPICS Channel Finder URL,
|
|
a <strong>🔍</strong> button appears when you type in the filter box.
|
|
Click it to search Channel Finder and import matching PV names.
|
|
</p>
|
|
|
|
<h3 class="help-h3">Synthetic signals</h3>
|
|
<p>Click <strong>+ Synthetic</strong> to compose a new signal from existing ones. A quick
|
|
<em>wizard</em> covers the common case (pick an input + a processing node such as gain,
|
|
offset, moving average, lowpass/highpass, derivative, integral, clamp or a custom
|
|
formula). For multi-input pipelines, the <strong>node-graph editor</strong> lets you wire
|
|
inputs through a chain of DSP blocks visually. The new signal appears in the tree
|
|
immediately.</p>
|
|
<p>Each synthetic signal has a <strong>visibility scope</strong>: <em>panel</em> (only the
|
|
panel that created it), <em>user</em>, or <em>global</em> (shared with everyone).</p>
|
|
|
|
<h3 class="help-h3">Local variables</h3>
|
|
<p>The <strong>Local</strong> group holds panel-local variables — values stored in the
|
|
panel itself with no data source. Use them for set-points, toggles and counters driven by
|
|
panel logic. They are added in the editor and saved with the panel.</p>
|
|
</div>
|
|
<DiagramSignalTree />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionHistory() {
|
|
return (
|
|
<div>
|
|
<p class="help-lead">
|
|
Historical data is retrieved from EPICS Archive Appliance when the server is configured with an <code>archive_url</code>.
|
|
Only the <strong>Plot</strong> widget in <em>time-series</em> mode supports historical ranges.
|
|
</p>
|
|
|
|
<DiagramHistory />
|
|
<p class="help-caption">Time navigation toolbar: ① Live button · ② start · ③ end datetime · ④ Load.</p>
|
|
|
|
<h3 class="help-h3">How to load historical data</h3>
|
|
<ol class="help-list">
|
|
<li>Set the <strong>start</strong> and <strong>end</strong> date/time fields in the toolbar.</li>
|
|
<li>Click <strong>Load</strong> — all plot widgets on the current panel will fetch the requested range.</li>
|
|
<li>While loading, a <em>"Loading history…"</em> overlay appears on each plot.</li>
|
|
<li>If no data is available, the overlay shows <em>"No archive data for this range"</em>.</li>
|
|
<li>Click <strong>● Live</strong> (highlighted when active) to return to streaming mode.</li>
|
|
</ol>
|
|
|
|
<h3 class="help-h3">Notes</h3>
|
|
<ul class="help-list">
|
|
<li>Historical data is requested over the WebSocket connection (not REST).</li>
|
|
<li>Requests time out after 15 seconds if the archive does not respond.</li>
|
|
<li>Non-timeseries plot types (FFT, waterfall, etc.) remain in live mode regardless of the time range.</li>
|
|
<li>Point-value widgets (gauge, bar, LED, …) are unaffected by the time range selection.</li>
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionShortcuts() {
|
|
const shortcuts: Array<[string, string]> = [
|
|
['Ctrl+C', 'Copy selected widgets (Edit mode)'],
|
|
['Ctrl+V', 'Paste widgets from clipboard (Edit mode)'],
|
|
['Ctrl+Z', 'Undo last change (Edit mode)'],
|
|
['Ctrl+Y / Ctrl+Shift+Z', 'Redo (Edit mode)'],
|
|
['Ctrl+A', 'Select all widgets (Edit mode)'],
|
|
['Delete / Backspace', 'Delete selected widgets (Edit mode)'],
|
|
['Arrow keys', 'Nudge selection by 1 px (Edit mode)'],
|
|
['Shift+Arrow', 'Nudge by grid size (Edit mode)'],
|
|
['Ctrl+click', 'Add/remove widget from multi-selection'],
|
|
['Escape', 'Close dialogs / cancel input'],
|
|
['?', 'Open this help'],
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<p class="help-lead">All shortcuts that work while the canvas or modal has focus.</p>
|
|
|
|
<table class="help-shortcut-table">
|
|
<thead>
|
|
<tr><th>Shortcut</th><th>Action</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{shortcuts.map(([key, action]) => (
|
|
<tr key={key}>
|
|
<td><kbd class="help-kbd">{key}</kbd></td>
|
|
<td>{action}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h3 class="help-h3">API & Metrics</h3>
|
|
<ul class="help-list">
|
|
<li>Signals/data: <code>/api/v1/datasources</code>, <code>/api/v1/signals</code>, <code>/api/v1/synthetic</code></li>
|
|
<li>Panels: <code>/api/v1/interfaces</code>, <code>/api/v1/folders</code>, <code>/api/v1/interfaces/{'{id}'}/acl</code></li>
|
|
<li>Identity & groups: <code>/api/v1/me</code>, <code>/api/v1/usergroups</code></li>
|
|
<li>Control logic: <code>/api/v1/controllogic</code></li>
|
|
<li>Prometheus metrics: <code>/metrics</code> · Health check: <code>/healthz</code> · WebSocket: <code>ws://<host>/ws</code></li>
|
|
</ul>
|
|
|
|
<h3 class="help-h3">Configuration file</h3>
|
|
<pre class="help-pre">[server]{'\n'}listen = ":8080"{'\n'}storage_dir = "./interfaces"{'\n'}# optional: restrict who may edit panel/control logic{'\n'}# logic_editors = ["operators"]{'\n\n'}[datasource.epics]{'\n'}enabled = true{'\n'}archive_url = "http://archiver:17665"{'\n\n'}[datasource.synthetic]{'\n'}enabled = true</pre>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const SECTION_COMPONENTS: Record<string, (props: { onNavigate: (section: string) => void }) => JSX.Element> = {
|
|
start: SectionStart,
|
|
tips: SectionTips,
|
|
view: SectionView,
|
|
edit: SectionEdit,
|
|
widgets: SectionWidgets,
|
|
plots: SectionPlots,
|
|
signals: SectionSignals,
|
|
logic: SectionLogic,
|
|
control: SectionControl,
|
|
sharing: SectionSharing,
|
|
audit: SectionAudit,
|
|
history: SectionHistory,
|
|
shortcuts: SectionShortcuts,
|
|
};
|
|
|
|
// ── Main modal ────────────────────────────────────────────────────────────────
|
|
|
|
export default function HelpModal({ initialSection = 'start', onClose }: Props) {
|
|
const [active, setActive] = useState(initialSection);
|
|
|
|
const Content: (props: { onNavigate: (section: string) => void }) => JSX.Element =
|
|
SECTION_COMPONENTS[active] ?? SectionStart;
|
|
|
|
// Close on backdrop click
|
|
function handleBackdrop(e: MouseEvent) {
|
|
if ((e.target as Element).classList.contains('help-backdrop')) onClose();
|
|
}
|
|
|
|
// Close on Escape
|
|
function handleKey(e: KeyboardEvent) {
|
|
if (e.key === 'Escape') onClose();
|
|
}
|
|
|
|
return (
|
|
<div class="help-backdrop" onClick={handleBackdrop} onKeyDown={handleKey} tabIndex={-1}>
|
|
<div class="help-modal" role="dialog" aria-modal="true" aria-label="uopi Help Manual">
|
|
{/* Header */}
|
|
<div class="help-header">
|
|
<span class="help-title">uopi — User Manual</span>
|
|
<button class="icon-btn help-close" onClick={onClose} title="Close help">✕</button>
|
|
</div>
|
|
|
|
<div class="help-body">
|
|
{/* Sidebar nav */}
|
|
<nav class="help-sidebar">
|
|
{SECTIONS.map(s => (
|
|
<button
|
|
key={s.id}
|
|
class={`help-nav-item${active === s.id ? ' active' : ''}`}
|
|
onClick={() => setActive(s.id)}
|
|
>
|
|
{s.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
|
|
{/* Content area */}
|
|
<div class="help-content">
|
|
<h2 class="help-h2">{SECTIONS.find(s => s.id === active)?.label}</h2>
|
|
<Content onNavigate={setActive} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|