Переделан frontend под структуру старой панели VPS

This commit is contained in:
2026-08-09 17:11:13 +03:00
parent cf83780286
commit bd22f073e1
+20 -18
View File
@@ -5,29 +5,31 @@ import TemplateManager from "./TemplateManager.jsx";
import "./styles.css";
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 <main className="page"><section className="card auth-card"><h1>Proxmox VPS Panel</h1><h2>{mode === "login" ? "Вход" : "Регистрация"}</h2><form onSubmit={submit}><label>Email<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required /></label><label>Пароль<input type="password" minLength={12} value={password} onChange={(e) => setPassword(e.target.value)} required /></label><button type="submit">{mode === "login" ? "Войти" : "Зарегистрироваться"}</button></form><button className="link" type="button" onClick={() => setMode(mode === "login" ? "register" : "login")}>{mode === "login" ? "Создать аккаунт" : "У меня уже есть аккаунт"}</button>{message && <p className="message">{message}</p>}</section></main>;
const [registerMode, setRegisterMode] = useState(false); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState("");
async function submit(event) { event.preventDefault(); try { if (registerMode) { await register(email, password); setRegisterMode(false); setError("Регистрация завершена. Войдите в панель."); return; } await login(email, password); onLogin(await getMe()); } catch (e) { setError(e.message); } }
return <main className="auth-page"><section className="auth-box"><div className="brand"><span className="brand-mark">VP</span><strong>VPS Panel</strong></div><h1>{registerMode ? "Регистрация" : "Вход в панель"}</h1><form onSubmit={submit}><label>Email<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required /></label><label>Пароль<input type="password" minLength={12} value={password} onChange={(e) => setPassword(e.target.value)} required /></label><button className="primary wide">{registerMode ? "Зарегистрироваться" : "Войти"}</button></form>{error && <p className="alert">{error}</p>}<button className="text-button" onClick={() => { setRegisterMode(!registerMode); setError(""); }}>{registerMode ? "Уже есть аккаунт" : "Создать аккаунт"}</button></section></main>;
}
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 }) {
function InstanceCard({ item, onRefresh }) {
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(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 <div className="item instance-item"><strong>{item.name}</strong><span>{item.guest_type.toUpperCase()} · VMID {item.vmid}</span><span className="muted">{stats?.status || item.status} · {item.node}</span>{stats && <div className="stats"><span>CPU: {stats.cpu_pct}%</span><span>RAM: {formatBytes(stats.memory_used_bytes)} / {formatBytes(stats.memory_total_bytes)}</span></div>}<div className="actions">{stats?.status !== "running" && <button disabled={busy || locked} onClick={() => action("start")}>Старт</button>}{stats?.status === "running" && <button disabled={busy || locked} onClick={() => action("shutdown")}>Стоп</button>}<button disabled={busy || locked} onClick={() => action("reboot")}>Перезапуск</button><button className="danger" disabled={busy || locked} onClick={remove}>{busy ? "Выполняется..." : "Удалить"}</button></div>{error && <span className="error-text">{error}</span>}</div>;
async function load() { try { setStats(await getInstanceStats(item.id)); } catch (e) { setError(e.message); } }
useEffect(() => { load(); const timer = setInterval(load, 10000); return () => clearInterval(timer); }, [item.id]);
async function action(name) { setBusy(true); try { await instanceAction(item.id, name); await onRefresh(); await load(); } 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 onRefresh(); } catch (e) { setError(e.message); setBusy(false); } }
const running = stats?.status === "running" || item.status === "running";
return <article className="vps-card"><div className="vps-status"><span className={running ? "dot green" : "dot gray"}></span>{running ? "работает" : item.status}<span className="badge">{item.guest_type.toUpperCase()}</span></div><h3>{item.name}</h3><div className="vps-meta">vmid {item.vmid} · node {item.node} · {stats ? `CPU ${stats.cpu_pct}%` : "обновление..."}</div>{stats && <div className="stats-line"><span>RAM: {Math.round((stats.memory_used_bytes || 0) / 1024 / 1024)} МБ</span><span>из {Math.round((stats.memory_total_bytes || 0) / 1024 / 1024)} МБ</span></div>}<div className="actions"><button disabled={busy || running} onClick={() => action("start")}>Старт</button><button disabled={busy || !running} onClick={() => action("shutdown")}>Стоп</button><button disabled={busy} onClick={() => action("reboot")}>Reboot</button><button className="danger-light" disabled={busy} onClick={remove}>Удалить</button></div>{error && <small className="error-text">{error}</small>}</article>;
}
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 [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 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 <Auth onLogin={setUser} />;
return <main className="page dashboard"><header className="topbar"><div><strong>Proxmox VPS Panel</strong><span className="muted">{user.email} · {user.role}</span></div><button onClick={() => { clearToken(); setUser(null); }}>Выйти</button></header>{message && <p className="message">{message}</p>}<section className="card"><h2>Мои инстансы</h2>{instances.length ? <div className="items">{instances.map((item) => <InstanceCard key={item.id} item={item} locked={creating} onChanged={refresh} />)}</div> : <p className="muted">Инстансов пока нет.</p>}</section><section className="card"><h2>Создать VPS</h2><form onSubmit={submit}><label>Имя VPS<input pattern="[a-zA-Z0-9][a-zA-Z0-9-]*" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} required /></label><label>Шаблон<select value={form.template_id} onChange={(e) => setForm({ ...form, template_id: e.target.value })} required><option value="">Выберите шаблон</option>{templates.map((item) => <option key={item.id} value={item.id}>{item.name} ({item.guest_type.toUpperCase()})</option>)}</select></label><div className="form-row"><label>Пользователь<input value={form.username} onChange={(e) => setForm({ ...form, username: e.target.value })} required /></label><label>Пароль<input type="password" minLength={12} value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} required /></label></div><button type="submit" disabled={!templates.length || creating}>{creating ? "Создание VPS..." : "Создать VPS"}</button></form></section>{user.role === "admin" && <TemplateManager />}</main>;
function Dashboard({ user, onNavigate }) {
const [instances, setInstances] = useState([]); const [templates, setTemplates] = useState([]); const [form, setForm] = useState({ name: "", template_id: "", username: "paneluser", password: "" }); const [busy, setBusy] = useState(false); const [message, setMessage] = useState("");
async function refresh() { const [ts, ins] = await Promise.all([listTemplates(), listInstances()]); setTemplates(ts); setInstances(ins); setForm((v) => ({ ...v, template_id: v.template_id || String(ts[0]?.id || "") })); }
useEffect(() => { refresh().catch((e) => setMessage(e.message)); }, []);
async function create(event) { event.preventDefault(); if (busy) return; setBusy(true); setMessage("Создание VPS выполняется..."); try { const result = await createInstance({ ...form, template_id: Number(form.template_id) }); setMessage(`VPS создан, VMID ${result.vmid}. Пароль: ${result.initial_password}`); setForm({ ...form, name: "", password: "" }); await refresh(); } catch (e) { setMessage(e.message); } finally { setBusy(false); } }
return <div className="content"><div className="page-heading"><div><h1>Мои VPS</h1><p>{instances.length} из {instances.length} запущено</p></div><button className="primary" onClick={() => document.getElementById("new-vps").scrollIntoView({ behavior: "smooth" })}>+ Новый VPS</button></div><div className="summary"><div><span>ВСЕГО</span><strong>{instances.length}</strong></div><div><span>ЗАПУЩЕНО</span><strong>{instances.filter((i) => i.status === "running").length}</strong></div><div><span>ОСТАНОВЛЕНО</span><strong>{instances.filter((i) => i.status !== "running").length}</strong></div></div>{message && <p className="alert">{message}</p>}<div className="vps-list">{instances.map((item) => <InstanceCard key={item.id} item={item} onRefresh={refresh} />)}</div><section id="new-vps" className="panel-card"><h2>Новый VPS</h2><form className="new-vps-form" onSubmit={create}><label>Название<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="ubuntu-server" required /></label><label>Шаблон<select value={form.template_id} onChange={(e) => setForm({ ...form, template_id: e.target.value })} required><option value="">Выберите шаблон</option>{templates.map((t) => <option key={t.id} value={t.id}>{t.name} ({t.guest_type.toUpperCase()})</option>)}</select></label><label>Пользователь<input value={form.username} onChange={(e) => setForm({ ...form, username: e.target.value })} required /></label><label>Пароль<input type="password" minLength={12} value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} required /></label><button className="primary" disabled={busy || !templates.length}>{busy ? "Создание..." : "Создать VPS"}</button></form></section></div>;
}
function Layout({ user, onLogout }) { const [page, setPage] = useState("vps"); return <div className="app-layout"><aside className="sidebar"><div className="brand"><span className="brand-mark">VP</span><strong>VPS Panel</strong></div><nav><button className={page === "vps" ? "nav-item active" : "nav-item"} onClick={() => setPage("vps")}> <span>Мои VPS</span></button>{user.role === "admin" && <button className={page === "templates" ? "nav-item active" : "nav-item"} onClick={() => setPage("templates")}> <span>Шаблоны</span></button>}{user.role === "admin" && <button className="nav-item" onClick={() => alert("Раздел пользователей будет добавлен следующим этапом")}> <span>Пользователи</span></button>}</nav><div className="sidebar-bottom"><div className="account"><span className="avatar">{user.email[0].toUpperCase()}</span><div>{user.email}<small>Администратор</small></div></div><button className="logout" onClick={onLogout}> Выйти</button></div></aside><main className="main-area">{page === "vps" ? <Dashboard user={user} /> : <div className="content"><TemplateManager /></div>}</main></div>; }
function App() { const [user, setUser] = useState(null); useEffect(() => { if (getToken()) getMe().then(setUser).catch(clearToken); }, []); if (!user) return <Auth onLogin={setUser} />; return <Layout user={user} onLogout={() => { clearToken(); setUser(null); }} />; }
createRoot(document.getElementById("root")).render(<React.StrictMode><App /></React.StrictMode>);