mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-10-28 12:01:13 -04:00
210 lines
8.5 KiB
PHP
210 lines
8.5 KiB
PHP
<?php
|
|
//------------------------------------------------------------------------------
|
|
// check if authenticated
|
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
|
|
?>
|
|
|
|
<!-- Main content ---------------------------------------------------------- -->
|
|
<div class="nav-tabs-custom content plugin-content field-view-content">
|
|
|
|
<?php require $_SERVER['DOCUMENT_ROOT'] . '/php/templates/skel_device_details_tab_field_view.php'; ?>
|
|
|
|
<ul id="field-tabs-location" class="nav nav-tabs col-sm-2 ">
|
|
<!-- PLACEHOLDER -->
|
|
</ul>
|
|
<div id="field-tabs-content-location-wrap" class="tab-content col-sm-10">
|
|
<div id="field-tabs-content-location" class="tab-content col-sm-12">
|
|
<!-- PLACEHOLDER -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Status values from Plugins_Objects are plugin-observation state, not network
|
|
// liveness - render them accordingly rather than implying reachability.
|
|
// Localized via getString(), not hardcoded English, matching the rest of the UI.
|
|
const FIELD_VIEW_STATUS_LABELS = {
|
|
'new': 'Plugins_FieldStatus_New',
|
|
'watched-changed': 'Plugins_FieldStatus_Changed',
|
|
'watched-not-changed': 'Plugins_FieldStatus_Unchanged',
|
|
'missing-in-last-scan': 'Plugins_FieldStatus_NotReported',
|
|
};
|
|
|
|
let fieldViewLastMac = null;
|
|
let fieldViewInitialized = false;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
function hideFieldViewSkeleton() {
|
|
$('#skel-tab-field-view').fadeOut(0, function() { $(this).hide(); });
|
|
}
|
|
function showFieldViewSkeleton() {
|
|
var $skel = $('#skel-tab-field-view');
|
|
$skel.stop(true, true).fadeIn(10);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Builds the side-nav of fields and an empty shell pane per field. Each pane's
|
|
// rows are built lazily, the first time its tab is actually focused (see the
|
|
// shown.bs.tab binding below) - fieldViews/entries are already fully fetched
|
|
// by this point (one shared getSourcesFieldData() call, not a per-field
|
|
// query), so deferring is purely about not building table markup the user
|
|
// may never look at, not an extra round-trip.
|
|
// getFieldViewDefinitions() is the shared lookup declared in deviceDetails.php -
|
|
// Session Info's Known IPs chip list reads the exact same data, not a duplicate.
|
|
async function renderFieldView(mac) {
|
|
const fieldViews = await getFieldViewDefinitions();
|
|
const entries = await getSourcesFieldData(mac);
|
|
|
|
const fieldKeys = Object.keys(fieldViews);
|
|
if (fieldKeys.length === 0) {
|
|
$('#field-tabs-content-location').html(`<p class="text-muted plugin-no-data">${getString('Gen_No_Data')}</p>`);
|
|
return;
|
|
}
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const requestedField = urlParams.get('field');
|
|
const activeField = fieldKeys.includes(requestedField) ? requestedField : fieldKeys[0];
|
|
|
|
// Tear down any DataTable instances from a previous mac before wiping the
|
|
// markup they're attached to - .empty() alone would orphan them instead of
|
|
// cleanly releasing their state.
|
|
$('#field-tabs-content-location table').each(function() {
|
|
if ($.fn.DataTable.isDataTable(this)) {
|
|
$(this).DataTable().destroy();
|
|
}
|
|
});
|
|
$('#field-tabs-location').empty();
|
|
$('#field-tabs-content-location').empty();
|
|
|
|
fieldKeys.forEach(fieldKey => {
|
|
const isActive = fieldKey === activeField;
|
|
const { label_key } = fieldViews[fieldKey];
|
|
|
|
$('#field-tabs-location').append(`
|
|
<li class="left-nav ${isActive ? 'active' : ''}">
|
|
<a class="col-sm-12 textOverflow" href="#fieldPane_${fieldKey}" data-toggle="tab">
|
|
${getString(label_key)}
|
|
</a>
|
|
</li>
|
|
`);
|
|
|
|
const navItems = buildTabNavItems([
|
|
{ href: `#objectsTarget_field_${fieldKey}`, icon: 'fa-cube', label: getString('Plugins_Objects'), active: true },
|
|
]);
|
|
|
|
$('#field-tabs-content-location').append(`
|
|
<div id="fieldPane_${fieldKey}" class="tab-pane ${isActive ? 'active' : ''}">
|
|
<div class="nav-tabs-custom plugin-tab-nav">
|
|
<ul class="nav nav-tabs">${navItems}</ul>
|
|
<div class="tab-content">
|
|
<div id="objectsTarget_field_${fieldKey}" class="tab-pane active">
|
|
<table id="fieldTable_${fieldKey}" class="display table table-striped table-stretched field-pane-table"></table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`);
|
|
});
|
|
|
|
const builtFields = new Set();
|
|
function buildFieldPaneTableOnce(fieldKey) {
|
|
if (builtFields.has(fieldKey)) return;
|
|
builtFields.add(fieldKey);
|
|
buildFieldPaneTable(fieldKey, fieldViews[fieldKey], entries);
|
|
}
|
|
|
|
// The field active from the start builds immediately; every other field's
|
|
// table waits for its tab to actually be clicked.
|
|
buildFieldPaneTableOnce(activeField);
|
|
$('#field-tabs-location a[data-toggle="tab"]').off('shown.bs.tab.fieldView').on('shown.bs.tab.fieldView', function() {
|
|
buildFieldPaneTableOnce($(this).attr('href').replace('#fieldPane_', ''));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Build fieldKey's DataTable - Plugin / Value / Status / Last Changed, one row
|
|
* per participating plugin entry (a plugin can report more than one entry for
|
|
* the same device, e.g. multiple IPs - every matching entry becomes its own
|
|
* row, not just the first). Reuses the exact same shared options
|
|
* (getStandardDataTableOptions()) and cell-rendering approach
|
|
* (createdCell + getFormControl()) pluginsCore.php's own DataTables use,
|
|
* rather than a parallel hand-rolled table - only the data source differs
|
|
* (a small, already-fetched client-side array here vs. a server-paginated
|
|
* GraphQL query there). The column each plugin's value is read from is
|
|
* whatever get_plugin_columns_for_field() determined server-side, not a
|
|
* hardcoded column role; value_type comes from the field's own
|
|
* DEVICE_FIELD_VIEWS declaration (e.g. 'device_ip' for the IP field's
|
|
* clickable link, 'none' for a plain value) rather than being hardcoded to
|
|
* the IP field's rendering for every field.
|
|
* @param {string} fieldKey - The field view's key (e.g. 'ip').
|
|
* @param {object} fieldView - fieldViews[fieldKey] from getFieldViewDefinitions().
|
|
* @param {object[]} entries - This device's pluginsObjects entries from getSourcesFieldData().
|
|
*/
|
|
function buildFieldPaneTable(fieldKey, fieldView, entries) {
|
|
const { columns: pluginColumns, value_type } = fieldView;
|
|
|
|
const rows = pluginColumns.flatMap(({ plugin, column }) =>
|
|
entries
|
|
.filter(e => e.plugin === plugin)
|
|
.map(entry => ({ plugin, value: entry[column], status: entry.status, dateTimeChanged: entry.dateTimeChanged || '' }))
|
|
);
|
|
|
|
$(`#fieldTable_${fieldKey}`).DataTable({
|
|
...getStandardDataTableOptions(null), // no per-table skeleton - the whole tab shares #skel-tab-field-view
|
|
data: rows,
|
|
columns: [
|
|
{ data: 'plugin', title: getString('AppEvents_Plugin') },
|
|
{
|
|
data: 'value',
|
|
title: getString('WF_Action_value'),
|
|
createdCell: function(td, cellData, rowData) {
|
|
$(td).html(getFormControl({ type: value_type }, cellData, rowData.plugin));
|
|
}
|
|
},
|
|
{
|
|
data: 'status',
|
|
title: getString('Device_TableHead_Status'),
|
|
render: function(data) {
|
|
const statusLabelKey = FIELD_VIEW_STATUS_LABELS[data];
|
|
return statusLabelKey ? getString(statusLabelKey) : data;
|
|
}
|
|
},
|
|
{ data: 'dateTimeChanged', title: getString('Plugins_Field_LastChanged') },
|
|
],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Poll for visibility the same way pluginsCore.php's own updater() does -
|
|
* #sourcesFieldView only reports :visible once both the Sources tab is the
|
|
* active top-level tab AND the Plugin/Field toggle has selected Field View.
|
|
* Resets fieldViewInitialized on a rejected render so a transient failure
|
|
* (e.g. getSourcesFieldData()'s GraphQL error path) gets retried on the next
|
|
* tick instead of leaving this mac permanently stuck unrendered.
|
|
*/
|
|
function fieldViewUpdater() {
|
|
if ($('#sourcesFieldView').is(':visible')) {
|
|
const currentMac = getMac();
|
|
if (!fieldViewInitialized || currentMac !== fieldViewLastMac) {
|
|
fieldViewInitialized = true;
|
|
fieldViewLastMac = currentMac;
|
|
showFieldViewSkeleton();
|
|
callAfterAppInitialized(() => {
|
|
renderFieldView(currentMac)
|
|
.catch(err => {
|
|
console.error('[Field View] render failed, will retry next tick:', err);
|
|
fieldViewInitialized = false;
|
|
})
|
|
.finally(hideFieldViewSkeleton);
|
|
});
|
|
}
|
|
}
|
|
setTimeout(fieldViewUpdater, 200);
|
|
}
|
|
|
|
fieldViewUpdater();
|
|
|
|
</script>
|