Archived
Добавлена форма создания VPS в кабинете
This commit is contained in:
+87
-152
@@ -1,56 +1,24 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import {
|
import {
|
||||||
clearToken,
|
clearToken, createInstance, createTemplate, deactivateTemplate,
|
||||||
createTemplate,
|
getMe, getToken, listInstances, listTemplates, login, register,
|
||||||
deactivateTemplate,
|
|
||||||
getMe,
|
|
||||||
getToken,
|
|
||||||
listInstances,
|
|
||||||
listTemplates,
|
|
||||||
login,
|
|
||||||
register,
|
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
function App() {
|
const emptyTemplate = {
|
||||||
|
name: "", description: "", guest_type: "vm", source_vmid: "",
|
||||||
|
source_template: "", cores: 1, memory_mb: 1024, disk_gb: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
function Auth({ onLogin }) {
|
||||||
const [mode, setMode] = useState("login");
|
const [mode, setMode] = useState("login");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [message, setMessage] = 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() {
|
async function submit(event) {
|
||||||
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();
|
event.preventDefault();
|
||||||
setMessage("");
|
|
||||||
try {
|
try {
|
||||||
if (mode === "register") {
|
if (mode === "register") {
|
||||||
await register(email, password);
|
await register(email, password);
|
||||||
@@ -59,136 +27,103 @@ function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await login(email, password);
|
await login(email, password);
|
||||||
const currentUser = await getMe();
|
onLogin(await getMe());
|
||||||
setUser(currentUser);
|
|
||||||
await loadPanel();
|
|
||||||
setMessage(`Вы вошли как ${currentUser.role}.`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage(error.message);
|
setMessage(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCreateTemplate(event) {
|
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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: "" });
|
||||||
|
|
||||||
|
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 || "") }));
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!getToken()) return;
|
||||||
|
Promise.all([getMe(), refresh()]).then(([current]) => setUser(current)).catch(() => clearToken());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function submitTemplate(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setMessage("");
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
await createTemplate({
|
||||||
...templateForm,
|
...templateForm,
|
||||||
cores: Number(templateForm.cores),
|
source_vmid: templateForm.guest_type === "vm" ? Number(templateForm.source_vmid) : null,
|
||||||
memory_mb: Number(templateForm.memory_mb),
|
source_template: templateForm.guest_type === "lxc" ? templateForm.source_template : null,
|
||||||
disk_gb: Number(templateForm.disk_gb),
|
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)
|
setTemplateForm(emptyTemplate);
|
||||||
: null,
|
|
||||||
source_template: templateForm.guest_type === "lxc"
|
|
||||||
? templateForm.source_template
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
await createTemplate(payload);
|
|
||||||
setMessage("Шаблон создан.");
|
setMessage("Шаблон создан.");
|
||||||
setTemplateForm({ ...templateForm, name: "", description: "" });
|
await refresh();
|
||||||
await loadPanel();
|
} catch (error) { setMessage(error.message); }
|
||||||
} catch (error) {
|
|
||||||
setMessage(error.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeactivate(id) {
|
async function submitInstance(event) {
|
||||||
if (!window.confirm("Деактивировать этот шаблон?")) return;
|
event.preventDefault();
|
||||||
try {
|
try {
|
||||||
await deactivateTemplate(id);
|
const result = await createInstance({ ...instanceForm, template_id: Number(instanceForm.template_id) });
|
||||||
await loadPanel();
|
setMessage(`Инстанс создан: VMID ${result.vmid}. Пароль покажите пользователю и больше нигде не сохраняйте: ${result.initial_password}`);
|
||||||
setMessage("Шаблон деактивирован.");
|
setInstanceForm((current) => ({ ...current, name: "", password: "" }));
|
||||||
} catch (error) {
|
await refresh();
|
||||||
setMessage(error.message);
|
} catch (error) { setMessage(error.message); }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
async function deactivate(id) {
|
||||||
clearToken();
|
if (!window.confirm("Деактивировать шаблон?")) return;
|
||||||
setUser(null);
|
try { await deactivateTemplate(id); await refresh(); } catch (error) { setMessage(error.message); }
|
||||||
setTemplates([]);
|
|
||||||
setInstances([]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) return <Auth onLogin={setUser} />;
|
||||||
return (
|
|
||||||
<main className="page">
|
|
||||||
<section className="card auth-card">
|
|
||||||
<h1>Proxmox VPS Panel</h1>
|
|
||||||
<h2>{mode === "login" ? "Вход" : "Регистрация"}</h2>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<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 (
|
return <main className="page dashboard">
|
||||||
<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>
|
||||||
<header className="topbar">
|
{message && <p className="message">{message}</p>}
|
||||||
<div><strong>Proxmox VPS Panel</strong><span className="muted">{user.email} · {user.role}</span></div>
|
<section className="grid">
|
||||||
<button type="button" onClick={logout}>Выйти</button>
|
<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>
|
||||||
</header>
|
<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" onClick={() => deactivate(item.id)}>Деактивировать</button>}</div>)}</div>}</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
{message && <p className="message">{message}</p>}
|
<section className="card"><h2>Создать VPS</h2><p className="muted">Операция может занимать несколько минут. Пароль будет показан только в ответе создания.</p><form onSubmit={submitInstance}>
|
||||||
|
<label>Имя VPS<input pattern="[a-zA-Z0-9][a-zA-Z0-9-]*" value={instanceForm.name} onChange={(e) => setInstanceForm({ ...instanceForm, name: e.target.value })} required /></label>
|
||||||
|
<label>Шаблон<select value={instanceForm.template_id} onChange={(e) => setInstanceForm({ ...instanceForm, 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={instanceForm.username} onChange={(e) => setInstanceForm({ ...instanceForm, username: e.target.value })} required /></label><label>Пароль<input type="password" minLength={12} value={instanceForm.password} onChange={(e) => setInstanceForm({ ...instanceForm, password: e.target.value })} required /></label></div>
|
||||||
|
<button type="submit" disabled={!templates.length}>Создать VPS</button>
|
||||||
|
</form></section>
|
||||||
|
|
||||||
<section className="grid">
|
{user.role === "admin" && <section className="card"><h2>Создать шаблон</h2><form onSubmit={submitTemplate}>
|
||||||
<article className="card">
|
<label>Имя<input value={templateForm.name} onChange={(e) => setTemplateForm({ ...templateForm, name: e.target.value })} required /></label>
|
||||||
<h2>Мои инстансы</h2>
|
<label>Описание<input value={templateForm.description} onChange={(e) => setTemplateForm({ ...templateForm, description: e.target.value })} /></label>
|
||||||
{instances.length === 0 ? <p className="muted">Инстансов пока нет.</p> : (
|
<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>
|
||||||
<div className="items">{instances.map((item) => (
|
{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="item" key={item.id}>
|
<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>
|
||||||
<strong>{item.name}</strong>
|
<button type="submit">Создать шаблон</button>
|
||||||
<span>{item.guest_type.toUpperCase()} · VMID {item.vmid ?? "не назначен"}</span>
|
</form></section>}
|
||||||
<span className="muted">{item.status} · {item.node}</span>
|
</main>;
|
||||||
</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>);
|
||||||
|
|||||||
Reference in New Issue
Block a user