// Registry Firmware Component - CRUD interface for firmware registry class FirmwareComponent extends Component { constructor(container, viewModel, eventBus) { super(container, viewModel, eventBus); logger.debug('FirmwareComponent: Constructor called'); logger.debug('FirmwareComponent: Container:', container); // Initialize drawer component this.drawer = new DrawerComponent(); // Registry connection status this.registryConnected = false; this.registryError = null; } setupEventListeners() { // Setup refresh button const refreshBtn = this.findElement('#refresh-firmware-btn'); if (refreshBtn) { this.addEventListener(refreshBtn, 'click', this.refreshFirmwareList.bind(this)); } // Setup add firmware button const addBtn = this.findElement('#add-firmware-btn'); if (addBtn) { this.addEventListener(addBtn, 'click', this.showAddFirmwareForm.bind(this)); } // Setup search input const searchInput = this.findElement('#firmware-search'); if (searchInput) { this.addEventListener(searchInput, 'input', this.handleSearch.bind(this)); } } setupViewModelListeners() { this.subscribeToProperty('firmwareList', this.renderFirmwareList.bind(this)); this.subscribeToProperty('isLoading', this.updateLoadingState.bind(this)); this.subscribeToProperty('searchQuery', this.updateSearchResults.bind(this)); this.subscribeToProperty('registryConnected', this.updateRegistryStatus.bind(this)); } mount() { super.mount(); logger.debug('FirmwareComponent: Mounting...'); // Check registry connection and load firmware list this.checkRegistryConnection(); this.loadFirmwareList(); logger.debug('FirmwareComponent: Mounted successfully'); } async checkRegistryConnection() { try { await window.apiClient.getRegistryHealth(); this.registryConnected = true; this.registryError = null; this.viewModel.set('registryConnected', true); } catch (error) { logger.error('Registry connection failed:', error); this.registryConnected = false; this.registryError = error.message; this.viewModel.set('registryConnected', false); } } async loadFirmwareList() { try { this.viewModel.set('isLoading', true); const firmwareList = await window.apiClient.listFirmwareFromRegistry(); this.viewModel.set('firmwareList', firmwareList); } catch (error) { logger.error('Failed to load firmware list:', error); this.viewModel.set('firmwareList', []); this.showError('Failed to load firmware list: ' + error.message); } finally { this.viewModel.set('isLoading', false); } } async refreshFirmwareList() { await this.checkRegistryConnection(); await this.loadFirmwareList(); } renderFirmwareList() { const container = this.findElement('#firmware-list-container'); if (!container) return; const groupedFirmware = this.viewModel.get('firmwareList') || []; const searchQuery = this.viewModel.get('searchQuery') || ''; // Filter grouped firmware based on search query const filteredGroups = groupedFirmware.map(group => { if (!searchQuery) return group; // Split search query into individual terms const searchTerms = searchQuery.toLowerCase().split(/\s+/).filter(term => term.length > 0); // Filter firmware versions within the group const filteredFirmware = group.firmware.filter(firmware => { // All search terms must match somewhere in the firmware data return searchTerms.every(term => { // Check group name if (group.name.toLowerCase().includes(term)) { return true; } // Check version if (firmware.version.toLowerCase().includes(term)) { return true; } // Check labels if (Object.values(firmware.labels || {}).some(label => label.toLowerCase().includes(term) )) { return true; } return false; }); }); // Return group with filtered firmware, or null if no firmware matches return filteredFirmware.length > 0 ? { ...group, firmware: filteredFirmware } : null; }).filter(group => group !== null); if (filteredGroups.length === 0) { container.innerHTML = `
${searchQuery ? 'No firmware found' : 'No firmware available'}
${searchQuery ? 'Try adjusting your search terms' : 'Upload your first firmware to get started'}
`; return; } const firmwareHTML = filteredGroups.map(group => this.renderFirmwareGroup(group)).join(''); container.innerHTML = `
${firmwareHTML}
`; // Setup event listeners for firmware items this.setupFirmwareItemListeners(); } renderFirmwareGroup(group) { const versionsHTML = group.firmware.map(firmware => this.renderFirmwareVersion(firmware)).join(''); return `

${this.escapeHtml(group.name)}

