Skip to content

Commit 77369c3

Browse files
committed
feat(plugins): Optimize plugin badge fetching and rendering to prevent flicker and enhance visibility
1 parent cd0a3f6 commit 77369c3

File tree

1 file changed

+139
-157
lines changed

1 file changed

+139
-157
lines changed

front/pluginsCore.php

Lines changed: 139 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ function genericSaveData (id) {
274274
// -----------------------------------------------------------------------------
275275
pluginDefinitions = []
276276

277+
// Global counts map, populated before tabs are rendered
278+
let pluginCounts = {};
279+
277280
async function getData() {
278281
try {
279282
showSpinner();
@@ -282,6 +285,10 @@ function genericSaveData (id) {
282285
const plugins = await fetchJson('plugins.json');
283286
pluginDefinitions = plugins.data;
284287

288+
// Fetch counts BEFORE rendering tabs so we can skip empty plugins (no flicker)
289+
const prefixes = pluginDefinitions.filter(p => p.show_ui).map(p => p.unique_prefix);
290+
pluginCounts = await fetchPluginCounts(prefixes);
291+
285292
generateTabs();
286293
} catch (err) {
287294
console.error("Failed to load data", err);
@@ -348,148 +355,117 @@ function postPluginGraphQL(gqlField, prefix, foreignKey, dtRequest, callback) {
348355
});
349356
}
350357

351-
// Fetch badge counts for every plugin and populate sidebar + sub-tab counters.
358+
// Fetch counts for all plugins. Returns { PREFIX: { objects, events, history } }.
352359
// Fast path: static JSON (~1KB) when no MAC filter is active.
353360
// Filtered path: batched GraphQL aliases when a foreignKey (MAC) is set.
354-
async function prefetchPluginBadges() {
361+
async function fetchPluginCounts(prefixes) {
362+
if (prefixes.length === 0) return {};
363+
355364
const mac = $("#txtMacFilter").val();
356365
const foreignKey = (mac && mac !== "--") ? mac : null;
357-
358-
const prefixes = pluginDefinitions
359-
.filter(p => p.show_ui)
360-
.map(p => p.unique_prefix);
361-
362-
if (prefixes.length === 0) return;
363-
364-
try {
365-
let counts = {}; // { PREFIX: { objects: N, events: N, history: N } }
366-
367-
if (!foreignKey) {
368-
// ---- FAST PATH: lightweight pre-computed JSON ----
369-
const stats = await fetchJson('table_plugins_stats.json');
370-
for (const row of stats.data) {
371-
const p = row.tableName; // 'objects' | 'events' | 'history'
372-
const plugin = row.plugin;
373-
if (!counts[plugin]) counts[plugin] = { objects: 0, events: 0, history: 0 };
374-
counts[plugin][p] = row.cnt;
375-
}
376-
} else {
377-
// ---- FILTERED PATH: GraphQL with foreignKey ----
378-
const apiToken = getSetting("API_TOKEN");
379-
const apiBase = getApiBase();
380-
const fkOpt = `, foreignKey: "${foreignKey}"`;
381-
const fragments = prefixes.map(p => [
382-
`${p}_obj: pluginsObjects(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
383-
`${p}_evt: pluginsEvents(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
384-
`${p}_hist: pluginsHistory(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
385-
].join('\n ')).join('\n ');
386-
387-
const query = `query BadgeCounts {\n ${fragments}\n }`;
388-
const response = await $.ajax({
389-
method: "POST",
390-
url: `${apiBase}/graphql`,
391-
headers: { "Authorization": `Bearer ${apiToken}`, "Content-Type": "application/json" },
392-
data: JSON.stringify({ query }),
393-
});
394-
if (response.errors) { console.error("[plugins] badge GQL errors:", response.errors); return; }
395-
for (const p of prefixes) {
396-
counts[p] = {
397-
objects: response.data[`${p}_obj`]?.dbCount ?? 0,
398-
events: response.data[`${p}_evt`]?.dbCount ?? 0,
399-
history: response.data[`${p}_hist`]?.dbCount ?? 0,
400-
};
401-
}
366+
let counts = {};
367+
368+
if (!foreignKey) {
369+
// ---- FAST PATH: lightweight pre-computed JSON ----
370+
const stats = await fetchJson('table_plugins_stats.json');
371+
for (const row of stats.data) {
372+
const p = row.tableName; // 'objects' | 'events' | 'history'
373+
const plugin = row.plugin;
374+
if (!counts[plugin]) counts[plugin] = { objects: 0, events: 0, history: 0 };
375+
counts[plugin][p] = row.cnt;
402376
}
403-
404-
// Update DOM
405-
for (const [prefix, c] of Object.entries(counts)) {
406-
$(`#badge_${prefix}`).text(c.objects);
407-
$(`#objCount_${prefix}`).text(c.objects);
408-
$(`#evtCount_${prefix}`).text(c.events);
409-
$(`#histCount_${prefix}`).text(c.history);
410-
}
411-
// Zero out plugins with no rows in any table
412-
prefixes.forEach(prefix => {
413-
if (!counts[prefix]) {
414-
$(`#badge_${prefix}`).text(0);
415-
$(`#objCount_${prefix}`).text(0);
416-
$(`#evtCount_${prefix}`).text(0);
417-
$(`#histCount_${prefix}`).text(0);
418-
}
377+
} else {
378+
// ---- FILTERED PATH: GraphQL with foreignKey ----
379+
const apiToken = getSetting("API_TOKEN");
380+
const apiBase = getApiBase();
381+
const fkOpt = `, foreignKey: "${foreignKey}"`;
382+
const fragments = prefixes.map(p => [
383+
`${p}_obj: pluginsObjects(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
384+
`${p}_evt: pluginsEvents(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
385+
`${p}_hist: pluginsHistory(options: {plugin: "${p}", page: 1, limit: 1${fkOpt}}) { dbCount }`,
386+
].join('\n ')).join('\n ');
387+
388+
const query = `query BadgeCounts {\n ${fragments}\n }`;
389+
const response = await $.ajax({
390+
method: "POST",
391+
url: `${apiBase}/graphql`,
392+
headers: { "Authorization": `Bearer ${apiToken}`, "Content-Type": "application/json" },
393+
data: JSON.stringify({ query }),
419394
});
395+
if (response.errors) { console.error("[plugins] badge GQL errors:", response.errors); return counts; }
396+
for (const p of prefixes) {
397+
counts[p] = {
398+
objects: response.data[`${p}_obj`]?.dbCount ?? 0,
399+
events: response.data[`${p}_evt`]?.dbCount ?? 0,
400+
history: response.data[`${p}_hist`]?.dbCount ?? 0,
401+
};
402+
}
403+
}
420404

421-
// Auto-hide tabs with zero results
422-
autoHideEmptyTabs(counts, prefixes);
405+
return counts;
406+
}
423407

424-
} catch (err) {
425-
console.error('[plugins] badge prefetch failed:', err);
408+
// Apply pre-fetched counts to the DOM badges and hide empty tabs/sub-tabs.
409+
function applyPluginBadges(counts, prefixes) {
410+
// Update DOM badges
411+
for (const [prefix, c] of Object.entries(counts)) {
412+
$(`#badge_${prefix}`).text(c.objects);
413+
$(`#objCount_${prefix}`).text(c.objects);
414+
$(`#evtCount_${prefix}`).text(c.events);
415+
$(`#histCount_${prefix}`).text(c.history);
426416
}
417+
// Zero out plugins with no rows in any table
418+
prefixes.forEach(prefix => {
419+
if (!counts[prefix]) {
420+
$(`#badge_${prefix}`).text(0);
421+
$(`#objCount_${prefix}`).text(0);
422+
$(`#evtCount_${prefix}`).text(0);
423+
$(`#histCount_${prefix}`).text(0);
424+
}
425+
});
426+
427+
// Auto-hide sub-tabs with zero results (outer tabs already excluded during creation)
428+
autoHideEmptyTabs(counts, prefixes);
427429
}
428430

429431
// ---------------------------------------------------------------
430-
// Hide plugin tabs (left-nav + pane) where all three counts are 0.
431-
// Within visible plugins, hide inner sub-tabs whose count is 0.
432-
// If the active tab was hidden, activate the first visible one.
432+
// Within visible plugins, hide inner sub-tabs (Objects/Events/History) whose count is 0.
433+
// Outer plugin tabs with zero total are already excluded during tab creation.
433434
function autoHideEmptyTabs(counts, prefixes) {
434435
prefixes.forEach(prefix => {
435436
const c = counts[prefix] || { objects: 0, events: 0, history: 0 };
436-
const total = c.objects + c.events + c.history;
437-
const $li = $(`#tabs-location li:has(a[href="#${prefix}"])`);
438437
const $pane = $(`#tabs-content-location > #${prefix}`);
439438

440-
if (total === 0) {
441-
// Hide the entire plugin tab and strip active from both nav item and pane
442-
$li.removeClass('active').hide();
443-
$pane.removeClass('active').css('display', '');
444-
} else {
445-
// Ensure nav item visible (in case a previous filter hid it)
446-
$li.show();
447-
// Clear any inline display override so Bootstrap CSS controls pane visibility via .active
448-
$pane.css('display', '');
449-
450-
// Hide inner sub-tabs with zero count
451-
const subTabs = [
452-
{ href: `#objectsTarget_${prefix}`, count: c.objects },
453-
{ href: `#eventsTarget_${prefix}`, count: c.events },
454-
{ href: `#historyTarget_${prefix}`, count: c.history },
455-
];
456-
457-
let activeSubHidden = false;
458-
subTabs.forEach(st => {
459-
const $subLi = $pane.find(`ul.nav-tabs li:has(a[href="${st.href}"])`);
460-
const $subPane = $pane.find(st.href);
461-
if (st.count === 0) {
462-
if ($subLi.hasClass('active')) activeSubHidden = true;
463-
$subLi.hide();
464-
$subPane.removeClass('active').css('display', '');
465-
} else {
466-
$subLi.show();
467-
$subPane.css('display', '');
468-
}
469-
});
439+
// Hide inner sub-tabs with zero count
440+
const subTabs = [
441+
{ href: `#objectsTarget_${prefix}`, count: c.objects },
442+
{ href: `#eventsTarget_${prefix}`, count: c.events },
443+
{ href: `#historyTarget_${prefix}`, count: c.history },
444+
];
445+
446+
let activeSubHidden = false;
447+
subTabs.forEach(st => {
448+
const $subLi = $pane.find(`ul.nav-tabs li:has(a[href="${st.href}"])`);
449+
const $subPane = $pane.find(st.href);
450+
if (st.count === 0) {
451+
if ($subLi.hasClass('active')) activeSubHidden = true;
452+
$subLi.hide();
453+
$subPane.removeClass('active').css('display', '');
454+
} else {
455+
$subLi.show();
456+
$subPane.css('display', '');
457+
}
458+
});
470459

471-
// If the active inner sub-tab was hidden, activate the first visible one
472-
// via Bootstrap's tab lifecycle so shown.bs.tab fires for deferred DataTable init
473-
if (activeSubHidden) {
474-
const $firstVisibleSubA = $pane.find('ul.nav-tabs li:visible:first a');
475-
if ($firstVisibleSubA.length) {
476-
$firstVisibleSubA.tab('show');
477-
}
460+
// If the active inner sub-tab was hidden, activate the first visible one
461+
// via Bootstrap's tab lifecycle so shown.bs.tab fires for deferred DataTable init
462+
if (activeSubHidden) {
463+
const $firstVisibleSubA = $pane.find('ul.nav-tabs li:visible:first a');
464+
if ($firstVisibleSubA.length) {
465+
$firstVisibleSubA.tab('show');
478466
}
479467
}
480468
});
481-
482-
// If the active left-nav tab was hidden, activate the first visible one
483-
const $activeLi = $(`#tabs-location li.active:visible`);
484-
if ($activeLi.length === 0) {
485-
const $firstVisibleLi = $(`#tabs-location li:visible`).first();
486-
if ($firstVisibleLi.length) {
487-
// Let Bootstrap's .tab('show') manage the active class on both
488-
// the <li> and the pane — adding it manually beforehand causes
489-
// Bootstrap to bail out early without firing shown.bs.tab.
490-
$firstVisibleLi.find('a').tab('show');
491-
}
492-
}
493469
}
494470

495471
function generateTabs() {
@@ -502,31 +478,36 @@ function generateTabs() {
502478

503479
let assignActive = true;
504480

505-
// Iterate over the sorted pluginDefinitions to create tab headers and content
506-
pluginDefinitions.forEach(pluginObj => {
507-
if (pluginObj.show_ui) {
508-
createTabContent(pluginObj, assignActive);
509-
createTabHeader(pluginObj, assignActive);
510-
assignActive = false;
511-
}
481+
// Build list of visible plugins (skip plugins with 0 total count)
482+
const visiblePlugins = pluginDefinitions.filter(pluginObj => {
483+
if (!pluginObj.show_ui) return false;
484+
const c = pluginCounts[pluginObj.unique_prefix] || { objects: 0, events: 0, history: 0 };
485+
return (c.objects + c.events + c.history) > 0;
486+
});
487+
488+
// Create tab DOM for visible plugins only — no flicker
489+
visiblePlugins.forEach(pluginObj => {
490+
const prefix = pluginObj.unique_prefix;
491+
const c = pluginCounts[prefix] || { objects: 0, events: 0, history: 0 };
492+
createTabContent(pluginObj, assignActive, c);
493+
createTabHeader(pluginObj, assignActive, c);
494+
assignActive = false;
512495
});
513496

514497
// Now that ALL DOM elements exist (both <a> headers and tab panes),
515498
// wire up DataTable initialization: immediate for the active tab,
516499
// deferred via shown.bs.tab for the rest.
517500
let firstVisible = true;
518-
pluginDefinitions.forEach(pluginObj => {
519-
if (pluginObj.show_ui) {
520-
const prefix = pluginObj.unique_prefix;
521-
const colDefinitions = getColumnDefinitions(pluginObj);
522-
if (firstVisible) {
501+
visiblePlugins.forEach(pluginObj => {
502+
const prefix = pluginObj.unique_prefix;
503+
const colDefinitions = getColumnDefinitions(pluginObj);
504+
if (firstVisible) {
505+
initializeDataTables(prefix, colDefinitions, pluginObj);
506+
firstVisible = false;
507+
} else {
508+
$(`a[href="#${prefix}"]`).one('shown.bs.tab', function() {
523509
initializeDataTables(prefix, colDefinitions, pluginObj);
524-
firstVisible = false;
525-
} else {
526-
$(`a[href="#${prefix}"]`).one('shown.bs.tab', function() {
527-
initializeDataTables(prefix, colDefinitions, pluginObj);
528-
});
529-
}
510+
});
530511
}
531512
});
532513

@@ -538,8 +519,9 @@ function generateTabs() {
538519
tabContainer: '#tabs-location'
539520
});
540521

541-
// Pre-fetch badge counts for every plugin in a single batched GraphQL call.
542-
prefetchPluginBadges();
522+
// Apply badge counts to the DOM and hide empty inner sub-tabs
523+
const prefixes = visiblePlugins.map(p => p.unique_prefix);
524+
applyPluginBadges(pluginCounts, prefixes);
543525

544526
hideSpinner()
545527
}
@@ -552,35 +534,32 @@ function resetTabs() {
552534

553535
// ---------------------------------------------------------------
554536
// left headers
555-
function createTabHeader(pluginObj, assignActive) {
556-
const prefix = pluginObj.unique_prefix; // Get the unique prefix for the plugin
557-
558-
// Determine the active class for the first tab
537+
function createTabHeader(pluginObj, assignActive, counts) {
538+
const prefix = pluginObj.unique_prefix;
559539
const activeClass = assignActive ? "active" : "";
540+
const badgeText = counts ? counts.objects : '…';
560541

561-
// Append the tab header to the tabs location
562542
$('#tabs-location').append(`
563543
<li class="left-nav ${activeClass} ">
564544
<a class="col-sm-12 textOverflow" href="#${prefix}" data-plugin-prefix="${prefix}" id="${prefix}_id" data-toggle="tab">
565545
${getString(`${prefix}_icon`)} ${getString(`${prefix}_display_name`)}
566546

567547
</a>
568-
<div class="pluginBadgeWrap"><span title="" class="badge pluginBadge" id="badge_${prefix}"></span></div>
548+
<div class="pluginBadgeWrap"><span title="" class="badge pluginBadge" id="badge_${prefix}">${badgeText}</span></div>
569549
</li>
570550
`);
571551

572552
}
573553

574554
// ---------------------------------------------------------------
575555
// Content of selected plugin (header)
576-
function createTabContent(pluginObj, assignActive) {
577-
const prefix = pluginObj.unique_prefix; // Get the unique prefix for the plugin
578-
const colDefinitions = getColumnDefinitions(pluginObj); // Get column definitions for DataTables
556+
function createTabContent(pluginObj, assignActive, counts) {
557+
const prefix = pluginObj.unique_prefix;
558+
const colDefinitions = getColumnDefinitions(pluginObj);
579559

580-
// Append the content structure for the plugin's tab to the content location
581560
$('#tabs-content-location').append(`
582561
<div id="${prefix}" class="tab-pane ${assignActive ? 'active' : ''}">
583-
${generateTabNavigation(prefix)} <!-- Create tab navigation -->
562+
${generateTabNavigation(prefix, counts)} <!-- Create tab navigation -->
584563
<div class="tab-content">
585564
${generateDataTable(prefix, 'Objects', colDefinitions)}
586565
${generateDataTable(prefix, 'Events', colDefinitions)}
@@ -601,19 +580,22 @@ function getColumnDefinitions(pluginObj) {
601580
return pluginObj["database_column_definitions"].filter(colDef => colDef.show);
602581
}
603582

604-
function generateTabNavigation(prefix) {
605-
// Create navigation tabs for Objects, Unprocessed Events, and History
583+
function generateTabNavigation(prefix, counts) {
584+
const objCount = counts ? counts.objects : '…';
585+
const evtCount = counts ? counts.events : '…';
586+
const histCount = counts ? counts.history : '…';
587+
606588
return `
607589
<div class="nav-tabs-custom" style="margin-bottom: 0px">
608590
<ul class="nav nav-tabs">
609591
<li class="active">
610-
<a href="#objectsTarget_${prefix}" data-toggle="tab"><i class="fa fa-cube"></i> ${getString('Plugins_Objects')} (<span id="objCount_${prefix}"></span>)</a>
592+
<a href="#objectsTarget_${prefix}" data-toggle="tab"><i class="fa fa-cube"></i> ${getString('Plugins_Objects')} (<span id="objCount_${prefix}">${objCount}</span>)</a>
611593
</li>
612594
<li>
613-
<a href="#eventsTarget_${prefix}" data-toggle="tab"><i class="fa fa-bolt"></i> ${getString('Plugins_Unprocessed_Events')} (<span id="evtCount_${prefix}"></span>)</a>
595+
<a href="#eventsTarget_${prefix}" data-toggle="tab"><i class="fa fa-bolt"></i> ${getString('Plugins_Unprocessed_Events')} (<span id="evtCount_${prefix}">${evtCount}</span>)</a>
614596
</li>
615597
<li>
616-
<a href="#historyTarget_${prefix}" data-toggle="tab"><i class="fa fa-clock"></i> ${getString('Plugins_History')} (<span id="histCount_${prefix}"></span>)</a>
598+
<a href="#historyTarget_${prefix}" data-toggle="tab"><i class="fa fa-clock"></i> ${getString('Plugins_History')} (<span id="histCount_${prefix}">${histCount}</span>)</a>
617599
</li>
618600
</ul>
619601
</div>

0 commit comments

Comments
 (0)