diff --git a/src/public/js/ui/storage.js b/src/public/js/ui/storage.js
index eb17087ad..8f953d132 100644
--- a/src/public/js/ui/storage.js
+++ b/src/public/js/ui/storage.js
@@ -119,14 +119,12 @@ PulseApp.ui.storage = (() => {
}
const contentTypes = contentString.split(',').map(ct => ct.trim()).filter(ct => ct);
- contentTypes.sort();
- const contentBadges = contentTypes.map(ct => {
- const details = getContentBadgeDetails(ct);
- return `
${ct}`;
- }).join('');
-
- const result = contentBadges || '-';
+ // Simplify display - just show comma-separated list with subtle styling
+ const result = contentTypes.length > 0
+ ? `
${contentTypes.join(', ')}`
+ : '-';
+
contentBadgeHTMLCache.set(contentString, result);
return result;
}
@@ -142,6 +140,89 @@ PulseApp.ui.storage = (() => {
return sortedArray;
}
+ let currentSortOrder = 'name'; // 'name', 'usage-asc', 'usage-desc'
+
+ function calculateStorageSummary(nodes) {
+ let totalUsed = 0;
+ let totalAvailable = 0;
+ let totalCapacity = 0;
+ let storageCount = 0;
+ let criticalCount = 0;
+ let warningCount = 0;
+
+ nodes.forEach(node => {
+ if (node && node.storage && Array.isArray(node.storage)) {
+ node.storage.forEach(store => {
+ if (store.enabled !== 0 && store.active !== 0) {
+ totalUsed += store.used || 0;
+ totalAvailable += store.avail || 0;
+ totalCapacity += store.total || 0;
+ storageCount++;
+
+ const usagePercent = store.total > 0 ? (store.used / store.total) * 100 : 0;
+ if (usagePercent >= 90) criticalCount++;
+ else if (usagePercent >= 80) warningCount++;
+ }
+ });
+ }
+ });
+
+ return {
+ totalUsed,
+ totalAvailable,
+ totalCapacity,
+ storageCount,
+ criticalCount,
+ warningCount,
+ usagePercent: totalCapacity > 0 ? (totalUsed / totalCapacity) * 100 : 0
+ };
+ }
+
+ function createStorageSummaryCard(summary) {
+ const usageColorClass = PulseApp.utils.getUsageColor(summary.usagePercent);
+
+ return `
+
+
+
+
Storage Summary
+
+
+
Total Storage
+
${summary.storageCount}
+
+
+
Total Capacity
+
${PulseApp.utils.formatBytes(summary.totalCapacity)}
+
+
+
Used
+
${PulseApp.utils.formatBytes(summary.totalUsed)}
+
+
+
Available
+
${PulseApp.utils.formatBytes(summary.totalAvailable)}
+
+
+
+
+
+ Overall Usage
+ ${summary.usagePercent.toFixed(1)}%
+
+ ${PulseApp.utils.createProgressTextBarHTML(summary.usagePercent, '', usageColorClass, '')}
+ ${summary.criticalCount > 0 || summary.warningCount > 0 ? `
+
+ ${summary.criticalCount > 0 ? `● ${summary.criticalCount} critical` : ''}
+ ${summary.warningCount > 0 ? `● ${summary.warningCount} warning` : ''}
+
+ ` : ''}
+
+
+
+ `;
+ }
+
function updateStorageInfo() {
const contentDiv = document.getElementById('storage-info-content');
if (!contentDiv) return;
@@ -171,11 +252,32 @@ PulseApp.ui.storage = (() => {
return;
}
- // Pre-sort storage data for each node to avoid repeated sorting
+ const container = document.createElement('div');
+
+ // Pre-sort storage data for each node
const storageByNode = nodes.reduce((acc, node) => {
if (node && node.node) {
- const storageData = Array.isArray(node.storage) ? node.storage : [];
- acc[node.node] = sortNodeStorageData(storageData);
+ let storageData = Array.isArray(node.storage) ? [...node.storage] : [];
+
+ // Apply current sort order
+ if (currentSortOrder === 'usage-desc') {
+ storageData.sort((a, b) => {
+ const percentA = a.total > 0 ? (a.used / a.total) * 100 : 0;
+ const percentB = b.total > 0 ? (b.used / b.total) * 100 : 0;
+ return percentB - percentA; // Descending
+ });
+ } else if (currentSortOrder === 'usage-asc') {
+ storageData.sort((a, b) => {
+ const percentA = a.total > 0 ? (a.used / a.total) * 100 : 0;
+ const percentB = b.total > 0 ? (b.used / b.total) * 100 : 0;
+ return percentA - percentB; // Ascending
+ });
+ } else {
+ // Default name sort
+ storageData = sortNodeStorageData(storageData);
+ }
+
+ acc[node.node] = storageData;
}
return acc;
}, {});
@@ -184,34 +286,66 @@ PulseApp.ui.storage = (() => {
if (nodeKeys.length === 0) {
if (PulseApp.ui.emptyStates) {
- contentDiv.innerHTML = PulseApp.ui.emptyStates.createEmptyState('no-storage');
+ container.innerHTML += PulseApp.ui.emptyStates.createEmptyState('no-storage');
} else {
- contentDiv.innerHTML = '
No storage data found associated with nodes.
';
+ container.innerHTML += '
No storage data found associated with nodes.
';
}
+ contentDiv.appendChild(container);
return;
}
+ const sortedNodeNames = Object.keys(storageByNode).sort((a, b) => a.localeCompare(b));
+
+ // Add node storage summary cards
+ const summaryCardsContainer = document.createElement('div');
+ summaryCardsContainer.className = 'mb-3';
+
+ const cardsGrid = document.createElement('div');
+ cardsGrid.className = 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3';
+
+ sortedNodeNames.forEach(nodeName => {
+ const nodeStorageData = storageByNode[nodeName];
+ if (nodeStorageData.length > 0) {
+ const card = createNodeStorageSummaryCard(nodeName, nodeStorageData);
+ cardsGrid.appendChild(card);
+ }
+ });
+
+ summaryCardsContainer.appendChild(cardsGrid);
+ container.appendChild(summaryCardsContainer);
+
+ // Table view with scroll container
+ const tableContainer = document.createElement('div');
+ tableContainer.className = 'table-container max-h-[80vh] overflow-y-auto overflow-x-auto border border-gray-200 dark:border-gray-700 rounded overflow-hidden scrollbar';
+
const table = document.createElement('table');
table.className = 'w-full text-sm border-collapse table-auto min-w-full';
- const thead = document.createElement('thead');
- thead.innerHTML = `
-
- | Storage |
- Content |
- Type |
- Shared |
- Usage |
- Avail |
- Total |
-
- `;
- table.appendChild(thead);
+ const thead = document.createElement('thead');
+ const sortIndicator = (order) => {
+ if (currentSortOrder === order) {
+ return order === 'usage-desc' ? ' ↓' : ' ↑';
+ }
+ return '';
+ };
+
+ thead.innerHTML = `
+
+ | Storage |
+ Content |
+ Type |
+ Shared |
+
+ Avail |
+ Total |
+
+ `;
+ table.appendChild(thead);
- const tbody = document.createElement('tbody');
- tbody.className = 'divide-y divide-gray-200 dark:divide-gray-600';
-
- const sortedNodeNames = Object.keys(storageByNode).sort((a, b) => a.localeCompare(b));
+ const tbody = document.createElement('tbody');
+ tbody.className = 'divide-y divide-gray-200 dark:divide-gray-600';
// Calculate dynamic column widths for responsive display
let maxStorageLength = 0;
@@ -255,16 +389,33 @@ PulseApp.ui.storage = (() => {
return;
}
- // Use pre-sorted data instead of sorting again
+ // Use pre-sorted data
nodeStorageData.forEach(store => {
const row = _createStorageRow(store);
tbody.appendChild(row);
});
});
- table.appendChild(thead);
table.appendChild(tbody);
- contentDiv.appendChild(table);
+ tableContainer.appendChild(table);
+ container.appendChild(tableContainer);
+ contentDiv.appendChild(container);
+
+ // Add click handler for sort
+ const usageSortHeader = document.getElementById('usage-sort-header');
+ if (usageSortHeader) {
+ usageSortHeader.addEventListener('click', () => {
+ // Cycle through sort orders: name -> usage-desc -> usage-asc -> name
+ if (currentSortOrder === 'name') {
+ currentSortOrder = 'usage-desc';
+ } else if (currentSortOrder === 'usage-desc') {
+ currentSortOrder = 'usage-asc';
+ } else {
+ currentSortOrder = 'name';
+ }
+ updateStorageInfo();
+ });
+ }
// Initialize mobile scroll indicators
if (window.innerWidth < 768) {
@@ -281,30 +432,173 @@ PulseApp.ui.storage = (() => {
}
}
+ function createNodeStorageSummaryCard(nodeName, storageList) {
+ const card = document.createElement('div');
+ card.className = 'bg-white dark:bg-gray-800 shadow-md rounded-lg p-2 border border-gray-200 dark:border-gray-700 flex flex-col gap-1';
+
+ // Get active storages and sort by usage percentage
+ const activeStorages = [];
+
+ storageList.forEach(store => {
+ if (store.enabled !== 0 && store.active !== 0 && store.total > 0) {
+ const usagePercent = (store.used / store.total) * 100;
+ activeStorages.push({
+ name: store.storage,
+ total: store.total,
+ used: store.used || 0,
+ avail: store.avail || 0,
+ usagePercent: usagePercent,
+ shared: store.shared === 1,
+ type: store.type
+ });
+ }
+ });
+
+ // Sort by usage percentage (most full first)
+ activeStorages.sort((a, b) => b.usagePercent - a.usagePercent);
+
+ // Count warnings/critical
+ let criticalCount = 0;
+ let warningCount = 0;
+ activeStorages.forEach(s => {
+ if (s.usagePercent >= 90) criticalCount++;
+ else if (s.usagePercent >= 80) warningCount++;
+ });
+
+ card.innerHTML = `
+
+
${nodeName}
+
+ ${criticalCount > 0 ? `● ${criticalCount}` : ''}
+ ${warningCount > 0 ? `● ${warningCount}` : ''}
+ ${activeStorages.length}
+
+
+ ${activeStorages.map(storage => {
+ const color = PulseApp.utils.getUsageColor(storage.usagePercent);
+ const progressColorClass = {
+ 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';
+
+ return `
+
+
+ ${storage.name}:
+ ${storage.shared ? '●' : ''}
+ ${storage.usagePercent.toFixed(0)}%
+
+
+
+ `;
+ }).join('')}
+ `;
+
+ return card;
+ }
+
+
+ function createStorageCard(store, nodeName) {
+ const usagePercent = store.total > 0 ? (store.used / store.total) * 100 : 0;
+ const isWarning = usagePercent >= 80 && usagePercent < 90;
+ const isCritical = usagePercent >= 90;
+ const isDisabled = store.enabled === 0 || store.active === 0;
+
+ const usageColorClass = PulseApp.utils.getUsageColor(usagePercent);
+ const usageBarHTML = PulseApp.utils.createProgressTextBarHTML(usagePercent, '', usageColorClass, `${usagePercent.toFixed(0)}%`);
+
+ let cardClasses = 'bg-white dark:bg-gray-800 shadow-md rounded-lg p-3 border border-gray-200 dark:border-gray-700 flex flex-col gap-2 transition-all duration-150 ease-out hover:shadow-lg hover:-translate-y-0.5';
+ if (isDisabled) {
+ cardClasses += ' opacity-50 grayscale-[50%]';
+ }
+ if (isCritical) {
+ cardClasses += ' ring-2 ring-red-500 border-red-500';
+ } else if (isWarning) {
+ cardClasses += ' ring-1 ring-yellow-500 border-yellow-500';
+ }
+
+ const contentTypes = store.content ? store.content.split(',').map(ct => ct.trim()).filter(ct => ct) : [];
+ const sharedBadge = store.shared === 1
+ ? '
Shared'
+ : '';
+
+ const card = document.createElement('div');
+ card.className = cardClasses;
+
+ card.innerHTML = `
+
+
+
${store.storage || 'N/A'}
+
${nodeName}
+
+
+ ${sharedBadge}
+ ${isCritical ? '' : (isWarning ? '' : '')}
+
+
+
+
+
+ Type: ${store.type || 'N/A'}
+ ${usagePercent.toFixed(0)}%
+
+ ${usageBarHTML}
+
+ ${PulseApp.utils.formatBytes(store.used)} used
+ ${PulseApp.utils.formatBytes(store.avail)} free
+
+
+
+ ${contentTypes.length > 0 ? `
+
+ Content: ${contentTypes.join(', ')}
+
+ ` : ''}
+ `;
+
+ return card;
+ }
+
function _createStorageRow(store) {
const row = document.createElement('tr');
const isDisabled = store.enabled === 0 || store.active === 0;
- row.className = `border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${isDisabled ? 'opacity-50 grayscale-[50%]' : ''}`;
-
const usagePercent = store.total > 0 ? (store.used / store.total) * 100 : 0;
+ const isWarning = usagePercent >= 80 && usagePercent < 90;
+ const isCritical = usagePercent >= 90;
+
+ let rowClasses = 'border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700';
+ if (isDisabled) {
+ rowClasses += ' opacity-50 grayscale-[50%]';
+ }
+ if (isCritical) {
+ rowClasses += ' bg-red-50 dark:bg-red-900/10';
+ } else if (isWarning) {
+ rowClasses += ' bg-yellow-50 dark:bg-yellow-900/10';
+ }
+ row.className = rowClasses;
+
const usageTooltipText = `${PulseApp.utils.formatBytes(store.used)} / ${PulseApp.utils.formatBytes(store.total)} (${usagePercent.toFixed(1)}%)`;
const usageColorClass = PulseApp.utils.getUsageColor(usagePercent);
const usageBarHTML = PulseApp.utils.createProgressTextBarHTML(usagePercent, usageTooltipText, usageColorClass, `${usagePercent.toFixed(0)}%`);
- const sharedIconTooltip = store.shared === 1 ? 'Shared across cluster' : 'Local to node';
- const isDarkMode = document.documentElement.classList.contains('dark');
- const localIconGrayClass = isDarkMode ? 'text-gray-400' : 'text-gray-300';
- const sharedIcon = store.shared === 1 ? `
`
- : `
`;
+ const sharedText = store.shared === 1
+ ? '
Shared'
+ : '
Local';
// Use cached content badge HTML instead of processing inline
const contentBadges = getContentBadgesHTML(store.content);
+ const warningBadge = isCritical ? '
' :
+ (isWarning ? '
' : '');
+
row.innerHTML = `
-
${store.storage || 'N/A'} |
-
${contentBadges} |
-
${store.type || 'N/A'} |
-
${sharedIcon} |
+
${store.storage || 'N/A'}${warningBadge} |
+
${contentBadges} |
+
${store.type || 'N/A'} |
+
${sharedText} |
${usageBarHTML} |
${PulseApp.utils.formatBytes(store.avail)} |
${PulseApp.utils.formatBytes(store.total)} |