Update UI components and charts functionality

This commit is contained in:
courtmanr@gmail.com
2025-05-27 16:06:38 +01:00
parent 9d71dc0b7e
commit f646b9855a
6 changed files with 393 additions and 138 deletions
+173 -19
View File
@@ -157,11 +157,161 @@
/* Enhanced metric cell layouts */
.usage-col {
min-width: 120px;
min-width: 100px;
}
.net-disk-col {
min-width: 70px;
min-width: 60px;
}
/* Prevent text wrapping in all table cells */
#main-table td,
#main-table th {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Special handling for name column to enable ellipsis */
#main-table td:first-child {
max-width: 250px;
}
/* Mobile table scroll styling */
@media (max-width: 768px) {
.table-container {
-webkit-overflow-scrolling: touch;
scroll-snap-type: x proximity;
}
.table-container::-webkit-scrollbar {
height: 10px;
}
.table-container::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 5px;
}
.table-container::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.3);
border-radius: 5px;
}
.dark .table-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
.dark .table-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
}
/* ALL first cells are sticky on mobile */
#main-table td:first-child,
#main-table th:first-child {
position: sticky !important;
left: -2px; /* Shift left to cover gap */
z-index: 1;
padding-left: 10px; /* Add extra padding to compensate */
}
/* Guest row first cells */
#main-table tbody tr:not(.node-header) td:first-child {
background: white;
background-clip: padding-box;
box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.05);
}
.dark #main-table tbody tr:not(.node-header) td:first-child {
background: rgb(31 41 55);
background-clip: padding-box;
box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.2);
}
#main-table tbody tr:not(.node-header):hover td:first-child {
background: rgb(249 250 251);
}
.dark #main-table tbody tr:not(.node-header):hover td:first-child {
background: rgb(55 65 81);
}
/* Table header first cell */
#main-table thead th:first-child {
background: rgb(243 244 246);
background-clip: padding-box;
z-index: 11;
box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.05);
}
.dark #main-table thead th:first-child {
background: rgb(55 65 81);
background-clip: padding-box;
box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.2);
}
/* Node header first cell styling */
#main-table tr.node-header td:first-child {
z-index: 2;
background: rgb(229 231 235) !important;
background-clip: padding-box;
box-shadow: 2px 0 4px -2px rgba(0, 0, 0, 0.1);
}
.dark #main-table tr.node-header td:first-child {
background: rgb(75 85 99) !important;
background-clip: padding-box;
box-shadow: 2px 0 4px -2px rgba(0, 0, 0, 0.3);
}
/* Ensure full coverage on sticky cells */
#main-table td:first-child,
#main-table th:first-child {
min-width: 140px;
position: relative;
}
/* Solid edge on sticky columns to prevent bleed-through */
#main-table td:first-child::before,
#main-table th:first-child::before {
content: '';
position: absolute;
top: -1px;
left: -20px; /* Extended further left */
right: -1px;
bottom: -1px;
background: inherit;
z-index: -1;
}
/* Additional left coverage with after element */
#main-table td:first-child::after,
#main-table th:first-child::after {
content: '';
position: absolute;
top: 0;
left: -5px;
bottom: 0;
width: 5px;
background: inherit;
z-index: 1;
}
/* Additional left edge coverage */
#main-table td:first-child,
#main-table th:first-child {
border-left: 2px solid;
}
#main-table tbody tr:not(.node-header) td:first-child {
border-left-color: white;
}
.dark #main-table tbody tr:not(.node-header) td:first-child {
border-left-color: rgb(31 41 55);
}
#main-table tr.node-header td:first-child {
border-left-color: rgb(229 231 235);
}
.dark #main-table tr.node-header td:first-child {
border-left-color: rgb(75 85 99);
}
#main-table thead th:first-child {
border-left-color: rgb(243 244 246);
}
.dark #main-table thead th:first-child {
border-left-color: rgb(55 65 81);
}
/* Remove the old node header mobile handling since we're using individual cells now */
}
</style>
</head>
@@ -317,33 +467,37 @@
</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-xs sm:text-sm" role="table" aria-label="Virtual machines and containers">
<!-- Mobile scroll hint -->
<div class="fixed bottom-4 right-4 bg-blue-500 text-white text-[11px] px-3 py-1.5 rounded-full md:hidden animate-pulse shadow-lg z-50 pointer-events-none" id="scroll-hint" style="display: none;">
← Swipe for more →
</div>
<table id="main-table" class="w-full min-w-[800px] table-auto 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="name-col" style="min-width: 140px; max-width: 250px;">
<col class="type-col" style="width: 50px;">
<col class="id-col" style="width: 50px;">
<col class="uptime-col" style="width: var(--uptime-col-width, 80px);" title="Uptime">
<col class="usage-col" style="width: 120px;">
<col class="usage-col" style="width: 120px;">
<col class="usage-col" style="width: 120px;">
<col class="net-disk-col" style="width: 70px;">
<col class="net-disk-col" style="width: 70px;">
<col class="net-disk-col" style="width: 70px;">
<col class="net-disk-col" style="width: 70px;">
<col class="uptime-col" style="min-width: 60px; max-width: 100px;">
<col class="usage-col" style="min-width: 100px; max-width: 150px;">
<col class="usage-col" style="min-width: 100px; max-width: 150px;">
<col class="usage-col" style="min-width: 100px; max-width: 150px;">
<col class="net-disk-col" style="min-width: 60px; max-width: 100px;">
<col class="net-disk-col" style="min-width: 60px; max-width: 100px;">
<col class="net-disk-col" style="min-width: 60px; max-width: 100px;">
<col class="net-disk-col" style="min-width: 60px; max-width: 100px;">
</colgroup>
<thead class="sticky top-0 z-10 bg-gray-100 dark:bg-gray-800">
<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 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="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" 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 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>
<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>
</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>
@@ -411,7 +565,7 @@
</tr>
</tbody>
</table>
</div>
</div>
<span id="dashboard-status-text" class="text-xs text-gray-500 dark:text-gray-400">Loading dashboard data...</span>
+4 -61
View File
@@ -327,67 +327,8 @@ PulseApp.charts = (() => {
}
});
// Touch events (mobile) - improved to prevent browser intervention
overlay.addEventListener('touchstart', (event) => {
// Only prevent default if the event is cancelable and we're actually interacting with the chart
if (event.cancelable && event.touches.length === 1) {
event.preventDefault(); // Prevent scrolling only when safe to do so
}
const touch = event.touches[0];
// Create a synthetic event object for touch
const syntheticEvent = {
clientX: touch.clientX,
clientY: touch.clientY,
target: event.target
};
// Change chart line to white on touch
const path = svg.querySelector('.chart-line');
if (path) {
path.setAttribute('data-original-color', path.getAttribute('stroke'));
// Use black for light mode, white for dark mode (same as mouse hover)
const isDarkMode = document.documentElement.classList.contains('dark');
const hoverColor = isDarkMode ? '#ffffff' : '#000000';
path.setAttribute('stroke', hoverColor);
}
showTooltipForPosition(syntheticEvent, touch.clientX, touch.clientY);
}, { passive: false }); // Allow preventDefault when needed
overlay.addEventListener('touchmove', (event) => {
// Only prevent default if the event is cancelable and we have a single touch
if (event.cancelable && event.touches.length === 1) {
event.preventDefault(); // Prevent scrolling only when safe to do so
}
const touch = event.touches[0];
// Create a synthetic event object for touch
const syntheticEvent = {
clientX: touch.clientX,
clientY: touch.clientY,
target: event.target
};
showTooltipForPosition(syntheticEvent, touch.clientX, touch.clientY);
}, { passive: false }); // Allow preventDefault when needed
overlay.addEventListener('touchend', () => {
// Restore original color
const path = svg.querySelector('.chart-line');
if (path) {
const originalColor = path.getAttribute('data-original-color');
if (originalColor) {
path.setAttribute('stroke', originalColor);
}
}
// Keep tooltip visible for a moment on mobile, then hide
setTimeout(() => {
if (PulseApp.tooltips) {
PulseApp.tooltips.hideTooltip();
}
}, 2000); // Hide after 2 seconds
});
// Touch events disabled for now - they interfere with scrolling
// Charts will still work with mouse events on devices that support them
svg.appendChild(overlay);
@@ -395,6 +336,8 @@ PulseApp.charts = (() => {
overlay._chartData = chartData.slice(); // Create a copy to avoid reference issues
overlay._metric = metric;
overlay._config = config;
// Remove the touch indicator - charts are discoverable enough without it
}
function updateChartPath(svg, chartData, config, metric, isNewChart = false, color) {
+14 -2
View File
@@ -310,8 +310,20 @@ 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-xs sm:text-sm text-gray-700 dark:text-gray-300';
return `<${cellTag} colspan="${colspan}" class="${cellClasses}">${text}</${cellTag}>`;
const baseClasses = 'py-0.5 px-2 text-left font-medium text-xs sm:text-sm text-gray-700 dark:text-gray-300';
// On mobile, create individual cells so first one can be sticky
if (window.innerWidth < 768) {
let html = `<${cellTag} class="${baseClasses} bg-gray-200 dark:bg-gray-700">${text}</${cellTag}>`;
// Add empty cells for remaining columns
for (let i = 1; i < colspan; i++) {
html += `<${cellTag} class="bg-gray-200 dark:bg-gray-700"></${cellTag}>`;
}
return html;
}
// Desktop: use colspan
return `<${cellTag} colspan="${colspan}" class="${baseClasses} bg-gray-200 dark:bg-gray-700 node-header-cell">${text}</${cellTag}>`;
}
return {
+57 -14
View File
@@ -26,6 +26,36 @@ PulseApp.ui.dashboard = (() => {
let virtualScroller = null;
const VIRTUAL_SCROLL_THRESHOLD = 100; // Use virtual scrolling for >100 items
function _initMobileScrollIndicators() {
const tableContainer = document.querySelector('.table-container');
const scrollHint = document.getElementById('scroll-hint');
if (!tableContainer || !scrollHint) return;
let scrollHintTimer;
// Hide scroll hint after 5 seconds or on first scroll
const hideScrollHint = () => {
if (scrollHint) {
scrollHint.style.display = 'none';
}
};
scrollHintTimer = setTimeout(hideScrollHint, 5000);
// Handle scroll events
tableContainer.addEventListener('scroll', () => {
hideScrollHint();
clearTimeout(scrollHintTimer);
}, { passive: true });
// Also hide on table container click/touch
tableContainer.addEventListener('touchstart', () => {
hideScrollHint();
clearTimeout(scrollHintTimer);
}, { passive: true });
}
function init() {
searchInput = document.getElementById('dashboard-search');
tableBodyEl = document.querySelector('#main-table tbody');
@@ -41,6 +71,14 @@ PulseApp.ui.dashboard = (() => {
if (chartsToggleButton) {
chartsToggleButton.addEventListener('click', toggleChartsMode);
}
// Initialize mobile scroll indicators
if (window.innerWidth < 768) {
_initMobileScrollIndicators();
}
// Add resize listener for progress bar text updates
window.addEventListener('resize', PulseApp.utils.updateProgressBarTextsDebounced);
document.addEventListener('keydown', (event) => {
// Handle Escape for resetting filters
@@ -500,14 +538,14 @@ PulseApp.ui.dashboard = (() => {
nameCell.title = guest.name;
}
// Ensure ID cell (2) has responsive classes
// Ensure ID cell (2) has proper classes
if (cells[2]) {
cells[2].className = 'p-1 px-2 hidden sm:table-cell';
cells[2].className = 'p-1 px-2';
}
// Ensure uptime cell (3) has responsive classes
// Ensure uptime cell (3) has proper classes
if (cells[3]) {
cells[3].className = 'p-1 px-2 whitespace-nowrap hidden md:table-cell';
cells[3].className = 'p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis';
}
// Update uptime (cell 3)
@@ -546,10 +584,10 @@ PulseApp.ui.dashboard = (() => {
diskCell.innerHTML = newDiskHTML;
}
// Ensure I/O cells (7-10) have responsive classes
// Ensure I/O cells (7-10) have proper classes
[7, 8, 9, 10].forEach(index => {
if (cells[index]) {
cells[index].className = 'p-1 px-2 hidden lg:table-cell';
cells[index].className = 'p-1 px-2';
}
});
@@ -678,7 +716,7 @@ PulseApp.ui.dashboard = (() => {
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>';
tableContainer.innerHTML = '<table id="main-table" class="w-full min-w-[800px] table-auto text-xs sm:text-sm" role="table" aria-label="Virtual machines and containers"><tbody></tbody></table>';
tableBodyEl = document.querySelector('#main-table tbody');
}
}
@@ -776,6 +814,11 @@ PulseApp.ui.dashboard = (() => {
PulseApp.charts.updateAllCharts();
});
}
// Update progress bar texts based on available width
requestAnimationFrame(() => {
PulseApp.utils.updateProgressBarTexts();
});
}
function _createCpuBarHtml(guest) {
@@ -900,17 +943,17 @@ 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 whitespace-nowrap overflow-hidden text-ellipsis max-w-0" title="${guest.name}">${guest.name}</td>
<td class="p-1 px-2">${typeIcon}</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">${guest.id}</td>
<td class="p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis">${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 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>
<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>
`;
return row;
}
+104 -34
View File
@@ -1,6 +1,7 @@
PulseApp.ui = PulseApp.ui || {};
PulseApp.ui.nodes = (() => {
let currentNodesData = null; // Store current nodes data for resize handling
function _createNodeCpuBarHtml(node) {
const cpuPercent = node.cpu ? (node.cpu * 100) : 0;
@@ -131,6 +132,11 @@ PulseApp.ui.nodes = (() => {
return;
}
// Store nodes data for resize handling
if (nodes) {
currentNodesData = nodes;
}
// Show loading skeletons if no data yet
if (!nodes || nodes.length === 0) {
if (PulseApp.ui.loadingSkeletons) {
@@ -144,44 +150,93 @@ PulseApp.ui.nodes = (() => {
container.innerHTML = ''; // Clear previous content
const numNodes = nodes.length;
// Helper function to determine optimal columns to avoid a single orphan
function calculateOptimalColumns(numItems, defaultCols) {
if (numItems <= 0) return defaultCols; // No items, use default or let it be empty
if (defaultCols <= 1) return 1; // Cannot reduce further, or already 1
// If numItems is less than or equal to defaultCols, use numItems as the column count.
if (numItems <= defaultCols) return numItems;
// Now numItems > defaultCols (multiple rows expected)
// Avoid a single orphan, but don't reduce to 1 column if defaultCols is 2.
if (numItems % defaultCols === 1) {
if (defaultCols === 2) {
// If default is 2 cols (sm), and we have an odd number of items (e.g., 3, 5),
// use 2 columns to avoid stacking. (Results in 2 side-by-side, then 1 or more).
return defaultCols;
}
// For other defaultCols (>=3), reducing by 1 to avoid an orphan is fine.
return Math.max(1, defaultCols - 1); // Ensure at least 1 column
}
return defaultCols;
}
const smCols = calculateOptimalColumns(numNodes, 2);
const mdCols = calculateOptimalColumns(numNodes, 3);
const lgCols = calculateOptimalColumns(numNodes, 4);
const xlCols = calculateOptimalColumns(numNodes, 4); // Based on previous preference
const gridDiv = document.createElement('div');
gridDiv.className = `grid grid-cols-1 sm:grid-cols-${smCols} md:grid-cols-${mdCols} lg:grid-cols-${lgCols} xl:grid-cols-${xlCols} gap-3`;
const isMobile = window.innerWidth < 640; // sm breakpoint
// Sort nodes by name for consistent order in summary cards
const sortedNodes = [...nodes].sort((a, b) => (a.node || '').localeCompare(b.node || ''));
sortedNodes.forEach(node => {
const cardElement = createNodeSummaryCard(node);
gridDiv.appendChild(cardElement);
});
container.appendChild(gridDiv);
if (isMobile) {
// Stack cards vertically on mobile with condensed layout
const stackDiv = document.createElement('div');
stackDiv.className = 'flex flex-col gap-2';
sortedNodes.forEach(node => {
const cardElement = createCondensedNodeCard(node);
stackDiv.appendChild(cardElement);
});
container.appendChild(stackDiv);
} else {
// Use grid layout for desktop
// Helper function to determine optimal columns to avoid a single orphan
function calculateOptimalColumns(numItems, defaultCols) {
if (numItems <= 0) return defaultCols;
if (defaultCols <= 1) return 1;
if (numItems <= defaultCols) return numItems;
if (numItems % defaultCols === 1) {
if (defaultCols === 2) {
return defaultCols;
}
return Math.max(1, defaultCols - 1);
}
return defaultCols;
}
const smCols = calculateOptimalColumns(numNodes, 2);
const mdCols = calculateOptimalColumns(numNodes, 3);
const lgCols = calculateOptimalColumns(numNodes, 4);
const xlCols = calculateOptimalColumns(numNodes, 4);
const gridDiv = document.createElement('div');
gridDiv.className = `grid grid-cols-1 sm:grid-cols-${smCols} md:grid-cols-${mdCols} lg:grid-cols-${lgCols} xl:grid-cols-${xlCols} gap-3`;
sortedNodes.forEach(node => {
const cardElement = createNodeSummaryCard(node);
gridDiv.appendChild(cardElement);
});
container.appendChild(gridDiv);
}
}
function createCondensedNodeCard(node) {
const isOnline = node && node.uptime > 0;
const statusDotColor = isOnline ? 'text-green-500' : 'text-red-500';
const cpuPercent = node.cpu ? (node.cpu * 100) : 0;
const memUsed = node.mem || 0;
const memTotal = node.maxmem || 0;
const memPercent = (memUsed && memTotal > 0) ? (memUsed / memTotal * 100) : 0;
const diskUsed = node.disk || 0;
const diskTotal = node.maxdisk || 0;
const diskPercent = (diskUsed && diskTotal > 0) ? (diskUsed / diskTotal * 100) : 0;
const card = document.createElement('div');
card.className = 'bg-white dark:bg-gray-800 shadow-sm rounded-lg p-2 border border-gray-200 dark:border-gray-700';
card.innerHTML = `
<div class="flex items-center justify-between gap-2">
<div class="flex items-center min-w-0">
<span class="h-2 w-2 rounded-full ${statusDotColor} mr-1.5 flex-shrink-0"></span>
<h3 class="font-semibold text-xs truncate">${node.node || 'Unknown'}</h3>
</div>
<div class="flex items-center gap-3 text-[10px] text-gray-600 dark:text-gray-400">
<span class="flex items-center gap-1">
<span class="font-medium">CPU</span>
<span class="font-bold ${PulseApp.utils.getUsageColor(cpuPercent, 'cpu')}">${cpuPercent.toFixed(0)}%</span>
</span>
<span class="flex items-center gap-1">
<span class="font-medium">MEM</span>
<span class="font-bold ${PulseApp.utils.getUsageColor(memPercent, 'memory')}">${memPercent.toFixed(0)}%</span>
</span>
<span class="flex items-center gap-1">
<span class="font-medium">DISK</span>
<span class="font-bold ${PulseApp.utils.getUsageColor(diskPercent, 'disk')}">${diskPercent.toFixed(0)}%</span>
</span>
</div>
</div>
`;
return card;
}
function updateNodesTable(nodes) {
@@ -237,7 +292,22 @@ PulseApp.ui.nodes = (() => {
}
}
function init() {
// Add resize listener for responsive behavior
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
// Re-render cards if we have data
if (currentNodesData) {
updateNodeSummaryCards(currentNodesData);
}
}, 250); // Debounce resize events
});
}
return {
init,
updateNodesTable,
updateNodeSummaryCards
};
+41 -8
View File
@@ -39,23 +39,25 @@ PulseApp.utils = (() => {
function createProgressTextBarHTML(percentage, text, color, simpleText = null) {
// Always use a neutral background regardless of the progress color
const bgColorClass = 'bg-gray-200 dark:bg-gray-700';
const bgColorClass = 'bg-gray-200 dark:bg-gray-600';
const progressColorClass = {
red: 'bg-red-500/50 dark:bg-red-500/50',
yellow: 'bg-yellow-500/50 dark:bg-yellow-500/50',
green: 'bg-green-500/50 dark:bg-green-500/50'
}[color] || 'bg-gray-500/50'; // Fallback progress color with opacity
red: 'bg-red-500/60 dark:bg-red-500/50',
yellow: 'bg-yellow-500/60 dark:bg-yellow-500/50',
green: 'bg-green-500/60 dark:bg-green-500/50'
}[color] || 'bg-gray-500/60 dark:bg-gray-500/50'; // Fallback progress color with opacity
// Use simpleText for narrow screens if provided, otherwise just show percentage
const mobileText = simpleText || `${percentage}%`;
// Generate a unique ID for this progress bar to handle dynamic text switching
const uniqueId = `pb-${Math.random().toString(36).substr(2, 9)}`;
return `
<div class="relative w-full h-3.5 rounded overflow-hidden ${bgColorClass}">
<div class="absolute top-0 left-0 h-full ${progressColorClass}" style="width: ${percentage}%;"></div>
<span class="absolute inset-0 flex items-center justify-center text-[10px] font-medium text-gray-800 dark:text-gray-100 leading-none">
<span class="hidden sm:inline truncate px-1">${text}</span>
<span class="inline sm:hidden">${mobileText}</span>
<span class="progress-text-full truncate px-1" data-progress-id="${uniqueId}" data-full-text="${text.replace(/"/g, '&quot;')}" data-simple-text="${mobileText.replace(/"/g, '&quot;')}">${text}</span>
</span>
</div>
`;
@@ -254,6 +256,35 @@ PulseApp.utils = (() => {
});
}
// Function to check and update progress bar text based on available width
function updateProgressBarTexts() {
const progressTexts = document.querySelectorAll('.progress-text-full');
progressTexts.forEach(span => {
const fullText = span.getAttribute('data-full-text');
const simpleText = span.getAttribute('data-simple-text');
if (!fullText || !simpleText) return;
// Check if the current text is overflowing
const parent = span.parentElement;
if (parent) {
// Temporarily set to full text to measure
span.textContent = fullText;
// Check if text is truncated (scrollWidth > clientWidth)
if (span.scrollWidth > parent.clientWidth - 8) { // 8px for padding
span.textContent = simpleText;
} else {
span.textContent = fullText;
}
}
});
}
// Debounced version for resize events
const updateProgressBarTextsDebounced = debounce(updateProgressBarTexts, 100);
// Return the public API for this module
return {
sanitizeForId: (str) => str.replace(/[^a-zA-Z0-9-]/g, '-'),
@@ -270,6 +301,8 @@ PulseApp.utils = (() => {
getReadableThresholdCriteria,
sortData,
renderTableBody,
debounce
debounce,
updateProgressBarTexts,
updateProgressBarTextsDebounced
};
})();