// Monitoring View Component class MonitoringViewComponent extends Component { constructor(container, viewModel, eventBus) { super(container, viewModel, eventBus); logger.debug('MonitoringViewComponent: Constructor called'); logger.debug('MonitoringViewComponent: Container:', container); logger.debug('MonitoringViewComponent: Container ID:', container?.id); // Track if we've already loaded data to prevent unnecessary reloads this.dataLoaded = false; // Drawer state for desktop this.drawer = new DrawerComponent(); } mount() { logger.debug('MonitoringViewComponent: Mounting...'); super.mount(); // Set up refresh button event listener this.setupRefreshButton(); // Only load data if we haven't already or if the view model is empty const clusterMembers = this.viewModel.get('clusterMembers'); if (!this.dataLoaded || !clusterMembers || clusterMembers.length === 0) { this.loadData(); } // Subscribe to view model changes this.setupSubscriptions(); } setupRefreshButton() { const refreshBtn = this.findElement('#refresh-monitoring-btn'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { this.refreshData(); }); } } setupSubscriptions() { // Subscribe to cluster members changes this.viewModel.subscribe('clusterMembers', () => { this.render(); }); // Subscribe to node resources changes this.viewModel.subscribe('nodeResources', () => { this.render(); }); // Subscribe to cluster summary changes this.viewModel.subscribe('clusterSummary', () => { this.render(); }); // Subscribe to loading state changes this.viewModel.subscribe('isLoading', () => { this.render(); }); // Subscribe to error changes this.viewModel.subscribe('error', () => { this.render(); }); } async loadData() { logger.debug('MonitoringViewComponent: Loading data...'); this.dataLoaded = true; await this.viewModel.loadClusterData(); } async refreshData() { logger.debug('MonitoringViewComponent: Refreshing data...'); await this.viewModel.refresh(); } // Determine if we should use desktop drawer behavior isDesktop() { return this.drawer.isDesktop(); } // Open drawer for a specific node openDrawerForNode(nodeData) { const { ip, hostname } = nodeData; // Get display name for drawer title let displayName = ip; if (hostname && ip) { displayName = `${hostname} - ${ip}`; } else if (hostname) { displayName = hostname; } else if (ip) { displayName = ip; } // Open drawer with content callback this.drawer.openDrawer(displayName, (contentContainer, setActiveComponent) => { // Load and mount NodeDetails into drawer const nodeDetailsVM = new NodeDetailsViewModel(); const nodeDetailsComponent = new NodeDetailsComponent(contentContainer, nodeDetailsVM, this.eventBus); setActiveComponent(nodeDetailsComponent); nodeDetailsVM.loadNodeDetails(ip).then(() => { nodeDetailsComponent.mount(); }).catch((error) => { logger.error('Failed to load node details for drawer:', error); contentContainer.innerHTML = `
Error loading node details:
${this.escapeHtml(error.message)}
`; }); }); } closeDrawer() { this.drawer.closeDrawer(); } // Clean up resources cleanup() { if (this.drawer) { this.drawer.cleanup(); } super.cleanup(); } // Setup click event listeners for node cards setupNodeCardClickListeners(container) { const nodeCards = container.querySelectorAll('.node-card'); nodeCards.forEach(card => { const nodeIp = card.dataset.nodeIp; if (nodeIp) { // Find the node data const nodeResources = this.viewModel.get('nodeResources'); const nodeData = nodeResources.get(nodeIp); if (nodeData) { this.addEventListener(card, 'click', (e) => { e.preventDefault(); e.stopPropagation(); this.openDrawerForNode(nodeData); }); // Add hover cursor style card.style.cursor = 'pointer'; } } }); } render() { logger.debug('MonitoringViewComponent: Rendering...'); const isLoading = this.viewModel.get('isLoading'); const error = this.viewModel.get('error'); const clusterSummary = this.viewModel.get('clusterSummary'); const nodeResources = this.viewModel.get('nodeResources'); const lastUpdated = this.viewModel.get('lastUpdated'); // Render cluster summary this.renderClusterSummary(isLoading, error, clusterSummary, lastUpdated); // Render nodes monitoring this.renderNodesMonitoring(isLoading, error, nodeResources); } renderClusterSummary(isLoading, error, clusterSummary, lastUpdated) { const container = this.findElement('#cluster-summary'); if (!container) return; if (isLoading) { container.innerHTML = `
Loading cluster resource summary...
`; return; } if (error) { container.innerHTML = `
❌ Error: ${error}
`; return; } const cpuUtilization = this.viewModel.getResourceUtilization('Cpu'); const memoryUtilization = this.viewModel.getResourceUtilization('Memory'); const storageUtilization = this.viewModel.getResourceUtilization('Storage'); const lastUpdatedText = lastUpdated ? `Last updated: ${lastUpdated.toLocaleTimeString()}` : 'Never updated'; container.innerHTML = `

Summary

