feat(web/mazenet): port-drag edges, context menus, delete actions

This commit is contained in:
2026-04-20 19:26:49 -04:00
parent 0401cccd1d
commit 6db5842a28
8 changed files with 320 additions and 34 deletions

View File

@@ -6,6 +6,7 @@ import Palette from './Palette';
import Canvas from './Canvas';
import Inspector from './Inspector';
import type { Selection } from './Inspector';
import ContextMenu, { type MenuItem } from './ContextMenu';
import { DEFAULT_SERVICES, DEMO_NETS, DEMO_NODES, DEMO_EDGES } from './data';
import type { ServiceDef } from './data';
import type { Net, MazeNode, Edge, PendingChange } from './types';
@@ -29,9 +30,103 @@ const MazeNET: React.FC = () => {
const canvasRef = useRef<HTMLDivElement>(null);
const applyChange = useCallback((pc: PendingChange) => {
setPending((p) => [...p, pc]);
if (pc.op === 'add_edge') {
const payload = pc.payload;
setEdges((prev) => prev.some((e) => e.id === payload.id)
? prev
: [...prev, { id: payload.id, from: payload.from, to: payload.to, traffic: 'active' as const }]);
}
}, []);
const interaction = useMazeInteraction({ nets, nodes, setNets, setNodes, applyChange, canvasRef });
const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; items: MenuItem[] } | null>(null);
const removeNet = (id: string) => {
const net = nets.find((n) => n.id === id);
if (!net || net.kind === 'internet') return;
setNets((p) => p.filter((n) => n.id !== id));
setNodes((p) => p.filter((n) => n.netId !== id));
setEdges((p) => p.filter((e) => {
const a = nodes.find((x) => x.id === e.from)?.netId;
const b = nodes.find((x) => x.id === e.to)?.netId;
return a !== id && b !== id;
}));
applyChange({ op: 'remove_lan', payload: { id } });
setSelection(null);
};
const removeNode = (id: string) => {
const node = nodes.find((n) => n.id === id);
if (!node || node.kind === 'observed') return;
setNodes((p) => p.filter((n) => n.id !== id));
setEdges((p) => p.filter((e) => e.from !== id && e.to !== id));
applyChange({ op: 'remove_decky', payload: { nodeId: id } });
setSelection(null);
};
const removeEdge = (id: string) => {
setEdges((p) => p.filter((e) => e.id !== id));
applyChange({ op: 'remove_edge', payload: { id } });
setSelection(null);
};
const onNodeContextMenu = (id: string) => (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const node = nodes.find((n) => n.id === id);
if (!node) return;
setSelection({ type: 'node', id });
const isObs = node.kind === 'observed';
setCtxMenu({
x: e.clientX, y: e.clientY,
items: [
{ label: 'INSPECT', onClick: () => setSelection({ type: 'node', id }) },
{ separator: true, label: '' },
{
label: 'DELETE NODE',
danger: true,
disabled: isObs,
title: isObs ? 'observed entity — not a deployed decky' : undefined,
onClick: () => removeNode(id),
},
],
});
};
const onNetContextMenu = (id: string) => (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const net = nets.find((n) => n.id === id);
if (!net) return;
setSelection({ type: 'net', id });
setCtxMenu({
x: e.clientX, y: e.clientY,
items: [
{ label: 'INSPECT', onClick: () => setSelection({ type: 'net', id }) },
{ separator: true, label: '' },
{
label: 'DELETE NET',
danger: true,
disabled: net.kind === 'internet',
title: net.kind === 'internet' ? 'internet zone cannot be removed' : undefined,
onClick: () => removeNet(id),
},
],
});
};
const onEdgeContextMenu = (id: string) => (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setSelection({ type: 'edge', id });
setCtxMenu({
x: e.clientX, y: e.clientY,
items: [
{ label: 'REMOVE EDGE', danger: true, onClick: () => removeEdge(id) },
],
});
};
/* Load service catalog from API (fall back to defaults if 401/offline). */
useEffect(() => {
let cancelled = false;
@@ -128,11 +223,19 @@ const MazeNET: React.FC = () => {
pan={interaction.pan}
dropTargetId={interaction.dropTargetId}
dragging={interaction.dragging}
edgeDraw={interaction.edgeDraw}
onCanvasMouseDown={interaction.onCanvasMouseDown}
onNodeMouseDown={interaction.onNodeMouseDown}
onNetMouseDown={interaction.onNetMouseDown}
onNetResizeMouseDown={interaction.onNetResizeMouseDown}
onPortMouseDown={interaction.onPortMouseDown}
onNodeContextMenu={onNodeContextMenu}
onNetContextMenu={onNetContextMenu}
onEdgeContextMenu={onEdgeContextMenu}
/>
{ctxMenu && (
<ContextMenu x={ctxMenu.x} y={ctxMenu.y} items={ctxMenu.items} onClose={() => setCtxMenu(null)} />
)}
{inspectorOpen && (
<Inspector
selection={selection}
@@ -141,6 +244,9 @@ const MazeNET: React.FC = () => {
edges={edges}
pending={pending}
onClose={() => setInspectorOpen(false)}
onDeleteNet={removeNet}
onDeleteNode={removeNode}
onDeleteEdge={removeEdge}
/>
)}
</div>