// Rollout Component - Shows rollout panel with matching nodes and starts rollout class RolloutComponent extends Component { constructor(container, viewModel, eventBus) { super(container, viewModel, eventBus); logger.debug('RolloutComponent: Constructor called'); this.rolloutData = null; this.matchingNodes = []; this.onRolloutCallback = null; this.onCancelCallback = null; } setupEventListeners() { // Rollout button const rolloutBtn = this.findElement('#rollout-confirm-btn'); if (rolloutBtn) { this.addEventListener(rolloutBtn, 'click', this.handleRollout.bind(this)); } // Cancel button const cancelBtn = this.findElement('#rollout-cancel-btn'); if (cancelBtn) { this.addEventListener(cancelBtn, 'click', this.handleCancel.bind(this)); } } // Start rollout - hide labels and show status indicators startRollout() { const nodeItems = this.container.querySelectorAll('.rollout-node-item'); nodeItems.forEach(item => { const labelsDiv = item.querySelector('.rollout-node-labels'); const statusDiv = item.querySelector('.status-indicator'); if (labelsDiv && statusDiv) { labelsDiv.style.display = 'none'; statusDiv.style.display = 'block'; statusDiv.textContent = 'Ready'; statusDiv.className = 'status-indicator ready'; } }); // Disable the confirm button const confirmBtn = this.findElement('#rollout-confirm-btn'); if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = 'Rollout in Progress...'; } } // Update status for a specific node updateNodeStatus(nodeIp, status) { const nodeItem = this.container.querySelector(`[data-node-ip="${nodeIp}"]`); if (!nodeItem) return; const statusDiv = nodeItem.querySelector('.status-indicator'); if (!statusDiv) return; let displayStatus = status; let statusClass = ''; switch (status) { case 'updating_labels': displayStatus = 'Updating Labels...'; statusClass = 'uploading'; break; case 'uploading': displayStatus = 'Uploading...'; statusClass = 'uploading'; break; case 'completed': displayStatus = 'Completed'; statusClass = 'success'; break; case 'failed': displayStatus = 'Failed'; statusClass = 'error'; break; default: displayStatus = status; statusClass = 'pending'; } statusDiv.textContent = displayStatus; statusDiv.className = `status-indicator ${statusClass}`; } // Check if rollout is complete isRolloutComplete() { const statusIndicators = this.container.querySelectorAll('.status-indicator'); for (const indicator of statusIndicators) { const status = indicator.textContent.toLowerCase(); if (status !== 'completed' && status !== 'failed') { return false; } } return true; } // Reset to initial state (show labels, hide status indicators) resetRolloutState() { const nodeItems = this.container.querySelectorAll('.rollout-node-item'); nodeItems.forEach(item => { const labelsDiv = item.querySelector('.rollout-node-labels'); const statusDiv = item.querySelector('.status-indicator'); if (labelsDiv && statusDiv) { labelsDiv.style.display = 'block'; statusDiv.style.display = 'none'; } }); // Re-enable the confirm button const confirmBtn = this.findElement('#rollout-confirm-btn'); if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.textContent = `Rollout to ${this.matchingNodes.length} Node${this.matchingNodes.length !== 1 ? 's' : ''}`; } } mount() { super.mount(); logger.debug('RolloutComponent: Mounting...'); this.render(); logger.debug('RolloutComponent: Mounted successfully'); } setRolloutData(name, version, labels, matchingNodes) { this.rolloutData = { name, version, labels }; this.matchingNodes = matchingNodes; } setOnRolloutCallback(callback) { this.onRolloutCallback = callback; } setOnCancelCallback(callback) { this.onCancelCallback = callback; } render() { if (!this.rolloutData) { this.container.innerHTML = '
No rollout data available
'; return; } const { name, version, labels } = this.rolloutData; // Render labels as chips const labelsHTML = Object.entries(labels).map(([key, value]) => `${key}: ${value}` ).join(''); // Render matching nodes const nodesHTML = this.matchingNodes.map(node => { const nodeLabelsHTML = Object.entries(node.labels || {}).map(([key, value]) => `${key}: ${value}` ).join(''); return `
${this.escapeHtml(node.ip)}
Version: ${this.escapeHtml(node.version)}
${nodeLabelsHTML}
`; }).join(''); this.container.innerHTML = `

Deploy firmware to matching cluster nodes

${this.escapeHtml(name)}
Version: ${this.escapeHtml(version)}
${labelsHTML}

Matching Nodes (${this.matchingNodes.length})

${nodesHTML}
Warning: This will update firmware on ${this.matchingNodes.length} node${this.matchingNodes.length !== 1 ? 's' : ''}. The rollout process cannot be cancelled once started.
`; this.setupEventListeners(); } handleRollout() { if (!this.onRolloutCallback || this.matchingNodes.length === 0) { return; } // Send the firmware info and matching nodes directly const rolloutData = { firmware: { name: this.rolloutData.name, version: this.rolloutData.version, labels: this.rolloutData.labels }, nodes: this.matchingNodes }; this.onRolloutCallback(rolloutData); } handleCancel() { if (this.onCancelCallback) { this.onCancelCallback(); } } escapeHtml(text) { if (typeof text !== 'string') return text; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } window.RolloutComponent = RolloutComponent;