mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-06 17:15:38 -08:00
1063 lines
34 KiB
PHP
Executable File
1063 lines
34 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') : 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);
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
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 (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();
|
|
});
|
|
|
|
</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) {
|
|
const item = list[i];
|
|
const parentMac = item.devParentMAC || ""; // null-safe
|
|
const nodeMac = node.devMac || ""; // null-safe
|
|
|
|
if (parentMac != "" && parentMac.toLowerCase() == nodeMac.toLowerCase() && !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++;
|
|
}
|
|
|
|
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()
|
|
{
|
|
// reset counters before rebuilding the hierarchy
|
|
leafNodesCount = 0;
|
|
visibleNodesCount = 0;
|
|
parentNodesCount = 0;
|
|
|
|
let internetNode = null;
|
|
|
|
for(i in deviceListGlobal)
|
|
{
|
|
if(deviceListGlobal[i].devMac == '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;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
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)
|
|
{
|
|
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",
|
|
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");
|
|
}
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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>
|
|
|
|
|
|
|