// Firmware Component
class FirmwareComponent extends Component {
constructor(container, viewModel, eventBus) {
super(container, viewModel, eventBus);
logger.debug('FirmwareComponent: Constructor called');
logger.debug('FirmwareComponent: Container:', container);
logger.debug('FirmwareComponent: Container ID:', container?.id);
// Initialize overlay dialog
this.overlayDialog = null;
// Check if the dropdown exists in the container
if (container) {
const dropdown = container.querySelector('#specific-node-select');
logger.debug('FirmwareComponent: Dropdown found in constructor:', !!dropdown);
if (dropdown) {
logger.debug('FirmwareComponent: Dropdown tagName:', dropdown.tagName);
logger.debug('FirmwareComponent: Dropdown id:', dropdown.id);
}
}
}
setupEventListeners() {
// Setup global firmware file input
const globalFirmwareFile = this.findElement('#global-firmware-file');
if (globalFirmwareFile) {
this.addEventListener(globalFirmwareFile, 'change', this.handleFileSelect.bind(this));
}
// Setup target selection
const targetRadios = this.findAllElements('input[name="target-type"]');
targetRadios.forEach(radio => {
this.addEventListener(radio, 'change', this.handleTargetChange.bind(this));
});
// Setup specific node select change handler
const specificNodeSelect = this.findElement('#specific-node-select');
logger.debug('FirmwareComponent: setupEventListeners - specificNodeSelect found:', !!specificNodeSelect);
if (specificNodeSelect) {
logger.debug('FirmwareComponent: specificNodeSelect element:', specificNodeSelect);
logger.debug('FirmwareComponent: specificNodeSelect tagName:', specificNodeSelect.tagName);
logger.debug('FirmwareComponent: specificNodeSelect id:', specificNodeSelect.id);
// Store the bound handler as an instance property
this._boundNodeSelectHandler = this.handleNodeSelect.bind(this);
this.addEventListener(specificNodeSelect, 'change', this._boundNodeSelectHandler);
logger.debug('FirmwareComponent: Event listener added to specificNodeSelect');
} else {
logger.warn('FirmwareComponent: specificNodeSelect element not found during setupEventListeners');
}
// Setup label select change handler (single-select add-to-chips)
const labelSelect = this.findElement('#label-select');
if (labelSelect) {
this._boundLabelSelectHandler = (e) => {
const value = e.target.value;
if (!value) return;
const current = this.viewModel.get('selectedLabels') || [];
if (!current.includes(value)) {
this.viewModel.setSelectedLabels([...current, value]);
}
// Reset select back to placeholder
e.target.value = '';
this.renderSelectedLabelChips();
this.updateAffectedNodesPreview();
this.updateDeployButton();
};
this.addEventListener(labelSelect, 'change', this._boundLabelSelectHandler);
}
// Setup deploy button
const deployBtn = this.findElement('#deploy-btn');
if (deployBtn) {
this.addEventListener(deployBtn, 'click', this.handleDeploy.bind(this));
}
}
setupViewModelListeners() {
this.subscribeToProperty('selectedFile', () => {
this.updateFileInfo();
this.updateDeployButton();
});
this.subscribeToProperty('targetType', () => {
this.updateTargetVisibility();
this.updateDeployButton();
this.updateAffectedNodesPreview();
});
this.subscribeToProperty('specificNode', this.updateDeployButton.bind(this));
this.subscribeToProperty('availableNodes', () => {
this.populateNodeSelect();
this.populateLabelSelect();
this.updateDeployButton();
this.updateAffectedNodesPreview();
});
this.subscribeToProperty('uploadProgress', this.updateUploadProgress.bind(this));
this.subscribeToProperty('uploadResults', this.updateUploadResults.bind(this));
this.subscribeToProperty('isUploading', this.updateUploadState.bind(this));
this.subscribeToProperty('selectedLabels', () => {
this.populateLabelSelect();
this.updateAffectedNodesPreview();
this.updateDeployButton();
});
}
mount() {
super.mount();
logger.debug('FirmwareComponent: Mounting...');
// Initialize overlay dialog
this.initializeOverlayDialog();
// Check if the dropdown exists when mounted
const dropdown = this.findElement('#specific-node-select');
logger.debug('FirmwareComponent: Mount - dropdown found:', !!dropdown);
if (dropdown) {
logger.debug('FirmwareComponent: Mount - dropdown tagName:', dropdown.tagName);
logger.debug('FirmwareComponent: Mount - dropdown id:', dropdown.id);
logger.debug('FirmwareComponent: Mount - dropdown innerHTML:', dropdown.innerHTML);
}
// Initialize target visibility and label list on first mount
try {
this.updateTargetVisibility();
this.populateLabelSelect();
this.updateAffectedNodesPreview();
} catch (e) {
logger.warn('FirmwareComponent: Initialization after mount failed:', e);
}
logger.debug('FirmwareComponent: Mounted successfully');
}
render() {
// Initial render is handled by the HTML template
this.updateDeployButton();
}
initializeOverlayDialog() {
// Create overlay container if it doesn't exist
let overlayContainer = document.getElementById('firmware-overlay-dialog');
if (!overlayContainer) {
overlayContainer = document.createElement('div');
overlayContainer.id = 'firmware-overlay-dialog';
overlayContainer.className = 'overlay-dialog';
document.body.appendChild(overlayContainer);
}
// Create and initialize the overlay dialog component
if (!this.overlayDialog) {
const overlayVM = new ViewModel();
this.overlayDialog = new OverlayDialogComponent(overlayContainer, overlayVM, this.eventBus);
this.overlayDialog.mount();
}
}
handleFileSelect(event) {
const file = event.target.files[0];
this.viewModel.setSelectedFile(file);
}
handleTargetChange(event) {
const targetType = event.target.value;
this.viewModel.setTargetType(targetType);
}
handleNodeSelect(event) {
const nodeIp = event.target.value;
logger.debug('FirmwareComponent: handleNodeSelect called with nodeIp:', nodeIp);
logger.debug('Event:', event);
logger.debug('Event target:', event.target);
logger.debug('Event target value:', event.target.value);
this.viewModel.setSpecificNode(nodeIp);
// Also update the deploy button state
this.updateDeployButton();
}
async handleDeploy() {
const file = this.viewModel.get('selectedFile');
const targetType = this.viewModel.get('targetType');
const specificNode = this.viewModel.get('specificNode');
if (!file) {
this.showConfirmationDialog({
title: 'No File Selected',
message: 'Please select a firmware file first.',
confirmText: 'OK',
cancelText: null,
onConfirm: () => {},
onCancel: null
});
return;
}
if (targetType === 'specific' && !specificNode) {
this.showConfirmationDialog({
title: 'No Node Selected',
message: 'Please select a specific node to update.',
confirmText: 'OK',
cancelText: null,
onConfirm: () => {},
onCancel: null
});
return;
}
// Show confirmation dialog for deployment
this.showDeploymentConfirmation(file, targetType, specificNode);
}
showConfirmationDialog(options) {
if (!this.overlayDialog) {
this.initializeOverlayDialog();
}
this.overlayDialog.show(options);
}
showDeploymentConfirmation(file, targetType, specificNode) {
let title, message;
if (targetType === 'all') {
const nodes = this.viewModel.get('availableNodes') || [];
title = 'Deploy to All Nodes';
message = `Upload firmware "${file.name}" to all ${nodes.length} nodes?
This will update:
${nodes.map(n => `• ${n.hostname || n.ip}`).join('
')}`;
} else if (targetType === 'specific') {
title = 'Deploy to Specific Node';
message = `Upload firmware "${file.name}" to node ${specificNode}?`;
} else if (targetType === 'labels') {
const nodes = this.viewModel.getAffectedNodesByLabels();
const labels = this.viewModel.get('selectedLabels') || [];
title = 'Deploy to Labeled Nodes';
message = `Upload firmware "${file.name}" to ${nodes.length} node(s) matching labels (${labels.join(', ')})?
This will update:
${nodes.map(n => `• ${n.hostname || n.ip}`).join('
')}`;
}
this.showConfirmationDialog({
title: title,
message: message,
confirmText: 'Deploy',
cancelText: 'Cancel',
onConfirm: () => this.performDeployment(file, targetType, specificNode),
onCancel: () => {}
});
}
async performDeployment(file, targetType, specificNode) {
try {
this.viewModel.startUpload();
if (targetType === 'all') {
await this.uploadToAllNodes(file);
} else if (targetType === 'specific') {
await this.uploadToSpecificNode(file, specificNode);
} else if (targetType === 'labels') {
await this.uploadToLabelFilteredNodes(file);
}
// Reset interface after successful upload
this.viewModel.resetUploadState();
} catch (error) {
logger.error('Firmware deployment failed:', error);
this.showConfirmationDialog({
title: 'Deployment Failed',
message: `Deployment failed: ${error.message}`,
confirmText: 'OK',
cancelText: null,
onConfirm: () => {},
onCancel: null
});
} finally {
this.viewModel.completeUpload();
}
}
async uploadToAllNodes(file) {
try {
// Get current cluster members
const response = await window.apiClient.getClusterMembers();
const nodes = response.members || [];
if (nodes.length === 0) {
this.showConfirmationDialog({
title: 'No Nodes Available',
message: 'No nodes available for firmware update.',
confirmText: 'OK',
cancelText: null,
onConfirm: () => {},
onCancel: null
});
return;
}
// Show upload progress area
this.showUploadProgress(file, nodes);
// Start batch upload
const results = await this.performBatchUpload(file, nodes);
// Display results
this.displayUploadResults(results);
} catch (error) {
logger.error('Failed to upload firmware to all nodes:', error);
throw error;
}
}
async uploadToSpecificNode(file, nodeIp) {
try {
// Show upload progress area
this.showUploadProgress(file, [{ ip: nodeIp, hostname: nodeIp }]);
// Update progress to show starting
this.updateNodeProgress(1, 1, nodeIp, 'Uploading...');
// Perform single node upload
const result = await this.performSingleUpload(file, nodeIp);
// Update progress to show completion
this.updateNodeProgress(1, 1, nodeIp, 'Completed');
this.updateOverallProgress(1, 1);
// Display results
this.displayUploadResults([result]);
} catch (error) {
logger.error(`Failed to upload firmware to node ${nodeIp}:`, error);
// Update progress to show failure
this.updateNodeProgress(1, 1, nodeIp, 'Failed');
this.updateOverallProgress(0, 1);
// Display error results
const errorResult = {
nodeIp: nodeIp,
hostname: nodeIp,
success: false,
error: error.message,
timestamp: new Date().toISOString()
};
this.displayUploadResults([errorResult]);
throw error;
}
}
async uploadToLabelFilteredNodes(file) {
try {
const nodes = this.viewModel.getAffectedNodesByLabels();
if (!nodes || nodes.length === 0) {
this.showConfirmationDialog({
title: 'No Matching Nodes',
message: 'No nodes match the selected labels.',
confirmText: 'OK',
cancelText: null,
onConfirm: () => {},
onCancel: null
});
return;
}
// Show upload progress area
this.showUploadProgress(file, nodes);
// Start batch upload
const results = await this.performBatchUpload(file, nodes);
// Display results
this.displayUploadResults(results);
} catch (error) {
logger.error('Failed to upload firmware to label-filtered nodes:', error);
throw error;
}
}
async performBatchUpload(file, nodes) {
const results = [];
const totalNodes = nodes.length;
let successfulUploads = 0;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const nodeIp = node.ip;
try {
// Update progress
this.updateNodeProgress(i + 1, totalNodes, nodeIp, 'Uploading...');
// Upload to this node
const result = await this.performSingleUpload(file, nodeIp);
results.push(result);
successfulUploads++;
// Update progress
this.updateNodeProgress(i + 1, totalNodes, nodeIp, 'Completed');
this.updateOverallProgress(successfulUploads, totalNodes);
} catch (error) {
logger.error(`Failed to upload to node ${nodeIp}:`, error);
const errorResult = {
nodeIp: nodeIp,
hostname: node.hostname || nodeIp,
success: false,
error: error.message,
timestamp: new Date().toISOString()
};
results.push(errorResult);
// Update progress
this.updateNodeProgress(i + 1, totalNodes, nodeIp, 'Failed');
this.updateOverallProgress(successfulUploads, totalNodes);
}
// Small delay between uploads
if (i < nodes.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
}
async performSingleUpload(file, nodeIp) {
try {
const result = await window.apiClient.uploadFirmware(file, nodeIp);
return {
nodeIp: nodeIp,
hostname: nodeIp,
success: true,
result: result,
timestamp: new Date().toISOString()
};
} catch (error) {
throw new Error(`Upload to ${nodeIp} failed: ${error.message}`);
}
}
showUploadProgress(file, nodes) {
const container = this.findElement('#firmware-nodes-list');
const progressHTML = `