@@ -274,6 +274,10 @@ function genericSaveData (id) {
274274// -----------------------------------------------------------------------------
275275pluginDefinitions = []
276276
277+ // Global counts map, populated before tabs are rendered.
278+ // null = counts unavailable (fail-open: show all plugins)
279+ let pluginCounts = null;
280+
277281async function getData() {
278282 try {
279283 showSpinner();
@@ -282,6 +286,11 @@ function genericSaveData (id) {
282286 const plugins = await fetchJson('plugins.json');
283287 pluginDefinitions = plugins.data;
284288
289+ // Fetch counts BEFORE rendering tabs so we can skip empty plugins (no flicker).
290+ // fetchPluginCounts never throws — returns null on failure (fail-open).
291+ const prefixes = pluginDefinitions.filter(p => p.show_ui).map(p => p.unique_prefix);
292+ pluginCounts = await fetchPluginCounts(prefixes);
293+
285294 generateTabs();
286295 } catch (err) {
287296 console.error("Failed to load data", err);
@@ -348,21 +357,17 @@ function postPluginGraphQL(gqlField, prefix, foreignKey, dtRequest, callback) {
348357 });
349358}
350359
351- // Fetch badge counts for every plugin and populate sidebar + sub-tab counters.
360+ // Fetch counts for all plugins. Returns { PREFIX: { objects, events, history } }
361+ // or null on failure (fail-open so tabs still render).
352362// Fast path: static JSON (~1KB) when no MAC filter is active.
353363// Filtered path: batched GraphQL aliases when a foreignKey (MAC) is set.
354- async function prefetchPluginBadges() {
355- const mac = $("#txtMacFilter").val();
356- 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;
364+ async function fetchPluginCounts(prefixes) {
365+ if (prefixes.length === 0) return {};
363366
364367 try {
365- let counts = {}; // { PREFIX: { objects: N, events: N, history: N } }
368+ const mac = $("#txtMacFilter").val();
369+ const foreignKey = (mac && mac !== "--") ? mac : null;
370+ let counts = {};
366371
367372 if (!foreignKey) {
368373 // ---- FAST PATH: lightweight pre-computed JSON ----
@@ -391,7 +396,10 @@ function postPluginGraphQL(gqlField, prefix, foreignKey, dtRequest, callback) {
391396 headers: { "Authorization": `Bearer ${apiToken}`, "Content-Type": "application/json" },
392397 data: JSON.stringify({ query }),
393398 });
394- if (response.errors) { console.error("[plugins] badge GQL errors:", response.errors); return; }
399+ if (response.errors) {
400+ console.error("[plugins] badge GQL errors:", response.errors);
401+ return null; // fail-open
402+ }
395403 for (const p of prefixes) {
396404 counts[p] = {
397405 objects: response.data[`${p}_obj`]?.dbCount ?? 0,
@@ -401,95 +409,74 @@ function postPluginGraphQL(gqlField, prefix, foreignKey, dtRequest, callback) {
401409 }
402410 }
403411
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- }
419- });
420-
421- // Auto-hide tabs with zero results
422- autoHideEmptyTabs(counts, prefixes);
423-
412+ return counts;
424413 } catch (err) {
425- console.error('[plugins] badge prefetch failed:', err);
414+ console.error('[plugins] fetchPluginCounts failed (fail-open):', err);
415+ return null;
426416 }
427417}
428418
419+ // Apply pre-fetched counts to the DOM badges and hide empty tabs/sub-tabs.
420+ function applyPluginBadges(counts, prefixes) {
421+ // Update DOM badges
422+ for (const [prefix, c] of Object.entries(counts)) {
423+ $(`#badge_${prefix}`).text(c.objects);
424+ $(`#objCount_${prefix}`).text(c.objects);
425+ $(`#evtCount_${prefix}`).text(c.events);
426+ $(`#histCount_${prefix}`).text(c.history);
427+ }
428+ // Zero out plugins with no rows in any table
429+ prefixes.forEach(prefix => {
430+ if (!counts[prefix]) {
431+ $(`#badge_${prefix}`).text(0);
432+ $(`#objCount_${prefix}`).text(0);
433+ $(`#evtCount_${prefix}`).text(0);
434+ $(`#histCount_${prefix}`).text(0);
435+ }
436+ });
437+
438+ // Auto-hide sub-tabs with zero results (outer tabs already excluded during creation)
439+ autoHideEmptyTabs(counts, prefixes);
440+ }
441+
429442// ---------------------------------------------------------------
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.
443+ // Within visible plugins, hide inner sub-tabs (Objects/Events/History) whose count is 0.
444+ // Outer plugin tabs with zero total are already excluded during tab creation.
433445function autoHideEmptyTabs(counts, prefixes) {
434446 prefixes.forEach(prefix => {
435447 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}"])`);
438448 const $pane = $(`#tabs-content-location > #${prefix}`);
439449
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- });
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+ });
470470
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- }
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');
478477 }
479478 }
480479 });
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- }
493480}
494481
495482function generateTabs() {
@@ -502,31 +489,39 @@ function generateTabs() {
502489
503490 let assignActive = true;
504491
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- }
492+ // When counts are available, skip plugins with 0 total count (no flicker).
493+ // When counts are null (fetch failed), show all show_ui plugins (fail-open).
494+ const countsAvailable = pluginCounts !== null;
495+ const visiblePlugins = pluginDefinitions.filter(pluginObj => {
496+ if (!pluginObj.show_ui) return false;
497+ if (!countsAvailable) return true; // fail-open: show all
498+ const c = pluginCounts[pluginObj.unique_prefix] || { objects: 0, events: 0, history: 0 };
499+ return (c.objects + c.events + c.history) > 0;
500+ });
501+
502+ // Create tab DOM for visible plugins only
503+ visiblePlugins.forEach(pluginObj => {
504+ const prefix = pluginObj.unique_prefix;
505+ const c = countsAvailable ? (pluginCounts[prefix] || { objects: 0, events: 0, history: 0 }) : null;
506+ createTabContent(pluginObj, assignActive, c);
507+ createTabHeader(pluginObj, assignActive, c);
508+ assignActive = false;
512509 });
513510
514511 // Now that ALL DOM elements exist (both <a> headers and tab panes),
515512 // wire up DataTable initialization: immediate for the active tab,
516513 // deferred via shown.bs.tab for the rest.
517514 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) {
515+ visiblePlugins.forEach(pluginObj => {
516+ const prefix = pluginObj.unique_prefix;
517+ const colDefinitions = getColumnDefinitions(pluginObj);
518+ if (firstVisible) {
519+ initializeDataTables(prefix, colDefinitions, pluginObj);
520+ firstVisible = false;
521+ } else {
522+ $(`a[href="#${prefix}"]`).one('shown.bs.tab', function() {
523523 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- }
524+ });
530525 }
531526 });
532527
@@ -538,8 +533,11 @@ function generateTabs() {
538533 tabContainer: '#tabs-location'
539534 });
540535
541- // Pre-fetch badge counts for every plugin in a single batched GraphQL call.
542- prefetchPluginBadges();
536+ // Apply badge counts to the DOM and hide empty inner sub-tabs (only if counts loaded)
537+ if (countsAvailable) {
538+ const prefixes = visiblePlugins.map(p => p.unique_prefix);
539+ applyPluginBadges(pluginCounts, prefixes);
540+ }
543541
544542 hideSpinner()
545543}
@@ -552,35 +550,32 @@ function resetTabs() {
552550
553551// ---------------------------------------------------------------
554552// 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
553+ function createTabHeader(pluginObj, assignActive, counts) {
554+ const prefix = pluginObj.unique_prefix;
559555 const activeClass = assignActive ? "active" : "";
556+ const badgeText = counts ? counts.objects : '…';
560557
561- // Append the tab header to the tabs location
562558 $('#tabs-location').append(`
563559 <li class="left-nav ${activeClass} ">
564560 <a class="col-sm-12 textOverflow" href="#${prefix}" data-plugin-prefix="${prefix}" id="${prefix}_id" data-toggle="tab">
565561 ${getString(`${prefix}_icon`)} ${getString(`${prefix}_display_name`)}
566562
567563 </a>
568- <div class="pluginBadgeWrap"><span title="" class="badge pluginBadge" id="badge_${prefix}">… </span></div>
564+ <div class="pluginBadgeWrap"><span title="" class="badge pluginBadge" id="badge_${prefix}">${badgeText} </span></div>
569565 </li>
570566 `);
571567
572568}
573569
574570// ---------------------------------------------------------------
575571// 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
572+ function createTabContent(pluginObj, assignActive, counts ) {
573+ const prefix = pluginObj.unique_prefix;
574+ const colDefinitions = getColumnDefinitions(pluginObj);
579575
580- // Append the content structure for the plugin's tab to the content location
581576 $('#tabs-content-location').append(`
582577 <div id="${prefix}" class="tab-pane ${assignActive ? 'active' : ''}">
583- ${generateTabNavigation(prefix)} <!-- Create tab navigation -->
578+ ${generateTabNavigation(prefix, counts )} <!-- Create tab navigation -->
584579 <div class="tab-content">
585580 ${generateDataTable(prefix, 'Objects', colDefinitions)}
586581 ${generateDataTable(prefix, 'Events', colDefinitions)}
@@ -601,19 +596,22 @@ function getColumnDefinitions(pluginObj) {
601596 return pluginObj["database_column_definitions"].filter(colDef => colDef.show);
602597}
603598
604- function generateTabNavigation(prefix) {
605- // Create navigation tabs for Objects, Unprocessed Events, and History
599+ function generateTabNavigation(prefix, counts) {
600+ const objCount = counts ? counts.objects : '…';
601+ const evtCount = counts ? counts.events : '…';
602+ const histCount = counts ? counts.history : '…';
603+
606604 return `
607605 <div class="nav-tabs-custom" style="margin-bottom: 0px">
608606 <ul class="nav nav-tabs">
609607 <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>
608+ <a href="#objectsTarget_${prefix}" data-toggle="tab"><i class="fa fa-cube"></i> ${getString('Plugins_Objects')} (<span id="objCount_${prefix}">${objCount} </span>)</a>
611609 </li>
612610 <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>
611+ <a href="#eventsTarget_${prefix}" data-toggle="tab"><i class="fa fa-bolt"></i> ${getString('Plugins_Unprocessed_Events')} (<span id="evtCount_${prefix}">${evtCount} </span>)</a>
614612 </li>
615613 <li>
616- <a href="#historyTarget_${prefix}" data-toggle="tab"><i class="fa fa-clock"></i> ${getString('Plugins_History')} (<span id="histCount_${prefix}">… </span>)</a>
614+ <a href="#historyTarget_${prefix}" data-toggle="tab"><i class="fa fa-clock"></i> ${getString('Plugins_History')} (<span id="histCount_${prefix}">${histCount} </span>)</a>
617615 </li>
618616 </ul>
619617 </div>
@@ -682,17 +680,27 @@ className: colDef.css_classes || '',
682680 });
683681 }
684682
685- // Initialize the Objects table immediately (it is the active/visible sub-tab).
686- // Defer Events and History tables until their sub-tab is first shown.
683+ // Initialize the DataTable for whichever inner sub-tab is currently active
684+ // (may not be Objects if autoHideEmptyTabs switched it).
685+ // Defer the remaining sub-tabs until their shown.bs.tab fires.
687686 const [objCfg, evtCfg, histCfg] = tableConfigs;
688- buildDT(objCfg.tableId, objCfg.gqlField, objCfg.countId, objCfg.badgeId);
689-
690- $(`a[ href=" #eventsTarget_${prefix}"]`).one('shown.bs.tab', function() {
691- buildDT(evtCfg.tableId, evtCfg.gqlField, evtCfg.countId, evtCfg.badgeId);
692- }) ;
687+ const allCfgs = [
688+ { cfg: objCfg, href: `#objectsTarget_${prefix}` },
689+ { cfg: evtCfg, href: ` #eventsTarget_${prefix}` },
690+ { cfg: histCfg, href: `#historyTarget_${prefix}` },
691+ ] ;
693692
694- $(`a[href="#historyTarget_${prefix}"]`).one('shown.bs.tab', function() {
695- buildDT(histCfg.tableId, histCfg.gqlField, histCfg.countId, histCfg.badgeId);
693+ allCfgs.forEach(({ cfg, href }) => {
694+ const $subPane = $(href);
695+ if ($subPane.hasClass('active') && $subPane.is(':visible')) {
696+ // This sub-tab is the currently active one — initialize immediately
697+ buildDT(cfg.tableId, cfg.gqlField, cfg.countId, cfg.badgeId);
698+ } else if ($subPane.closest('.tab-pane').length) {
699+ // Defer until shown
700+ $(`a[href="${href}"]`).one('shown.bs.tab', function() {
701+ buildDT(cfg.tableId, cfg.gqlField, cfg.countId, cfg.badgeId);
702+ });
703+ }
696704 });
697705}
698706
0 commit comments