mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 03:33:53 +00:00
Update public HTML and existing JS files
This commit is contained in:
+29
-21
@@ -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
@@ -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
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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}>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user