feat: LXC container creation from CT templates
- Add LXC deploy API endpoints: GET /proxmox/api/lxc-templates/{id},
GET /proxmox/api/lxc-storages/{id}, POST /proxmox/api/lxc/deploy
- Add ProxmoxClient.get_lxc_storage_templates() and get_node_storages()
- Fix duplicate create_lxc_container() — removed dead copy, updated
canonical with nameserver/searchdomain params
- Fix wait_for_task(): accept WARNINGS:N exitstatus as success
- Add IPAM IP release on deploy failure
- Add frontend hooks: useLXCTemplates, useLXCStorages, useDeployLXC
with 5-min staleTime caching
- Add LXC wizard steps to CreateInstanceWizard (type, template, config)
- Add LXC i18n keys to en.json and ru.json
- Add LXCTemplate and LXCDeployRequest TypeScript interfaces
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Server, Cpu, CheckCircle, ChevronLeft, ChevronRight, Loader2, Monitor } from 'lucide-react';
|
||||
import { Server, Cpu, CheckCircle, ChevronLeft, ChevronRight, Loader2, Monitor, Container, HardDrive } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -12,12 +12,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { useServers } from '@/hooks/use-nodes';
|
||||
import { useTemplates, useTemplateGroups } from '@/hooks/use-templates';
|
||||
import { useIPAMNetworks } from '@/hooks/use-ipam';
|
||||
import { useLXCTemplates, useLXCStorages, useDeployLXC } from '@/hooks/use-lxc-templates';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { useDeployTasksStore } from '@/stores/deploy-tasks-store';
|
||||
import type { VMDeployRequest, OSTemplate, ProxmoxServer } from '@/types';
|
||||
import type { VMDeployRequest, OSTemplate, ProxmoxServer, LXCTemplate } from '@/types';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const STEPS = ['server', 'template', 'config', 'confirm'] as const;
|
||||
type InstanceKind = 'vm' | 'lxc';
|
||||
const STEPS = ['server', 'type', 'template', 'config', 'confirm'] as const;
|
||||
type Step = (typeof STEPS)[number];
|
||||
|
||||
export default function CreateInstanceWizard({ onClose }: { onClose?: () => void }) {
|
||||
@@ -29,8 +31,11 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
|
||||
const addDeployTask = useDeployTasksStore((s) => s.addTask);
|
||||
|
||||
// State
|
||||
// ── Shared state ──────────────────────────────────────────────────────────
|
||||
const [kind, setKind] = useState<InstanceKind>('lxc');
|
||||
const [selectedServer, setSelectedServer] = useState<ProxmoxServer | null>(null);
|
||||
|
||||
// ── VM state ──────────────────────────────────────────────────────────────
|
||||
const [selectedNode, setSelectedNode] = useState('');
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<OSTemplate | null>(null);
|
||||
const [config, setConfig] = useState<{
|
||||
@@ -63,11 +68,34 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
ssh_keys: '',
|
||||
});
|
||||
|
||||
// ── LXC state ─────────────────────────────────────────────────────────────
|
||||
const [selectedLXCTemplate, setSelectedLXCTemplate] = useState<LXCTemplate | null>(null);
|
||||
const [lxcConfig, setLxcConfig] = useState({
|
||||
name: '',
|
||||
cores: 1,
|
||||
memory: 512,
|
||||
swap: 512,
|
||||
disk: 8,
|
||||
storage: 'local-lvm',
|
||||
bridge: 'vmbr0',
|
||||
ipam_network_id: null as number | null,
|
||||
ip_address: '',
|
||||
gateway: '',
|
||||
password: '',
|
||||
ssh_keys: '',
|
||||
nameserver: '',
|
||||
unprivileged: true,
|
||||
start_after_create: true,
|
||||
onboot: false,
|
||||
});
|
||||
|
||||
// Queries
|
||||
const { data: servers = [] } = useServers();
|
||||
const { data: groups = [] } = useTemplateGroups();
|
||||
const { data: templates = [] } = useTemplates(undefined, selectedServer?.id);
|
||||
const { data: networks = [] } = useIPAMNetworks();
|
||||
const { data: lxcTemplates = [], isLoading: lxcLoading } = useLXCTemplates(selectedServer?.id);
|
||||
const { data: lxcStorages = [] } = useLXCStorages(selectedServer?.id);
|
||||
|
||||
const deploy = useMutation({
|
||||
mutationFn: (data: VMDeployRequest) =>
|
||||
@@ -89,6 +117,8 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
onError: (err: Error) => toast.error(err.message),
|
||||
});
|
||||
|
||||
const deployLXC = useDeployLXC();
|
||||
|
||||
const next = () => {
|
||||
const i = STEPS.indexOf(step);
|
||||
if (i < STEPS.length - 1) setStep(STEPS[i + 1]);
|
||||
@@ -100,30 +130,75 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
|
||||
const canNext = () => {
|
||||
if (step === 'server') return !!selectedServer;
|
||||
if (step === 'template') return !!selectedTemplate;
|
||||
if (step === 'config') return config.name.length >= 2;
|
||||
if (step === 'type') return !!kind;
|
||||
if (step === 'template') return kind === 'vm' ? !!selectedTemplate : !!selectedLXCTemplate;
|
||||
if (step === 'config') return kind === 'vm' ? config.name.length >= 2 : lxcConfig.name.length >= 2;
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleDeploy = () => {
|
||||
if (!selectedTemplate) return;
|
||||
deploy.mutate({
|
||||
template_id: selectedTemplate.id,
|
||||
name: config.name,
|
||||
target_node: selectedNode || undefined,
|
||||
cores: config.cores,
|
||||
memory: config.memory,
|
||||
disk: config.disk,
|
||||
target_storage: config.storage,
|
||||
start_after_create: config.start_after_create,
|
||||
network_bridge: config.bridge,
|
||||
ip_address: config.ip_address || undefined,
|
||||
gateway: config.gateway || undefined,
|
||||
ipam_network_id: config.ipam_network_id || undefined,
|
||||
cloud_init_user: config.cloud_init_user || undefined,
|
||||
cloud_init_password: config.cloud_init_password || undefined,
|
||||
ssh_keys: config.ssh_keys || undefined,
|
||||
});
|
||||
if (kind === 'lxc') {
|
||||
if (!selectedLXCTemplate || !selectedServer) return;
|
||||
deployLXC.mutate(
|
||||
{
|
||||
server_id: selectedServer.id,
|
||||
ostemplate: selectedLXCTemplate.volid,
|
||||
node: selectedLXCTemplate.node || undefined,
|
||||
name: lxcConfig.name,
|
||||
cores: lxcConfig.cores,
|
||||
memory: lxcConfig.memory,
|
||||
swap: lxcConfig.swap,
|
||||
disk: lxcConfig.disk,
|
||||
storage: lxcConfig.storage,
|
||||
bridge: lxcConfig.bridge,
|
||||
ip_address: lxcConfig.ip_address || undefined,
|
||||
gateway: lxcConfig.gateway || undefined,
|
||||
ipam_network_id: lxcConfig.ipam_network_id || undefined,
|
||||
password: lxcConfig.password || undefined,
|
||||
ssh_keys: lxcConfig.ssh_keys || undefined,
|
||||
nameserver: lxcConfig.nameserver || undefined,
|
||||
unprivileged: lxcConfig.unprivileged,
|
||||
start_after_create: lxcConfig.start_after_create,
|
||||
onboot: lxcConfig.onboot,
|
||||
},
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
addDeployTask({
|
||||
id: res.task_id,
|
||||
name: res.name,
|
||||
status: 'pending',
|
||||
step: t('wizard.deploy_starting'),
|
||||
progress: 0,
|
||||
vmid: null,
|
||||
node: null,
|
||||
error_message: null,
|
||||
});
|
||||
toast.success(t('wizard.deploy_queued', { name: res.name }));
|
||||
navigate('/instances');
|
||||
},
|
||||
onError: (err: Error) => toast.error(err.message),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if (!selectedTemplate) return;
|
||||
deploy.mutate({
|
||||
template_id: selectedTemplate.id,
|
||||
name: config.name,
|
||||
target_node: selectedNode || undefined,
|
||||
cores: config.cores,
|
||||
memory: config.memory,
|
||||
disk: config.disk,
|
||||
target_storage: config.storage,
|
||||
start_after_create: config.start_after_create,
|
||||
network_bridge: config.bridge,
|
||||
ip_address: config.ip_address || undefined,
|
||||
gateway: config.gateway || undefined,
|
||||
ipam_network_id: config.ipam_network_id || undefined,
|
||||
cloud_init_user: config.cloud_init_user || undefined,
|
||||
cloud_init_password: config.cloud_init_password || undefined,
|
||||
ssh_keys: config.ssh_keys || undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Apply template defaults when template selected
|
||||
@@ -164,7 +239,7 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
<Card
|
||||
key={srv.id}
|
||||
className={`cursor-pointer transition-colors hover:border-blue-500/50 ${selectedServer?.id === srv.id ? 'border-blue-500 bg-blue-500/5' : ''}`}
|
||||
onClick={() => setSelectedServer(srv)}
|
||||
onClick={() => { setSelectedServer(srv); setSelectedLXCTemplate(null); setSelectedTemplate(null); }}
|
||||
>
|
||||
<CardContent className="flex items-center gap-3 p-4">
|
||||
<Server className="h-8 w-8 text-muted-foreground" />
|
||||
@@ -182,8 +257,43 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Template */}
|
||||
{step === 'template' && (
|
||||
{/* Step 2: Type selection */}
|
||||
{step === 'type' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">{t('wizard.select_type')}</h3>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Card
|
||||
className={`cursor-pointer transition-colors hover:border-blue-500/50 ${kind === 'vm' ? 'border-blue-500 bg-blue-500/5' : ''}`}
|
||||
onClick={() => setKind('vm')}
|
||||
>
|
||||
<CardContent className="flex items-center gap-4 p-5">
|
||||
<HardDrive className="h-10 w-10 text-blue-400 shrink-0" />
|
||||
<div>
|
||||
<p className="font-semibold">{t('wizard.type_vm')}</p>
|
||||
<p className="text-sm text-muted-foreground">{t('wizard.type_vm_desc')}</p>
|
||||
</div>
|
||||
{kind === 'vm' && <CheckCircle className="ml-auto h-5 w-5 text-blue-400 shrink-0" />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card
|
||||
className={`cursor-pointer transition-colors hover:border-green-500/50 ${kind === 'lxc' ? 'border-green-500 bg-green-500/5' : ''}`}
|
||||
onClick={() => setKind('lxc')}
|
||||
>
|
||||
<CardContent className="flex items-center gap-4 p-5">
|
||||
<Container className="h-10 w-10 text-green-400 shrink-0" />
|
||||
<div>
|
||||
<p className="font-semibold">{t('wizard.type_lxc')}</p>
|
||||
<p className="text-sm text-muted-foreground">{t('wizard.type_lxc_desc')}</p>
|
||||
</div>
|
||||
{kind === 'lxc' && <CheckCircle className="ml-auto h-5 w-5 text-green-400 shrink-0" />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Template — VM */}
|
||||
{step === 'template' && kind === 'vm' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">{t('wizard.select_template')}</h3>
|
||||
{groups.map(group => {
|
||||
@@ -224,8 +334,50 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Configuration */}
|
||||
{step === 'config' && (
|
||||
{/* Step 3: Template — LXC */}
|
||||
{step === 'template' && kind === 'lxc' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">{t('wizard.select_lxc_template')}</h3>
|
||||
{lxcLoading && (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>{t('common.loading')}</span>
|
||||
</div>
|
||||
)}
|
||||
{!lxcLoading && lxcTemplates.length === 0 && (
|
||||
<p className="text-center text-muted-foreground py-8">{t('wizard.no_lxc_templates')}</p>
|
||||
)}
|
||||
{!lxcLoading && lxcTemplates.length > 0 && (
|
||||
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{lxcTemplates.map(tpl => (
|
||||
<Card
|
||||
key={tpl.volid}
|
||||
className={`cursor-pointer transition-colors hover:border-green-500/50 ${selectedLXCTemplate?.volid === tpl.volid ? 'border-green-500 bg-green-500/5' : ''}`}
|
||||
onClick={() => setSelectedLXCTemplate(tpl)}
|
||||
>
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Container className="h-5 w-5 text-green-400 shrink-0" />
|
||||
<span className="font-medium text-sm truncate">{tpl.name}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground font-mono">
|
||||
{tpl.storage} · {tpl.node}
|
||||
</p>
|
||||
{tpl.size > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(tpl.size / 1024 / 1024).toFixed(0)} MB
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Configuration — VM */}
|
||||
{step === 'config' && kind === 'vm' && (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold">{t('wizard.configure')}</h3>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
@@ -350,13 +502,155 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Confirm */}
|
||||
{step === 'confirm' && (
|
||||
{/* Step 4: Configuration — LXC */}
|
||||
{step === 'config' && kind === 'lxc' && (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold">{t('wizard.configure')}</h3>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
{/* Basic */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm">{t('wizard.basic')}</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<Label>{t('wizard.hostname')}</Label>
|
||||
<Input
|
||||
value={lxcConfig.name}
|
||||
onChange={e => setLxcConfig(p => ({ ...p, name: e.target.value }))}
|
||||
placeholder={t('common.placeholder_vm_name')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>{t('wizard.storage')}</Label>
|
||||
<Select value={lxcConfig.storage} onValueChange={v => setLxcConfig(p => ({ ...p, storage: v }))}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{lxcStorages.length > 0
|
||||
? lxcStorages.map(s => <SelectItem key={s.storage} value={s.storage}>{s.storage} ({s.type})</SelectItem>)
|
||||
: <SelectItem value="local-lvm">local-lvm</SelectItem>
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="lxc_start_after"
|
||||
checked={lxcConfig.start_after_create}
|
||||
onChange={e => setLxcConfig(p => ({ ...p, start_after_create: e.target.checked }))}
|
||||
className="rounded"
|
||||
/>
|
||||
<Label htmlFor="lxc_start_after">{t('wizard.start_after_create')}</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="lxc_unprivileged"
|
||||
checked={lxcConfig.unprivileged}
|
||||
onChange={e => setLxcConfig(p => ({ ...p, unprivileged: e.target.checked }))}
|
||||
className="rounded"
|
||||
/>
|
||||
<Label htmlFor="lxc_unprivileged">{t('wizard.unprivileged')}</Label>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Resources */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm flex items-center gap-2"><Cpu className="h-4 w-4" />{t('wizard.resources')}</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<Label>vCPU</Label>
|
||||
<Input type="number" min={1} value={lxcConfig.cores} onChange={e => setLxcConfig(p => ({ ...p, cores: Number(e.target.value) }))} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>{t('wizard.memory_mb')}</Label>
|
||||
<Input type="number" min={64} step={128} value={lxcConfig.memory} onChange={e => setLxcConfig(p => ({ ...p, memory: Number(e.target.value) }))} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>{t('wizard.swap_mb')}</Label>
|
||||
<Input type="number" min={0} step={128} value={lxcConfig.swap} onChange={e => setLxcConfig(p => ({ ...p, swap: Number(e.target.value) }))} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>{t('wizard.disk_gb')}</Label>
|
||||
<Input type="number" min={1} value={lxcConfig.disk} onChange={e => setLxcConfig(p => ({ ...p, disk: Number(e.target.value) }))} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Network */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm">{t('wizard.network')}</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<Label>{t('wizard.bridge')}</Label>
|
||||
<Input value={lxcConfig.bridge} onChange={e => setLxcConfig(p => ({ ...p, bridge: e.target.value }))} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>IPAM {t('wizard.network')}</Label>
|
||||
<Select value={String(lxcConfig.ipam_network_id || '')} onValueChange={v => setLxcConfig(p => ({ ...p, ipam_network_id: v ? Number(v) : null }))}>
|
||||
<SelectTrigger><SelectValue placeholder={t('wizard.manual_ip')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{networks.map(n => (
|
||||
<SelectItem key={n.id} value={String(n.id)}>{n.name} ({n.network})</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{!lxcConfig.ipam_network_id && (
|
||||
<>
|
||||
<div>
|
||||
<Label>IP (CIDR)</Label>
|
||||
<Input value={lxcConfig.ip_address} onChange={e => setLxcConfig(p => ({ ...p, ip_address: e.target.value }))} placeholder="10.10.10.5/24 или dhcp" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Gateway</Label>
|
||||
<Input value={lxcConfig.gateway} onChange={e => setLxcConfig(p => ({ ...p, gateway: e.target.value }))} placeholder="10.10.10.1" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<Label>DNS</Label>
|
||||
<Input value={lxcConfig.nameserver} onChange={e => setLxcConfig(p => ({ ...p, nameserver: e.target.value }))} placeholder="8.8.8.8" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Auth */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm">{t('wizard.lxc_password')}</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<Label>{t('wizard.lxc_password')}</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={lxcConfig.password}
|
||||
onChange={e => setLxcConfig(p => ({ ...p, password: e.target.value }))}
|
||||
placeholder={t('wizard.lxc_password_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>SSH Keys</Label>
|
||||
<textarea
|
||||
className="w-full rounded-md border bg-transparent px-3 py-2 text-sm font-mono min-h-[60px]"
|
||||
value={lxcConfig.ssh_keys}
|
||||
onChange={e => setLxcConfig(p => ({ ...p, ssh_keys: e.target.value }))}
|
||||
placeholder={t('common.placeholder_ssh_key')}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 5: Confirm — VM */}
|
||||
{step === 'confirm' && kind === 'vm' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">{t('wizard.confirm')}</h3>
|
||||
<Card>
|
||||
<CardContent className="p-6 space-y-3">
|
||||
<Row label={t('wizard.server_label')} value={selectedServer?.name || ''} />
|
||||
<Row label={t('common.type')} value="VM (QEMU)" />
|
||||
<Row label={t('wizard.template_label')} value={selectedTemplate?.name || ''} />
|
||||
<Row label={t('common.name')} value={config.name} />
|
||||
<Row label={t('common.vcpu')} value={String(config.cores)} />
|
||||
@@ -371,14 +665,38 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 5: Confirm — LXC */}
|
||||
{step === 'confirm' && kind === 'lxc' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">{t('wizard.confirm')}</h3>
|
||||
<Card>
|
||||
<CardContent className="p-6 space-y-3">
|
||||
<Row label={t('wizard.server_label')} value={selectedServer?.name || ''} />
|
||||
<Row label={t('common.type')} value="LXC Container" />
|
||||
<Row label={t('wizard.lxc_template_label')} value={selectedLXCTemplate?.name || ''} />
|
||||
<Row label={t('wizard.hostname')} value={lxcConfig.name} />
|
||||
<Row label={t('common.vcpu')} value={String(lxcConfig.cores)} />
|
||||
<Row label={t('wizard.memory_mb')} value={`${lxcConfig.memory} MB`} />
|
||||
<Row label={t('wizard.swap_mb')} value={`${lxcConfig.swap} MB`} />
|
||||
<Row label={t('wizard.disk_gb')} value={`${lxcConfig.disk} GB`} />
|
||||
<Row label={t('wizard.storage')} value={lxcConfig.storage} />
|
||||
<Row label={t('wizard.bridge')} value={lxcConfig.bridge} />
|
||||
{lxcConfig.ipam_network_id && <Row label={t('common.ipam')} value={networks.find(n => n.id === lxcConfig.ipam_network_id)?.name || 'Auto'} />}
|
||||
{lxcConfig.ip_address && <Row label={t('common.primary_ip')} value={lxcConfig.ip_address} />}
|
||||
<Row label={t('wizard.unprivileged')} value={lxcConfig.unprivileged ? t('common.yes') : t('common.no')} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex items-center justify-between border-t pt-4">
|
||||
<Button variant="outline" onClick={stepIdx === 0 ? handleClose : prev}>
|
||||
{stepIdx === 0 ? t('common.cancel') : <><ChevronLeft className="mr-1 h-4 w-4" />{t('wizard.back')}</>}
|
||||
</Button>
|
||||
{step === 'confirm' ? (
|
||||
<Button onClick={handleDeploy} disabled={deploy.isPending}>
|
||||
{deploy.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
<Button onClick={handleDeploy} disabled={deploy.isPending || deployLXC.isPending}>
|
||||
{(deploy.isPending || deployLXC.isPending) && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{t('wizard.deploy')}
|
||||
</Button>
|
||||
) : (
|
||||
@@ -390,7 +708,6 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user