This commit is contained in:
@@ -294,3 +294,14 @@ kbd {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Network toolbar */
|
||||
.toolbar-node-item:hover,
|
||||
.toolbar-shape-item:hover {
|
||||
filter: brightness(1.3);
|
||||
}
|
||||
|
||||
.toolbar-node-item:active,
|
||||
.toolbar-shape-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
+113
-1
@@ -149,6 +149,7 @@ function initApp() {
|
||||
document.getElementById('saveShape').addEventListener('click', saveShape);
|
||||
document.getElementById('saveLinkDocs').addEventListener('click', saveLinkDocs);
|
||||
document.getElementById('addUserBtn').addEventListener('click', addUser);
|
||||
document.getElementById('addUserBtn').addEventListener('click', addUser);
|
||||
document.getElementById('logoutBtn').addEventListener('click', logout);
|
||||
document.getElementById('teamFilter').addEventListener('change', renderTimeline);
|
||||
document.getElementById('tagFilter').addEventListener('change', renderTimeline);
|
||||
@@ -725,6 +726,9 @@ async function loadNetworkData() {
|
||||
populateNodeSelects();
|
||||
renderNodeList();
|
||||
renderShapeList();
|
||||
renderNodeToolbar();
|
||||
setupCanvasDrop();
|
||||
startSync();
|
||||
if (shapes.length) nextShapeZ = Math.max(...shapes.map(x => x.z_index)) + 1;
|
||||
buildCanvasGraph();
|
||||
renderNetwork();
|
||||
@@ -1143,6 +1147,111 @@ function getShapeHandles(s) {
|
||||
];
|
||||
}
|
||||
|
||||
function renderNodeToolbar() {
|
||||
const bar = document.getElementById('nodeToolbar');
|
||||
if (!bar) return;
|
||||
const iconMap = [
|
||||
{ type: 'host', icon: 'fa-desktop', color: '#3b82f6', label: 'Host' },
|
||||
{ type: 'server', icon: 'fa-server', color: '#8b5cf6', label: 'Server' },
|
||||
{ type: 'router', icon: 'fa-route', color: '#f59e0b', label: 'Router' },
|
||||
{ type: 'firewall', icon: 'fa-shield-halved', color: '#ef4444', label: 'Firewall' },
|
||||
{ type: 'switch', icon: 'fa-network-wired', color: '#06b6d4', label: 'Switch' },
|
||||
{ type: 'cloud', icon: 'fa-cloud', color: '#22c55e', label: 'Cloud' },
|
||||
{ type: 'endpoint', icon: 'fa-laptop', color: '#ec4899', label: 'Endpoint' },
|
||||
{ type: 'other', icon: 'fa-circle', color: '#6b7280', label: 'Other' },
|
||||
];
|
||||
bar.innerHTML = '<span class="text-secondary me-1 small" style="font-size:.7rem;line-height:26px;">Drag to canvas:</span>';
|
||||
iconMap.forEach(t => {
|
||||
const el = document.createElement('span');
|
||||
el.className = 'toolbar-node-item d-inline-flex align-items-center gap-1 px-2 py-1 rounded';
|
||||
el.draggable = true;
|
||||
el.dataset.nodeType = t.type;
|
||||
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.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('text/plain', t.type);
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
});
|
||||
bar.appendChild(el);
|
||||
});
|
||||
bar.innerHTML += '<span class="toolbar-shape-item d-inline-flex align-items-center gap-1 px-2 py-1 rounded ms-1" draggable="true" data-shape="rectangle" style="cursor:grab;font-size:.75rem;color:#3b82f6;background:#3b82f612;border:1px solid #3b82f630;"><i class="fas fa-vector-square" style="font-size:.75rem;"></i><span>Box</span></span>';
|
||||
bar.innerHTML += '<span class="toolbar-shape-item d-inline-flex align-items-center gap-1 px-2 py-1 rounded ms-1" draggable="true" data-shape="ellipse" style="cursor:grab;font-size:.75rem;color:#3b82f6;background:#3b82f612;border:1px solid #3b82f630;"><i class="fas fa-circle" style="font-size:.75rem;"></i><span>Ellipse</span></span>';
|
||||
|
||||
bar.querySelectorAll('.toolbar-shape-item').forEach(el => {
|
||||
el.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('text/plain', 'shape:' + el.dataset.shape);
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupCanvasDrop() {
|
||||
const wrapper = document.getElementById('networkCanvasWrapper');
|
||||
wrapper.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; });
|
||||
wrapper.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
const data = e.dataTransfer.getData('text/plain');
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left - panX;
|
||||
const my = e.clientY - rect.top - panY;
|
||||
|
||||
if (data.startsWith('shape:')) {
|
||||
const shapeType = data.split(':')[1];
|
||||
await apiFetch('shapes', { method: 'POST', body: JSON.stringify({
|
||||
label: shapeType === 'rectangle' ? 'Box' : 'Ellipse',
|
||||
shape_type: shapeType,
|
||||
pos_x: mx - 100, pos_y: my - 75,
|
||||
width: 200, height: 150,
|
||||
color: '#1e3a5f', border_color: '#3b82f6',
|
||||
opacity: 0.15, z_index: nextShapeZ++
|
||||
})});
|
||||
loadNetworkData();
|
||||
} else {
|
||||
await apiFetch('nodes', { method: 'POST', body: JSON.stringify({
|
||||
label: data.charAt(0).toUpperCase() + data.slice(1),
|
||||
ip_address: '', node_type: data, status: 'unknown',
|
||||
group_name: 'default', notes: '',
|
||||
pos_x: mx, pos_y: my
|
||||
})});
|
||||
loadNetworkData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Real-time sync: poll backend every 5 seconds only when network tab is visible
|
||||
let syncInterval = null;
|
||||
let lastSyncData = null;
|
||||
|
||||
function startSync() {
|
||||
if (syncInterval) return;
|
||||
syncInterval = setInterval(async () => {
|
||||
const netTab = document.getElementById('network-tab');
|
||||
if (!netTab || !netTab.classList.contains('active')) return;
|
||||
try {
|
||||
const [n, l, s] = await Promise.all([
|
||||
apiFetch('nodes'), apiFetch('links'), apiFetch('shapes')
|
||||
]);
|
||||
const hash = JSON.stringify({ n, l, s });
|
||||
if (hash !== lastSyncData) {
|
||||
lastSyncData = hash;
|
||||
nodes = Array.isArray(n) ? n : [];
|
||||
links = Array.isArray(l) ? l : [];
|
||||
shapes = Array.isArray(s) ? s : [];
|
||||
populateNodeSelects();
|
||||
renderNodeList();
|
||||
renderShapeList();
|
||||
if (shapes.length) nextShapeZ = Math.max(...shapes.map(x => x.z_index)) + 1;
|
||||
buildCanvasGraph();
|
||||
renderNetwork();
|
||||
}
|
||||
} catch (e) {}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function stopSync() {
|
||||
if (syncInterval) { clearInterval(syncInterval); syncInterval = null; }
|
||||
}
|
||||
|
||||
function getCanvasNodeAt(mx, my) {
|
||||
return canvasNodes.find(n => Math.hypot(mx - n.x, my - n.y) < 28);
|
||||
}
|
||||
@@ -1248,7 +1357,10 @@ function onMouseMove(e) {
|
||||
renderNetwork(); return;
|
||||
}
|
||||
if (isPanning) {
|
||||
panX = e.clientX - panStartX; panY = e.clientY - panStartY;
|
||||
panX += e.clientX - panStartX;
|
||||
panY += e.clientY - panStartY;
|
||||
panStartX = e.clientX;
|
||||
panStartY = e.clientY;
|
||||
renderNetwork(); return;
|
||||
}
|
||||
const mx = e.clientX - rect.left - panX;
|
||||
|
||||
+7
-4
@@ -90,7 +90,7 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<h4><i class="fas fa-project-diagram text-primary"></i> Network Map</h4>
|
||||
<small class="text-secondary">Drag nodes & shapes · Resize via corner handles · <kbd class="bg-dark text-secondary">Del</kbd> to delete</small>
|
||||
<small class="text-secondary">Drag nodes & shapes · Resize via corner handles · <kbd class="bg-dark text-secondary">Del</kbd> to delete · Right-click to pan</small>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#nodeModal">
|
||||
@@ -108,10 +108,13 @@
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-body p-0" id="networkCanvasWrapper" style="height: 70vh; position: relative; overflow: hidden;">
|
||||
<div class="card-body p-0" id="networkCanvasWrapper" style="height: 65vh; position: relative; overflow: hidden;">
|
||||
<canvas id="networkCanvas" style="width: 100%; height: 100%;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1 mt-1 p-1 rounded" style="background:#0d1117;border:1px solid var(--neptune-border);" id="nodeToolbar">
|
||||
<span class="text-secondary me-1 small" style="font-size:.7rem;line-height:26px;">Drag to canvas:</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-dark border-secondary mb-2">
|
||||
@@ -138,10 +141,10 @@
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="nodesPanel">
|
||||
<div class="list-group list-group-flush bg-dark border-secondary rounded" id="nodeList" style="max-height: 35vh; overflow-y: auto;"></div>
|
||||
<div class="list-group list-group-flush bg-dark border-secondary rounded" id="nodeList" style="max-height: 30vh; overflow-y: auto;"></div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="shapesPanel">
|
||||
<div class="list-group list-group-flush bg-dark border-secondary rounded" id="shapeList" style="max-height: 35vh; overflow-y: auto;"></div>
|
||||
<div class="list-group list-group-flush bg-dark border-secondary rounded" id="shapeList" style="max-height: 30vh; overflow-y: auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user