mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 18:53:37 +00:00
Refactor(ui/dashboard): Decompose dashboard update logic into focused helper functions
This commit is contained in:
+315
-315
@@ -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 = `<td colspan="11" class="px-2 py-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>
|
||||
${nodeName}
|
||||
</td>`;
|
||||
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 = `<td colspan="11" class="px-2 py-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>
|
||||
${nodeName}
|
||||
</td>`;
|
||||
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 = `<tr><td colspan="11" class="p-4 text-center text-gray-500 dark:text-gray-400">${message}</td></tr>`;
|
||||
}
|
||||
});
|
||||
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 = `<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);
|
||||
|
||||
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 `<span class="text-xs text-gray-700 dark:text-gray-200 truncate">${PulseApp.utils.formatBytes(guest.diskTotal)}</span>`;
|
||||
}
|
||||
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 = `<span class="type-icon inline-block rounded text-xs align-middle ${typeIconClass}">${guest.type === GUEST_TYPE_VM ? GUEST_TYPE_VM : 'LXC'}</span>`;
|
||||
|
||||
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 = `<span class="text-xs text-gray-700 dark:text-gray-200 truncate">${totalDiskFormatted}</span>`;
|
||||
} 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 = `<span class="type-icon inline-block rounded text-xs align-middle ${typeIconClass}">${guest.type === GUEST_TYPE_VM ? GUEST_TYPE_VM : 'LXC'}</span>`;
|
||||
|
||||
row.innerHTML = `
|
||||
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>
|
||||
@@ -451,10 +452,9 @@ PulseApp.ui.dashboard = (() => {
|
||||
<td class="p-1 px-2 whitespace-nowrap">${diskWriteFormatted}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap">${netInFormatted}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap">${netOutFormatted}</td>
|
||||
`;
|
||||
|
||||
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
|
||||
};
|
||||
})();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user