fix(sdn): zone DNS is an SDN DNS entry id, not an IP — use select
All checks were successful
Backend Tests / pytest (Python 3.12) (push) Successful in 52s
Deploy to Production / Trigger production update (push) Successful in 8s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 03:05:06 +05:00
parent 91080d8574
commit d3fbdb1ff5
4 changed files with 40 additions and 8 deletions

View File

@@ -11,7 +11,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { import {
useSDNStatus, useSDNZones, useSDNVNets, useSDNSubnets, useSDNStatus, useSDNZones, useSDNVNets, useSDNSubnets, useSDNDns,
useCreateSDNZone, useUpdateSDNZone, useDeleteSDNZone, useCreateSDNZone, useUpdateSDNZone, useDeleteSDNZone,
useCreateSDNVNet, useUpdateSDNVNet, useDeleteSDNVNet, useCreateSDNVNet, useUpdateSDNVNet, useDeleteSDNVNet,
useCreateSDNSubnet, useDeleteSDNSubnet, useApplySDN, useCreateSDNSubnet, useDeleteSDNSubnet, useApplySDN,
@@ -42,6 +42,9 @@ function ZoneDialog({ serverId, open, onOpenChange, editing }: {
const { t } = useTranslation(); const { t } = useTranslation();
const createZone = useCreateSDNZone(serverId); const createZone = useCreateSDNZone(serverId);
const updateZone = useUpdateSDNZone(serverId); const updateZone = useUpdateSDNZone(serverId);
// Zone `dns` is the ID of an SDN DNS entry (Datacenter → SDN → Options), not an IP
const { data: dnsData } = useSDNDns(serverId, open);
const dnsEntries = dnsData?.dns ?? [];
// Dialog is freshly mounted on each open, so init state lazily from `editing`. // Dialog is freshly mounted on each open, so init state lazily from `editing`.
const [form, setForm] = useState<ZoneForm>(() => editing ? { const [form, setForm] = useState<ZoneForm>(() => editing ? {
zone: editing.zone, zone: editing.zone,
@@ -110,7 +113,22 @@ function ZoneDialog({ serverId, open, onOpenChange, editing }: {
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div><Label>MTU</Label><Input type="number" value={form.mtu} onChange={e => set('mtu', e.target.value)} className="mt-1" /></div> <div><Label>MTU</Label><Input type="number" value={form.mtu} onChange={e => set('mtu', e.target.value)} className="mt-1" /></div>
<div><Label>{t('sdn.nodes')}</Label><Input value={form.nodes} onChange={e => set('nodes', e.target.value)} className="mt-1" placeholder="node1,node2" /></div> <div><Label>{t('sdn.nodes')}</Label><Input value={form.nodes} onChange={e => set('nodes', e.target.value)} className="mt-1" placeholder="node1,node2" /></div>
<div><Label>DNS</Label><Input value={form.dns} onChange={e => set('dns', e.target.value)} className="mt-1" /></div> <div>
<Label>DNS</Label>
<Select
value={form.dns}
onValueChange={v => set('dns', v === '__none__' ? '' : v)}
disabled={dnsEntries.length === 0}
>
<SelectTrigger className="mt-1">
<SelectValue placeholder={dnsEntries.length ? '—' : t('sdn.no_dns_entries')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">—</SelectItem>
{dnsEntries.map(d => <SelectItem key={d.dns} value={d.dns}>{d.dns}</SelectItem>)}
</SelectContent>
</Select>
</div>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>

View File

@@ -41,6 +41,20 @@ export function useSDNSubnets(serverId: number, vnet: string) {
}); });
} }
export interface SDNDnsEntry {
dns: string;
type?: string;
url?: string;
}
export function useSDNDns(serverId: number, enabled = true) {
return useQuery({
queryKey: ['sdn-dns', serverId] as const,
queryFn: () => apiClient.get<{ dns: SDNDnsEntry[] }>(`/proxmox/api/servers/${serverId}/sdn/dns`),
enabled: serverId > 0 && enabled,
});
}
// Invalidate the SDN status (pending indicator) alongside the changed entity. // Invalidate the SDN status (pending indicator) alongside the changed entity.
function useSDNInvalidator(serverId: number) { function useSDNInvalidator(serverId: number) {
const qc = useQueryClient(); const qc = useQueryClient();

View File

@@ -493,7 +493,8 @@
"gateway": "Gateway", "gateway": "Gateway",
"snat": "SNAT", "snat": "SNAT",
"dnszoneprefix": "DNS zone prefix", "dnszoneprefix": "DNS zone prefix",
"create_ipam": "Also create a matching IPAM network" "create_ipam": "Also create a matching IPAM network",
"no_dns_entries": "No SDN DNS servers"
}, },
"templates": { "templates": {
"title": "OS Templates", "title": "OS Templates",
@@ -637,7 +638,6 @@
"vmids_hint": "Comma-separated VMIDs (e.g. 100, 101) or 'all' for every VM/CT.", "vmids_hint": "Comma-separated VMIDs (e.g. 100, 101) or 'all' for every VM/CT.",
"all_vms": "All VMs/CTs", "all_vms": "All VMs/CTs",
"all_vms_selected": "All VMs/CTs on the node selected", "all_vms_selected": "All VMs/CTs on the node selected",
"selected_count": "Selected: {{count}}",
"select_all_visible": "Select all visible", "select_all_visible": "Select all visible",
"clear_selection": "Clear", "clear_selection": "Clear",
"pick_server_node_first": "Select a server and node first", "pick_server_node_first": "Select a server and node first",
@@ -945,4 +945,4 @@
"connected": "Connected", "connected": "Connected",
"error": "Error" "error": "Error"
} }
} }

View File

@@ -493,7 +493,8 @@
"gateway": "Шлюз", "gateway": "Шлюз",
"snat": "SNAT", "snat": "SNAT",
"dnszoneprefix": "Префикс DNS-зоны", "dnszoneprefix": "Префикс DNS-зоны",
"create_ipam": "Также создать соответствующую IPAM-сеть" "create_ipam": "Также создать соответствующую IPAM-сеть",
"no_dns_entries": "Нет DNS-серверов SDN"
}, },
"templates": { "templates": {
"title": "Шаблоны ОС", "title": "Шаблоны ОС",
@@ -637,7 +638,6 @@
"vmids_hint": "Список VMID через запятую (напр. 100, 101) или слово all для всех VM/CT.", "vmids_hint": "Список VMID через запятую (напр. 100, 101) или слово all для всех VM/CT.",
"all_vms": "Все VM/CT", "all_vms": "Все VM/CT",
"all_vms_selected": "Выбраны все VM/CT на узле", "all_vms_selected": "Выбраны все VM/CT на узле",
"selected_count": "Выбрано: {{count}}",
"select_all_visible": "Выбрать все видимые", "select_all_visible": "Выбрать все видимые",
"clear_selection": "Очистить", "clear_selection": "Очистить",
"pick_server_node_first": "Сначала выберите сервер и узел", "pick_server_node_first": "Сначала выберите сервер и узел",
@@ -945,4 +945,4 @@
"connected": "Подключено", "connected": "Подключено",
"error": "Ошибка" "error": "Ошибка"
} }
} }