first commit
This commit is contained in:
357
static/js/app.js
Normal file
357
static/js/app.js
Normal file
@@ -0,0 +1,357 @@
|
||||
const form = document.getElementById('createForm');
|
||||
const nodeEl = document.getElementById('node');
|
||||
const storageEl = document.getElementById('storage');
|
||||
const templateEl = document.getElementById('template');
|
||||
const vmidEl = document.getElementById('vmid');
|
||||
const ipModeEl = document.getElementById('ip_mode');
|
||||
const refreshVmidBtn = document.getElementById('refreshVmid');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const taskProgressBar = document.getElementById('taskProgressBar');
|
||||
const taskStage = document.getElementById('taskStage');
|
||||
const taskLogs = document.getElementById('taskLogs');
|
||||
const containerDetails = document.getElementById('containerDetails');
|
||||
const globalAlert = document.getElementById('globalAlert');
|
||||
|
||||
let taskPollTimer = null;
|
||||
|
||||
function showAlert(message, level = 'danger') {
|
||||
globalAlert.innerHTML = `<div class="alert alert-${level} py-2">${message}</div>`;
|
||||
}
|
||||
|
||||
function clearAlert() {
|
||||
globalAlert.innerHTML = '';
|
||||
}
|
||||
|
||||
function setFieldError(name, message = '') {
|
||||
const target = document.querySelector(`[data-error-for="${name}"]`);
|
||||
const field = form.querySelector(`[name="${name}"]`);
|
||||
if (target) target.textContent = message || '';
|
||||
if (field) field.classList.toggle('is-invalid', Boolean(message));
|
||||
}
|
||||
|
||||
function clearErrors() {
|
||||
form.querySelectorAll('.is-invalid').forEach((el) => el.classList.remove('is-invalid'));
|
||||
form.querySelectorAll('[data-error-for]').forEach((el) => {
|
||||
el.textContent = '';
|
||||
});
|
||||
}
|
||||
|
||||
async function parseResponse(response, url) {
|
||||
const text = await response.text();
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (e) {
|
||||
throw new Error(`Server nevrátil JSON pro ${url}.`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function apiGet(url) {
|
||||
const response = await fetch(url, {
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
const data = await parseResponse(response, url);
|
||||
|
||||
if (!response.ok || data.success === false) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function apiPost(url, payload) {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await parseResponse(response, url);
|
||||
|
||||
if (!response.ok || data.success === false) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function option(text, value) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = value;
|
||||
opt.textContent = text;
|
||||
return opt;
|
||||
}
|
||||
|
||||
async function loadNodes() {
|
||||
const data = await apiGet('/api/nodes');
|
||||
nodeEl.innerHTML = '';
|
||||
|
||||
data.nodes.forEach((node) => {
|
||||
nodeEl.appendChild(option(node, node));
|
||||
});
|
||||
|
||||
if (window.APP_DEFAULTS?.defaultNode && data.nodes.includes(window.APP_DEFAULTS.defaultNode)) {
|
||||
nodeEl.value = window.APP_DEFAULTS.defaultNode;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStorage() {
|
||||
if (!nodeEl.value) {
|
||||
storageEl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await apiGet(`/api/storage/${encodeURIComponent(nodeEl.value)}`);
|
||||
storageEl.innerHTML = '';
|
||||
|
||||
data.storage.forEach((item) => {
|
||||
storageEl.appendChild(option(item, item));
|
||||
});
|
||||
|
||||
if (window.APP_DEFAULTS?.defaultStorage && data.storage.includes(window.APP_DEFAULTS.defaultStorage)) {
|
||||
storageEl.value = window.APP_DEFAULTS.defaultStorage;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
templateEl.innerHTML = '';
|
||||
|
||||
if (!nodeEl.value || !storageEl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await apiGet(
|
||||
`/api/templates/${encodeURIComponent(nodeEl.value)}/${encodeURIComponent(storageEl.value)}`
|
||||
);
|
||||
|
||||
data.templates.forEach((item) => {
|
||||
templateEl.appendChild(option(item, item));
|
||||
});
|
||||
}
|
||||
|
||||
async function loadNextId() {
|
||||
const data = await apiGet('/api/next-vmid');
|
||||
vmidEl.value = data.vmid;
|
||||
}
|
||||
|
||||
function toggleStaticIpFields() {
|
||||
const staticOnly = document.querySelectorAll('.static-ip-only');
|
||||
const isStatic = ipModeEl.value === 'static';
|
||||
staticOnly.forEach((el) => el.classList.toggle('d-none', !isStatic));
|
||||
}
|
||||
|
||||
function collectFormData() {
|
||||
return {
|
||||
node: nodeEl.value,
|
||||
storage: storageEl.value,
|
||||
template: templateEl.value,
|
||||
vmid: vmidEl.value,
|
||||
hostname: document.getElementById('hostname').value,
|
||||
bridge: document.getElementById('bridge').value,
|
||||
cores: document.getElementById('cores').value,
|
||||
memory: document.getElementById('memory').value,
|
||||
disk: document.getElementById('disk_gb').value,
|
||||
ip_mode: document.getElementById('ip_mode').value,
|
||||
ip_last_octet: document.getElementById('ip_host').value,
|
||||
vlan: document.getElementById('vlan_tag').value,
|
||||
gateway: document.getElementById('gateway').value,
|
||||
nameserver: document.getElementById('dns').value,
|
||||
searchdomain: document.getElementById('searchdomain').value,
|
||||
password: document.getElementById('password').value,
|
||||
start_on_boot: document.getElementById('onboot').checked,
|
||||
start_now: document.getElementById('start_now').checked,
|
||||
unprivileged: document.getElementById('unprivileged').checked,
|
||||
enable_nesting: document.getElementById('enable_nesting').checked,
|
||||
};
|
||||
}
|
||||
|
||||
function renderContainerDetails(container) {
|
||||
containerDetails.innerHTML = `
|
||||
<dl class="row small mb-0">
|
||||
<dt class="col-5">Hostname</dt><dd class="col-7">${container.hostname || '-'}</dd>
|
||||
<dt class="col-5">VMID</dt><dd class="col-7">${container.vmid || '-'}</dd>
|
||||
<dt class="col-5">Node</dt><dd class="col-7">${container.node || '-'}</dd>
|
||||
<dt class="col-5">Stav</dt><dd class="col-7">${container.status || '-'}</dd>
|
||||
<dt class="col-5">CPU</dt><dd class="col-7">${container.cores || '-'}</dd>
|
||||
<dt class="col-5">RAM</dt><dd class="col-7">${container.memory || '-'} MB</dd>
|
||||
<dt class="col-5">RootFS</dt><dd class="col-7 text-break">${container.rootfs || '-'}</dd>
|
||||
<dt class="col-5">Síť</dt><dd class="col-7 text-break">${container.net0 || '-'}</dd>
|
||||
<dt class="col-5">DNS</dt><dd class="col-7">${container.nameserver || '-'}</dd>
|
||||
<dt class="col-5">Search domain</dt><dd class="col-7">${container.searchdomain || '-'}</dd>
|
||||
<dt class="col-5">Template</dt><dd class="col-7 text-break">${container.ostemplate || '-'}</dd>
|
||||
</dl>
|
||||
`;
|
||||
}
|
||||
|
||||
function updateProgress(progress, text, logs = [], variant = 'primary') {
|
||||
taskProgressBar.style.width = `${progress}%`;
|
||||
taskProgressBar.textContent = `${progress}%`;
|
||||
taskProgressBar.className = `progress-bar ${progress < 100 ? 'progress-bar-striped progress-bar-animated' : ''} bg-${variant}`.trim();
|
||||
taskStage.textContent = text;
|
||||
taskLogs.textContent = logs.length ? logs.join('\n') : '-';
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (taskPollTimer) {
|
||||
clearTimeout(taskPollTimer);
|
||||
taskPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadContainerDetails(node, vmid) {
|
||||
const data = await apiGet(`/api/container-details/${encodeURIComponent(node)}/${encodeURIComponent(vmid)}`);
|
||||
if (data.details) {
|
||||
renderContainerDetails(data.details);
|
||||
}
|
||||
}
|
||||
|
||||
async function startContainerNow(node, vmid) {
|
||||
return apiPost('/api/start-lxc', { node, vmid });
|
||||
}
|
||||
|
||||
async function pollTask(node, upid) {
|
||||
const data = await apiGet(
|
||||
`/api/task-status?node=${encodeURIComponent(node)}&upid=${encodeURIComponent(upid)}`
|
||||
);
|
||||
|
||||
const failed = !data.running && data.exitstatus && data.exitstatus !== 'OK';
|
||||
|
||||
let stageText = 'Úloha běží...';
|
||||
if (!data.running && data.exitstatus === 'OK') {
|
||||
stageText = 'Úloha byla úspěšně dokončena.';
|
||||
} else if (!data.running && data.exitstatus && data.exitstatus !== 'OK') {
|
||||
stageText = `Úloha skončila chybou: ${data.exitstatus}`;
|
||||
}
|
||||
|
||||
updateProgress(
|
||||
data.progress ?? 0,
|
||||
stageText,
|
||||
data.logs || [],
|
||||
failed ? 'danger' : (data.progress === 100 && data.exitstatus === 'OK' ? 'success' : 'primary')
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function waitForTask(node, upid) {
|
||||
while (true) {
|
||||
const data = await pollTask(node, upid);
|
||||
|
||||
if (!data.running) {
|
||||
return data;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => {
|
||||
taskPollTimer = setTimeout(resolve, 2000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
nodeEl.addEventListener('change', async () => {
|
||||
clearAlert();
|
||||
try {
|
||||
await loadStorage();
|
||||
await loadTemplates();
|
||||
} catch (error) {
|
||||
showAlert(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
storageEl.addEventListener('change', async () => {
|
||||
clearAlert();
|
||||
try {
|
||||
await loadTemplates();
|
||||
} catch (error) {
|
||||
showAlert(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
ipModeEl.addEventListener('change', toggleStaticIpFields);
|
||||
|
||||
refreshVmidBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await loadNextId();
|
||||
} catch (error) {
|
||||
showAlert(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('reset', () => {
|
||||
stopPolling();
|
||||
clearErrors();
|
||||
clearAlert();
|
||||
|
||||
setTimeout(() => {
|
||||
toggleStaticIpFields();
|
||||
updateProgress(0, 'Ještě nebyla spuštěna žádná úloha.', []);
|
||||
containerDetails.textContent = 'Po úspěšném vytvoření se tady zobrazí detaily.';
|
||||
}, 0);
|
||||
});
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
stopPolling();
|
||||
clearErrors();
|
||||
clearAlert();
|
||||
submitBtn.disabled = true;
|
||||
containerDetails.textContent = 'Čekám na dokončení vytváření kontejneru.';
|
||||
updateProgress(8, 'Odesílám požadavek do Proxmoxu...', []);
|
||||
|
||||
try {
|
||||
const payload = collectFormData();
|
||||
const startNow = payload.start_now;
|
||||
|
||||
const createData = await apiPost('/api/create-lxc', payload);
|
||||
|
||||
showAlert(`Úloha byla spuštěna. VMID ${createData.vmid}, hostname ${createData.hostname}.`, 'info');
|
||||
updateProgress(12, 'Vytváření kontejneru bylo přijato Proxmoxem.', []);
|
||||
|
||||
const createTaskResult = await waitForTask(createData.node, createData.upid);
|
||||
|
||||
if (createTaskResult.exitstatus !== 'OK') {
|
||||
submitBtn.disabled = false;
|
||||
showAlert(`Vytvoření kontejneru selhalo: ${createTaskResult.exitstatus || 'neznámá chyba'}`, 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
if (startNow) {
|
||||
updateProgress(90, 'Spouštím nově vytvořený kontejner...', createTaskResult.logs || []);
|
||||
const startData = await startContainerNow(createData.node, createData.vmid);
|
||||
const startTaskResult = await waitForTask(startData.node, startData.upid);
|
||||
|
||||
if (startTaskResult.exitstatus !== 'OK') {
|
||||
submitBtn.disabled = false;
|
||||
showAlert(`Kontejner byl vytvořen, ale nepodařilo se ho spustit: ${startTaskResult.exitstatus || 'neznámá chyba'}`, 'warning');
|
||||
await loadContainerDetails(createData.node, createData.vmid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(100, 'Kontejner byl úspěšně vytvořen.', [], 'success');
|
||||
showAlert(`Kontejner VMID ${createData.vmid} byl úspěšně vytvořen.`, 'success');
|
||||
await loadContainerDetails(createData.node, createData.vmid);
|
||||
submitBtn.disabled = false;
|
||||
} catch (error) {
|
||||
submitBtn.disabled = false;
|
||||
showAlert(error.message);
|
||||
updateProgress(100, `Požadavek selhal: ${error.message}`, [], 'danger');
|
||||
}
|
||||
});
|
||||
|
||||
(async function init() {
|
||||
toggleStaticIpFields();
|
||||
|
||||
try {
|
||||
await loadNodes();
|
||||
await loadStorage();
|
||||
await loadTemplates();
|
||||
await loadNextId();
|
||||
} catch (error) {
|
||||
showAlert(error.message);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user