// Cluster View Component class ClusterViewComponent extends Component { constructor(container, viewModel, eventBus) { super(container, viewModel, eventBus); logger.debug('ClusterViewComponent: Constructor called'); logger.debug('ClusterViewComponent: Container:', container); logger.debug('ClusterViewComponent: Container ID:', container?.id); // Find elements for sub-components const primaryNodeContainer = this.findElement('.primary-node-info'); const clusterMembersContainer = this.findElement('#cluster-members-container'); logger.debug('ClusterViewComponent: Primary node container:', primaryNodeContainer); logger.debug('ClusterViewComponent: Cluster members container:', clusterMembersContainer); logger.debug('ClusterViewComponent: Cluster members container ID:', clusterMembersContainer?.id); logger.debug('ClusterViewComponent: Cluster members container innerHTML:', clusterMembersContainer?.innerHTML); // Create sub-components this.primaryNodeComponent = new PrimaryNodeComponent( primaryNodeContainer, viewModel, eventBus ); this.clusterMembersComponent = new ClusterMembersComponent( clusterMembersContainer, viewModel, eventBus ); logger.debug('ClusterViewComponent: Sub-components created'); // Track if we've already loaded data to prevent unnecessary reloads this.dataLoaded = false; } mount() { logger.debug('ClusterViewComponent: Mounting...'); super.mount(); logger.debug('ClusterViewComponent: Mounting sub-components...'); // Mount sub-components this.primaryNodeComponent.mount(); this.clusterMembersComponent.mount(); // Set up refresh button event listener (since it's in the cluster header, not in the members container) this.setupRefreshButton(); // Set up deploy button event listener this.setupDeployButton(); // Only load data if we haven't already or if the view model is empty const members = this.viewModel.get('members'); const shouldLoadData = true; // always perform initial refresh quickly if (shouldLoadData) { logger.debug('ClusterViewComponent: Starting initial data load...'); // Initial data load - ensure it happens after mounting // Trigger immediately to reduce perceived startup latency this.viewModel.updateClusterMembers().then(() => { this.dataLoaded = true; }).catch(error => { logger.error('ClusterViewComponent: Failed to load initial data:', error); }); } else { logger.debug('ClusterViewComponent: Data already loaded, skipping initial load'); } // Set up periodic updates // this.setupPeriodicUpdates(); // Disabled automatic refresh logger.debug('ClusterViewComponent: Mounted successfully'); } setupRefreshButton() { logger.debug('ClusterViewComponent: Setting up refresh button...'); const refreshBtn = this.findElement('.refresh-btn'); logger.debug('ClusterViewComponent: Found refresh button:', !!refreshBtn, refreshBtn); if (refreshBtn) { logger.debug('ClusterViewComponent: Adding click event listener to refresh button'); this.addEventListener(refreshBtn, 'click', this.handleRefresh.bind(this)); logger.debug('ClusterViewComponent: Event listener added successfully'); } else { logger.error('ClusterViewComponent: Refresh button not found!'); logger.debug('ClusterViewComponent: Container HTML:', this.container.innerHTML); logger.debug('ClusterViewComponent: All buttons in container:', this.container.querySelectorAll('button')); } } setupDeployButton() { logger.debug('ClusterViewComponent: Setting up deploy button...'); const deployBtn = this.findElement('#deploy-firmware-btn'); logger.debug('ClusterViewComponent: Found deploy button:', !!deployBtn, deployBtn); if (deployBtn) { logger.debug('ClusterViewComponent: Adding click event listener to deploy button'); this.addEventListener(deployBtn, 'click', this.handleDeploy.bind(this)); logger.debug('ClusterViewComponent: Event listener added successfully'); } else { logger.error('ClusterViewComponent: Deploy button not found!'); logger.debug('ClusterViewComponent: Container HTML:', this.container.innerHTML); logger.debug('ClusterViewComponent: All buttons in container:', this.container.querySelectorAll('button')); } } async handleDeploy() { logger.debug('ClusterViewComponent: Deploy button clicked, opening firmware upload drawer...'); // Get current filtered members from cluster members component const filteredMembers = this.clusterMembersComponent ? this.clusterMembersComponent.getFilteredMembers() : []; if (!filteredMembers || filteredMembers.length === 0) { alert('No nodes available for firmware deployment. Please ensure cluster members are loaded and visible.'); return; } // Open drawer with firmware upload interface this.openFirmwareUploadDrawer(filteredMembers); } openFirmwareUploadDrawer(targetNodes) { logger.debug('ClusterViewComponent: Opening firmware upload drawer for', targetNodes.length, 'nodes'); // Get display name for drawer title const nodeCount = targetNodes.length; const displayName = `Firmware Deployment - ${nodeCount} node${nodeCount !== 1 ? 's' : ''}`; // Open drawer with content callback (hide terminal button for firmware upload) this.clusterMembersComponent.drawer.openDrawer(displayName, (contentContainer, setActiveComponent) => { // Create firmware upload view model and component const firmwareUploadVM = new ClusterFirmwareViewModel(); firmwareUploadVM.setTargetNodes(targetNodes); // Create HTML for firmware upload interface contentContainer.innerHTML = `
No file selected

