import { useState } from "react"; import { Plus } from "lucide-react"; import { useTranslation } from "react-i18next"; import { ConfirmFooter } from "@/components/ConfirmFooter"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api, humanizeError } from "@/lib/api"; import { useAsync } from "@/lib/hooks"; import { hostFor, type RuntimeConfig } from "@/lib/config"; import { DISPLAY_NAME_MAX, serverNameIssue, type ServerNameIssue } from "@/lib/naming"; import type { AutostartPolicy, CreateServerRequest } from "@/lib/types"; import { InlineError } from "@/components/MessageLine"; // CreateServerDialog IS the §15 收口 ("structured form, not free YAML"): every // dangerous knob (hostNetwork, hostPath, arbitrary image, raw resource quantities) // is simply absent from the UI. The image is chosen from the server-side whitelist // (GET /images); memory/storage are constrained text the API re-validates. Nothing // here can express a privileged pod — the form's value space IS the allowlist. const MEMORY_OPTIONS = ["2Gi", "4Gi", "6Gi", "8Gi"]; const STORAGE_OPTIONS = ["5Gi", "10Gi", "20Gi", "50Gi"]; function policyOptions(t: (key: string) => string): { value: AutostartPolicy; label: string }[] { return [ { value: "ownerOnly", label: t("create_server_policy_owner") }, { value: "public", label: t("create_server_policy_public") }, { value: "allowlist", label: t("create_server_policy_allowlist") }, ]; } interface Props { cfg: RuntimeConfig; onCreated: () => void; } export function CreateServerDialog({ cfg, onCreated }: Props) { const { t } = useTranslation("servers"); const [open, setOpen] = useState(false); const images = useAsync(() => api.listImages(), []); const [form, setForm] = useState({ name: "", subdomain: "", displayName: "", image: "", memory: "4Gi", storage: "10Gi", autostartPolicy: "ownerOnly", }); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); function set(k: K, v: CreateServerRequest[K]) { setForm((f) => ({ ...f, [k]: v })); } const enabledImages = (images.data ?? []).filter((i) => i.enabled); const nameIssue = serverNameIssue(form.name); const subdomainIssue = serverNameIssue(form.subdomain); const canSubmit = nameIssue === null && subdomainIssue === null && !!form.image && !!form.memory && !!form.storage && !submitting; async function submit() { setSubmitting(true); setError(null); try { const payload: CreateServerRequest = { ...form, displayName: form.displayName?.trim() || undefined, }; await api.createServer(payload); setOpen(false); onCreated(); setForm((f) => ({ ...f, name: "", subdomain: "", displayName: "" })); } catch (e) { setError(humanizeError(e)); } finally { setSubmitting(false); } } const policies = policyOptions(t); // A field's hint turns into the reason it would be refused once something is typed. const issueText = (issue: ServerNameIssue, value: string) => issue === "reserved" ? t("create_server_name_reserved", { name: value }) : t("create_server_name_invalid"); const hintClass = (value: string, issue: ServerNameIssue | null) => value && issue ? "text-xs text-destructive" : "text-xs text-muted-foreground"; return ( {t("create_server_title")} {t("create_server_desc")}
set("name", e.target.value.toLowerCase())} aria-invalid={!!form.name && nameIssue !== null} aria-describedby="cs-name-hint" />

{form.name && nameIssue ? issueText(nameIssue, form.name) : t("create_server_name_hint")}

set("subdomain", e.target.value.toLowerCase())} aria-invalid={!!form.subdomain && subdomainIssue !== null} aria-describedby="cs-sub-hint" />

{form.subdomain && subdomainIssue ? issueText(subdomainIssue, form.subdomain) : t("create_server_subdomain_hint", { host: hostFor(form.subdomain || "name", cfg) })}

set("displayName", e.target.value)} />
setOpen(false)} onConfirm={submit} disabled={!canSubmit} loading={submitting} cancelLabel={t("create_server_cancel")} confirmLabel={t("create_server_submit")} confirmVariant="default" />
); }