diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 37b9951..f2b15cf 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,87 +1,46 @@ import React, { useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import { - clearToken, createInstance, createTemplate, deactivateTemplate, - deleteInstance, getMe, getToken, listInstances, listTemplates, login, register, + clearToken, createInstance, createTemplate, deactivateTemplate, deleteInstance, + getInstanceStats, getMe, getToken, instanceAction, listInstances, listTemplates, + login, register, } from "./api"; import "./styles.css"; const emptyTemplate = { name: "", description: "", guest_type: "vm", source_vmid: "", source_template: "", cores: 1, memory_mb: 1024, disk_gb: 10 }; +function formatBytes(value) { + if (!value) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + let number = value; let index = 0; + while (number >= 1024 && index < units.length - 1) { number /= 1024; index += 1; } + return `${number.toFixed(index > 1 ? 1 : 0)} ${units[index]}`; +} + function Auth({ onLogin }) { - const [mode, setMode] = useState("login"); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [message, setMessage] = useState(""); - async function submit(event) { - event.preventDefault(); - try { - if (mode === "register") { await register(email, password); setMessage("Регистрация завершена. Теперь войдите."); setMode("login"); return; } - await login(email, password); onLogin(await getMe()); - } catch (error) { setMessage(error.message); } - } + const [mode, setMode] = useState("login"); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [message, setMessage] = useState(""); + async function submit(event) { event.preventDefault(); try { if (mode === "register") { await register(email, password); setMessage("Регистрация завершена. Теперь войдите."); setMode("login"); return; } await login(email, password); onLogin(await getMe()); } catch (error) { setMessage(error.message); } } return

Proxmox VPS Panel

{mode === "login" ? "Вход" : "Регистрация"}

{message &&

{message}

}
; } +function InstanceCard({ item, onChanged, onDeleted, locked }) { + const [stats, setStats] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); + async function loadStats() { try { setStats(await getInstanceStats(item.id)); } catch (e) { setError(e.message); } } + useEffect(() => { loadStats(); const timer = setInterval(loadStats, 10000); return () => clearInterval(timer); }, [item.id]); + async function action(action) { setBusy(true); setError(""); try { await instanceAction(item.id, action); await onChanged(); await loadStats(); } catch (e) { setError(e.message); } finally { setBusy(false); } } + async function remove() { if (!window.confirm(`Удалить ${item.name} (VMID ${item.vmid})?`)) return; setBusy(true); try { await deleteInstance(item.id); await onDeleted(); } catch (e) { setError(e.message); setBusy(false); } } + return
{item.name}{item.guest_type.toUpperCase()} · VMID {item.vmid}{stats?.status || item.status} · {item.node}{stats &&
CPU: {stats.cpu_pct}%RAM: {formatBytes(stats.memory_used_bytes)} / {formatBytes(stats.memory_total_bytes)}Диск: {formatBytes(stats.disk_used_bytes)} / {formatBytes(stats.disk_total_bytes)}
}
{(stats?.status !== "running") && }{stats?.status === "running" && }{}
{error && {error}}
; +} + function App() { - const [user, setUser] = useState(null); - const [message, setMessage] = useState(""); - const [templates, setTemplates] = useState([]); - const [instances, setInstances] = useState([]); - const [templateForm, setTemplateForm] = useState(emptyTemplate); - const [instanceForm, setInstanceForm] = useState({ name: "", template_id: "", username: "paneluser", password: "" }); - const [creating, setCreating] = useState(false); - const [deletingId, setDeletingId] = useState(null); - - async function refresh() { - const [templateData, instanceData] = await Promise.all([listTemplates(), listInstances()]); - setTemplates(templateData); setInstances(instanceData); - setInstanceForm((current) => ({ ...current, template_id: current.template_id || String(templateData[0]?.id || "") })); - } + const [user, setUser] = useState(null); const [message, setMessage] = useState(""); const [templates, setTemplates] = useState([]); const [instances, setInstances] = useState([]); const [creating, setCreating] = useState(false); const [templateForm, setTemplateForm] = useState(emptyTemplate); const [instanceForm, setInstanceForm] = useState({ name: "", template_id: "", username: "paneluser", password: "" }); + async function refresh() { const [ts, ins] = await Promise.all([listTemplates(), listInstances()]); setTemplates(ts); setInstances(ins); setInstanceForm((current) => ({ ...current, template_id: current.template_id || String(ts[0]?.id || "") })); } useEffect(() => { if (!getToken()) return; Promise.all([getMe(), refresh()]).then(([current]) => setUser(current)).catch(() => clearToken()); }, []); - - async function submitTemplate(event) { - event.preventDefault(); - try { - await createTemplate({ ...templateForm, source_vmid: templateForm.guest_type === "vm" ? Number(templateForm.source_vmid) : null, source_template: templateForm.guest_type === "lxc" ? templateForm.source_template : null, cores: Number(templateForm.cores), memory_mb: Number(templateForm.memory_mb), disk_gb: Number(templateForm.disk_gb) }); - setTemplateForm(emptyTemplate); setMessage("Шаблон создан."); await refresh(); - } catch (error) { setMessage(error.message); } - } - - async function submitInstance(event) { - event.preventDefault(); - if (creating) return; - setCreating(true); setMessage("Создание VPS выполняется. Не нажимайте кнопку повторно..."); - try { - const result = await createInstance({ ...instanceForm, template_id: Number(instanceForm.template_id) }); - setMessage(`Инстанс создан: VMID ${result.vmid}. Пароль: ${result.initial_password}`); - setInstanceForm((current) => ({ ...current, name: "", password: "" })); - await refresh(); - } catch (error) { setMessage(error.message); } - finally { setCreating(false); } - } - - async function removeInstance(item) { - if (deletingId || creating) return; - if (!window.confirm(`Удалить ${item.name} (VMID ${item.vmid}) из Proxmox? Это действие необратимо.`)) return; - setDeletingId(item.id); setMessage(`Удаление ${item.name} выполняется...`); - try { await deleteInstance(item.id); setMessage("Инстанс удалён."); await refresh(); } - catch (error) { setMessage(error.message); } - finally { setDeletingId(null); } - } - - async function deactivate(id) { - if (!window.confirm("Деактивировать шаблон?")) return; - try { await deactivateTemplate(id); await refresh(); } catch (error) { setMessage(error.message); } - } - + async function submitInstance(event) { event.preventDefault(); if (creating) return; setCreating(true); setMessage("Создание VPS выполняется. Повторные клики заблокированы..."); try { const result = await createInstance({ ...instanceForm, template_id: Number(instanceForm.template_id) }); setMessage(`Создан VMID ${result.vmid}. Пароль: ${result.initial_password}`); setInstanceForm((v) => ({ ...v, name: "", password: "" })); await refresh(); } catch (e) { setMessage(e.message); } finally { setCreating(false); } } + async function submitTemplate(event) { event.preventDefault(); try { await createTemplate({ ...templateForm, source_vmid: templateForm.guest_type === "vm" ? Number(templateForm.source_vmid) : null, source_template: templateForm.guest_type === "lxc" ? templateForm.source_template : null, cores: Number(templateForm.cores), memory_mb: Number(templateForm.memory_mb), disk_gb: Number(templateForm.disk_gb) }); setTemplateForm(emptyTemplate); setMessage("Шаблон создан."); await refresh(); } catch (e) { setMessage(e.message); } } + async function deactivate(id) { if (!window.confirm("Деактивировать шаблон?")) return; try { await deactivateTemplate(id); await refresh(); } catch (e) { setMessage(e.message); } } if (!user) return ; - return
Proxmox VPS Panel{user.email} · {user.role}
{message &&

{message}

} -

Мои инстансы

{instances.length === 0 ?

Инстансов пока нет.

:
{instances.map((item) =>
{item.name}{item.guest_type.toUpperCase()} · VMID {item.vmid ?? "не назначен"}{item.status} · {item.node}{item.vmid && }
)}
}
-

Доступные шаблоны

{templates.length === 0 ?

Шаблонов пока нет.

:
{templates.map((item) =>
{item.name}{item.guest_type.toUpperCase()} · {item.cores} CPU · {item.memory_mb} MB · {item.disk_gb} GB{user.role === "admin" && }
)}
}
-

Создать VPS

Операция может занимать несколько минут. Повторная отправка заблокирована до завершения операции.

- {user.role === "admin" &&

Создать шаблон

{templateForm.guest_type === "vm" ? : }
} -
; + return
Proxmox VPS Panel{user.email} · {user.role}
{message &&

{message}

}

Мои инстансы

{instances.length === 0 ?

Инстансов пока нет.

:
{instances.map((item) => )}
}

Доступные шаблоны

{templates.length === 0 ?

Шаблонов пока нет.

:
{templates.map((item) =>
{item.name}{item.guest_type.toUpperCase()} · {item.cores} CPU · {item.memory_mb} MB · {item.disk_gb} GB{user.role === "admin" && }
)}
}

Создать VPS

Повторная отправка заблокирована до завершения операции.

{user.role === "admin" &&

Создать шаблон

{templateForm.guest_type === "vm" ? : }
}
; } createRoot(document.getElementById("root")).render();