GraphQl 0.123 - Dynamic columns + re-adding old Device table columns

This commit is contained in:
jokob-sk
2024-11-14 16:50:23 +11:00
parent 072821181a
commit c1c6813b6e
7 changed files with 235 additions and 85 deletions

View File

@@ -160,6 +160,7 @@
var tableColumnOrder = []; var tableColumnOrder = [];
var tableColumnVisible = []; var tableColumnVisible = [];
headersDefaultOrder = []; headersDefaultOrder = [];
missingNumbers = [];
// Read parameters & Initialize components // Read parameters & Initialize components
callAfterAppInitialized(main) callAfterAppInitialized(main)
@@ -193,7 +194,7 @@ function main () {
const fullArray = Array.from({ length: tableColumnOrder.length }, (_, i) => i); const fullArray = Array.from({ length: tableColumnOrder.length }, (_, i) => i);
// Filter out the elements already present in inputArray // Filter out the elements already present in inputArray
const missingNumbers = fullArray.filter(num => !tableColumnVisible.includes(num)); missingNumbers = fullArray.filter(num => !tableColumnVisible.includes(num));
// Concatenate the inputArray with the missingNumbers // Concatenate the inputArray with the missingNumbers
tableColumnOrder = [...tableColumnVisible, ...missingNumbers]; tableColumnOrder = [...tableColumnVisible, ...missingNumbers];
@@ -336,47 +337,43 @@ function filterDataByStatus(data, status) {
}); });
} }
// -----------------------------------------------------------------------------
function getDeviceStatus(item)
{
if(item.devIsNew === 1)
{
return 'New';
}
else if(item.devPresentLastScan === 1)
{
return 'On-line';
}
else if(item.devPresentLastScan === 0 && item.devAlertDown !== 0)
{
return 'Down';
}
else if(item.devIsArchived === 1)
{
return 'Archived';
}
else if(item.devPresentLastScan === 0)
{
return 'Off-line';
}
return "Unknown status"
}
// Map column index to column name for GraphQL query // Map column index to column name for GraphQL query
function mapColumnIndexToFieldName(index) { function mapColumnIndexToFieldName(index, tableColumnVisible) {
const columnNames = [ const columnNames = [
"rowid", "devMac", "devName", "devOwner", "devType", "devVendor", "devName",
"devFavorite", "devGroup", "devComments", "devFirstConnection", "devOwner",
"devLastConnection", "devLastIP", "devStaticIP", "devScan", "devLogEvents", "devType",
"devAlertEvents", "devAlertDown", "devSkipRepeated", "devLastNotification", "devIcon",
"devPresentLastScan", "devIsNew", "devLocation", "devIsArchived", "devFavorite",
"devParentMAC", "devParentPort", "devIcon", "devGUID", "devSite", "devSSID", "devGroup",
"devSyncHubNode", "devSourcePlugin" "devFirstConnection",
"devLastConnection",
"devLastIP",
"devIsRandomMac", // resolved on the fly
"devStatus", // resolved on the fly
"devMac",
"devIpLong", //formatIPlong(device.devLastIP) || "", // IP orderable
"rowid",
"devParentMAC",
"devParentChildrenCount", // resolved on the fly
"devLocation",
"devVendor",
"devParentPort",
"devGUID",
"devSyncHubNode",
"devSite",
"devSSID",
"devSourcePlugin"
]; ];
return columnNames[index] || null; console.log(index);
console.log(tableColumnVisible);
console.log(tableColumnOrder); // this
console.log(missingNumbers);
console.log(columnNames[tableColumnOrder[index]]);
return columnNames[tableColumnOrder[index]] || null;
} }
@@ -465,6 +462,7 @@ function initializeDatatable (status) {
devLastNotification devLastNotification
devPresentLastScan devPresentLastScan
devIsNew devIsNew
devIsRandomMac
devLocation devLocation
devIsArchived devIsArchived
devParentMAC devParentMAC
@@ -475,6 +473,9 @@ function initializeDatatable (status) {
devSSID devSSID
devSyncHubNode devSyncHubNode
devSourcePlugin devSourcePlugin
devStatus
devParentChildrenCount
devIpLong
} }
count count
} }
@@ -493,7 +494,7 @@ function initializeDatatable (status) {
"page": Math.floor(d.start / d.length) + 1, // Page number (1-based) "page": Math.floor(d.start / d.length) + 1, // Page number (1-based)
"limit": parseInt(d.length, 10), // Page size (ensure it's an integer) "limit": parseInt(d.length, 10), // Page size (ensure it's an integer)
"sort": d.order && d.order[0] ? [{ "sort": d.order && d.order[0] ? [{
"field": mapColumnIndexToFieldName(d.order[0].column), // Sort field from DataTable column "field": mapColumnIndexToFieldName(d.order[0].column, tableColumnVisible), // Sort field from DataTable column
"order": d.order[0].dir.toUpperCase() // Sort direction (ASC/DESC) "order": d.order[0].dir.toUpperCase() // Sort direction (ASC/DESC)
}] : [], // Default to an empty array if no sorting is defined }] : [], // Default to an empty array if no sorting is defined
"search": d.search.value // Search query "search": d.search.value // Search query
@@ -518,13 +519,13 @@ function initializeDatatable (status) {
device.devFirstConnection || "", device.devFirstConnection || "",
device.devLastConnection || "", device.devLastConnection || "",
device.devLastIP || "", device.devLastIP || "",
(isRandomMAC(device.devMac)) || "", // Custom logic for randomized MAC device.devIsRandomMac || "", // Custom logic for randomized MAC
getDeviceStatus(device) || "", device.devStatus || "",
device.devMac || "", // hidden device.devMac || "", // hidden
formatIPlong(device.devLastIP) || "", // IP orderable device.devIpLong || "", // IP orderable
device.rowid || "", device.rowid || "",
device.devParentMAC || "", device.devParentMAC || "",
getNumberOfChildren(device.devMac, json.devices.devices) || 0, device.devParentChildrenCount || 0,
device.devLocation || "", device.devLocation || "",
device.devVendor || "", device.devVendor || "",
device.devParentPort || 0, device.devParentPort || 0,
@@ -751,26 +752,6 @@ function initializeDatatable (status) {
} }
// -----------------------------------------------------------------------------
function getNumberOfChildren(mac, devices)
{
childrenCount = 0;
$.each(devices, function(index, dev) {
if(dev.devParentMAC != null && dev.devParentMAC.trim() == mac.trim())
{
childrenCount++;
}
});
return childrenCount;
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
function handleLoadingDialog(needsReload = false) function handleLoadingDialog(needsReload = false)
{ {

View File

@@ -12,7 +12,6 @@ require dirname(__FILE__).'/../server/init.php';
// Helper function to get GraphQL URL (you can replace this with environment variables) // Helper function to get GraphQL URL (you can replace this with environment variables)
function getGraphQLUrl() { function getGraphQLUrl() {
$port = getSettingValue("GRAPHQL_PORT"); // Port for the GraphQL server $port = getSettingValue("GRAPHQL_PORT"); // Port for the GraphQL server
// return "$url:$port/graphql"; // Full URL to the GraphQL endpoint
return "0.0.0.0:$port/graphql"; // Full URL to the GraphQL endpoint return "0.0.0.0:$port/graphql"; // Full URL to the GraphQL endpoint
} }

View File

@@ -27,7 +27,20 @@ vendorsPathNewest = '/usr/share/arp-scan/ieee-oui_all_filtered.txt'
#=============================================================================== #===============================================================================
# SQL queries # SQL queries
#=============================================================================== #===============================================================================
sql_devices_all = """select rowid, * from Devices""" sql_devices_all = """
SELECT
rowid,
*,
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
"""
sql_appevents = """select * from AppEvents""" sql_appevents = """select * from AppEvents"""
sql_devices_stats = """SELECT Online_Devices as online, Down_Devices as down, All_Devices as 'all', Archived_Devices as archived, 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 devIsNew = 1 ) as new,

View File

@@ -28,7 +28,7 @@ class DB():
mylog('debug','openDB: database already open') mylog('debug','openDB: database already open')
return return
mylog('none', '[Database] Opening DB' ) mylog('verbose', '[Database] Opening DB' )
# Open DB and Cursor # Open DB and Cursor
try: try:
self.sql_connection = sqlite3.connect (fullDbPath, isolation_level=None) self.sql_connection = sqlite3.connect (fullDbPath, isolation_level=None)
@@ -37,7 +37,7 @@ class DB():
self.sql_connection.row_factory = sqlite3.Row self.sql_connection.row_factory = sqlite3.Row
self.sql = self.sql_connection.cursor() self.sql = self.sql_connection.cursor()
except sqlite3.Error as e: except sqlite3.Error as e:
mylog('none',[ '[Database] - Open DB Error: ', e]) mylog('verbose',[ '[Database] - Open DB Error: ', e])
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
@@ -96,7 +96,7 @@ class DB():
""") """)
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# DevicesNew - cleanup after 6/6/2025 # DevicesNew - cleanup after 6/6/2025 - need to update also DB in the source code!
# check if migration already done based on devMac # check if migration already done based on devMac
devMac_missing = self.sql.execute (""" devMac_missing = self.sql.execute ("""
@@ -104,6 +104,104 @@ class DB():
""").fetchone()[0] == 0 """).fetchone()[0] == 0
if devMac_missing: if devMac_missing:
# -------------------------------------------------------------------------
# Alter Devices table
# -------------------------------------------------------------------------
# dev_Network_Node_MAC_ADDR column
dev_Network_Node_MAC_ADDR_missing = self.sql.execute ("""
SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Devices') WHERE name='dev_Network_Node_MAC_ADDR'
""").fetchone()[0] == 0
if dev_Network_Node_MAC_ADDR_missing :
mylog('verbose', ["[upgradeDB] Adding dev_Network_Node_MAC_ADDR to the Devices table"])
self.sql.execute("""
ALTER TABLE "Devices" ADD "dev_Network_Node_MAC_ADDR" TEXT
""")
# dev_Network_Node_port column
dev_Network_Node_port_missing = self.sql.execute ("""
SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Devices') WHERE name='dev_Network_Node_port'
""").fetchone()[0] == 0
if dev_Network_Node_port_missing :
mylog('verbose', ["[upgradeDB] Adding dev_Network_Node_port to the Devices table"])
self.sql.execute("""
ALTER TABLE "Devices" ADD "dev_Network_Node_port" INTEGER
""")
# dev_Icon column
dev_Icon_missing = self.sql.execute ("""
SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Devices') WHERE name='dev_Icon'
""").fetchone()[0] == 0
if dev_Icon_missing :
mylog('verbose', ["[upgradeDB] Adding dev_Icon to the Devices table"])
self.sql.execute("""
ALTER TABLE "Devices" ADD "dev_Icon" TEXT
""")
# dev_GUID column
dev_GUID_missing = self.sql.execute ("""
SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Devices') WHERE name='dev_GUID'
""").fetchone()[0] == 0
if dev_GUID_missing :
mylog('verbose', ["[upgradeDB] Adding dev_GUID to the Devices table"])
self.sql.execute("""
ALTER TABLE "Devices" ADD "dev_GUID" TEXT
""")
# dev_NetworkSite column
dev_NetworkSite_missing = self.sql.execute ("""
SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Devices') WHERE name='dev_NetworkSite'
""").fetchone()[0] == 0
if dev_NetworkSite_missing :
mylog('verbose', ["[upgradeDB] Adding dev_NetworkSite to the Devices table"])
self.sql.execute("""
ALTER TABLE "Devices" ADD "dev_NetworkSite" TEXT
""")
# dev_SSID column
dev_SSID_missing = self.sql.execute ("""
SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Devices') WHERE name='dev_SSID'
""").fetchone()[0] == 0
if dev_SSID_missing :
mylog('verbose', ["[upgradeDB] Adding dev_SSID to the Devices table"])
self.sql.execute("""
ALTER TABLE "Devices" ADD "dev_SSID" TEXT
""")
# SQL query to update missing dev_GUID
self.sql.execute(f'''
UPDATE Devices
SET dev_GUID = {sql_generateGuid}
WHERE dev_GUID IS NULL
''')
# dev_SyncHubNodeName column
dev_SyncHubNodeName_missing = self.sql.execute ("""
SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Devices') WHERE name='dev_SyncHubNodeName'
""").fetchone()[0] == 0
if dev_SyncHubNodeName_missing :
mylog('verbose', ["[upgradeDB] Adding dev_SyncHubNodeName to the Devices table"])
self.sql.execute("""
ALTER TABLE "Devices" ADD "dev_SyncHubNodeName" TEXT
""")
# dev_SourcePlugin column
dev_SourcePlugin_missing = self.sql.execute ("""
SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Devices') WHERE name='dev_SourcePlugin'
""").fetchone()[0] == 0
if dev_SourcePlugin_missing :
mylog('verbose', ["[upgradeDB] Adding dev_SourcePlugin to the Devices table"])
self.sql.execute("""
ALTER TABLE "Devices" ADD "dev_SourcePlugin" TEXT
""")
# SQL to create Devices table with indexes # SQL to create Devices table with indexes
sql_create_devices_new_tmp = """ sql_create_devices_new_tmp = """
@@ -743,7 +841,7 @@ class DB():
columnNames = list(map(lambda x: x[0], self.sql.description)) columnNames = list(map(lambda x: x[0], self.sql.description))
rows = self.sql.fetchall() rows = self.sql.fetchall()
except sqlite3.Error as e: except sqlite3.Error as e:
mylog('none',[ '[Database] - SQL ERROR: ', e]) mylog('verbose',[ '[Database] - SQL ERROR: ', e])
return json_obj({}, []) # return empty object return json_obj({}, []) # return empty object
result = {"data":[]} result = {"data":[]}
@@ -768,9 +866,9 @@ class DB():
rows = self.sql.fetchall() rows = self.sql.fetchall()
return rows return rows
except AssertionError: except AssertionError:
mylog('none',[ '[Database] - ERROR: inconsistent query and/or arguments.', query, " params: ", args]) mylog('verbose',[ '[Database] - ERROR: inconsistent query and/or arguments.', query, " params: ", args])
except sqlite3.Error as e: except sqlite3.Error as e:
mylog('none',[ '[Database] - SQL ERROR: ', e]) mylog('verbose',[ '[Database] - SQL ERROR: ', e])
return None return None
def read_one(self, query, *args): def read_one(self, query, *args):
@@ -785,7 +883,7 @@ class DB():
return rows[0] return rows[0]
if len(rows) > 1: if len(rows) > 1:
mylog('none',[ '[Database] - Warning!: query returns multiple rows, only first row is passed on!', query, " params: ", args]) mylog('verbose',[ '[Database] - Warning!: query returns multiple rows, only first row is passed on!', query, " params: ", args])
return rows[0] return rows[0]
# empty result set # empty result set
return None return None

View File

@@ -9,6 +9,7 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
from logger import mylog from logger import mylog
from const import apiPath from const import apiPath
from helper import is_random_mac, get_number_of_children, format_ip_long
# Define a base URL with the user's home directory # Define a base URL with the user's home directory
folder = apiPath folder = apiPath
@@ -57,6 +58,11 @@ class Device(ObjectType):
devSSID = String() devSSID = String()
devSyncHubNode = String() devSyncHubNode = String()
devSourcePlugin = String() devSourcePlugin = String()
devStatus = String()
devIsRandomMac = Int()
devParentChildrenCount = Int()
devIpLong = Int()
class DeviceResult(ObjectType): class DeviceResult(ObjectType):
devices = List(Device) devices = List(Device)
@@ -67,6 +73,7 @@ class Query(ObjectType):
devices = Field(DeviceResult, options=PageQueryOptionsInput()) devices = Field(DeviceResult, options=PageQueryOptionsInput())
def resolve_devices(self, info, options=None): def resolve_devices(self, info, options=None):
mylog('none', f'[graphql_schema] resolve_devices: {self}')
try: try:
with open(folder + 'table_devices.json', 'r') as f: with open(folder + 'table_devices.json', 'r') as f:
devices_data = json.load(f)["data"] devices_data = json.load(f)["data"]
@@ -74,16 +81,19 @@ class Query(ObjectType):
mylog('none', f'[graphql_schema] Error loading devices data: {e}') mylog('none', f'[graphql_schema] Error loading devices data: {e}')
return DeviceResult(devices=[], count=0) return DeviceResult(devices=[], count=0)
# Add dynamic fields to each device
for device in devices_data:
device["devIsRandomMac"] = 1 if is_random_mac(device["devMac"]) else 0
device["devParentChildrenCount"] = get_number_of_children(device["devMac"], devices_data)
device["devIpLong"] = format_ip_long(device.get("devLastIP", ""))
total_count = len(devices_data) total_count = len(devices_data)
# Apply pagination and sorting if options are provided mylog('none', f'[graphql_schema] devices_data: {devices_data}')
if options:
# Implement pagination and sorting here
if options.page and options.limit:
start = (options.page - 1) * options.limit
end = start + options.limit
devices_data = devices_data[start:end]
# Apply sorting if options are provided
if options:
if options.sort: if options.sort:
for sort_option in options.sort: for sort_option in options.sort:
devices_data = sorted( devices_data = sorted(
@@ -91,7 +101,7 @@ class Query(ObjectType):
key=lambda x: x.get(sort_option.field), key=lambda x: x.get(sort_option.field),
reverse=(sort_option.order.lower() == "desc") reverse=(sort_option.order.lower() == "desc")
) )
# Filter data if a search term is provided # Filter data if a search term is provided
if options.search: if options.search:
devices_data = [ devices_data = [
@@ -99,7 +109,17 @@ class Query(ObjectType):
if options.search.lower() in device.get("devName", "").lower() if options.search.lower() in device.get("devName", "").lower()
] ]
return DeviceResult(devices=devices_data, count=total_count) # Then apply pagination
if options.page and options.limit:
start = (options.page - 1) * options.limit
end = start + options.limit
devices_data = devices_data[start:end]
# Convert dict objects to Device instances to enable field resolution
devices = [Device(**device) for device in devices_data]
return DeviceResult(devices=devices, count=total_count)
# Schema Definition # Schema Definition

View File

@@ -12,7 +12,8 @@ INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/server"])
from logger import mylog from logger import mylog
from helper import get_setting_value from helper import get_setting_value, timeNowTZ
from notification import write_notification
app = Flask(__name__) app = Flask(__name__)
@@ -24,22 +25,23 @@ def graphql_endpoint():
# Check for API token in headers # Check for API token in headers
token = request.headers.get("Authorization") token = request.headers.get("Authorization")
if token != f"Bearer {API_TOKEN}": if token != f"Bearer {API_TOKEN}":
mylog('none', [f'[graphql_server] Unauthorized access attempt']) mylog('verbose', [f'[graphql_server] Unauthorized access attempt'])
return jsonify({"error": "Unauthorized"}), 401 return jsonify({"error": "Unauthorized"}), 401
data = request.get_json() data = request.get_json()
mylog('none', [f'[graphql_server] data: {data}']) mylog('verbose', [f'[graphql_server] data: {data}'])
# Use the schema to execute the GraphQL query # Use the schema to execute the GraphQL query
result = devicesSchema.execute(data.get("query"), variables=data.get("variables")) result = devicesSchema.execute(data.get("query"), variables=data.get("variables"))
mylog('none', [f'[graphql_server] result: {result}'])
# Return the data from the query in JSON format # Return the data from the query in JSON format
return jsonify(result.data) return jsonify(result.data)
def start_server(): def start_server():
"""Function to start the GraphQL server in a background thread.""" """Function to start the GraphQL server in a background thread."""
mylog('none', [f'[graphql_server] Starting on port "{GRAPHQL_PORT}"']) mylog('verbose', [f'[graphql_server] Starting on port: {GRAPHQL_PORT}'])
# Start the Flask app in a separate thread # Start the Flask app in a separate thread
thread = threading.Thread(target=lambda: app.run(host="0.0.0.0", port=GRAPHQL_PORT, debug=True, use_reloader=False)) thread = threading.Thread(target=lambda: app.run(host="0.0.0.0", port=GRAPHQL_PORT, debug=True, use_reloader=False))

View File

@@ -17,6 +17,7 @@ import base64
import hashlib import hashlib
import random import random
import string import string
import ipaddress
import conf import conf
@@ -911,6 +912,42 @@ def generate_random_string(length):
characters = string.ascii_letters + string.digits characters = string.ascii_letters + string.digits
return ''.join(random.choice(characters) for _ in range(length)) return ''.join(random.choice(characters) for _ in range(length))
# Helper function to determine if a MAC address is random
def is_random_mac(mac):
# Check if second character matches "2", "6", "A", "E" (case insensitive)
is_random = mac[1].upper() in ["2", "6", "A", "E"]
# Check against user-defined non-random MAC prefixes
if is_random:
not_random_prefixes = get_setting_value("UI_NOT_RANDOM_MAC")
for prefix in not_random_prefixes:
if mac.startswith(prefix):
is_random = False
break
return is_random
# Helper function to calculate number of children
def get_number_of_children(mac, devices):
# Count children by checking devParentMAC for each device
return sum(1 for dev in devices if dev.get("devParentMAC", "").strip() == mac.strip())
# Function to convert IP to a long integer
def format_ip_long(ip_address):
try:
# Check if it's an IPv6 address
if ':' in ip_address:
ip = ipaddress.IPv6Address(ip_address)
else:
# Assume it's an IPv4 address
ip = ipaddress.IPv4Address(ip_address)
return int(ip)
except ValueError:
# Return a default error value if IP is invalid
return -1
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
# JSON methods # JSON methods
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------