Update public HTML and existing JS files

This commit is contained in:
courtmanr@gmail.com
2025-05-27 10:01:03 +01:00
parent f605f498a9
commit caf6a2bd3a
8 changed files with 281 additions and 57 deletions
+29 -21
View File
@@ -188,7 +188,7 @@
<span class="text-lg font-medium text-gray-800 dark:text-gray-200">Pulse</span>
</div>
<div class="header-controls flex justify-end items-center gap-4 md:flex-1">
<button id="theme-toggle-button" type="button" class="p-1 rounded-md text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none" aria-label="Toggle theme">
<button id="theme-toggle-button" type="button" class="p-1 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none" aria-label="Toggle theme">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 block dark:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
@@ -196,23 +196,23 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</button>
<div id="connection-status" class="status disconnected text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400">Disconnected</div>
<div id="connection-status" class="status disconnected text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300" role="status" aria-live="polite">Disconnected</div>
</div>
</div>
<div class="tabs flex mb-2 border-b border-gray-300 dark:border-gray-700 overflow-x-auto overflow-y-hidden whitespace-nowrap scrollbar">
<div class="tab active px-3 py-1.5 cursor-pointer bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border-b-0 rounded-t text-sm -mb-px flex items-center gap-1.5" data-tab="main">
<div class="tabs flex mb-2 border-b border-gray-300 dark:border-gray-700 overflow-x-auto overflow-y-hidden whitespace-nowrap scrollbar" role="tablist" aria-label="Main navigation tabs">
<div class="tab active px-3 py-1.5 cursor-pointer bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border-b-0 rounded-t text-sm -mb-px flex items-center gap-1.5" data-tab="main" role="tab" aria-selected="true" aria-controls="main" tabindex="0">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block align-middle"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
<span class="hidden md:inline">Main</span>
</div>
<div class="tab px-3 py-1.5 cursor-pointer bg-gray-100 dark:bg-gray-700/50 border-transparent text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-t flex items-center gap-1.5" data-tab="storage">
<div class="tab px-3 py-1.5 cursor-pointer bg-gray-100 dark:bg-gray-700/50 border-transparent text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-t flex items-center gap-1.5" data-tab="storage" role="tab" aria-selected="false" aria-controls="storage" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block align-middle"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>
<span class="hidden md:inline">Storage</span>
</div>
<div class="tab px-3 py-1.5 cursor-pointer bg-gray-100 dark:bg-gray-700/50 border-transparent text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-t flex items-center gap-1.5" data-tab="backups">
<div class="tab px-3 py-1.5 cursor-pointer bg-gray-100 dark:bg-gray-700/50 border-transparent text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-t flex items-center gap-1.5" data-tab="backups" role="tab" aria-selected="false" aria-controls="backups" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block align-middle"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><path d="m9 12 2 2 4-4"></path></svg>
<span class="hidden md:inline">Backups</span>
</div>
<div class="tab px-3 py-1.5 cursor-pointer bg-gray-100 dark:bg-gray-700/50 border-transparent text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-t flex items-center gap-1.5" data-tab="pbs">
<div class="tab px-3 py-1.5 cursor-pointer bg-gray-100 dark:bg-gray-700/50 border-transparent text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-t flex items-center gap-1.5" data-tab="pbs" role="tab" aria-selected="false" aria-controls="pbs" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block align-middle"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
<span class="hidden md:inline">PBS</span>
</div>
@@ -228,18 +228,18 @@
<div class="dashboard-filter flex flex-col md:flex-row justify-between items-stretch md:items-center gap-3 mb-3 p-2 bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-700 rounded">
<div class="dashboard-filter-controls flex-grow flex items-center gap-2">
<input type="text" id="dashboard-search" placeholder="Search Name, ID, Node (use ',' for OR)" class="flex-grow p-1 px-2 h-7 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none" />
<button id="reset-filters-button" title="Reset Filters & Sort (Esc)" class="p-1 h-7 w-7 flex-shrink-0 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none">
<input type="text" id="dashboard-search" placeholder="Search Name, ID, Node (use ',' for OR)" aria-label="Search virtual machines and containers" class="flex-grow p-1 px-2 h-7 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none" />
<button id="reset-filters-button" title="Reset Filters & Sort (Esc)" class="p-1.5 sm:p-1 h-9 w-9 sm:h-7 sm:w-7 flex-shrink-0 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
</button>
<button id="toggle-charts-button" title="Toggle Charts View" class="p-1 h-7 w-7 flex-shrink-0 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none">
<button id="toggle-charts-button" title="Toggle Charts View" aria-label="Toggle between charts and metrics view" class="p-1.5 sm:p-1 h-9 w-9 sm:h-7 sm:w-7 flex-shrink-0 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto">
<line x1="18" y1="20" x2="18" y2="10"></line>
<line x1="12" y1="20" x2="12" y2="4"></line>
<line x1="6" y1="20" x2="6" y2="14"></line>
</svg>
</button>
<button id="toggle-thresholds-button" title="Toggle Threshold Filters" class="relative p-1 h-7 w-7 flex-shrink-0 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none">
<button id="toggle-thresholds-button" title="Toggle Threshold Filters" aria-label="Toggle threshold filters panel" class="relative p-1.5 sm:p-1 h-9 w-9 sm:h-7 sm:w-7 flex-shrink-0 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none">
<span class="relative inline-block">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto">
<line x1="4" y1="21" x2="4" y2="14"></line>
@@ -260,12 +260,12 @@
<div class="flex items-center gap-2">
<div class="segmented-control inline-flex border border-gray-300 dark:border-gray-600 rounded overflow-hidden">
<input type="radio" id="group-grouped" name="group-filter" value="grouped" class="hidden peer/grouped" checked>
<label for="group-grouped" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/grouped:bg-blue-100 dark:peer-checked/grouped:bg-blue-800/50 peer-checked/grouped:text-blue-700 dark:peer-checked/grouped:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
<label for="group-grouped" class="flex items-center justify-center gap-1 px-3 py-1 sm:px-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 peer-checked/grouped:bg-blue-100 dark:peer-checked/grouped:bg-blue-800/50 peer-checked/grouped:text-blue-700 dark:peer-checked/grouped:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden"><path d="M4 4h4v4H4zM10 4h4v4h-4zM16 4h4v4h-4zM4 10h4v4H4zM10 10h4v4h-4zM16 10h4v4h-4zM4 16h4v4H4zM10 16h4v4h-4zM16 16h4v4h-4z"/></svg>
<span class="hidden md:inline">Grouped</span>
</label>
<input type="radio" id="group-list" name="group-filter" value="list" class="hidden peer/list">
<label for="group-list" class="flex items-center justify-center gap-1 px-2 py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/list:bg-blue-100 dark:peer-checked/list:bg-blue-800/50 peer-checked/list:text-blue-700 dark:peer-checked/list:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
<label for="group-list" class="flex items-center justify-center gap-1 px-3 py-1 sm:px-2 sm:py-0.5 text-xs cursor-pointer bg-white dark:bg-gray-800 border-l border-gray-300 dark:border-gray-600 peer-checked/list:bg-blue-100 dark:peer-checked/list:bg-blue-800/50 peer-checked/list:text-blue-700 dark:peer-checked/list:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700 select-none">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline md:hidden"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>
<span class="hidden md:inline">List</span>
</label>
@@ -311,7 +311,7 @@
</div>
<div class="table-container max-h-[70vh] overflow-y-auto overflow-x-auto mb-2 border border-gray-200 dark:border-gray-700 rounded overflow-hidden scrollbar">
<table id="main-table" class="w-full table-fixed text-sm">
<table id="main-table" class="w-full table-fixed text-xs sm:text-sm" role="table" aria-label="Virtual machines and containers">
<colgroup>
<col class="name-col" style="width: var(--name-col-width, 150px);">
<col class="type-col" style="width: 50px;">
@@ -326,18 +326,18 @@
<col class="net-disk-col" style="width: 70px;">
</colgroup>
<thead class="sticky top-0 z-10 bg-gray-100 dark:bg-gray-800">
<tr class="text-xs font-medium tracking-wider text-left text-gray-600 uppercase bg-gray-100 dark:bg-gray-700 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600">
<tr class="text-[10px] sm:text-xs font-medium tracking-wider text-left text-gray-600 uppercase bg-gray-100 dark:bg-gray-700 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600">
<th class="sortable p-1 px-2" data-sort="name" style="width: var(--name-col-width, 150px);">Name</th>
<th class="p-1 px-2" style="width: 50px;">Type</th>
<th class="sortable p-1 px-2" data-sort="id" style="width: 50px;">ID</th>
<th class="sortable p-1 px-2" data-sort="uptime" style="width: var(--uptime-col-width, 80px);" title="Uptime">Up</th>
<th class="sortable p-1 px-2 hidden sm:table-cell" data-sort="id" style="width: 50px;">ID</th>
<th class="sortable p-1 px-2 hidden md:table-cell" data-sort="uptime" style="width: var(--uptime-col-width, 80px);" title="Uptime">Up</th>
<th class="sortable p-1 px-2" data-sort="cpu" style="width: 120px;">CPU</th>
<th class="sortable p-1 px-2" data-sort="memory" style="width: 120px;">Memory</th>
<th class="sortable p-1 px-2" data-sort="disk" style="width: 120px;">Disk</th>
<th class="sortable p-1 px-2" data-sort="diskread" style="width: 70px;">Read</th>
<th class="sortable p-1 px-2" data-sort="diskwrite" style="width: 70px;">Write</th>
<th class="sortable p-1 px-2" data-sort="netin" style="width: 70px;">Net In</th>
<th class="sortable p-1 px-2" data-sort="netout" style="width: 70px;">Net Out</th>
<th class="sortable p-1 px-2 hidden lg:table-cell" data-sort="diskread" style="width: 70px;">Read</th>
<th class="sortable p-1 px-2 hidden lg:table-cell" data-sort="diskwrite" style="width: 70px;">Write</th>
<th class="sortable p-1 px-2 hidden lg:table-cell" data-sort="netin" style="width: 70px;">Net In</th>
<th class="sortable p-1 px-2 hidden lg:table-cell" data-sort="netout" style="width: 70px;">Net Out</th>
</tr>
<tr id="threshold-slider-row" class="hidden mt-3 p-3 border rounded-md bg-gray-50 dark:bg-gray-700/30 border-gray-200 dark:border-gray-600">
<th class="py-1 px-2 align-middle"></th>
@@ -493,6 +493,9 @@
</div>
</div>
<div id="backups-table-container" class="hidden space-y-2 mb-2">
<div id="pbs-instances-summary" class="text-xs text-gray-600 dark:text-gray-400 mb-2 p-2 bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-700 rounded">
<!-- PBS instances summary will be inserted here dynamically -->
</div>
<div class="overflow-x-auto border border-gray-200 dark:border-gray-700 rounded scrollbar">
<table id="backups-overview-table" class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
<thead class="sticky top-0 z-10 bg-gray-100 dark:bg-gray-800">
@@ -531,6 +534,8 @@
<!-- <script src="/app.js" defer></script> -->
<script src="/js/config.js" defer></script>
<script src="/js/state.js" defer></script>
<script src="/js/debounce.js" defer></script>
<script src="/js/virtual-scroll.js" defer></script>
<script src="/js/utils.js" defer></script>
<script src="/js/theme.js" defer></script>
<script src="/js/tooltips.js" defer></script>
@@ -538,6 +543,9 @@
<script src="/js/tabs.js" defer></script>
<script src="/js/charts.js" defer></script>
<script src="/js/ui/thresholds.js" defer></script>
<script src="/js/ui/empty-states.js" defer></script>
<script src="/js/ui/status-icons.js" defer></script>
<script src="/js/ui/loading-skeletons.js" defer></script>
<script src="/js/ui/common.js" defer></script>
<script src="/js/ui/nodes.js" defer></script>
<script src="/js/ui/dashboard.js" defer></script>
+67 -4
View File
@@ -7,6 +7,7 @@ PulseApp.ui.tabs = (() => {
let nestedTabContentContainer = null;
let mainTabsContainer = null;
let logSessionArea = null;
let loadedTabs = new Set();
function init() {
tabs = Array.from(document.querySelectorAll('.tab'));
@@ -40,19 +41,40 @@ PulseApp.ui.tabs = (() => {
}
});
}
// Mark main tab as loaded since it's shown by default
loadedTabs.add('main');
}
function activateMainTab(clickedTab, tabId) {
if (clickedTab.classList.contains('pointer-events-none')) return;
tabs.forEach(t => styleMainTab(t, false)); // Use the helper
tabContents.forEach(content => content.classList.add('hidden'));
tabs.forEach(t => {
styleMainTab(t, false);
// Update ARIA attributes
t.setAttribute('aria-selected', 'false');
t.setAttribute('tabindex', '-1');
});
tabContents.forEach(content => {
content.classList.add('hidden');
content.setAttribute('aria-hidden', 'true');
});
styleMainTab(clickedTab, true); // Use the helper
styleMainTab(clickedTab, true);
// Update ARIA attributes for active tab
clickedTab.setAttribute('aria-selected', 'true');
clickedTab.setAttribute('tabindex', '0');
const activeContent = document.getElementById(tabId);
if (activeContent) {
activeContent.classList.remove('hidden');
activeContent.setAttribute('aria-hidden', 'false');
// Lazy load tab content
if (!loadedTabs.has(tabId)) {
loadTabContent(tabId);
loadedTabs.add(tabId);
}
if (tabId === 'main') {
activateNestedTab('nested-tab-dashboard');
@@ -70,6 +92,18 @@ PulseApp.ui.tabs = (() => {
console.warn('[Tabs] PulseApp.ui.backups not available for updateBackupsTab')
}
}
if (tabId === 'storage') {
if (PulseApp.ui && PulseApp.ui.storage) {
PulseApp.ui.storage.updateStorageTab();
}
}
if (tabId === 'pbs') {
if (PulseApp.ui && PulseApp.ui.pbs) {
PulseApp.ui.pbs.updatePBSTab();
}
}
}
}
@@ -349,9 +383,38 @@ PulseApp.ui.tabs = (() => {
}
}
function loadTabContent(tabId) {
// Load tab-specific data only when the tab is first accessed
switch(tabId) {
case 'main':
// Main tab is loaded by default
break;
case 'storage':
if (PulseApp.ui && PulseApp.ui.storage) {
console.log(`[Tabs] Lazy loading ${tabId} content`);
// Storage data is already fetched, just update the UI
}
break;
case 'backups':
if (PulseApp.ui && PulseApp.ui.backups) {
console.log(`[Tabs] Lazy loading ${tabId} content`);
// Backups data is fetched on demand in updateBackupsTab
}
break;
case 'pbs':
if (PulseApp.ui && PulseApp.ui.pbs) {
console.log(`[Tabs] Lazy loading ${tabId} content`);
// PBS data is already fetched, just update the UI
}
break;
}
}
return {
init,
activateNestedTab,
updateTabAvailability
updateTabAvailability,
addLogTab,
removeLogTabAndContent
};
})();
+13 -2
View File
@@ -27,9 +27,20 @@ PulseApp.tooltips = (() => {
}
function handleMouseOver(event) {
const target = event.target.closest('.metric-tooltip-trigger, .storage-tooltip-trigger');
const target = event.target.closest('[data-tooltip], .metric-tooltip-trigger, .storage-tooltip-trigger, .truncate');
if (target) {
const tooltipText = target.getAttribute('data-tooltip');
let tooltipText = target.getAttribute('data-tooltip');
// Auto-generate tooltip for truncated text
if (!tooltipText && target.classList.contains('truncate')) {
const fullText = target.textContent.trim();
const title = target.getAttribute('title');
// Only show tooltip if text is actually truncated
if ((title && title !== fullText) || target.scrollWidth > target.clientWidth) {
tooltipText = title || fullText;
}
}
if (tooltipText && tooltipElement) {
tooltipElement.textContent = tooltipText;
positionTooltip(event);
+40
View File
@@ -293,6 +293,7 @@ PulseApp.ui.backups = (() => {
const loadingMsg = document.getElementById('backups-loading-message');
const noDataMsg = document.getElementById('backups-no-data-message');
const statusTextElement = document.getElementById('backups-status-text');
const pbsSummaryElement = document.getElementById('pbs-instances-summary');
if (!tableContainer || !tableBody || !loadingMsg || !noDataMsg || !statusTextElement) {
console.error("UI elements for Backups tab not found!");
@@ -321,6 +322,45 @@ PulseApp.ui.backups = (() => {
const backupStatusByGuest = allGuests.map(guest => _determineGuestBackupStatus(guest, snapshotsByGuest.get(`${guest.vmid}-${guest.type === 'qemu' ? 'vm' : 'ct'}`) || [], tasksByGuest.get(`${guest.vmid}-${guest.type === 'qemu' ? 'vm' : 'ct'}`) || [], dayBoundaries, threeDaysAgo, sevenDaysAgo));
const filteredBackupStatus = _filterBackupData(backupStatusByGuest, backupsSearchInput);
// Calculate PBS instances summary - only show if multiple PBS instances
const pbsDataArray = PulseApp.state.get('pbsDataArray') || [];
const pbsSummaryDismissed = PulseApp.state.get('pbsSummaryDismissed') || false;
if (pbsSummaryElement) {
if (pbsDataArray.length > 1 && !pbsSummaryDismissed) {
const pbsSummary = pbsDataArray.map(pbs => {
const backupCount = (pbs.datastores || []).reduce((total, ds) =>
total + (ds.snapshots ? ds.snapshots.length : 0), 0);
return `${pbs.pbsInstanceName}: ${backupCount} backups`;
}).join(' | ');
pbsSummaryElement.innerHTML = `
<div class="flex justify-between items-center">
<div>
<strong>PBS Instances (${pbsDataArray.length}):</strong> ${pbsSummary}
<span class="text-gray-500 dark:text-gray-400 ml-2">• Showing aggregated backup data from all instances</span>
</div>
<button id="dismiss-pbs-summary" class="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 ml-4" title="Dismiss">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
`;
pbsSummaryElement.classList.remove('hidden');
// Add dismiss handler
const dismissBtn = document.getElementById('dismiss-pbs-summary');
if (dismissBtn) {
dismissBtn.addEventListener('click', () => {
pbsSummaryElement.classList.add('hidden');
PulseApp.state.set('pbsSummaryDismissed', true);
PulseApp.state.saveFilterState();
});
}
} else {
pbsSummaryElement.classList.add('hidden');
}
}
const sortStateBackups = PulseApp.state.getSortState('backups');
const sortedBackupStatus = PulseApp.utils.sortData(filteredBackupStatus, sortStateBackups.column, sortStateBackups.direction, 'backups');
+15 -2
View File
@@ -247,7 +247,12 @@ PulseApp.ui.common = (() => {
const tableType = tableTypeMatch[1];
tableElement.querySelectorAll('th.sortable').forEach(th => {
th.addEventListener('click', () => {
// Make sortable headers keyboard accessible
th.setAttribute('tabindex', '0');
th.setAttribute('role', 'button');
th.setAttribute('aria-label', `Sort by ${th.textContent.trim()}`);
const handleSort = () => {
const column = th.getAttribute('data-sort');
if (!column) return;
@@ -271,6 +276,14 @@ PulseApp.ui.common = (() => {
}
updateSortUI(tableId, th);
};
th.addEventListener('click', handleSort);
th.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSort();
}
});
});
}
@@ -297,7 +310,7 @@ PulseApp.ui.common = (() => {
}
function generateNodeGroupHeaderCellHTML(text, colspan, cellTag = 'td') {
const cellClasses = 'py-0.5 px-2 bg-gray-200 dark:bg-gray-700 subtle-stripes-light dark:subtle-stripes-dark text-left font-medium text-sm text-gray-700 dark:text-gray-300';
const cellClasses = 'py-0.5 px-2 bg-gray-200 dark:bg-gray-700 subtle-stripes-light dark:subtle-stripes-dark text-left font-medium text-xs sm:text-sm text-gray-700 dark:text-gray-300';
return `<${cellTag} colspan="${colspan}" class="${cellClasses}">${text}</${cellTag}>`;
}
+94 -23
View File
@@ -23,6 +23,8 @@ PulseApp.ui.dashboard = (() => {
let guestMetricDragSnapshot = {}; // To store metrics during slider drag
let tableBodyEl = null;
let statusElementEl = null;
let virtualScroller = null;
const VIRTUAL_SCROLL_THRESHOLD = 100; // Use virtual scrolling for >100 items
function init() {
searchInput = document.getElementById('dashboard-search');
@@ -490,12 +492,23 @@ PulseApp.ui.dashboard = (() => {
if (cells.length >= 10) {
// Cell order: name(0), type(1), id(2), uptime(3), cpu(4), memory(5), disk(6), diskread(7), diskwrite(8), netin(9), netout(10)
// Update name (cell 0) if changed
// Update name (cell 0)
const nameCell = cells[0];
if (nameCell.textContent !== guest.name) {
nameCell.textContent = guest.name;
nameCell.title = guest.name;
}
// Ensure ID cell (2) has responsive classes
if (cells[2]) {
cells[2].className = 'p-1 px-2 hidden sm:table-cell';
}
// Ensure uptime cell (3) has responsive classes
if (cells[3]) {
cells[3].className = 'p-1 px-2 whitespace-nowrap hidden md:table-cell';
}
// Update uptime (cell 3)
const uptimeCell = cells[3];
@@ -533,6 +546,13 @@ PulseApp.ui.dashboard = (() => {
diskCell.innerHTML = newDiskHTML;
}
// Ensure I/O cells (7-10) have responsive classes
[7, 8, 9, 10].forEach(index => {
if (cells[index]) {
cells[index].className = 'p-1 px-2 hidden lg:table-cell';
}
});
// Update I/O cells (7-10) if running
if (guest.status === STATUS_RUNNING) {
// Disk Read (cell 7)
@@ -624,6 +644,14 @@ PulseApp.ui.dashboard = (() => {
return;
}
// Show loading skeleton if no data yet
const currentData = PulseApp.state.get('dashboardData');
if (!currentData || currentData.length === 0) {
if (PulseApp.ui.loadingSkeletons && tableBodyEl) {
PulseApp.ui.loadingSkeletons.showTableSkeleton(tableBodyEl.closest('table'), 5, 11);
}
}
refreshDashboardData();
const dashboardData = PulseApp.state.get('dashboardData') || [];
@@ -642,8 +670,45 @@ PulseApp.ui.dashboard = (() => {
// Check if we need a full rebuild (grouping mode changed or first render)
const needsFullRebuild = previousGroupByNode !== groupByNode || previousTableData === null;
if (needsFullRebuild) {
// Full rebuild
// Destroy existing virtual scroller if switching modes or data size changes significantly
if (virtualScroller && (groupByNode || sortedData.length <= VIRTUAL_SCROLL_THRESHOLD)) {
virtualScroller.destroy();
virtualScroller = null;
// Restore normal table structure
const tableContainer = document.querySelector('.table-container');
if (tableContainer) {
tableContainer.style.height = '';
tableContainer.innerHTML = '<table id="main-table" class="w-full table-fixed text-xs sm:text-sm" role="table" aria-label="Virtual machines and containers"><tbody></tbody></table>';
tableBodyEl = document.querySelector('#main-table tbody');
}
}
// Use virtual scrolling for large datasets when not grouped
if (!groupByNode && sortedData.length > VIRTUAL_SCROLL_THRESHOLD && PulseApp.virtualScroll) {
const tableContainer = document.querySelector('.table-container');
if (tableContainer && !virtualScroller) {
// Set fixed height for virtual scroll container
tableContainer.style.height = '600px';
virtualScroller = PulseApp.virtualScroll.createVirtualScroller(
tableContainer,
sortedData,
(guest) => {
const row = createGuestRow(guest);
// Remove hover effects for virtual rows
if (row) {
row.style.borderBottom = '1px solid rgb(229 231 235)';
row.classList.remove('hover:bg-gray-50', 'dark:hover:bg-gray-700/50');
}
return row;
}
);
} else if (virtualScroller) {
virtualScroller.updateItems(sortedData);
}
visibleCount = sortedData.length;
sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase()));
} else if (needsFullRebuild) {
// Full rebuild for normal rendering
if (groupByNode) {
const groupRenderResult = _renderGroupedByNode(tableBodyEl, sortedData, createGuestRow);
visibleCount = groupRenderResult.visibleCount;
@@ -664,23 +729,29 @@ PulseApp.ui.dashboard = (() => {
previousTableData = sortedData;
if (visibleCount === 0 && tableBodyEl) {
let filterDescription = [];
if (filterGuestType !== FILTER_ALL) filterDescription.push(`Type: ${filterGuestType.toUpperCase()}`);
if (filterStatus !== FILTER_ALL) filterDescription.push(`Status: ${filterStatus}`);
const textSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : [];
if (textSearchTerms.length > 0) filterDescription.push(`Search: "${textSearchTerms.join(', ')}"`);
const activeThresholds = Object.entries(thresholdState).filter(([_, state]) => state.value > 0);
if (activeThresholds.length > 0) {
const thresholdTexts = activeThresholds.map(([key, state]) => {
return `${PulseApp.utils.getReadableThresholdName(key)}>=${PulseApp.utils.formatThresholdValue(key, state.value)}`;
});
filterDescription.push(`Thresholds: ${thresholdTexts.join(', ')}`);
const thresholdTexts = activeThresholds.map(([key, state]) => {
return `${PulseApp.utils.getReadableThresholdName(key)}>=${PulseApp.utils.formatThresholdValue(key, state.value)}`;
});
const hasFilters = filterGuestType !== FILTER_ALL || filterStatus !== FILTER_ALL || textSearchTerms.length > 0 || activeThresholds.length > 0;
if (PulseApp.ui.emptyStates) {
const context = {
filterType: filterGuestType,
filterStatus: filterStatus,
searchTerms: textSearchTerms,
thresholds: thresholdTexts
};
const emptyType = hasFilters ? 'no-results' : 'no-guests';
tableBodyEl.innerHTML = PulseApp.ui.emptyStates.createTableEmptyState(emptyType, context, 11);
} else {
// Fallback to simple message
let message = hasFilters ? "No guests match the current filters." : "No guests found.";
tableBodyEl.innerHTML = `<tr><td colspan="11" class="p-4 text-center text-gray-500 dark:text-gray-400">${message}</td></tr>`;
}
let message = "No guests match the current filters";
if (filterDescription.length > 0) message += ` (${filterDescription.join('; ')})`;
message += ".";
tableBodyEl.innerHTML = `<tr><td colspan="11" class="p-4 text-center text-gray-500 dark:text-gray-400">${message}</td></tr>`;
}
_updateDashboardStatusMessage(statusElementEl, visibleCount, visibleNodes, groupByNode, filterGuestType, filterStatus, searchInput, thresholdState);
@@ -831,15 +902,15 @@ PulseApp.ui.dashboard = (() => {
row.innerHTML = `
<td class="p-1 px-2 whitespace-nowrap truncate" title="${guest.name}">${guest.name}</td>
<td class="p-1 px-2">${typeIcon}</td>
<td class="p-1 px-2">${guest.id}</td>
<td class="p-1 px-2 whitespace-nowrap">${uptimeDisplay}</td>
<td class="p-1 px-2 hidden sm:table-cell">${guest.id}</td>
<td class="p-1 px-2 whitespace-nowrap hidden md:table-cell">${uptimeDisplay}</td>
<td class="p-1 px-2">${cpuBarHTML}</td>
<td class="p-1 px-2">${memoryBarHTML}</td>
<td class="p-1 px-2">${diskBarHTML}</td>
<td class="p-1 px-2">${diskReadCell}</td>
<td class="p-1 px-2">${diskWriteCell}</td>
<td class="p-1 px-2">${netInCell}</td>
<td class="p-1 px-2">${netOutCell}</td>
<td class="p-1 px-2 hidden lg:table-cell">${diskReadCell}</td>
<td class="p-1 px-2 hidden lg:table-cell">${diskWriteCell}</td>
<td class="p-1 px-2 hidden lg:table-cell">${netInCell}</td>
<td class="p-1 px-2 hidden lg:table-cell">${netOutCell}</td>
`;
return row;
}
+8 -2
View File
@@ -130,12 +130,18 @@ PulseApp.ui.nodes = (() => {
console.error('Critical element #node-summary-cards-container not found for node summary cards update!');
return;
}
container.innerHTML = ''; // Clear previous content
// Show loading skeletons if no data yet
if (!nodes || nodes.length === 0) {
if (PulseApp.ui.loadingSkeletons) {
PulseApp.ui.loadingSkeletons.showNodeCardsSkeleton(container, 3);
return;
}
container.innerHTML = '<p class="text-sm text-gray-500 dark:text-gray-400">No node data available for summary.</p>';
return;
}
container.innerHTML = ''; // Clear previous content
const numNodes = nodes.length;
+15 -3
View File
@@ -121,7 +121,11 @@ PulseApp.ui.storage = (() => {
const nodes = PulseApp.state.get('nodesData') || [];
if (!Array.isArray(nodes) || nodes.length === 0) {
contentDiv.innerHTML = '<p class="text-gray-500 dark:text-gray-400 p-4 text-center">No node or storage data available.</p>';
if (PulseApp.ui.emptyStates) {
contentDiv.innerHTML = PulseApp.ui.emptyStates.createEmptyState('no-storage');
} else {
contentDiv.innerHTML = '<p class="text-gray-500 dark:text-gray-400 p-4 text-center">No node or storage data available.</p>';
}
return;
}
@@ -137,7 +141,11 @@ PulseApp.ui.storage = (() => {
const nodeKeys = Object.keys(storageByNode);
if (nodeKeys.length === 0) {
contentDiv.innerHTML = '<p class="text-gray-500 dark:text-gray-400 p-4 text-center">No storage data found associated with nodes.</p>';
if (PulseApp.ui.emptyStates) {
contentDiv.innerHTML = PulseApp.ui.emptyStates.createEmptyState('no-storage');
} else {
contentDiv.innerHTML = '<p class="text-gray-500 dark:text-gray-400 p-4 text-center">No storage data found associated with nodes.</p>';
}
return;
}
@@ -173,7 +181,11 @@ PulseApp.ui.storage = (() => {
if (nodeStorageData.length === 0) {
const noDataRow = document.createElement('tr');
noDataRow.innerHTML = `<td colspan="7" class="p-2 px-3 text-sm text-gray-500 dark:text-gray-400 italic">No storage configured or found for this node.</td>`;
if (PulseApp.ui.emptyStates) {
noDataRow.innerHTML = `<td colspan="7" class="p-0">${PulseApp.ui.emptyStates.createEmptyState('no-storage')}</td>`;
} else {
noDataRow.innerHTML = `<td colspan="7" class="p-2 px-3 text-sm text-gray-500 dark:text-gray-400 italic">No storage configured or found for this node.</td>`;
}
tbody.appendChild(noDataRow);
return;
}