diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
index f2b15cf..adc9bef 100644
--- a/frontend/src/main.jsx
+++ b/frontend/src/main.jsx
@@ -1,46 +1,33 @@
import React, { useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
-import {
- clearToken, createInstance, createTemplate, deactivateTemplate, deleteInstance,
- getInstanceStats, getMe, getToken, instanceAction, listInstances, listTemplates,
- login, register,
-} from "./api";
+import { clearToken, createInstance, getInstanceStats, getMe, getToken, instanceAction, listInstances, listTemplates, login, register, deleteInstance } from "./api";
+import TemplateManager from "./TemplateManager.jsx";
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); } }
return Proxmox VPS Panel
{mode === "login" ? "Вход" : "Регистрация"}
{message && {message}
};
}
-function InstanceCard({ item, onChanged, onDeleted, locked }) {
+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 InstanceCard({ item, locked, onChanged }) {
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}}
;
+ async function action(name) { setBusy(true); setError(""); try { await instanceAction(item.id, name); 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 onChanged(); } 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)}
}
{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 [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 || "") })); }
+ const [user, setUser] = useState(null); const [message, setMessage] = useState(""); const [templates, setTemplates] = useState([]); const [instances, setInstances] = useState([]); const [creating, setCreating] = useState(false); const [form, setForm] = useState({ name: "", template_id: "", username: "paneluser", password: "" });
+ async function refresh() { const [ts, ins] = await Promise.all([listTemplates(), listInstances()]); setTemplates(ts); setInstances(ins); setForm((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 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); } }
+ async function submit(event) { event.preventDefault(); if (creating) return; setCreating(true); setMessage("Создание VPS выполняется. Повторные клики заблокированы..."); try { const result = await createInstance({ ...form, template_id: Number(form.template_id) }); setMessage(`Создан VMID ${result.vmid}. Пароль: ${result.initial_password}`); setForm((v) => ({ ...v, name: "", password: "" })); await refresh(); } catch (e) { setMessage(e.message); } finally { setCreating(false); } }
if (!user) return ;
- 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" && };
+ return {message && {message}
}Мои инстансы
{instances.length ? {instances.map((item) => )}
: Инстансов пока нет.
}{user.role === "admin" && };
}
createRoot(document.getElementById("root")).render();