diff --git a/public/index.html b/public/index.html
index faa8101..a46fcf2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -263,6 +263,7 @@
+
diff --git a/public/scripts/components/ClusterViewComponent.js b/public/scripts/components/ClusterViewComponent.js
index 7f200b2..c581946 100644
--- a/public/scripts/components/ClusterViewComponent.js
+++ b/public/scripts/components/ClusterViewComponent.js
@@ -33,6 +33,9 @@ class ClusterViewComponent extends Component {
// Track if we've already loaded data to prevent unnecessary reloads
this.dataLoaded = false;
+
+ // Initialize overlay dialog
+ this.overlayDialog = null;
}
mount() {
@@ -50,6 +53,9 @@ class ClusterViewComponent extends Component {
// Set up deploy button event listener
this.setupDeployButton();
+ // Initialize overlay dialog
+ this.initializeOverlayDialog();
+
// 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
@@ -106,6 +112,32 @@ class ClusterViewComponent extends Component {
}
}
+ initializeOverlayDialog() {
+ // Create overlay container if it doesn't exist
+ let overlayContainer = document.getElementById('cluster-overlay-dialog');
+ if (!overlayContainer) {
+ overlayContainer = document.createElement('div');
+ overlayContainer.id = 'cluster-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);
+ }
+
async handleDeploy() {
logger.debug('ClusterViewComponent: Deploy button clicked, opening firmware upload drawer...');
@@ -113,7 +145,14 @@ class ClusterViewComponent extends 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.');
+ this.showConfirmationDialog({
+ title: 'No Nodes Available',
+ message: 'No nodes available for firmware deployment. Please ensure cluster members are loaded and visible.',
+ confirmText: 'OK',
+ cancelText: null,
+ onConfirm: () => {},
+ onCancel: null
+ });
return;
}
diff --git a/public/scripts/components/FirmwareComponent.js b/public/scripts/components/FirmwareComponent.js
index 83c385f..535578c 100644
--- a/public/scripts/components/FirmwareComponent.js
+++ b/public/scripts/components/FirmwareComponent.js
@@ -7,6 +7,9 @@ class FirmwareComponent extends Component {
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');
@@ -105,6 +108,9 @@ class FirmwareComponent extends Component {
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);
@@ -131,6 +137,24 @@ class FirmwareComponent extends Component {
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);
@@ -160,15 +184,69 @@ class FirmwareComponent extends Component {
const specificNode = this.viewModel.get('specificNode');
if (!file) {
- alert('Please select a firmware file first.');
+ 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) {
- alert('Please select a specific node to update.');
+ 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();
@@ -185,7 +263,14 @@ class FirmwareComponent extends Component {
} catch (error) {
logger.error('Firmware deployment failed:', error);
- alert(`Deployment failed: ${error.message}`);
+ this.showConfirmationDialog({
+ title: 'Deployment Failed',
+ message: `Deployment failed: ${error.message}`,
+ confirmText: 'OK',
+ cancelText: null,
+ onConfirm: () => {},
+ onCancel: null
+ });
} finally {
this.viewModel.completeUpload();
}
@@ -198,13 +283,17 @@ class FirmwareComponent extends Component {
const nodes = response.members || [];
if (nodes.length === 0) {
- alert('No nodes available for firmware update.');
+ this.showConfirmationDialog({
+ title: 'No Nodes Available',
+ message: 'No nodes available for firmware update.',
+ confirmText: 'OK',
+ cancelText: null,
+ onConfirm: () => {},
+ onCancel: null
+ });
return;
}
- const confirmed = confirm(`Upload firmware to all ${nodes.length} nodes? This will update: ${nodes.map(n => n.hostname || n.ip).join(', ')}`);
- if (!confirmed) return;
-
// Show upload progress area
this.showUploadProgress(file, nodes);
@@ -222,9 +311,6 @@ class FirmwareComponent extends Component {
async uploadToSpecificNode(file, nodeIp) {
try {
- const confirmed = confirm(`Upload firmware to node ${nodeIp}?`);
- if (!confirmed) return;
-
// Show upload progress area
this.showUploadProgress(file, [{ ip: nodeIp, hostname: nodeIp }]);
@@ -266,12 +352,16 @@ class FirmwareComponent extends Component {
try {
const nodes = this.viewModel.getAffectedNodesByLabels();
if (!nodes || nodes.length === 0) {
- alert('No nodes match the selected labels.');
+ this.showConfirmationDialog({
+ title: 'No Matching Nodes',
+ message: 'No nodes match the selected labels.',
+ confirmText: 'OK',
+ cancelText: null,
+ onConfirm: () => {},
+ onCancel: null
+ });
return;
}
- const labels = this.viewModel.get('selectedLabels') || [];
- const confirmed = confirm(`Upload firmware to ${nodes.length} node(s) matching labels (${labels.join(', ')})?`);
- if (!confirmed) return;
// Show upload progress area
this.showUploadProgress(file, nodes);
diff --git a/public/scripts/components/FirmwareUploadComponent.js b/public/scripts/components/FirmwareUploadComponent.js
index 8aed430..4799ba2 100644
--- a/public/scripts/components/FirmwareUploadComponent.js
+++ b/public/scripts/components/FirmwareUploadComponent.js
@@ -6,6 +6,9 @@ class FirmwareUploadComponent extends Component {
logger.debug('FirmwareUploadComponent: Constructor called');
logger.debug('FirmwareUploadComponent: Container:', container);
logger.debug('FirmwareUploadComponent: Container ID:', container?.id);
+
+ // Initialize overlay dialog
+ this.overlayDialog = null;
}
setupEventListeners() {
@@ -37,6 +40,9 @@ class FirmwareUploadComponent extends Component {
logger.debug('FirmwareUploadComponent: Mounting...');
+ // Initialize overlay dialog
+ this.initializeOverlayDialog();
+
// Initialize UI state
this.updateFileInfo();
this.updateDeployButton();
@@ -49,6 +55,32 @@ class FirmwareUploadComponent extends Component {
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);
@@ -59,28 +91,54 @@ class FirmwareUploadComponent extends Component {
const targetNodes = this.viewModel.get('targetNodes');
if (!file) {
- alert('Please select a firmware file first.');
+ 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) {
- alert('No target nodes available for firmware update.');
+ 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();
- const confirmed = confirm(`Upload firmware to ${targetNodes.length} node(s)?\n\nTarget nodes:\n${targetNodes.map(n => `• ${n.hostname || n.ip} (${n.ip})`).join('\n')}\n\nThis will update the firmware on all selected nodes.`);
- if (!confirmed) {
- this.viewModel.completeUpload();
- this.hideProgressOverlay();
- return;
- }
-
// Show upload progress area
this.showUploadProgress(file, targetNodes);
@@ -95,7 +153,14 @@ class FirmwareUploadComponent extends Component {
} catch (error) {
logger.error('Firmware deployment failed:', error);
- alert(`Deployment failed: ${error.message}`);
+ this.showConfirmationDialog({
+ title: 'Deployment Failed',
+ message: `Deployment failed: ${error.message}`,
+ confirmText: 'OK',
+ cancelText: null,
+ onConfirm: () => {},
+ onCancel: null
+ });
} finally {
this.viewModel.completeUpload();
this.hideProgressOverlay();
diff --git a/public/scripts/components/OverlayDialogComponent.js b/public/scripts/components/OverlayDialogComponent.js
new file mode 100644
index 0000000..50a6f71
--- /dev/null
+++ b/public/scripts/components/OverlayDialogComponent.js
@@ -0,0 +1,126 @@
+// Overlay Dialog Component - Reusable confirmation dialog
+class OverlayDialogComponent extends Component {
+ constructor(container, viewModel, eventBus) {
+ super(container, viewModel, eventBus);
+ this.isVisible = false;
+ this.onConfirm = null;
+ this.onCancel = null;
+ this.title = '';
+ this.message = '';
+ this.confirmText = 'Yes';
+ this.cancelText = 'No';
+ }
+
+ mount() {
+ super.mount();
+ this.setupEventListeners();
+ }
+
+ setupEventListeners() {
+ // Close overlay when clicking outside or pressing escape
+ this.addEventListener(this.container, 'click', (e) => {
+ if (!this.isVisible) return;
+ if (e.target === this.container) {
+ this.hide();
+ }
+ });
+
+ this.addEventListener(document, 'keydown', (e) => {
+ if (e.key === 'Escape' && this.isVisible) {
+ this.hide();
+ }
+ });
+ }
+
+ show(options = {}) {
+ const {
+ title = 'Confirm Action',
+ message = 'Are you sure you want to proceed?',
+ confirmText = 'Yes',
+ cancelText = 'No',
+ onConfirm = null,
+ onCancel = null
+ } = options;
+
+ this.title = title;
+ this.message = message;
+ this.confirmText = confirmText;
+ this.cancelText = cancelText;
+ this.onConfirm = onConfirm;
+ this.onCancel = onCancel;
+
+ this.render();
+ this.container.classList.add('visible');
+ this.isVisible = true;
+ }
+
+ hide() {
+ this.container.classList.remove('visible');
+ this.isVisible = false;
+
+ // Call cancel callback if provided
+ if (this.onCancel) {
+ this.onCancel();
+ }
+ }
+
+ handleConfirm() {
+ this.hide();
+
+ // Call confirm callback if provided
+ if (this.onConfirm) {
+ this.onConfirm();
+ }
+ }
+
+ render() {
+ this.container.innerHTML = `
+