156 lines
6.1 KiB
JavaScript
156 lines
6.1 KiB
JavaScript
import { rowHTML, setRows, safe, showToast, badge } from './helpers.js';
|
||
import { api } from './api.js';
|
||
|
||
// helpers
|
||
const q = (r, s) => (r || document).querySelector(s);
|
||
const qq = (r, s) => Array.from((r || document).querySelectorAll(s));
|
||
const low = (x) => String(x ?? '').toLowerCase();
|
||
|
||
// --- dropdown: wybierz pierwszy inny node od bieżącego
|
||
function rebuildTargetSelect(selectEl, currentNode, nodes) {
|
||
if (!selectEl) return;
|
||
const cur = String(currentNode || '').trim();
|
||
const all = (nodes || []).map(n => String(n.name || n.node || n).trim()).filter(Boolean);
|
||
const others = all.filter(n => n && n !== cur);
|
||
selectEl.innerHTML = others.map(n => `<option value="${n}">${n}</option>`).join('');
|
||
// domyślnie pierwszy sensowny inny node
|
||
if (selectEl.options.length > 0) selectEl.selectedIndex = 0;
|
||
}
|
||
|
||
// --- statusy + przyciski
|
||
function boolRunning(statusRaw) { return /running|online|started/i.test(statusRaw || ''); }
|
||
function setButtonsByStatus(tr, statusRaw) {
|
||
const running = boolRunning(statusRaw);
|
||
// start tylko gdy nie działa
|
||
const bStart = tr.querySelector('.act-start');
|
||
const bStop = tr.querySelector('.act-stop');
|
||
const bShutdown = tr.querySelector('.act-shutdown');
|
||
|
||
if (bStart) bStart.disabled = running;
|
||
if (bStop) bStop.disabled = !running;
|
||
if (bShutdown) bShutdown.disabled = !running;
|
||
|
||
// MIGRATE: aktywny także dla offline (offline migrate)
|
||
const bMig = tr.querySelector('.act-migrate');
|
||
const sel = tr.querySelector('.target-node');
|
||
const hasTarget = !!(sel && sel.value);
|
||
// blokujemy tylko gdy nie ma targetu albo trwa „working/progress”
|
||
const busy = /working|progress|busy/i.test(statusRaw || '');
|
||
if (bMig) bMig.disabled = !hasTarget || busy;
|
||
}
|
||
|
||
// --- render
|
||
export async function renderVMAdmin() {
|
||
const wrap = document.getElementById('vm-admin');
|
||
if (!wrap) return;
|
||
const tbody = wrap.querySelector('tbody');
|
||
if (!tbody) return;
|
||
|
||
// pobierz listy
|
||
const [list, nodesSummary] = await Promise.all([api.listAllVmct(), api.nodesSummary()]);
|
||
const all = Array.isArray(list.all) ? list.all : [];
|
||
const nodes = Array.isArray(nodesSummary?.nodes) ? nodesSummary.nodes : [];
|
||
|
||
// zbuduj wiersze (bez checkboxa)
|
||
const rows = all.map(vm => {
|
||
const status = safe(vm.status || vm.current?.status || vm.current?.qmpstatus || '—');
|
||
const isRun = boolRunning(status);
|
||
return rowHTML([
|
||
safe(vm.sid),
|
||
safe(vm.name || vm.vmid || vm.sid),
|
||
safe(vm.node || '—'),
|
||
badge(status, isRun ? 'ok' : 'dark'),
|
||
`<div class="btn-group">
|
||
<button type="button" class="btn btn-sm btn-outline-success act-start">Start</button>
|
||
<button type="button" class="btn btn-sm btn-outline-warning act-shutdown">Shutdown</button>
|
||
<button type="button" class="btn btn-sm btn-outline-danger act-stop">Stop</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary act-unlock">Unlock</button>
|
||
</div>`,
|
||
`<div class="d-grid gap-1">
|
||
<select class="form-select form-select-sm target-node"></select>
|
||
<button type="button" class="btn btn-sm btn-outline-primary act-migrate">MIGRATE</button>
|
||
</div>`
|
||
], `data-sid="${safe(vm.sid)}"`);
|
||
});
|
||
|
||
setRows(tbody, rows);
|
||
|
||
// uzupełnij selecty + stany przycisków
|
||
qq(tbody, 'tr[data-sid]').forEach(tr => {
|
||
const nodeCell = tr.children[2];
|
||
const statusCell= tr.children[3];
|
||
const sel = tr.querySelector('.target-node');
|
||
rebuildTargetSelect(sel, nodeCell?.textContent?.trim(), nodes);
|
||
setButtonsByStatus(tr, statusCell?.innerText || '');
|
||
});
|
||
|
||
// --- delegacja CLICK globalnie: łapie też elementy po rerenderach
|
||
document.addEventListener('click', async (ev) => {
|
||
const btn = ev.target.closest?.('.act-start,.act-stop,.act-shutdown,.act-unlock,.act-migrate');
|
||
if (!btn) return;
|
||
// zapobiegamy submitom, bąbelkowaniu itp.
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
|
||
const tr = btn.closest('tr[data-sid]');
|
||
if (!tr) return;
|
||
const sid = tr.getAttribute('data-sid');
|
||
const name = tr.children[1]?.textContent?.trim() || sid;
|
||
|
||
// mapowanie akcji
|
||
let action = '';
|
||
let target = undefined;
|
||
if (btn.classList.contains('act-start')) action = 'start';
|
||
if (btn.classList.contains('act-stop')) action = 'stop';
|
||
if (btn.classList.contains('act-shutdown')) action = 'shutdown';
|
||
if (btn.classList.contains('act-unlock')) action = 'unlock';
|
||
if (btn.classList.contains('act-migrate')) {
|
||
action = 'migrate';
|
||
target = tr.querySelector('.target-node')?.value;
|
||
if (!target) {
|
||
showToast('Migrate', 'Wybierz docelowy node inny niż bieżący', 'warning');
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
const res = await api.vmAction(sid, action, target);
|
||
if (res?.ok) {
|
||
showToast('OK', `${action.toUpperCase()} × ${name}`, 'success');
|
||
} else {
|
||
showToast('Błąd', res?.error || `Nie udało się: ${action}`, 'danger');
|
||
}
|
||
} catch (e) {
|
||
showToast('Błąd', String(e?.message || e), 'danger');
|
||
}
|
||
}, { capture: true }); // capture: pewność, że nasz handler zadziała przed innymi
|
||
|
||
// prosty, lekki refresh co 30s (statusy i node)
|
||
setInterval(async () => {
|
||
try {
|
||
const latest = await api.listAllVmct();
|
||
const bySid = new Map((latest?.all || []).map(x => [String(x.sid), x]));
|
||
qq(tbody, 'tr[data-sid]').forEach(tr => {
|
||
const sid = tr.getAttribute('data-sid');
|
||
const row = bySid.get(sid);
|
||
if (!row) return;
|
||
const nodeCell = tr.children[2];
|
||
const statusCell = tr.children[3];
|
||
const sel = tr.querySelector('.target-node');
|
||
|
||
const newNode = String(row.node || '').trim();
|
||
if (newNode && nodeCell?.textContent?.trim() !== newNode) {
|
||
nodeCell.textContent = newNode;
|
||
rebuildTargetSelect(sel, newNode, nodes);
|
||
}
|
||
|
||
const st = String(row.status || row.current?.status || row.current?.qmpstatus || '').trim();
|
||
if (st && statusCell?.innerText?.trim() !== st) {
|
||
statusCell.innerHTML = badge(st, boolRunning(st) ? 'ok' : 'dark');
|
||
setButtonsByStatus(tr, st);
|
||
}
|
||
});
|
||
} catch { /* no-op */ }
|
||
}, 30000);
|
||
}
|