Files
proxui/frontend/src/components/VMCreate.tsx
T

192 lines
4.7 KiB
TypeScript

import { useEffect, useState } from "react";
import {
createVM,
fetchTemplates,
fetchSSHKeys,
type Template,
type SSHKey,
} from "../api";
interface Props {
token: string;
projectID: string;
tenantID: string;
onCreated: () => void;
}
export function VMCreate({ token, projectID, tenantID, onCreated }: Props) {
const [name, setName] = useState("");
const [templateID, setTemplateID] = useState("");
const [node, setNode] = useState("");
const [vcpu, setVCPU] = useState(2);
const [ramMB, setRAMMB] = useState(2048);
const [diskGB, setDiskGB] = useState(10);
const [sshKeyID, setSSHKeyID] = useState("");
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
const [open, setOpen] = useState(false);
const [templates, setTemplates] = useState<Template[]>([]);
const [sshKeys, setSSHKeys] = useState<SSHKey[]>([]);
useEffect(() => {
if (!open) return;
fetchTemplates(token)
.then((res) => {
setTemplates(res.data ?? []);
if (res.data && res.data.length > 0 && !templateID) {
setTemplateID(res.data[0].id);
}
})
.catch(() => {});
if (tenantID) {
fetchSSHKeys(token, tenantID)
.then((res) => setSSHKeys(res.data ?? []))
.catch(() => {});
}
}, [open, token, tenantID]);
useEffect(() => {
if (templateID) {
const tpl = templates.find((t) => t.id === templateID);
if (tpl && !node) {
setNode(tpl.proxmox_node);
}
}
}, [templateID, templates]);
async function handleCreate() {
setError("");
if (!name.trim()) {
setError("Name erforderlich");
return;
}
if (!templateID) {
setError("Template erforderlich");
return;
}
if (!node.trim()) {
setError("Node erforderlich");
return;
}
setBusy(true);
try {
await createVM(token, projectID, {
name: name.trim(),
template_id: templateID,
node: node.trim(),
vcpu,
ram_mb: ramMB,
disk_gb: diskGB,
ssh_key_id: sshKeyID || undefined,
});
setName("");
setOpen(false);
onCreated();
} catch (err) {
setError(err instanceof Error ? err.message : "Fehler");
} finally {
setBusy(false);
}
}
if (!open) {
return (
<button className="primary-button" onClick={() => setOpen(true)}>
Neue VM erstellen
</button>
);
}
return (
<div className="panel">
<div className="panel-header">
<div>
<p className="eyebrow">Provisioning</p>
<h2>VM erstellen</h2>
</div>
<button className="ghost-button" onClick={() => setOpen(false)}>
Abbrechen
</button>
</div>
{error ? <p className="form-error">{error}</p> : null}
<div className="metric-list">
<label>
Name
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="meine-vm"
/>
</label>
<label>
Template
<select
value={templateID}
onChange={(e) => setTemplateID(e.target.value)}
>
{templates.map((t) => (
<option key={t.id} value={t.id}>
{t.name} (VMID {t.proxmox_template_vmid}, {t.proxmox_node})
</option>
))}
</select>
</label>
<label>
Node
<input
value={node}
onChange={(e) => setNode(e.target.value)}
placeholder="pve"
/>
</label>
<label>
vCPU
<input
type="number"
value={vcpu}
onChange={(e) => setVCPU(Number(e.target.value))}
/>
</label>
<label>
RAM (MB)
<input
type="number"
value={ramMB}
onChange={(e) => setRAMMB(Number(e.target.value))}
/>
</label>
<label>
Disk (GB)
<input
type="number"
value={diskGB}
onChange={(e) => setDiskGB(Number(e.target.value))}
/>
</label>
<label>
SSH-Key (optional)
<select
value={sshKeyID}
onChange={(e) => setSSHKeyID(e.target.value)}
>
<option value="">Kein SSH-Key</option>
{sshKeys.map((k) => (
<option key={k.id} value={k.id}>
{k.name}
</option>
))}
</select>
</label>
</div>
<button
className="primary-button"
disabled={busy}
onClick={handleCreate}
style={{ marginTop: 14 }}
>
{busy ? "Erstelle..." : "VM erstellen"}
</button>
</div>
);
}