Откат выбора архивного LXC-шаблона к предыдущей версии
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user