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>
149 lines
5.7 KiB
TypeScript
149 lines
5.7 KiB
TypeScript
// ui/src/pages/AppsTab/AppDeploymentPage/hooks/useDeploymentPageState.ts
|
|
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
import type { ApplicationConfig } from '../../../../api/queries/commands';
|
|
import type { App } from '../../../../api/queries/admin/apps';
|
|
|
|
export interface MonitoringFormState {
|
|
engineLevel: string;
|
|
payloadCaptureMode: string;
|
|
payloadSize: string;
|
|
payloadUnit: string;
|
|
applicationLogLevel: string;
|
|
agentLogLevel: string;
|
|
metricsEnabled: boolean;
|
|
metricsInterval: string;
|
|
samplingRate: string;
|
|
compressSuccess: boolean;
|
|
replayEnabled: boolean;
|
|
routeControlEnabled: boolean;
|
|
}
|
|
|
|
export interface ResourcesFormState {
|
|
memoryLimit: string;
|
|
memoryReserve: string;
|
|
cpuRequest: string;
|
|
cpuLimit: string;
|
|
appPort: string;
|
|
replicas: string;
|
|
deployStrategy: string;
|
|
stripPrefix: boolean;
|
|
sslOffloading: boolean;
|
|
runtimeType: string;
|
|
customArgs: string;
|
|
extraNetworks: string[];
|
|
}
|
|
|
|
export interface VariablesFormState {
|
|
envVars: { key: string; value: string }[];
|
|
}
|
|
|
|
export interface SensitiveKeysFormState {
|
|
sensitiveKeys: string[];
|
|
}
|
|
|
|
export interface DeploymentPageFormState {
|
|
monitoring: MonitoringFormState;
|
|
resources: ResourcesFormState;
|
|
variables: VariablesFormState;
|
|
sensitiveKeys: SensitiveKeysFormState;
|
|
}
|
|
|
|
export const defaultForm: DeploymentPageFormState = {
|
|
monitoring: {
|
|
engineLevel: 'REGULAR',
|
|
payloadCaptureMode: 'BOTH',
|
|
payloadSize: '4',
|
|
payloadUnit: 'KB',
|
|
applicationLogLevel: 'INFO',
|
|
agentLogLevel: 'INFO',
|
|
metricsEnabled: true,
|
|
metricsInterval: '60',
|
|
samplingRate: '1.0',
|
|
compressSuccess: false,
|
|
replayEnabled: true,
|
|
routeControlEnabled: true,
|
|
},
|
|
resources: {
|
|
memoryLimit: '512', memoryReserve: '', cpuRequest: '500', cpuLimit: '',
|
|
appPort: '8080', replicas: '1', deployStrategy: 'blue-green',
|
|
stripPrefix: true, sslOffloading: true, runtimeType: 'auto', customArgs: '',
|
|
extraNetworks: [],
|
|
},
|
|
variables: { envVars: [] },
|
|
sensitiveKeys: { sensitiveKeys: [] },
|
|
};
|
|
|
|
export function useDeploymentPageState(
|
|
app: App | null,
|
|
agentConfig: ApplicationConfig | null,
|
|
envDefaults: Record<string, unknown>,
|
|
): {
|
|
form: DeploymentPageFormState;
|
|
setForm: React.Dispatch<React.SetStateAction<DeploymentPageFormState>>;
|
|
reset: () => void;
|
|
serverState: DeploymentPageFormState;
|
|
} {
|
|
const serverState = useMemo<DeploymentPageFormState>(() => {
|
|
const merged = { ...envDefaults, ...(app?.containerConfig ?? {}) } as Record<string, unknown>;
|
|
return {
|
|
monitoring: {
|
|
engineLevel: (agentConfig?.engineLevel as string) ?? defaultForm.monitoring.engineLevel,
|
|
payloadCaptureMode: (agentConfig?.payloadCaptureMode as string) ?? defaultForm.monitoring.payloadCaptureMode,
|
|
payloadSize: defaultForm.monitoring.payloadSize,
|
|
payloadUnit: defaultForm.monitoring.payloadUnit,
|
|
applicationLogLevel: (agentConfig?.applicationLogLevel as string) ?? defaultForm.monitoring.applicationLogLevel,
|
|
agentLogLevel: (agentConfig?.agentLogLevel as string) ?? defaultForm.monitoring.agentLogLevel,
|
|
metricsEnabled: agentConfig?.metricsEnabled ?? defaultForm.monitoring.metricsEnabled,
|
|
metricsInterval: defaultForm.monitoring.metricsInterval,
|
|
samplingRate: agentConfig?.samplingRate !== undefined
|
|
? String(agentConfig.samplingRate)
|
|
: defaultForm.monitoring.samplingRate,
|
|
compressSuccess: agentConfig?.compressSuccess ?? defaultForm.monitoring.compressSuccess,
|
|
replayEnabled: defaultForm.monitoring.replayEnabled,
|
|
routeControlEnabled: defaultForm.monitoring.routeControlEnabled,
|
|
},
|
|
resources: {
|
|
memoryLimit: String(merged.memoryLimitMb ?? defaultForm.resources.memoryLimit),
|
|
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,
|
|
appPort: String(merged.appPort ?? defaultForm.resources.appPort),
|
|
replicas: String(merged.replicas ?? defaultForm.resources.replicas),
|
|
deployStrategy: String(merged.deploymentStrategy ?? defaultForm.resources.deployStrategy),
|
|
stripPrefix: merged.stripPathPrefix !== false,
|
|
sslOffloading: merged.sslOffloading !== false,
|
|
runtimeType: String(merged.runtimeType ?? defaultForm.resources.runtimeType),
|
|
customArgs: String(merged.customArgs ?? defaultForm.resources.customArgs),
|
|
extraNetworks: Array.isArray(merged.extraNetworks) ? (merged.extraNetworks as string[]) : defaultForm.resources.extraNetworks,
|
|
},
|
|
variables: {
|
|
envVars: merged.customEnvVars
|
|
? Object.entries(merged.customEnvVars as Record<string, string>).map(([key, value]) => ({ key, value }))
|
|
: [],
|
|
},
|
|
sensitiveKeys: {
|
|
sensitiveKeys: Array.isArray(agentConfig?.sensitiveKeys)
|
|
? (agentConfig!.sensitiveKeys as string[])
|
|
: [],
|
|
},
|
|
};
|
|
}, [app, agentConfig, envDefaults]);
|
|
|
|
const [form, setForm] = useState<DeploymentPageFormState>(serverState);
|
|
const prevServerStateRef = useRef<DeploymentPageFormState>(serverState);
|
|
|
|
useEffect(() => {
|
|
// Only overwrite form if the current form value still matches the previous
|
|
// server state (i.e., the user has no local edits). Otherwise preserve
|
|
// user edits through background refetches.
|
|
setForm((current) => {
|
|
const hadLocalEdits =
|
|
JSON.stringify(current) !== JSON.stringify(prevServerStateRef.current);
|
|
prevServerStateRef.current = serverState;
|
|
return hadLocalEdits ? current : serverState;
|
|
});
|
|
}, [serverState]);
|
|
|
|
return { form, setForm, reset: () => setForm(serverState), serverState };
|
|
}
|