diff --git a/frontend/src/pages/ContainerDetail.tsx b/frontend/src/pages/ContainerDetail.tsx index 2a1a077..64edfa8 100644 --- a/frontend/src/pages/ContainerDetail.tsx +++ b/frontend/src/pages/ContainerDetail.tsx @@ -124,7 +124,7 @@ export default function ContainerDetail() { const vncFullscreenRef = useRef(null) const [vncFullscreen, setVncFullscreen] = useState(false) const [showNat, setShowNat] = useState(false) - const [showNatAdd, setShowNatAdd] = useState(false) + const [showMappingEditor, setShowMappingEditor] = useState(false) const [showExpiryEdit, setShowExpiryEdit] = useState(false) const [editExpiry, setEditExpiry] = useState('') const [savingExpiry, setSavingExpiry] = useState(false) @@ -517,21 +517,11 @@ export default function ContainerDetail() { if (isSubUser && container?.policy_blocked) return setDraft(emptyDraft) setShowNat(true) + setShowMappingEditor(true) } const openEditMapping = (pm: PortMapping, index: number) => { if (isSubUser && container?.policy_blocked) return - if (isSubUser) { - // Sub-user: only edit container_port in a simple modal - setDraft({ - index, - description: pm.description, - host_port: String(pm.host_port), - container_port: String(pm.container_port), - protocol: pm.protocol || 'all', - }) - return - } setDraft({ index, description: pm.description, @@ -539,6 +529,8 @@ export default function ContainerDetail() { container_port: String(pm.container_port), protocol: pm.protocol || 'all', }) + setShowNat(true) + setShowMappingEditor(true) } const submitMapping = async (): Promise => { @@ -1394,9 +1386,9 @@ export default function ContainerDetail() { )} {showNat && ( - { setShowNat(false); setDraft(emptyDraft); setShowNatAdd(false) }} wide extra={ - !isSubUser && canAddMapping && !showNatAdd && ( - ) @@ -1411,96 +1403,33 @@ export default function ContainerDetail() { )} {} : removeMapping} isSubUser={isSubUser} /> - {showNatAdd && !isSubUser && ( -
-
-

添加映射规则

- -
-
- - setDraft({ ...draft, description: e.target.value })} className={inputClass} placeholder="Web / API" /> - - - - - -
- setDraft({ ...draft, host_port: e.target.value })} className={inputClass + ' flex-1'} placeholder="默认同内部" /> - -
-
- - setDraft({ ...draft, container_port: e.target.value })} - className={inputClass} - placeholder="例如 80" - /> - -
- -
-
-
- )} - {/* Sub-user edit port modal: only container_port is editable */} - {draft.index !== null && isSubUser && ( -
-
-

修改端口映射

- -
-
- - - - - - - - - - - setDraft({ ...draft, container_port: e.target.value })} - className={inputClass} - placeholder="例如 80" - /> - -
-
- - -
-
- )}
)} + {showMappingEditor && ( + { setShowMappingEditor(false); setDraft(emptyDraft) }} + > + { setShowMappingEditor(false); setDraft(emptyDraft) }} + onSubmit={async () => { + if (await submitMapping()) { + setShowMappingEditor(false) + setDraft(emptyDraft) + } + }} + /> + + )} + {showSubUser && subUser && ( setShowSubUser(false)}>
@@ -1803,6 +1732,116 @@ function SnapshotTable({ snapshots, busy, onRestore, onDelete }: { ) } +function MappingEditor({ + draft, + setDraft, + isSubUser, + canAddMapping, + saving, + containerIdentifier, + onCancel, + onSubmit, +}: { + draft: MappingDraft + setDraft: (draft: MappingDraft) => void + isSubUser: boolean + canAddMapping: boolean + saving: boolean + containerIdentifier: string + onCancel: () => void + onSubmit: () => void +}) { + const isEditing = draft.index !== null + const updateDraft = (patch: Partial) => setDraft({ ...draft, ...patch }) + const disabledInputClass = 'w-full px-3 py-2 border border-gray-200 rounded-md text-sm text-gray-400 bg-gray-50' + + const fillRandomPort = async () => { + try { + const res = await api.get>(`/containers/${containerIdentifier}/random-port`) + const port = res.data.data?.port || 0 + if (port > 0) updateDraft({ host_port: String(port) }) + } catch { + // keep manual input available if random port lookup fails + } + } + + return ( +
+
+ + updateDraft({ description: e.target.value })} + className={isSubUser ? disabledInputClass : inputClass} + placeholder="Web / API" + /> + + + + {isSubUser ? ( + + ) : ( + + )} + + + + {isSubUser ? ( + + ) : ( +
+ updateDraft({ host_port: e.target.value })} + className={`${inputClass} flex-1`} + placeholder="默认同内部" + /> + +
+ )} +
+ + + updateDraft({ container_port: e.target.value })} + className={inputClass} + placeholder="例如 80" + /> + +
+ +
+ + +
+
+ ) +} + function MappingTable({ mappings, publicHost, onEdit, onDelete, compact = false, isSubUser = false }: { mappings: PortMapping[]; publicHost: string; onEdit: (pm: PortMapping, index: number) => void; onDelete: (index: number) => void; compact?: boolean; isSubUser?: boolean }) { if (mappings.length === 0) { return

暂无端口映射