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();
// 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) {
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();
+95 -60
View File
@@ -1,8 +1,7 @@
<?php
//------------------------------------------------------------------------------
// check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
?>
//------------------------------------------------------------------------------
// check if authenticated
require_once $_SERVER["DOCUMENT_ROOT"] . "/php/templates/security.php"; ?>
<div class="row" id="deviceDetailsEdit">
@@ -20,7 +19,7 @@
id="btnDelete"
onclick="askDeleteDevice()">
<i class="fas fa-trash-alt"></i>
<?= lang('DevDetail_button_Delete');?>
<?= lang("DevDetail_button_Delete") ?>
</button>
<button type="button"
class="btn btn-primary pa-btn"
@@ -28,7 +27,7 @@
id="btnSave"
onclick="setDeviceData()" >
<i class="fas fa-save"></i>
<?= lang('DevDetail_button_Save');?>
<?= lang("DevDetail_button_Save") ?>
</button>
</div>
</div>
@@ -37,36 +36,78 @@
<script defer>
// -------------------------------------------------------------------
// Get plugin and settings data from API endpoints
function getDeviceData(){
// -------------------------------------------------------------------
// Get plugin and settings data from API endpoints
function getDeviceData() {
mac = getMac()
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('php/server/devices.php?action=getServerDeviceData&mac='+ mac + '&period='+ period, function(data) {
// show loading dialog
showSpinner()
var deviceData = JSON.parse(data);
$.ajax({
url: url,
method: "GET",
headers: {
"Authorization": `Bearer ${apiToken}`
},
dataType: "json",
success: function(deviceData) {
// some race condition, need to implement delay
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
hiddenFields = ["NEWDEV_devScan", "NEWDEV_devPresentLastScan" ]
hiddenFields = ["NEWDEV_devScan", "NEWDEV_devPresentLastScan"]
// 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
const fieldGroups = {
@@ -169,12 +210,14 @@
groupSettings.forEach(setting => {
const column = $('<div>'); // Create a column for each setting (Bootstrap column)
console.log(setting);
// Get the field data (replace 'NEWDEV_' prefix from the key)
fieldData = deviceData[setting.setKey.replace('NEWDEV_', '')]
fieldData = fieldData == null ? "" : fieldData;
fieldOptionsOverride = null;
// console.log(setting.setKey);
console.log(setting.setKey);
// console.log(fieldData);
// Additional form elements like the random MAC address button for devMac
@@ -209,9 +252,8 @@
if (
Array.isArray(fieldData) &&
(setting.setKey == "NEWDEV_devChildrenDynamic" ||
setting.setKey == "NEWDEV_devChildrenNicsDynamic" )
)
{
setting.setKey == "NEWDEV_devChildrenNicsDynamic")
) {
fieldDataNew = []
fieldData.forEach(child => {
fieldDataNew.push(child.devMac)
@@ -261,19 +303,16 @@
hideSpinner();
})
}, 100);
});
}
}}); // $.get callback
}, 100); // setTimeout
} // 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 => {
const element = $(`#${setting.setKey}`);
if (disabledFields.includes(setting.setKey)) {
@@ -282,11 +321,11 @@
element.prop('readonly', false);
}
});
}
}
// -----------------------------------------------------------------------------
// Save device data to DB
function setDeviceData(direction = '', refreshCallback = '') {
// -----------------------------------------------------------------------------
// Save device data to DB
function setDeviceData(direction = '', refreshCallback = '') {
// Check MAC
mac = getMac()
@@ -303,7 +342,7 @@
const newMac = $('#NEWDEV_devMac').val()
// 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");
return;
}
@@ -368,7 +407,7 @@
"Content-Type": "application/json"
},
data: JSON.stringify(payload),
success: function (resp) {
success: function(resp) {
if (resp && resp.success) {
showMessage("Device saved successfully");
@@ -390,7 +429,7 @@
hideSpinner();
},
error: function (xhr) {
error: function(xhr) {
if (xhr.status === 403) {
showMessage("Unauthorized - invalid API token");
} else {
@@ -399,27 +438,27 @@
hideSpinner();
}
});
}
}
//-----------------------------------------------------------------------------------
// Disables or enables network configuration for the root node
function toggleNetworkConfiguration(disable) {
//-----------------------------------------------------------------------------------
// Disables or enables network configuration for the root node
function toggleNetworkConfiguration(disable) {
if (disable) {
// Completely disable the NEWDEV_devParentMAC <select> and NEWDEV_devParentPort
$('#NEWDEV_devParentMAC').prop('disabled', true).val("").prop('selectedIndex', 0);
$('#NEWDEV_devParentMAC').empty() // Remove all options
.append('<option value="">Root Node</option>')
$('#NEWDEV_devParentPort').prop('disabled', true);
$('#NEWDEV_devParentPort').prop('readonly', true );
$('#NEWDEV_devParentMAC').prop('readonly', true );
$('#NEWDEV_devParentPort').prop('readonly', true);
$('#NEWDEV_devParentMAC').prop('readonly', true);
} else {
// Enable the NEWDEV_devParentMAC <select> and NEWDEV_devParentPort
$('#NEWDEV_devParentMAC').prop('disabled', false);
$('#NEWDEV_devParentPort').prop('disabled', false);
$('#NEWDEV_devParentPort').prop('readonly', false );
$('#NEWDEV_devParentMAC').prop('readonly', false );
}
$('#NEWDEV_devParentPort').prop('readonly', false);
$('#NEWDEV_devParentMAC').prop('readonly', false);
}
}
// -----------------------------------------------
// INIT with polling for panel element visibility
@@ -427,8 +466,7 @@
var deviceDetailsPageInitialized = false;
function initdeviceDetailsPage()
{
function initdeviceDetailsPage() {
// Only proceed if .plugin-content is visible
if (!$('#panDetails:visible').length) {
return; // exit early if nothing is visible
@@ -456,12 +494,9 @@ function deviceDetailsPageUpdater() {
// if visible, load immediately, if not start updater
if (!$('#panDetails:visible').length) {
deviceDetailsPageUpdater();
}
else
{
} else {
getDeviceData();
}
</script>
-145
View File
@@ -31,7 +31,6 @@
$action = $_REQUEST['action'];
switch ($action) {
// 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 '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
//------------------------------------------------------------------------------
+14 -3
View File
@@ -366,9 +366,9 @@ class Query(ObjectType):
return DeviceResult(devices=devices, count=total_count)
# --- SETTINGS ---
settings = Field(SettingResult)
settings = Field(SettingResult, filters=List(FilterOptionsInput))
def resolve_settings(root, info):
def resolve_settings(root, info, filters=None):
try:
with open(folder + "table_settings.json", "r") as f:
settings_data = json.load(f)["data"]
@@ -379,7 +379,18 @@ class Query(ObjectType):
mylog("trace", f"[graphql_schema] settings_data: {settings_data}")
# 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))