feat: rollout
This commit is contained in:
@@ -168,8 +168,13 @@ class FirmwareComponent extends Component {
|
||||
return `
|
||||
<div class="firmware-group">
|
||||
<div class="firmware-group-header">
|
||||
<h3 class="firmware-group-name">${this.escapeHtml(group.name)}</h3>
|
||||
<span class="firmware-group-count">${group.firmware.length} version${group.firmware.length !== 1 ? 's' : ''}</span>
|
||||
<div class="firmware-group-header-content">
|
||||
<h3 class="firmware-group-name">${this.escapeHtml(group.name)}</h3>
|
||||
<span class="firmware-group-count">${group.firmware.length} version${group.firmware.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<svg class="firmware-group-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="firmware-versions">
|
||||
${versionsHTML}
|
||||
@@ -198,6 +203,13 @@ class FirmwareComponent extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<div class="firmware-version-actions">
|
||||
<button class="action-btn rollout-btn" title="Rollout firmware" data-name="${firmware.name}" data-version="${firmware.version}" data-labels='${JSON.stringify(firmware.labels)}'>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
||||
<path d="M2 17l10 5 10-5"/>
|
||||
<path d="M2 12l10 5 10-5"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-btn download-btn" title="Download firmware" data-name="${firmware.name}" data-version="${firmware.version}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
@@ -266,6 +278,15 @@ class FirmwareComponent extends Component {
|
||||
}
|
||||
|
||||
setupFirmwareItemListeners() {
|
||||
// Firmware group header clicks (for expand/collapse)
|
||||
const groupHeaders = this.findAllElements('.firmware-group-header');
|
||||
groupHeaders.forEach(header => {
|
||||
this.addEventListener(header, 'click', (e) => {
|
||||
const group = header.closest('.firmware-group');
|
||||
group.classList.toggle('expanded');
|
||||
});
|
||||
});
|
||||
|
||||
// Version item clicks (for editing)
|
||||
const versionItems = this.findAllElements('.firmware-version-item.clickable');
|
||||
versionItems.forEach(item => {
|
||||
@@ -281,6 +302,18 @@ class FirmwareComponent extends Component {
|
||||
});
|
||||
});
|
||||
|
||||
// Rollout buttons
|
||||
const rolloutBtns = this.findAllElements('.rollout-btn');
|
||||
rolloutBtns.forEach(btn => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// Download buttons
|
||||
const downloadBtns = this.findAllElements('.download-btn');
|
||||
downloadBtns.forEach(btn => {
|
||||
@@ -375,6 +408,170 @@ class FirmwareComponent extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="rollout-progress-content">
|
||||
<div class="rollout-progress-header">
|
||||
<h3>Rolling Out Firmware</h3>
|
||||
</div>
|
||||
<div class="rollout-progress-body">
|
||||
<div class="rollout-progress-info">
|
||||
<p>Firmware rollout in progress...</p>
|
||||
<p class="rollout-progress-text">Preparing rollout...</p>
|
||||
</div>
|
||||
<div class="rollout-progress-bar">
|
||||
<div class="rollout-progress-fill" id="rollout-progress-fill"></div>
|
||||
</div>
|
||||
<div class="rollout-progress-details" id="rollout-progress-details">
|
||||
<div class="rollout-node-list" id="rollout-node-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 {
|
||||
// Note: The registry API doesn't have a delete endpoint in the OpenAPI spec
|
||||
|
||||
Reference in New Issue
Block a user