let agentData = []; let isCyInitialized = false; let cy; function initializeCytoscape() { if (isCyInitialized) return; // cytoscape.use(cxtmenu); cy = cytoscape({ container: document.getElementById('cyto-graph'), /* --- single, central stylesheet -------------------------------- */ style: [ /* base node look (shared) */ /* background-opacity: 0 will remove the node color and background of the svg */ { selector: 'node', style: { 'width' : 50, 'height' : 50, 'label' : 'data(name)', 'text-outline-width' : 2, 'text-outline-color' : '#333', 'color' : '#fff', 'background-opacity' : 1, 'background-color': '#f8f9fa' } }, /* agent nodes — online / offline variants */ { selector: 'node.online[type = "Agent"]', style: { 'background-image' : 'url(static/img/computer-online.svg)', 'background-fit' : 'contain' } }, { selector: 'node.offline[type = "Agent"]', style: { 'background-image' : 'url(static/img/computer-offline.svg)', 'background-fit' : 'contain' } }, /* target / server node */ { selector: 'node.server', style: { 'background-image' : 'url(static/img/server-online.svg)', 'background-fit' : 'contain' } }, /* edge style */ { selector: 'edge', style: { 'width' : 3, 'line-color' : '#ccc', 'target-arrow-color': '#ccc', 'target-arrow-shape': 'triangle', 'curve-style': 'round-segments' } }, { selector: 'node.darkTheme', style : { 'background-color': '#212529' } } ], layout: { name: 'grid', rows: 2 } }); isCyInitialized = true; } // Load the graph after the page has fully loaded document.addEventListener('DOMContentLoaded', function () { initializeCytoscape(); loadGraphData(); document.body.addEventListener('htmx:afterSwap', function (event) { if (event.target.id === 'agentList') { // initializeCytoscape(); loadGraphData(); } }); }); // Load the graph after HTMX swap async function updateGraph(agentData) { if (!cy) { console.error('Cytoscape not initialised'); return; } cy.elements().remove(); // clear existing /* --- add agent nodes ------------------------------------------- */ agentData.forEach(agent => { const id = agent.agentId; const name = agent.agentName; const online = agent.status === 'Connected'; if (!id || !name) return; cy.add({ group : 'nodes', data : { id : id, name : name, type : 'Agent', ip : agent.IPv4Address, status: agent.status }, classes: online ? 'online' : 'offline' // ← only classes }); }); /* --- ensure target/server node exists --------------------------- */ const targetId = 'g2'; if (cy.getElementById(targetId).length === 0) { cy.add({ group : 'nodes', data : { id: targetId, name: 'g2' }, classes: 'server' }); } /* --- connect every agent to the target ------------------------- */ agentData.forEach(agent => { if (agent.agentId) { cy.add({ group: 'edges', data : { source: agent.agentId, target: targetId } }); } }); /* --- relayout --------------------------------------------------- */ cy.layout({ name: 'grid', rows: 2 }).run(); /* --- CxtMenu --- */ if (window.nodeContextMenu) { window.nodeContextMenu.destroy(); } window.nodeContextMenu = cy.cxtmenu({ selector: 'node', commands: [ { content: 'Details', select: function (ele) { const data = ele.data(); alert(`Node Details:\n\nName: ${data.name}\nID: ${data.id}\nType: ${data.type}\nIP: ${data.ip}`); } }, { content: 'Ping', select: function(ele) { const ip = ele.data().ip; console.log(`Ping to ${ip} triggered!`); } } ], menuRadius: 100, fillColor: `rgba(0, 0, 0, 0.75)`, activeFillColor: `rgba(255, 255, 255, 0.3)`, activePadding: 10, indicatorSize: 18, separatorWidth: 3, spotlightPadding: 4, minSpotlightRadius: 20, maxSpotlightRadius: 40, openMenuEvents: 'cxttap', }); window.edgeContextMenu = cy.cxtmenu({ selector: 'edge', commands: [ { content: 'View Info', select: function (ele) { const source = ele.source().data().name; const target = ele.target().data().name; alert(`Edge from "${source}" to "${target}"`); } }, { content: 'Delete Edge', select: function (ele) { ele.remove(); } } ], menuRadius: 100, }); } async function fetchData() { const url = "/agents"; try { const response = await fetch(url, {headers: {Accept: 'application/json'}}); if (!response.ok) { throw new Error(`Response status: ${response.status}`); } const data = await response.json(); return data; // Return the fetched data } catch (error) { console.error(error.message); return []; // Return an empty array on error } } // Function to get agent data and update the graph async function loadGraphData() { // console.log("Function loadGraphData()"); var agentDataCurrent = "" if (agentData.length > 0) { agentDataCurrent = JSON.stringify(agentData); } // Fetch agent data asynchronously agentData = await fetchData(); console.log('Extracted agent data:', agentData); // Only update the graph if agent data is available if (JSON.stringify(agentData) != agentDataCurrent) { // if (agentData && agentData.length > 0) { console.log("updating graph"); await updateGraph(agentData); } else { console.log('No agent data found or extracted.'); } }