Исправлен импорт LXC-шаблонов и улучшен экран шаблонов
This commit is contained in:
@@ -1,7 +1,16 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
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() {
|
||||
const [templates, setTemplates] = useState([]);
|
||||
@@ -9,66 +18,196 @@ export default function AdminTemplates() {
|
||||
const [form, setForm] = useState(empty);
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [importing, setImporting] = useState(null);
|
||||
|
||||
async function refresh() { setTemplates(await api.listTemplates()); }
|
||||
async function fetchProxmox() { setBusy(true); try { setProxmoxTemplates(await api.fetchProxmoxTemplates()); } catch (err) { setError(err.message); } finally { setBusy(false); } }
|
||||
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(); }
|
||||
async function refresh() {
|
||||
setTemplates(await api.listTemplates());
|
||||
}
|
||||
|
||||
useEffect(() => { refresh(); }, []);
|
||||
function set(f, v) { setForm(fr => ({ ...fr, [f]: v })); }
|
||||
async function fetchProxmox() {
|
||||
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 (
|
||||
<div className="container">
|
||||
<h1 className="page-title">Шаблоны VPS</h1>
|
||||
{error && <div className="error-box">{error}</div>}
|
||||
|
||||
<div className="card" style={{ marginBottom: 24 }}>
|
||||
<div className="card template-discovery">
|
||||
<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 && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
{proxmoxTemplates.vm_templates?.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}><h4>🖥 VM-шаблоны</h4>
|
||||
<table className="data-table"><thead><tr><th>VMID</th><th>Имя</th><th>vCPU</th><th>RAM</th><th></th></tr></thead>
|
||||
<tbody>{proxmoxTemplates.vm_templates.map(vm => (
|
||||
<tr key={vm.vmid}><td>{vm.vmid}</td><td>{vm.name}</td><td>{vm.cores}</td><td>{vm.memory_mb} МБ</td>
|
||||
<td><button className="btn btn-primary" onClick={() => importVm(vm)}>Импорт</button></td></tr>))}
|
||||
</tbody></table></div>)}
|
||||
{proxmoxTemplates.lxc_templates?.length > 0 && (
|
||||
<div><h4>📦 LXC-шаблоны</h4>
|
||||
<table className="data-table"><thead><tr><th>Имя</th><th>Хранилище</th><th>Размер</th><th></th></tr></thead>
|
||||
<tbody>{proxmoxTemplates.lxc_templates.map((lxc,i) => (
|
||||
<tr key={i}><td>{lxc.name}</td><td>{lxc.storage}</td><td>{lxc.size_mb} МБ</td>
|
||||
<td><button className="btn btn-primary" onClick={() => importLxc(lxc)}>Импорт</button></td></tr>))}
|
||||
</tbody></table></div>)}
|
||||
{(!proxmoxTemplates.vm_templates?.length && !proxmoxTemplates.lxc_templates?.length) && <p>Шаблоны не найдены.</p>}
|
||||
</div>)}
|
||||
<div className="template-results">
|
||||
<section>
|
||||
<h4>🖥 VM-шаблоны</h4>
|
||||
{vmTemplates.length > 0 ? (
|
||||
<table className="data-table">
|
||||
<thead><tr><th>VMID</th><th>Имя</th><th>vCPU</th><th>RAM</th><th /></tr></thead>
|
||||
<tbody>
|
||||
{vmTemplates.map((vm) => {
|
||||
const key = `vm-${vm.vmid}`;
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>{vm.vmid}</td><td>{vm.name}</td><td>{vm.cores}</td><td>{vm.memory_mb} МБ</td>
|
||||
<td><button className="btn btn-primary" disabled={importing === key} onClick={() => importVm(vm)}>{importing === key ? "Импорт…" : "Импорт"}</button></td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
|
||||
<form onSubmit={onSubmit} className="card" style={{ marginBottom: 32 }}>
|
||||
<form onSubmit={onSubmit} className="card template-form">
|
||||
<h3>➕ Добавить вручную</h3>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
|
||||
<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>
|
||||
{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>Описание</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>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="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><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>
|
||||
: <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>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>Диск, ГБ</label><input type="number" min={1} value={form.disk_gb} onChange={(e) => set("disk_gb", e.target.value)} /></div>
|
||||
</div>
|
||||
<button className="btn btn-primary" disabled={busy} type="submit">Добавить шаблон</button>
|
||||
<button className="btn btn-primary" disabled={busy} type="submit">{busy ? "Сохраняем…" : "Добавить шаблон"}</button>
|
||||
</form>
|
||||
|
||||
<h3>📋 Активные шаблоны</h3>
|
||||
<table className="data-table"><thead><tr><th>Название</th><th>Тип</th><th>vCPU</th><th>RAM</th><th>Диск</th><th></th></tr></thead>
|
||||
<tbody>{templates.map(t => (
|
||||
<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>
|
||||
<td><button className="btn btn-danger" onClick={() => deleteTemplate(t.id)}>Удалить</button></td></tr>))}
|
||||
</tbody></table>
|
||||
</div>);
|
||||
<table className="data-table">
|
||||
<thead><tr><th>Название</th><th>Тип</th><th>vCPU</th><th>RAM</th><th>Диск</th><th /></tr></thead>
|
||||
<tbody>{templates.map((template) => (
|
||||
<tr key={template.id}>
|
||||
<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>
|
||||
<td><button className="btn btn-danger" onClick={() => deleteTemplate(template.id)}>Удалить</button></td>
|
||||
</tr>
|
||||
))}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user