+28
-77
@@ -732,7 +732,7 @@ async function loadNetworkData() {
|
|||||||
renderNodeList();
|
renderNodeList();
|
||||||
renderShapeList();
|
renderShapeList();
|
||||||
renderNodeToolbar();
|
renderNodeToolbar();
|
||||||
setupCanvasDrop();
|
setupCanvasClickCreate();
|
||||||
startSync();
|
startSync();
|
||||||
if (shapes.length) nextShapeZ = Math.max(...shapes.map(x => x.z_index)) + 1;
|
if (shapes.length) nextShapeZ = Math.max(...shapes.map(x => x.z_index)) + 1;
|
||||||
buildCanvasGraph();
|
buildCanvasGraph();
|
||||||
@@ -1167,39 +1167,37 @@ function renderNodeToolbar() {
|
|||||||
{ type: 'shape:rectangle', icon: 'fa-vector-square', color: '#3b82f6', label: 'Box', isShape: true },
|
{ 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 },
|
{ 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 => {
|
iconMap.forEach(t => {
|
||||||
const el = document.createElement('span');
|
const el = document.createElement('span');
|
||||||
el.className = 'd-inline-flex align-items-center gap-1 px-2 py-1 rounded';
|
el.className = 'd-inline-flex align-items-center gap-1 px-2 py-1 rounded';
|
||||||
el.draggable = true;
|
el.style.cssText = 'cursor:pointer;font-size:.75rem;color:' + t.color + ';background:' + t.color + '12;border:1px solid ' + t.color + '30;';
|
||||||
el.style.cssText = 'cursor:grab;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.innerHTML = '<i class="fas ' + t.icon + '" style="font-size:.75rem;"></i><span>' + t.label + '</span>';
|
||||||
el.addEventListener('dragstart', (e) => {
|
el.addEventListener('click', () => {
|
||||||
e.dataTransfer.setData('text/plain', t.type);
|
bar.querySelectorAll('.d-inline-flex').forEach(s => s.style.outline = 'none');
|
||||||
e.dataTransfer.effectAllowed = 'copy';
|
el.style.outline = '2px solid ' + t.color;
|
||||||
|
pendingCanvasCreate = t.type;
|
||||||
});
|
});
|
||||||
bar.appendChild(el);
|
bar.appendChild(el);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupCanvasDrop() {
|
let pendingCanvasCreate = null;
|
||||||
|
|
||||||
|
function setupCanvasClickCreate() {
|
||||||
const wrapper = document.getElementById('networkCanvasWrapper');
|
const wrapper = document.getElementById('networkCanvasWrapper');
|
||||||
let dropPending = false;
|
const bar = document.getElementById('nodeToolbar');
|
||||||
wrapper.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; });
|
wrapper.addEventListener('click', async (e) => {
|
||||||
wrapper.addEventListener('drop', async (e) => {
|
if (!pendingCanvasCreate) return;
|
||||||
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 rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const mx = e.clientX - rect.left - panX;
|
const mx = e.clientX - rect.left - panX;
|
||||||
const my = e.clientY - rect.top - panY;
|
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:')) {
|
if (type.startsWith('shape:')) {
|
||||||
const shapeType = data.split(':')[1];
|
const shapeType = type.split(':')[1];
|
||||||
await apiFetch('shapes', { method: 'POST', body: JSON.stringify({
|
await apiFetch('shapes', { method: 'POST', body: JSON.stringify({
|
||||||
label: shapeType === 'rectangle' ? 'Box' : 'Ellipse',
|
label: shapeType === 'rectangle' ? 'Box' : 'Ellipse',
|
||||||
shape_type: shapeType,
|
shape_type: shapeType,
|
||||||
@@ -1210,67 +1208,16 @@ function setupCanvasDrop() {
|
|||||||
})});
|
})});
|
||||||
} else {
|
} else {
|
||||||
await apiFetch('nodes', { method: 'POST', body: JSON.stringify({
|
await apiFetch('nodes', { method: 'POST', body: JSON.stringify({
|
||||||
label: data.charAt(0).toUpperCase() + data.slice(1),
|
label: type.charAt(0).toUpperCase() + type.slice(1),
|
||||||
ip_address: '', node_type: data || 'host', status: 'unknown',
|
ip_address: '', node_type: type || 'host', status: 'unknown',
|
||||||
group_name: 'default', notes: '',
|
group_name: 'default', notes: '',
|
||||||
pos_x: mx, pos_y: my
|
pos_x: mx, pos_y: my
|
||||||
})});
|
})});
|
||||||
}
|
}
|
||||||
await loadNetworkData();
|
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) {
|
function getCanvasNodeAt(mx, my) {
|
||||||
return canvasNodes.find(n => Math.hypot(mx - n.x, my - n.y) < 28);
|
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 rw = Math.abs(mx - selectStartX);
|
||||||
const rh = Math.abs(my - selectStartY);
|
const rh = Math.abs(my - selectStartY);
|
||||||
if (rw > 5 || rh > 5) {
|
if (rw > 5 || rh > 5) {
|
||||||
const found = canvasNodes.filter(n => n.x >= rx && n.x <= rx + rw && n.y >= ry && n.y <= ry + rh);
|
const foundNodes = canvasNodes.filter(n => n.x >= rx && n.x <= rx + rw && n.y >= ry && n.y <= ry + rh);
|
||||||
selectedNodeIds = found.map(n => n.id);
|
const foundShapes = canvasShapes.filter(s => {
|
||||||
selectedNodeId = found.length > 0 ? found[0].id : null;
|
const sx = s.x, sy = s.y, ex = s.x + s.w, ey = s.y + s.h;
|
||||||
selectedShapeId = null;
|
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();
|
renderNodeList(); renderShapeList(); renderNetwork();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user