diff --git a/front/devices.php b/front/devices.php index 88119941..cb497356 100755 --- a/front/devices.php +++ b/front/devices.php @@ -171,6 +171,9 @@ function main () { showSpinner(); + // render tiles + getDevicesTotals(); + //initialize the table headers in the correct order var availableColumns = getSettingOptions("UI_device_columns").split(","); headersDefaultOrder = availableColumns.map(val => getString(val)); @@ -223,60 +226,77 @@ function mapIndx(oldIndex) //------------------------------------------------------------------------------ // Query total numbers of Devices by status //------------------------------------------------------------------------------ -function getDevicesTotals(devicesData) { +function getDevicesTotals() { + // Check cache first + let resultJSON = getCache("getDevicesTotals"); - let resultJSON = ""; - - if (getCache("getDevicesTotals") !== "") { - resultJSON = getCache("getDevicesTotals"); + if (resultJSON !== "") { + resultJSON = JSON.parse(resultJSON); + processDeviceTotals(resultJSON); } else { + // Fetch data via AJAX + $.ajax({ + url: "/api/table_devices_tiles.json", + type: "GET", + dataType: "json", + success: function(response) { + if (response && response.data) { + resultJSON = response.data[0]; // Assuming the structure {"data": [ ... ]} + + // Save the result to cache + setCache("getDevicesTotals", JSON.stringify(resultJSON)); - // Define filter conditions and corresponding objects - const filters = [ - { status: 'my_devices', color: 'bg-aqua', label: getString('Device_Shortcut_AllDevices'), icon: 'fa-laptop' }, - { status: 'all', color: 'bg-aqua', label: getString('Gen_All_Devices'), icon: 'fa-laptop' }, - { status: 'connected', color: 'bg-green', label: getString('Device_Shortcut_Connected'), icon: 'fa-plug' }, - { status: 'favorites', color: 'bg-yellow', label: getString('Device_Shortcut_Favorites'), icon: 'fa-star' }, - { status: 'new', color: 'bg-yellow', label: getString('Device_Shortcut_NewDevices'), icon: 'fa-plus' }, - { status: 'down', color: 'bg-red', label: getString('Device_Shortcut_DownOnly'), icon: 'fa-warning' }, - { status: 'archived', color: 'bg-gray', label: getString('Device_Shortcut_Archived'), icon: 'fa-eye-slash' }, - { status: 'offline', color: 'bg-gray', label: getString('Gen_Offline'), icon: 'fa-xmark' } - ]; - - // Initialize an empty array to store the final objects - let dataArray = []; - - // Loop through each filter condition - filters.forEach(filter => { - // Calculate count dynamically based on filter condition - let count = filterDataByStatus(devicesData, filter.status).length; - - // Check any condition to skip adding the object to dataArray - if ( - (['', 'False'].includes(getSetting('UI_hide_empty')) || (getSetting('UI_hide_empty') == "True" && count > 0)) && - (getSetting('UI_shown_cards') == "" || getSetting('UI_shown_cards').includes(filter.status)) - ) { - dataArray.push({ - onclickEvent: `initializeDatatable('${filter.status}')`, - color: filter.color, - title: count, - label: filter.label, - icon: filter.icon - }); + // Process the fetched data + processDeviceTotals(resultJSON); + } else { + console.error("Invalid response format from API"); + } + }, + error: function(xhr, status, error) { + console.error("Failed to fetch devices data:", error); } - }); + } +} - // render info boxes/tile cards - renderInfoboxes( - dataArray - ) +function processDeviceTotals(devicesData) { + // Define filter conditions and corresponding objects + const filters = [ + { status: 'my_devices', color: 'bg-aqua', label: getString('Device_Shortcut_AllDevices'), icon: 'fa-laptop' }, + { status: 'all', color: 'bg-aqua', label: getString('Gen_All_Devices'), icon: 'fa-laptop' }, + { status: 'connected', color: 'bg-green', label: getString('Device_Shortcut_Connected'), icon: 'fa-plug' }, + { status: 'favorites', color: 'bg-yellow', label: getString('Device_Shortcut_Favorites'), icon: 'fa-star' }, + { status: 'new', color: 'bg-yellow', label: getString('Device_Shortcut_NewDevices'), icon: 'fa-plus' }, + { status: 'down', color: 'bg-red', label: getString('Device_Shortcut_DownOnly'), icon: 'fa-warning' }, + { status: 'archived', color: 'bg-gray', label: getString('Device_Shortcut_Archived'), icon: 'fa-eye-slash' }, + { status: 'offline', color: 'bg-gray', label: getString('Gen_Offline'), icon: 'fa-xmark' } + ]; - // save to cache - setCache("getDevicesTotals", resultJSON); - } + // Initialize an empty array to store the final objects + let dataArray = []; - // console.log(resultJSON); + // Loop through each filter condition + filters.forEach(filter => { + // Get count directly from API response data + let count = devicesData[filter.status] || 0; + + // Check any condition to skip adding the object to dataArray + if ( + (['', 'False'].includes(getSetting('UI_hide_empty')) || (getSetting('UI_hide_empty') == "True" && count > 0)) && + (getSetting('UI_shown_cards') == "" || getSetting('UI_shown_cards').includes(filter.status)) + ) { + dataArray.push({ + onclickEvent: `initializeDatatable('${filter.status}')`, + color: filter.color, + title: count, + label: filter.label, + icon: filter.icon + }); + } + }); + + // Render info boxes/tile cards + renderInfoboxes(dataArray); } //------------------------------------------------------------------------------ @@ -340,6 +360,7 @@ function filterDataByStatus(data, status) { // Map column index to column name for GraphQL query function mapColumnIndexToFieldName(index, tableColumnVisible) { + // the order is important, don't change it! const columnNames = [ "devName", "devOwner", @@ -367,6 +388,8 @@ function mapColumnIndexToFieldName(index, tableColumnVisible) { "devSourcePlugin" ]; + console.log("OrderBy: " + columnNames[tableColumnOrder[index]]); + return columnNames[tableColumnOrder[index]] || null; } @@ -522,7 +545,7 @@ function initializeDatatable (status) { device.devParentChildrenCount || 0, device.devLocation || "", device.devVendor || "", - device.devParentPort || 0, + device.devParentPort || "", device.devGUID || "", device.devSyncHubNode || "", device.devSite || "", @@ -708,6 +731,7 @@ function initializeDatatable (status) { }, initComplete: function (settings, devices) { // Handle any additional interactions or event listeners as required + // Save cookie Rows displayed, and Parameters rows & order $('#tableDevices').on( 'length.dt', function ( e, settings, len ) { setCookie ("nax_parTableRows", len, 129600); // save for 90 days diff --git a/server/__main__.py b/server/__main__.py index 5a37c8e0..d13e2cf6 100755 --- a/server/__main__.py +++ b/server/__main__.py @@ -185,12 +185,12 @@ def main (): db.commitDB() # Footer - updateState("Process: Wait") + mylog('verbose', ['[MAIN] Process: Wait']) else: # do something # mylog('verbose', ['[MAIN] Waiting to start next loop']) - dummyVariable = 1 + updateState("Process: Wait") #loop diff --git a/server/api.py b/server/api.py index 0ac45225..46a8af1b 100755 --- a/server/api.py +++ b/server/api.py @@ -3,9 +3,9 @@ import json # Register NetAlertX modules import conf -from const import (apiPath, sql_appevents, sql_devices_all, sql_events_pending_alert, sql_settings, sql_plugins_events, sql_plugins_history, sql_plugins_objects,sql_language_strings, sql_notifications_all, sql_online_history) +from const import (apiPath, sql_appevents, sql_devices_all, sql_events_pending_alert, sql_settings, sql_plugins_events, sql_plugins_history, sql_plugins_objects,sql_language_strings, sql_notifications_all, sql_online_history, sql_devices_tiles) from logger import mylog -from helper import write_file, get_setting_value +from helper import write_file, get_setting_value, updateState # Import the start_server function from graphql_server.graphql_server_start import start_server @@ -17,8 +17,10 @@ apiEndpoints = [] #=============================================================================== def update_api(db, all_plugins, isNotification = False, updateOnlyDataSources = []): mylog('debug', ['[API] Update API starting']) - # return + # update app_state.json and retrieve app_state to chjeck if GraphQL server is running + app_state = updateState("Update: API", None, None, None, None) + folder = apiPath # Save plugins @@ -36,6 +38,7 @@ def update_api(db, all_plugins, isNotification = False, updateOnlyDataSources = ["plugins_language_strings", sql_language_strings], ["notifications", sql_notifications_all], ["online_history", sql_online_history], + ["devices_tiles", sql_devices_tiles], ["custom_endpoint", conf.API_CUSTOM_SQL], ] @@ -50,15 +53,17 @@ def update_api(db, all_plugins, isNotification = False, updateOnlyDataSources = graphql_port_value = get_setting_value("GRAPHQL_PORT") api_token_value = get_setting_value("API_TOKEN") - # Validate and start the server if settings are available - if graphql_port_value is not None and api_token_value is not None: - try: - graphql_port_value = int(graphql_port_value) # Ensure port is an integer - start_server(graphql_port=graphql_port_value) # Start the server - except ValueError: - mylog('none', [f"[API] Invalid GRAPHQL_PORT value, must be an integer: {graphql_port_value}"]) - else: - mylog('none', [f"[API] GRAPHQL_PORT or API_TOKEN is not set, will try later."]) + # start GraphQL server if not yet running + if app_state.graphQLServerStarted == 0: + # Validate if settings are available + if graphql_port_value is not None and len(api_token_value) > 1: + try: + graphql_port_value = int(graphql_port_value) # Ensure port is an integer + start_server(graphql_port_value, app_state) # Start the server + except ValueError: + mylog('none', [f"[API] Invalid GRAPHQL_PORT value, must be an integer: {graphql_port_value}"]) + else: + mylog('none', [f"[API] GRAPHQL_PORT or API_TOKEN is not set, will try later."]) #------------------------------------------------------------------------------- diff --git a/server/const.py b/server/const.py index 65becf44..1fc439af 100755 --- a/server/const.py +++ b/server/const.py @@ -42,6 +42,38 @@ sql_devices_all = """ FROM Devices """ sql_appevents = """select * from AppEvents""" +# The below query calculates counts of devices in various categories: +# (connected/online, offline, down, new, archived), +# as well as a combined count for devices that match any status listed in the UI_MY_DEVICES setting +sql_devices_tiles = """ + WITH Statuses AS ( + SELECT Value + FROM Settings + WHERE Code_Name = 'UI_MY_DEVICES' + ), + MyDevicesFilter AS ( + SELECT + -- Build a dynamic filter for devices matching any status in UI_MY_DEVICES + devPresentLastScan, devAlertDown, devIsNew, devIsArchived + FROM Devices + WHERE + (instr((SELECT Value FROM Statuses), 'online') > 0 AND devPresentLastScan = 1) OR + (instr((SELECT Value FROM Statuses), 'offline') > 0 AND devPresentLastScan = 0) OR + (instr((SELECT Value FROM Statuses), 'down') > 0 AND devPresentLastScan = 0 AND devAlertDown = 1) OR + (instr((SELECT Value FROM Statuses), 'new') > 0 AND devIsNew = 1) OR + (instr((SELECT Value FROM Statuses), 'archived') > 0 AND devIsArchived = 1) + ) + SELECT + -- Counts for each individual status + (SELECT COUNT(*) FROM Devices WHERE devPresentLastScan = 1) AS connected, + (SELECT COUNT(*) FROM Devices WHERE devPresentLastScan = 0) AS offline, + (SELECT COUNT(*) FROM Devices WHERE devPresentLastScan = 0 AND devAlertDown = 1) AS down, + (SELECT COUNT(*) FROM Devices WHERE devIsNew = 1) AS new, + (SELECT COUNT(*) FROM Devices WHERE devIsArchived = 1) AS archived, + -- My Devices count + (SELECT COUNT(*) FROM MyDevicesFilter) AS my_devices + FROM Statuses; + """ sql_devices_stats = """SELECT Online_Devices as online, Down_Devices as down, All_Devices as 'all', Archived_Devices as archived, (select count(*) from Devices a where devIsNew = 1 ) as new, (select count(*) from Devices a where devName = '(unknown)' or devName = '(name not found)' ) as unknown diff --git a/server/graphql_server/graphql_schema.py b/server/graphql_server/graphql_schema.py index 8ce7af19..1f2bbf81 100755 --- a/server/graphql_server/graphql_schema.py +++ b/server/graphql_server/graphql_schema.py @@ -62,6 +62,7 @@ class Device(ObjectType): devIsRandomMac = Int() devParentChildrenCount = Int() devIpLong = Int() + devFilterStatus = String() class DeviceResult(ObjectType): @@ -92,6 +93,13 @@ class Query(ObjectType): mylog('none', f'[graphql_schema] devices_data: {devices_data}') + # Define static list of searchable fields + searchable_fields = [ + "devName", "devMac", "devOwner", "devType", "devVendor", + "devGroup", "devComments", "devLocation", "devStatus", + "devSSID", "devSite", "devSourcePlugin", "devSyncHubNode" + ] + # Apply sorting if options are provided if options: if options.sort: @@ -104,9 +112,14 @@ class Query(ObjectType): # Filter data if a search term is provided if options.search: + search_term = options.search.lower() + devices_data = [ device for device in devices_data - if options.search.lower() in device.get("devName", "").lower() + if any( + search_term in str(device.get(field, "")).lower() + for field in searchable_fields # Search only predefined fields + ) ] # Then apply pagination diff --git a/server/graphql_server/graphql_server_start.py b/server/graphql_server/graphql_server_start.py index e2924740..a55b64a6 100755 --- a/server/graphql_server/graphql_server_start.py +++ b/server/graphql_server/graphql_server_start.py @@ -38,11 +38,11 @@ def graphql_endpoint(): # Return the result as JSON return jsonify(result.data) -def start_server(graphql_port): +def start_server(graphql_port, app_state): """Start the GraphQL server in a background thread.""" - state = updateState("GraphQL: Starting", None, None, None, None) - if state.graphQLServerStarted == 0: + if app_state.graphQLServerStarted == 0: + mylog('verbose', [f'[graphql_server] Starting on port: {graphql_port}']) # Start Flask app in a separate thread @@ -57,4 +57,4 @@ def start_server(graphql_port): thread.start() # Update the state to indicate the server has started - state = updateState("Process: Wait", None, None, None, 1) \ No newline at end of file + app_state = updateState("Process: Wait", None, None, None, 1) \ No newline at end of file