ui(deploy): extract ResourcesTab component

Pure presentational tab receiving ResourcesFormState via value/onChange.
Local useState buffers for newPort/newNetwork keep the "add next item"
inputs isolated from form state. isProd prop gates the memory-reserve field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 22:56:05 +02:00
parent 4f5a11f715
commit 5c48b780b2

View File

@@ -0,0 +1,244 @@
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 [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)) {
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}>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}
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>
<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.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)}
>
&times;
</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>
);
}