Откат выбора архивного LXC-шаблона к предыдущей версии

This commit is contained in:
2026-08-09 19:22:42 +03:00
parent 4647e26973
commit db04d6b9c3
+27 -68
View File
@@ -20,29 +20,20 @@ export default function AdminTemplates() {
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [importing, setImporting] = useState(null); const [importing, setImporting] = useState(null);
const vmTemplates = proxmoxTemplates?.vm_templates || []; async function refresh() { setTemplates(await api.listTemplates()); }
const lxcTemplates = proxmoxTemplates?.lxc_templates || [];
async function refresh() { async function fetchProxmox() {
setTemplates(await api.listTemplates());
}
async function findTemplates() {
setError(""); setError("");
setBusy(true); setBusy(true);
try { try { setProxmoxTemplates(await api.fetchProxmoxTemplates()); }
setProxmoxTemplates(await api.fetchProxmoxTemplates()); catch (err) { setError(err.message); }
} catch (err) { finally { setBusy(false); }
setError(err.message);
} finally {
setBusy(false);
}
} }
async function importVm(vm) { async function importVm(vm) {
const key = `vm-${vm.vmid}`; const key = `vm-${vm.vmid}`;
setImporting(key);
setError(""); setError("");
setImporting(key);
try { try {
await api.createTemplate({ await api.createTemplate({
name: vm.name || `VM-${vm.vmid}`, name: vm.name || `VM-${vm.vmid}`,
@@ -54,17 +45,14 @@ export default function AdminTemplates() {
disk_gb: vm.disk_gb || 10, disk_gb: vm.disk_gb || 10,
}); });
await refresh(); await refresh();
} catch (err) { } catch (err) { setError(err.message); }
setError(err.message); finally { setImporting(null); }
} finally {
setImporting(null);
}
} }
async function importLxc(archive) { async function importLxc(archive) {
const key = `lxc-${archive.volid}`; const key = `lxc-${archive.volid}`;
setImporting(key);
setError(""); setError("");
setImporting(key);
try { try {
await api.createTemplate({ await api.createTemplate({
name: archive.name, name: archive.name,
@@ -76,26 +64,17 @@ export default function AdminTemplates() {
disk_gb: 8, disk_gb: 8,
}); });
await refresh(); await refresh();
} catch (err) { } catch (err) { setError(err.message); }
setError(err.message); finally { setImporting(null); }
} finally {
setImporting(null);
}
} }
async function deleteTemplate(id) { async function deleteTemplate(id) {
if (!confirm("Удалить шаблон?")) return; if (!confirm("Удалить шаблон?")) return;
try { try { await api.deleteTemplate(id); await refresh(); }
await api.deleteTemplate(id); catch (err) { setError(err.message); }
await refresh();
} catch (err) {
setError(err.message);
}
} }
useEffect(() => { useEffect(() => { refresh().catch((err) => setError(err.message)); }, []);
refresh().catch((err) => setError(err.message));
}, []);
function set(field, value) { function set(field, value) {
setForm((current) => ({ ...current, [field]: value })); setForm((current) => ({ ...current, [field]: value }));
@@ -116,13 +95,13 @@ export default function AdminTemplates() {
}); });
setForm(empty); setForm(empty);
await refresh(); await refresh();
} catch (err) { } catch (err) { setError(err.message); }
setError(err.message); finally { setBusy(false); }
} 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>
@@ -130,7 +109,7 @@ export default function AdminTemplates() {
<div className="card template-discovery"> <div className="card template-discovery">
<h3>🔍 Найти шаблоны на Proxmox</h3> <h3>🔍 Найти шаблоны на Proxmox</h3>
<button className="btn btn-primary" onClick={findTemplates} disabled={busy}> <button className="btn btn-primary" onClick={fetchProxmox} disabled={busy}>
{busy ? "Ищем…" : "Найти шаблоны"} {busy ? "Ищем…" : "Найти шаблоны"}
</button> </button>
@@ -143,12 +122,10 @@ export default function AdminTemplates() {
<thead><tr><th>VMID</th><th>Имя</th><th>vCPU</th><th>RAM</th><th /></tr></thead> <thead><tr><th>VMID</th><th>Имя</th><th>vCPU</th><th>RAM</th><th /></tr></thead>
<tbody>{vmTemplates.map((vm) => { <tbody>{vmTemplates.map((vm) => {
const key = `vm-${vm.vmid}`; const key = `vm-${vm.vmid}`;
return ( return <tr key={key}>
<tr key={key}>
<td>{vm.vmid}</td><td>{vm.name}</td><td>{vm.cores}</td><td>{vm.memory_mb} МБ</td> <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> <td><button className="btn btn-primary" disabled={importing === key} onClick={() => importVm(vm)}>{importing === key ? "Импорт…" : "Импорт"}</button></td>
</tr> </tr>;
);
})}</tbody> })}</tbody>
</table> </table>
) : <p className="muted">VM-шаблоны не найдены.</p>} ) : <p className="muted">VM-шаблоны не найдены.</p>}
@@ -161,12 +138,10 @@ export default function AdminTemplates() {
<thead><tr><th>Имя</th><th>Хранилище</th><th>Размер</th><th /></tr></thead> <thead><tr><th>Имя</th><th>Хранилище</th><th>Размер</th><th /></tr></thead>
<tbody>{lxcTemplates.map((archive) => { <tbody>{lxcTemplates.map((archive) => {
const key = `lxc-${archive.volid}`; const key = `lxc-${archive.volid}`;
return ( return <tr key={key}>
<tr key={key}>
<td>{archive.name}</td><td>{archive.storage || "—"}</td><td>{archive.size_mb || 0} МБ</td> <td>{archive.name}</td><td>{archive.storage || "—"}</td><td>{archive.size_mb || 0} МБ</td>
<td><button className="btn btn-primary" disabled={importing === key} onClick={() => importLxc(archive)}>{importing === key ? "Импорт…" : "Импорт"}</button></td> <td><button className="btn btn-primary" disabled={importing === key} onClick={() => importLxc(archive)}>{importing === key ? "Импорт…" : "Импорт"}</button></td>
</tr> </tr>;
);
})}</tbody> })}</tbody>
</table> </table>
) : <p className="muted">Архивные LXC-шаблоны не найдены.</p>} ) : <p className="muted">Архивные LXC-шаблоны не найдены.</p>}
@@ -180,25 +155,11 @@ export default function AdminTemplates() {
<div className="template-form-grid"> <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" ? ( {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>VMID шаблона</label><input type="number" value={form.source_vmid} onChange={(e) => set("source_vmid", e.target.value)} placeholder="999" required /></div>
) : ( ) : (
<div className="field"> <div className="field"><label>Архив LXC-шаблона</label><input value={form.source_template} onChange={(e) => set("source_template", e.target.value)} placeholder="local:vztmpl/..." required /></div>
<label>Архив LXC-шаблона</label>
<select value={form.source_template} onChange={(e) => set("source_template", e.target.value)} required>
<option value="" disabled>
{lxcTemplates.length ? "Выберите архивный шаблон" : "Сначала нажмите «Найти шаблоны»"}
</option>
{lxcTemplates.map((archive) => (
<option key={archive.volid} value={archive.volid}>
{archive.name} · {archive.storage} · {archive.size_mb} МБ
</option>
))}
</select>
</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>
@@ -210,12 +171,10 @@ export default function AdminTemplates() {
<h3>📋 Активные шаблоны</h3> <h3>📋 Активные шаблоны</h3>
<table className="data-table"> <table className="data-table">
<thead><tr><th>Название</th><th>Тип</th><th>vCPU</th><th>RAM</th><th>Диск</th><th /></tr></thead> <thead><tr><th>Название</th><th>Тип</th><th>vCPU</th><th>RAM</th><th>Диск</th><th /></tr></thead>
<tbody>{templates.map((template) => ( <tbody>{templates.map((template) => <tr key={template.id}>
<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>{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> <td><button className="btn btn-danger" onClick={() => deleteTemplate(template.id)}>Удалить</button></td>
</tr> </tr>)}</tbody>
))}</tbody>
</table> </table>
</div> </div>
); );