mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-30 23:03:03 -07:00
feat: Implement network topology management with API integration
- Added network-api.js for handling API calls related to network devices and nodes. - Introduced network-events.js to manage event handlers for node interactions and window resizing. - Created network-init.js for initializing network topology on page load and fetching device data. - Developed network-tabs.js for rendering network tabs and managing tab content. - Implemented network-tree.js for constructing and rendering the tree hierarchy of network devices. - Enhanced error handling and user feedback for API calls and data loading processes. - Included caching mechanisms for user preferences regarding device visibility.
This commit is contained in:
283
front/js/network-api.js
Normal file
283
front/js/network-api.js
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
// network-api.js
|
||||||
|
// API calls and data loading functions for network topology
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API token, waiting if necessary for settings to load
|
||||||
|
* @returns {string} The API token
|
||||||
|
*/
|
||||||
|
function getApiToken() {
|
||||||
|
let token = getSetting("API_TOKEN");
|
||||||
|
|
||||||
|
// If token is not yet available, log warning
|
||||||
|
if (!token || token.trim() === '') {
|
||||||
|
console.warn("API_TOKEN not yet loaded from settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load network nodes (network device types)
|
||||||
|
* Creates top-level tabs for each network device
|
||||||
|
*/
|
||||||
|
function loadNetworkNodes() {
|
||||||
|
// Create Top level tabs (List of network devices), explanation of the terminology below:
|
||||||
|
//
|
||||||
|
// Switch 1 (node)
|
||||||
|
// /(p1) \ (p2) <----- port numbers
|
||||||
|
// / \
|
||||||
|
// Smart TV (leaf) Switch 2 (node (for the PC) and leaf (for Switch 1))
|
||||||
|
// \
|
||||||
|
// PC (leaf) <------- leafs are not included in this SQL query
|
||||||
|
const rawSql = `
|
||||||
|
SELECT
|
||||||
|
parent.devName AS node_name,
|
||||||
|
parent.devMac AS node_mac,
|
||||||
|
parent.devPresentLastScan AS online,
|
||||||
|
parent.devType AS node_type,
|
||||||
|
parent.devParentMAC AS parent_mac,
|
||||||
|
parent.devIcon AS node_icon,
|
||||||
|
parent.devAlertDown AS node_alert,
|
||||||
|
COUNT(child.devMac) AS node_ports_count
|
||||||
|
FROM Devices AS parent
|
||||||
|
LEFT JOIN Devices AS child
|
||||||
|
/* CRITICAL FIX: COLLATE NOCASE ensures the join works
|
||||||
|
even if devParentMAC is uppercase and devMac is lowercase
|
||||||
|
*/
|
||||||
|
ON child.devParentMAC = parent.devMac COLLATE NOCASE
|
||||||
|
WHERE parent.devType IN (${networkDeviceTypes})
|
||||||
|
AND parent.devIsArchived = 0
|
||||||
|
GROUP BY parent.devMac, parent.devName, parent.devPresentLastScan,
|
||||||
|
parent.devType, parent.devParentMAC, parent.devIcon, parent.devAlertDown
|
||||||
|
ORDER BY parent.devName;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const apiBase = getApiBase();
|
||||||
|
const apiToken = getApiToken();
|
||||||
|
|
||||||
|
// Verify token is available
|
||||||
|
if (!apiToken || apiToken.trim() === '') {
|
||||||
|
console.error("API_TOKEN not available. Settings may not be loaded yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${apiBase}/dbquery/read`;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url,
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Authorization": `Bearer ${apiToken}` },
|
||||||
|
data: JSON.stringify({ rawSql: btoa(unescape(encodeURIComponent(rawSql))) }),
|
||||||
|
contentType: "application/json",
|
||||||
|
success: function(data) {
|
||||||
|
const nodes = data.results || [];
|
||||||
|
renderNetworkTabs(nodes);
|
||||||
|
loadUnassignedDevices();
|
||||||
|
checkTabsOverflow();
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error("Error loading network nodes:", status, error);
|
||||||
|
// Check if it's an auth error
|
||||||
|
if (xhr.status === 401) {
|
||||||
|
console.error("Authorization failed. API_TOKEN may be invalid or not yet loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load device table with configurable SQL and rendering
|
||||||
|
* @param {Object} options - Configuration object
|
||||||
|
* @param {string} options.sql - SQL query to fetch devices
|
||||||
|
* @param {string} options.containerSelector - jQuery selector for container
|
||||||
|
* @param {string} options.tableId - ID for DataTable instance
|
||||||
|
* @param {string} options.wrapperHtml - HTML wrapper for table
|
||||||
|
* @param {boolean} options.assignMode - Whether to show assign/unassign buttons
|
||||||
|
*/
|
||||||
|
function loadDeviceTable({ sql, containerSelector, tableId, wrapperHtml = null, assignMode = true }) {
|
||||||
|
const apiBase = getApiBase();
|
||||||
|
const apiToken = getApiToken();
|
||||||
|
|
||||||
|
// Verify token is available
|
||||||
|
if (!apiToken || apiToken.trim() === '') {
|
||||||
|
console.error("API_TOKEN not available. Settings may not be loaded yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${apiBase}/dbquery/read`;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url,
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Authorization": `Bearer ${apiToken}` },
|
||||||
|
data: JSON.stringify({ rawSql: btoa(unescape(encodeURIComponent(sql))) }),
|
||||||
|
contentType: "application/json",
|
||||||
|
success: function(data) {
|
||||||
|
const devices = data.results || [];
|
||||||
|
const $container = $(containerSelector);
|
||||||
|
|
||||||
|
// end if nothing to show
|
||||||
|
if(devices.length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$container.html(wrapperHtml);
|
||||||
|
|
||||||
|
const $table = $(`#${tableId}`);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: assignMode ? getString('Network_ManageAssign') : getString('Network_ManageUnassign'),
|
||||||
|
data: 'devMac',
|
||||||
|
orderable: false,
|
||||||
|
width: '5%',
|
||||||
|
render: function (mac) {
|
||||||
|
const label = assignMode ? 'assign' : 'unassign';
|
||||||
|
const btnClass = assignMode ? 'btn-primary' : 'btn-primary bg-red';
|
||||||
|
const btnText = assignMode ? getString('Network_ManageAssign') : getString('Network_ManageUnassign');
|
||||||
|
return `<button class="btn ${btnClass} btn-sm" data-myleafmac="${mac}" onclick="updateLeaf('${mac}','${label}')">
|
||||||
|
${btnText}
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: getString('Device_TableHead_Name'),
|
||||||
|
data: 'devName',
|
||||||
|
width: '15%',
|
||||||
|
render: function (name, type, device) {
|
||||||
|
return `<a href="./deviceDetails.php?mac=${device.devMac}" target="_blank">
|
||||||
|
<b class="anonymize">${name || '-'}</b>
|
||||||
|
</a>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: getString('Device_TableHead_Status'),
|
||||||
|
data: 'devStatus',
|
||||||
|
width: '15%',
|
||||||
|
render: function (_, type, device) {
|
||||||
|
const badge = getStatusBadgeParts(
|
||||||
|
device.devPresentLastScan,
|
||||||
|
device.devAlertDown,
|
||||||
|
device.devMac,
|
||||||
|
device.devStatus
|
||||||
|
);
|
||||||
|
return `<a href="${badge.url}" class="badge ${badge.cssClass}">${badge.iconHtml} ${badge.text}</a>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'MAC',
|
||||||
|
data: 'devMac',
|
||||||
|
width: '5%',
|
||||||
|
render: (data) => `<span class="anonymize">${data}</span>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: getString('Network_Table_IP'),
|
||||||
|
data: 'devLastIP',
|
||||||
|
width: '5%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: getString('Device_TableHead_Port'),
|
||||||
|
data: 'devParentPort',
|
||||||
|
width: '5%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: getString('Device_TableHead_Vendor'),
|
||||||
|
data: 'devVendor',
|
||||||
|
width: '20%'
|
||||||
|
}
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
tableConfig = {
|
||||||
|
data: devices,
|
||||||
|
columns: columns,
|
||||||
|
pageLength: 10,
|
||||||
|
order: assignMode ? [[2, 'asc']] : [],
|
||||||
|
responsive: true,
|
||||||
|
autoWidth: false,
|
||||||
|
searching: true,
|
||||||
|
createdRow: function (row, data) {
|
||||||
|
$(row).attr('data-mac', data.devMac);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($.fn.DataTable.isDataTable($table)) {
|
||||||
|
$table.DataTable(tableConfig).clear().rows.add(devices).draw();
|
||||||
|
} else {
|
||||||
|
$table.DataTable(tableConfig);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error("Error loading device table:", status, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load unassigned devices (devices without parent)
|
||||||
|
*/
|
||||||
|
function loadUnassignedDevices() {
|
||||||
|
const sql = `
|
||||||
|
SELECT devMac, devPresentLastScan, devName, devLastIP, devVendor, devAlertDown, devParentPort
|
||||||
|
FROM Devices
|
||||||
|
WHERE (devParentMAC IS NULL OR devParentMAC IN ("", " ", "undefined", "null"))
|
||||||
|
AND devMac NOT LIKE "%internet%"
|
||||||
|
AND devIsArchived = 0
|
||||||
|
ORDER BY devName ASC`;
|
||||||
|
|
||||||
|
const wrapperHtml = `
|
||||||
|
<div class="content">
|
||||||
|
<div id="unassignedDevices" class="box box-aqua box-body table-responsive">
|
||||||
|
<section>
|
||||||
|
<h5><i class="fa-solid fa-plug-circle-xmark"></i> ${getString('Network_UnassignedDevices')}</h5>
|
||||||
|
<table id="unassignedDevicesTable" class="table table-striped" width="100%"></table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
loadDeviceTable({
|
||||||
|
sql,
|
||||||
|
containerSelector: '#unassigned-devices-wrapper',
|
||||||
|
tableId: 'unassignedDevicesTable',
|
||||||
|
wrapperHtml,
|
||||||
|
assignMode: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load devices connected to a specific node
|
||||||
|
* @param {string} node_mac - MAC address of the parent node
|
||||||
|
*/
|
||||||
|
function loadConnectedDevices(node_mac) {
|
||||||
|
// Standardize the input just in case
|
||||||
|
const normalized_mac = node_mac.toLowerCase();
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT devName, devMac, devLastIP, devVendor, devPresentLastScan, devAlertDown, devParentPort, devVlan,
|
||||||
|
CASE
|
||||||
|
WHEN devIsNew = 1 THEN 'New'
|
||||||
|
WHEN devPresentLastScan = 1 THEN 'On-line'
|
||||||
|
WHEN devPresentLastScan = 0 AND devAlertDown != 0 THEN 'Down'
|
||||||
|
WHEN devIsArchived = 1 THEN 'Archived'
|
||||||
|
WHEN devPresentLastScan = 0 THEN 'Off-line'
|
||||||
|
ELSE 'Unknown status'
|
||||||
|
END AS devStatus
|
||||||
|
FROM Devices
|
||||||
|
/* Using COLLATE NOCASE here solves the 'TEXT' vs 'NOCASE' mismatch */
|
||||||
|
WHERE devParentMac = '${normalized_mac}' COLLATE NOCASE`;
|
||||||
|
|
||||||
|
// Keep the ID generation consistent
|
||||||
|
const id = normalized_mac.replace(/:/g, '_');
|
||||||
|
|
||||||
|
const wrapperHtml = `
|
||||||
|
<table class="table table-bordered table-striped node-leafs-table " id="table_leafs_${id}" data-node-mac="${normalized_mac}">
|
||||||
|
</table>`;
|
||||||
|
|
||||||
|
loadDeviceTable({
|
||||||
|
sql,
|
||||||
|
containerSelector: `#leafs_${id}`,
|
||||||
|
tableId: `table_leafs_${id}`,
|
||||||
|
wrapperHtml,
|
||||||
|
assignMode: false
|
||||||
|
});
|
||||||
|
}
|
||||||
133
front/js/network-events.js
Normal file
133
front/js/network-events.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// network-events.js
|
||||||
|
// Event handlers and tree node click interactions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle network node click - select correct tab and scroll to appropriate content
|
||||||
|
* @param {HTMLElement} el - The clicked element
|
||||||
|
*/
|
||||||
|
function handleNodeClick(el)
|
||||||
|
{
|
||||||
|
|
||||||
|
isNetworkDevice = $(el).data("devisnetworknodedynamic") == 1;
|
||||||
|
targetTabMAC = ""
|
||||||
|
thisDevMac= $(el).data("mac");
|
||||||
|
|
||||||
|
if (isNetworkDevice == false)
|
||||||
|
{
|
||||||
|
targetTabMAC = $(el).data("parentmac");
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
targetTabMAC = thisDevMac;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetTab = $(`a[data-mytabmac="${targetTabMAC}"]`);
|
||||||
|
|
||||||
|
if (targetTab.length) {
|
||||||
|
// Simulate a click event on the target tab
|
||||||
|
targetTab.click();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNetworkDevice) {
|
||||||
|
// Smooth scroll to the tab content
|
||||||
|
$('html, body').animate({
|
||||||
|
scrollTop: targetTab.offset().top - 50
|
||||||
|
}, 500); // Adjust the duration as needed
|
||||||
|
} else {
|
||||||
|
$("tr.selected").removeClass("selected");
|
||||||
|
$(`tr[data-mac="${thisDevMac}"]`).addClass("selected");
|
||||||
|
|
||||||
|
const tableId = "table_leafs_" + targetTabMAC.replace(/:/g, '_');
|
||||||
|
const $table = $(`#${tableId}`).DataTable();
|
||||||
|
|
||||||
|
// Find the row index (in the full data set) that matches
|
||||||
|
const rowIndex = $table
|
||||||
|
.rows()
|
||||||
|
.eq(0)
|
||||||
|
.filter(function(idx) {
|
||||||
|
return $table.row(idx).node().getAttribute("data-mac") === thisDevMac;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rowIndex.length > 0) {
|
||||||
|
// Change to the page where this row is
|
||||||
|
$table.page(Math.floor(rowIndex[0] / $table.page.len())).draw(false);
|
||||||
|
|
||||||
|
// Delay needed so the row is in the DOM after page draw
|
||||||
|
setTimeout(() => {
|
||||||
|
const rowNode = $table.row(rowIndex[0]).node();
|
||||||
|
$(rowNode).addClass("selected");
|
||||||
|
|
||||||
|
// Smooth scroll to the row
|
||||||
|
$('html, body').animate({
|
||||||
|
scrollTop: $(rowNode).offset().top - 50
|
||||||
|
}, 500);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle window resize events to recheck tab overflow
|
||||||
|
*/
|
||||||
|
let resizeTimeout;
|
||||||
|
$(window).on('resize', function () {
|
||||||
|
clearTimeout(resizeTimeout);
|
||||||
|
resizeTimeout = setTimeout(() => {
|
||||||
|
checkTabsOverflow();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize page on document ready
|
||||||
|
* Sets up toggle filters and event handlers
|
||||||
|
*/
|
||||||
|
$(document).ready(function () {
|
||||||
|
// Restore cached values on load
|
||||||
|
const cachedOffline = getCache('showOffline');
|
||||||
|
if (cachedOffline !== null) {
|
||||||
|
$('input[name="showOffline"]').prop('checked', cachedOffline === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedArchived = getCache('showArchived');
|
||||||
|
if (cachedArchived !== null) {
|
||||||
|
$('input[name="showArchived"]').prop('checked', cachedArchived === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to enable/disable showArchived based on showOffline
|
||||||
|
function updateArchivedToggle() {
|
||||||
|
const isOfflineChecked = $('input[name="showOffline"]').is(':checked');
|
||||||
|
const archivedToggle = $('input[name="showArchived"]');
|
||||||
|
|
||||||
|
if (!isOfflineChecked) {
|
||||||
|
archivedToggle.prop('checked', false);
|
||||||
|
archivedToggle.prop('disabled', true);
|
||||||
|
setCache('showArchived', false);
|
||||||
|
} else {
|
||||||
|
archivedToggle.prop('disabled', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state on load
|
||||||
|
updateArchivedToggle();
|
||||||
|
|
||||||
|
// Bind change event for both toggles
|
||||||
|
$('input[name="showOffline"], input[name="showArchived"]').on('change', function () {
|
||||||
|
const name = $(this).attr('name');
|
||||||
|
const value = $(this).is(':checked');
|
||||||
|
setCache(name, value);
|
||||||
|
|
||||||
|
// Update state of showArchived if showOffline changed
|
||||||
|
if (name === 'showOffline') {
|
||||||
|
updateArchivedToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh page after a brief delay to ensure cache is written
|
||||||
|
setTimeout(() => {
|
||||||
|
location.reload();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// init pop up hover boxes for device details
|
||||||
|
initHoverNodeInfo();
|
||||||
|
});
|
||||||
149
front/js/network-init.js
Normal file
149
front/js/network-init.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// network-init.js
|
||||||
|
// Main initialization and data loading logic for network topology
|
||||||
|
|
||||||
|
// Global variables needed by other modules
|
||||||
|
var networkDeviceTypes = "";
|
||||||
|
var showArchived = false;
|
||||||
|
var showOffline = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize network topology on page load
|
||||||
|
* Fetches all devices and sets up the tree visualization
|
||||||
|
*/
|
||||||
|
function initNetworkTopology() {
|
||||||
|
networkDeviceTypes = getSetting("NETWORK_DEVICE_TYPES").replace("[", "").replace("]", "");
|
||||||
|
showArchived = getCache('showArchived') === "true";
|
||||||
|
showOffline = getCache('showOffline') === "true";
|
||||||
|
|
||||||
|
console.log('showArchived:', showArchived);
|
||||||
|
console.log('showOffline:', showOffline);
|
||||||
|
|
||||||
|
// Always get all devices
|
||||||
|
const rawSql = `
|
||||||
|
SELECT *,
|
||||||
|
CASE
|
||||||
|
WHEN devAlertDown != 0 AND devPresentLastScan = 0 THEN "Down"
|
||||||
|
WHEN devPresentLastScan = 1 THEN "On-line"
|
||||||
|
ELSE "Off-line"
|
||||||
|
END AS devStatus,
|
||||||
|
CASE
|
||||||
|
WHEN devType IN (${networkDeviceTypes}) THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS devIsNetworkNodeDynamic
|
||||||
|
FROM Devices a
|
||||||
|
`;
|
||||||
|
|
||||||
|
const apiBase = getApiBase();
|
||||||
|
const apiToken = getApiToken();
|
||||||
|
|
||||||
|
// Verify token is available before making API call
|
||||||
|
if (!apiToken || apiToken.trim() === '') {
|
||||||
|
console.error("API_TOKEN not available. Settings may not be loaded yet. Retrying in 500ms...");
|
||||||
|
// Retry after a short delay to allow settings to load
|
||||||
|
setTimeout(() => {
|
||||||
|
initNetworkTopology();
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${apiBase}/dbquery/read`;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url,
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Authorization": `Bearer ${apiToken}` },
|
||||||
|
data: JSON.stringify({ rawSql: btoa(unescape(encodeURIComponent(rawSql))) }),
|
||||||
|
contentType: "application/json",
|
||||||
|
success: function(data) {
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
const allDevices = data.results || [];
|
||||||
|
|
||||||
|
console.log(allDevices);
|
||||||
|
|
||||||
|
if (!allDevices || allDevices.length === 0) {
|
||||||
|
showModalOK(getString('Gen_Warning'), getString('Network_NoDevices'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count totals for UI
|
||||||
|
let archivedCount = 0;
|
||||||
|
let offlineCount = 0;
|
||||||
|
|
||||||
|
allDevices.forEach(device => {
|
||||||
|
if (parseInt(device.devIsArchived) === 1) archivedCount++;
|
||||||
|
if (parseInt(device.devPresentLastScan) === 0 && parseInt(device.devIsArchived) === 0) offlineCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(archivedCount > 0)
|
||||||
|
{
|
||||||
|
$('#showArchivedNumber').text(`(${archivedCount})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(offlineCount > 0)
|
||||||
|
{
|
||||||
|
$('#showOfflineNumber').text(`(${offlineCount})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now apply UI filter based on toggles (always keep root)
|
||||||
|
const filteredDevices = allDevices.filter(device => {
|
||||||
|
const isRoot = (device.devMac || '').toLowerCase() === 'internet';
|
||||||
|
|
||||||
|
if (isRoot) return true;
|
||||||
|
if (!showArchived && parseInt(device.devIsArchived) === 1) return false;
|
||||||
|
if (!showOffline && parseInt(device.devPresentLastScan) === 0) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort filtered devices
|
||||||
|
const orderTopologyBy = createArray(getSetting("UI_TOPOLOGY_ORDER"));
|
||||||
|
const devicesSorted = filteredDevices.sort((a, b) => {
|
||||||
|
const parsePort = (port) => {
|
||||||
|
const parsed = parseInt(port, 10);
|
||||||
|
return isNaN(parsed) ? Infinity : parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (orderTopologyBy[0]) {
|
||||||
|
case "Name":
|
||||||
|
// ensuring string
|
||||||
|
const nameA = (a.devName ?? "").toString();
|
||||||
|
const nameB = (b.devName ?? "").toString();
|
||||||
|
const nameCompare = nameA.localeCompare(nameB);
|
||||||
|
return nameCompare !== 0
|
||||||
|
? nameCompare
|
||||||
|
: parsePort(a.devParentPort) - parsePort(b.devParentPort);
|
||||||
|
|
||||||
|
case "Port":
|
||||||
|
return parsePort(a.devParentPort) - parsePort(b.devParentPort);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return a.rowid - b.rowid;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setCache('devicesListNew', JSON.stringify(devicesSorted));
|
||||||
|
deviceListGlobal = devicesSorted;
|
||||||
|
|
||||||
|
// Render filtered result
|
||||||
|
initTree(getHierarchy());
|
||||||
|
loadNetworkNodes();
|
||||||
|
attachTreeEvents();
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error("Error loading topology data:", status, error);
|
||||||
|
if (xhr.status === 401) {
|
||||||
|
console.error("Authorization failed! API_TOKEN may be invalid. Check that API_TOKEN setting is correct and not empty.");
|
||||||
|
showMessage("Authorization Failed: API_TOKEN setting may be invalid or not loaded. Please refresh the page.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
$(document).ready(function () {
|
||||||
|
// show spinning icon
|
||||||
|
showSpinner();
|
||||||
|
|
||||||
|
// Start loading the network topology
|
||||||
|
initNetworkTopology();
|
||||||
|
});
|
||||||
259
front/js/network-tabs.js
Normal file
259
front/js/network-tabs.js
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
// network-tabs.js
|
||||||
|
// Tab management and tab content rendering functions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render network tabs from nodes
|
||||||
|
* @param {Array} nodes - Array of network node objects
|
||||||
|
*/
|
||||||
|
function renderNetworkTabs(nodes) {
|
||||||
|
let html = '';
|
||||||
|
nodes.forEach((node, i) => {
|
||||||
|
const iconClass = node.online == 1 ? "text-green" :
|
||||||
|
(node.node_alert == 1 ? "text-red" : "text-gray50");
|
||||||
|
|
||||||
|
const portLabel = node.node_ports_count ? ` (${node.node_ports_count})` : '';
|
||||||
|
const icon = atob(node.node_icon);
|
||||||
|
const id = node.node_mac.replace(/:/g, '_');
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<li class="networkNodeTabHeaders ${i === 0 ? 'active' : ''}">
|
||||||
|
<a href="#${id}" data-mytabmac="${node.node_mac}" id="${id}_id" data-toggle="tab" title="${node.node_name}">
|
||||||
|
<div class="icon ${iconClass}">${icon}</div>
|
||||||
|
<span class="node-name">${node.node_name}</span>${portLabel}
|
||||||
|
</a>
|
||||||
|
</li>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.nav-tabs').html(html);
|
||||||
|
|
||||||
|
// populate tabs
|
||||||
|
renderNetworkTabContent(nodes);
|
||||||
|
|
||||||
|
// init selected (first) tab
|
||||||
|
initTab();
|
||||||
|
|
||||||
|
// init selected node highlighting
|
||||||
|
initSelectedNodeHighlighting()
|
||||||
|
|
||||||
|
// Register events on tab change
|
||||||
|
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||||
|
initSelectedNodeHighlighting()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render content for each network tab
|
||||||
|
* @param {Array} nodes - Array of network node objects
|
||||||
|
*/
|
||||||
|
function renderNetworkTabContent(nodes) {
|
||||||
|
$('.tab-content').empty();
|
||||||
|
|
||||||
|
nodes.forEach((node, i) => {
|
||||||
|
const id = node.node_mac.replace(/:/g, '_');
|
||||||
|
|
||||||
|
const badge = getStatusBadgeParts(
|
||||||
|
node.online,
|
||||||
|
node.node_alert,
|
||||||
|
node.node_mac
|
||||||
|
);
|
||||||
|
|
||||||
|
const badgeHtml = `<a href="${badge.url}" class="badge ${badge.cssClass}">${badge.iconHtml} ${badge.status}</a>`;
|
||||||
|
const parentId = node.parent_mac.replace(/:/g, '_');
|
||||||
|
|
||||||
|
isRootNode = node.parent_mac == "";
|
||||||
|
|
||||||
|
const paneHtml = `
|
||||||
|
<div class="tab-pane box box-aqua box-body ${i === 0 ? 'active' : ''}" id="${id}">
|
||||||
|
<h5><i class="fa fa-server"></i> ${getString('Network_Node')}</h5>
|
||||||
|
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label class="col-sm-3 col-form-label fw-bold">${getString('DevDetail_Tab_Details')}</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<a href="./deviceDetails.php?mac=${node.node_mac}" target="_blank" class="anonymize">${node.node_name}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label class="col-sm-3 col-form-label fw-bold">MAC</label>
|
||||||
|
<div class="col-sm-9 anonymize">${node.node_mac}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label class="col-sm-3 col-form-label fw-bold">${getString('Device_TableHead_Type')}</label>
|
||||||
|
<div class="col-sm-9">${node.node_type}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label class="col-sm-3 col-form-label fw-bold">${getString('Device_TableHead_Status')}</label>
|
||||||
|
<div class="col-sm-9">${badgeHtml}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label class="col-sm-3 col-form-label fw-bold">${getString('Network_Parent')}</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
${isRootNode ? '' : `<a class="anonymize" href="#">`}
|
||||||
|
<span my-data-mac="${node.parent_mac}" data-mac="${node.parent_mac}" data-devIsNetworkNodeDynamic="1" onclick="handleNodeClick(this)">
|
||||||
|
${isRootNode ? getString('Network_Root') : getDevDataByMac(node.parent_mac, "devName")}
|
||||||
|
</span>
|
||||||
|
${isRootNode ? '' : `</a>`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<div class="box box-aqua box-body" id="connected">
|
||||||
|
<h5>
|
||||||
|
<i class="fa fa-sitemap fa-rotate-270"></i>
|
||||||
|
${getString('Network_Connected')}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div id="leafs_${id}" class="table-responsive"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$('.tab-content').append(paneHtml);
|
||||||
|
loadConnectedDevices(node.node_mac);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the active tab based on cache or query parameter
|
||||||
|
*/
|
||||||
|
function initTab()
|
||||||
|
{
|
||||||
|
key = "activeNetworkTab"
|
||||||
|
|
||||||
|
// default selection
|
||||||
|
selectedTab = "Internet_id"
|
||||||
|
|
||||||
|
// the #target from the url
|
||||||
|
target = getQueryString('mac')
|
||||||
|
|
||||||
|
// update cookie if target specified
|
||||||
|
if(target != "")
|
||||||
|
{
|
||||||
|
setCache(key, target.replaceAll(":","_")+'_id') // _id is added so it doesn't conflict with AdminLTE tab behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the tab id from the cookie (already overridden by the target)
|
||||||
|
if(!emptyArr.includes(getCache(key)))
|
||||||
|
{
|
||||||
|
selectedTab = getCache(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate panel
|
||||||
|
$('.nav-tabs a[id='+ selectedTab +']').tab('show');
|
||||||
|
|
||||||
|
// When changed save new current tab
|
||||||
|
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||||
|
setCache(key, $(e.target).attr('id'))
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight the currently selected node in the tree
|
||||||
|
*/
|
||||||
|
function initSelectedNodeHighlighting()
|
||||||
|
{
|
||||||
|
|
||||||
|
var currentNodeMac = $(".networkNodeTabHeaders.active a").data("mytabmac");
|
||||||
|
|
||||||
|
// change highlighted node in the tree
|
||||||
|
selNode = $("#networkTree .highlightedNode")[0]
|
||||||
|
|
||||||
|
console.log(selNode)
|
||||||
|
|
||||||
|
if(selNode)
|
||||||
|
{
|
||||||
|
$(selNode).attr('class', $(selNode).attr('class').replace('highlightedNode'))
|
||||||
|
}
|
||||||
|
|
||||||
|
newSelNode = $("#networkTree div[data-mac='"+currentNodeMac+"']")[0]
|
||||||
|
|
||||||
|
console.log(newSelNode)
|
||||||
|
|
||||||
|
$(newSelNode).attr('class', $(newSelNode).attr('class') + ' highlightedNode')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a device's network assignment
|
||||||
|
* @param {string} leafMac - MAC address of device to update
|
||||||
|
* @param {string} action - 'assign' or 'unassign'
|
||||||
|
*/
|
||||||
|
function updateLeaf(leafMac, action) {
|
||||||
|
console.log(leafMac); // child
|
||||||
|
console.log(action); // action
|
||||||
|
|
||||||
|
const nodeMac = $(".networkNodeTabHeaders.active a").data("mytabmac") || "";
|
||||||
|
|
||||||
|
if (action === "assign") {
|
||||||
|
if (!nodeMac) {
|
||||||
|
showMessage(getString("Network_Cant_Assign_No_Node_Selected"));
|
||||||
|
} else if (leafMac.toLowerCase().includes("internet")) {
|
||||||
|
showMessage(getString("Network_Cant_Assign"));
|
||||||
|
} else {
|
||||||
|
saveData("updateNetworkLeaf", leafMac, nodeMac);
|
||||||
|
setTimeout(() => location.reload(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (action === "unassign") {
|
||||||
|
saveData("updateNetworkLeaf", leafMac, "");
|
||||||
|
setTimeout(() => location.reload(), 500);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.warn("Unknown action:", action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically show/hide tab names based on available space
|
||||||
|
* Hides tab names when tabs overflow, shows them again when space is available
|
||||||
|
*/
|
||||||
|
function checkTabsOverflow() {
|
||||||
|
const $ul = $('.nav-tabs');
|
||||||
|
const $lis = $ul.find('li');
|
||||||
|
|
||||||
|
// First measure widths with current state
|
||||||
|
let totalWidth = 0;
|
||||||
|
$lis.each(function () {
|
||||||
|
totalWidth += $(this).outerWidth(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ulWidth = $ul.width();
|
||||||
|
const isOverflowing = totalWidth > ulWidth;
|
||||||
|
|
||||||
|
if (isOverflowing) {
|
||||||
|
if (!$ul.hasClass('hide-node-names')) {
|
||||||
|
$ul.addClass('hide-node-names');
|
||||||
|
|
||||||
|
// Re-check: did hiding fix it?
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
let newTotal = 0;
|
||||||
|
$lis.each(function () {
|
||||||
|
newTotal += $(this).outerWidth(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newTotal > $ul.width()) {
|
||||||
|
// Still overflowing — do nothing, keep class
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($ul.hasClass('hide-node-names')) {
|
||||||
|
$ul.removeClass('hide-node-names');
|
||||||
|
|
||||||
|
// Re-check: did un-hiding break it?
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
let newTotal = 0;
|
||||||
|
$lis.each(function () {
|
||||||
|
newTotal += $(this).outerWidth(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newTotal > $ul.width()) {
|
||||||
|
// Oops, that broke it — re-hide
|
||||||
|
$ul.addClass('hide-node-names');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
346
front/js/network-tree.js
Normal file
346
front/js/network-tree.js
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
// network-tree.js
|
||||||
|
// Tree hierarchy construction and rendering functions
|
||||||
|
|
||||||
|
// Global state variables
|
||||||
|
var leafNodesCount = 0;
|
||||||
|
var visibleNodesCount = 0;
|
||||||
|
var parentNodesCount = 0;
|
||||||
|
var hiddenMacs = []; // hidden children
|
||||||
|
var hiddenChildren = [];
|
||||||
|
var deviceListGlobal = null;
|
||||||
|
var myTree;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively get children nodes and build a tree
|
||||||
|
* @param {Object} node - Current node
|
||||||
|
* @param {Array} list - Full device list
|
||||||
|
* @param {string} path - Path to current node
|
||||||
|
* @param {Array} visited - Visited nodes (for cycle detection)
|
||||||
|
* @returns {Object} Tree node with children
|
||||||
|
*/
|
||||||
|
function getChildren(node, list, path, visited = [])
|
||||||
|
{
|
||||||
|
var children = [];
|
||||||
|
|
||||||
|
// Check for infinite recursion by seeing if the node has been visited before
|
||||||
|
if (visited.includes(node.devMac.toLowerCase())) {
|
||||||
|
console.error("Infinite recursion detected at node:", node.devMac);
|
||||||
|
write_notification("[ERROR] ⚠ Infinite recursion detected. You probably have assigned the Internet node to another children node or to itself. Please open a new issue on GitHub and describe how you did it.", 'interrupt')
|
||||||
|
return { error: "Infinite recursion detected", node: node.devMac };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add current node to visited list
|
||||||
|
visited.push(node.devMac.toLowerCase());
|
||||||
|
|
||||||
|
// Loop through all items to find children of the current node
|
||||||
|
for (var i in list) {
|
||||||
|
const item = list[i];
|
||||||
|
const parentMac = item.devParentMAC?.toLowerCase() || ""; // null-safe
|
||||||
|
const nodeMac = node.devMac?.toLowerCase() || ""; // null-safe
|
||||||
|
|
||||||
|
if (parentMac != "" && parentMac == nodeMac && !hiddenMacs.includes(parentMac)) {
|
||||||
|
|
||||||
|
visibleNodesCount++;
|
||||||
|
|
||||||
|
// Process children recursively, passing a copy of the visited list
|
||||||
|
children.push(getChildren(list[i], list, path + ((path == "") ? "" : '|') + parentMac, visited));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track leaf and parent node counts
|
||||||
|
if (children.length == 0) {
|
||||||
|
leafNodesCount++;
|
||||||
|
} else {
|
||||||
|
parentNodesCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(node);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: node.devName,
|
||||||
|
path: path,
|
||||||
|
mac: node.devMac,
|
||||||
|
port: node.devParentPort,
|
||||||
|
id: node.devMac,
|
||||||
|
parentMac: node.devParentMAC,
|
||||||
|
icon: node.devIcon,
|
||||||
|
type: node.devType,
|
||||||
|
devIsNetworkNodeDynamic: node.devIsNetworkNodeDynamic,
|
||||||
|
vendor: node.devVendor,
|
||||||
|
lastseen: node.devLastConnection,
|
||||||
|
firstseen: node.devFirstConnection,
|
||||||
|
ip: node.devLastIP,
|
||||||
|
status: node.devStatus,
|
||||||
|
presentLastScan: node.devPresentLastScan,
|
||||||
|
alertDown: node.devAlertDown,
|
||||||
|
hasChildren: children.length > 0 || hiddenMacs.includes(node.devMac),
|
||||||
|
relType: node.devParentRelType,
|
||||||
|
devVlan: node.devVlan,
|
||||||
|
devSSID: node.devSSID,
|
||||||
|
hiddenChildren: hiddenMacs.includes(node.devMac),
|
||||||
|
qty: children.length,
|
||||||
|
children: children
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build complete hierarchy starting from the Internet node
|
||||||
|
* @returns {Object} Root hierarchy object
|
||||||
|
*/
|
||||||
|
function getHierarchy()
|
||||||
|
{
|
||||||
|
// reset counters before rebuilding the hierarchy
|
||||||
|
leafNodesCount = 0;
|
||||||
|
visibleNodesCount = 0;
|
||||||
|
parentNodesCount = 0;
|
||||||
|
|
||||||
|
let internetNode = null;
|
||||||
|
|
||||||
|
for(i in deviceListGlobal)
|
||||||
|
{
|
||||||
|
if(deviceListGlobal[i].devMac.toLowerCase() == 'internet')
|
||||||
|
{
|
||||||
|
internetNode = deviceListGlobal[i];
|
||||||
|
|
||||||
|
return (getChildren(internetNode, deviceListGlobal, ''))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!internetNode) {
|
||||||
|
showModalOk(
|
||||||
|
getString('Network_Configuration_Error'),
|
||||||
|
getString('Network_Root_Not_Configured')
|
||||||
|
);
|
||||||
|
console.error("getHierarchy(): Internet node not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle collapse/expand state of a subtree
|
||||||
|
* @param {string} parentMac - MAC address of parent node to toggle
|
||||||
|
* @param {string} treePath - Path in tree (colon-separated)
|
||||||
|
*/
|
||||||
|
function toggleSubTree(parentMac, treePath)
|
||||||
|
{
|
||||||
|
treePath = treePath.split('|')
|
||||||
|
|
||||||
|
parentMac = parentMac.toLowerCase()
|
||||||
|
|
||||||
|
if(!hiddenMacs.includes(parentMac))
|
||||||
|
{
|
||||||
|
hiddenMacs.push(parentMac)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
removeItemFromArray(hiddenMacs, parentMac)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedTree = getHierarchy()
|
||||||
|
myTree.refresh(updatedTree);
|
||||||
|
|
||||||
|
// re-attach any onclick events
|
||||||
|
attachTreeEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach click events to tree collapse/expand controls
|
||||||
|
*/
|
||||||
|
function attachTreeEvents()
|
||||||
|
{
|
||||||
|
// toggle subtree functionality
|
||||||
|
$("div[data-mytreemac]").each(function(){
|
||||||
|
$(this).attr('onclick', 'toggleSubTree("'+$(this).attr('data-mytreemac')+'","'+ $(this).attr('data-mytreepath')+'")')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert pixels to em units
|
||||||
|
* @param {number} px - Pixel value
|
||||||
|
* @param {HTMLElement} element - Reference element for font-size
|
||||||
|
* @returns {number} Value in em units
|
||||||
|
*/
|
||||||
|
function pxToEm(px, element) {
|
||||||
|
var baseFontSize = parseFloat($(element || "body").css("font-size"));
|
||||||
|
return px / baseFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert em units to pixels
|
||||||
|
* @param {number} em - Value in em units
|
||||||
|
* @param {HTMLElement} element - Reference element for font-size
|
||||||
|
* @returns {number} Value in pixels (rounded)
|
||||||
|
*/
|
||||||
|
function emToPx(em, element) {
|
||||||
|
var baseFontSize = parseFloat($(element || "body").css("font-size"));
|
||||||
|
return Math.round(em * baseFontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize tree visualization
|
||||||
|
* @param {Object} myHierarchy - Hierarchy object to render
|
||||||
|
*/
|
||||||
|
function initTree(myHierarchy)
|
||||||
|
{
|
||||||
|
if(myHierarchy && myHierarchy.type !== "")
|
||||||
|
{
|
||||||
|
// calculate the drawing area based on the tree width and available screen size
|
||||||
|
let baseFontSize = parseFloat($('html').css('font-size'));
|
||||||
|
let treeAreaHeight = ($(window).height() - 155); ;
|
||||||
|
let minNodeWidth = 60 // min safe node width not breaking the tree
|
||||||
|
|
||||||
|
// calculate the font size of the leaf nodes to fit everything into the tree area
|
||||||
|
leafNodesCount == 0 ? 1 : leafNodesCount;
|
||||||
|
|
||||||
|
emSize = pxToEm((treeAreaHeight/(leafNodesCount)).toFixed(2));
|
||||||
|
|
||||||
|
// let screenWidthEm = pxToEm($('.networkTable').width()-15);
|
||||||
|
let minTreeWidthPx = parentNodesCount * minNodeWidth;
|
||||||
|
let actualWidthPx = $('.networkTable').width() - 15;
|
||||||
|
|
||||||
|
let finalWidthPx = Math.max(actualWidthPx, minTreeWidthPx);
|
||||||
|
|
||||||
|
// override original value
|
||||||
|
let screenWidthEm = pxToEm(finalWidthPx);
|
||||||
|
|
||||||
|
// handle canvas and node size if only a few nodes
|
||||||
|
emSize > 1 ? emSize = 1 : emSize = emSize;
|
||||||
|
|
||||||
|
let nodeHeightPx = emToPx(emSize*1);
|
||||||
|
let nodeWidthPx = emToPx(screenWidthEm / (parentNodesCount));
|
||||||
|
|
||||||
|
// handle if only a few nodes
|
||||||
|
nodeWidthPx > 160 ? nodeWidthPx = 160 : nodeWidthPx = nodeWidthPx;
|
||||||
|
if (nodeWidthPx < minNodeWidth) nodeWidthPx = minNodeWidth; // minimum safe width
|
||||||
|
|
||||||
|
console.log("Calculated nodeWidthPx =", nodeWidthPx, "emSize =", emSize , " screenWidthEm:", screenWidthEm, " emToPx(screenWidthEm):" , emToPx(screenWidthEm));
|
||||||
|
|
||||||
|
// init the drawing area size
|
||||||
|
$("#networkTree").attr('style', `height:${treeAreaHeight}px; width:${emToPx(screenWidthEm)}px`)
|
||||||
|
|
||||||
|
console.log(Treeviz);
|
||||||
|
|
||||||
|
myTree = Treeviz.create({
|
||||||
|
htmlId: "networkTree",
|
||||||
|
renderNode: nodeData => {
|
||||||
|
|
||||||
|
(!emptyArr.includes(nodeData.data.port )) ? port = nodeData.data.port : port = "";
|
||||||
|
|
||||||
|
(port == "" || port == 0 || port == 'None' ) ? portBckgIcon = `<i class="fa fa-wifi"></i>` : portBckgIcon = `<i class="fa fa-ethernet"></i>`;
|
||||||
|
|
||||||
|
portHtml = (port == "" || port == 0 || port == 'None' ) ? "   " : port;
|
||||||
|
|
||||||
|
// Build HTML for individual nodes in the network diagram
|
||||||
|
deviceIcon = (!emptyArr.includes(nodeData.data.icon )) ?
|
||||||
|
`<div class="netIcon">
|
||||||
|
${atob(nodeData.data.icon)}
|
||||||
|
</div>` : "";
|
||||||
|
devicePort = `<div class="netPort"
|
||||||
|
style="width:${emSize}em;height:${emSize}em">
|
||||||
|
${portHtml}</div>
|
||||||
|
<div class="portBckgIcon"
|
||||||
|
style="margin-left:-${emSize*0.7}em;">
|
||||||
|
${portBckgIcon}
|
||||||
|
</div>`;
|
||||||
|
collapseExpandIcon = nodeData.data.hiddenChildren ?
|
||||||
|
"square-plus" : "square-minus";
|
||||||
|
|
||||||
|
// generate +/- icon if node has children nodes
|
||||||
|
collapseExpandHtml = nodeData.data.hasChildren ?
|
||||||
|
`<div class="netCollapse"
|
||||||
|
style="font-size:${nodeHeightPx/2}px;top:${Math.floor(nodeHeightPx / 4)}px"
|
||||||
|
data-mytreepath="${nodeData.data.path}"
|
||||||
|
data-mytreemac="${nodeData.data.mac}">
|
||||||
|
<i class="fa fa-${collapseExpandIcon} pointer"></i>
|
||||||
|
</div>` : "";
|
||||||
|
|
||||||
|
selectedNodeMac = $(".nav-tabs-custom .active a").attr('data-mytabmac')
|
||||||
|
|
||||||
|
highlightedCss = nodeData.data.mac == selectedNodeMac ?
|
||||||
|
" highlightedNode " : "";
|
||||||
|
cssNodeType = nodeData.data.devIsNetworkNodeDynamic ?
|
||||||
|
" node-network-device " : " node-standard-device ";
|
||||||
|
|
||||||
|
networkHardwareIcon = nodeData.data.devIsNetworkNodeDynamic ? `<span class="network-hw-icon">
|
||||||
|
<i class="fa-solid fa-hard-drive"></i>
|
||||||
|
</span>` : "";
|
||||||
|
|
||||||
|
const badgeConf = getStatusBadgeParts(nodeData.data.presentLastScan, nodeData.data.alertDown, nodeData.data.mac, statusText = '')
|
||||||
|
|
||||||
|
return result = `<div
|
||||||
|
class="node-inner hover-node-info box pointer ${highlightedCss} ${cssNodeType}"
|
||||||
|
style="height:${nodeHeightPx}px;font-size:${nodeHeightPx-5}px;"
|
||||||
|
onclick="handleNodeClick(this)"
|
||||||
|
data-mac="${nodeData.data.mac}"
|
||||||
|
data-parentMac="${nodeData.data.parentMac}"
|
||||||
|
data-name="${nodeData.data.name}"
|
||||||
|
data-ip="${nodeData.data.ip}"
|
||||||
|
data-mac="${nodeData.data.mac}"
|
||||||
|
data-vendor="${nodeData.data.vendor}"
|
||||||
|
data-type="${nodeData.data.type}"
|
||||||
|
data-devIsNetworkNodeDynamic="${nodeData.data.devIsNetworkNodeDynamic}"
|
||||||
|
data-lastseen="${nodeData.data.lastseen}"
|
||||||
|
data-firstseen="${nodeData.data.firstseen}"
|
||||||
|
data-relationship="${nodeData.data.relType}"
|
||||||
|
data-status="${nodeData.data.status}"
|
||||||
|
data-present="${nodeData.data.presentLastScan}"
|
||||||
|
data-alert="${nodeData.data.alertDown}"
|
||||||
|
data-icon="${nodeData.data.icon}"
|
||||||
|
>
|
||||||
|
<div class="netNodeText">
|
||||||
|
<strong><span>${devicePort} <span class="${badgeConf.cssText}">${deviceIcon}</span></span>
|
||||||
|
<span class="spanNetworkTree anonymizeDev" style="width:${nodeWidthPx-50}px">${nodeData.data.name}</span>
|
||||||
|
${networkHardwareIcon}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${collapseExpandHtml}`;
|
||||||
|
},
|
||||||
|
mainAxisNodeSpacing: 'auto',
|
||||||
|
// secondaryAxisNodeSpacing: 0.3,
|
||||||
|
nodeHeight: nodeHeightPx,
|
||||||
|
nodeWidth: nodeWidthPx,
|
||||||
|
marginTop: '5',
|
||||||
|
isHorizontal : true,
|
||||||
|
hasZoom: true,
|
||||||
|
hasPan: true,
|
||||||
|
marginLeft: '10',
|
||||||
|
marginRight: '10',
|
||||||
|
idKey: "mac",
|
||||||
|
hasFlatData: false,
|
||||||
|
relationnalField: "children",
|
||||||
|
linkLabel: {
|
||||||
|
render: (parent, child) => {
|
||||||
|
// Return text or HTML to display on the connection line
|
||||||
|
connectionLabel = (child?.data.devVlan ?? "") + "/" + (child?.data.devSSID ?? "");
|
||||||
|
if(connectionLabel == "/")
|
||||||
|
{
|
||||||
|
connectionLabel = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return connectionLabel;
|
||||||
|
// or with HTML:
|
||||||
|
// return "<tspan><strong>reports to</strong></tspan>";
|
||||||
|
},
|
||||||
|
color: "#336c87ff", // Label text color (optional)
|
||||||
|
fontSize: nodeHeightPx - 5 // Label font size in px (optional)
|
||||||
|
},
|
||||||
|
linkWidth: (nodeData) => 2,
|
||||||
|
linkColor: (nodeData) => {
|
||||||
|
relConf = getRelationshipConf(nodeData.data.relType)
|
||||||
|
return relConf.color;
|
||||||
|
}
|
||||||
|
// onNodeClick: (nodeData) => handleNodeClick(nodeData),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(deviceListGlobal);
|
||||||
|
myTree.refresh(myHierarchy);
|
||||||
|
|
||||||
|
// hide spinning icon
|
||||||
|
hideSpinner()
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
console.error("getHierarchy() not returning expected result");
|
||||||
|
}
|
||||||
|
}
|
||||||
1063
front/network.php
1063
front/network.php
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user