+28
-77
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user