Skip to content

Commit bd55316

Browse files
committed
feat: improve UI responsiveness and fix table scrolling
1 parent f8a4ff3 commit bd55316

File tree

9 files changed

+257
-267
lines changed

9 files changed

+257
-267
lines changed

src/public/index.html

Lines changed: 72 additions & 175 deletions
Large diffs are not rendered by default.

src/public/js/ui/backups.js

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ PulseApp.ui.backups = (() => {
254254
sevenDayDots += '</div>';
255255

256256
row.innerHTML = `
257-
<td class="p-1 px-2 whitespace-nowrap font-medium text-gray-900 dark:text-gray-100" title="${guestStatus.guestName}">${guestStatus.guestName}</td>
257+
<td class="sticky left-0 bg-white dark:bg-gray-800 z-10 p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0 text-gray-900 dark:text-gray-100" title="${guestStatus.guestName}">${guestStatus.guestName}</td>
258258
<td class="p-1 px-2 text-gray-500 dark:text-gray-400">${guestStatus.guestId}</td>
259259
<td class="p-1 px-2">${typeIcon}</td>
260260
<td class="p-1 px-2 whitespace-nowrap text-gray-500 dark:text-gray-400">${guestStatus.node}</td>
@@ -299,6 +299,11 @@ PulseApp.ui.backups = (() => {
299299
console.error("UI elements for Backups tab not found!");
300300
return;
301301
}
302+
303+
// Find the scrollable container
304+
const scrollableContainer = PulseApp.utils.getScrollableParent(tableBody) ||
305+
tableContainer.closest('.overflow-x-auto') ||
306+
tableContainer;
302307

303308
const { allGuests, initialDataReceived, tasksByGuest, snapshotsByGuest, dayBoundaries, threeDaysAgo, sevenDaysAgo } = _getInitialBackupData();
304309

@@ -364,15 +369,16 @@ PulseApp.ui.backups = (() => {
364369
const sortStateBackups = PulseApp.state.getSortState('backups');
365370
const sortedBackupStatus = PulseApp.utils.sortData(filteredBackupStatus, sortStateBackups.column, sortStateBackups.direction, 'backups');
366371

367-
tableBody.innerHTML = '';
368-
if (sortedBackupStatus.length > 0) {
369-
sortedBackupStatus.forEach(guestStatus => {
370-
const row = _renderBackupTableRow(guestStatus);
371-
tableBody.appendChild(row);
372-
});
373-
noDataMsg.classList.add('hidden');
374-
tableContainer.classList.remove('hidden');
375-
} else {
372+
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
373+
tableBody.innerHTML = '';
374+
if (sortedBackupStatus.length > 0) {
375+
sortedBackupStatus.forEach(guestStatus => {
376+
const row = _renderBackupTableRow(guestStatus);
377+
tableBody.appendChild(row);
378+
});
379+
noDataMsg.classList.add('hidden');
380+
tableContainer.classList.remove('hidden');
381+
} else {
376382
tableContainer.classList.add('hidden');
377383
let emptyMessage = "No backup information found for any guests.";
378384
if (backupStatusByGuest.length > 0 && filteredBackupStatus.length === 0) { // Data exists, but filters hide all
@@ -393,6 +399,7 @@ PulseApp.ui.backups = (() => {
393399
noDataMsg.textContent = emptyMessage;
394400
noDataMsg.classList.remove('hidden');
395401
}
402+
}); // End of preserveScrollPosition
396403

397404
const backupsSortColumn = sortStateBackups.column;
398405
const backupsHeader = document.querySelector(`#backups-overview-table th[data-sort="${backupsSortColumn}"]`);

src/public/js/ui/common.js

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -312,18 +312,13 @@ PulseApp.ui.common = (() => {
312312
function generateNodeGroupHeaderCellHTML(text, colspan, cellTag = 'td') {
313313
const baseClasses = 'py-0.5 px-2 text-left font-medium text-xs sm:text-sm text-gray-700 dark:text-gray-300';
314314

315-
// On mobile, create individual cells so first one can be sticky
316-
if (window.innerWidth < 768) {
317-
let html = `<${cellTag} class="${baseClasses} bg-gray-200 dark:bg-gray-700">${text}</${cellTag}>`;
318-
// Add empty cells for remaining columns
319-
for (let i = 1; i < colspan; i++) {
320-
html += `<${cellTag} class="bg-gray-200 dark:bg-gray-700"></${cellTag}>`;
321-
}
322-
return html;
315+
// Always create individual cells so first one can be sticky
316+
let html = `<${cellTag} class="sticky left-0 bg-gray-200 dark:bg-gray-700 z-10 ${baseClasses}">${text}</${cellTag}>`;
317+
// Add empty cells for remaining columns
318+
for (let i = 1; i < colspan; i++) {
319+
html += `<${cellTag} class="bg-gray-200 dark:bg-gray-700"></${cellTag}>`;
323320
}
324-
325-
// Desktop: use colspan
326-
return `<${cellTag} colspan="${colspan}" class="${baseClasses} bg-gray-200 dark:bg-gray-700 node-header-cell">${text}</${cellTag}>`;
321+
return html;
327322
}
328323

329324
return {

src/public/js/ui/dashboard.js

Lines changed: 69 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ PulseApp.ui.dashboard = (() => {
6767
}
6868

6969
// Initialize charts toggle
70-
const chartsToggleButton = document.getElementById('toggle-charts-button');
71-
if (chartsToggleButton) {
72-
chartsToggleButton.addEventListener('click', toggleChartsMode);
70+
const chartsToggleCheckbox = document.getElementById('toggle-charts-checkbox');
71+
if (chartsToggleCheckbox) {
72+
chartsToggleCheckbox.addEventListener('change', toggleChartsMode);
7373
}
7474

7575
// Initialize mobile scroll indicators
@@ -524,18 +524,21 @@ PulseApp.ui.dashboard = (() => {
524524
} else {
525525
row.className = 'border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50';
526526
}
527-
527+
528528
// Update specific cells that might have changed
529529
const cells = row.querySelectorAll('td');
530+
531+
// Ensure name cell keeps sticky styling even after row class updates
532+
if (cells[0]) {
533+
cells[0].className = 'sticky left-0 bg-white dark:bg-gray-800 z-10 p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0';
534+
}
530535
if (cells.length >= 10) {
531536
// Cell order: name(0), type(1), id(2), uptime(3), cpu(4), memory(5), disk(6), diskread(7), diskwrite(8), netin(9), netout(10)
532537

533-
// Update name (cell 0)
534-
const nameCell = cells[0];
535-
536-
if (nameCell.textContent !== guest.name) {
537-
nameCell.textContent = guest.name;
538-
nameCell.title = guest.name;
538+
// Update name (cell 0) content only (styling handled above)
539+
if (cells[0].textContent !== guest.name) {
540+
cells[0].textContent = guest.name;
541+
cells[0].title = guest.name;
539542
}
540543

541544
// Ensure ID cell (2) has proper classes
@@ -682,6 +685,11 @@ PulseApp.ui.dashboard = (() => {
682685
return;
683686
}
684687

688+
// Find the scrollable container
689+
const scrollableContainer = PulseApp.utils.getScrollableParent(tableBodyEl) ||
690+
document.querySelector('.table-container') ||
691+
tableBodyEl.closest('.overflow-x-auto');
692+
685693
// Show loading skeleton if no data yet
686694
const currentData = PulseApp.state.get('dashboardData');
687695
if (!currentData || currentData.length === 0) {
@@ -746,50 +754,56 @@ PulseApp.ui.dashboard = (() => {
746754
visibleCount = sortedData.length;
747755
sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase()));
748756
} else if (needsFullRebuild) {
749-
// Full rebuild for normal rendering
750-
if (groupByNode) {
751-
const groupRenderResult = _renderGroupedByNode(tableBodyEl, sortedData, createGuestRow);
752-
visibleCount = groupRenderResult.visibleCount;
753-
visibleNodes = groupRenderResult.visibleNodes;
754-
} else {
755-
PulseApp.utils.renderTableBody(tableBodyEl, sortedData, createGuestRow, "No matching guests found.", 11);
756-
visibleCount = sortedData.length;
757-
sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase()));
758-
}
757+
// Full rebuild for normal rendering with scroll preservation
758+
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
759+
if (groupByNode) {
760+
const groupRenderResult = _renderGroupedByNode(tableBodyEl, sortedData, createGuestRow);
761+
visibleCount = groupRenderResult.visibleCount;
762+
visibleNodes = groupRenderResult.visibleNodes;
763+
} else {
764+
PulseApp.utils.renderTableBody(tableBodyEl, sortedData, createGuestRow, "No matching guests found.", 11);
765+
visibleCount = sortedData.length;
766+
sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase()));
767+
}
768+
});
759769
previousGroupByNode = groupByNode;
760770
} else {
761-
// Incremental update using DOM diffing
762-
const result = _updateTableIncremental(tableBodyEl, sortedData, createGuestRow, groupByNode);
763-
visibleCount = result.visibleCount;
764-
visibleNodes = result.visibleNodes;
771+
// Incremental update using DOM diffing with scroll preservation
772+
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
773+
const result = _updateTableIncremental(tableBodyEl, sortedData, createGuestRow, groupByNode);
774+
visibleCount = result.visibleCount;
775+
visibleNodes = result.visibleNodes;
776+
});
765777
}
766778

767779
previousTableData = sortedData;
768780

769781
if (visibleCount === 0 && tableBodyEl) {
770-
const textSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : [];
771-
const activeThresholds = Object.entries(thresholdState).filter(([_, state]) => state.value > 0);
772-
const thresholdTexts = activeThresholds.map(([key, state]) => {
773-
return `${PulseApp.utils.getReadableThresholdName(key)}>=${PulseApp.utils.formatThresholdValue(key, state.value)}`;
774-
});
775-
776-
const hasFilters = filterGuestType !== FILTER_ALL || filterStatus !== FILTER_ALL || textSearchTerms.length > 0 || activeThresholds.length > 0;
777-
778-
if (PulseApp.ui.emptyStates) {
779-
const context = {
780-
filterType: filterGuestType,
781-
filterStatus: filterStatus,
782-
searchTerms: textSearchTerms,
783-
thresholds: thresholdTexts
784-
};
782+
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
783+
const textSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : [];
784+
const activeThresholds = Object.entries(thresholdState).filter(([_, state]) => state.value > 0);
785+
const thresholdTexts = activeThresholds.map(([key, state]) => {
786+
return `${PulseApp.utils.getReadableThresholdName(key)}>=${PulseApp.utils.formatThresholdValue(key, state.value)}`;
787+
});
785788

786-
const emptyType = hasFilters ? 'no-results' : 'no-guests';
787-
tableBodyEl.innerHTML = PulseApp.ui.emptyStates.createTableEmptyState(emptyType, context, 11);
788-
} else {
789-
// Fallback to simple message
790-
let message = hasFilters ? "No guests match the current filters." : "No guests found.";
791-
tableBodyEl.innerHTML = `<tr><td colspan="11" class="p-4 text-center text-gray-500 dark:text-gray-400">${message}</td></tr>`;
792-
}
789+
const hasFilters = filterGuestType !== FILTER_ALL || filterStatus !== FILTER_ALL || textSearchTerms.length > 0 || activeThresholds.length > 0;
790+
791+
if (PulseApp.ui.emptyStates) {
792+
const context = {
793+
filterType: filterGuestType,
794+
filterStatus: filterStatus,
795+
searchTerms: textSearchTerms,
796+
thresholds: thresholdTexts
797+
};
798+
799+
const emptyType = hasFilters ? 'no-results' : 'no-guests';
800+
tableBodyEl.innerHTML = PulseApp.ui.emptyStates.createTableEmptyState(emptyType, context, 11);
801+
} else {
802+
// Fallback to simple message
803+
let message = hasFilters ? "No guests match the current filters." : "No guests found.";
804+
tableBodyEl.innerHTML = `<tr><td colspan="11" class="p-4 text-center text-gray-500 dark:text-gray-400">${message}</td></tr>`;
805+
}
806+
});
793807
}
794808

795809
_updateDashboardStatusMessage(statusElementEl, visibleCount, visibleNodes, groupByNode, filterGuestType, filterStatus, searchInput, thresholdState);
@@ -943,7 +957,7 @@ PulseApp.ui.dashboard = (() => {
943957
}
944958

945959
row.innerHTML = `
946-
<td class="p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0" title="${guest.name}">${guest.name}</td>
960+
<td class="sticky left-0 bg-white dark:bg-gray-800 z-10 p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0" title="${guest.name}">${guest.name}</td>
947961
<td class="p-1 px-2">${typeIcon}</td>
948962
<td class="p-1 px-2">${guest.id}</td>
949963
<td class="p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis">${uptimeDisplay}</td>
@@ -980,23 +994,24 @@ PulseApp.ui.dashboard = (() => {
980994

981995
function toggleChartsMode() {
982996
const mainContainer = document.getElementById('main');
983-
const button = document.getElementById('toggle-charts-button');
997+
const checkbox = document.getElementById('toggle-charts-checkbox');
998+
const label = checkbox ? checkbox.parentElement : null;
984999

985-
if (mainContainer.classList.contains('charts-mode')) {
986-
// Switch to metrics mode
987-
mainContainer.classList.remove('charts-mode');
988-
button.title = 'Toggle Charts View';
989-
} else {
1000+
if (checkbox && checkbox.checked) {
9901001
// Switch to charts mode
9911002
mainContainer.classList.add('charts-mode');
992-
button.title = 'Toggle Metrics View';
1003+
if (label) label.title = 'Toggle Metrics View';
9931004

9941005
// Immediately render charts when switching to charts mode
9951006
if (PulseApp.charts) {
9961007
requestAnimationFrame(() => {
9971008
PulseApp.charts.updateAllCharts();
9981009
});
9991010
}
1011+
} else {
1012+
// Switch to metrics mode
1013+
mainContainer.classList.remove('charts-mode');
1014+
if (label) label.title = 'Toggle Charts View';
10001015
}
10011016
}
10021017

src/public/js/ui/nodes.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ PulseApp.ui.nodes = (() => {
6464
<span class="capitalize">${statusText}</span>
6565
</span>
6666
</td>
67-
<td class="p-1 px-2 whitespace-nowrap font-medium text-gray-900 dark:text-gray-100" title="${node.node || 'N/A'}">${node.node || 'N/A'}</td>
67+
<td class="p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0 text-gray-900 dark:text-gray-100" title="${node.node || 'N/A'}">${node.node || 'N/A'}</td>
6868
<td class="p-1 px-2 min-w-[200px]">${cpuBarHTML}</td>
6969
<td class="p-1 px-2 min-w-[200px]">${memoryBarHTML}</td>
7070
<td class="p-1 px-2 min-w-[200px]">${diskBarHTML}</td>
@@ -147,7 +147,13 @@ PulseApp.ui.nodes = (() => {
147147
return;
148148
}
149149

150-
container.innerHTML = ''; // Clear previous content
150+
// Find the scrollable container
151+
const scrollableContainer = PulseApp.utils.getScrollableParent(container) ||
152+
container.closest('.overflow-x-auto') ||
153+
container.parentElement;
154+
155+
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
156+
container.innerHTML = ''; // Clear previous content
151157

152158
const numNodes = nodes.length;
153159
const isMobile = window.innerWidth < 640; // sm breakpoint
@@ -197,6 +203,7 @@ PulseApp.ui.nodes = (() => {
197203
});
198204
container.appendChild(gridDiv);
199205
}
206+
}); // End of preserveScrollPosition
200207
}
201208

202209
function createCondensedNodeCard(node) {
@@ -246,7 +253,14 @@ PulseApp.ui.nodes = (() => {
246253
console.log('[Nodes] Node table not found - using summary cards display instead');
247254
return;
248255
}
249-
tbody.innerHTML = '';
256+
257+
// Find the scrollable container
258+
const scrollableContainer = PulseApp.utils.getScrollableParent(tbody) ||
259+
tbody.closest('.overflow-x-auto') ||
260+
tbody.parentElement;
261+
262+
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
263+
tbody.innerHTML = '';
250264

251265
if (!nodes || nodes.length === 0) {
252266
tbody.innerHTML = '<tr><td colspan="7" class="p-4 text-center text-gray-500 dark:text-gray-400">No nodes found or data unavailable</td></tr>';
@@ -290,6 +304,7 @@ PulseApp.ui.nodes = (() => {
290304
});
291305
}
292306
}
307+
}); // End of preserveScrollPosition
293308
}
294309

295310
function init() {

src/public/js/ui/pbs.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,15 @@ PulseApp.ui.pbs = (() => {
367367
const table = tableBody.closest('table');
368368
const tableId = table?.id;
369369
const isShowMoreExpanded = tableId ? expandedShowMoreState.has(tableId) : false;
370+
371+
// Find the scrollable container
372+
const scrollableContainer = PulseApp.utils.getScrollableParent(tableBody) ||
373+
parentSectionElement.closest('.overflow-x-auto') ||
374+
parentSectionElement;
370375

371376
// Use global expanded state instead of scanning DOM
372-
tableBody.innerHTML = '';
377+
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
378+
tableBody.innerHTML = '';
373379

374380
const tasks = fullTasksArray || [];
375381

@@ -478,11 +484,19 @@ PulseApp.ui.pbs = (() => {
478484
}
479485
}
480486
}
487+
}); // End of preserveScrollPosition
481488
}
482489

483490
const _populateDsTableBody = (dsTableBody, datastores, statusText, showDetails) => {
484491
if (!dsTableBody) return;
485-
dsTableBody.innerHTML = '';
492+
493+
// Find the scrollable container
494+
const scrollableContainer = PulseApp.utils.getScrollableParent(dsTableBody) ||
495+
dsTableBody.closest('.overflow-x-auto') ||
496+
dsTableBody.parentElement;
497+
498+
PulseApp.utils.preserveScrollPosition(scrollableContainer, () => {
499+
dsTableBody.innerHTML = '';
486500

487501
if (showDetails && datastores) {
488502
if (datastores.length === 0) {
@@ -566,6 +580,7 @@ PulseApp.ui.pbs = (() => {
566580
cell.className = `px-4 py-4 ${CSS_CLASSES.TEXT_SM} ${CSS_CLASSES.TEXT_GRAY_400} text-center`;
567581
cell.textContent = statusText;
568582
}
583+
}); // End of preserveScrollPosition
569584
};
570585

571586
const _populateInstanceTaskSections = (detailsContainer, instanceId, pbsInstance, statusText, showDetails) => {

0 commit comments

Comments
 (0)