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:
@@ -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)}
|
||||
>
|
||||
×
|
||||
</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)}
|
||||
>
|
||||
×
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user