+98
-17
@@ -5,6 +5,7 @@ let nodes = [];
|
|||||||
let links = [];
|
let links = [];
|
||||||
let shapes = [];
|
let shapes = [];
|
||||||
let selectedNodeId = null;
|
let selectedNodeId = null;
|
||||||
|
let selectedNodeIds = [];
|
||||||
let selectedShapeId = null;
|
let selectedShapeId = null;
|
||||||
|
|
||||||
let canvas, ctx;
|
let canvas, ctx;
|
||||||
@@ -20,6 +21,8 @@ let dragType = null;
|
|||||||
let dragOffX, dragOffY;
|
let dragOffX, dragOffY;
|
||||||
let dragHandle = null;
|
let dragHandle = null;
|
||||||
let dragOrig = null;
|
let dragOrig = null;
|
||||||
|
let selectStartX, selectStartY;
|
||||||
|
let selectRect = null;
|
||||||
|
|
||||||
let nextShapeZ = 0;
|
let nextShapeZ = 0;
|
||||||
let copyBuffer = null;
|
let copyBuffer = null;
|
||||||
@@ -63,9 +66,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
||||||
|
if (!document.activeElement || document.activeElement.tagName !== 'INPUT') {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedNodeIds = canvasNodes.map(n => n.id);
|
||||||
|
selectedNodeId = selectedNodeIds.length > 0 ? selectedNodeIds[0] : null;
|
||||||
|
selectedShapeId = null;
|
||||||
|
renderNodeList(); renderShapeList(); renderNetwork();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e.key === 'Delete') {
|
if (e.key === 'Delete') {
|
||||||
if (!document.activeElement || document.activeElement.tagName !== 'INPUT') {
|
if (!document.activeElement || document.activeElement.tagName !== 'INPUT') {
|
||||||
if (selectedNodeId) deleteSelectedNode(selectedNodeId);
|
if (selectedNodeIds.length > 0) deleteSelectedNodes();
|
||||||
else if (selectedShapeId) deleteSelectedShape(selectedShapeId);
|
else if (selectedShapeId) deleteSelectedShape(selectedShapeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,7 +253,7 @@ function renderNodeList() {
|
|||||||
list.innerHTML = nodes.map(n => {
|
list.innerHTML = nodes.map(n => {
|
||||||
const iconMap = { host:'fa-desktop', server:'fa-server', router:'fa-route', firewall:'fa-shield-halved', switch:'fa-network-wired', cloud:'fa-cloud', endpoint:'fa-laptop', other:'fa-circle' };
|
const iconMap = { host:'fa-desktop', server:'fa-server', router:'fa-route', firewall:'fa-shield-halved', switch:'fa-network-wired', cloud:'fa-cloud', endpoint:'fa-laptop', other:'fa-circle' };
|
||||||
return `
|
return `
|
||||||
<div class="list-group-item bg-dark border-secondary node-list-item py-2 ${selectedNodeId == n.id ? 'active' : ''}" onclick="selectNode(${n.id})">
|
<div class="list-group-item bg-dark border-secondary node-list-item py-2 ${selectedNodeIds.includes(n.id) ? 'active' : ''}" onclick="selectNode(${n.id}, event.shiftKey)">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span class="status-dot status-${n.status}"></span>
|
<span class="status-dot status-${n.status}"></span>
|
||||||
<i class="fas ${iconMap[n.node_type] || 'fa-circle'} me-2" style="color:${getNodeColorVal(n.node_type)};font-size:.85rem;"></i>
|
<i class="fas ${iconMap[n.node_type] || 'fa-circle'} me-2" style="color:${getNodeColorVal(n.node_type)};font-size:.85rem;"></i>
|
||||||
@@ -268,8 +281,15 @@ function renderShapeList() {
|
|||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectNode(id) {
|
function selectNode(id, add = false) {
|
||||||
|
if (add) {
|
||||||
|
const idx = selectedNodeIds.indexOf(id);
|
||||||
|
if (idx >= 0) { selectedNodeIds.splice(idx, 1); if (selectedNodeIds.length === 0) selectedNodeId = null; else selectedNodeId = selectedNodeIds[0]; }
|
||||||
|
else { selectedNodeIds.push(id); selectedNodeId = id; }
|
||||||
|
} else {
|
||||||
selectedNodeId = id;
|
selectedNodeId = id;
|
||||||
|
selectedNodeIds = [id];
|
||||||
|
}
|
||||||
selectedShapeId = null;
|
selectedShapeId = null;
|
||||||
const n = nodes.find(x => x.id == id);
|
const n = nodes.find(x => x.id == id);
|
||||||
if (n) {
|
if (n) {
|
||||||
@@ -280,7 +300,8 @@ function selectNode(id) {
|
|||||||
<div><span class="text-secondary">Type:</span> ${n.node_type}</div>
|
<div><span class="text-secondary">Type:</span> ${n.node_type}</div>
|
||||||
<div><span class="text-secondary">Status:</span> ${n.status}</div>
|
<div><span class="text-secondary">Status:</span> ${n.status}</div>
|
||||||
<div><span class="text-secondary">Group:</span> ${n.group_name}</div>
|
<div><span class="text-secondary">Group:</span> ${n.group_name}</div>
|
||||||
<button class="btn btn-outline-danger btn-sm mt-2" onclick="deleteSelectedNode(${n.id})"><i class="fas fa-trash me-1"></i>Delete</button>
|
${selectedNodeIds.length > 1 ? `<small class="text-secondary">+${selectedNodeIds.length - 1} more selected</small>` : ''}
|
||||||
|
<button class="btn btn-outline-danger btn-sm mt-2" onclick="deleteSelectedNodes()"><i class="fas fa-trash me-1"></i>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -297,12 +318,14 @@ function selectShape(id) {
|
|||||||
renderNetwork();
|
renderNetwork();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteSelectedNode(id) {
|
async function deleteSelectedNodes() {
|
||||||
if (!id) return;
|
const ids = [...selectedNodeIds];
|
||||||
const ok = await showConfirm('Delete this node and its connections?');
|
if (!ids.length) return;
|
||||||
|
const ok = await showConfirm(`Delete ${ids.length} node(s) and their connections?`);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
selectedNodeId = null;
|
selectedNodeId = null;
|
||||||
await apiFetch(`nodes/${id}`, { method: 'DELETE' });
|
selectedNodeIds = [];
|
||||||
|
for (const id of ids) await apiFetch(`nodes/${id}`, { method: 'DELETE' });
|
||||||
loadNetworkData();
|
loadNetworkData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,6 +526,18 @@ function renderNetwork() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
canvasNodes.forEach(drawCanvasNode);
|
canvasNodes.forEach(drawCanvasNode);
|
||||||
|
|
||||||
|
// Selection rectangle
|
||||||
|
if (selectRect && (selectRect.w > 0 || selectRect.h > 0)) {
|
||||||
|
ctx.strokeStyle = '#3b82f6';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.setLineDash([4, 4]);
|
||||||
|
ctx.strokeRect(selectRect.x, selectRect.y, selectRect.w, selectRect.h);
|
||||||
|
ctx.fillStyle = 'rgba(59, 130, 246, 0.08)';
|
||||||
|
ctx.fillRect(selectRect.x, selectRect.y, selectRect.w, selectRect.h);
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,7 +582,7 @@ function drawShape(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function drawCanvasNode(n) {
|
function drawCanvasNode(n) {
|
||||||
const sel = selectedNodeId == n.id;
|
const sel = selectedNodeIds.includes(n.id);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
if (sel) { ctx.shadowColor = n.color; ctx.shadowBlur = 18; }
|
if (sel) { ctx.shadowColor = n.color; ctx.shadowBlur = 18; }
|
||||||
@@ -639,9 +674,18 @@ function onMouseDown(e) {
|
|||||||
|
|
||||||
const node = getCanvasNodeAt(mx, my);
|
const node = getCanvasNodeAt(mx, my);
|
||||||
if (node) {
|
if (node) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
selectNode(node.id, true);
|
||||||
|
// Start multi-drag for all selected
|
||||||
dragType = 'node'; dragTarget = node;
|
dragType = 'node'; dragTarget = node;
|
||||||
dragOffX = mx - node.x; dragOffY = my - node.y;
|
dragOffX = mx - node.x; dragOffY = my - node.y;
|
||||||
|
} else {
|
||||||
|
if (!selectedNodeIds.includes(node.id)) {
|
||||||
selectNode(node.id);
|
selectNode(node.id);
|
||||||
|
}
|
||||||
|
dragType = 'node'; dragTarget = node;
|
||||||
|
dragOffX = mx - node.x; dragOffY = my - node.y;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,19 +705,23 @@ function onMouseDown(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedNodeId = null; selectedShapeId = null;
|
selectedNodeId = null; selectedNodeIds = []; selectedShapeId = null;
|
||||||
renderNodeList(); renderShapeList();
|
renderNodeList(); renderShapeList();
|
||||||
isPanning = true;
|
dragType = 'select';
|
||||||
panStartX = e.clientX - panX; panStartY = e.clientY - panY;
|
selectStartX = mx; selectStartY = my;
|
||||||
canvas.style.cursor = 'grabbing';
|
selectRect = { x: mx, y: my, w: 0, h: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseMove(e) {
|
function onMouseMove(e) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
if (dragType === 'node' && dragTarget) {
|
if (dragType === 'node' && dragTarget) {
|
||||||
dragTarget.x = e.clientX - rect.left - panX - dragOffX;
|
const dx = e.clientX - rect.left - panX - dragOffX - dragTarget.x;
|
||||||
dragTarget.y = e.clientY - rect.top - panY - dragOffY;
|
const dy = e.clientY - rect.top - panY - dragOffY - dragTarget.y;
|
||||||
|
// Move all selected nodes by delta
|
||||||
|
for (const c of canvasNodes) {
|
||||||
|
if (selectedNodeIds.includes(c.id)) { c.x += dx; c.y += dy; }
|
||||||
|
}
|
||||||
renderNetwork();
|
renderNetwork();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -699,6 +747,16 @@ function onMouseMove(e) {
|
|||||||
renderNetwork();
|
renderNetwork();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (dragType === 'select') {
|
||||||
|
const mx = e.clientX - rect.left - panX;
|
||||||
|
const my = e.clientY - rect.top - panY;
|
||||||
|
selectRect.x = Math.min(selectStartX, mx);
|
||||||
|
selectRect.y = Math.min(selectStartY, my);
|
||||||
|
selectRect.w = Math.abs(mx - selectStartX);
|
||||||
|
selectRect.h = Math.abs(my - selectStartY);
|
||||||
|
renderNetwork();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isPanning) {
|
if (isPanning) {
|
||||||
panX = e.clientX - panStartX; panY = e.clientY - panStartY;
|
panX = e.clientX - panStartX; panY = e.clientY - panStartY;
|
||||||
renderNetwork();
|
renderNetwork();
|
||||||
@@ -713,12 +771,35 @@ function onMouseMove(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onMouseUp(e) {
|
function onMouseUp(e) {
|
||||||
if (dragTarget && dragType === 'node') {
|
if (dragType === 'node') {
|
||||||
apiFetch(`nodes/${dragTarget.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: dragTarget.x, pos_y: dragTarget.y }) });
|
for (const c of canvasNodes) {
|
||||||
|
if (selectedNodeIds.includes(c.id)) {
|
||||||
|
apiFetch(`nodes/${c.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: c.x, pos_y: c.y }) });
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (dragTarget && (dragType === 'shape' || dragType === 'resize')) {
|
} else if (dragTarget && (dragType === 'shape' || dragType === 'resize')) {
|
||||||
apiFetch(`shapes/${dragTarget.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: dragTarget.x, pos_y: dragTarget.y, width: dragTarget.w, height: dragTarget.h }) });
|
apiFetch(`shapes/${dragTarget.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: dragTarget.x, pos_y: dragTarget.y, width: dragTarget.w, height: dragTarget.h }) });
|
||||||
|
} else if (dragType === 'select') {
|
||||||
|
selectRect = null;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const mx = e.clientX - rect.left - panX;
|
||||||
|
const my = e.clientY - rect.top - panY;
|
||||||
|
const rx = Math.min(selectStartX, mx);
|
||||||
|
const ry = Math.min(selectStartY, my);
|
||||||
|
const rw = Math.abs(mx - selectStartX);
|
||||||
|
const rh = Math.abs(my - selectStartY);
|
||||||
|
if (rw > 5 || rh > 5) {
|
||||||
|
const found = canvasNodes.filter(n =>
|
||||||
|
n.x >= rx && n.x <= rx + rw && n.y >= ry && n.y <= ry + rh
|
||||||
|
);
|
||||||
|
selectedNodeIds = found.map(n => n.id);
|
||||||
|
selectedNodeId = found.length > 0 ? found[0].id : null;
|
||||||
|
if (found.length > 0) selectNode(found[0].id);
|
||||||
|
else { selectedNodeId = null; selectedNodeIds = []; renderNodeList(); renderShapeList(); renderNetwork(); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dragType = null; dragTarget = null; dragOrig = null;
|
dragType = null; dragTarget = null; dragOrig = null;
|
||||||
|
selectRect = null;
|
||||||
isPanning = false;
|
isPanning = false;
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user