Target Nodes (${targetNodes.length})

${targetNodes.map(node => `
${node.hostname || node.ip} ${node.ip}
Ready
`).join('')}
`; // Create and mount firmware upload component const firmwareUploadComponent = new FirmwareUploadComponent(contentContainer, firmwareUploadVM, this.eventBus); setActiveComponent(firmwareUploadComponent); firmwareUploadComponent.mount(); }, null, () => { // Close callback - clear any upload state logger.debug('ClusterViewComponent: Firmware upload drawer closed'); }, true); // Hide terminal button for firmware upload } async handleRefresh() { logger.debug('ClusterViewComponent: Refresh button clicked, performing full refresh...'); // Get the refresh button and show loading state const refreshBtn = this.findElement('.refresh-btn'); logger.debug('ClusterViewComponent: Found refresh button for loading state:', !!refreshBtn); if (refreshBtn) { const originalText = refreshBtn.innerHTML; logger.debug('ClusterViewComponent: Original button text:', originalText); refreshBtn.innerHTML = ` Refreshing... `; refreshBtn.disabled = true; try { logger.debug('ClusterViewComponent: Starting cluster members update...'); // Always perform a full refresh when user clicks refresh button await this.viewModel.updateClusterMembers(); logger.debug('ClusterViewComponent: Cluster members update completed successfully'); } catch (error) { logger.error('ClusterViewComponent: Error during refresh:', error); // Show error state if (this.clusterMembersComponent && this.clusterMembersComponent.showErrorState) { this.clusterMembersComponent.showErrorState(error.message || 'Refresh failed'); } } finally { logger.debug('ClusterViewComponent: Restoring button state...'); // Restore button state refreshBtn.innerHTML = originalText; refreshBtn.disabled = false; } } else { logger.warn('ClusterViewComponent: Refresh button not found, using fallback refresh'); // Fallback if button not found try { await this.viewModel.updateClusterMembers(); } catch (error) { logger.error('ClusterViewComponent: Fallback refresh failed:', error); if (this.clusterMembersComponent && this.clusterMembersComponent.showErrorState) { this.clusterMembersComponent.showErrorState(error.message || 'Refresh failed'); } } } } unmount() { logger.debug('ClusterViewComponent: Unmounting...'); // Unmount sub-components if (this.primaryNodeComponent) { this.primaryNodeComponent.unmount(); } if (this.clusterMembersComponent) { this.clusterMembersComponent.unmount(); } // Clear intervals if (this.updateInterval) { clearInterval(this.updateInterval); } super.unmount(); logger.debug('ClusterViewComponent: Unmounted'); } // Override pause method to handle sub-components onPause() { logger.debug('ClusterViewComponent: Pausing...'); // Pause sub-components if (this.primaryNodeComponent && this.primaryNodeComponent.isMounted) { this.primaryNodeComponent.pause(); } if (this.clusterMembersComponent && this.clusterMembersComponent.isMounted) { this.clusterMembersComponent.pause(); } // Clear any active intervals if (this.updateInterval) { clearInterval(this.updateInterval); this.updateInterval = null; } } // Override resume method to handle sub-components onResume() { logger.debug('ClusterViewComponent: Resuming...'); // Resume sub-components if (this.primaryNodeComponent && this.primaryNodeComponent.isMounted) { this.primaryNodeComponent.resume(); } if (this.clusterMembersComponent && this.clusterMembersComponent.isMounted) { this.clusterMembersComponent.resume(); } // Restart periodic updates if needed // this.setupPeriodicUpdates(); // Disabled automatic refresh } // Override to determine if re-render is needed on resume shouldRenderOnResume() { // Don't re-render on resume - the component should maintain its state return false; } setupPeriodicUpdates() { // Update primary node display every 10 seconds this.updateInterval = setInterval(() => { this.viewModel.updatePrimaryNodeDisplay(); }, 10000); } } window.ClusterViewComponent = ClusterViewComponent;