// Reusable Firmware Upload Component
class FirmwareUploadComponent extends Component {
constructor(container, viewModel, eventBus) {
super(container, viewModel, eventBus);
logger.debug('FirmwareUploadComponent: Constructor called');
logger.debug('FirmwareUploadComponent: Container:', container);
logger.debug('FirmwareUploadComponent: Container ID:', container?.id);
// Initialize overlay dialog
this.overlayDialog = null;
}
setupEventListeners() {
// Setup firmware file input
const firmwareFile = this.findElement('#firmware-file');
if (firmwareFile) {
this.addEventListener(firmwareFile, 'change', this.handleFileSelect.bind(this));
}
// 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('isUploading', this.updateUploadState.bind(this));
this.subscribeToProperty('uploadProgress', this.updateUploadProgress.bind(this));
this.subscribeToProperty('uploadResults', this.updateUploadResults.bind(this));
}
mount() {
super.mount();
logger.debug('FirmwareUploadComponent: Mounting...');
// Initialize overlay dialog
this.initializeOverlayDialog();
// Initialize UI state
this.updateFileInfo();
this.updateDeployButton();
logger.debug('FirmwareUploadComponent: 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-upload-overlay-dialog');
if (!overlayContainer) {
overlayContainer = document.createElement('div');
overlayContainer.id = 'firmware-upload-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();
}
}
showConfirmationDialog(options) {
if (!this.overlayDialog) {
this.initializeOverlayDialog();
}
this.overlayDialog.show(options);
}
handleFileSelect(event) {
const file = event.target.files[0];
this.viewModel.setSelectedFile(file);
}
async handleDeploy() {
const file = this.viewModel.get('selectedFile');
const targetNodes = this.viewModel.get('targetNodes');
if (!file) {
this.showConfirmationDialog({
title: 'No File Selected',
message: 'Please select a firmware file first.',
confirmText: 'OK',
cancelText: null,
onConfirm: () => {},
onCancel: null
});
return;
}
if (!targetNodes || targetNodes.length === 0) {
this.showConfirmationDialog({
title: 'No Target Nodes',
message: 'No target nodes available for firmware update.',
confirmText: 'OK',
cancelText: null,
onConfirm: () => {},
onCancel: null
});
return;
}
// Show confirmation dialog for deployment
this.showDeploymentConfirmation(file, targetNodes);
}
showDeploymentConfirmation(file, targetNodes) {
const title = 'Deploy Firmware';
const message = `Upload firmware "${file.name}" to ${targetNodes.length} node(s)?
Target nodes:
${targetNodes.map(n => `• ${n.hostname || n.ip} (${n.ip})`).join('
')}
This will update the firmware on all selected nodes.`;
this.showConfirmationDialog({
title: title,
message: message,
confirmText: 'Deploy',
cancelText: 'Cancel',
onConfirm: () => this.performDeployment(file, targetNodes),
onCancel: () => {}
});
}
async performDeployment(file, targetNodes) {
try {
this.viewModel.startUpload();
// Show progress overlay to block UI interactions
this.showProgressOverlay();
// Show upload progress area
this.showUploadProgress(file, targetNodes);
// Start batch upload
const results = await this.performBatchUpload(file, targetNodes);
// Display results
this.displayUploadResults(results);
// 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();
this.hideProgressOverlay();
}
}
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) {
// Update the target nodes section header to show upload progress
const targetNodesSection = this.findElement('.target-nodes-section');
if (targetNodesSection) {
const h3 = targetNodesSection.querySelector('h3');
if (h3) {
h3.innerHTML = `
Firmware Upload Progress (${nodes.length} nodes)
`;
}
}
// Add progress info to the firmware-progress-container
const container = this.findElement('#firmware-progress-container');
if (container) {
const progressHTML = `