${lastUpdatedText}
🖥️
Total Nodes
${clusterSummary.totalNodes}
CPU
${Math.round(clusterSummary.totalCpu - clusterSummary.availableCpu)}MHz / ${Math.round(clusterSummary.totalCpu)}MHz
${cpuUtilization}% used
🧠
Memory
${this.viewModel.formatResourceValue(clusterSummary.totalMemory - clusterSummary.availableMemory, 'memory')} / ${this.viewModel.formatResourceValue(clusterSummary.totalMemory, 'memory')}
${memoryUtilization}% used
💾
Storage
${this.viewModel.formatResourceValue(clusterSummary.totalStorage - clusterSummary.availableStorage, 'storage')} / ${this.viewModel.formatResourceValue(clusterSummary.totalStorage, 'storage')}
${storageUtilization}% used
`; } renderNodesMonitoring(isLoading, error, nodeResources) { const container = this.findElement('#nodes-monitoring'); if (!container) return; if (isLoading) { container.innerHTML = `
Loading node resource data...
`; return; } if (error) { container.innerHTML = `
❌ Error: ${error}
`; return; } if (!nodeResources || nodeResources.size === 0) { container.innerHTML = `
No node resource data available
`; return; } const nodesHtml = Array.from(nodeResources.values()).map(nodeData => { return this.renderNodeCard(nodeData); }).join(''); container.innerHTML = `

🖥️ Node Resource Details

${nodesHtml}
`; // Add click event listeners to node cards this.setupNodeCardClickListeners(container); } renderNodeCard(nodeData) { const { ip, hostname, resources, hasResources, error, resourceSource } = nodeData; if (!hasResources) { return `
${hostname || ip}
${ip}
❌ No Resources
${error || 'Monitoring endpoint not available'}
`; } // Extract resource data (handle both monitoring API and basic data formats) const cpu = resources?.cpu || {}; const memory = resources?.memory || {}; const storage = resources?.filesystem || resources?.storage || {}; let cpuTotal, cpuAvailable, cpuUsed, cpuUtilization; let memoryTotal, memoryAvailable, memoryUsed, memoryUtilization; let storageTotal, storageAvailable, storageUsed, storageUtilization; if (resourceSource === 'monitoring') { // Real monitoring API format const cpuFreqMHz = nodeData.basic?.cpuFreqMHz || 80; // Get CPU frequency from basic resources cpuTotal = cpuFreqMHz; // Total CPU frequency in MHz cpuAvailable = cpuFreqMHz * (100 - (cpu.average_usage || 0)) / 100; // Available frequency cpuUsed = cpuFreqMHz * (cpu.average_usage || 0) / 100; // Used frequency cpuUtilization = Math.round(cpu.average_usage || 0); memoryTotal = memory.total_heap || 0; memoryAvailable = memory.free_heap || 0; memoryUsed = memoryTotal - memoryAvailable; memoryUtilization = memoryTotal > 0 ? Math.round((memoryUsed / memoryTotal) * 100) : 0; storageTotal = storage.total_bytes || 0; storageAvailable = storage.free_bytes || 0; storageUsed = storageTotal - storageAvailable; storageUtilization = storageTotal > 0 ? Math.round((storageUsed / storageTotal) * 100) : 0; } else { // Basic data format - use CPU frequency from basic resources const cpuFreqMHz = nodeData.basic?.cpuFreqMHz || 80; cpuTotal = cpuFreqMHz; // Total CPU frequency in MHz cpuAvailable = cpuFreqMHz * (cpu.available || 0.8); // Available frequency cpuUsed = cpuFreqMHz * (cpu.used || 0.2); // Used frequency cpuUtilization = cpuTotal > 0 ? Math.round((cpuUsed / cpuTotal) * 100) : 0; memoryTotal = memory.total || 0; memoryAvailable = memory.available || memory.free || 0; memoryUsed = memoryTotal - memoryAvailable; memoryUtilization = memoryTotal > 0 ? Math.round((memoryUsed / memoryTotal) * 100) : 0; storageTotal = storage.total || 0; storageAvailable = storage.available || storage.free || 0; storageUsed = storageTotal - storageAvailable; storageUtilization = storageTotal > 0 ? Math.round((storageUsed / storageTotal) * 100) : 0; } const resourceSourceText = resourceSource === 'monitoring' ? '📊 Full Monitoring' : resourceSource === 'basic' ? '📋 Basic Data' : '❓ Unknown'; return `
${hostname || ip}
${ip}
${resourceSourceText}
⚡ CPU
${Math.round(cpuUsed)}MHz / ${Math.round(cpuTotal)}MHz
${cpuUtilization}% used
🧠 Memory
${this.viewModel.formatResourceValue(memoryUsed, 'memory')} / ${this.viewModel.formatResourceValue(memoryTotal, 'memory')}
${memoryUtilization}% used
💾 Storage
${this.viewModel.formatResourceValue(storageUsed, 'storage')} / ${this.viewModel.formatResourceValue(storageTotal, 'storage')}
${storageUtilization}% used
`; } }