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>
210 lines
7.0 KiB
TypeScript
210 lines
7.0 KiB
TypeScript
import { useState } from 'react';
|
||
import { Select, Input, Toggle } from '@cameleer/design-system';
|
||
import type { ResourcesFormState } from '../hooks/useDeploymentPageState';
|
||
import styles from '../AppDeploymentPage.module.css';
|
||
|
||
interface Props {
|
||
value: ResourcesFormState;
|
||
onChange: (next: ResourcesFormState) => void;
|
||
disabled?: boolean;
|
||
isProd?: boolean;
|
||
}
|
||
|
||
export function ResourcesTab({ value, onChange, disabled, isProd = false }: Props) {
|
||
const [newNetwork, setNewNetwork] = useState('');
|
||
|
||
const update = <K extends keyof ResourcesFormState>(key: K, v: ResourcesFormState[K]) =>
|
||
onChange({ ...value, [key]: v });
|
||
|
||
function addNetwork() {
|
||
const v = newNetwork.trim();
|
||
if (v && !value.extraNetworks.includes(v)) {
|
||
onChange({ ...value, extraNetworks: [...value.extraNetworks, v] });
|
||
setNewNetwork('');
|
||
}
|
||
}
|
||
|
||
function removeNetwork(network: string) {
|
||
if (!disabled) update('extraNetworks', value.extraNetworks.filter((x) => x !== network));
|
||
}
|
||
|
||
return (
|
||
<div className={styles.configGrid}>
|
||
<span className={styles.configLabel}>Runtime Type</span>
|
||
<Select
|
||
disabled={disabled}
|
||
value={value.runtimeType}
|
||
onChange={(e) => update('runtimeType', e.target.value)}
|
||
options={[
|
||
{ value: 'auto', label: 'Auto (detect from JAR)' },
|
||
{ value: 'spring-boot', label: 'Spring Boot' },
|
||
{ value: 'quarkus', label: 'Quarkus' },
|
||
{ value: 'plain-java', label: 'Plain Java' },
|
||
{ value: 'native', label: 'Native' },
|
||
]}
|
||
/>
|
||
|
||
<span className={styles.configLabel}>Custom Arguments</span>
|
||
<div>
|
||
<Input
|
||
disabled={disabled}
|
||
value={value.customArgs}
|
||
onChange={(e) => update('customArgs', e.target.value)}
|
||
placeholder="-Xmx256m -Dfoo=bar"
|
||
className={styles.inputLg}
|
||
/>
|
||
<span className={styles.configHint}>
|
||
{value.runtimeType === 'native'
|
||
? 'Arguments passed to the native binary'
|
||
: 'Additional JVM arguments appended to the start command'}
|
||
</span>
|
||
</div>
|
||
|
||
<span className={styles.configLabel}>Memory Limit</span>
|
||
<div className={styles.configInline}>
|
||
<Input
|
||
disabled={disabled}
|
||
value={value.memoryLimit}
|
||
onChange={(e) => update('memoryLimit', e.target.value)}
|
||
className={styles.inputLg}
|
||
placeholder="e.g. 512"
|
||
/>
|
||
<span className={styles.cellMeta}>MB</span>
|
||
</div>
|
||
|
||
<span className={styles.configLabel}>Memory Reserve</span>
|
||
<div>
|
||
<div className={styles.configInline}>
|
||
<Input
|
||
disabled={!isProd || disabled}
|
||
value={value.memoryReserve}
|
||
onChange={(e) => update('memoryReserve', e.target.value)}
|
||
placeholder="e.g. 256"
|
||
className={styles.inputLg}
|
||
/>
|
||
<span className={styles.cellMeta}>MB</span>
|
||
</div>
|
||
{!isProd && (
|
||
<span className={styles.configHint}>Available in production environments only</span>
|
||
)}
|
||
</div>
|
||
|
||
<span className={styles.configLabel}>CPU Request</span>
|
||
<Input
|
||
disabled={disabled}
|
||
value={value.cpuRequest}
|
||
onChange={(e) => update('cpuRequest', e.target.value)}
|
||
className={styles.inputLg}
|
||
placeholder="e.g. 500 millicores"
|
||
/>
|
||
|
||
<span className={styles.configLabel}>CPU Limit</span>
|
||
<div className={styles.configInline}>
|
||
<Input
|
||
disabled={disabled}
|
||
value={value.cpuLimit}
|
||
onChange={(e) => update('cpuLimit', e.target.value)}
|
||
placeholder="e.g. 1000"
|
||
className={styles.inputLg}
|
||
/>
|
||
<span className={styles.cellMeta}>millicores</span>
|
||
</div>
|
||
|
||
<span className={styles.configLabel}>App Port</span>
|
||
<Input
|
||
disabled={disabled}
|
||
value={value.appPort}
|
||
onChange={(e) => update('appPort', e.target.value)}
|
||
className={styles.inputLg}
|
||
placeholder="e.g. 8080"
|
||
/>
|
||
|
||
<span className={styles.configLabel}>Replicas</span>
|
||
<Input
|
||
disabled={disabled}
|
||
value={value.replicas}
|
||
onChange={(e) => update('replicas', e.target.value)}
|
||
className={styles.inputSm}
|
||
type="number"
|
||
placeholder="1"
|
||
/>
|
||
|
||
<span className={styles.configLabel}>Deploy Strategy</span>
|
||
<div>
|
||
<Select
|
||
disabled={disabled}
|
||
value={value.deployStrategy}
|
||
onChange={(e) => update('deployStrategy', e.target.value)}
|
||
options={[
|
||
{ value: 'blue-green', label: 'Blue/Green' },
|
||
{ value: 'rolling', label: 'Rolling' },
|
||
]}
|
||
/>
|
||
<span className={styles.configHint}>
|
||
{value.deployStrategy === 'rolling'
|
||
? 'Replace one replica at a time; peak = replicas + 1. Partial failure leaves remaining old replicas serving.'
|
||
: 'Start all new replicas, swap once all are healthy; peak = 2 × replicas. Partial failure preserves the previous deployment.'}
|
||
</span>
|
||
</div>
|
||
|
||
<span className={styles.configLabel}>Strip Path Prefix</span>
|
||
<div className={styles.configInline}>
|
||
<Toggle
|
||
checked={value.stripPrefix}
|
||
onChange={() => !disabled && update('stripPrefix', !value.stripPrefix)}
|
||
disabled={disabled}
|
||
/>
|
||
<span className={value.stripPrefix ? styles.toggleEnabled : styles.toggleDisabled}>
|
||
{value.stripPrefix ? 'Enabled' : 'Disabled'}
|
||
</span>
|
||
</div>
|
||
|
||
<span className={styles.configLabel}>SSL Offloading</span>
|
||
<div className={styles.configInline}>
|
||
<Toggle
|
||
checked={value.sslOffloading}
|
||
onChange={() => !disabled && update('sslOffloading', !value.sslOffloading)}
|
||
disabled={disabled}
|
||
/>
|
||
<span className={value.sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>
|
||
{value.sslOffloading ? 'Enabled' : 'Disabled'}
|
||
</span>
|
||
</div>
|
||
|
||
<span className={styles.configLabel}>Extra Networks</span>
|
||
<div>
|
||
<div className={styles.portPills}>
|
||
{value.extraNetworks.map((n) => (
|
||
<span key={n} className={styles.portPill}>
|
||
{n}
|
||
<button
|
||
className={styles.portPillDelete}
|
||
disabled={disabled}
|
||
onClick={() => removeNetwork(n)}
|
||
>
|
||
×
|
||
</button>
|
||
</span>
|
||
))}
|
||
<input
|
||
className={styles.portAddInput}
|
||
disabled={disabled}
|
||
placeholder="+ network"
|
||
value={newNetwork}
|
||
onChange={(e) => setNewNetwork(e.target.value)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
addNetwork();
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
<span className={styles.configHint}>
|
||
Additional Docker networks to join (e.g., monitoring, prometheus)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|