Compare commits
1 Commits
65493b2c17
...
0b3f0cbca4
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b3f0cbca4 |
@@ -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' });
|
||||
|
||||
@@ -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,59 +299,42 @@ 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() {
|
||||
@@ -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');
|
||||
|
||||
@@ -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.isVisible = true;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.container.classList.remove('visible');
|
||||
|
||||
setTimeout(() => {
|
||||
this.isVisible = false;
|
||||
|
||||
// Call cancel callback if provided
|
||||
if (this.onCancel) {
|
||||
this.onCancel();
|
||||
this.onCancel = null;
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
handleConfirm() {
|
||||
this.container.classList.remove('visible');
|
||||
|
||||
setTimeout(() => {
|
||||
this.isVisible = false;
|
||||
this.hide();
|
||||
|
||||
// Call confirm callback if provided
|
||||
if (this.onConfirm) {
|
||||
this.onConfirm();
|
||||
this.onConfirm = null;
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
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">
|
||||
<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">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
<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)}
|
||||
${this.cancelText}
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="overlay-dialog-btn ${this.confirmClass}" type="button">
|
||||
${this.escapeHtml(this.confirmText)}
|
||||
<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
|
||||
});
|
||||
};
|
||||
|
||||
@@ -228,17 +228,6 @@ class RolloutComponent extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeCount = this.matchingNodes.length;
|
||||
const nodePlural = nodeCount !== 1 ? 's' : '';
|
||||
const { name, version } = this.rolloutData;
|
||||
|
||||
// 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: {
|
||||
@@ -251,8 +240,6 @@ class RolloutComponent extends Component {
|
||||
|
||||
this.onRolloutCallback(rolloutData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
if (this.onCancelCallback) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user