From ad01faf789faeba24e02cc0e25504dd9342a16cf Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Thu, 8 May 2025 11:24:46 +0100 Subject: [PATCH] Refactor(ui/dashboard): Decompose dashboard update logic into focused helper functions --- src/public/js/ui/dashboard.js | 630 +++++++++++++++++----------------- 1 file changed, 315 insertions(+), 315 deletions(-) diff --git a/src/public/js/ui/dashboard.js b/src/public/js/ui/dashboard.js index 94569dc8f..b4daf7bf6 100644 --- a/src/public/js/ui/dashboard.js +++ b/src/public/js/ui/dashboard.js @@ -30,199 +30,164 @@ PulseApp.ui.dashboard = (() => { statusElementEl = document.getElementById('dashboard-status-text'); } - function refreshDashboardData() { - PulseApp.state.set('dashboardData', []); - let dashboardData = []; + function _calculateAverage(historyArray, key) { + if (!historyArray || historyArray.length === 0) return null; + const validEntries = historyArray.filter(entry => typeof entry[key] === 'number' && !isNaN(entry[key])); + if (validEntries.length === 0) return null; + const sum = validEntries.reduce((acc, curr) => acc + curr[key], 0); + return sum / validEntries.length; + } + function _calculateAverageRate(historyArray, key) { + if (!historyArray || historyArray.length < 2) return null; + const validHistory = historyArray.filter(entry => + typeof entry.timestamp === 'number' && !isNaN(entry.timestamp) && + typeof entry[key] === 'number' && !isNaN(entry[key]) + ); + + if (validHistory.length < 2) return null; + + const oldest = validHistory[0]; + const newest = validHistory[validHistory.length - 1]; + const valueDiff = newest[key] - oldest[key]; + const timeDiffSeconds = (newest.timestamp - oldest.timestamp) / 1000; + + if (timeDiffSeconds <= 0) return null; + if (valueDiff < 0) return null; + + return valueDiff / timeDiffSeconds; + } + + function _processSingleGuestData(guest) { + let avgCpu = 0, avgMem = 0, avgDisk = 0; + let avgDiskReadRate = null, avgDiskWriteRate = null, avgNetInRate = null, avgNetOutRate = null; + let avgMemoryPercent = 'N/A', avgDiskPercent = 'N/A'; + let effectiveMemorySource = 'host'; + let currentMemForAvg = 0; + let currentMemTotalForDisplay = guest.maxmem; + + const metricsData = PulseApp.state.get('metricsData') || []; + const metrics = metricsData.find(m => + m.id === guest.vmid && + m.type === guest.type && + m.node === guest.node && + m.endpointId === guest.endpointId + ); + const guestUniqueId = guest.id; + + const isDragging = PulseApp.ui.thresholds && PulseApp.ui.thresholds.isThresholdDragInProgress && PulseApp.ui.thresholds.isThresholdDragInProgress(); + const snapshot = guestMetricDragSnapshot[guestUniqueId]; + + if (isDragging && snapshot) { + avgDiskReadRate = snapshot.diskread; + avgDiskWriteRate = snapshot.diskwrite; + avgNetInRate = snapshot.netin; + avgNetOutRate = snapshot.netout; + + if (guest.status === STATUS_RUNNING && metrics && metrics.current) { + const currentDataPoint = { timestamp: Date.now(), ...metrics.current }; + PulseApp.state.updateDashboardHistory(guestUniqueId, currentDataPoint); + const history = PulseApp.state.getDashboardHistory()[guestUniqueId] || []; + avgCpu = _calculateAverage(history, 'cpu') ?? 0; + avgMem = _calculateAverage(history, 'mem') ?? 0; + avgDisk = _calculateAverage(history, 'disk') ?? 0; + } else { + PulseApp.state.clearDashboardHistoryEntry(guestUniqueId); + } + } else { + if (guest.status === STATUS_RUNNING && metrics && metrics.current) { + let baseMemoryValue = metrics.current.mem; + currentMemTotalForDisplay = guest.maxmem; + effectiveMemorySource = 'host'; + + if (metrics.current.guest_mem_actual_used_bytes !== undefined && metrics.current.guest_mem_actual_used_bytes !== null) { + baseMemoryValue = metrics.current.guest_mem_actual_used_bytes; + effectiveMemorySource = 'guest'; + if (metrics.current.guest_mem_total_bytes !== undefined && metrics.current.guest_mem_total_bytes > 0) { + currentMemTotalForDisplay = metrics.current.guest_mem_total_bytes; + } + } + currentMemForAvg = baseMemoryValue; + + const currentDataPoint = { + timestamp: Date.now(), + ...metrics.current, + effective_mem: currentMemForAvg, + effective_mem_total: currentMemTotalForDisplay, + effective_mem_source: effectiveMemorySource + }; + PulseApp.state.updateDashboardHistory(guestUniqueId, currentDataPoint); + const history = PulseApp.state.getDashboardHistory()[guestUniqueId] || []; + + avgCpu = _calculateAverage(history, 'cpu') ?? 0; + avgMem = _calculateAverage(history, 'effective_mem') ?? 0; + avgDisk = _calculateAverage(history, 'disk') ?? 0; + avgDiskReadRate = _calculateAverageRate(history, 'diskread'); + avgDiskWriteRate = _calculateAverageRate(history, 'diskwrite'); + avgNetInRate = _calculateAverageRate(history, 'netin'); + avgNetOutRate = _calculateAverageRate(history, 'netout'); + } else { + PulseApp.state.clearDashboardHistoryEntry(guestUniqueId); + } + } + + const historyForGuest = PulseApp.state.getDashboardHistory()[guestUniqueId]; + let finalMemTotalForPercent = guest.maxmem; + let finalMemSourceForTooltip = 'host'; + + if (historyForGuest && historyForGuest.length > 0) { + const lastHistoryEntry = historyForGuest[historyForGuest.length - 1]; + if (lastHistoryEntry.effective_mem_total !== undefined && lastHistoryEntry.effective_mem_total > 0) { + finalMemTotalForPercent = lastHistoryEntry.effective_mem_total; + } + if (lastHistoryEntry.effective_mem_source) { + finalMemSourceForTooltip = lastHistoryEntry.effective_mem_source; + } + } + + avgMemoryPercent = (finalMemTotalForPercent > 0 && typeof avgMem === 'number') ? Math.round(avgMem / finalMemTotalForPercent * 100) : 'N/A'; + avgDiskPercent = (guest.maxdisk > 0 && typeof avgDisk === 'number') ? Math.round(avgDisk / guest.maxdisk * 100) : 'N/A'; + + let rawHostReportedMem = null; + if (guest.status === STATUS_RUNNING && metrics && metrics.current && metrics.current.mem !== undefined) { + rawHostReportedMem = metrics.current.mem; + } + + return { + id: guest.vmid, + uniqueId: guestUniqueId, + vmid: guest.vmid, + name: guest.name || `${guest.type === 'qemu' ? GUEST_TYPE_VM : GUEST_TYPE_CT} ${guest.vmid}`, + node: guest.node, + type: guest.type === 'qemu' ? GUEST_TYPE_VM : GUEST_TYPE_CT, + status: guest.status, + cpu: avgCpu, + cpus: guest.cpus || 1, + memory: avgMemoryPercent, + memoryCurrent: avgMem, + memoryTotal: finalMemTotalForPercent, + memorySource: finalMemSourceForTooltip, + rawHostMemory: rawHostReportedMem, + disk: avgDiskPercent, + diskCurrent: avgDisk, + diskTotal: guest.maxdisk, + uptime: guest.status === STATUS_RUNNING ? guest.uptime : 0, + diskread: avgDiskReadRate, + diskwrite: avgDiskWriteRate, + netin: avgNetInRate, + netout: avgNetOutRate + }; + } + + function _setDashboardColumnWidths(dashboardData) { let maxNameLength = 0; let maxUptimeLength = 0; - function calculateAverage(historyArray, key) { - if (!historyArray || historyArray.length === 0) return null; - const validEntries = historyArray.filter(entry => typeof entry[key] === 'number' && !isNaN(entry[key])); - if (validEntries.length === 0) return null; - const sum = validEntries.reduce((acc, curr) => acc + curr[key], 0); - return sum / validEntries.length; - } - - function calculateAverageRate(historyArray, key) { - if (!historyArray || historyArray.length < 2) return null; - const validHistory = historyArray.filter(entry => - typeof entry.timestamp === 'number' && !isNaN(entry.timestamp) && - typeof entry[key] === 'number' && !isNaN(entry[key]) - ); - - if (validHistory.length < 2) return null; - - const oldest = validHistory[0]; - const newest = validHistory[validHistory.length - 1]; - const valueDiff = newest[key] - oldest[key]; - const timeDiffSeconds = (newest.timestamp - oldest.timestamp) / 1000; - - if (timeDiffSeconds <= 0) { - return null; - } - - if (valueDiff < 0) { - return null; - } - - return valueDiff / timeDiffSeconds; - } - - const processGuest = (guest, type) => { - let avgCpu = 0, avgMem = 0, avgDisk = 0; - let avgDiskReadRate = null, avgDiskWriteRate = null, avgNetInRate = null, avgNetOutRate = null; - let avgMemoryPercent = 'N/A', avgDiskPercent = 'N/A'; - let effectiveMemorySource = 'host'; // 'host' or 'guest' - let currentMemForAvg = 0; - let currentMemTotalForDisplay = guest.maxmem; - - const metricsData = PulseApp.state.get('metricsData') || []; - const metrics = metricsData.find(m => - m.id === guest.vmid && - m.type === guest.type && - m.node === guest.node && - m.endpointId === guest.endpointId - ); - - const guestUniqueId = guest.id; - - // Check for drag state and apply snapshot if active - const isDragging = PulseApp.ui.thresholds && PulseApp.ui.thresholds.isThresholdDragInProgress && PulseApp.ui.thresholds.isThresholdDragInProgress(); - const snapshot = guestMetricDragSnapshot[guestUniqueId]; - - if (isDragging && snapshot) { - avgDiskReadRate = snapshot.diskread; - avgDiskWriteRate = snapshot.diskwrite; - avgNetInRate = snapshot.netin; - avgNetOutRate = snapshot.netout; - - // For other metrics, continue with live data or defaults if snapshot doesn't cover them - // or if we decide to only snapshot I/O rates. - // For now, let other metrics be calculated as usual even during drag. - if (guest.status === 'running' && metrics && metrics.current) { - const currentDataPoint = { - timestamp: Date.now(), - ...metrics.current - }; - PulseApp.state.updateDashboardHistory(guestUniqueId, currentDataPoint); - const history = PulseApp.state.getDashboardHistory()[guestUniqueId] || []; - avgCpu = calculateAverage(history, 'cpu') ?? 0; - avgMem = calculateAverage(history, 'mem') ?? 0; - avgDisk = calculateAverage(history, 'disk') ?? 0; - // I/O rates are already set from snapshot if dragging - } else { - PulseApp.state.clearDashboardHistoryEntry(guestUniqueId); - // If not running, CPU/Mem/Disk also go to their defaults (0 or N/A) - } - - } else { - // Original logic if not dragging or no snapshot - if (guest.status === 'running' && metrics && metrics.current) { - let baseMemoryValue = metrics.current.mem; // Default to host memory - currentMemTotalForDisplay = guest.maxmem; // Default to host allocated max memory - effectiveMemorySource = 'host'; - - // Check if guest agent memory data is available and valid - if (metrics.current.guest_mem_actual_used_bytes !== undefined && metrics.current.guest_mem_actual_used_bytes !== null) { - baseMemoryValue = metrics.current.guest_mem_actual_used_bytes; - effectiveMemorySource = 'guest'; - // If guest provides its own total, use it, otherwise stick to guest.maxmem - if (metrics.current.guest_mem_total_bytes !== undefined && metrics.current.guest_mem_total_bytes > 0) { - currentMemTotalForDisplay = metrics.current.guest_mem_total_bytes; - } - // console.log(`VM ${guest.vmid}: Using GUEST memory: ${baseMemoryValue / (1024*1024)} MB / ${currentMemTotalForDisplay / (1024*1024)} MB`); - } else { - // console.log(`VM ${guest.vmid}: Using HOST memory: ${baseMemoryValue / (1024*1024)} MB / ${currentMemTotalForDisplay / (1024*1024)} MB`); - } - currentMemForAvg = baseMemoryValue; - - const currentDataPoint = { - timestamp: Date.now(), - ...metrics.current, - effective_mem: currentMemForAvg, // Store the memory value actually used for averaging - effective_mem_total: currentMemTotalForDisplay, // Store total used for percentage calc - effective_mem_source: effectiveMemorySource // Store the source for tooltip - }; - PulseApp.state.updateDashboardHistory(guestUniqueId, currentDataPoint); - const history = PulseApp.state.getDashboardHistory()[guestUniqueId] || []; - - avgCpu = calculateAverage(history, 'cpu') ?? 0; - avgMem = calculateAverage(history, 'effective_mem') ?? 0; // Average the memory value we decided to use - avgDisk = calculateAverage(history, 'disk') ?? 0; - avgDiskReadRate = calculateAverageRate(history, 'diskread'); - avgDiskWriteRate = calculateAverageRate(history, 'diskwrite'); - avgNetInRate = calculateAverageRate(history, 'netin'); - avgNetOutRate = calculateAverageRate(history, 'netout'); - } else { - PulseApp.state.clearDashboardHistoryEntry(guestUniqueId); - // Rates remain null (will be N/A), others default - } - } - - // Use the total memory that corresponds to the source of avgMem for percentage calculation - // If history has entries, the last one should have effective_mem_total and effective_mem_source - const historyForGuest = PulseApp.state.getDashboardHistory()[guestUniqueId]; - let finalMemTotalForPercent = guest.maxmem; // Fallback - let finalMemSourceForTooltip = 'host'; // Fallback - - if (historyForGuest && historyForGuest.length > 0) { - const lastHistoryEntry = historyForGuest[historyForGuest.length - 1]; - if (lastHistoryEntry.effective_mem_total !== undefined && lastHistoryEntry.effective_mem_total > 0) { - finalMemTotalForPercent = lastHistoryEntry.effective_mem_total; - } - if (lastHistoryEntry.effective_mem_source) { - finalMemSourceForTooltip = lastHistoryEntry.effective_mem_source; - } - } - - avgMemoryPercent = (finalMemTotalForPercent > 0 && typeof avgMem === 'number') ? Math.round(avgMem / finalMemTotalForPercent * 100) : 'N/A'; - avgDiskPercent = (guest.maxdisk > 0 && typeof avgDisk === 'number') ? Math.round(avgDisk / guest.maxdisk * 100) : 'N/A'; - - const name = guest.name || `${guest.type === 'qemu' ? 'VM' : 'CT'} ${guest.vmid}`; + dashboardData.forEach(guest => { const uptimeFormatted = PulseApp.utils.formatUptime(guest.uptime); - if (name.length > maxNameLength) maxNameLength = name.length; + if (guest.name.length > maxNameLength) maxNameLength = guest.name.length; if (uptimeFormatted.length > maxUptimeLength) maxUptimeLength = uptimeFormatted.length; - - let rawHostReportedMem = null; - if (guest.status === 'running' && metrics && metrics.current && metrics.current.mem !== undefined) { - rawHostReportedMem = metrics.current.mem; - } - - dashboardData.push({ - id: guest.vmid, - uniqueId: guestUniqueId, - vmid: guest.vmid, - name: name, - node: guest.node, - type: guest.type === 'qemu' ? 'VM' : 'CT', - status: guest.status, - cpu: avgCpu, - cpus: guest.cpus || 1, - memory: avgMemoryPercent, - memoryCurrent: avgMem, // This is now potentially guest actual used or host used - memoryTotal: finalMemTotalForPercent, // This is now potentially guest total or host allocated - memorySource: finalMemSourceForTooltip, // Add source for tooltip generation - rawHostMemory: rawHostReportedMem, // Store host raw memory for tooltip comparison - disk: avgDiskPercent, - diskCurrent: avgDisk, - diskTotal: guest.maxdisk, - uptime: guest.status === 'running' ? guest.uptime : 0, - diskread: avgDiskReadRate, - diskwrite: avgDiskWriteRate, - netin: avgNetInRate, - netout: avgNetOutRate - }); - }; - - const vmsData = PulseApp.state.get('vmsData') || []; - const containersData = PulseApp.state.get('containersData') || []; - - vmsData.forEach(vm => processGuest(vm, 'qemu')); - containersData.forEach(ct => processGuest(ct, 'lxc')); - - PulseApp.state.set('dashboardData', dashboardData); + }); const nameColWidth = Math.min(Math.max(maxNameLength * 8 + 16, 100), 300); const uptimeColWidth = Math.max(maxUptimeLength * 7 + 16, 80); @@ -233,22 +198,24 @@ PulseApp.ui.dashboard = (() => { } } - function updateDashboardTable() { - if (!tableBodyEl || !statusElementEl) { - console.error('Dashboard table body or status element not found/initialized!'); - return; - } + function refreshDashboardData() { + PulseApp.state.set('dashboardData', []); + let dashboardData = []; - refreshDashboardData(); + const vmsData = PulseApp.state.get('vmsData') || []; + const containersData = PulseApp.state.get('containersData') || []; - const dashboardData = PulseApp.state.get('dashboardData') || []; - const filterGuestType = PulseApp.state.get('filterGuestType'); - const filterStatus = PulseApp.state.get('filterStatus'); - const thresholdState = PulseApp.state.getThresholdState(); + vmsData.forEach(vm => dashboardData.push(_processSingleGuestData(vm))); + containersData.forEach(ct => dashboardData.push(_processSingleGuestData(ct))); + + PulseApp.state.set('dashboardData', dashboardData); + _setDashboardColumnWidths(dashboardData); + } + function _filterDashboardData(dashboardData, searchInput, filterGuestType, filterStatus, thresholdState) { const textSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : []; - let filteredData = dashboardData.filter(guest => { + return dashboardData.filter(guest => { const typeMatch = filterGuestType === FILTER_ALL || (filterGuestType === FILTER_VM && guest.type === GUEST_TYPE_VM) || (filterGuestType === FILTER_LXC && guest.type === GUEST_TYPE_CT); @@ -280,80 +247,54 @@ PulseApp.ui.dashboard = (() => { thresholdsMet = false; break; } - if (!(guestValue >= state.value)) { thresholdsMet = false; break; } } } - return typeMatch && statusMatch && searchMatch && thresholdsMet; }); + } - const sortStateMain = PulseApp.state.getSortState('main'); - let sortedData = PulseApp.utils.sortData(filteredData, sortStateMain.column, sortStateMain.direction, 'main'); - - let visibleCount = 0; + function _renderGroupedByNode(tableBody, sortedData, createRowFn) { + const nodeGroups = {}; let visibleNodes = new Set(); - const groupByNode = PulseApp.state.get('groupByNode'); + let visibleCount = 0; - if (groupByNode) { - const nodeGroups = {}; - sortedData.forEach(guest => { - const nodeName = guest.node || 'Unknown Node'; - if (!nodeGroups[nodeName]) nodeGroups[nodeName] = []; - nodeGroups[nodeName].push(guest); + sortedData.forEach(guest => { + const nodeName = guest.node || 'Unknown Node'; + if (!nodeGroups[nodeName]) nodeGroups[nodeName] = []; + nodeGroups[nodeName].push(guest); + }); + + tableBody.innerHTML = ''; + + Object.keys(nodeGroups).sort().forEach(nodeName => { + visibleNodes.add(nodeName.toLowerCase()); + const nodeHeaderRow = document.createElement('tr'); + nodeHeaderRow.className = 'node-header bg-gray-100 dark:bg-gray-700/80 font-semibold text-gray-700 dark:text-gray-300 text-xs'; + nodeHeaderRow.innerHTML = ` + + ${nodeName} + `; + tableBody.appendChild(nodeHeaderRow); + + nodeGroups[nodeName].forEach(guest => { + const guestRow = createRowFn(guest); + if (guestRow) { + tableBody.appendChild(guestRow); + visibleCount++; + } }); - - tableBodyEl.innerHTML = ''; - - Object.keys(nodeGroups).sort().forEach(nodeName => { - visibleNodes.add(nodeName.toLowerCase()); - const nodeHeaderRow = document.createElement('tr'); - nodeHeaderRow.className = 'node-header bg-gray-100 dark:bg-gray-700/80 font-semibold text-gray-700 dark:text-gray-300 text-xs'; - nodeHeaderRow.innerHTML = ` - - ${nodeName} - `; - tableBodyEl.appendChild(nodeHeaderRow); - - nodeGroups[nodeName].forEach(guest => { - const guestRow = createGuestRow(guest); - if (guestRow) { - tableBodyEl.appendChild(guestRow); - visibleCount++; - } - }); - }); - } else { - PulseApp.utils.renderTableBody(tableBodyEl, sortedData, createGuestRow, "No matching guests found.", 11); - visibleCount = sortedData.length; - sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase())); - } - - if (visibleCount === 0) { - let filterDescription = []; - if (filterGuestType !== FILTER_ALL) filterDescription.push(`Type: ${filterGuestType.toUpperCase()}`); - if (filterStatus !== FILTER_ALL) filterDescription.push(`Status: ${filterStatus}`); - 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(', ')}`); - } - - let message = "No guests match the current filters"; - if (filterDescription.length > 0) { - message += ` (${filterDescription.join('; ')})`; - } - message += "."; - - tableBodyEl.innerHTML = `${message}`; - } + }); + return { visibleCount, visibleNodes }; + } + function _updateDashboardStatusMessage(statusElement, visibleCount, visibleNodes, groupByNode, filterGuestType, filterStatus, searchInput, thresholdState) { + if (!statusElement) return; + const textSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : []; + const statusBaseText = `Updated: ${new Date().toLocaleTimeString()}`; let statusFilterText = textSearchTerms.length > 0 ? ` | Search: "${textSearchTerms.join(', ')}"` : ''; const typeLabel = filterGuestType !== FILTER_ALL ? filterGuestType.toUpperCase() : ''; @@ -362,9 +303,73 @@ PulseApp.ui.dashboard = (() => { if (otherFilters) { statusFilterText += ` | ${otherFilters}`; } + + 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)}`; + }); + statusFilterText += ` | Thresholds: ${thresholdTexts.join(', ')}`; + } + let statusCountText = ` | Showing ${visibleCount} guests`; if (groupByNode && visibleNodes.size > 0) statusCountText += ` across ${visibleNodes.size} nodes`; - statusElementEl.textContent = statusBaseText + statusFilterText + statusCountText; + statusElement.textContent = statusBaseText + statusFilterText + statusCountText; + } + + + function updateDashboardTable() { + if (!tableBodyEl || !statusElementEl) { + console.error('Dashboard table body or status element not found/initialized!'); + return; + } + + refreshDashboardData(); + + const dashboardData = PulseApp.state.get('dashboardData') || []; + const filterGuestType = PulseApp.state.get('filterGuestType'); + const filterStatus = PulseApp.state.get('filterStatus'); + const thresholdState = PulseApp.state.getThresholdState(); + const groupByNode = PulseApp.state.get('groupByNode'); + + const filteredData = _filterDashboardData(dashboardData, searchInput, filterGuestType, filterStatus, thresholdState); + const sortStateMain = PulseApp.state.getSortState('main'); + const sortedData = PulseApp.utils.sortData(filteredData, sortStateMain.column, sortStateMain.direction, 'main'); + + let visibleCount = 0; + let visibleNodes = new Set(); + + if (groupByNode) { + const groupRenderResult = _renderGroupedByNode(tableBodyEl, sortedData, createGuestRow); + visibleCount = groupRenderResult.visibleCount; + visibleNodes = groupRenderResult.visibleNodes; + } else { + PulseApp.utils.renderTableBody(tableBodyEl, sortedData, createGuestRow, "No matching guests found.", 11); + visibleCount = sortedData.length; + sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase())); + } + + 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(', ')}`); + } + let message = "No guests match the current filters"; + if (filterDescription.length > 0) message += ` (${filterDescription.join('; ')})`; + message += "."; + tableBodyEl.innerHTML = `${message}`; + } + + _updateDashboardStatusMessage(statusElementEl, visibleCount, visibleNodes, groupByNode, filterGuestType, filterStatus, searchInput, thresholdState); const mainSortColumn = sortStateMain.column; const mainHeader = document.querySelector(`#main-table th[data-sort="${mainSortColumn}"]`); @@ -379,6 +384,40 @@ PulseApp.ui.dashboard = (() => { } } + function _createCpuBarHtml(guest) { + if (guest.status !== STATUS_RUNNING) return '-'; + const cpuPercent = Math.round(guest.cpu * 100); + const cpuTooltipText = `${cpuPercent}% ${guest.cpus ? `(${(guest.cpu * guest.cpus).toFixed(1)}/${guest.cpus} cores)` : ''}`; + const cpuColorClass = PulseApp.utils.getUsageColor(cpuPercent); + return PulseApp.utils.createProgressTextBarHTML(cpuPercent, cpuTooltipText, cpuColorClass); + } + + function _createMemoryBarHtml(guest) { + if (guest.status !== STATUS_RUNNING) return '-'; + const memoryPercent = guest.memory; // This is already a percentage + let memoryTooltipText = `${PulseApp.utils.formatBytes(guest.memoryCurrent)} / ${PulseApp.utils.formatBytes(guest.memoryTotal)} (${memoryPercent}%)`; + if (guest.type === GUEST_TYPE_VM && guest.memorySource === 'guest' && guest.rawHostMemory !== null && guest.rawHostMemory !== undefined) { + memoryTooltipText += ` (Host: ${PulseApp.utils.formatBytes(guest.rawHostMemory)})`; + } + const memColorClass = PulseApp.utils.getUsageColor(memoryPercent); + return PulseApp.utils.createProgressTextBarHTML(memoryPercent, memoryTooltipText, memColorClass); + } + + function _createDiskBarHtml(guest) { + if (guest.status !== STATUS_RUNNING) return '-'; + if (guest.type === GUEST_TYPE_CT) { + const diskPercent = guest.disk; // This is already a percentage + const diskTooltipText = guest.diskTotal ? `${PulseApp.utils.formatBytes(guest.diskCurrent)} / ${PulseApp.utils.formatBytes(guest.diskTotal)} (${diskPercent}%)` : `${diskPercent}%`; + const diskColorClass = PulseApp.utils.getUsageColor(diskPercent); + return PulseApp.utils.createProgressTextBarHTML(diskPercent, diskTooltipText, diskColorClass); + } else { // For VMs, show total disk size, not a progress bar + if (guest.diskTotal) { + return `${PulseApp.utils.formatBytes(guest.diskTotal)}`; + } + return '-'; + } + } + function createGuestRow(guest) { const row = document.createElement('tr'); row.className = `border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 ${guest.status === STATUS_STOPPED ? 'opacity-60 grayscale' : ''}`; @@ -387,59 +426,21 @@ PulseApp.ui.dashboard = (() => { row.setAttribute('data-node', guest.node.toLowerCase()); row.setAttribute('data-id', guest.id); - let cpuBarHTML = '-'; - let memoryBarHTML = '-'; - let diskBarHTML = '-'; - let diskReadFormatted = '-'; - let diskWriteFormatted = '-'; - let netInFormatted = '-'; - let netOutFormatted = '-'; + const cpuBarHTML = _createCpuBarHtml(guest); + const memoryBarHTML = _createMemoryBarHtml(guest); + const diskBarHTML = _createDiskBarHtml(guest); - if (guest.status === STATUS_RUNNING) { - const cpuPercent = Math.round(guest.cpu * 100); - const memoryPercent = guest.memory; + const diskReadFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeed(guest.diskread, 0) : '-'; + const diskWriteFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeed(guest.diskwrite, 0) : '-'; + const netInFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeed(guest.netin, 0) : '-'; + const netOutFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeed(guest.netout, 0) : '-'; - const cpuTooltipText = `${cpuPercent}% ${guest.cpus ? `(${(guest.cpu * guest.cpus).toFixed(1)}/${guest.cpus} cores)` : ''}`; - - // Simplified memoryTooltipText - let memoryTooltipText = `${PulseApp.utils.formatBytes(guest.memoryCurrent)} / ${PulseApp.utils.formatBytes(guest.memoryTotal)} (${memoryPercent}%)`; - if (guest.type === GUEST_TYPE_VM && guest.memorySource === 'guest' && guest.rawHostMemory !== null && guest.rawHostMemory !== undefined) { - // If guest source is used for the main display, append host's view for comparison. - memoryTooltipText += ` (Host: ${PulseApp.utils.formatBytes(guest.rawHostMemory)})`; - } + const typeIconClass = guest.type === GUEST_TYPE_VM + ? 'vm-icon bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 px-1.5 py-0.5 font-medium' + : 'ct-icon bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300 px-1.5 py-0.5 font-medium'; + const typeIcon = `${guest.type === GUEST_TYPE_VM ? GUEST_TYPE_VM : 'LXC'}`; - const cpuColorClass = PulseApp.utils.getUsageColor(cpuPercent); - const memColorClass = PulseApp.utils.getUsageColor(memoryPercent); - - cpuBarHTML = PulseApp.utils.createProgressTextBarHTML(cpuPercent, cpuTooltipText, cpuColorClass); - memoryBarHTML = PulseApp.utils.createProgressTextBarHTML(memoryPercent, memoryTooltipText, memColorClass); - - if (guest.type === GUEST_TYPE_CT) { - const diskPercent = guest.disk; - const diskTooltipText = guest.diskTotal ? `${PulseApp.utils.formatBytes(guest.diskCurrent)} / ${PulseApp.utils.formatBytes(guest.diskTotal)} (${diskPercent}%)` : `${diskPercent}%`; - const diskColorClass = PulseApp.utils.getUsageColor(diskPercent); - diskBarHTML = PulseApp.utils.createProgressTextBarHTML(diskPercent, diskTooltipText, diskColorClass); - } else { - if (guest.diskTotal) { - const totalDiskFormatted = PulseApp.utils.formatBytes(guest.diskTotal); - diskBarHTML = `${totalDiskFormatted}`; - } else { - diskBarHTML = '-'; - } - } - - diskReadFormatted = PulseApp.utils.formatSpeed(guest.diskread, 0); - diskWriteFormatted = PulseApp.utils.formatSpeed(guest.diskwrite, 0); - netInFormatted = PulseApp.utils.formatSpeed(guest.netin, 0); - netOutFormatted = PulseApp.utils.formatSpeed(guest.netout, 0); - } - - const typeIconClass = guest.type === GUEST_TYPE_VM - ? 'vm-icon bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 px-1.5 py-0.5 font-medium' - : 'ct-icon bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300 px-1.5 py-0.5 font-medium'; - const typeIcon = `${guest.type === GUEST_TYPE_VM ? GUEST_TYPE_VM : 'LXC'}`; - - row.innerHTML = ` + row.innerHTML = ` ${guest.name} ${typeIcon} ${guest.id} @@ -451,10 +452,9 @@ PulseApp.ui.dashboard = (() => { ${diskWriteFormatted} ${netInFormatted} ${netOutFormatted} - `; - - return row; - } + `; + return row; + } function snapshotGuestMetricsForDrag() { guestMetricDragSnapshot = {}; // Clear previous snapshot @@ -484,4 +484,4 @@ PulseApp.ui.dashboard = (() => { snapshotGuestMetricsForDrag, // Export snapshot function clearGuestMetricSnapshots // Export clear function }; -})(); \ No newline at end of file +})();