Files
NetAlertX/front/network.php
2025-08-10 20:08:09 +10:00

1023 lines
33 KiB
PHP
Executable File

<?php
require 'php/templates/header.php';
require 'php/templates/modals.php';
?>
<script>
// show spinning icon
showSpinner()
</script>
<!-- Page ------------------------------------------------------------------ -->
<div class="content-wrapper">
<span class="helpIcon">
<a target="_blank" href="https://github.com/jokob-sk/NetAlertX/blob/main/docs/NETWORK_TREE.md">
<i class="fa fa-circle-question"></i>
</a>
</span>
<div id="toggleFilters" class="">
<div class="checkbox icheck col-xs-12">
<label>
<input type="checkbox" name="showOffline" checked>
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
<?= lang('Network_ShowOffline');?>
<span id="showOfflineNumber">
<!-- placeholder -->
</span>
</div>
</label>
</div>
<div class="checkbox icheck col-xs-12">
<label>
<input type="checkbox" name="showArchived">
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
<?= lang('Network_ShowArchived');?>
<span id="showArchivedNumber">
<!-- placeholder -->
</span>
</div>
</label>
</div>
</div>
<div id="networkTree" class="drag">
<!-- Tree topology Placeholder -->
</div>
<!-- Main content ---------------------------------------------------------- -->
<section class="content networkTable">
<!-- /.content -->
<div class="nav-tabs-custom">
<ul class="nav nav-tabs">
<!-- Placeholder -->
</ul>
</div>
<div class="tab-content">
<!-- Placeholder -->
</div>
</section>
<section id="unassigned-devices-wrapper">
<!-- Placeholder -->
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
<!-- ----------------------------------------------------------------------- -->
<?php
require 'php/templates/footer.php';
?>
<script src="lib/treeviz/bundle.js"></script>
<script defer>
// -----------------------------------------------------------------------
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 node_name, node_mac, online, node_type, node_ports_count, parent_mac, node_icon, node_alert
FROM (
SELECT a.devName as node_name, a.devMac as node_mac, a.devPresentLastScan as online,
a.devType as node_type, a.devParentMAC as parent_mac, a.devIcon as node_icon, a.devAlertDown as node_alert
FROM Devices a
WHERE a.devType IN (${networkDeviceTypes}) and a.devIsArchived = 0
) t1
LEFT JOIN (
SELECT b.devParentMAC as node_mac_2, count() as node_ports_count
FROM Devices b
WHERE b.devParentMAC NOT NULL
GROUP BY b.devParentMAC
) t2
ON (t1.node_mac = t2.node_mac_2)
`;
const apiUrl = `php/server/dbHelper.php?action=read&rawSql=${btoa(encodeURIComponent(rawSql))}`;
$.get(apiUrl, function (data) {
const nodes = JSON.parse(data);
renderNetworkTabs(nodes);
loadUnassignedDevices();
checkTabsOverflow();
});
}
// -----------------------------------------------------------------------
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()
});
}
// -----------------------------------------------------------------------
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') : getNameByMacAddress(node.parent_mac)}
</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);
});
}
// ----------------------------------------------------
function loadDeviceTable({ sql, containerSelector, tableId, wrapperHtml = null, assignMode = true }) {
const apiUrl = `php/server/dbHelper.php?action=read&rawSql=${btoa(encodeURIComponent(sql))}`;
$.get(apiUrl, function (data) {
const devices = JSON.parse(data);
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);
}
});
}
// ----------------------------------------------------
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
});
}
// ----------------------------------------------------
function loadConnectedDevices(node_mac) {
const sql = `
SELECT devName, devMac, devLastIP, devVendor, devPresentLastScan, devAlertDown, devParentPort,
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
WHERE devParentMac = '${node_mac}'`;
const id = node_mac.replace(/:/g, '_');
const wrapperHtml = `
<table class="table table-bordered table-striped node-leafs-table " id="table_leafs_${id}" data-node-mac="${node_mac}">
</table>`;
loadDeviceTable({
sql,
containerSelector: `#leafs_${id}`,
tableId: `table_leafs_${id}`,
wrapperHtml,
assignMode: false
});
}
// -----------------------------------------------------------
// INIT
// -----------------------------------------------------------
const networkDeviceTypes = getSetting("NETWORK_DEVICE_TYPES").replace("[", "").replace("]", "");
const showArchived = getCache('showArchived') === "true";
const 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 apiUrl = `php/server/dbHelper.php?action=read&rawSql=${btoa(encodeURIComponent(rawSql))}`;
$.get(apiUrl, function (data) {
console.log(data);
const parsed = JSON.parse(data);
const allDevices = parsed;
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
const filteredDevices = allDevices.filter(device => {
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":
const nameCompare = a.devName.localeCompare(b.devName);
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();
});
</script>
<script defer>
// ---------------------------------------------------------------------------
// Tree functionality
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
var leafNodesCount = 0;
var visibleNodesCount = 0;
var parentNodesCount = 0;
var hiddenMacs = []; // hidden children
var hiddenChildren = [];
var deviceListGlobal = null;
// ---------------------------------------------------------------------------
// Recursively get children nodes and build a tree
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) {
if (list[i].devParentMAC.toLowerCase() == node.devMac.toLowerCase() && !hiddenMacs.includes(list[i].devParentMAC)) {
visibleNodesCount++;
// Process children recursively, passing a copy of the visited list
children.push(getChildren(list[i], list, path + ((path == "") ? "" : '|') + list[i].devParentMAC, visited));
}
}
// Track leaf and parent node counts
if (children.length == 0) {
leafNodesCount++;
} else {
parentNodesCount++;
}
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,
hiddenChildren: hiddenMacs.includes(node.devMac),
qty: children.length,
children: children
};
}
// ---------------------------------------------------------------------------
function getHierarchy()
{
for(i in deviceListGlobal)
{
if(deviceListGlobal[i].devMac == 'Internet')
{
return (getChildren(deviceListGlobal[i], deviceListGlobal, ''))
break;
}
}
}
//---------------------------------------------------------------------------
function toggleSubTree(parentMac, treePath)
{
treePath = treePath.split('|')
if(!hiddenMacs.includes(parentMac))
{
hiddenMacs.push(parentMac)
}
else
{
removeItemFromArray(hiddenMacs, parentMac)
}
updatedTree = getHierarchy()
myTree.refresh(updatedTree);
// re-attach any onclick events
attachTreeEvents();
}
// ---------------------------------------------------------------------------
function attachTreeEvents()
{
// toggle subtree functionality
$("div[data-mytreemac]").each(function(){
$(this).attr('onclick', 'toggleSubTree("'+$(this).attr('data-mytreemac')+'","'+ $(this).attr('data-mytreepath')+'")')
});
}
// ---------------------------------------------------------------------------
// Handle network node click - select correct tab in the bottom table
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);
}
}
}
// ---------------------------------------------------------------------------
var myTree;
var emSize;
var nodeHeight;
// var sizeCoefficient = 1.4
function pxToEm(px, element) {
var baseFontSize = parseFloat($(element || "body").css("font-size"));
return px / baseFontSize;
}
function emToPx(em, element) {
var baseFontSize = parseFloat($(element || "body").css("font-size"));
return Math.round(em * baseFontSize);
}
function initTree(myHierarchy)
{
// calculate the drawing area based on teh tree width and available screen size
let baseFontSize = parseFloat($('html').css('font-size'));
let treeAreaHeight = ($(window).height() - 155); ;
// 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);
// init the drawing area size
$("#networkTree").attr('style', `height:${treeAreaHeight}px; width:${emToPx(screenWidthEm)}px`)
if(myHierarchy.type == "")
{
showModalOk(getString('Network_Configuration_Error'), getString('Network_Root_Not_Configured'))
return;
}
// 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;
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' ) ? " &nbsp " : 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",
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()
}
// ---------------------------------------------------------------------------
// Tabs functionality
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
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'))
});
}
// ---------------------------------------------------------------------------
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')
}
// ---------------------------------------------------------------------------
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);
}
}
// ---------------------------------------------------------------------------
// showing icons or device names in tabs depending on available screen size
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');
}
});
}
}
}
let resizeTimeout;
$(window).on('resize', function () {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
checkTabsOverflow();
}, 100);
});
// init pop up hover boxes for device details
initHoverNodeInfo();
$(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);
});
});
</script>