// Registry Firmware Component - CRUD interface for firmware registry
class FirmwareComponent extends Component {
constructor(container, viewModel, eventBus) {
super(container, viewModel, eventBus);
logger.debug('FirmwareComponent: Constructor called');
logger.debug('FirmwareComponent: Container:', container);
// Initialize drawer component
this.drawer = new DrawerComponent();
// Registry connection status
this.registryConnected = false;
this.registryError = null;
}
setupEventListeners() {
// Setup refresh button
const refreshBtn = this.findElement('#refresh-firmware-btn');
if (refreshBtn) {
this.addEventListener(refreshBtn, 'click', this.refreshFirmwareList.bind(this));
}
// Setup add firmware button
const addBtn = this.findElement('#add-firmware-btn');
if (addBtn) {
this.addEventListener(addBtn, 'click', this.showAddFirmwareForm.bind(this));
}
// Setup search input
const searchInput = this.findElement('#firmware-search');
if (searchInput) {
this.addEventListener(searchInput, 'input', this.handleSearch.bind(this));
}
}
setupViewModelListeners() {
this.subscribeToProperty('firmwareList', this.renderFirmwareList.bind(this));
this.subscribeToProperty('isLoading', this.updateLoadingState.bind(this));
this.subscribeToProperty('searchQuery', this.updateSearchResults.bind(this));
this.subscribeToProperty('registryConnected', this.updateRegistryStatus.bind(this));
}
mount() {
super.mount();
logger.debug('FirmwareComponent: Mounting...');
// Check registry connection and load firmware list
this.checkRegistryConnection();
this.loadFirmwareList();
logger.debug('FirmwareComponent: Mounted successfully');
}
unmount() {
this.cleanupDynamicListeners();
super.unmount();
}
async checkRegistryConnection() {
try {
await window.apiClient.getRegistryHealth();
this.registryConnected = true;
this.registryError = null;
this.viewModel.set('registryConnected', true);
} catch (error) {
logger.error('Registry connection failed:', error);
this.registryConnected = false;
this.registryError = error.message;
this.viewModel.set('registryConnected', false);
}
}
async loadFirmwareList() {
try {
this.viewModel.set('isLoading', true);
const firmwareList = await window.apiClient.listFirmwareFromRegistry();
this.viewModel.set('firmwareList', firmwareList);
} catch (error) {
logger.error('Failed to load firmware list:', error);
this.viewModel.set('firmwareList', []);
this.showError('Failed to load firmware list: ' + error.message);
} finally {
this.viewModel.set('isLoading', false);
}
}
async refreshFirmwareList() {
await this.checkRegistryConnection();
await this.loadFirmwareList();
}
renderFirmwareList() {
const container = this.findElement('#firmware-list-container');
if (!container) return;
const groupedFirmware = this.viewModel.get('firmwareList') || [];
const searchQuery = this.viewModel.get('searchQuery') || '';
// Filter grouped firmware based on search query
const filteredGroups = groupedFirmware.map(group => {
if (!searchQuery) return group;
// Split search query into individual terms
const searchTerms = searchQuery.toLowerCase().split(/\s+/).filter(term => term.length > 0);
// Filter firmware versions within the group
const filteredFirmware = group.firmware.filter(firmware => {
// All search terms must match somewhere in the firmware data
return searchTerms.every(term => {
// Check group name
if (group.name.toLowerCase().includes(term)) {
return true;
}
// Check version
if (firmware.version.toLowerCase().includes(term)) {
return true;
}
// Check labels
if (Object.values(firmware.labels || {}).some(label =>
label.toLowerCase().includes(term)
)) {
return true;
}
return false;
});
});
// Return group with filtered firmware, or null if no firmware matches
return filteredFirmware.length > 0 ? {
...group,
firmware: filteredFirmware
} : null;
}).filter(group => group !== null);
if (filteredGroups.length === 0) {
container.innerHTML = `
${searchQuery ? 'No firmware found' : 'No firmware available'}
${searchQuery ? 'Try adjusting your search terms' : 'Upload your first firmware to get started'}
`;
return;
}
// Auto-expand groups when search is active to show results
const autoExpand = searchQuery.trim().length > 0;
const firmwareHTML = filteredGroups.map(group => this.renderFirmwareGroup(group, autoExpand)).join('');
container.innerHTML = `
${firmwareHTML}
`;
// Setup event listeners for firmware items
this.setupFirmwareItemListeners();
}
renderFirmwareGroup(group, autoExpand = false) {
const versionsHTML = group.firmware.map(firmware => this.renderFirmwareVersion(firmware)).join('');
// Add 'expanded' class if autoExpand is true (e.g., when search results are shown)
const expandedClass = autoExpand ? 'expanded' : '';
return `
`;
}
renderFirmwareVersion(firmware) {
const labels = firmware.labels || {};
const labelsHTML = Object.entries(labels).map(([key, value]) =>
`${key}: ${value} `
).join('');
const sizeKB = Math.round(firmware.size / 1024);
return `
v${this.escapeHtml(firmware.version)}
${sizeKB} KB
${labelsHTML}
`;
}
renderFirmwareItem(firmware) {
const labels = firmware.labels || {};
const labelsHTML = Object.entries(labels).map(([key, value]) =>
`${key}: ${value} `
).join('');
const sizeKB = Math.round(firmware.size / 1024);
return `
${this.escapeHtml(firmware.name)}
v${this.escapeHtml(firmware.version)}
${sizeKB} KB
${labelsHTML}
`;
}
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) => {
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) => {
// Don't trigger if clicking on action buttons
if (e.target.closest('.firmware-version-actions')) {
return;
}
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) => {
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) => {
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) => {
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);
}
showEditFirmwareForm(name, version) {
const groupedFirmware = this.viewModel.get('firmwareList') || [];
// Find the firmware in the grouped data
let firmware = null;
for (const group of groupedFirmware) {
if (group.name === name) {
firmware = group.firmware.find(f => f.version === version);
if (firmware) break;
}
}
if (firmware) {
this.openFirmwareForm('Edit Firmware', firmware, null);
}
}
openFirmwareForm(title, firmwareData, onCloseCallback) {
this.drawer.openDrawer(title, (contentContainer, setActiveComponent) => {
const formComponent = new FirmwareFormComponent(contentContainer, this.viewModel, this.eventBus);
setActiveComponent(formComponent);
formComponent.setFirmwareData(firmwareData);
formComponent.setOnSaveCallback(() => {
this.loadFirmwareList();
this.drawer.closeDrawer();
});
formComponent.setOnCancelCallback(() => {
this.drawer.closeDrawer();
});
formComponent.mount();
}, null, onCloseCallback, true); // Hide terminal button
}
async downloadFirmware(name, version) {
try {
const blob = await window.apiClient.downloadFirmwareFromRegistry(name, version);
// Create download link
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${name}-${version}.bin`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
this.showSuccess(`Firmware ${name} v${version} downloaded successfully`);
} catch (error) {
logger.error('Download failed:', error);
this.showError('Download failed: ' + error.message);
}
}
showDeleteConfirmation(name, version) {
OverlayDialogComponent.danger({
title: 'Delete Firmware',
message: `Are you sure you want to delete firmware "${name}" version "${version}"? This action cannot be undone.`,
confirmText: 'Delete',
cancelText: 'Cancel',
onConfirm: () => this.deleteFirmware(name, version)
});
}
async showRolloutPanel(name, version, labels) {
try {
// Get cluster node versions to show which nodes will be affected
const nodeVersions = await window.apiClient.getClusterNodeVersions();
// Filter nodes that match the firmware labels
const matchingNodes = nodeVersions.nodes.filter(node => {
return this.nodeMatchesLabels(node.labels, labels);
});
this.openRolloutDrawer(name, version, labels, matchingNodes);
} catch (error) {
logger.error('Failed to get cluster node versions:', error);
this.showError('Failed to get cluster information: ' + error.message);
}
}
nodeMatchesLabels(nodeLabels, firmwareLabels) {
for (const [key, value] of Object.entries(firmwareLabels)) {
if (!nodeLabels[key] || nodeLabels[key] !== value) {
return false;
}
}
return true;
}
openRolloutDrawer(name, version, labels, matchingNodes) {
this.drawer.openDrawer('Rollout Firmware', (contentContainer, setActiveComponent) => {
const rolloutComponent = new RolloutComponent(contentContainer, this.viewModel, this.eventBus);
setActiveComponent(rolloutComponent);
rolloutComponent.setRolloutData(name, version, labels, matchingNodes);
rolloutComponent.setOnRolloutCallback((rolloutData) => {
this.startRollout(rolloutData);
});
rolloutComponent.setOnCancelCallback(() => {
this.drawer.closeDrawer();
});
// Store reference for status updates
this.currentRolloutComponent = rolloutComponent;
rolloutComponent.mount();
}, null, null, true); // Hide terminal button
}
async startRollout(rolloutData) {
try {
// Start rollout in the panel (no backdrop)
if (this.currentRolloutComponent) {
this.currentRolloutComponent.startRollout();
}
const response = await window.apiClient.startRollout(rolloutData);
logger.info('Rollout started:', response);
this.showSuccess(`Rollout started for ${response.totalNodes} nodes`);
// Set up WebSocket listener for rollout progress
this.setupRolloutProgressListener(response.rolloutId);
} catch (error) {
logger.error('Rollout failed:', error);
this.showError('Rollout failed: ' + error.message);
// Reset rollout state on error
if (this.currentRolloutComponent) {
this.currentRolloutComponent.resetRolloutState();
}
}
}
showRolloutProgress() {
// Create backdrop and progress overlay
const backdrop = document.createElement('div');
backdrop.className = 'rollout-backdrop';
backdrop.id = 'rollout-backdrop';
const progressOverlay = document.createElement('div');
progressOverlay.className = 'rollout-progress-overlay';
progressOverlay.innerHTML = `
Firmware rollout in progress...
Preparing rollout...
`;
backdrop.appendChild(progressOverlay);
document.body.appendChild(backdrop);
// Block UI interactions
document.body.style.overflow = 'hidden';
}
hideRolloutProgress() {
const backdrop = document.getElementById('rollout-backdrop');
if (backdrop) {
document.body.removeChild(backdrop);
}
document.body.style.overflow = '';
}
setupRolloutProgressListener(rolloutId) {
// Track completed nodes for parallel processing
this.completedNodes = new Set();
this.totalNodes = 0;
const progressListener = (data) => {
if (data.rolloutId === rolloutId) {
// Set total nodes from first update
if (this.totalNodes === 0) {
this.totalNodes = data.total;
}
this.updateRolloutProgress(data);
}
};
window.wsClient.on('rolloutProgress', progressListener);
// Store listener for cleanup
this.currentRolloutListener = progressListener;
}
updateRolloutProgress(data) {
// Update status in the rollout panel
if (this.currentRolloutComponent) {
this.currentRolloutComponent.updateNodeStatus(data.nodeIp, data.status);
}
// Track completed nodes for parallel processing
if (data.status === 'completed') {
this.completedNodes.add(data.nodeIp);
} else if (data.status === 'failed') {
// Also count failed nodes as "processed" for completion check
this.completedNodes.add(data.nodeIp);
}
// Check if rollout is complete (all nodes processed)
if (this.completedNodes.size >= this.totalNodes) {
setTimeout(() => {
this.showSuccess('Rollout completed successfully');
// Clean up WebSocket listener
if (this.currentRolloutListener) {
window.wsClient.off('rolloutProgress', this.currentRolloutListener);
this.currentRolloutListener = null;
}
}, 2000);
}
}
async deleteFirmware(name, version) {
try {
await window.apiClient.deleteFirmwareFromRegistry(name, version);
this.showSuccess(`Firmware ${name} v${version} deleted successfully`);
await this.loadFirmwareList();
} catch (error) {
logger.error('Delete failed:', error);
this.showError('Delete failed: ' + error.message);
}
}
handleSearch(event) {
const query = event.target.value;
this.viewModel.set('searchQuery', query);
}
updateSearchResults() {
// This method is called when searchQuery property changes
// The actual filtering is handled in renderFirmwareList
this.renderFirmwareList();
}
updateLoadingState() {
const isLoading = this.viewModel.get('isLoading');
const container = this.findElement('#firmware-list-container');
if (isLoading && container) {
container.innerHTML = `
`;
}
}
updateRegistryStatus() {
const isConnected = this.viewModel.get('registryConnected');
const statusElement = this.findElement('#registry-status');
if (statusElement) {
if (isConnected) {
statusElement.innerHTML = `
Registry Connected
`;
} else {
statusElement.innerHTML = `
Registry Disconnected
`;
}
}
}
showSuccess(message) {
this.showNotification(message, 'success');
}
showError(message) {
this.showNotification(message, 'error');
}
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('show');
}, 100);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
if (notification.parentNode) {
document.body.removeChild(notification);
}
}, 300);
}, 3000);
}
escapeHtml(text) {
if (typeof text !== 'string') return text;
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
window.FirmwareComponent = FirmwareComponent;