Archived
Добавлен интерфейс поиска и импорта шаблонов Proxmox
This commit is contained in:
@@ -0,0 +1,125 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
createTemplate,
|
||||||
|
deactivateTemplate,
|
||||||
|
listProxmoxTemplates,
|
||||||
|
listTemplates,
|
||||||
|
} from "./api";
|
||||||
|
|
||||||
|
const empty = {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
guest_type: "vm",
|
||||||
|
source_vmid: "",
|
||||||
|
source_template: "",
|
||||||
|
cores: 1,
|
||||||
|
memory_mb: 1024,
|
||||||
|
disk_gb: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TemplateManager() {
|
||||||
|
const [templates, setTemplates] = useState([]);
|
||||||
|
const [discovered, setDiscovered] = useState(null);
|
||||||
|
const [form, setForm] = useState(empty);
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
setTemplates(await listTemplates());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findTemplates() {
|
||||||
|
setBusy(true);
|
||||||
|
setMessage("Ищем шаблоны на Proxmox...");
|
||||||
|
try {
|
||||||
|
setDiscovered(await listProxmoxTemplates());
|
||||||
|
setMessage("Шаблоны Proxmox найдены.");
|
||||||
|
} catch (error) {
|
||||||
|
setMessage(error.message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importTemplate(payload) {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await createTemplate(payload);
|
||||||
|
await refresh();
|
||||||
|
setMessage("Шаблон импортирован в каталог панели.");
|
||||||
|
} catch (error) {
|
||||||
|
setMessage(error.message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await createTemplate({
|
||||||
|
...form,
|
||||||
|
source_vmid: form.source_vmid ? Number(form.source_vmid) : null,
|
||||||
|
source_template: form.source_template || null,
|
||||||
|
cores: Number(form.cores),
|
||||||
|
memory_mb: Number(form.memory_mb),
|
||||||
|
disk_gb: Number(form.disk_gb),
|
||||||
|
});
|
||||||
|
setForm(empty);
|
||||||
|
await refresh();
|
||||||
|
setMessage("Шаблон создан.");
|
||||||
|
} catch (error) {
|
||||||
|
setMessage(error.message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(id) {
|
||||||
|
if (!window.confirm("Деактивировать шаблон?")) return;
|
||||||
|
await deactivateTemplate(id);
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => { refresh().catch((error) => setMessage(error.message)); }, []);
|
||||||
|
|
||||||
|
return <section className="card template-manager">
|
||||||
|
<h2>Шаблоны VPS</h2>
|
||||||
|
{message && <p className="message">{message}</p>}
|
||||||
|
|
||||||
|
<h3>Найти шаблоны на Proxmox</h3>
|
||||||
|
<button type="button" onClick={findTemplates} disabled={busy}>
|
||||||
|
{busy ? "Ищем..." : "Найти шаблоны"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{discovered && <div className="discovered">
|
||||||
|
<h3>VM-шаблоны</h3>
|
||||||
|
{discovered.vm_templates?.map((vm) => <div className="item" key={`vm-${vm.vmid}`}>
|
||||||
|
<strong>{vm.vmid} — {vm.name}</strong>
|
||||||
|
<span>{vm.cores} vCPU · {vm.memory_mb} MB</span>
|
||||||
|
<button type="button" disabled={busy} onClick={() => importTemplate({ name: vm.name || `VM-${vm.vmid}`, guest_type: "vm", source_vmid: vm.vmid, cores: vm.cores, memory_mb: vm.memory_mb, disk_gb: 10 })}>Импортировать</button>
|
||||||
|
</div>)}
|
||||||
|
|
||||||
|
<h3>LXC-шаблоны</h3>
|
||||||
|
{discovered.lxc_templates?.map((lxc) => <div className="item" key={lxc.volid}>
|
||||||
|
<strong>{lxc.name}</strong>
|
||||||
|
<span>{lxc.source_kind === "vmid" ? `CTID ${lxc.vmid}` : `${lxc.storage} · ${lxc.size_mb} MB`}</span>
|
||||||
|
<button type="button" disabled={busy} onClick={() => importTemplate({ name: lxc.name, guest_type: "lxc", source_template: lxc.volid, cores: lxc.cores || 1, memory_mb: lxc.memory_mb || 1024, disk_gb: lxc.disk_gb || 10 })}>Импортировать</button>
|
||||||
|
</div>)}
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
<h3>Добавить вручную</h3>
|
||||||
|
<form onSubmit={submit}>
|
||||||
|
<label>Название<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} required /></label>
|
||||||
|
<label>Тип<select value={form.guest_type} onChange={(e) => setForm({ ...form, guest_type: e.target.value })}><option value="vm">VM</option><option value="lxc">LXC</option></select></label>
|
||||||
|
{form.guest_type === "vm" ? <label>VMID шаблона<input type="number" min="1" value={form.source_vmid} onChange={(e) => setForm({ ...form, source_vmid: e.target.value })} required /></label> : <label>CTID или CT-шаблон<input placeholder="2000 или local:vztmpl/..." value={form.source_template} onChange={(e) => setForm({ ...form, source_template: e.target.value.startsWith("2000") ? "vmid:2000" : e.target.value })} required /></label>}
|
||||||
|
<label>Описание<input value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></label>
|
||||||
|
<div className="form-row"><label>vCPU<input type="number" min="1" value={form.cores} onChange={(e) => setForm({ ...form, cores: e.target.value })} /></label><label>RAM, MB<input type="number" min="128" value={form.memory_mb} onChange={(e) => setForm({ ...form, memory_mb: e.target.value })} /></label><label>Диск, GB<input type="number" min="1" value={form.disk_gb} onChange={(e) => setForm({ ...form, disk_gb: e.target.value })} /></label></div>
|
||||||
|
<button type="submit" disabled={busy}>Добавить шаблон</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h3>Активные шаблоны</h3>
|
||||||
|
<div className="items">{templates.map((template) => <div className="item" key={template.id}><strong>{template.name}</strong><span>{template.guest_type.toUpperCase()} · {template.cores} CPU · {template.memory_mb} MB · {template.disk_gb} GB</span><button className="danger" type="button" onClick={() => remove(template.id)}>Деактивировать</button></div>)}</div>
|
||||||
|
</section>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user