${group.firmware.length} version${group.firmware.length !== 1 ? 's' : ''}
${versionsHTML}
`; } renderFirmwareVersion(firmware) { const labels = firmware.labels || {}; const labelsHTML = Object.entries(labels).map(([key, value]) => `${key}: ${value}` ).join(''); const sizeKB = Math.round(firmware.size / 1024); return `
v${this.escapeHtml(firmware.version)}
${sizeKB} KB
${labelsHTML}
`; } renderFirmwareItem(firmware) { const labels = firmware.labels || {}; const labelsHTML = Object.entries(labels).map(([key, value]) => `${key}: ${value}` ).join(''); const sizeKB = Math.round(firmware.size / 1024); return `
${this.escapeHtml(firmware.name)}
v${this.escapeHtml(firmware.version)}
${sizeKB} KB
${labelsHTML}
`; } setupFirmwareItemListeners() { // Firmware group header clicks (for expand/collapse) const groupHeaders = this.findAllElements('.firmware-group-header'); groupHeaders.forEach(header => { this.addEventListener(header, 'click', (e) => { const group = header.closest('.firmware-group'); group.classList.toggle('expanded'); }); }); // Version item clicks (for editing) const versionItems = this.findAllElements('.firmware-version-item.clickable'); versionItems.forEach(item => { this.addEventListener(item, 'click', (e) => { // Don't trigger if clicking on action buttons if (e.target.closest('.firmware-version-actions')) { return; } const name = item.getAttribute('data-name'); const version = item.getAttribute('data-version'); this.showEditFirmwareForm(name, version); }); }); // Rollout buttons const rolloutBtns = this.findAllElements('.rollout-btn'); rolloutBtns.forEach(btn => { this.addEventListener(btn, 'click', (e) => { e.stopPropagation(); const name = btn.getAttribute('data-name'); const version = btn.getAttribute('data-version'); const labels = JSON.parse(btn.getAttribute('data-labels') || '{}'); this.showRolloutPanel(name, version, labels); }); }); // Download buttons const downloadBtns = this.findAllElements('.download-btn'); downloadBtns.forEach(btn => { this.addEventListener(btn, 'click', (e) => { e.stopPropagation(); const name = btn.getAttribute('data-name'); const version = btn.getAttribute('data-version'); this.downloadFirmware(name, version); }); }); // Delete buttons const deleteBtns = this.findAllElements('.delete-btn'); deleteBtns.forEach(btn => { this.addEventListener(btn, 'click', (e) => { e.stopPropagation(); const name = btn.getAttribute('data-name'); const version = btn.getAttribute('data-version'); this.showDeleteConfirmation(name, version); }); }); } showAddFirmwareForm() { this.openFirmwareForm('Add Firmware', null, null); } showEditFirmwareForm(name, version) { const groupedFirmware = this.viewModel.get('firmwareList') || []; // Find the firmware in the grouped data let firmware = null; for (const group of groupedFirmware) { if (group.name === name) { firmware = group.firmware.find(f => f.version === version); if (firmware) break; } } if (firmware) { this.openFirmwareForm('Edit Firmware', firmware, null); } } openFirmwareForm(title, firmwareData, onCloseCallback) { this.drawer.openDrawer(title, (contentContainer, setActiveComponent) => { const formComponent = new FirmwareFormComponent(contentContainer, this.viewModel, this.eventBus); setActiveComponent(formComponent); formComponent.setFirmwareData(firmwareData); formComponent.setOnSaveCallback(() => { this.loadFirmwareList(); this.drawer.closeDrawer(); }); formComponent.setOnCancelCallback(() => { this.drawer.closeDrawer(); }); formComponent.mount(); }, null, onCloseCallback, true); // Hide terminal button } async downloadFirmware(name, version) { try { const blob = await window.apiClient.downloadFirmwareFromRegistry(name, version); // Create download link const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${name}-${version}.bin`; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); this.showSuccess(`Firmware ${name} v${version} downloaded successfully`); } catch (error) { logger.error('Download failed:', error); this.showError('Download failed: ' + error.message); } } showDeleteConfirmation(name, version) { this.showConfirmationDialog({ title: 'Delete Firmware', message: `Are you sure you want to delete firmware "${name}" version "${version}"?

This action cannot be undone.`, confirmText: 'Delete', cancelText: 'Cancel', onConfirm: () => this.deleteFirmware(name, version), onCancel: () => {} }); } async showRolloutPanel(name, version, labels) { try { // Get cluster node versions to show which nodes will be affected const nodeVersions = await window.apiClient.getClusterNodeVersions(); // Filter nodes that match the firmware labels const matchingNodes = nodeVersions.nodes.filter(node => { return this.nodeMatchesLabels(node.labels, labels); }); this.openRolloutDrawer(name, version, labels, matchingNodes); } catch (error) { logger.error('Failed to get cluster node versions:', error); this.showError('Failed to get cluster information: ' + error.message); } } nodeMatchesLabels(nodeLabels, firmwareLabels) { for (const [key, value] of Object.entries(firmwareLabels)) { if (!nodeLabels[key] || nodeLabels[key] !== value) { return false; } } return true; } openRolloutDrawer(name, version, labels, matchingNodes) { this.drawer.openDrawer('Rollout Firmware', (contentContainer, setActiveComponent) => { const rolloutComponent = new RolloutComponent(contentContainer, this.viewModel, this.eventBus); setActiveComponent(rolloutComponent); rolloutComponent.setRolloutData(name, version, labels, matchingNodes); rolloutComponent.setOnRolloutCallback((rolloutData) => { this.startRollout(rolloutData); }); rolloutComponent.setOnCancelCallback(() => { this.drawer.closeDrawer(); }); // Store reference for status updates this.currentRolloutComponent = rolloutComponent; rolloutComponent.mount(); }, null, null, true); // Hide terminal button } async startRollout(rolloutData) { try { // Start rollout in the panel (no backdrop) if (this.currentRolloutComponent) { this.currentRolloutComponent.startRollout(); } const response = await window.apiClient.startRollout(rolloutData); logger.info('Rollout started:', response); this.showSuccess(`Rollout started for ${response.totalNodes} nodes`); // Set up WebSocket listener for rollout progress this.setupRolloutProgressListener(response.rolloutId); } catch (error) { logger.error('Rollout failed:', error); this.showError('Rollout failed: ' + error.message); // Reset rollout state on error if (this.currentRolloutComponent) { this.currentRolloutComponent.resetRolloutState(); } } } showRolloutProgress() { // Create backdrop and progress overlay const backdrop = document.createElement('div'); backdrop.className = 'rollout-backdrop'; backdrop.id = 'rollout-backdrop'; const progressOverlay = document.createElement('div'); progressOverlay.className = 'rollout-progress-overlay'; progressOverlay.innerHTML = `

Rolling Out Firmware

Firmware rollout in progress...

Preparing rollout...

`; backdrop.appendChild(progressOverlay); document.body.appendChild(backdrop); // Block UI interactions document.body.style.overflow = 'hidden'; } hideRolloutProgress() { const backdrop = document.getElementById('rollout-backdrop'); if (backdrop) { document.body.removeChild(backdrop); } document.body.style.overflow = ''; } setupRolloutProgressListener(rolloutId) { // Track completed nodes for parallel processing this.completedNodes = new Set(); this.totalNodes = 0; const progressListener = (data) => { if (data.rolloutId === rolloutId) { // Set total nodes from first update if (this.totalNodes === 0) { this.totalNodes = data.total; } this.updateRolloutProgress(data); } }; window.wsClient.on('rolloutProgress', progressListener); // Store listener for cleanup this.currentRolloutListener = progressListener; } updateRolloutProgress(data) { // Update status in the rollout panel if (this.currentRolloutComponent) { this.currentRolloutComponent.updateNodeStatus(data.nodeIp, data.status); } // Track completed nodes for parallel processing if (data.status === 'completed') { this.completedNodes.add(data.nodeIp); } else if (data.status === 'failed') { // Also count failed nodes as "processed" for completion check this.completedNodes.add(data.nodeIp); } // Check if rollout is complete (all nodes processed) if (this.completedNodes.size >= this.totalNodes) { setTimeout(() => { this.showSuccess('Rollout completed successfully'); // Clean up WebSocket listener if (this.currentRolloutListener) { window.wsClient.off('rolloutProgress', this.currentRolloutListener); this.currentRolloutListener = null; } }, 2000); } } async deleteFirmware(name, version) { try { // Note: The registry API doesn't have a delete endpoint in the OpenAPI spec // This would need to be implemented in the registry service this.showError('Delete functionality not yet implemented in registry API'); } catch (error) { logger.error('Delete failed:', error); this.showError('Delete failed: ' + error.message); } } handleSearch(event) { const query = event.target.value; this.viewModel.set('searchQuery', query); } updateSearchResults() { // This method is called when searchQuery property changes // The actual filtering is handled in renderFirmwareList this.renderFirmwareList(); } updateLoadingState() { const isLoading = this.viewModel.get('isLoading'); const container = this.findElement('#firmware-list-container'); if (isLoading && container) { container.innerHTML = `
Loading firmware...
`; } } updateRegistryStatus() { const isConnected = this.viewModel.get('registryConnected'); const statusElement = this.findElement('#registry-status'); if (statusElement) { if (isConnected) { statusElement.innerHTML = ` Registry Connected `; } else { statusElement.innerHTML = ` Registry Disconnected `; } } } showConfirmationDialog(options) { // Create a simple confirmation dialog const overlay = document.createElement('div'); overlay.className = 'overlay-dialog'; overlay.innerHTML = `

${options.title}

${options.message}

`; document.body.appendChild(overlay); const confirmBtn = overlay.querySelector('#confirm-btn'); const cancelBtn = overlay.querySelector('#cancel-btn'); confirmBtn.addEventListener('click', () => { document.body.removeChild(overlay); if (options.onConfirm) options.onConfirm(); }); if (cancelBtn) { cancelBtn.addEventListener('click', () => { document.body.removeChild(overlay); if (options.onCancel) options.onCancel(); }); } // Close on escape key const handleEscape = (e) => { if (e.key === 'Escape') { document.body.removeChild(overlay); document.removeEventListener('keydown', handleEscape); if (options.onCancel) options.onCancel(); } }; document.addEventListener('keydown', handleEscape); } showSuccess(message) { this.showNotification(message, 'success'); } showError(message) { this.showNotification(message, 'error'); } showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.classList.add('show'); }, 100); setTimeout(() => { notification.classList.remove('show'); setTimeout(() => { if (notification.parentNode) { document.body.removeChild(notification); } }, 300); }, 3000); } escapeHtml(text) { if (typeof text !== 'string') return text; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } window.FirmwareComponent = FirmwareComponent;