mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-06 05:52:19 +08:00
修复了一些已知问题
This commit is contained in:
@@ -432,7 +432,8 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig
|
|||||||
}
|
}
|
||||||
ipv6List := configIPv6AssignmentAddresses(ipv6Assignments)
|
ipv6List := configIPv6AssignmentAddresses(ipv6Assignments)
|
||||||
ipv4List := configIPv4AssignmentAddresses(publicIPv4s)
|
ipv4List := configIPv4AssignmentAddresses(publicIPv4s)
|
||||||
defaultHostIP := lxc.DefaultPortMappingHostIP(publicIPv4s)
|
// NAT4 port mappings should bind to the host IP, not the VM's independent public IPv4.
|
||||||
|
defaultHostIP := ""
|
||||||
|
|
||||||
var xml string
|
var xml string
|
||||||
winAdminPassword := ""
|
winAdminPassword := ""
|
||||||
@@ -1850,8 +1851,7 @@ func windowsIPv6PowerShell(ipv6s []string) string {
|
|||||||
}
|
}
|
||||||
return strings.Join([]string{
|
return strings.Join([]string{
|
||||||
"$clicdIPv6=@(" + strings.Join(quoted, ",") + ")",
|
"$clicdIPv6=@(" + strings.Join(quoted, ",") + ")",
|
||||||
"$iface=$null",
|
// Reuse $iface already found by the main script
|
||||||
"for ($i=0; $i -lt 60 -and -not $iface; $i++) { $iface=Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.HardwareInterface } | Sort-Object ifIndex | Select-Object -First 1; if (-not $iface) { Start-Sleep -Seconds 5 } }",
|
|
||||||
"if ($iface) {",
|
"if ($iface) {",
|
||||||
" foreach ($ip in $clicdIPv6) {",
|
" foreach ($ip in $clicdIPv6) {",
|
||||||
" Get-NetIPAddress -InterfaceIndex $iface.ifIndex -AddressFamily IPv6 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -eq $ip } | Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue",
|
" Get-NetIPAddress -InterfaceIndex $iface.ifIndex -AddressFamily IPv6 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -eq $ip } | Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue",
|
||||||
@@ -1875,8 +1875,7 @@ func windowsIPv4PowerShell(ipv4s []string) string {
|
|||||||
}
|
}
|
||||||
return strings.Join([]string{
|
return strings.Join([]string{
|
||||||
"$clicdIPv4=@(" + strings.Join(quoted, ",") + ")",
|
"$clicdIPv4=@(" + strings.Join(quoted, ",") + ")",
|
||||||
"$iface=$null",
|
// Reuse $iface already found by the main script
|
||||||
"for ($i=0; $i -lt 60 -and -not $iface; $i++) { $iface=Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.HardwareInterface } | Sort-Object ifIndex | Select-Object -First 1; if (-not $iface) { Start-Sleep -Seconds 5 } }",
|
|
||||||
"if ($iface) {",
|
"if ($iface) {",
|
||||||
" foreach ($ip in $clicdIPv4) {",
|
" foreach ($ip in $clicdIPv4) {",
|
||||||
" Get-NetIPAddress -InterfaceIndex $iface.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -eq $ip } | Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue",
|
" Get-NetIPAddress -InterfaceIndex $iface.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -eq $ip } | Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue",
|
||||||
|
|||||||
@@ -345,12 +345,7 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
|||||||
|
|
||||||
// Setup default port mappings (SSH only)
|
// Setup default port mappings (SSH only)
|
||||||
portMappings = SetupDefaultPortMappings(sshPort)
|
portMappings = SetupDefaultPortMappings(sshPort)
|
||||||
defaultHostIP := defaultPortMappingHostIP(publicIPv4s)
|
// NAT4 port mappings should bind to the host IP, not the container's independent public IPv4.
|
||||||
if defaultHostIP != "" {
|
|
||||||
for i := range portMappings {
|
|
||||||
portMappings[i].HostIP = defaultHostIP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tempC := &config.Container{ID: id, PublicIPv4s: publicIPv4s, PortMappings: portMappings}
|
tempC := &config.Container{ID: id, PublicIPv4s: publicIPv4s, PortMappings: portMappings}
|
||||||
|
|
||||||
extraPorts := cfg.ExtraPorts
|
extraPorts := cfg.ExtraPorts
|
||||||
@@ -364,7 +359,7 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
|||||||
pm, err := normalizePortMapping(tempC, -1, config.PortMapping{
|
pm, err := normalizePortMapping(tempC, -1, config.PortMapping{
|
||||||
ContainerPort: containerPort,
|
ContainerPort: containerPort,
|
||||||
HostPort: containerPort,
|
HostPort: containerPort,
|
||||||
HostIP: defaultHostIP,
|
HostIP: "",
|
||||||
Protocol: "tcp",
|
Protocol: "tcp",
|
||||||
Description: fmt.Sprintf("Port-%d", containerPort),
|
Description: fmt.Sprintf("Port-%d", containerPort),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ func (m *Manager) ApplyPortMappings(id int) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When container has public IPv4 but no port mappings (independent IP mode),
|
// When container has public IPv4, apply full port passthrough DNAT so the
|
||||||
// ensure inbound DNAT for standard service ports (SSH / RDP).
|
// container owns all ports on its public IP (no NAT management needed).
|
||||||
if len(c.PortMappings) == 0 && len(c.PublicIPv4s) > 0 {
|
if len(c.PublicIPv4s) > 0 {
|
||||||
ensureIndependentIPv4Ingress(c, tag)
|
ensureIndependentIPv4Ingress(c, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,40 +75,30 @@ func ensureIndependentIPv4Ingress(c *config.Container, tag string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type svcPort struct {
|
|
||||||
port int
|
|
||||||
protocol string
|
|
||||||
desc string
|
|
||||||
}
|
|
||||||
servicePorts := []svcPort{{port: 22, protocol: "tcp", desc: "SSH"}}
|
|
||||||
if strings.Contains(strings.ToLower(c.Template), "windows") {
|
|
||||||
servicePorts = []svcPort{{port: 3389, protocol: "tcp", desc: "RDP"}}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, assignment := range c.PublicIPv4s {
|
for _, assignment := range c.PublicIPv4s {
|
||||||
hostIP := strings.TrimSpace(assignment.Address)
|
hostIP := strings.TrimSpace(assignment.Address)
|
||||||
if hostIP == "" {
|
if hostIP == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, svc := range servicePorts {
|
// Full port passthrough: DNAT all TCP+UDP traffic on this public IP to the container.
|
||||||
|
for _, proto := range []string{"tcp", "udp"} {
|
||||||
args := []string{
|
args := []string{
|
||||||
"-t", "nat",
|
"-t", "nat",
|
||||||
"-I", "PREROUTING", "1",
|
"-I", "PREROUTING", "1",
|
||||||
"-d", hostIP,
|
"-d", hostIP,
|
||||||
"-p", svc.protocol,
|
"-p", proto,
|
||||||
"--dport", fmt.Sprintf("%d", svc.port),
|
|
||||||
"-j", "DNAT",
|
"-j", "DNAT",
|
||||||
"--to-destination", fmt.Sprintf("%s:%d", c.IP, svc.port),
|
"--to-destination", c.IP,
|
||||||
"-m", "comment", "--comment", fmt.Sprintf("clicd-%s-%s-%d", tag, natRuleIPTag(hostIP), svc.port),
|
"-m", "comment", "--comment", fmt.Sprintf("clicd-%s-%s-all-%s", tag, natRuleIPTag(hostIP), proto),
|
||||||
}
|
}
|
||||||
cmd := exec.Command("iptables", args...)
|
cmd := exec.Command("iptables", args...)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Warning: failed to apply %s ingress %s:%d->%s:%d: %v, output: %s\n",
|
fmt.Printf("Warning: failed to apply %s passthrough %s->%s: %v, output: %s\n",
|
||||||
svc.desc, hostIP, svc.port, c.IP, svc.port, err, string(output))
|
proto, hostIP, c.IP, err, string(output))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Printf("%s ingress: %s:%d -> %s:%d\n", svc.desc, hostIP, svc.port, c.IP, svc.port)
|
fmt.Printf("IPv4 passthrough (%s): %s -> %s (all ports)\n", proto, hostIP, c.IP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -321,7 +321,12 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={!!form.assign_ipv4}
|
checked={!!form.assign_ipv4}
|
||||||
disabled={!ipv4Available}
|
disabled={!ipv4Available}
|
||||||
onChange={(event) => setForm({ ...form, assign_ipv4: event.target.checked, public_ipv4s: event.target.checked ? form.public_ipv4s : [] })}
|
onChange={(event) => setForm({
|
||||||
|
...form,
|
||||||
|
assign_ipv4: event.target.checked,
|
||||||
|
public_ipv4s: event.target.checked ? form.public_ipv4s : [],
|
||||||
|
...(event.target.checked ? { assign_nat: false, port_mapping_count: 0, extra_ports: [] } : {}),
|
||||||
|
})}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
<span className="min-w-0">
|
<span className="min-w-0">
|
||||||
@@ -429,6 +434,7 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
|||||||
assign_nat: checked,
|
assign_nat: checked,
|
||||||
port_mapping_count: checked ? Math.max(2, form.port_mapping_count || 2) : 0,
|
port_mapping_count: checked ? Math.max(2, form.port_mapping_count || 2) : 0,
|
||||||
extra_ports: [],
|
extra_ports: [],
|
||||||
|
...(checked ? { assign_ipv4: false, public_ipv4s: [], ipv4_count: 0 } : {}),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
@@ -680,9 +686,10 @@ function validateResourceInputs(form: CreateContainerRequest, maxVCPU: number, m
|
|||||||
|
|
||||||
function normalizeCreateForm(form: CreateContainerRequest): CreateContainerRequest {
|
function normalizeCreateForm(form: CreateContainerRequest): CreateContainerRequest {
|
||||||
const normalized = applyTemplateDefaults(form)
|
const normalized = applyTemplateDefaults(form)
|
||||||
const wantsNAT = normalized.assign_nat !== false
|
|
||||||
const wantsIPv4 = !!normalized.assign_ipv4
|
const wantsIPv4 = !!normalized.assign_ipv4
|
||||||
const wantsIPv6 = !!normalized.assign_ipv6
|
const wantsIPv6 = !!normalized.assign_ipv6
|
||||||
|
// IPv4 and NAT are mutually exclusive
|
||||||
|
const wantsNAT = wantsIPv4 ? false : normalized.assign_nat !== false
|
||||||
const linuxTemplate = !isWindowsTemplate(normalized.template_id)
|
const linuxTemplate = !isWindowsTemplate(normalized.template_id)
|
||||||
const sshAuthMode = linuxTemplate ? (normalized.ssh_auth_mode || 'auto_password') : 'auto_password'
|
const sshAuthMode = linuxTemplate ? (normalized.ssh_auth_mode || 'auto_password') : 'auto_password'
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -773,23 +773,26 @@ export default function ContainerDetail() {
|
|||||||
const maxRAMMB = hostInfo?.ram.total_mb ? Number(hostInfo.ram.total_mb) : undefined
|
const maxRAMMB = hostInfo?.ram.total_mb ? Number(hostInfo.ram.total_mb) : undefined
|
||||||
const hasIndependentIPv4 = assignedIPv4List.length > 0
|
const hasIndependentIPv4 = assignedIPv4List.length > 0
|
||||||
const hasIndependentIPv6 = ipv6List.length > 0
|
const hasIndependentIPv6 = ipv6List.length > 0
|
||||||
const hasIndependentIP = hasIndependentIPv4 || hasIndependentIPv6
|
|
||||||
const defaultConnPort = isWindows ? 3389 : 22
|
const defaultConnPort = isWindows ? 3389 : 22
|
||||||
|
|
||||||
let publicEndpoint = '-'
|
let publicEndpoint = '-'
|
||||||
let sshCommand = ''
|
let sshCommand = ''
|
||||||
|
|
||||||
if (container.ssh_port > 0) {
|
if (hasIndependentIPv4) {
|
||||||
|
// Direct connection via independent IPv4 — all ports forwarded
|
||||||
|
publicEndpoint = `${assignedIPv4List[0]}:${defaultConnPort}`
|
||||||
|
if (!isWindows) {
|
||||||
|
sshCommand = `ssh root@${assignedIPv4List[0]}`
|
||||||
|
}
|
||||||
|
} else if (hasIndependentIPv6) {
|
||||||
|
publicEndpoint = `[${ipv6List[0]}]:${defaultConnPort}`
|
||||||
|
if (!isWindows) {
|
||||||
|
sshCommand = `ssh root@[${ipv6List[0]}]`
|
||||||
|
}
|
||||||
|
} else if (container.ssh_port > 0) {
|
||||||
// NAT port mapping mode
|
// NAT port mapping mode
|
||||||
publicEndpoint = `${publicHost}:${container.ssh_port}`
|
publicEndpoint = `${publicHost}:${container.ssh_port}`
|
||||||
sshCommand = `ssh -p ${container.ssh_port} root@${publicHost}`
|
sshCommand = `ssh -p ${container.ssh_port} root@${publicHost}`
|
||||||
} else if (hasIndependentIP) {
|
|
||||||
// Direct connection via independent IPv4 or IPv6
|
|
||||||
const connIP = hasIndependentIPv4 ? assignedIPv4List[0] : `[${ipv6List[0]}]`
|
|
||||||
publicEndpoint = `${connIP}:${defaultConnPort}`
|
|
||||||
if (!isWindows) {
|
|
||||||
sshCommand = `ssh root@${connIP}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const editingSSH = draft.index !== null && !!container.port_mappings?.[draft.index] && (
|
const editingSSH = draft.index !== null && !!container.port_mappings?.[draft.index] && (
|
||||||
container.port_mappings[draft.index].description === 'SSH' || container.port_mappings[draft.index].container_port === 22 ||
|
container.port_mappings[draft.index].description === 'SSH' || container.port_mappings[draft.index].container_port === 22 ||
|
||||||
@@ -879,7 +882,11 @@ export default function ContainerDetail() {
|
|||||||
<InfoTag color="blue">系统 {container.template}</InfoTag>
|
<InfoTag color="blue">系统 {container.template}</InfoTag>
|
||||||
<InfoTag color="slate">类型 {(container.virtualization || 'lxc').toUpperCase()}</InfoTag>
|
<InfoTag color="slate">类型 {(container.virtualization || 'lxc').toUpperCase()}</InfoTag>
|
||||||
<InfoTag color="emerald">内网 {container.ip || '-'}</InfoTag>
|
<InfoTag color="emerald">内网 {container.ip || '-'}</InfoTag>
|
||||||
|
{hasIndependentIPv4 ? (
|
||||||
|
<InfoTag color="amber">独立 IPv4 {assignedIPv4List[0]}</InfoTag>
|
||||||
|
) : (
|
||||||
<InfoTag color="amber">IPv4 NAT {hasNATQuota ? `${mappingCount} 条` : '未分配'}</InfoTag>
|
<InfoTag color="amber">IPv4 NAT {hasNATQuota ? `${mappingCount} 条` : '未分配'}</InfoTag>
|
||||||
|
)}
|
||||||
<InfoTag color="violet">{isWindows ? 'RDP' : 'SSH'} {publicEndpoint}</InfoTag>
|
<InfoTag color="violet">{isWindows ? 'RDP' : 'SSH'} {publicEndpoint}</InfoTag>
|
||||||
{isPolicyBlocked && <InfoTag color="red">策略封禁</InfoTag>}
|
{isPolicyBlocked && <InfoTag color="red">策略封禁</InfoTag>}
|
||||||
</div>
|
</div>
|
||||||
@@ -922,12 +929,14 @@ export default function ContainerDetail() {
|
|||||||
管理链接
|
管理链接
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
|
{!hasIndependentIPv4 && (
|
||||||
<>
|
<>
|
||||||
<ActionButton disabled={isSubUserPolicyBlocked} onClick={() => setShowNat(true)}>
|
<ActionButton disabled={isSubUserPolicyBlocked} onClick={() => setShowNat(true)}>
|
||||||
<Settings className="w-3.5 h-3.5" />
|
<Settings className="w-3.5 h-3.5" />
|
||||||
IPv4 NAT 管理
|
IPv4 NAT 管理
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
<ActionButton onClick={() => setShowSnapshots(true)} disabled={!!taskStatus || !!snapshotBusy || isSubUserPolicyBlocked}>
|
<ActionButton onClick={() => setShowSnapshots(true)} disabled={!!taskStatus || !!snapshotBusy || isSubUserPolicyBlocked}>
|
||||||
<Camera className="w-3.5 h-3.5" />
|
<Camera className="w-3.5 h-3.5" />
|
||||||
快照
|
快照
|
||||||
@@ -1437,7 +1446,7 @@ export default function ContainerDetail() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showNat && (
|
{showNat && !hasIndependentIPv4 && (
|
||||||
<Modal title="IPv4 NAT 端口管理" onClose={() => { setShowNat(false); setDraft(emptyDraft); setShowMappingEditor(false) }} wide extra={
|
<Modal title="IPv4 NAT 端口管理" onClose={() => { setShowNat(false); setDraft(emptyDraft); setShowMappingEditor(false) }} wide extra={
|
||||||
!isSubUser && canAddMapping && (
|
!isSubUser && canAddMapping && (
|
||||||
<button onClick={openAddMapping} className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-black text-white rounded-md text-xs hover:bg-gray-800">
|
<button onClick={openAddMapping} className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-black text-white rounded-md text-xs hover:bg-gray-800">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
||||||
import { Globe2, Network, Pencil, Plus, RefreshCw, Router, Save, Search, Server, Trash2, X } from 'lucide-react'
|
import { Globe2, Network, Pencil, Plus, RefreshCw, Router, Save, Search, Server, Trash2, X } from 'lucide-react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useLanguage, type Language } from '../contexts/LanguageContext'
|
import { useLanguage, type Language } from '../contexts/LanguageContext'
|
||||||
@@ -23,7 +23,8 @@ export default function Routing() {
|
|||||||
const [ipv4EditMode, setIPv4EditMode] = useState<'pool' | 'address'>('pool')
|
const [ipv4EditMode, setIPv4EditMode] = useState<'pool' | 'address'>('pool')
|
||||||
const [editingIPv4Address, setEditingIPv4Address] = useState('')
|
const [editingIPv4Address, setEditingIPv4Address] = useState('')
|
||||||
const [savingIPv4, setSavingIPv4] = useState(false)
|
const [savingIPv4, setSavingIPv4] = useState(false)
|
||||||
const [ipv4Draft, setIPv4Draft] = useState<PublicIPv4Info[]>([])
|
const [ipv4Draft, setIPv4Draft] = useState<(PublicIPv4Info & { _id: number })[]>([])
|
||||||
|
const nextDraftId = useRef(0)
|
||||||
const [nat4Page, setNat4Page] = useState(1)
|
const [nat4Page, setNat4Page] = useState(1)
|
||||||
const [ipv6Page, setIPv6Page] = useState(1)
|
const [ipv6Page, setIPv6Page] = useState(1)
|
||||||
const [nat4Search, setNat4Search] = useState('')
|
const [nat4Search, setNat4Search] = useState('')
|
||||||
@@ -54,7 +55,7 @@ export default function Routing() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editingIPv4) {
|
if (!editingIPv4) {
|
||||||
setIPv4Draft(publicIPv4s.map((ip) => ({ ...ip })))
|
setIPv4Draft(publicIPv4s.map((ip) => ({ ...ip, _id: nextDraftId.current++ })))
|
||||||
}
|
}
|
||||||
}, [editingIPv4, publicIPv4s])
|
}, [editingIPv4, publicIPv4s])
|
||||||
|
|
||||||
@@ -65,14 +66,14 @@ export default function Routing() {
|
|||||||
}, [ipv4Assignments])
|
}, [ipv4Assignments])
|
||||||
|
|
||||||
const startEditIPv4 = () => {
|
const startEditIPv4 = () => {
|
||||||
setIPv4Draft(publicIPv4s.map((ip) => ({ ...ip })))
|
setIPv4Draft(publicIPv4s.map((ip) => ({ ...ip, _id: nextDraftId.current++ })))
|
||||||
setIPv4EditMode('pool')
|
setIPv4EditMode('pool')
|
||||||
setEditingIPv4Address('')
|
setEditingIPv4Address('')
|
||||||
setEditingIPv4(true)
|
setEditingIPv4(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const startEditIPv4Address = (ip: PublicIPv4Info) => {
|
const startEditIPv4Address = (ip: PublicIPv4Info) => {
|
||||||
setIPv4Draft([{ ...ip }])
|
setIPv4Draft([{ ...ip, _id: nextDraftId.current++ }])
|
||||||
setIPv4EditMode('address')
|
setIPv4EditMode('address')
|
||||||
setEditingIPv4Address(ip.address)
|
setEditingIPv4Address(ip.address)
|
||||||
setEditingIPv4(true)
|
setEditingIPv4(true)
|
||||||
@@ -82,13 +83,14 @@ export default function Routing() {
|
|||||||
setEditingIPv4(false)
|
setEditingIPv4(false)
|
||||||
setIPv4EditMode('pool')
|
setIPv4EditMode('pool')
|
||||||
setEditingIPv4Address('')
|
setEditingIPv4Address('')
|
||||||
setIPv4Draft(publicIPv4s.map((ip) => ({ ...ip })))
|
setIPv4Draft([])
|
||||||
}
|
}
|
||||||
|
|
||||||
const addIPv4Row = () => {
|
const addIPv4Row = () => {
|
||||||
setIPv4Draft((items) => [
|
setIPv4Draft((items) => [
|
||||||
...items,
|
...items,
|
||||||
{
|
{
|
||||||
|
_id: nextDraftId.current++,
|
||||||
address: '',
|
address: '',
|
||||||
interface: defaultIPv4Interface,
|
interface: defaultIPv4Interface,
|
||||||
prefix: '',
|
prefix: '',
|
||||||
@@ -108,7 +110,7 @@ export default function Routing() {
|
|||||||
setSavingIPv4(true)
|
setSavingIPv4(true)
|
||||||
try {
|
try {
|
||||||
const draftItems = ipv4Draft
|
const draftItems = ipv4Draft
|
||||||
.map((item) => ({
|
.map(({ _id, ...item }) => ({
|
||||||
...item,
|
...item,
|
||||||
address: (item.address || '').trim(),
|
address: (item.address || '').trim(),
|
||||||
interface: (item.interface || defaultIPv4Interface).trim(),
|
interface: (item.interface || defaultIPv4Interface).trim(),
|
||||||
@@ -285,7 +287,7 @@ export default function Routing() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{ipv4Draft.map((item, index) => (
|
{ipv4Draft.map((item, index) => (
|
||||||
<tr key={`${item.address}-${index}`}>
|
<tr key={item._id}>
|
||||||
<td className="px-3 py-2"><input value={item.address || ''} onChange={(e) => updateIPv4Draft(index, { address: e.target.value })} placeholder={text.ipv4CIDR} className={smallInputClass} /></td>
|
<td className="px-3 py-2"><input value={item.address || ''} onChange={(e) => updateIPv4Draft(index, { address: e.target.value })} placeholder={text.ipv4CIDR} className={smallInputClass} /></td>
|
||||||
<td className="px-3 py-2"><input value={item.gateway || ''} onChange={(e) => updateIPv4Draft(index, { gateway: e.target.value })} placeholder={defaultIPv4Gateway || text.gateway} className={smallInputClass} /></td>
|
<td className="px-3 py-2"><input value={item.gateway || ''} onChange={(e) => updateIPv4Draft(index, { gateway: e.target.value })} placeholder={defaultIPv4Gateway || text.gateway} className={smallInputClass} /></td>
|
||||||
<td className="px-3 py-2"><input value={item.interface || ''} onChange={(e) => updateIPv4Draft(index, { interface: e.target.value })} placeholder={defaultIPv4Interface} className={smallInputClass} /></td>
|
<td className="px-3 py-2"><input value={item.interface || ''} onChange={(e) => updateIPv4Draft(index, { interface: e.target.value })} placeholder={defaultIPv4Interface} className={smallInputClass} /></td>
|
||||||
|
|||||||
Reference in New Issue
Block a user