Исправлен импорт LXC-шаблонов и улучшен экран шаблонов

This commit is contained in:
2026-08-09 18:04:22 +03:00
parent b9233d50fb
commit 21f878db7b
+184 -45
View File
@@ -1,7 +1,16 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { api } from "../api.js"; import { api } from "../api.js";
const empty = { name: "", description: "", guest_type: "vm", source_vmid: "", source_template: "", cores: 1, memory_mb: 1024, disk_gb: 10 }; const empty = {
name: "",
description: "",
guest_type: "vm",
source_vmid: "",
source_template: "",
cores: 1,
memory_mb: 1024,
disk_gb: 10,
};
export default function AdminTemplates() { export default function AdminTemplates() {
const [templates, setTemplates] = useState([]); const [templates, setTemplates] = useState([]);
@@ -9,66 +18,196 @@ export default function AdminTemplates() {
const [form, setForm] = useState(empty); const [form, setForm] = useState(empty);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [importing, setImporting] = useState(null);
async function refresh() { setTemplates(await api.listTemplates()); } async function refresh() {
async function fetchProxmox() { setBusy(true); try { setProxmoxTemplates(await api.fetchProxmoxTemplates()); } catch (err) { setError(err.message); } finally { setBusy(false); } } setTemplates(await api.listTemplates());
async function importVm(vm) { await api.createTemplate({ name: vm.name || "VM-"+vm.vmid, guest_type: "vm", source_vmid: vm.vmid, cores: vm.cores, memory_mb: vm.memory_mb, disk_gb: vm.disk_gb || 10 }); await refresh(); } }
async function importLxc(lxc) { await api.createTemplate({ name: lxc.name, guest_type: "lxc", source_template: lxc.volid, cores: 1, memory_mb: 1024, disk_gb: 8 }); await refresh(); }
async function deleteTemplate(id) { if (!confirm("Удалить шаблон?")) return; await api.deleteTemplate(id); await refresh(); }
useEffect(() => { refresh(); }, []); async function fetchProxmox() {
function set(f, v) { setForm(fr => ({ ...fr, [f]: v })); } setError("");
setBusy(true);
try {
setProxmoxTemplates(await api.fetchProxmoxTemplates());
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
}
async function onSubmit(e) { e.preventDefault(); setError(""); setBusy(true); try { await api.createTemplate({ ...form, cores: Number(form.cores), memory_mb: Number(form.memory_mb), disk_gb: Number(form.disk_gb), source_vmid: form.source_vmid ? Number(form.source_vmid) : null, source_template: form.source_template || null }); setForm(empty); await refresh(); } catch (err) { setError(err.message); } finally { setBusy(false); } } async function importVm(vm) {
setError("");
setImporting(`vm-${vm.vmid}`);
try {
await api.createTemplate({
name: vm.name || `VM-${vm.vmid}`,
guest_type: "vm",
source_vmid: Number(vm.vmid),
source_template: null,
cores: vm.cores || 1,
memory_mb: vm.memory_mb || 1024,
disk_gb: vm.disk_gb || 10,
});
await refresh();
} catch (err) {
setError(err.message);
} finally {
setImporting(null);
}
}
async function importLxc(lxc) {
setError("");
const key = lxc.vmid ? `lxc-vmid-${lxc.vmid}` : `lxc-${lxc.volid}`;
setImporting(key);
try {
await api.createTemplate({
name: lxc.name || (lxc.vmid ? `LXC-${lxc.vmid}` : "LXC"),
guest_type: "lxc",
source_vmid: lxc.vmid ? Number(lxc.vmid) : null,
source_template: lxc.vmid ? null : lxc.volid,
cores: lxc.cores || 1,
memory_mb: lxc.memory_mb || 1024,
disk_gb: lxc.disk_gb || 8,
});
await refresh();
} catch (err) {
setError(err.message);
} finally {
setImporting(null);
}
}
async function deleteTemplate(id) {
if (!confirm("Удалить шаблон?")) return;
try {
await api.deleteTemplate(id);
await refresh();
} catch (err) {
setError(err.message);
}
}
useEffect(() => {
refresh().catch((err) => setError(err.message));
}, []);
function set(field, value) {
setForm((current) => ({ ...current, [field]: value }));
}
async function onSubmit(event) {
event.preventDefault();
setError("");
setBusy(true);
try {
await api.createTemplate({
...form,
cores: Number(form.cores),
memory_mb: Number(form.memory_mb),
disk_gb: Number(form.disk_gb),
source_vmid: form.source_vmid ? Number(form.source_vmid) : null,
source_template: form.source_template || null,
});
setForm(empty);
await refresh();
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
}
const vmTemplates = proxmoxTemplates?.vm_templates || [];
const lxcTemplates = proxmoxTemplates?.lxc_templates || [];
return ( return (
<div className="container"> <div className="container">
<h1 className="page-title">Шаблоны VPS</h1> <h1 className="page-title">Шаблоны VPS</h1>
{error && <div className="error-box">{error}</div>} {error && <div className="error-box">{error}</div>}
<div className="card" style={{ marginBottom: 24 }}> <div className="card template-discovery">
<h3>🔍 Найти шаблоны на Proxmox</h3> <h3>🔍 Найти шаблоны на Proxmox</h3>
<button className="btn btn-primary" onClick={fetchProxmox} disabled={busy}>{busy ? "Ищем…" : "Найти шаблоны"}</button> <button className="btn btn-primary" onClick={fetchProxmox} disabled={busy}>
{busy ? "Ищем…" : "Найти шаблоны"}
</button>
{proxmoxTemplates && ( {proxmoxTemplates && (
<div style={{ marginTop: 16 }}> <div className="template-results">
{proxmoxTemplates.vm_templates?.length > 0 && ( <section>
<div style={{ marginBottom: 16 }}><h4>🖥 VM-шаблоны</h4> <h4>🖥 VM-шаблоны</h4>
<table className="data-table"><thead><tr><th>VMID</th><th>Имя</th><th>vCPU</th><th>RAM</th><th></th></tr></thead> {vmTemplates.length > 0 ? (
<tbody>{proxmoxTemplates.vm_templates.map(vm => ( <table className="data-table">
<tr key={vm.vmid}><td>{vm.vmid}</td><td>{vm.name}</td><td>{vm.cores}</td><td>{vm.memory_mb} МБ</td> <thead><tr><th>VMID</th><th>Имя</th><th>vCPU</th><th>RAM</th><th /></tr></thead>
<td><button className="btn btn-primary" onClick={() => importVm(vm)}>Импорт</button></td></tr>))} <tbody>
</tbody></table></div>)} {vmTemplates.map((vm) => {
{proxmoxTemplates.lxc_templates?.length > 0 && ( const key = `vm-${vm.vmid}`;
<div><h4>📦 LXC-шаблоны</h4> return (
<table className="data-table"><thead><tr><th>Имя</th><th>Хранилище</th><th>Размер</th><th></th></tr></thead> <tr key={key}>
<tbody>{proxmoxTemplates.lxc_templates.map((lxc,i) => ( <td>{vm.vmid}</td><td>{vm.name}</td><td>{vm.cores}</td><td>{vm.memory_mb} МБ</td>
<tr key={i}><td>{lxc.name}</td><td>{lxc.storage}</td><td>{lxc.size_mb} МБ</td> <td><button className="btn btn-primary" disabled={importing === key} onClick={() => importVm(vm)}>{importing === key ? "Импорт…" : "Импорт"}</button></td>
<td><button className="btn btn-primary" onClick={() => importLxc(lxc)}>Импорт</button></td></tr>))} </tr>
</tbody></table></div>)} );
{(!proxmoxTemplates.vm_templates?.length && !proxmoxTemplates.lxc_templates?.length) && <p>Шаблоны не найдены.</p>} })}
</div>)} </tbody>
</table>
) : <p className="muted">VM-шаблоны не найдены.</p>}
</section>
<section>
<h4>📦 LXC-шаблоны</h4>
{lxcTemplates.length > 0 ? (
<table className="data-table">
<thead><tr><th>VMID</th><th>Имя</th><th>Источник</th><th>vCPU</th><th>RAM</th><th /></tr></thead>
<tbody>
{lxcTemplates.map((lxc, index) => {
const key = lxc.vmid ? `lxc-vmid-${lxc.vmid}` : `lxc-${lxc.volid || index}`;
const source = lxc.vmid ? "LXC-контейнер" : `${lxc.storage || "—"}: vzdump`;
return (
<tr key={key}>
<td>{lxc.vmid || "—"}</td>
<td>{lxc.name}</td>
<td>{source}</td>
<td>{lxc.cores || 1}</td>
<td>{lxc.memory_mb || `${lxc.size_mb || 0} МБ`}</td>
<td><button className="btn btn-primary" disabled={importing === key} onClick={() => importLxc(lxc)}>{importing === key ? "Импорт…" : "Импорт"}</button></td>
</tr>
);
})}
</tbody>
</table>
) : <p className="muted">LXC-шаблоны не найдены.</p>}
</section>
</div>
)}
</div> </div>
<form onSubmit={onSubmit} className="card" style={{ marginBottom: 32 }}> <form onSubmit={onSubmit} className="card template-form">
<h3> Добавить вручную</h3> <h3> Добавить вручную</h3>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}> <div className="template-form-grid">
<div className="field"><label>Название</label><input value={form.name} onChange={e => set("name", e.target.value)} required /></div> <div className="field"><label>Название</label><input value={form.name} onChange={(e) => set("name", e.target.value)} required /></div>
<div className="field"><label>Тип</label><select value={form.guest_type} onChange={e => set("guest_type", e.target.value)}><option value="vm">VM</option><option value="lxc">LXC</option></select></div> <div className="field"><label>Тип</label><select value={form.guest_type} onChange={(e) => set("guest_type", e.target.value)}><option value="vm">VM</option><option value="lxc">LXC</option></select></div>
{form.guest_type === "vm" ? <div className="field"><label>VMID шаблона</label><input type="number" value={form.source_vmid} onChange={e => set("source_vmid", e.target.value)} placeholder="999" required /></div> {form.guest_type === "vm" ? <div className="field"><label>VMID шаблона</label><input type="number" value={form.source_vmid} onChange={(e) => set("source_vmid", e.target.value)} placeholder="999" required /></div>
: <div className="field"><label>CT-шаблон (volid)</label><input value={form.source_template} onChange={e => set("source_template", e.target.value)} placeholder="local:vztmpl/..." required /></div>} : <div className="field"><label>CT-шаблон (volid)</label><input value={form.source_template} onChange={(e) => set("source_template", e.target.value)} placeholder="local:vztmpl/..." required /> </div>}
<div className="field"><label>Описание</label><input value={form.description} onChange={e => set("description", e.target.value)} /></div> <div className="field"><label>Описание</label><input value={form.description} onChange={(e) => set("description", e.target.value)} /></div>
<div className="field"><label>vCPU</label><input type="number" min={1} value={form.cores} onChange={e => set("cores", e.target.value)} /></div> <div className="field"><label>vCPU</label><input type="number" min={1} value={form.cores} onChange={(e) => set("cores", e.target.value)} /></div>
<div className="field"><label>RAM, МБ</label><input type="number" min={256} step={256} value={form.memory_mb} onChange={e => set("memory_mb", e.target.value)} /></div> <div className="field"><label>RAM, МБ</label><input type="number" min={256} step={256} value={form.memory_mb} onChange={(e) => set("memory_mb", e.target.value)} /></div>
<div className="field"><label>Диск, ГБ</label><input type="number" min={1} value={form.disk_gb} onChange={e => set("disk_gb", e.target.value)} /></div> <div className="field"><label>Диск, ГБ</label><input type="number" min={1} value={form.disk_gb} onChange={(e) => set("disk_gb", e.target.value)} /></div>
</div> </div>
<button className="btn btn-primary" disabled={busy} type="submit">Добавить шаблон</button> <button className="btn btn-primary" disabled={busy} type="submit">{busy ? "Сохраняем…" : "Добавить шаблон"}</button>
</form> </form>
<h3>📋 Активные шаблоны</h3> <h3>📋 Активные шаблоны</h3>
<table className="data-table"><thead><tr><th>Название</th><th>Тип</th><th>vCPU</th><th>RAM</th><th>Диск</th><th></th></tr></thead> <table className="data-table">
<tbody>{templates.map(t => ( <thead><tr><th>Название</th><th>Тип</th><th>vCPU</th><th>RAM</th><th>Диск</th><th /></tr></thead>
<tr key={t.id}><td>{t.name}</td><td>{t.guest_type==="vm"?"VM":"LXC"}</td><td>{t.cores}</td><td>{t.memory_mb} МБ</td><td>{t.disk_gb} ГБ</td> <tbody>{templates.map((template) => (
<td><button className="btn btn-danger" onClick={() => deleteTemplate(t.id)}>Удалить</button></td></tr>))} <tr key={template.id}>
</tbody></table> <td>{template.name}</td><td>{template.guest_type === "vm" ? "VM" : "LXC"}</td><td>{template.cores}</td><td>{template.memory_mb} МБ</td><td>{template.disk_gb} ГБ</td>
</div>); <td><button className="btn btn-danger" onClick={() => deleteTemplate(template.id)}>Удалить</button></td>
</tr>
))}</tbody>
</table>
</div>
);
} }