cleanup and styling
|
@ -8,19 +8,50 @@ function initializeCytoscape() {
|
|||
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: {
|
||||
'background-color': '#007bff',
|
||||
'label': 'data(name)', // Ensure the label uses the AgentName
|
||||
'color': 'white',
|
||||
'width' : 50,
|
||||
'height' : 50,
|
||||
'label' : 'data(name)',
|
||||
'text-outline-width' : 2,
|
||||
'text-outline-color' : '#333',
|
||||
'width': '50px',
|
||||
'height': '50px'
|
||||
'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: {
|
||||
|
@ -29,16 +60,17 @@ function initializeCytoscape() {
|
|||
'target-arrow-color': '#ccc',
|
||||
'target-arrow-shape': 'triangle'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.darkTheme',
|
||||
style : { 'background-color': '#212529' }
|
||||
}
|
||||
],
|
||||
|
||||
layout: {
|
||||
name: 'grid',
|
||||
rows: 2
|
||||
}
|
||||
layout: { name: 'grid', rows: 2 }
|
||||
});
|
||||
|
||||
isCyInitialized = true; // Mark Cytoscape as initialized
|
||||
isCyInitialized = true;
|
||||
}
|
||||
|
||||
// Load the graph after the page has fully loaded
|
||||
|
@ -58,107 +90,57 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
|
||||
async function updateGraph(agentData) {
|
||||
if (!cy) {
|
||||
console.error('Cytoscape is not initialized yet.');
|
||||
console.error('Cytoscape not initialised');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('Updating graph with agent data:', agentData);
|
||||
cy.elements().remove(); // clear existing
|
||||
|
||||
// Clear existing nodes and edges
|
||||
cy.elements().remove();
|
||||
|
||||
// Add nodes for each agent with the AgentName as the label
|
||||
/* --- add agent nodes ------------------------------------------- */
|
||||
agentData.forEach(agent => {
|
||||
const id = agent.agentId;
|
||||
const name = agent.agentName;
|
||||
const status = agent.status;
|
||||
const online = agent.status === 'Connected';
|
||||
|
||||
if (id && name) {
|
||||
// let nodeColor = (status === 'Connected') ? '#28a745' : '#dc3545'; // Green for connected, Red for disconnected
|
||||
let nodeBg = (status === 'Connected') ? 'url(static/computer-online.svg)' : 'url(static/computer-offline.svg)'; // Green for connected, Red for disconnected
|
||||
if (!id || !name) return;
|
||||
|
||||
cy.add({
|
||||
group : 'nodes',
|
||||
data : {
|
||||
id : id,
|
||||
name : name,
|
||||
status: status,
|
||||
type: agent.agentType,
|
||||
ip: agent.IPv4Address
|
||||
type : 'Agent',
|
||||
ip : agent.IPv4Address,
|
||||
status: agent.status
|
||||
},
|
||||
style: {
|
||||
// 'background-color': '#f8f9fa',
|
||||
'background-opacity': '0',
|
||||
'background-image': nodeBg,
|
||||
'background-fit': 'contain',
|
||||
'background-clip': 'none',
|
||||
'label': name, // Display agent's name
|
||||
'color': 'white',
|
||||
'text-outline-width': 2,
|
||||
'text-outline-color': '#333',
|
||||
'width': '50px',
|
||||
'height': '50px'
|
||||
}
|
||||
classes: online ? 'online' : 'offline' // ← only classes
|
||||
});
|
||||
} else {
|
||||
console.warn('Skipping agent with missing data:', agent);
|
||||
}
|
||||
});
|
||||
|
||||
// Define the target node (`g2` in your case)
|
||||
const targetNode = 'g2';
|
||||
|
||||
// Ensure the target node (`g2`) exists, if not, create it
|
||||
if (cy.getElementById(targetNode).length === 0) {
|
||||
/* --- ensure target/server node exists --------------------------- */
|
||||
const targetId = 'g2';
|
||||
if (cy.getElementById(targetId).length === 0) {
|
||||
cy.add({
|
||||
group : 'nodes',
|
||||
data: {
|
||||
id: targetNode,
|
||||
name: 'g2',
|
||||
status: 'Target',
|
||||
type: 'Server',
|
||||
ip: 'N/A'
|
||||
},
|
||||
style: {
|
||||
// 'background-color': '#6c757d', // Gray for target node
|
||||
'background-opacity': '0',
|
||||
'background-image': 'url(static/server-online.svg)',
|
||||
'background-fit': 'contain',
|
||||
'background-clip': 'none',
|
||||
'label': 'g2',
|
||||
'color': 'white',
|
||||
'text-outline-width': 2,
|
||||
'text-outline-color': '#333',
|
||||
'width': '50px',
|
||||
'height': '50px'
|
||||
}
|
||||
data : { id: targetId, name: 'g2' },
|
||||
classes: 'server'
|
||||
});
|
||||
}
|
||||
|
||||
// Connect each agent to the target node (`g2`)
|
||||
/* --- connect every agent to the target ------------------------- */
|
||||
agentData.forEach(agent => {
|
||||
const id = agent.agentId;
|
||||
if (id) {
|
||||
if (agent.agentId) {
|
||||
cy.add({
|
||||
group: 'edges',
|
||||
data: {
|
||||
source: id,
|
||||
target: targetNode
|
||||
}
|
||||
data : { source: agent.agentId, target: targetId }
|
||||
});
|
||||
} else {
|
||||
console.warn('Skipping edge for agent with missing agentId:', agent);
|
||||
}
|
||||
});
|
||||
|
||||
// Force a layout update
|
||||
cy.layout({
|
||||
name: 'grid',
|
||||
rows: 2
|
||||
}).run();
|
||||
/* --- relayout --------------------------------------------------- */
|
||||
cy.layout({ name: 'grid', rows: 2 }).run();
|
||||
}
|
||||
|
||||
|
||||
async function fetchData() {
|
||||
const url = "http://localhost:3333/agents";
|
||||
try {
|
||||
|
@ -176,7 +158,7 @@ async function fetchData() {
|
|||
|
||||
// Function to get agent data and update the graph
|
||||
async function loadGraphData() {
|
||||
console.log("Function loadGraphData()");
|
||||
// console.log("Function loadGraphData()");
|
||||
|
||||
// Fetch agent data asynchronously
|
||||
agentData = await fetchData();
|
||||
|
|
|
@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.target.id === "agentList") {
|
||||
restoreCheckboxState();
|
||||
updateAgentDropdown();
|
||||
updateAgentStatus();
|
||||
bindRowClicks();
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
restoreCheckboxState();
|
||||
|
||||
themeToggle();
|
||||
focusCommandInput();
|
||||
});
|
||||
|
||||
let cachedAgentNames = '';
|
||||
|
@ -83,6 +84,7 @@ function restoreCheckboxState() {
|
|||
});
|
||||
}
|
||||
|
||||
// Because of this function, you can click anywhere on the row to select it
|
||||
function bindRowClicks() {
|
||||
const rows = document.querySelectorAll('#agentList tbody tr');
|
||||
rows.forEach(row => {
|
||||
|
@ -101,7 +103,9 @@ function bindRowClicks() {
|
|||
});
|
||||
}
|
||||
|
||||
function updateAgentDropdown() {
|
||||
|
||||
// Colorize connection status inside agent list
|
||||
function updateAgentStatus() {
|
||||
const select = document.getElementById("agentName");
|
||||
const optionValues = Array.from(select.options).map(opt => opt.value);
|
||||
const rows = document.querySelectorAll("#agentList tbody tr");
|
||||
|
@ -130,6 +134,12 @@ function updateAgentDropdown() {
|
|||
});
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Toggle them icon from sun/moon on button click
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function themeToggle() {
|
||||
const body = document.body;
|
||||
const toggleBtn = document.getElementById('themeToggleButton');
|
||||
|
@ -139,10 +149,10 @@ function themeToggle() {
|
|||
body.classList.remove('bg-light', 'text-dark', 'bg-dark', 'text-light');
|
||||
if (theme === 'dark') {
|
||||
body.classList.add('bg-dark', 'text-light');
|
||||
toggleBtn.innerHTML = '<i class="bi bi-moon"></i>';
|
||||
toggleBtn.innerHTML = '☽'; // Could switch to the colored version 🌙
|
||||
} else {
|
||||
body.classList.add('bg-light', 'text-dark');
|
||||
toggleBtn.innerHTML = '<i class="bi bi-sun"></i>';
|
||||
toggleBtn.innerHTML = '☼'; // Could switch to the colored version ☀
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,14 +181,28 @@ function styleCommandOutput() {
|
|||
// Map lines: if contains "Error", wrap in red span, else plain text
|
||||
lines = lines.map(line => {
|
||||
if (line.includes("ERROR")) {
|
||||
return `<span style="color: red;">${line}</span>`;
|
||||
return `<span style="color: var(--bs-danger);">${line}</span>`;
|
||||
} else if (/^\[\S*\]$/.test(line)) {
|
||||
return `<span style="color: blue;">${line}</span>`;
|
||||
return `<span style="color: var(--bs-primary); font-size: 16px">${line}</span>`;
|
||||
} else {
|
||||
return `<span style="font-size: 16px">${line}</span>`;
|
||||
return `<span style="font-size: 16px; line-height: 1em;">${line}</span>`;
|
||||
}
|
||||
});
|
||||
|
||||
// Replace innerHTML with the joined lines, joined by <br> for line breaks in HTML
|
||||
commandOutput.innerHTML = lines.join('<br>');
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Focus on modal command inpu
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function focusCommandInput() {
|
||||
// const modalEl = document.getElementById('exampleModal');
|
||||
var modalElement = document.getElementById('exampleModal')
|
||||
modalElement.addEventListener('shown.bs.modal', event => {
|
||||
document.getElementById('modalCommand').focus();
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,11 +15,12 @@
|
|||
overflow-y: auto; /* enables vertical scroll when content overflows */
|
||||
border: 1px solid #fff; /* optional: for visual clarity */
|
||||
padding: 10px; /* optional: spacing inside the container */
|
||||
line-height: 1em;
|
||||
/* background-color: #f8f9fa; /1* optional: subtle background for log readability *1/ */
|
||||
}
|
||||
|
||||
.log-info, .log-warning, .log-error, .log-fatal, .log-debug{ font-family: "Lucida Console", Monaco, monospace;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.log-info {
|
||||
|
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 655 B |
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 652 B |
Before Width: | Height: | Size: 674 B After Width: | Height: | Size: 674 B |
Before Width: | Height: | Size: 673 B After Width: | Height: | Size: 673 B |
|
@ -4,13 +4,11 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link href="static/bootstrap/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href="static/gontrol-stylesheet.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<!-- Include Cytoscape.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.23/dist/cytoscape.min.js"></script>
|
||||
<script type="text/javascript" src="static/bootstrap/bootstrap.bundle.min.js"></script>
|
||||
<script type="text/javascript" src="static/htmx/htmx.org@1.9.12.js"></script>
|
||||
<script type="text/javascript" src="static/cytoscape.min.js"></script>
|
||||
<script type="text/javascript" src="static/agents-graph.js"></script>
|
||||
<script type="text/javascript" src="static/gontrol-helper.js"></script>
|
||||
|
||||
|
@ -24,12 +22,12 @@
|
|||
<body class="bg-light text-dark" data-bs-theme="light">
|
||||
<button id="themeToggleButton"
|
||||
class="btn btn-outline-secondary position-fixed"
|
||||
style="top: 1rem; right: 1rem; z-index: 1;"
|
||||
style="top: 1rem; right: 1.5rem; z-index: 1; border-color: var(--bs-tertiary-color);"
|
||||
aria-label="Toggle Theme">
|
||||
<i class="bi bi-sun"></i>
|
||||
☀
|
||||
</button>
|
||||
|
||||
<div class="container-fluid px-4 py-3 pb-5">
|
||||
<div class="container-fluid px-4 py-4 pb-5">
|
||||
<div class="row g-2">
|
||||
<!-- Agent List -->
|
||||
<div class="col-12 col-md-6">
|
||||
|
@ -121,7 +119,7 @@
|
|||
<select id="modalAgentName"
|
||||
class="form-select d-none"
|
||||
name="agentName"
|
||||
hx-on="htmx:afterSwap:updateAgentDropdown">
|
||||
hx-on="htmx:afterSwap:updateAgentStatus">
|
||||
<option value="" disabled selected>Select an Agent</option>
|
||||
</select>
|
||||
</form>
|
||||
|
@ -147,7 +145,7 @@
|
|||
<select id="agentName"
|
||||
class="form-select d-none"
|
||||
name="agentName"
|
||||
hx-on="htmx:afterSwap:updateAgentDropdown">
|
||||
hx-on="htmx:afterSwap:updateAgentStatus">
|
||||
<option value="" disabled selected>Select an Agent</option>
|
||||
</select>
|
||||
</form>
|
||||
|
|
|
@ -228,7 +228,8 @@ func (wsh webSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
|
|||
logger.InsertLog(logger.Error, fmt.Sprintf("Error reading from agent %s: %v", agentName, err))
|
||||
break
|
||||
}
|
||||
log.Printf("Message from agent %s: %s", agentName, message)
|
||||
// log.Printf("Message from agent %s: %s", agentName, message)
|
||||
log.Printf("Message from agent %s received", agentName)
|
||||
logger.InsertLog(logger.Debug, fmt.Sprintf("Message from agent %s: %s", agentName, message))
|
||||
|
||||
if ch, ok := responseChannels.Load(agentName); ok {
|
||||
|
|