Files
pve-ha-web/static/js/admin.js
Mateusz Gruszczyński 88f42687bf refator_comm1
2025-10-18 23:51:21 +02:00

156 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}