Compare commits

..

1 Commits

Author SHA1 Message Date
0b3f0cbca4 feat: rollout 2025-10-22 19:58:09 +02:00
5 changed files with 101 additions and 215 deletions

View File

@@ -182,12 +182,6 @@ class ApiClient {
return response.blob();
}
async deleteFirmwareFromRegistry(name, version) {
return this.request(`/api/registry/firmware/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, {
method: 'DELETE'
});
}
// Rollout API methods
async getClusterNodeVersions() {
return this.request('/api/cluster/node/versions', { method: 'GET' });

View File

@@ -53,11 +53,6 @@ class FirmwareComponent extends Component {
logger.debug('FirmwareComponent: Mounted successfully');
}
unmount() {
this.cleanupDynamicListeners();
super.unmount();
}
async checkRegistryConnection() {
try {
await window.apiClient.getRegistryHealth();
@@ -283,25 +278,19 @@ class FirmwareComponent extends Component {
}
setupFirmwareItemListeners() {
// First, clean up existing listeners for dynamically created content
this.cleanupDynamicListeners();
this.dynamicUnsubscribers = [];
// Firmware group header clicks (for expand/collapse)
const groupHeaders = this.findAllElements('.firmware-group-header');
groupHeaders.forEach(header => {
const handler = (e) => {
this.addEventListener(header, 'click', (e) => {
const group = header.closest('.firmware-group');
group.classList.toggle('expanded');
};
header.addEventListener('click', handler);
this.dynamicUnsubscribers.push(() => header.removeEventListener('click', handler));
});
});
// Version item clicks (for editing)
const versionItems = this.findAllElements('.firmware-version-item.clickable');
versionItems.forEach(item => {
const handler = (e) => {
this.addEventListener(item, 'click', (e) => {
// Don't trigger if clicking on action buttons
if (e.target.closest('.firmware-version-actions')) {
return;
@@ -310,61 +299,44 @@ class FirmwareComponent extends Component {
const name = item.getAttribute('data-name');
const version = item.getAttribute('data-version');
this.showEditFirmwareForm(name, version);
};
item.addEventListener('click', handler);
this.dynamicUnsubscribers.push(() => item.removeEventListener('click', handler));
});
});
// Rollout buttons
const rolloutBtns = this.findAllElements('.rollout-btn');
rolloutBtns.forEach(btn => {
const handler = (e) => {
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);
};
btn.addEventListener('click', handler);
this.dynamicUnsubscribers.push(() => btn.removeEventListener('click', handler));
});
});
// Download buttons
const downloadBtns = this.findAllElements('.download-btn');
downloadBtns.forEach(btn => {
const handler = (e) => {
this.addEventListener(btn, 'click', (e) => {
e.stopPropagation();
const name = btn.getAttribute('data-name');
const version = btn.getAttribute('data-version');
this.downloadFirmware(name, version);
};
btn.addEventListener('click', handler);
this.dynamicUnsubscribers.push(() => btn.removeEventListener('click', handler));
});
});
// Delete buttons
const deleteBtns = this.findAllElements('.delete-btn');
logger.debug('Found delete buttons:', deleteBtns.length);
deleteBtns.forEach(btn => {
const handler = (e) => {
this.addEventListener(btn, 'click', (e) => {
e.stopPropagation();
const name = btn.getAttribute('data-name');
const version = btn.getAttribute('data-version');
logger.debug('Delete button clicked:', name, version);
this.showDeleteConfirmation(name, version);
};
btn.addEventListener('click', handler);
this.dynamicUnsubscribers.push(() => btn.removeEventListener('click', handler));
});
});
}
cleanupDynamicListeners() {
if (this.dynamicUnsubscribers) {
this.dynamicUnsubscribers.forEach(unsub => unsub());
this.dynamicUnsubscribers = [];
}
}
showAddFirmwareForm() {
this.openFirmwareForm('Add Firmware', null, null);
}
@@ -426,12 +398,13 @@ class FirmwareComponent extends Component {
}
showDeleteConfirmation(name, version) {
OverlayDialogComponent.danger({
this.showConfirmationDialog({
title: 'Delete Firmware',
message: `Are you sure you want to delete firmware "${name}" version "${version}"?<br><br>This action cannot be undone.`,
confirmText: 'Delete',
cancelText: 'Cancel',
onConfirm: () => this.deleteFirmware(name, version)
onConfirm: () => this.deleteFirmware(name, version),
onCancel: () => {}
});
}
@@ -601,9 +574,9 @@ class FirmwareComponent extends Component {
async deleteFirmware(name, version) {
try {
await window.apiClient.deleteFirmwareFromRegistry(name, version);
this.showSuccess(`Firmware ${name} v${version} deleted successfully`);
await this.loadFirmwareList();
// 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);
@@ -665,6 +638,52 @@ class FirmwareComponent extends Component {
}
}
showConfirmationDialog(options) {
// Create a simple confirmation dialog
const overlay = document.createElement('div');
overlay.className = 'overlay-dialog';
overlay.innerHTML = `
<div class="dialog">
<div class="dialog-header">
<h3>${options.title}</h3>
</div>
<div class="dialog-body">
<p>${options.message}</p>
</div>
<div class="dialog-footer">
${options.cancelText ? `<button class="btn btn-secondary" id="cancel-btn">${options.cancelText}</button>` : ''}
<button class="btn btn-primary" id="confirm-btn">${options.confirmText}</button>
</div>
</div>
`;
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');

View File

@@ -9,8 +9,6 @@ class OverlayDialogComponent extends Component {
this.message = '';
this.confirmText = 'Yes';
this.cancelText = 'No';
this.confirmClass = 'overlay-dialog-btn-confirm';
this.showCloseButton = true;
}
mount() {
@@ -40,8 +38,6 @@ class OverlayDialogComponent extends Component {
message = 'Are you sure you want to proceed?',
confirmText = 'Yes',
cancelText = 'No',
confirmClass = 'overlay-dialog-btn-confirm',
showCloseButton = true,
onConfirm = null,
onCancel = null
} = options;
@@ -50,74 +46,53 @@ class OverlayDialogComponent extends Component {
this.message = message;
this.confirmText = confirmText;
this.cancelText = cancelText;
this.confirmClass = confirmClass;
this.showCloseButton = showCloseButton;
this.onConfirm = onConfirm;
this.onCancel = onCancel;
this.render();
// Add visible class with small delay for animation
setTimeout(() => {
this.container.classList.add('visible');
}, 10);
this.container.classList.add('visible');
this.isVisible = true;
}
hide() {
this.container.classList.remove('visible');
this.isVisible = false;
setTimeout(() => {
this.isVisible = false;
// Call cancel callback if provided
if (this.onCancel) {
this.onCancel();
this.onCancel = null;
}
}, 300);
// Call cancel callback if provided
if (this.onCancel) {
this.onCancel();
}
}
handleConfirm() {
this.container.classList.remove('visible');
this.hide();
setTimeout(() => {
this.isVisible = false;
// Call confirm callback if provided
if (this.onConfirm) {
this.onConfirm();
this.onConfirm = null;
}
}, 300);
// Call confirm callback if provided
if (this.onConfirm) {
this.onConfirm();
}
}
render() {
this.container.innerHTML = `
<div class="overlay-dialog-content">
<div class="overlay-dialog-header">
<h3 class="overlay-dialog-title">${this.escapeHtml(this.title)}</h3>
${this.showCloseButton ? `
<button class="overlay-dialog-close" type="button" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
` : ''}
<h3 class="overlay-dialog-title">${this.title}</h3>
<button class="overlay-dialog-close" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
</div>
<div class="overlay-dialog-body">
<p class="overlay-dialog-message">${this.message}</p>
<div class="overlay-dialog-message">${this.message}</div>
</div>
<div class="overlay-dialog-footer">
${this.cancelText ? `
<button class="overlay-dialog-btn overlay-dialog-btn-cancel" type="button">
${this.escapeHtml(this.cancelText)}
</button>
` : ''}
<button class="overlay-dialog-btn ${this.confirmClass}" type="button">
${this.escapeHtml(this.confirmText)}
<button class="overlay-dialog-btn overlay-dialog-btn-cancel" type="button">
${this.cancelText}
</button>
<button class="overlay-dialog-btn overlay-dialog-btn-confirm" type="button">
${this.confirmText}
</button>
</div>
</div>
@@ -126,7 +101,7 @@ class OverlayDialogComponent extends Component {
// Add event listeners to buttons
const closeBtn = this.container.querySelector('.overlay-dialog-close');
const cancelBtn = this.container.querySelector('.overlay-dialog-btn-cancel');
const confirmBtn = this.container.querySelector(`.${this.confirmClass}`);
const confirmBtn = this.container.querySelector('.overlay-dialog-btn-confirm');
if (closeBtn) {
this.addEventListener(closeBtn, 'click', () => this.hide());
@@ -141,13 +116,6 @@ class OverlayDialogComponent extends Component {
}
}
escapeHtml(text) {
if (typeof text !== 'string') return text;
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
unmount() {
// Clean up event listeners
this.removeAllEventListeners();
@@ -156,68 +124,3 @@ class OverlayDialogComponent extends Component {
super.unmount();
}
}
// Static utility methods for easy usage without mounting
OverlayDialogComponent.show = function(options) {
// Create a temporary container
const container = document.createElement('div');
container.className = 'overlay-dialog';
document.body.appendChild(container);
// Create component instance
const dialog = new OverlayDialogComponent(container, null, null);
// Override hide to clean up container
const originalHide = dialog.hide.bind(dialog);
dialog.hide = function() {
originalHide();
setTimeout(() => {
if (container.parentNode) {
document.body.removeChild(container);
}
}, 350);
};
// Override handleConfirm to clean up container
const originalHandleConfirm = dialog.handleConfirm.bind(dialog);
dialog.handleConfirm = function() {
originalHandleConfirm();
setTimeout(() => {
if (container.parentNode) {
document.body.removeChild(container);
}
}, 350);
};
dialog.mount();
dialog.show(options);
return dialog;
};
// Convenience method for confirmation dialogs
OverlayDialogComponent.confirm = function(options) {
return OverlayDialogComponent.show({
...options,
confirmClass: options.confirmClass || 'overlay-dialog-btn-confirm'
});
};
// Convenience method for danger/delete confirmations
OverlayDialogComponent.danger = function(options) {
return OverlayDialogComponent.show({
...options,
confirmClass: 'overlay-dialog-btn-danger'
});
};
// Convenience method for alerts
OverlayDialogComponent.alert = function(message, title = 'Notice') {
return OverlayDialogComponent.show({
title,
message,
confirmText: 'OK',
cancelText: null,
showCloseButton: false
});
};

View File

@@ -228,30 +228,17 @@ class RolloutComponent extends Component {
return;
}
const nodeCount = this.matchingNodes.length;
const nodePlural = nodeCount !== 1 ? 's' : '';
const { name, version } = this.rolloutData;
// 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
};
// Show confirmation dialog
OverlayDialogComponent.confirm({
title: 'Confirm Firmware Rollout',
message: `Are you sure you want to deploy firmware <strong>${this.escapeHtml(name)}</strong> version <strong>${this.escapeHtml(version)}</strong> to <strong>${nodeCount} node${nodePlural}</strong>?<br><br>The rollout process cannot be cancelled once started. All nodes will be updated and rebooted.`,
confirmText: `Rollout to ${nodeCount} Node${nodePlural}`,
cancelText: 'Cancel',
onConfirm: () => {
// 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);
}
});
this.onRolloutCallback(rolloutData);
}
handleCancel() {

View File

@@ -79,6 +79,7 @@ p {
background: var(--bg-secondary);
border-radius: 16px;
backdrop-filter: var(--backdrop-blur);
box-shadow: var(--shadow-primary);
border: 1px solid var(--border-primary);
padding: 0.75rem;
margin-bottom: 1rem;
@@ -2139,6 +2140,7 @@ p {
background: var(--bg-secondary);
border-radius: 16px;
backdrop-filter: var(--backdrop-blur);
box-shadow: var(--shadow-primary);
border: 1px solid var(--border-primary);
padding: 0.75rem;
margin-bottom: 1rem;
@@ -2317,6 +2319,7 @@ p {
.firmware-group.expanded .firmware-group-header {
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border-primary);
}
.firmware-group-header-content {
@@ -4482,6 +4485,7 @@ select.param-input:focus {
#topology-graph-container {
background: var(--bg-tertiary);
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
height: 100%;
width: 100%;
margin: 0;
@@ -5052,15 +5056,6 @@ select.param-input:focus {
background: rgba(244, 67, 54, 0.2);
color: #f44336;
border: 1px solid rgba(244, 67, 54, 0.3);
padding: 0.5rem 0.75rem !important;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border-radius: 8px;
margin: 0;
}
/* Ultra-compact layout for upload progress */
@@ -6485,6 +6480,7 @@ html {
background: var(--bg-secondary);
border-radius: 16px;
backdrop-filter: var(--backdrop-blur);
box-shadow: var(--shadow-primary);
border: 1px solid var(--border-primary);
padding: 1rem;
margin-bottom: 1rem;
@@ -7506,19 +7502,6 @@ html {
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.overlay-dialog-btn-danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
border: 1px solid #ef4444;
color: white;
}
.overlay-dialog-btn-danger:hover {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
border-color: #dc2626;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
/* === Rollout Component Styles === */
.rollout-btn {
background: transparent;