FE+BE: deviceDetials migration to graphQL endpoints

Signed-off-by: jokob-sk <jokob.sk@gmail.com>
This commit is contained in:
jokob-sk
2025-12-25 11:39:28 +11:00
parent d119708538
commit ee5de27413
4 changed files with 382 additions and 465 deletions
+18 -2
View File
@@ -416,9 +416,25 @@ async function renderSmallBoxes() {
showSpinner(); showSpinner();
// Get data from the server // Get data from the server
const response = await fetch(`php/server/devices.php?action=getServerDeviceData&mac=${getMac()}&period=${period}`); const protocol = window.location.protocol.replace(':', '');
const host = window.location.hostname;
const apiToken = getSetting("API_TOKEN");
const port = getSetting("GRAPHQL_PORT"); // same port your Flask app runs on
const apiBase = `${protocol}://${host}:${port}`;
const url = `${apiBase}/device/${getMac()}?period=${encodeURIComponent(period)}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Authorization": `Bearer ${apiToken}`,
"Content-Type": "application/json"
}
});
if (!response.ok) { if (!response.ok) {
throw new Error(`Error fetching device data: ${response.statusText}`); const text = await response.text();
throw new Error(`Error fetching device data: ${response.status} ${text}`);
} }
const deviceData = await response.json(); const deviceData = await response.json();
+95 -60
View File
@@ -1,8 +1,7 @@
<?php <?php
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// check if authenticated // check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php'; require_once $_SERVER["DOCUMENT_ROOT"] . "/php/templates/security.php"; ?>
?>
<div class="row" id="deviceDetailsEdit"> <div class="row" id="deviceDetailsEdit">
@@ -20,7 +19,7 @@
id="btnDelete" id="btnDelete"
onclick="askDeleteDevice()"> onclick="askDeleteDevice()">
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
<?= lang('DevDetail_button_Delete');?> <?= lang("DevDetail_button_Delete") ?>
</button> </button>
<button type="button" <button type="button"
class="btn btn-primary pa-btn" class="btn btn-primary pa-btn"
@@ -28,7 +27,7 @@
id="btnSave" id="btnSave"
onclick="setDeviceData()" > onclick="setDeviceData()" >
<i class="fas fa-save"></i> <i class="fas fa-save"></i>
<?= lang('DevDetail_button_Save');?> <?= lang("DevDetail_button_Save") ?>
</button> </button>
</div> </div>
</div> </div>
@@ -37,36 +36,78 @@
<script defer> <script defer>
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Get plugin and settings data from API endpoints // Get plugin and settings data from API endpoints
function getDeviceData(){ function getDeviceData() {
mac = getMac() mac = getMac()
console.log(mac); console.log(mac);
const protocol = window.location.protocol.replace(':', '');
const host = window.location.hostname;
const apiToken = getSetting("API_TOKEN");
const port = getSetting("GRAPHQL_PORT");
const apiBase = `${protocol}://${host}:${port}`;
const url = `${apiBase}/device/${mac}?period=${encodeURIComponent(period)}`;
// get data from server // get data from server
$.get('php/server/devices.php?action=getServerDeviceData&mac='+ mac + '&period='+ period, function(data) { $.ajax({
url: url,
// show loading dialog method: "GET",
showSpinner() headers: {
"Authorization": `Bearer ${apiToken}`
var deviceData = JSON.parse(data); },
dataType: "json",
success: function(deviceData) {
// some race condition, need to implement delay // some race condition, need to implement delay
setTimeout(() => { setTimeout(() => {
$.get('php/server/query_json.php', {
file: 'table_settings.json',
// nocache: Date.now()
},
function(res) {
settingsData = res["data"]; const query = `
query($filters: [FilterOptionsInput]) {
settings(filters: $filters) {
settings {
setKey
setName
setDescription
setType
setOptions
setGroup
setValue
setEvents
setOverriddenByEnv
}
count
}
}
`;
// I need to get a subset of settings only (performance improvement), but include both NEWDEV and CUSTPROP settings
const variables = {
filters: [
{ filterColumn: "setGroup", filterValue: "NEWDEV" },
{ filterColumn: "setGroup", filterValue: "CUSTPROP" }
]
};
const graphQlUrl = `${apiBase}/graphql`;
$.ajax({
url: graphQlUrl,
method: "POST",
contentType: "application/json",
headers: { "Authorization": `Bearer ${apiToken}` },
data: JSON.stringify({ query, variables }),
success: function(response) {
const settingsData = response.data.settings.settings;
// columns to hide // columns to hide
hiddenFields = ["NEWDEV_devScan", "NEWDEV_devPresentLastScan" ] hiddenFields = ["NEWDEV_devScan", "NEWDEV_devPresentLastScan"]
// columns to disable/readonly - conditional depending if a new dummy device is created // columns to disable/readonly - conditional depending if a new dummy device is created
disabledFields = mac == "new" ? ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection"] : ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection", "NEWDEV_devMac", "NEWDEV_devLastIP", "NEWDEV_devSyncHubNode", "NEWDEV_devFQDN" ]; disabledFields = mac == "new" ? ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection"] : ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection", "NEWDEV_devMac", "NEWDEV_devLastIP", "NEWDEV_devSyncHubNode", "NEWDEV_devFQDN"];
// Grouping of fields into categories with associated documentation links // Grouping of fields into categories with associated documentation links
const fieldGroups = { const fieldGroups = {
@@ -169,12 +210,14 @@
groupSettings.forEach(setting => { groupSettings.forEach(setting => {
const column = $('<div>'); // Create a column for each setting (Bootstrap column) const column = $('<div>'); // Create a column for each setting (Bootstrap column)
console.log(setting);
// Get the field data (replace 'NEWDEV_' prefix from the key) // Get the field data (replace 'NEWDEV_' prefix from the key)
fieldData = deviceData[setting.setKey.replace('NEWDEV_', '')] fieldData = deviceData[setting.setKey.replace('NEWDEV_', '')]
fieldData = fieldData == null ? "" : fieldData; fieldData = fieldData == null ? "" : fieldData;
fieldOptionsOverride = null; fieldOptionsOverride = null;
// console.log(setting.setKey); console.log(setting.setKey);
// console.log(fieldData); // console.log(fieldData);
// Additional form elements like the random MAC address button for devMac // Additional form elements like the random MAC address button for devMac
@@ -209,9 +252,8 @@
if ( if (
Array.isArray(fieldData) && Array.isArray(fieldData) &&
(setting.setKey == "NEWDEV_devChildrenDynamic" || (setting.setKey == "NEWDEV_devChildrenDynamic" ||
setting.setKey == "NEWDEV_devChildrenNicsDynamic" ) setting.setKey == "NEWDEV_devChildrenNicsDynamic")
) ) {
{
fieldDataNew = [] fieldDataNew = []
fieldData.forEach(child => { fieldData.forEach(child => {
fieldDataNew.push(child.devMac) fieldDataNew.push(child.devMac)
@@ -261,19 +303,16 @@
hideSpinner(); hideSpinner();
}) }}); // $.get callback
}, 100); // setTimeout
}, 100); } // ajax success
}); }); // $.ajax
} // getDeviceData
}
// ----------------------------------------
// Handle the read-only fields
// ---------------------------------------- function handleReadOnly(settingsData, disabledFields) {
// Handle the read-only fields
function handleReadOnly(settingsData, disabledFields) {
settingsData.forEach(setting => { settingsData.forEach(setting => {
const element = $(`#${setting.setKey}`); const element = $(`#${setting.setKey}`);
if (disabledFields.includes(setting.setKey)) { if (disabledFields.includes(setting.setKey)) {
@@ -282,11 +321,11 @@
element.prop('readonly', false); element.prop('readonly', false);
} }
}); });
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Save device data to DB // Save device data to DB
function setDeviceData(direction = '', refreshCallback = '') { function setDeviceData(direction = '', refreshCallback = '') {
// Check MAC // Check MAC
mac = getMac() mac = getMac()
@@ -303,7 +342,7 @@
const newMac = $('#NEWDEV_devMac').val() const newMac = $('#NEWDEV_devMac').val()
// Validate MAC and Last IP // Validate MAC and Last IP
if (mac === '' || !isValidMac(newMac) || !( isValidIPv4(devLastIP) || isValidIPv6(devLastIP) )) { if (mac === '' || !isValidMac(newMac) || !(isValidIPv4(devLastIP) || isValidIPv6(devLastIP))) {
showMessage(getString("DeviceEdit_ValidMacIp"), 5000, "modal_red"); showMessage(getString("DeviceEdit_ValidMacIp"), 5000, "modal_red");
return; return;
} }
@@ -368,7 +407,7 @@
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
data: JSON.stringify(payload), data: JSON.stringify(payload),
success: function (resp) { success: function(resp) {
if (resp && resp.success) { if (resp && resp.success) {
showMessage("Device saved successfully"); showMessage("Device saved successfully");
@@ -390,7 +429,7 @@
hideSpinner(); hideSpinner();
}, },
error: function (xhr) { error: function(xhr) {
if (xhr.status === 403) { if (xhr.status === 403) {
showMessage("Unauthorized - invalid API token"); showMessage("Unauthorized - invalid API token");
} else { } else {
@@ -399,27 +438,27 @@
hideSpinner(); hideSpinner();
} }
}); });
} }
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
// Disables or enables network configuration for the root node // Disables or enables network configuration for the root node
function toggleNetworkConfiguration(disable) { function toggleNetworkConfiguration(disable) {
if (disable) { if (disable) {
// Completely disable the NEWDEV_devParentMAC <select> and NEWDEV_devParentPort // Completely disable the NEWDEV_devParentMAC <select> and NEWDEV_devParentPort
$('#NEWDEV_devParentMAC').prop('disabled', true).val("").prop('selectedIndex', 0); $('#NEWDEV_devParentMAC').prop('disabled', true).val("").prop('selectedIndex', 0);
$('#NEWDEV_devParentMAC').empty() // Remove all options $('#NEWDEV_devParentMAC').empty() // Remove all options
.append('<option value="">Root Node</option>') .append('<option value="">Root Node</option>')
$('#NEWDEV_devParentPort').prop('disabled', true); $('#NEWDEV_devParentPort').prop('disabled', true);
$('#NEWDEV_devParentPort').prop('readonly', true ); $('#NEWDEV_devParentPort').prop('readonly', true);
$('#NEWDEV_devParentMAC').prop('readonly', true ); $('#NEWDEV_devParentMAC').prop('readonly', true);
} else { } else {
// Enable the NEWDEV_devParentMAC <select> and NEWDEV_devParentPort // Enable the NEWDEV_devParentMAC <select> and NEWDEV_devParentPort
$('#NEWDEV_devParentMAC').prop('disabled', false); $('#NEWDEV_devParentMAC').prop('disabled', false);
$('#NEWDEV_devParentPort').prop('disabled', false); $('#NEWDEV_devParentPort').prop('disabled', false);
$('#NEWDEV_devParentPort').prop('readonly', false ); $('#NEWDEV_devParentPort').prop('readonly', false);
$('#NEWDEV_devParentMAC').prop('readonly', false ); $('#NEWDEV_devParentMAC').prop('readonly', false);
}
} }
}
// ----------------------------------------------- // -----------------------------------------------
// INIT with polling for panel element visibility // INIT with polling for panel element visibility
@@ -427,8 +466,7 @@
var deviceDetailsPageInitialized = false; var deviceDetailsPageInitialized = false;
function initdeviceDetailsPage() function initdeviceDetailsPage() {
{
// Only proceed if .plugin-content is visible // Only proceed if .plugin-content is visible
if (!$('#panDetails:visible').length) { if (!$('#panDetails:visible').length) {
return; // exit early if nothing is visible return; // exit early if nothing is visible
@@ -456,12 +494,9 @@ function deviceDetailsPageUpdater() {
// if visible, load immediately, if not start updater // if visible, load immediately, if not start updater
if (!$('#panDetails:visible').length) { if (!$('#panDetails:visible').length) {
deviceDetailsPageUpdater(); deviceDetailsPageUpdater();
} } else {
else
{
getDeviceData(); getDeviceData();
} }
</script> </script>
-145
View File
@@ -31,7 +31,6 @@
$action = $_REQUEST['action']; $action = $_REQUEST['action'];
switch ($action) { switch ($action) {
// check server/api_server/api_server_start.py for equivalents // check server/api_server/api_server_start.py for equivalents
case 'getServerDeviceData': getServerDeviceData(); break; // equivalent: get_device_data
case 'deleteDevice': deleteDevice(); break; // equivalent: delete_device(mac) case 'deleteDevice': deleteDevice(); break; // equivalent: delete_device(mac)
case 'deleteAllWithEmptyMACs': deleteAllWithEmptyMACs(); break; // equivalent: delete_all_with_empty_macs case 'deleteAllWithEmptyMACs': deleteAllWithEmptyMACs(); break; // equivalent: delete_all_with_empty_macs
@@ -55,150 +54,6 @@
} }
//------------------------------------------------------------------------------
// Query Device Data
//------------------------------------------------------------------------------
function getServerDeviceData() {
global $db;
// Request Parameters
$periodDate = getDateFromPeriod();
$mac = $_REQUEST['mac'];
// Check for "new" MAC case
if ($mac === "new") {
$now = date('Y-m-d H:i');
$deviceData = [
"devMac" => "",
"devName" => "",
"devOwner" => "",
"devType" => "",
"devVendor" => "",
"devFavorite" => 0,
"devGroup" => "",
"devComments" => "",
"devFirstConnection" => $now,
"devLastConnection" => $now,
"devLastIP" => "",
"devStaticIP" => 0,
"devScan" => 0,
"devLogEvents" => 0,
"devAlertEvents" => 0,
"devAlertDown" => 0,
"devParentRelType" => "default",
"devReqNicsOnline" => 0,
"devSkipRepeated" => 0,
"devLastNotification" => "",
"devPresentLastScan" => 0,
"devIsNew" => 1,
"devLocation" => "",
"devIsArchived" => 0,
"devParentMAC" => "",
"devParentPort" => "",
"devIcon" => "",
"devGUID" => "",
"devSite" => "",
"devSSID" => "",
"devSyncHubNode" => "",
"devSourcePlugin" => "",
"devCustomProps" => "",
"devStatus" => "Unknown",
"devIsRandomMAC" => false,
"devSessions" => 0,
"devEvents" => 0,
"devDownAlerts" => 0,
"devPresenceHours" => 0,
"devFQDN" => ""
];
echo json_encode($deviceData);
return;
}
// Get current date (used in presence calc)
$currentdate = date("Y-m-d H:i:s");
// Fetch Device Info + Children + Events Stats
$sql =<<<SQL
SELECT
d.rowid,
d.*,
CASE
WHEN d.devAlertDown != 0 AND d.devPresentLastScan = 0 THEN "Down"
WHEN d.devPresentLastScan = 1 THEN "On-line"
ELSE "Off-line"
END AS devStatus,
-- Event counters
(SELECT COUNT(*) FROM Sessions
WHERE ses_MAC = d.devMac AND (
ses_DateTimeConnection >= $periodDate OR
ses_DateTimeDisconnection >= $periodDate OR
ses_StillConnected = 1
)
) AS devSessions,
(SELECT COUNT(*) FROM Events
WHERE eve_MAC = d.devMac AND
eve_DateTime >= $periodDate AND
eve_EventType NOT IN ("Connected", "Disconnected")
) AS devEvents,
(SELECT COUNT(*) FROM Events
WHERE eve_MAC = d.devMac AND
eve_DateTime >= $periodDate AND
eve_EventType = "Device Down"
) AS devDownAlerts,
(SELECT CAST(( MAX (0, SUM (julianday (IFNULL (ses_DateTimeDisconnection,'$currentdate'))
- julianday (CASE WHEN ses_DateTimeConnection < $periodDate
THEN $periodDate
ELSE ses_DateTimeConnection END)) *24 )) AS INT)
FROM Sessions
WHERE ses_MAC = d.devMac AND
ses_DateTimeConnection IS NOT NULL AND
(ses_DateTimeDisconnection IS NOT NULL OR ses_StillConnected = 1) AND
(
ses_DateTimeConnection >= $periodDate OR
ses_DateTimeDisconnection >= $periodDate OR
ses_StillConnected = 1
)
) AS devPresenceHours
FROM Devices d
WHERE d.devMac = "$mac" OR CAST(d.rowid AS TEXT) = "$mac"
SQL;
$row = $db->query($sql)->fetchArray(SQLITE3_ASSOC);
$deviceData = $row;
$mac = $deviceData['devMac'];
$deviceData['devFirstConnection'] = formatDate($deviceData['devFirstConnection']);
$deviceData['devLastConnection'] = formatDate($deviceData['devLastConnection']);
$deviceData['devIsRandomMAC'] = isRandomMAC($mac);
// Fetch children once and split in PHP
$sql = 'SELECT rowid, * FROM Devices WHERE devParentMAC = "' . $mac . '" ORDER BY devPresentLastScan DESC';
$result = $db->query($sql);
$children = [];
$childrenNics = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$children[] = $row;
if ($row['devParentRelType'] === 'nic') {
$childrenNics[] = $row;
}
}
$deviceData['devChildrenDynamic'] = $children;
$deviceData['devChildrenNicsDynamic'] = $childrenNics;
// Return JSON
echo json_encode($deviceData);
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Delete Device // Delete Device
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
+14 -3
View File
@@ -366,9 +366,9 @@ class Query(ObjectType):
return DeviceResult(devices=devices, count=total_count) return DeviceResult(devices=devices, count=total_count)
# --- SETTINGS --- # --- SETTINGS ---
settings = Field(SettingResult) settings = Field(SettingResult, filters=List(FilterOptionsInput))
def resolve_settings(root, info): def resolve_settings(root, info, filters=None):
try: try:
with open(folder + "table_settings.json", "r") as f: with open(folder + "table_settings.json", "r") as f:
settings_data = json.load(f)["data"] settings_data = json.load(f)["data"]
@@ -379,7 +379,18 @@ class Query(ObjectType):
mylog("trace", f"[graphql_schema] settings_data: {settings_data}") mylog("trace", f"[graphql_schema] settings_data: {settings_data}")
# Convert to Setting objects # Convert to Setting objects
settings = [Setting(**setting) for setting in settings_data] settings = [Setting(**s) for s in settings_data]
# Apply dynamic filters (OR)
if filters:
filtered_settings = []
for s in settings:
for f in filters:
if f.filterColumn and f.filterValue is not None:
if str(getattr(s, f.filterColumn, "")).lower() == str(f.filterValue).lower():
filtered_settings.append(s)
break # match one filter is enough (OR)
settings = filtered_settings
return SettingResult(settings=settings, count=len(settings)) return SettingResult(settings=settings, count=len(settings))