Files
spore-ui/public/scripts/components/RolloutComponent.js
2025-10-22 19:00:26 +02:00

259 lines
9.2 KiB
JavaScript

// Rollout Component - Shows rollout panel with matching nodes and starts rollout
class RolloutComponent extends Component {
constructor(container, viewModel, eventBus) {
super(container, viewModel, eventBus);
logger.debug('RolloutComponent: Constructor called');
this.rolloutData = null;
this.matchingNodes = [];
this.onRolloutCallback = null;
this.onCancelCallback = null;
}
setupEventListeners() {
// Rollout button
const rolloutBtn = this.findElement('#rollout-confirm-btn');
if (rolloutBtn) {
this.addEventListener(rolloutBtn, 'click', this.handleRollout.bind(this));
}
// Cancel button
const cancelBtn = this.findElement('#rollout-cancel-btn');
if (cancelBtn) {
this.addEventListener(cancelBtn, 'click', this.handleCancel.bind(this));
}
}
// Start rollout - hide labels and show status indicators
startRollout() {
const nodeItems = this.container.querySelectorAll('.rollout-node-item');
nodeItems.forEach(item => {
const labelsDiv = item.querySelector('.rollout-node-labels');
const statusDiv = item.querySelector('.status-indicator');
if (labelsDiv && statusDiv) {
labelsDiv.style.display = 'none';
statusDiv.style.display = 'block';
statusDiv.textContent = 'Ready';
statusDiv.className = 'status-indicator ready';
}
});
// Disable the confirm button
const confirmBtn = this.findElement('#rollout-confirm-btn');
if (confirmBtn) {
confirmBtn.disabled = true;
confirmBtn.textContent = 'Rollout in Progress...';
}
}
// Update status for a specific node
updateNodeStatus(nodeIp, status) {
const nodeItem = this.container.querySelector(`[data-node-ip="${nodeIp}"]`);
if (!nodeItem) return;
const statusDiv = nodeItem.querySelector('.status-indicator');
if (!statusDiv) return;
let displayStatus = status;
let statusClass = '';
switch (status) {
case 'updating_labels':
displayStatus = 'Updating Labels...';
statusClass = 'uploading';
break;
case 'uploading':
displayStatus = 'Uploading...';
statusClass = 'uploading';
break;
case 'completed':
displayStatus = 'Completed';
statusClass = 'success';
break;
case 'failed':
displayStatus = 'Failed';
statusClass = 'error';
break;
default:
displayStatus = status;
statusClass = 'pending';
}
statusDiv.textContent = displayStatus;
statusDiv.className = `status-indicator ${statusClass}`;
}
// Check if rollout is complete
isRolloutComplete() {
const statusIndicators = this.container.querySelectorAll('.status-indicator');
for (const indicator of statusIndicators) {
const status = indicator.textContent.toLowerCase();
if (status !== 'completed' && status !== 'failed') {
return false;
}
}
return true;
}
// Reset to initial state (show labels, hide status indicators)
resetRolloutState() {
const nodeItems = this.container.querySelectorAll('.rollout-node-item');
nodeItems.forEach(item => {
const labelsDiv = item.querySelector('.rollout-node-labels');
const statusDiv = item.querySelector('.status-indicator');
if (labelsDiv && statusDiv) {
labelsDiv.style.display = 'block';
statusDiv.style.display = 'none';
}
});
// Re-enable the confirm button
const confirmBtn = this.findElement('#rollout-confirm-btn');
if (confirmBtn) {
confirmBtn.disabled = false;
confirmBtn.textContent = `Rollout to ${this.matchingNodes.length} Node${this.matchingNodes.length !== 1 ? 's' : ''}`;
}
}
mount() {
super.mount();
logger.debug('RolloutComponent: Mounting...');
this.render();
logger.debug('RolloutComponent: Mounted successfully');
}
setRolloutData(name, version, labels, matchingNodes) {
this.rolloutData = { name, version, labels };
this.matchingNodes = matchingNodes;
}
setOnRolloutCallback(callback) {
this.onRolloutCallback = callback;
}
setOnCancelCallback(callback) {
this.onCancelCallback = callback;
}
render() {
if (!this.rolloutData) {
this.container.innerHTML = '<div class="error">No rollout data available</div>';
return;
}
const { name, version, labels } = this.rolloutData;
// Render labels as chips
const labelsHTML = Object.entries(labels).map(([key, value]) =>
`<span class="label-chip" title="${key}: ${value}">${key}: ${value}</span>`
).join('');
// Render matching nodes
const nodesHTML = this.matchingNodes.map(node => {
const nodeLabelsHTML = Object.entries(node.labels || {}).map(([key, value]) =>
`<span class="label-chip" title="${key}: ${value}">${key}: ${value}</span>`
).join('');
return `
<div class="rollout-node-item" data-node-ip="${node.ip}">
<div class="rollout-node-info">
<div class="rollout-node-ip">${this.escapeHtml(node.ip)}</div>
<div class="rollout-node-version">Version: ${this.escapeHtml(node.version)}</div>
</div>
<div class="rollout-node-status">
<div class="rollout-node-labels">
${nodeLabelsHTML}
</div>
<div class="status-indicator ready" style="display: none;">Ready</div>
</div>
</div>
`;
}).join('');
this.container.innerHTML = `
<div class="rollout-panel">
<div class="rollout-header">
<p>Deploy firmware to matching cluster nodes</p>
</div>
<div class="rollout-firmware-info">
<div class="rollout-firmware-name">${this.escapeHtml(name)}</div>
<div class="rollout-firmware-version">Version: ${this.escapeHtml(version)}</div>
<div class="rollout-firmware-labels">
${labelsHTML}
</div>
</div>
<div class="rollout-matching-nodes">
<h4>Matching Nodes (${this.matchingNodes.length})</h4>
<div class="rollout-nodes-list">
${nodesHTML}
</div>
</div>
<div class="rollout-warning">
<div class="warning-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
</div>
<div class="warning-text">
<strong>Warning:</strong> This will update firmware on ${this.matchingNodes.length} node${this.matchingNodes.length !== 1 ? 's' : ''}.
The rollout process cannot be cancelled once started.
</div>
</div>
<div class="rollout-actions">
<button id="rollout-cancel-btn" class="refresh-btn">Cancel</button>
<button id="rollout-confirm-btn" class="deploy-btn" ${this.matchingNodes.length === 0 ? 'disabled' : ''}>
Rollout to ${this.matchingNodes.length} Node${this.matchingNodes.length !== 1 ? 's' : ''}
</button>
</div>
</div>
`;
this.setupEventListeners();
}
handleRollout() {
if (!this.onRolloutCallback || this.matchingNodes.length === 0) {
return;
}
// 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);
}
handleCancel() {
if (this.onCancelCallback) {
this.onCancelCallback();
}
}
escapeHtml(text) {
if (typeof text !== 'string') return text;
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
window.RolloutComponent = RolloutComponent;