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 @@ +