mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
318 lines
13 KiB
TypeScript
318 lines
13 KiB
TypeScript
import { Checkbox } from '../ui/checkbox';
|
|
import { PACKET_LEGEND_ITEMS } from '../../utils/visualizerUtils';
|
|
import { NODE_LEGEND_ITEMS } from './shared';
|
|
|
|
interface VisualizerControlsProps {
|
|
showControls: boolean;
|
|
setShowControls: (value: boolean) => void;
|
|
fullScreen?: boolean;
|
|
onFullScreenChange?: (fullScreen: boolean) => void;
|
|
showAmbiguousPaths: boolean;
|
|
setShowAmbiguousPaths: (value: boolean) => void;
|
|
showAmbiguousNodes: boolean;
|
|
setShowAmbiguousNodes: (value: boolean) => void;
|
|
useAdvertPathHints: boolean;
|
|
setUseAdvertPathHints: (value: boolean) => void;
|
|
splitAmbiguousByTraffic: boolean;
|
|
setSplitAmbiguousByTraffic: (value: boolean) => void;
|
|
observationWindowSec: number;
|
|
setObservationWindowSec: (value: number) => void;
|
|
pruneStaleNodes: boolean;
|
|
setPruneStaleNodes: (value: boolean) => void;
|
|
pruneStaleMinutes: number;
|
|
setPruneStaleMinutes: (value: number) => void;
|
|
letEmDrift: boolean;
|
|
setLetEmDrift: (value: boolean) => void;
|
|
autoOrbit: boolean;
|
|
setAutoOrbit: (value: boolean) => void;
|
|
chargeStrength: number;
|
|
setChargeStrength: (value: number) => void;
|
|
particleSpeedMultiplier: number;
|
|
setParticleSpeedMultiplier: (value: number) => void;
|
|
nodeCount: number;
|
|
linkCount: number;
|
|
onExpandContract: () => void;
|
|
onClearAndReset: () => void;
|
|
}
|
|
|
|
export function VisualizerControls({
|
|
showControls,
|
|
setShowControls,
|
|
fullScreen,
|
|
onFullScreenChange,
|
|
showAmbiguousPaths,
|
|
setShowAmbiguousPaths,
|
|
showAmbiguousNodes,
|
|
setShowAmbiguousNodes,
|
|
useAdvertPathHints,
|
|
setUseAdvertPathHints,
|
|
splitAmbiguousByTraffic,
|
|
setSplitAmbiguousByTraffic,
|
|
observationWindowSec,
|
|
setObservationWindowSec,
|
|
pruneStaleNodes,
|
|
setPruneStaleNodes,
|
|
pruneStaleMinutes,
|
|
setPruneStaleMinutes,
|
|
letEmDrift,
|
|
setLetEmDrift,
|
|
autoOrbit,
|
|
setAutoOrbit,
|
|
chargeStrength,
|
|
setChargeStrength,
|
|
particleSpeedMultiplier,
|
|
setParticleSpeedMultiplier,
|
|
nodeCount,
|
|
linkCount,
|
|
onExpandContract,
|
|
onClearAndReset,
|
|
}: VisualizerControlsProps) {
|
|
return (
|
|
<>
|
|
{showControls && (
|
|
<div className="absolute bottom-4 left-4 bg-background/80 backdrop-blur-sm rounded-lg p-3 text-xs border border-border z-10">
|
|
<div className="flex gap-6">
|
|
<div className="flex flex-col gap-1.5">
|
|
<div className="text-muted-foreground font-medium mb-1">Packets</div>
|
|
{PACKET_LEGEND_ITEMS.map((item) => (
|
|
<div key={item.label} className="flex items-center gap-2">
|
|
<div
|
|
className="w-5 h-5 rounded-full flex items-center justify-center text-[8px] font-bold text-white"
|
|
style={{ backgroundColor: item.color }}
|
|
>
|
|
{item.label}
|
|
</div>
|
|
<span>{item.description}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<div className="text-muted-foreground font-medium mb-1">Nodes</div>
|
|
{NODE_LEGEND_ITEMS.map((item) => (
|
|
<div key={item.label} className="flex items-center gap-2">
|
|
<div
|
|
className="rounded-full"
|
|
style={{
|
|
width: item.size,
|
|
height: item.size,
|
|
backgroundColor: item.color,
|
|
}}
|
|
/>
|
|
<span>{item.label}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
className={`absolute top-4 left-4 bg-background/80 backdrop-blur-sm rounded-lg p-3 text-xs border border-border z-10 transition-opacity ${!showControls ? 'opacity-40 hover:opacity-100' : ''}`}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex flex-col gap-2">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<Checkbox
|
|
checked={showControls}
|
|
onCheckedChange={(c) => setShowControls(c === true)}
|
|
/>
|
|
<span title="Toggle legends and controls visibility">Show controls</span>
|
|
</label>
|
|
{onFullScreenChange && (
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<Checkbox
|
|
checked={!fullScreen}
|
|
onCheckedChange={(c) => onFullScreenChange(c !== true)}
|
|
/>
|
|
<span title="Show or hide the packet feed sidebar">Show packet feed sidebar</span>
|
|
</label>
|
|
)}
|
|
</div>
|
|
{showControls && (
|
|
<>
|
|
<div className="border-t border-border pt-2 mt-1 flex flex-col gap-2">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<Checkbox
|
|
checked={showAmbiguousPaths}
|
|
onCheckedChange={(c) => setShowAmbiguousPaths(c === true)}
|
|
/>
|
|
<span title="Show placeholder nodes for repeaters when the 1-byte prefix matches multiple contacts">
|
|
Show ambiguous repeaters
|
|
</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<Checkbox
|
|
checked={showAmbiguousNodes}
|
|
onCheckedChange={(c) => setShowAmbiguousNodes(c === true)}
|
|
/>
|
|
<span title="Show placeholder nodes for senders/recipients when only a 1-byte prefix is known">
|
|
Show ambiguous sender/recipient
|
|
</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<Checkbox
|
|
checked={useAdvertPathHints}
|
|
onCheckedChange={(c) => setUseAdvertPathHints(c === true)}
|
|
disabled={!showAmbiguousPaths}
|
|
/>
|
|
<span
|
|
title="Use stored repeater advert paths to assign likely identity labels for ambiguous repeater nodes"
|
|
className={!showAmbiguousPaths ? 'text-muted-foreground' : ''}
|
|
>
|
|
Use repeater advert-path identity hints
|
|
</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<Checkbox
|
|
checked={splitAmbiguousByTraffic}
|
|
onCheckedChange={(c) => setSplitAmbiguousByTraffic(c === true)}
|
|
disabled={!showAmbiguousPaths}
|
|
/>
|
|
<span
|
|
title="Split ambiguous repeaters into separate nodes based on traffic patterns (prev→next). Helps identify colliding prefixes representing different physical nodes, but requires enough traffic to disambiguate."
|
|
className={!showAmbiguousPaths ? 'text-muted-foreground' : ''}
|
|
>
|
|
Heuristically group repeaters by traffic pattern
|
|
</span>
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<label
|
|
htmlFor="observation-window-3d"
|
|
className="text-muted-foreground"
|
|
title="How long to wait for duplicate packets via different paths before animating"
|
|
>
|
|
Ack/echo listen window:
|
|
</label>
|
|
<input
|
|
id="observation-window-3d"
|
|
type="number"
|
|
min="1"
|
|
max="60"
|
|
value={observationWindowSec}
|
|
onChange={(e) =>
|
|
setObservationWindowSec(
|
|
Math.max(1, Math.min(60, parseInt(e.target.value, 10) || 1))
|
|
)
|
|
}
|
|
className="w-12 px-1 py-0.5 bg-background border border-border rounded text-xs text-center"
|
|
/>
|
|
<span className="text-muted-foreground">sec</span>
|
|
</div>
|
|
<div className="border-t border-border pt-2 mt-1 flex flex-col gap-2">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<Checkbox
|
|
checked={pruneStaleNodes}
|
|
onCheckedChange={(c) => setPruneStaleNodes(c === true)}
|
|
/>
|
|
<span title="Automatically remove nodes with no traffic within the configured window to keep the mesh manageable">
|
|
Only show recently heard/in-a-path nodes
|
|
</span>
|
|
</label>
|
|
{pruneStaleNodes && (
|
|
<div className="flex items-center gap-2 pl-6">
|
|
<label
|
|
htmlFor="prune-window"
|
|
className="text-muted-foreground whitespace-nowrap"
|
|
>
|
|
Window:
|
|
</label>
|
|
<input
|
|
id="prune-window"
|
|
type="number"
|
|
min={1}
|
|
max={60}
|
|
value={pruneStaleMinutes}
|
|
onChange={(e) => {
|
|
const v = parseInt(e.target.value, 10);
|
|
if (!isNaN(v) && v >= 1 && v <= 60) setPruneStaleMinutes(v);
|
|
}}
|
|
className="w-14 rounded border border-border bg-background px-2 py-0.5 text-sm"
|
|
/>
|
|
<span className="text-muted-foreground" aria-hidden="true">
|
|
min
|
|
</span>
|
|
</div>
|
|
)}
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<Checkbox
|
|
checked={letEmDrift}
|
|
onCheckedChange={(c) => setLetEmDrift(c === true)}
|
|
/>
|
|
<span title="When enabled, the graph continuously reorganizes itself into a better layout">
|
|
Let 'em drift
|
|
</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<Checkbox
|
|
checked={autoOrbit}
|
|
onCheckedChange={(c) => setAutoOrbit(c === true)}
|
|
/>
|
|
<span title="Automatically orbit the camera around the scene">
|
|
Orbit the mesh
|
|
</span>
|
|
</label>
|
|
<div className="flex flex-col gap-1 mt-1">
|
|
<label
|
|
htmlFor="viz-repulsion"
|
|
className="text-muted-foreground"
|
|
title="How strongly nodes repel each other. Higher values spread nodes out more."
|
|
>
|
|
Repulsion: {Math.abs(chargeStrength)}
|
|
</label>
|
|
<input
|
|
id="viz-repulsion"
|
|
type="range"
|
|
min="50"
|
|
max="2500"
|
|
value={Math.abs(chargeStrength)}
|
|
onChange={(e) => setChargeStrength(-parseInt(e.target.value, 10))}
|
|
className="w-full h-2 bg-border rounded-lg appearance-none cursor-pointer accent-primary"
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1 mt-1">
|
|
<label
|
|
htmlFor="viz-packet-speed"
|
|
className="text-muted-foreground"
|
|
title="How fast particles travel along links. Higher values make packets move faster."
|
|
>
|
|
Packet speed: {particleSpeedMultiplier}x
|
|
</label>
|
|
<input
|
|
id="viz-packet-speed"
|
|
type="range"
|
|
min="1"
|
|
max="5"
|
|
step="0.5"
|
|
value={particleSpeedMultiplier}
|
|
onChange={(e) => setParticleSpeedMultiplier(parseFloat(e.target.value))}
|
|
className="w-full h-2 bg-border rounded-lg appearance-none cursor-pointer accent-primary"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onExpandContract}
|
|
className="mt-1 px-3 py-1.5 bg-primary/20 hover:bg-primary/30 text-primary rounded text-xs transition-colors"
|
|
title="Expand nodes apart then contract back - can help untangle the graph"
|
|
>
|
|
Oooh Big Stretch!
|
|
</button>
|
|
<button
|
|
onClick={onClearAndReset}
|
|
className="mt-1 rounded border border-warning/40 bg-warning/10 px-3 py-1.5 text-warning text-xs transition-colors hover:bg-warning/20"
|
|
title="Clear all nodes and links from the visualization - packets are preserved"
|
|
>
|
|
Clear & Reset
|
|
</button>
|
|
</div>
|
|
<div className="border-t border-border pt-2 mt-1">
|
|
<div>Nodes: {nodeCount}</div>
|
|
<div>Links: {linkCount}</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|