Добавлен frontend кабинета шаблонов и инстансов

This commit is contained in:
2026-08-09 14:43:09 +03:00
parent 6bcc36ea97
commit 39aee24fdc
+151 -26
View File
@@ -1,6 +1,16 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import { getMe, login, register } from "./api";
import {
clearToken,
createTemplate,
deactivateTemplate,
getMe,
getToken,
listInstances,
listTemplates,
login,
register,
} from "./api";
import "./styles.css";
function App() {
@@ -9,6 +19,34 @@ function App() {
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
const [user, setUser] = useState(null);
const [templates, setTemplates] = useState([]);
const [instances, setInstances] = useState([]);
const [templateForm, setTemplateForm] = useState({
name: "",
description: "",
guest_type: "vm",
source_vmid: "",
source_template: "",
cores: 1,
memory_mb: 1024,
disk_gb: 10,
});
async function loadPanel() {
const [templateData, instanceData] = await Promise.all([
listTemplates(),
listInstances(),
]);
setTemplates(templateData);
setInstances(instanceData);
}
useEffect(() => {
if (!getToken()) return;
Promise.all([getMe(), loadPanel()]).then(([currentUser]) => {
setUser(currentUser);
}).catch(() => clearToken());
}, []);
async function handleSubmit(event) {
event.preventDefault();
@@ -20,50 +58,137 @@ function App() {
setMode("login");
return;
}
const tokenData = await login(email, password);
const currentUser = await getMe(tokenData.access_token);
await login(email, password);
const currentUser = await getMe();
setUser(currentUser);
await loadPanel();
setMessage(`Вы вошли как ${currentUser.role}.`);
} catch (error) {
setMessage(error.message);
}
}
async function handleCreateTemplate(event) {
event.preventDefault();
setMessage("");
try {
const payload = {
...templateForm,
cores: Number(templateForm.cores),
memory_mb: Number(templateForm.memory_mb),
disk_gb: Number(templateForm.disk_gb),
source_vmid: templateForm.guest_type === "vm"
? Number(templateForm.source_vmid)
: null,
source_template: templateForm.guest_type === "lxc"
? templateForm.source_template
: null,
};
await createTemplate(payload);
setMessage("Шаблон создан.");
setTemplateForm({ ...templateForm, name: "", description: "" });
await loadPanel();
} catch (error) {
setMessage(error.message);
}
}
async function handleDeactivate(id) {
if (!window.confirm("Деактивировать этот шаблон?")) return;
try {
await deactivateTemplate(id);
await loadPanel();
setMessage("Шаблон деактивирован.");
} catch (error) {
setMessage(error.message);
}
}
function logout() {
clearToken();
setUser(null);
setTemplates([]);
setInstances([]);
}
if (!user) {
return (
<main className="page">
<section className="card">
<section className="card auth-card">
<h1>Proxmox VPS Panel</h1>
{user ? (
<>
<p>Добро пожаловать, {user.email}</p>
<p className="muted">Роль: {user.role}</p>
<button type="button" onClick={() => setUser(null)}>Выйти</button>
</>
) : (
<>
<h2>{mode === "login" ? "Вход" : "Регистрация"}</h2>
<form onSubmit={handleSubmit}>
<label>
Email
<input type="email" value={email} onChange={(event) => setEmail(event.target.value)} required />
</label>
<label>
Пароль
<input type="password" minLength={12} value={password} onChange={(event) => setPassword(event.target.value)} required />
</label>
<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>
);
}
return (
<main className="page dashboard">
<header className="topbar">
<div><strong>Proxmox VPS Panel</strong><span className="muted">{user.email} · {user.role}</span></div>
<button type="button" onClick={logout}>Выйти</button>
</header>
{message && <p className="message">{message}</p>}
<section className="grid">
<article className="card">
<h2>Мои инстансы</h2>
{instances.length === 0 ? <p className="muted">Инстансов пока нет.</p> : (
<div className="items">{instances.map((item) => (
<div className="item" key={item.id}>
<strong>{item.name}</strong>
<span>{item.guest_type.toUpperCase()} · VMID {item.vmid ?? "не назначен"}</span>
<span className="muted">{item.status} · {item.node}</span>
</div>
))}</div>
)}
</article>
<article className="card">
<h2>Доступные шаблоны</h2>
{templates.length === 0 ? <p className="muted">Шаблонов пока нет.</p> : (
<div className="items">{templates.map((item) => (
<div className="item" key={item.id}>
<strong>{item.name}</strong>
<span>{item.guest_type.toUpperCase()} · {item.cores} CPU · {item.memory_mb} MB · {item.disk_gb} GB</span>
{user.role === "admin" && <button className="danger" type="button" onClick={() => handleDeactivate(item.id)}>Деактивировать</button>}
</div>
))}</div>
)}
</article>
</section>
{user.role === "admin" && <section className="card admin-form">
<h2>Создать шаблон</h2>
<form onSubmit={handleCreateTemplate}>
<label>Имя<input value={templateForm.name} onChange={(e) => setTemplateForm({ ...templateForm, name: e.target.value })} required /></label>
<label>Описание<input value={templateForm.description} onChange={(e) => setTemplateForm({ ...templateForm, description: e.target.value })} /></label>
<label>Тип<select value={templateForm.guest_type} onChange={(e) => setTemplateForm({ ...templateForm, guest_type: e.target.value })}><option value="vm">VM</option><option value="lxc">LXC</option></select></label>
{templateForm.guest_type === "vm" ? (
<label>Source VMID<input type="number" min="1" value={templateForm.source_vmid} onChange={(e) => setTemplateForm({ ...templateForm, source_vmid: e.target.value })} required /></label>
) : (
<label>CT template volume<input placeholder="local:vztmpl/debian.tar.zst" value={templateForm.source_template} onChange={(e) => setTemplateForm({ ...templateForm, source_template: e.target.value })} required /></label>
)}
<div className="form-row">
<label>CPU<input type="number" min="1" max="64" value={templateForm.cores} onChange={(e) => setTemplateForm({ ...templateForm, cores: e.target.value })} /></label>
<label>RAM MB<input type="number" min="128" value={templateForm.memory_mb} onChange={(e) => setTemplateForm({ ...templateForm, memory_mb: e.target.value })} /></label>
<label>Диск GB<input type="number" min="1" value={templateForm.disk_gb} onChange={(e) => setTemplateForm({ ...templateForm, disk_gb: e.target.value })} /></label>
</div>
<button type="submit">Создать шаблон</button>
</form>
</section>}
</main>
);
}
createRoot(document.getElementById("root")).render(
<React.StrictMode><App /></React.StrictMode>,
);
createRoot(document.getElementById("root")).render(<React.StrictMode><App /></React.StrictMode>);