54 lines
3.3 KiB
JavaScript
54 lines
3.3 KiB
JavaScript
// ------ helpers ------
|
|
export const $ = (q) => document.querySelector(q);
|
|
export function safe(v) { return (v === undefined || v === null || v === '') ? '—' : String(v); }
|
|
export function ensureArr(a) { return Array.isArray(a) ? a : []; }
|
|
export function pct(p) { if (p == null) return '—'; return (p * 100).toFixed(1) + '%'; }
|
|
export function humanBytes(n) { if (n == null) return '—'; const u = ['B','KiB','MiB','GiB','TiB','PiB']; let i=0,x=+n; while (x>=1024 && i<u.length-1) { x/=1024; i++; } return x.toFixed(1)+' '+u[i]; }
|
|
export function badge(txt, kind) {
|
|
const cls = { ok:'bg-success-subtle text-success-emphasis', warn:'bg-warning-subtle text-warning-emphasis',
|
|
err:'bg-danger-subtle text-danger-emphasis', info:'bg-info-subtle text-info-emphasis', dark:'bg-secondary-subtle text-secondary-emphasis' }[kind||'dark'];
|
|
return `<span class="badge rounded-pill ${cls}">${safe(txt)}</span>`;
|
|
}
|
|
export function rowHTML(cols, attrs='') { return `<tr ${attrs}>${cols.map(c => `<td>${c ?? '—'}</td>`).join('')}</tr>`; }
|
|
export function setRows(tbody, rows) { tbody.innerHTML = rows.length ? rows.join('') : rowHTML(['—']); }
|
|
export function fmtSeconds(s) {
|
|
if (s == null) return '—'; s = Math.floor(s);
|
|
const d = Math.floor(s/86400); s%=86400;
|
|
const h = Math.floor(s/3600); s%=3600;
|
|
const m = Math.floor(s/60); s%=60;
|
|
const parts = [h.toString().padStart(2,'0'), m.toString().padStart(2,'0'), s.toString().padStart(2,'0')].join(':');
|
|
return d>0 ? `${d}d ${parts}` : parts;
|
|
}
|
|
export function parseNetConf(val) {
|
|
const out = {}; if (!val) return out;
|
|
val.split(',').forEach(kv => { const [k,v] = kv.split('='); if (k && v !== undefined) out[k.trim()] = v.trim(); });
|
|
return out;
|
|
}
|
|
export function parseVmNetworks(config) {
|
|
const nets = [];
|
|
for (const [k, v] of Object.entries(config || {})) {
|
|
const m = k.match(/^net(\d+)$/); if (m) nets.push({ idx:+m[1], raw:v, ...parseNetConf(v) });
|
|
}
|
|
nets.sort((a,b)=>a.idx-b.idx); return nets;
|
|
}
|
|
export function kvGrid(obj, keys, titleMap={}) {
|
|
return `<div class="row row-cols-1 row-cols-md-2 g-2">${
|
|
keys.map(k => `<div class="col"><div class="card border-0"><div class="card-body p-2">
|
|
<div class="text-muted small">${titleMap[k] || k}</div><div class="fw-semibold">${safe(obj[k])}</div>
|
|
</div></div></div>`).join('')}</div>`;
|
|
}
|
|
// prefer first non-empty
|
|
export function pick(...vals) { for (const v of vals) { if (v !== undefined && v !== null && v !== '') return v; } return ''; }
|
|
|
|
// toast
|
|
export function showToast(title, body, variant) {
|
|
const cont = document.getElementById('toast-container'); if (!cont) return;
|
|
const vcls = { success:'text-bg-success', info:'text-bg-info', warning:'text-bg-warning', danger:'text-bg-danger', secondary:'text-bg-secondary' }[variant||'secondary'];
|
|
const el = document.createElement('div');
|
|
el.className = 'toast align-items-center ' + vcls;
|
|
el.setAttribute('role','alert'); el.setAttribute('aria-live','assertive'); el.setAttribute('aria-atomic','true');
|
|
el.innerHTML = `<div class="d-flex"><div class="toast-body"><strong>${title||''}</strong>${title?': ':''}${body||''}</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button></div>`;
|
|
cont.appendChild(el); const t = new bootstrap.Toast(el, { delay: 5000 }); t.show(); el.addEventListener('hidden.bs.toast', () => el.remove());
|
|
}
|