From 58e32a5b43e0bf5daef34721aba928d8e5c82d2c Mon Sep 17 00:00:00 2001
From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com>
Date: Sat, 28 Feb 2026 00:06:04 +0000
Subject: [PATCH] feat: Refactor device column management and integrate new
device-columns.js for centralized field definitions
---
front/devices.php | 224 +++++++++------------------------
front/js/device-columns.js | 124 ++++++++++++++++++
front/js/ui_components.js | 42 +------
front/php/templates/footer.php | 1 +
4 files changed, 187 insertions(+), 204 deletions(-)
create mode 100644 front/js/device-columns.js
diff --git a/front/devices.php b/front/devices.php
index 2fd16c46..3de3080f 100755
--- a/front/devices.php
+++ b/front/devices.php
@@ -143,6 +143,9 @@
headersDefaultOrder = [];
missingNumbers = [];
+ // DEVICE_COLUMN_FIELDS, COL, NUMERIC_DEFAULTS, GRAPHQL_EXTRA_FIELDS, COLUMN_NAME_MAP
+ // are all defined in js/device-columns.js — edit that file to add new columns.
+
// Read parameters & Initialize components
callAfterAppInitialized(main)
showSpinner();
@@ -512,47 +515,8 @@ function collectFilters() {
// -----------------------------------------------------------------------------
// Map column index to column name for GraphQL query
function mapColumnIndexToFieldName(index, tableColumnVisible) {
- // the order is important, don't change it!
- const columnNames = [
- "devName", // 0
- "devOwner", // 1
- "devType", // 2
- "devIcon", // 3
- "devFavorite", // 4
- "devGroup", // 5
- "devFirstConnection", // 6
- "devLastConnection", // 7
- "devLastIP", // 8
- "devIsRandomMac", // 9 resolved on the fly
- "devStatus", // 10 resolved on the fly
- "devMac", // 11
- "devIpLong", // 12 formatIPlong(device.devLastIP) || "", // IP orderable
- "rowid", // 13
- "devParentMAC", // 14
- "devParentChildrenCount", // 15 resolved on the fly
- "devLocation", // 16
- "devVendor", // 17
- "devParentPort", // 18
- "devGUID", // 19
- "devSyncHubNode", // 20
- "devSite", // 21
- "devSSID", // 22
- "devSourcePlugin", // 23
- "devPresentLastScan", // 24
- "devAlertDown", // 25
- "devCustomProps", // 26
- "devFQDN", // 27
- "devParentRelType", // 28
- "devReqNicsOnline", // 29
- "devVlan", // 30
- "devPrimaryIPv4", // 31
- "devPrimaryIPv6", // 32
- "devFlapping", // 33
- ];
-
- // console.log("OrderBy: " + columnNames[tableColumnOrder[index]]);
-
- return columnNames[tableColumnOrder[index]] || null;
+ // Derives field name from the authoritative DEVICE_COLUMN_FIELDS constant.
+ return DEVICE_COLUMN_FIELDS[tableColumnOrder[index]] || null;
}
@@ -620,54 +584,15 @@ function initializeDatatable (status) {
"type": "POST",
"contentType": "application/json",
"data": function (d) {
- // Construct GraphQL query with pagination and sorting options
+ // GraphQL fields are derived from DEVICE_COLUMN_FIELDS + GRAPHQL_EXTRA_FIELDS
+ // (both defined in js/device-columns.js). No manual field list to maintain.
+ const _gqlFields = [...new Set([...DEVICE_COLUMN_FIELDS, ...GRAPHQL_EXTRA_FIELDS])]
+ .join('\n ');
let graphqlQuery = `
query devices($options: PageQueryOptionsInput) {
devices(options: $options) {
devices {
- rowid
- devMac
- devName
- devOwner
- devType
- devVendor
- devFavorite
- devGroup
- devComments
- devFirstConnection
- devLastConnection
- devLastIP
- devStaticIP
- devScan
- devLogEvents
- devAlertEvents
- devAlertDown
- devSkipRepeated
- devLastNotification
- devPresentLastScan
- devIsNew
- devIsRandomMac
- devLocation
- devIsArchived
- devParentMAC
- devParentPort
- devIcon
- devGUID
- devSite
- devSSID
- devSyncHubNode
- devSourcePlugin
- devStatus
- devParentChildrenCount
- devIpLong
- devCustomProps
- devFQDN
- devParentRelType
- devReqNicsOnline
- devVlan
- devPrimaryIPv4
- devPrimaryIPv6
- devFlapping
+ ${_gqlFields}
}
count
}
@@ -719,44 +644,13 @@ function initializeDatatable (status) {
// Return only the array of rows for the table
return json.devices.devices.map(device => {
- // Convert each device record into the required DataTable row format
- // Order has to be the same as in the UI_device_columns setting options
- const originalRow = [
- device.devName || "",
- device.devOwner || "",
- device.devType || "",
- device.devIcon || "",
- device.devFavorite || "",
- device.devGroup || "",
- device.devFirstConnection || "",
- device.devLastConnection || "",
- device.devLastIP || "",
- device.devIsRandomMac || "",
- device.devStatus || "",
- device.devMac || "",
- device.devIpLong || "",
- device.rowid || "",
- device.devParentMAC || "",
- device.devParentChildrenCount || 0,
- device.devLocation || "",
- device.devVendor || "",
- device.devParentPort || "",
- device.devGUID || "",
- device.devSyncHubNode || "",
- device.devSite || "",
- device.devSSID || "",
- device.devSourcePlugin || "",
- device.devPresentLastScan || "",
- device.devAlertDown || "",
- device.devCustomProps || "",
- device.devFQDN || "",
- device.devParentRelType || "",
- device.devReqNicsOnline || 0,
- device.devVlan || "",
- device.devPrimaryIPv4 || "",
- device.devPrimaryIPv6 || "",
- device.devFlapping || 0,
- ];
+ // Build positional row directly from DEVICE_COLUMN_FIELDS.
+ // NUMERIC_DEFAULTS controls which fields default to 0 vs "".
+ // Adding a new column: add to DEVICE_COLUMN_FIELDS (and NUMERIC_DEFAULTS
+ // if needed) in js/device-columns.js — nothing to change here.
+ const originalRow = DEVICE_COLUMN_FIELDS.map(
+ field => device[field] ?? (NUMERIC_DEFAULTS.has(field) ? 0 : "")
+ );
const newRow = [];
// Reorder data based on user-defined columns order
@@ -790,15 +684,15 @@ function initializeDatatable (status) {
'columnDefs' : [
{visible: false, targets: tableColumnHide },
- {className: 'text-center', targets: [mapIndx(4), mapIndx(9), mapIndx(10), mapIndx(15), mapIndx(18)] },
- {className: 'iconColumn text-center', targets: [mapIndx(3)]},
- {width: '80px', targets: [mapIndx(6), mapIndx(7), mapIndx(15), mapIndx(27)] },
- {width: '85px', targets: [mapIndx(9)] },
- {width: '30px', targets: [mapIndx(3), mapIndx(10), mapIndx(13), mapIndx(18)] },
- {orderData: [mapIndx(12)], targets: mapIndx(8) },
+ {className: 'text-center', targets: [mapIndx(COL.devFavorite), mapIndx(COL.devIsRandomMac), mapIndx(COL.devStatus), mapIndx(COL.devParentChildrenCount), mapIndx(COL.devParentPort)] },
+ {className: 'iconColumn text-center', targets: [mapIndx(COL.devIcon)]},
+ {width: '80px', targets: [mapIndx(COL.devFirstConnection), mapIndx(COL.devLastConnection), mapIndx(COL.devParentChildrenCount), mapIndx(COL.devFQDN)] },
+ {width: '85px', targets: [mapIndx(COL.devIsRandomMac)] },
+ {width: '30px', targets: [mapIndx(COL.devIcon), mapIndx(COL.devStatus), mapIndx(COL.rowid), mapIndx(COL.devParentPort)] },
+ {orderData: [mapIndx(COL.devIpLong)], targets: mapIndx(COL.devLastIP) },
// Device Name and FQDN
- {targets: [mapIndx(0), mapIndx(27)],
+ {targets: [mapIndx(COL.devName), mapIndx(COL.devFQDN)],
'createdCell': function (td, cellData, rowData, row, col) {
// console.log(cellData)
@@ -812,20 +706,20 @@ function initializeDatatable (status) {
$(td).html (
`
-
+ data-ip="${rowData[mapIndx(COL.devLastIP)]}"
+ data-mac="${rowData[mapIndx(COL.devMac)]}"
+ data-vendor="${rowData[mapIndx(COL.devVendor)]}"
+ data-type="${rowData[mapIndx(COL.devType)]}"
+ data-firstseen="${rowData[mapIndx(COL.devFirstConnection)]}"
+ data-lastseen="${rowData[mapIndx(COL.devLastConnection)]}"
+ data-relationship="${rowData[mapIndx(COL.devParentRelType)]}"
+ data-status="${rowData[mapIndx(COL.devStatus)]}"
+ data-present="${rowData[mapIndx(COL.devPresentLastScan)]}"
+ data-alert="${rowData[mapIndx(COL.devAlertDown)]}"
+ data-flapping="${rowData[mapIndx(COL.devFlapping)]}"
+ data-icon="${rowData[mapIndx(COL.devIcon)]}">
${displayedValue}
`
@@ -833,12 +727,12 @@ function initializeDatatable (status) {
} },
// Connected Devices
- {targets: [mapIndx(15)],
+ {targets: [mapIndx(COL.devParentChildrenCount)],
'createdCell': function (td, cellData, rowData, row, col) {
// check if this is a network device
- if(getSetting("NETWORK_DEVICE_TYPES").includes(`'${rowData[mapIndx(2)]}'`) )
+ if(getSetting("NETWORK_DEVICE_TYPES").includes(`'${rowData[mapIndx(COL.devType)]}'`) )
{
- $(td).html (''+ cellData +'');
+ $(td).html (''+ cellData +'');
}
else
{
@@ -848,7 +742,7 @@ function initializeDatatable (status) {
} },
// Icon
- {targets: [mapIndx(3)],
+ {targets: [mapIndx(COL.devIcon)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
@@ -859,7 +753,7 @@ function initializeDatatable (status) {
} },
// Full MAC
- {targets: [mapIndx(11)],
+ {targets: [mapIndx(COL.devMac)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
$(td).html (''+cellData+'');
@@ -869,7 +763,7 @@ function initializeDatatable (status) {
} },
// IP address
- {targets: [mapIndx(8)],
+ {targets: [mapIndx(COL.devLastIP)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
$(td).html (`
@@ -887,8 +781,8 @@ function initializeDatatable (status) {
}
}
},
- // IP address (ordeable)
- {targets: [mapIndx(12)],
+ // IP address (orderable)
+ {targets: [mapIndx(COL.devIpLong)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
$(td).html (`${cellData}`);
@@ -899,10 +793,10 @@ function initializeDatatable (status) {
},
// Custom Properties
- {targets: [mapIndx(26)],
+ {targets: [mapIndx(COL.devCustomProps)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
- $(td).html (`${renderCustomProps(cellData, rowData[mapIndx(11)])}`);
+ $(td).html (`${renderCustomProps(cellData, rowData[mapIndx(COL.devMac)])}`);
} else {
$(td).html ('');
}
@@ -910,7 +804,7 @@ function initializeDatatable (status) {
},
// Favorite
- {targets: [mapIndx(4)],
+ {targets: [mapIndx(COL.devFavorite)],
'createdCell': function (td, cellData, rowData, row, col) {
if (cellData == 1){
$(td).html ('');
@@ -920,7 +814,7 @@ function initializeDatatable (status) {
} },
// Dates
- {targets: [mapIndx(6), mapIndx(7)],
+ {targets: [mapIndx(COL.devFirstConnection), mapIndx(COL.devLastConnection)],
'createdCell': function (td, cellData, rowData, row, col) {
var result = cellData.toString(); // Convert to string
if (result.includes("+")) { // Check if timezone offset is present
@@ -930,7 +824,7 @@ function initializeDatatable (status) {
} },
// Random MAC
- {targets: [mapIndx(9)],
+ {targets: [mapIndx(COL.devIsRandomMac)],
'createdCell': function (td, cellData, rowData, row, col) {
// console.log(cellData)
if (cellData == 1){
@@ -941,7 +835,7 @@ function initializeDatatable (status) {
} },
// Parent Mac
- {targets: [mapIndx(14)],
+ {targets: [mapIndx(COL.devParentMAC)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!isValidMac(cellData)) {
$(td).html('');
@@ -963,13 +857,13 @@ function initializeDatatable (status) {
}
},
// Status color
- {targets: [mapIndx(10)],
+ {targets: [mapIndx(COL.devStatus)],
'createdCell': function (td, cellData, rowData, row, col) {
- tmp_devPresentLastScan = rowData[mapIndx(24)]
- tmp_devAlertDown = rowData[mapIndx(25)]
- tmp_devMac = rowData[mapIndx(11)]
- tmp_devFlapping = rowData[mapIndx(33)]
+ tmp_devPresentLastScan = rowData[mapIndx(COL.devPresentLastScan)]
+ tmp_devAlertDown = rowData[mapIndx(COL.devAlertDown)]
+ tmp_devMac = rowData[mapIndx(COL.devMac)]
+ tmp_devFlapping = rowData[mapIndx(COL.devFlapping)]
const badge = getStatusBadgeParts(
tmp_devPresentLastScan, // tmp_devPresentLastScan
@@ -1044,7 +938,7 @@ function initializeDatatable (status) {
},
createdRow: function(row, data, dataIndex) {
// add devMac to the table row
- $(row).attr('my-devMac', data[mapIndx(11)]);
+ $(row).attr('my-devMac', data[mapIndx(COL.devMac)]);
}
@@ -1090,7 +984,7 @@ function multiEditDevices()
macs = ""
for (var j = 0; j < selectedDevicesDataTableData.length; j++) {
- macs += selectedDevicesDataTableData[j][mapIndx(11)] + ","; // [11] == MAC
+ macs += selectedDevicesDataTableData[j][mapIndx(COL.devMac)] + ","; // MAC
}
// redirect to the Maintenance section
@@ -1111,7 +1005,7 @@ function getMacsOfShownDevices() {
allIndexes.each(function(idx) {
var rowData = table.row(idx).data();
if (rowData) {
- macs.push(rowData[mapIndx(11)]); // mapIndx(11) == MAC column
+ macs.push(rowData[mapIndx(COL.devMac)]); // MAC column
}
});
diff --git a/front/js/device-columns.js b/front/js/device-columns.js
new file mode 100644
index 00000000..5d68c9ec
--- /dev/null
+++ b/front/js/device-columns.js
@@ -0,0 +1,124 @@
+// =============================================================================
+// device-columns.js — Single source of truth for device field definitions.
+//
+// To add a new device column, update ONLY these places:
+// 1. DEVICE_COLUMN_FIELDS — add the field name in the correct position
+// 2. COLUMN_NAME_MAP — add Device_TableHead_X → fieldName mapping
+// 3. NUMERIC_DEFAULTS — add fieldName if its default value is 0 not ""
+// 4. GRAPHQL_EXTRA_FIELDS — add fieldName ONLY if it is NOT a display column
+// (i.e. fetched for logic but not shown in table)
+// 5. front/plugins/ui_settings/config.json options[]
+// 6. front/php/templates/language/en_us.json Device_TableHead_X
+// then run merge_translations.py for other languages
+// 7. Backend: DB view + GraphQL type
+// =============================================================================
+
+// Ordered list of all device table column field names.
+// Position here determines the positional index used throughout devices.php.
+const DEVICE_COLUMN_FIELDS = [
+ "devName", // 0 Device_TableHead_Name
+ "devOwner", // 1 Device_TableHead_Owner
+ "devType", // 2 Device_TableHead_Type
+ "devIcon", // 3 Device_TableHead_Icon
+ "devFavorite", // 4 Device_TableHead_Favorite
+ "devGroup", // 5 Device_TableHead_Group
+ "devFirstConnection", // 6 Device_TableHead_FirstSession
+ "devLastConnection", // 7 Device_TableHead_LastSession
+ "devLastIP", // 8 Device_TableHead_LastIP
+ "devIsRandomMac", // 9 Device_TableHead_MAC (random MAC flag column)
+ "devStatus", // 10 Device_TableHead_Status
+ "devMac", // 11 Device_TableHead_MAC_full
+ "devIpLong", // 12 Device_TableHead_LastIPOrder
+ "rowid", // 13 Device_TableHead_Rowid
+ "devParentMAC", // 14 Device_TableHead_Parent_MAC
+ "devParentChildrenCount",// 15 Device_TableHead_Connected_Devices
+ "devLocation", // 16 Device_TableHead_Location
+ "devVendor", // 17 Device_TableHead_Vendor
+ "devParentPort", // 18 Device_TableHead_Port
+ "devGUID", // 19 Device_TableHead_GUID
+ "devSyncHubNode", // 20 Device_TableHead_SyncHubNodeName
+ "devSite", // 21 Device_TableHead_NetworkSite
+ "devSSID", // 22 Device_TableHead_SSID
+ "devSourcePlugin", // 23 Device_TableHead_SourcePlugin
+ "devPresentLastScan", // 24 Device_TableHead_PresentLastScan
+ "devAlertDown", // 25 Device_TableHead_AlertDown
+ "devCustomProps", // 26 Device_TableHead_CustomProps
+ "devFQDN", // 27 Device_TableHead_FQDN
+ "devParentRelType", // 28 Device_TableHead_ParentRelType
+ "devReqNicsOnline", // 29 Device_TableHead_ReqNicsOnline
+ "devVlan", // 30 Device_TableHead_Vlan
+ "devPrimaryIPv4", // 31 Device_TableHead_IPv4
+ "devPrimaryIPv6", // 32 Device_TableHead_IPv6
+ "devFlapping", // 33 Device_TableHead_Flapping
+];
+
+// Named index constants — eliminates all mapIndx(N) magic numbers.
+// Access as COL.devFlapping, COL.devMac, etc.
+const COL = Object.fromEntries(DEVICE_COLUMN_FIELDS.map((name, i) => [name, i]));
+
+// Fields whose GraphQL response value should default to 0 instead of "".
+const NUMERIC_DEFAULTS = new Set([
+ "devParentChildrenCount",
+ "devReqNicsOnline",
+ "devFlapping",
+]);
+
+// Fields fetched from GraphQL for internal logic only — not display columns.
+// These are merged with DEVICE_COLUMN_FIELDS to build the GraphQL query.
+const GRAPHQL_EXTRA_FIELDS = [
+ "devComments",
+ "devStaticIP",
+ "devScan",
+ "devLogEvents",
+ "devAlertEvents",
+ "devSkipRepeated",
+ "devLastNotification",
+ "devIsNew",
+ "devIsArchived",
+];
+
+// Maps Device_TableHead_* language keys to their GraphQL/DB field names.
+// Used by getColumnNameFromLangString() in ui_components.js and by
+// column filter logic in devices.php.
+//
+// NOTE: Device_TableHead_MAC maps to devMac (display), while position 9 in
+// DEVICE_COLUMN_FIELDS uses devIsRandomMac (the random-MAC flag column).
+// These are intentionally different; do not collapse them.
+const COLUMN_NAME_MAP = {
+ "Device_TableHead_Name": "devName",
+ "Device_TableHead_Owner": "devOwner",
+ "Device_TableHead_Type": "devType",
+ "Device_TableHead_Icon": "devIcon",
+ "Device_TableHead_Favorite": "devFavorite",
+ "Device_TableHead_Group": "devGroup",
+ "Device_TableHead_FirstSession": "devFirstConnection",
+ "Device_TableHead_LastSession": "devLastConnection",
+ "Device_TableHead_LastIP": "devLastIP",
+ "Device_TableHead_MAC": "devMac",
+ "Device_TableHead_Status": "devStatus",
+ "Device_TableHead_MAC_full": "devMac",
+ "Device_TableHead_LastIPOrder": "devIpLong",
+ "Device_TableHead_Rowid": "rowid",
+ "Device_TableHead_Parent_MAC": "devParentMAC",
+ "Device_TableHead_Connected_Devices": "devParentChildrenCount",
+ "Device_TableHead_Location": "devLocation",
+ "Device_TableHead_Vendor": "devVendor",
+ "Device_TableHead_Port": "devParentPort",
+ "Device_TableHead_GUID": "devGUID",
+ "Device_TableHead_SyncHubNodeName": "devSyncHubNode",
+ "Device_TableHead_NetworkSite": "devSite",
+ "Device_TableHead_SSID": "devSSID",
+ "Device_TableHead_SourcePlugin": "devSourcePlugin",
+ "Device_TableHead_PresentLastScan": "devPresentLastScan",
+ "Device_TableHead_AlertDown": "devAlertDown",
+ "Device_TableHead_CustomProps": "devCustomProps",
+ "Device_TableHead_FQDN": "devFQDN",
+ "Device_TableHead_ParentRelType": "devParentRelType",
+ "Device_TableHead_ReqNicsOnline": "devReqNicsOnline",
+ "Device_TableHead_Vlan": "devVlan",
+ "Device_TableHead_IPv4": "devPrimaryIPv4",
+ "Device_TableHead_IPv6": "devPrimaryIPv6",
+ "Device_TableHead_Flapping": "devFlapping",
+};
+
+console.log("init device-columns.js");
diff --git a/front/js/ui_components.js b/front/js/ui_components.js
index 217d624c..51425a9c 100755
--- a/front/js/ui_components.js
+++ b/front/js/ui_components.js
@@ -730,46 +730,10 @@ function showIconSelection(setKey) {
// -----------------------------------------------------------------------------
-// Get the correct db column code name based on table header title string
+// Get the correct db column code name based on table header title string.
+// COLUMN_NAME_MAP is defined in device-columns.js, loaded before this file.
function getColumnNameFromLangString(headStringKey) {
- columnNameMap = {
- "Device_TableHead_Name": "devName",
- "Device_TableHead_Owner": "devOwner",
- "Device_TableHead_Type": "devType",
- "Device_TableHead_Icon": "devIcon",
- "Device_TableHead_Favorite": "devFavorite",
- "Device_TableHead_Group": "devGroup",
- "Device_TableHead_FirstSession": "devFirstConnection",
- "Device_TableHead_LastSession": "devLastConnection",
- "Device_TableHead_LastIP": "devLastIP",
- "Device_TableHead_MAC": "devMac",
- "Device_TableHead_Status": "devStatus",
- "Device_TableHead_MAC_full": "devMac",
- "Device_TableHead_LastIPOrder": "devIpLong",
- "Device_TableHead_Rowid": "rowid",
- "Device_TableHead_Parent_MAC": "devParentMAC",
- "Device_TableHead_Connected_Devices": "devParentChildrenCount",
- "Device_TableHead_Location": "devLocation",
- "Device_TableHead_Vendor": "devVendor",
- "Device_TableHead_Port": "devParentPort",
- "Device_TableHead_GUID": "devGUID",
- "Device_TableHead_SyncHubNodeName": "devSyncHubNode",
- "Device_TableHead_NetworkSite": "devSite",
- "Device_TableHead_SSID": "devSSID",
- "Device_TableHead_SourcePlugin": "devSourcePlugin",
- "Device_TableHead_PresentLastScan": "devPresentLastScan",
- "Device_TableHead_AlertDown": "devAlertDown",
- "Device_TableHead_CustomProps": "devCustomProps",
- "Device_TableHead_FQDN": "devFQDN",
- "Device_TableHead_ParentRelType": "devParentRelType",
- "Device_TableHead_ReqNicsOnline": "devReqNicsOnline",
- "Device_TableHead_Vlan": "devVlan",
- "Device_TableHead_IPv4": "devPrimaryIPv4",
- "Device_TableHead_IPv6": "devPrimaryIPv6",
- "Device_TableHead_Flapping": "devFlapping"
- };
-
- return columnNameMap[headStringKey] || "";
+ return COLUMN_NAME_MAP[headStringKey] || "";
}
//--------------------------------------------------------------
diff --git a/front/php/templates/footer.php b/front/php/templates/footer.php
index 02caeaff..88c020b4 100755
--- a/front/php/templates/footer.php
+++ b/front/php/templates/footer.php
@@ -55,6 +55,7 @@
+