refator_comm1
This commit is contained in:
@@ -1,155 +1,271 @@
|
|||||||
import { rowHTML, setRows, safe, showToast, badge } from './helpers.js';
|
import { rowHTML, setRows, safe, showToast, badge } from './helpers.js';
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
|
|
||||||
// helpers
|
// ==== helpers ====
|
||||||
const q = (r, s) => (r || document).querySelector(s);
|
const q = (r, s) => (r || document).querySelector(s);
|
||||||
const qq = (r, s) => Array.from((r || document).querySelectorAll(s));
|
const qq = (r, s) => Array.from((r || document).querySelectorAll(s));
|
||||||
const low = (x) => String(x ?? '').toLowerCase();
|
const low = (x) => String(x ?? '').toLowerCase();
|
||||||
|
|
||||||
// --- dropdown: wybierz pierwszy inny node od bieżącego
|
function isRunning(st) { return /running|online|started/.test(low(st)); }
|
||||||
|
function isBusy(st) { return /working|progress|busy/.test(low(st)); }
|
||||||
|
|
||||||
|
function setBadge(cell, statusRaw) {
|
||||||
|
if (!cell) return;
|
||||||
|
const s = String(statusRaw || '').trim() || '—';
|
||||||
|
let hue = 'dark';
|
||||||
|
if (isRunning(s)) hue = 'ok';
|
||||||
|
else if (isBusy(s)) hue = 'info';
|
||||||
|
cell.innerHTML = badge(s, hue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractNodes(nodesSummary) {
|
||||||
|
if (!nodesSummary) return [];
|
||||||
|
const arr = Array.isArray(nodesSummary.nodes) ? nodesSummary.nodes : nodesSummary;
|
||||||
|
return Array.from(new Set((arr || []).map(n => String(n?.name || n?.node || n || '').trim()).filter(Boolean)));
|
||||||
|
}
|
||||||
|
|
||||||
function rebuildTargetSelect(selectEl, currentNode, nodes) {
|
function rebuildTargetSelect(selectEl, currentNode, nodes) {
|
||||||
if (!selectEl) return;
|
if (!selectEl) return;
|
||||||
const cur = String(currentNode || '').trim();
|
const cur = String(currentNode || '').trim();
|
||||||
const all = (nodes || []).map(n => String(n.name || n.node || n).trim()).filter(Boolean);
|
const list = (nodes || []).map(n => String(n).trim()).filter(Boolean);
|
||||||
const others = all.filter(n => n && n !== cur);
|
const others = list.filter(n => n && n !== cur);
|
||||||
selectEl.innerHTML = others.map(n => `<option value="${n}">${n}</option>`).join('');
|
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;
|
if (selectEl.options.length > 0) selectEl.selectedIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- statusy + przyciski
|
function syncButtonsForRow(tr, statusRaw) {
|
||||||
function boolRunning(statusRaw) { return /running|online|started/i.test(statusRaw || ''); }
|
const running = isRunning(statusRaw);
|
||||||
function setButtonsByStatus(tr, statusRaw) {
|
const busy = isBusy(statusRaw);
|
||||||
const running = boolRunning(statusRaw);
|
|
||||||
// start tylko gdy nie działa
|
|
||||||
const bStart = tr.querySelector('.act-start');
|
const bStart = tr.querySelector('.act-start');
|
||||||
const bStop = tr.querySelector('.act-stop');
|
const bStop = tr.querySelector('.act-stop');
|
||||||
const bShutdown = tr.querySelector('.act-shutdown');
|
const bShutdown = tr.querySelector('.act-shutdown');
|
||||||
|
const bUnlock = tr.querySelector('.act-unlock');
|
||||||
|
const bMigrate = tr.querySelector('.act-migrate');
|
||||||
|
const sel = tr.querySelector('.target-node');
|
||||||
|
|
||||||
if (bStart) bStart.disabled = running;
|
if (bStart) bStart.disabled = running || busy;
|
||||||
if (bStop) bStop.disabled = !running;
|
if (bStop) bStop.disabled = !running || busy;
|
||||||
if (bShutdown) bShutdown.disabled = !running;
|
if (bShutdown) bShutdown.disabled = !running || busy;
|
||||||
|
if (bUnlock) bUnlock.disabled = busy;
|
||||||
|
|
||||||
// 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);
|
const hasTarget = !!(sel && sel.value);
|
||||||
// blokujemy tylko gdy nie ma targetu albo trwa „working/progress”
|
// MIGRATE włączony także dla offline; blokujemy tylko gdy brak targetu albo busy
|
||||||
const busy = /working|progress|busy/i.test(statusRaw || '');
|
if (bMigrate) bMigrate.disabled = !hasTarget || busy;
|
||||||
if (bMig) bMig.disabled = !hasTarget || busy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- render
|
// ==== state: szybki refresh dla aktywnych SID-ów ====
|
||||||
|
const activeSids = new Set();
|
||||||
|
let slowTimer = null;
|
||||||
|
let fastTimer = null;
|
||||||
|
let cachedNodes = [];
|
||||||
|
|
||||||
|
// ==== rendering ====
|
||||||
export async function renderVMAdmin() {
|
export async function renderVMAdmin() {
|
||||||
const wrap = document.getElementById('vm-admin');
|
const table = document.getElementById('vm-admin');
|
||||||
if (!wrap) return;
|
if (!table) return;
|
||||||
const tbody = wrap.querySelector('tbody');
|
const tbody = table.querySelector('tbody');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
|
|
||||||
// pobierz listy
|
// początkowe pobranie
|
||||||
const [list, nodesSummary] = await Promise.all([api.listAllVmct(), api.nodesSummary()]);
|
const [list, nodesSummary] = await Promise.all([api.listAllVmct(), api.nodesSummary()]);
|
||||||
const all = Array.isArray(list.all) ? list.all : [];
|
const all = Array.isArray(list?.all) ? list.all : [];
|
||||||
const nodes = Array.isArray(nodesSummary?.nodes) ? nodesSummary.nodes : [];
|
cachedNodes = extractNodes(nodesSummary);
|
||||||
|
|
||||||
// zbuduj wiersze (bez checkboxa)
|
// zbuduj rzędy 1:1 z nagłówkiem (8 kolumn)
|
||||||
const rows = all.map(vm => {
|
const rows = all.map(vm => {
|
||||||
|
const sid = safe(vm.sid);
|
||||||
|
const type = safe(vm.type || vm.meta?.type || vm.kind || '—');
|
||||||
|
const name = safe(vm.name || vm.vmid || vm.sid);
|
||||||
|
const node = safe(vm.node || vm.meta?.node || '—');
|
||||||
const status = safe(vm.status || vm.current?.status || vm.current?.qmpstatus || '—');
|
const status = safe(vm.status || vm.current?.status || vm.current?.qmpstatus || '—');
|
||||||
const isRun = boolRunning(status);
|
|
||||||
return rowHTML([
|
return rowHTML([
|
||||||
safe(vm.sid),
|
sid, // SID
|
||||||
safe(vm.name || vm.vmid || vm.sid),
|
type, // Type
|
||||||
safe(vm.node || '—'),
|
name, // Name
|
||||||
badge(status, isRun ? 'ok' : 'dark'),
|
node, // Node
|
||||||
|
badge(status, isRunning(status) ? 'ok' : 'dark'), // Status
|
||||||
|
// Actions
|
||||||
`<div class="btn-group">
|
`<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-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-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-danger act-stop">Stop</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary act-unlock">Unlock</button>
|
<button type="button" class="btn btn-sm btn-outline-secondary act-unlock">Unlock</button>
|
||||||
</div>`,
|
</div>`,
|
||||||
`<div class="d-grid gap-1">
|
// Target
|
||||||
<select class="form-select form-select-sm target-node"></select>
|
`<select class="form-select form-select-sm target-node"></select>`,
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary act-migrate">MIGRATE</button>
|
// Migrate
|
||||||
</div>`
|
`<button type="button" class="btn btn-sm btn-outline-primary act-migrate">Migrate</button>`
|
||||||
], `data-sid="${safe(vm.sid)}"`);
|
], `data-sid="${sid}"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
setRows(tbody, rows);
|
setRows(tbody, rows);
|
||||||
|
|
||||||
// uzupełnij selecty + stany przycisków
|
// wstępne wypełnienie selectów i ustawienie stanów przycisków
|
||||||
qq(tbody, 'tr[data-sid]').forEach(tr => {
|
qq(tbody, 'tr[data-sid]').forEach(tr => {
|
||||||
const nodeCell = tr.children[2];
|
const nodeCell = tr.children[3];
|
||||||
const statusCell= tr.children[3];
|
const statusCell = tr.children[4];
|
||||||
const sel = tr.querySelector('.target-node');
|
const sel = tr.querySelector('.target-node');
|
||||||
rebuildTargetSelect(sel, nodeCell?.textContent?.trim(), nodes);
|
rebuildTargetSelect(sel, nodeCell?.textContent?.trim(), cachedNodes);
|
||||||
setButtonsByStatus(tr, statusCell?.innerText || '');
|
syncButtonsForRow(tr, statusCell?.innerText || '');
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- delegacja CLICK globalnie: łapie też elementy po rerenderach
|
// ==== delegacja kliknięć (capture, żeby nic nie przechwyciło) ====
|
||||||
document.addEventListener('click', async (ev) => {
|
document.addEventListener('click', onClickAction, { capture: true });
|
||||||
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]');
|
// ==== odświeżanie ====
|
||||||
if (!tr) return;
|
if (slowTimer) clearInterval(slowTimer);
|
||||||
const sid = tr.getAttribute('data-sid');
|
if (fastTimer) clearInterval(fastTimer);
|
||||||
const name = tr.children[1]?.textContent?.trim() || sid;
|
|
||||||
|
|
||||||
// mapowanie akcji
|
// cały listing co 30s
|
||||||
let action = '';
|
slowTimer = setInterval(refreshAllRows, 30000);
|
||||||
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 {
|
// aktywne SID-y co 10s
|
||||||
const res = await api.vmAction(sid, action, target);
|
fastTimer = setInterval(refreshActiveRows, 10000);
|
||||||
if (res?.ok) {
|
}
|
||||||
showToast('OK', `${action.toUpperCase()} × ${name}`, 'success');
|
|
||||||
} else {
|
async function onClickAction(ev) {
|
||||||
showToast('Błąd', res?.error || `Nie udało się: ${action}`, 'danger');
|
const btn = ev.target.closest?.('.act-start,.act-stop,.act-shutdown,.act-unlock,.act-migrate');
|
||||||
}
|
if (!btn) return;
|
||||||
} catch (e) {
|
ev.preventDefault();
|
||||||
showToast('Błąd', String(e?.message || e), 'danger');
|
ev.stopPropagation();
|
||||||
}
|
|
||||||
}, { capture: true }); // capture: pewność, że nasz handler zadziała przed innymi
|
const tr = btn.closest('tr[data-sid]');
|
||||||
|
if (!tr) return;
|
||||||
// prosty, lekki refresh co 30s (statusy i node)
|
const sid = tr.getAttribute('data-sid');
|
||||||
setInterval(async () => {
|
const name = tr.children[2]?.textContent?.trim() || sid;
|
||||||
try {
|
const statusCell = tr.children[4];
|
||||||
const latest = await api.listAllVmct();
|
const nodeCell = tr.children[3];
|
||||||
const bySid = new Map((latest?.all || []).map(x => [String(x.sid), x]));
|
|
||||||
qq(tbody, 'tr[data-sid]').forEach(tr => {
|
let action = '';
|
||||||
const sid = tr.getAttribute('data-sid');
|
let target = undefined;
|
||||||
const row = bySid.get(sid);
|
|
||||||
if (!row) return;
|
if (btn.classList.contains('act-start')) action = 'start';
|
||||||
const nodeCell = tr.children[2];
|
if (btn.classList.contains('act-stop')) action = 'stop';
|
||||||
const statusCell = tr.children[3];
|
if (btn.classList.contains('act-shutdown')) action = 'shutdown';
|
||||||
const sel = tr.querySelector('.target-node');
|
if (btn.classList.contains('act-unlock')) action = 'unlock';
|
||||||
|
if (btn.classList.contains('act-migrate')) {
|
||||||
const newNode = String(row.node || '').trim();
|
action = 'migrate';
|
||||||
if (newNode && nodeCell?.textContent?.trim() !== newNode) {
|
const sel = tr.querySelector('.target-node');
|
||||||
nodeCell.textContent = newNode;
|
target = sel?.value;
|
||||||
rebuildTargetSelect(sel, newNode, nodes);
|
const curNode = nodeCell?.textContent?.trim();
|
||||||
}
|
if (!target || target === curNode) {
|
||||||
|
showToast('Migrate', 'Wybierz docelowy node inny niż bieżący', 'warning');
|
||||||
const st = String(row.status || row.current?.status || row.current?.qmpstatus || '').trim();
|
return;
|
||||||
if (st && statusCell?.innerText?.trim() !== st) {
|
}
|
||||||
statusCell.innerHTML = badge(st, boolRunning(st) ? 'ok' : 'dark');
|
}
|
||||||
setButtonsByStatus(tr, st);
|
|
||||||
}
|
try {
|
||||||
});
|
// status natychmiast na "working" + blokady
|
||||||
} catch { /* no-op */ }
|
setBadge(statusCell, 'working');
|
||||||
}, 30000);
|
syncButtonsForRow(tr, 'working');
|
||||||
|
activeSids.add(sid);
|
||||||
|
|
||||||
|
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');
|
||||||
|
// pozwól wrócić przyciski do sensownych stanów po błędzie
|
||||||
|
await refreshOneRow(sid, tr);
|
||||||
|
activeSids.delete(sid);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Błąd', String(e?.message || e), 'danger');
|
||||||
|
await refreshOneRow(sid, tr);
|
||||||
|
activeSids.delete(sid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAllRows() {
|
||||||
|
const table = document.getElementById('vm-admin');
|
||||||
|
if (!table) return;
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const latest = await api.listAllVmct();
|
||||||
|
const list = Array.isArray(latest?.all) ? latest.all : [];
|
||||||
|
const bySid = new Map(list.map(x => [String(x.sid), x]));
|
||||||
|
const nodes = Array.isArray(latest?.nodes) ? latest.nodes : null;
|
||||||
|
if (nodes) cachedNodes = extractNodes({ nodes });
|
||||||
|
|
||||||
|
qq(tbody, 'tr[data-sid]').forEach(tr => {
|
||||||
|
const sid = tr.getAttribute('data-sid');
|
||||||
|
const row = bySid.get(sid);
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
const typeCell = tr.children[1];
|
||||||
|
const nameCell = tr.children[2];
|
||||||
|
const nodeCell = tr.children[3];
|
||||||
|
const statusCell = tr.children[4];
|
||||||
|
const sel = tr.querySelector('.target-node');
|
||||||
|
|
||||||
|
// Node (i przebudowa selecta, jeśli się zmienił)
|
||||||
|
const newNode = String(row.node || row.meta?.node || '').trim();
|
||||||
|
if (newNode && nodeCell?.textContent?.trim() !== newNode) {
|
||||||
|
nodeCell.textContent = newNode;
|
||||||
|
rebuildTargetSelect(sel, newNode, cachedNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type/Name
|
||||||
|
const newType = String(row.type || row.meta?.type || row.kind || '—');
|
||||||
|
if (typeCell && typeCell.textContent !== newType) typeCell.textContent = newType;
|
||||||
|
|
||||||
|
const newName = String(row.name || row.vmid || row.sid);
|
||||||
|
if (nameCell && nameCell.textContent !== newName) nameCell.textContent = newName;
|
||||||
|
|
||||||
|
// Status – nie nadpisujemy "working" dla aktywnych SID-ów,
|
||||||
|
// żeby nie migało w trakcie taska; resztę aktualizujemy.
|
||||||
|
const st = String(row.status || row.current?.status || row.current?.qmpstatus || '').trim();
|
||||||
|
if (!activeSids.has(sid) && st) {
|
||||||
|
setBadge(statusCell, st);
|
||||||
|
syncButtonsForRow(tr, st);
|
||||||
|
} else {
|
||||||
|
// ale jeśli working „utknął”, to podtrzymujemy blokady
|
||||||
|
syncButtonsForRow(tr, 'working');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch { /* cicho */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshActiveRows() {
|
||||||
|
if (activeSids.size === 0) return;
|
||||||
|
const table = document.getElementById('vm-admin');
|
||||||
|
if (!table) return;
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
for (const sid of Array.from(activeSids)) {
|
||||||
|
const tr = tbody.querySelector(`tr[data-sid="${sid}"]`);
|
||||||
|
if (!tr) { activeSids.delete(sid); continue; }
|
||||||
|
await refreshOneRow(sid, tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshOneRow(sid, tr) {
|
||||||
|
try {
|
||||||
|
const detail = await api.vmDetail(sid);
|
||||||
|
const nodeCell = tr.children[3];
|
||||||
|
const statusCell = tr.children[4];
|
||||||
|
const sel = tr.querySelector('.target-node');
|
||||||
|
|
||||||
|
// status z detail
|
||||||
|
const st = String(detail?.current?.status || detail?.current?.qmpstatus || detail?.status || '').trim();
|
||||||
|
if (st) {
|
||||||
|
setBadge(statusCell, st);
|
||||||
|
syncButtonsForRow(tr, st);
|
||||||
|
// jeśli task się skończył, zdejmij z listy aktywnych
|
||||||
|
if (/running|stopped|shutdown|locked|error|failed|unknown|offline/.test(low(st))) {
|
||||||
|
activeSids.delete(sid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// node z detail
|
||||||
|
const newNode = String(detail?.node || detail?.meta?.node || '').trim();
|
||||||
|
if (newNode && nodeCell?.textContent?.trim() !== newNode) {
|
||||||
|
nodeCell.textContent = newNode;
|
||||||
|
rebuildTargetSelect(sel, newNode, cachedNodes);
|
||||||
|
}
|
||||||
|
} catch { /* no-op */ }
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user