ui(deploy): remove Exposed Ports field from Resources tab
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m25s
CI / docker (push) Successful in 1m4s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 41s

The field was cosmetic — `containerConfig.exposedPorts` only fed Docker's
`Config.ExposedPorts` metadata via `withExposedPorts(...)`. It never
published a host port and Traefik routing uses `appPort` from the label
builder, not this list. Users reading the label "Exposed Ports" reasonably
expected it to expose their port externally; removing it until real
multi-port Traefik routing lands (tracked in #149).

Backend DTOs (`ContainerRequest.exposedPorts`, `ConfigMerger.intList
("exposedPorts")`) are left in place so existing containerConfig JSONB
rows continue to deserialize. New writes from the UI will no longer
include the field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-23 17:51:46 +02:00
parent 0cf64b2928
commit ade1733418
7 changed files with 2 additions and 51 deletions

View File

@@ -66,7 +66,7 @@ describe('ConfigPanel', () => {
},
resources: {
memoryLimit: '256', memoryReserve: '', cpuRequest: '500', cpuLimit: '',
ports: [], appPort: '8080', replicas: '1', deployStrategy: 'blue-green',
appPort: '8080', replicas: '1', deployStrategy: 'blue-green',
stripPrefix: true, sslOffloading: true, runtimeType: 'auto', customArgs: '',
extraNetworks: [],
},

View File

@@ -28,7 +28,6 @@ describe('snapshotToForm', () => {
replicas: 3,
deploymentStrategy: 'rolling',
customEnvVars: { FOO: 'bar', BAZ: 'qux' },
exposedPorts: [8080, 9090],
},
sensitiveKeys: ['SECRET_KEY'],
};
@@ -36,7 +35,6 @@ describe('snapshotToForm', () => {
expect(result.resources.memoryLimit).toBe('1024');
expect(result.resources.replicas).toBe('3');
expect(result.resources.deployStrategy).toBe('rolling');
expect(result.resources.ports).toEqual([8080, 9090]);
expect(result.variables.envVars).toEqual([
{ key: 'FOO', value: 'bar' },
{ key: 'BAZ', value: 'qux' },

View File

@@ -36,7 +36,6 @@ export function snapshotToForm(
memoryReserve: c.memoryReserveMb != null ? String(c.memoryReserveMb) : defaults.resources.memoryReserve,
cpuRequest: c.cpuRequest !== undefined ? String(c.cpuRequest) : defaults.resources.cpuRequest,
cpuLimit: c.cpuLimit != null ? String(c.cpuLimit) : defaults.resources.cpuLimit,
ports: Array.isArray(c.exposedPorts) ? (c.exposedPorts as number[]) : defaults.resources.ports,
appPort: c.appPort !== undefined ? String(c.appPort) : defaults.resources.appPort,
replicas: c.replicas !== undefined ? String(c.replicas) : defaults.resources.replicas,
deployStrategy: (c.deploymentStrategy as string) ?? defaults.resources.deployStrategy,

View File

@@ -11,24 +11,11 @@ interface Props {
}
export function ResourcesTab({ value, onChange, disabled, isProd = false }: Props) {
const [newPort, setNewPort] = useState('');
const [newNetwork, setNewNetwork] = useState('');
const update = <K extends keyof ResourcesFormState>(key: K, v: ResourcesFormState[K]) =>
onChange({ ...value, [key]: v });
function addPort() {
const p = parseInt(newPort);
if (p && !value.ports.includes(p)) {
onChange({ ...value, ports: [...value.ports, p] });
setNewPort('');
}
}
function removePort(port: number) {
if (!disabled) update('ports', value.ports.filter((x) => x !== port));
}
function addNetwork() {
const v = newNetwork.trim();
if (v && !value.extraNetworks.includes(v)) {
@@ -123,35 +110,6 @@ export function ResourcesTab({ value, onChange, disabled, isProd = false }: Prop
<span className={styles.cellMeta}>millicores</span>
</div>
<span className={styles.configLabel}>Exposed Ports</span>
<div className={styles.portPills}>
{value.ports.map((p) => (
<span key={p} className={styles.portPill}>
{p}
<button
className={styles.portPillDelete}
disabled={disabled}
onClick={() => removePort(p)}
>
&times;
</button>
</span>
))}
<input
className={styles.portAddInput}
disabled={disabled}
placeholder="+ port"
value={newPort}
onChange={(e) => setNewPort(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addPort();
}
}}
/>
</div>
<span className={styles.configLabel}>App Port</span>
<Input
disabled={disabled}

View File

@@ -47,7 +47,6 @@ const defaultResources: ResourcesFormState = {
memoryReserve: '',
cpuRequest: '500',
cpuLimit: '',
ports: [],
appPort: '8080',
replicas: '1',
deployStrategy: 'blue-green',

View File

@@ -23,7 +23,6 @@ export interface ResourcesFormState {
memoryReserve: string;
cpuRequest: string;
cpuLimit: string;
ports: number[];
appPort: string;
replicas: string;
deployStrategy: string;
@@ -66,7 +65,7 @@ export const defaultForm: DeploymentPageFormState = {
},
resources: {
memoryLimit: '512', memoryReserve: '', cpuRequest: '500', cpuLimit: '',
ports: [], appPort: '8080', replicas: '1', deployStrategy: 'blue-green',
appPort: '8080', replicas: '1', deployStrategy: 'blue-green',
stripPrefix: true, sslOffloading: true, runtimeType: 'auto', customArgs: '',
extraNetworks: [],
},
@@ -108,7 +107,6 @@ export function useDeploymentPageState(
memoryReserve: merged.memoryReserveMb != null ? String(merged.memoryReserveMb) : defaultForm.resources.memoryReserve,
cpuRequest: String(merged.cpuRequest ?? defaultForm.resources.cpuRequest),
cpuLimit: merged.cpuLimit != null ? String(merged.cpuLimit) : defaultForm.resources.cpuLimit,
ports: Array.isArray(merged.exposedPorts) ? (merged.exposedPorts as number[]) : defaultForm.resources.ports,
appPort: String(merged.appPort ?? defaultForm.resources.appPort),
replicas: String(merged.replicas ?? defaultForm.resources.replicas),
deployStrategy: String(merged.deploymentStrategy ?? defaultForm.resources.deployStrategy),

View File

@@ -200,7 +200,6 @@ export default function AppDeploymentPage() {
memoryReserveMb: r.memoryReserve ? parseInt(r.memoryReserve) : null,
cpuRequest: r.cpuRequest ? parseInt(r.cpuRequest) : null,
cpuLimit: r.cpuLimit ? parseInt(r.cpuLimit) : null,
exposedPorts: r.ports,
customEnvVars: Object.fromEntries(
form.variables.envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value]),
),