making shapes selectable
Deploy / deploy (push) Successful in 38s

This commit is contained in:
2026-05-12 10:48:46 +02:00
parent d1e974be59
commit 8e44b5fbd5
+28 -77
View File
@@ -732,7 +732,7 @@ async function loadNetworkData() {
renderNodeList();
renderShapeList();
renderNodeToolbar();
setupCanvasDrop();
setupCanvasClickCreate();
startSync();
if (shapes.length) nextShapeZ = Math.max(...shapes.map(x => x.z_index)) + 1;
buildCanvasGraph();
@@ -1167,39 +1167,37 @@ function renderNodeToolbar() {
{ type: 'shape:rectangle', icon: 'fa-vector-square', color: '#3b82f6', label: 'Box', isShape: true },
{ type: 'shape:ellipse', icon: 'fa-circle', color: '#3b82f6', label: 'Ellipse', isShape: true },
];
bar.innerHTML = '<span class="text-secondary me-1 small" style="font-size:.7rem;line-height:26px;">Drag to canvas:</span>';
bar.innerHTML = '<span class="text-secondary me-1 small" style="font-size:.7rem;line-height:26px;">Click item then click canvas:</span>';
iconMap.forEach(t => {
const el = document.createElement('span');
el.className = 'd-inline-flex align-items-center gap-1 px-2 py-1 rounded';
el.draggable = true;
el.style.cssText = 'cursor:grab;font-size:.75rem;color:' + t.color + ';background:' + t.color + '12;border:1px solid ' + t.color + '30;';
el.style.cssText = 'cursor:pointer;font-size:.75rem;color:' + t.color + ';background:' + t.color + '12;border:1px solid ' + t.color + '30;';
el.innerHTML = '<i class="fas ' + t.icon + '" style="font-size:.75rem;"></i><span>' + t.label + '</span>';
el.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', t.type);
e.dataTransfer.effectAllowed = 'copy';
el.addEventListener('click', () => {
bar.querySelectorAll('.d-inline-flex').forEach(s => s.style.outline = 'none');
el.style.outline = '2px solid ' + t.color;
pendingCanvasCreate = t.type;
});
bar.appendChild(el);
});
}
function setupCanvasDrop() {
let pendingCanvasCreate = null;
function setupCanvasClickCreate() {
const wrapper = document.getElementById('networkCanvasWrapper');
let dropPending = false;
wrapper.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; });
wrapper.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
if (dropPending) return;
dropPending = true;
suppressNextMouse = Date.now() + 500;
const data = e.dataTransfer.getData('text/plain');
if (!data) { dropPending = false; return; }
const bar = document.getElementById('nodeToolbar');
wrapper.addEventListener('click', async (e) => {
if (!pendingCanvasCreate) return;
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left - panX;
const my = e.clientY - rect.top - panY;
const type = pendingCanvasCreate;
pendingCanvasCreate = null;
bar.querySelectorAll('.d-inline-flex').forEach(s => s.style.outline = 'none');
if (data.startsWith('shape:')) {
const shapeType = data.split(':')[1];
if (type.startsWith('shape:')) {
const shapeType = type.split(':')[1];
await apiFetch('shapes', { method: 'POST', body: JSON.stringify({
label: shapeType === 'rectangle' ? 'Box' : 'Ellipse',
shape_type: shapeType,
@@ -1210,67 +1208,16 @@ function setupCanvasDrop() {
})});
} else {
await apiFetch('nodes', { method: 'POST', body: JSON.stringify({
label: data.charAt(0).toUpperCase() + data.slice(1),
ip_address: '', node_type: data || 'host', status: 'unknown',
label: type.charAt(0).toUpperCase() + type.slice(1),
ip_address: '', node_type: type || 'host', status: 'unknown',
group_name: 'default', notes: '',
pos_x: mx, pos_y: my
})});
}
await loadNetworkData();
dropPending = false;
});
}
// Real-time sync: poll backend every 5 seconds for all data
let syncInterval = null;
let syncHashes = { events: null, documents: null, network: null };
function startSync() {
if (syncInterval) return;
syncInterval = setInterval(async () => {
if (dragType || dragTarget || isPanning) return;
const openModals = document.querySelectorAll('.modal.show');
try {
const [eventsData, docsData, nodesData, linksData, shapesData] = await Promise.all([
apiFetch('events'),
apiFetch('documents'),
apiFetch('nodes'),
apiFetch('links'),
apiFetch('shapes'),
]);
const eventsHash = JSON.stringify(eventsData);
if (eventsHash !== syncHashes.events) {
syncHashes.events = eventsHash;
events = Array.isArray(eventsData) ? eventsData : [];
loadAllTags();
renderTimeline();
}
const docsHash = JSON.stringify(docsData);
if (docsHash !== syncHashes.documents) {
syncHashes.documents = docsHash;
documents = Array.isArray(docsData) ? docsData : [];
renderDocuments();
}
const netHash = JSON.stringify({ n: nodesData, l: linksData, s: shapesData });
if (netHash !== syncHashes.network) {
syncHashes.network = netHash;
nodes = Array.isArray(nodesData) ? nodesData : [];
links = Array.isArray(linksData) ? linksData : [];
shapes = Array.isArray(shapesData) ? shapesData : [];
populateNodeSelects();
renderNodeList();
renderShapeList();
if (shapes.length) nextShapeZ = Math.max(...shapes.map(x => x.z_index)) + 1;
buildCanvasGraph();
renderNetwork();
}
} catch (e) {}
}, 5000);
}
function getCanvasNodeAt(mx, my) {
return canvasNodes.find(n => Math.hypot(mx - n.x, my - n.y) < 28);
}
@@ -1416,10 +1363,14 @@ function onMouseUp(e) {
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;
selectedShapeId = null;
const foundNodes = canvasNodes.filter(n => n.x >= rx && n.x <= rx + rw && n.y >= ry && n.y <= ry + rh);
const foundShapes = canvasShapes.filter(s => {
const sx = s.x, sy = s.y, ex = s.x + s.w, ey = s.y + s.h;
return sx < rx + rw && ex > rx && sy < ry + rh && ey > ry;
});
selectedNodeIds = foundNodes.map(n => n.id);
selectedNodeId = foundNodes.length > 0 ? foundNodes[0].id : null;
selectedShapeId = foundShapes.length > 0 ? foundShapes[foundShapes.length - 1].id : null;
renderNodeList(); renderShapeList(); renderNetwork();
}
}