@@ -274,6 +274,9 @@ function genericSaveData (id) {
274274// -----------------------------------------------------------------------------
275275pluginDefinitions = []
276276
277+ // Global counts map, populated before tabs are rendered
278+ let pluginCounts = {};
279+
277280async 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.
433434function 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
495471function 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