Files
NetAlertX/front/deviceDetailsEdit.php
2025-12-30 10:55:55 +11:00

509 lines
20 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
//------------------------------------------------------------------------------
// check if authenticated
require_once $_SERVER["DOCUMENT_ROOT"] . "/php/templates/security.php"; ?>
<div class="row" id="deviceDetailsEdit">
<div class="box-body form-horizontal">
<form id="edit-form">
<!-- Form fields will be appended here -->
</form>
</div>
<!-- Buttons -->
<div class="col-xs-12">
<div class="pull-right">
<button type="button"
class="btn btn-default pa-btn pa-btn-delete"
style="margin-left:0px;"
id="btnDelete"
onclick="askDeleteDevice()">
<i class="fas fa-trash-alt"></i>
<?= lang("DevDetail_button_Delete") ?>
</button>
<button type="button"
class="btn btn-primary pa-btn"
style="margin-left:6px; "
id="btnSave"
onclick="setDeviceData()" >
<i class="fas fa-save"></i>
<?= lang("DevDetail_button_Save") ?>
</button>
</div>
</div>
</div>
<script defer>
// -------------------------------------------------------------------
// 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
$.ajax({
url: url,
method: "GET",
headers: {
"Authorization": `Bearer ${apiToken}`
},
dataType: "json",
success: function(deviceData) {
// some race condition, need to implement delay
setTimeout(() => {
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"]
// 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"];
// Grouping of fields into categories with associated documentation links
const fieldGroups = {
// Group for device main information
DevDetail_MainInfo_Title: {
data: ["devMac", "devLastIP", "devName", "devOwner", "devType", "devVendor", "devGroup", "devIcon", "devLocation", "devComments"],
docs: "https://github.com/jokob-sk/NetAlertX/blob/main/docs/DEVICE_MANAGEMENT.md",
iconClass: "fa fa-pencil",
inputGroupClasses: "field-group main-group col-lg-4 col-sm-6 col-xs-12",
labelClasses: "col-sm-4 col-xs-12 control-label",
inputClasses: "col-sm-8 col-xs-12 input-group"
},
// Group for event and alert settings
DevDetail_EveandAl_Title: {
data: ["devAlertEvents", "devAlertDown", "devSkipRepeated", "devReqNicsOnline", "devChildrenNicsDynamic"],
docs: "https://github.com/jokob-sk/NetAlertX/blob/main/docs/NOTIFICATIONS.md",
iconClass: "fa fa-bell",
inputGroupClasses: "field-group alert-group col-lg-4 col-sm-6 col-xs-12",
labelClasses: "col-sm-4 col-xs-12 control-label",
inputClasses: "col-sm-8 col-xs-12 input-group"
},
// Group for network details
DevDetail_MainInfo_Network_Title: {
data: ["devParentMAC", "devParentRelType", "devParentPort", "devSSID", "devSite", "devSyncHubNode"],
docs: "https://github.com/jokob-sk/NetAlertX/blob/main/docs/NETWORK_TREE.md",
iconClass: "fa fa-sitemap fa-rotate-270",
inputGroupClasses: "field-group network-group col-lg-4 col-sm-6 col-xs-12",
labelClasses: "col-sm-4 col-xs-12 control-label",
inputClasses: "col-sm-8 col-xs-12 input-group"
},
// Group for other fields like static IP, archived status, etc.
DevDetail_DisplayFields_Title: {
data: ["devStaticIP", "devIsNew", "devFavorite", "devIsArchived"],
docs: "https://github.com/jokob-sk/NetAlertX/blob/main/docs/DEVICE_DISPLAY_SETTINGS.md",
iconClass: "fa fa-list-check",
inputGroupClasses: "field-group display-group col-lg-4 col-sm-6 col-xs-12",
labelClasses: "col-sm-4 col-xs-12 control-label",
inputClasses: "col-sm-8 col-xs-12 input-group"
},
// Group for session information
DevDetail_SessionInfo_Title: {
data: ["devStatus", "devLastConnection", "devFirstConnection", "devFQDN"],
docs: "https://github.com/jokob-sk/NetAlertX/blob/main/docs/SESSION_INFO.md",
iconClass: "fa fa-calendar",
inputGroupClasses: "field-group session-group col-lg-4 col-sm-6 col-xs-12",
labelClasses: "col-sm-4 col-xs-12 control-label",
inputClasses: "col-sm-8 col-xs-12 input-group"
},
// Group for Children.
DevDetail_Children_Title: {
data: ["devChildrenDynamic"],
docs: "https://github.com/jokob-sk/NetAlertX/blob/main/docs/NETWORK_TREE.md",
iconClass: "fa fa-list",
inputGroupClasses: "field-group cutprop-group col-lg-6 col-sm-12 col-xs-12",
labelClasses: "col-sm-12 col-xs-12 control-label",
inputClasses: "col-sm-12 col-xs-12 input-group"
},
// Group for Custom properties.
DevDetail_CustomProperties_Title: {
data: ["devCustomProps"],
docs: "https://github.com/jokob-sk/NetAlertX/blob/main/docs/CUSTOM_PROPERTIES.md",
iconClass: "fa fa-list",
inputGroupClasses: "field-group cutprop-group col-lg-6 col-sm-12 col-xs-12",
labelClasses: "col-sm-12 col-xs-12 control-label",
inputClasses: "col-sm-12 col-xs-12 input-group"
}
};
// Filter settings data to get relevant settings
const relevantSettings = settingsData.filter(set =>
set.setGroup === "NEWDEV" && // Filter for settings in the "NEWDEV" group
set.setKey.includes("_dev") && // Include settings with '_dev' in the key
!hiddenFields.includes(set.setKey) && // Exclude settings listed in hiddenFields
!set.setKey.includes("__metadata") // Exclude metadata fields
);
// Function to generate the form
const generateSimpleForm = settings => {
const form = $('#edit-form'); // Get the form element to append generated fields
// Loop over each field group to generate sections for each category
Object.entries(fieldGroups).forEach(([groupName, obj]) => {
const groupDiv = $('<div>').addClass(obj.inputGroupClasses); // Create a div for each group with responsive Bootstrap classes
// Add group title and documentation link
groupDiv.append(`<h5><i class="${obj.iconClass}"></i> ${getString(groupName)}
<span class="helpIconSmallTopRight">
<a target="_blank" href="${obj.docs}">
<i class="fa fa-circle-question"></i>
</a>
</span>
</h5>
<hr>
`);
// Filter relevant settings for the current group
const groupSettings = settings.filter(set => obj.data.includes(set.setKey.replace('NEWDEV_', '')));
// Loop over each setting in the group to generate form fields
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(fieldData);
// Additional form elements like the random MAC address button for devMac
let inlineControl = "";
// handle random mac
if (setting.setKey == "NEWDEV_devMac" && deviceData["devIsRandomMAC"] == true) {
inlineControl += `<span class="input-group-addon pointer"
title="${getString("RandomMAC_hover")}">
<a href="https://github.com/jokob-sk/NetAlertX/blob/main/docs/RANDOM_MAC.md" target="_blank">
<i class="fa-solid fa-shuffle"></i>
</a>
</span>`;
}
// handle FAKE mac
if (setting.setKey == "NEWDEV_devMac" && isFakeMac(deviceData["devMac"])) {
inlineControl += `<span class="input-group-addon"
title="${getString("FakeMAC_hover")}">
<i class="fa-solid fa-dice"></i>
</span>`;
}
// handle generate MAC for new device
if (setting.setKey == "NEWDEV_devMac" && deviceData["devMac"] == "") {
inlineControl += `<span class="input-group-addon pointer"
onclick="generate_NEWDEV_devMac()"
title="${getString("Gen_Generate")}">
<i class="fa-solid fa-dice" ></i>
</span>`;
}
// handle generate IP for new device
if (setting.setKey == "NEWDEV_devLastIP" && deviceData["devLastIP"] == "") {
inlineControl += `<span class="input-group-addon pointer"
onclick="generate_NEWDEV_devLastIP()"
title="${getString("Gen_Generate")}">
<i class="fa-solid fa-dice" ></i>
</span>`;
}
// handle devChildrenDynamic or NEWDEV_devChildrenNicsDynamic - selected values and options are the same
if (
Array.isArray(fieldData) &&
(setting.setKey == "NEWDEV_devChildrenDynamic" ||
setting.setKey == "NEWDEV_devChildrenNicsDynamic")
) {
fieldDataNew = []
fieldData.forEach(child => {
fieldDataNew.push(child.devMac)
})
fieldData = fieldDataNew;
fieldOptionsOverride = fieldDataNew;
}
// Generate the input field HTML
const inputFormHtml = `<div class="form-group col-xs-12">
<label id="${setting.setKey}_label" class="${obj.labelClasses}" > ${setting.setName}
<i my-set-key="${setting.setKey}"
title="${getString("Settings_Show_Description")}"
class="fa fa-circle-info pointer helpIconSmallTopRight"
onclick="showDescriptionPopup(this)">
</i>
</label>
<div class="${obj.inputClasses}">
${generateFormHtml(settingsData, setting, fieldData.toString(), fieldOptionsOverride, null)}
${inlineControl}
</div>
</div>`;
column.append(inputFormHtml); // Append the input field to the column
groupDiv.append(column); // Append the column to the group div
});
form.append(groupDiv); // Append the group div (containing columns) to the form
});
// wait until everything is initialized to update icons
updateAllIconPreviews();
// update readonly fields
handleReadOnly(settingsData, disabledFields);
};
// console.log(relevantSettings)
generateSimpleForm(relevantSettings);
toggleNetworkConfiguration(mac == 'Internet')
initSelect2();
initHoverNodeInfo();
hideSpinner();
}}); // $.get callback
}, 100); // setTimeout
} // ajax success
}); // $.ajax
} // getDeviceData
// ----------------------------------------
// Handle the read-only fields
function handleReadOnly(settingsData, disabledFields) {
settingsData.forEach(setting => {
const element = $(`#${setting.setKey}`);
if (disabledFields.includes(setting.setKey)) {
element.prop('readonly', true);
} else {
element.prop('readonly', false);
}
});
}
// -----------------------------------------------------------------------------
// Save device data to DB
function setDeviceData(direction = '', refreshCallback = '') {
// Check MAC
mac = getMac()
if (isEmpty(mac)) {
console.error("Mac not defined");
return;
}
// Determine if a new device should be created
const createNew = mac === 'new' ? 1 : 0;
const devLastIP = $('#NEWDEV_devLastIP').val();
const newMac = $('#NEWDEV_devMac').val()
// Validate MAC and Last IP
if (mac === '' || !isValidMac(newMac) || !(isValidIPv4(devLastIP) || isValidIPv6(devLastIP))) {
showMessage(getString("DeviceEdit_ValidMacIp"), 5000, "modal_red");
return;
}
showSpinner();
const apiToken = getSetting("API_TOKEN"); // dynamic token
const host = window.location.hostname;
const protocol = window.location.protocol;
const port = getSetting("GRAPHQL_PORT");
mac = $('#NEWDEV_devMac').val();
// Build payload for new endpoint
const payload = {
devName: $('#NEWDEV_devName').val().replace(/'/g, ""),
devOwner: $('#NEWDEV_devOwner').val().replace(/'/g, ""),
devType: $('#NEWDEV_devType').val().replace(/'/g, ""),
devVendor: $('#NEWDEV_devVendor').val().replace(/'/g, ""),
devIcon: $('#NEWDEV_devIcon').val(),
devFavorite: ($('#NEWDEV_devFavorite')[0].checked * 1),
devGroup: $('#NEWDEV_devGroup').val().replace(/'/g, ""),
devLocation: $('#NEWDEV_devLocation').val().replace(/'/g, ""),
devComments: encodeSpecialChars($('#NEWDEV_devComments').val()),
devParentMAC: $('#NEWDEV_devParentMAC').val(),
devParentPort: $('#NEWDEV_devParentPort').val(),
devParentRelType: $('#NEWDEV_devParentRelType').val().replace(/'/g, ""),
devSSID: $('#NEWDEV_devSSID').val(),
devSite: $('#NEWDEV_devSite').val(),
devStaticIP: ($('#NEWDEV_devStaticIP')[0].checked * 1),
devScan: 1,
devAlertEvents: ($('#NEWDEV_devAlertEvents')[0].checked * 1),
devAlertDown: ($('#NEWDEV_devAlertDown')[0].checked * 1),
devSkipRepeated: $('#NEWDEV_devSkipRepeated').val().split(' ')[0],
devReqNicsOnline: ($('#NEWDEV_devReqNicsOnline')[0].checked * 1),
devIsNew: ($('#NEWDEV_devIsNew')[0].checked * 1),
devIsArchived: ($('#NEWDEV_devIsArchived')[0].checked * 1),
devFirstConnection: $('#NEWDEV_devFirstConnection').val(),
devLastConnection: $('#NEWDEV_devLastConnection').val(),
devLastIP: $('#NEWDEV_devLastIP').val(),
devCustomProps: btoa(
JSON.stringify(collectTableData("#NEWDEV_devCustomProps_table"))
),
createNew: createNew
};
$.ajax({
url: `${protocol}//${host}:${port}/device/${encodeURIComponent(mac)}`,
type: "POST",
headers: {
"Authorization": "Bearer " + apiToken,
"Content-Type": "application/json"
},
data: JSON.stringify(payload),
success: function(resp) {
if (resp && resp.success) {
showMessage("Device saved successfully");
} else {
showMessage("Device update returned an unexpected response");
}
// Remove navigation prompt
window.onbeforeunload = null;
somethingChanged = false;
// Refresh API
updateApi("devices,appevents");
// Callback
if (typeof refreshCallback === "function") {
refreshCallback(direction);
}
hideSpinner();
},
error: function(xhr) {
if (xhr.status === 403) {
showMessage("Unauthorized - invalid API token");
} else {
showMessage("Failed to save device (" + xhr.status + ")");
}
hideSpinner();
}
});
}
//-----------------------------------------------------------------------------------
// 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);
} 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);
}
}
// -----------------------------------------------
// INIT with polling for panel element visibility
// -----------------------------------------------
var deviceDetailsPageInitialized = false;
function initdeviceDetailsPage() {
// Only proceed if .plugin-content is visible
if (!$('#panDetails:visible').length) {
return; // exit early if nothing is visible
}
// init page once
if (deviceDetailsPageInitialized) return; // ENSURE ONCE
deviceDetailsPageInitialized = true;
showSpinner();
getDeviceData();
}
// -----------------------------------------------------------------------------
// Recurring function to monitor the URL and reinitialize if needed
function deviceDetailsPageUpdater() {
initdeviceDetailsPage();
// Run updater again after delay
setTimeout(deviceDetailsPageUpdater, 200);
}
// if visible, load immediately, if not start updater
if (!$('#panDetails:visible').length) {
deviceDetailsPageUpdater();
} else {
getDeviceData();
}
</script>