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 {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" && }
- ;
+ return {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" && };
}
createRoot(document.getElementById("root")).render();