Add build cancellation feature to cloud-init process</message>
<message>Implement a new API endpoint for cancelling ongoing cloud-init builds, allowing users to request a build cancellation via the dashboard. Update the dashboard UI to include a cancel button that appears during the build process, enhancing user experience by providing control over long-running operations. Modify the build script to check for cancellation requests, ensuring that builds can be stopped gracefully. This feature improves usability and responsiveness in the cloud-init image building workflow.
This commit is contained in:
@@ -52,6 +52,11 @@
|
||||
.progress-fill { height: 100%; background: var(--accent); transition: width 0.3s; }
|
||||
.progress-fill.indeterminate { width: 35%; animation: slide 1.2s ease-in-out infinite; }
|
||||
@keyframes slide { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }
|
||||
.firstboot-progress-wrap { margin-top: 0.6rem; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
|
||||
.firstboot-progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent-dim), var(--accent)); border-radius: 3px; transition: width 0.4s ease-out; }
|
||||
.firstboot-progress-fill.animated { background: linear-gradient(90deg, var(--accent-dim), var(--accent) 50%, var(--accent-dim)); background-size: 200% 100%; animation: firstboot-shimmer 1.5s ease-in-out infinite; }
|
||||
@keyframes firstboot-shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
|
||||
.firstboot-step-label { font-size: 0.8rem; color: var(--text-dim); margin-bottom: 0.35rem; }
|
||||
.device-item {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
|
||||
padding: 0.6rem 0.75rem; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||
@@ -110,7 +115,11 @@
|
||||
|
||||
<div id="firstBootCard" class="card" style="margin-bottom: 1rem; display:none;">
|
||||
<h2 class="card-title">First-boot progress</h2>
|
||||
<div id="firstBootStatus" class="status-row">
|
||||
<p id="firstBootStepLabel" class="firstboot-step-label">Step 0 of 13</p>
|
||||
<div id="firstBootProgressWrap" class="firstboot-progress-wrap" style="display:none;">
|
||||
<div id="firstBootProgressFill" class="firstboot-progress-fill" style="width:0%;"></div>
|
||||
</div>
|
||||
<div id="firstBootStatus" class="status-row" style="margin-top: 0.5rem;">
|
||||
<span id="firstBootStep" class="status-pill idle"></span>
|
||||
<span id="firstBootMsg" class="status-msg"></span>
|
||||
</div>
|
||||
@@ -239,6 +248,7 @@
|
||||
function fmtSize(n) { if (n >= 1e9) return (n/1e9).toFixed(1)+' GB'; if (n >= 1e6) return (n/1e6).toFixed(1)+' MB'; return (n/1e3).toFixed(0)+' KB'; }
|
||||
function fmtDate(ts) { return new Date(ts*1000).toLocaleString(); }
|
||||
function fetchStatus() { fetch('/api/status').then(function(r){ return r.json(); }).then(renderStatus).catch(function(){ renderStatus({ phase: 'error', message: 'Could not load status.' }); }); }
|
||||
const FIRST_BOOT_TOTAL_STEPS = 13;
|
||||
function renderFirstBootStatus(data) {
|
||||
const phase = data.phase || 'idle';
|
||||
const card = document.getElementById('firstBootCard');
|
||||
@@ -246,8 +256,23 @@
|
||||
card.style.display = 'block';
|
||||
const stepEl = document.getElementById('firstBootStep');
|
||||
const msgEl = document.getElementById('firstBootMsg');
|
||||
const stepLabelEl = document.getElementById('firstBootStepLabel');
|
||||
const progressWrap = document.getElementById('firstBootProgressWrap');
|
||||
const progressFill = document.getElementById('firstBootProgressFill');
|
||||
const ipWrap = document.getElementById('firstBootIpWrap');
|
||||
const ipEl = document.getElementById('firstBootIp');
|
||||
var stepNum = parseInt(data.step, 10) || 0;
|
||||
if (phase === 'done') stepNum = FIRST_BOOT_TOTAL_STEPS;
|
||||
var progress = stepNum > 0 ? (stepNum / FIRST_BOOT_TOTAL_STEPS * 100) : 0;
|
||||
var inProgress = (phase === 'started' || phase === 'running');
|
||||
stepLabelEl.textContent = 'Step ' + stepNum + ' of ' + FIRST_BOOT_TOTAL_STEPS + (data.step_name ? ' · ' + data.step_name : '');
|
||||
if (inProgress || phase === 'done' || phase === 'error') {
|
||||
progressWrap.style.display = 'block';
|
||||
progressFill.style.width = progress + '%';
|
||||
if (inProgress) progressFill.classList.add('animated'); else progressFill.classList.remove('animated');
|
||||
} else {
|
||||
progressWrap.style.display = 'none';
|
||||
}
|
||||
stepEl.textContent = data.step ? 'Step ' + data.step : (phase === 'done' ? 'Done' : phase);
|
||||
stepEl.className = 'status-pill ' + (phase === 'done' ? 'done' : phase === 'error' ? 'error' : 'flashing');
|
||||
msgEl.textContent = data.message || (phase === 'done' && data.ip ? 'First-boot finished. Device IP: ' + data.ip : '');
|
||||
|
||||
Reference in New Issue
Block a user