From 5a7c0ce4bcd080c123c8ebbd54bbc90a0848c317 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:16:38 +0200 Subject: [PATCH] ui(deploy): delete CreateAppView + AppDetailView + ConfigSubTab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppsTab.tsx shrunk from 1387 to 109 lines — router now owns /apps/new and /apps/:slug via AppDeploymentPage; list-only file retained. Co-Authored-By: Claude Sonnet 4.6 --- ui/src/pages/AppsTab/AppsTab.tsx | 1290 +----------------------------- 1 file changed, 6 insertions(+), 1284 deletions(-) diff --git a/ui/src/pages/AppsTab/AppsTab.tsx b/ui/src/pages/AppsTab/AppsTab.tsx index 963ed156..d29ccfe9 100644 --- a/ui/src/pages/AppsTab/AppsTab.tsx +++ b/ui/src/pages/AppsTab/AppsTab.tsx @@ -1,60 +1,22 @@ -import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; -import { useParams, useNavigate, useLocation } from 'react-router'; +import { useMemo } from 'react'; +import { useNavigate } from 'react-router'; import { - AlertDialog, Badge, Button, - ConfirmDialog, DataTable, - EmptyState, - Input, - MonoText, - SectionHeader, - Select, StatusDot, - Tabs, - Tag, - Toggle, - useToast, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; -import { Shield, Info } from 'lucide-react'; -import { EnvEditor } from '../../components/EnvEditor'; import { useEnvironmentStore } from '../../api/environment-store'; import { useEnvironments } from '../../api/queries/admin/environments'; -import { - useApps, - useCreateApp, - useDeleteApp, - useAppVersions, - useUploadJar, - useDeployments, - useCreateDeployment, - useStopDeployment, - useUpdateContainerConfig, -} from '../../api/queries/admin/apps'; -import type { App, AppVersion, Deployment } from '../../api/queries/admin/apps'; +import { useApps } from '../../api/queries/admin/apps'; +import type { App } from '../../api/queries/admin/apps'; import type { Environment } from '../../api/queries/admin/environments'; -import { useApplicationConfig, useUpdateApplicationConfig, useProcessorRouteMapping } from '../../api/queries/commands'; -import type { ApplicationConfig, TapDefinition } from '../../api/queries/commands'; import { useCatalog } from '../../api/queries/catalog'; -import { useSensitiveKeys } from '../../api/queries/admin/sensitive-keys'; -import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog'; -import { DeploymentProgress } from '../../components/DeploymentProgress'; -import { StartupLogPanel } from '../../components/StartupLogPanel'; -import { timeAgo } from '../../utils/format-utils'; -import { applyTracedProcessorUpdate, applyRouteRecordingUpdate } from '../../utils/config-draft-utils'; +import type { CatalogApp } from '../../api/queries/catalog'; import { PageLoader } from '../../components/PageLoader'; +import { timeAgo } from '../../utils/format-utils'; import styles from './AppsTab.module.css'; -import sectionStyles from '../../styles/section-card.module.css'; -import tableStyles from '../../styles/table-section.module.css'; -import skStyles from '../Admin/SensitiveKeysPage.module.css'; - -function formatBytes(bytes: number): string { - if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`; - if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`; - return `${bytes} B`; -} const STATUS_COLORS: Record = { RUNNING: 'running', STARTING: 'warning', FAILED: 'error', STOPPED: 'auto', @@ -66,22 +28,9 @@ const DEPLOY_STATUS_DOT: Record(); - const location = useLocation(); const selectedEnv = useEnvironmentStore((s) => s.environment); const { data: environments = [] } = useEnvironments(); - - if (location.pathname.endsWith('/apps/new')) return ; - if (appId) return ; return ; } @@ -158,1230 +107,3 @@ function AppListView({ selectedEnv, environments }: { selectedEnv: string | unde ); } - -// ═══════════════════════════════════════════════════════════════════ -// CREATE APP PAGE -// ═══════════════════════════════════════════════════════════════════ - -function CreateAppView({ environments, selectedEnv }: { environments: Environment[]; selectedEnv: string | undefined }) { - const { toast } = useToast(); - const navigate = useNavigate(); - const createApp = useCreateApp(); - const uploadJar = useUploadJar(); - const createDeployment = useCreateDeployment(); - const { data: globalKeysConfig } = useSensitiveKeys(); - const globalKeys = globalKeysConfig?.keys ?? []; - const updateAgentConfig = useUpdateApplicationConfig(); - const updateContainerConfig = useUpdateContainerConfig(); - - const defaultEnvId = useMemo(() => environments.find((e) => e.slug === selectedEnv)?.id ?? (environments.length > 0 ? environments[0].id : ''), [environments, selectedEnv]); - - // Identity - const [name, setName] = useState(''); - const [slugEdited, setSlugEdited] = useState(false); - const [slug, setSlug] = useState(''); - const [envId, setEnvId] = useState(defaultEnvId); - const [file, setFile] = useState(null); - const [deploy, setDeploy] = useState(true); - const fileInputRef = useRef(null); - - // Monitoring - const [engineLevel, setEngineLevel] = useState('REGULAR'); - const [payloadCapture, setPayloadCapture] = useState('BOTH'); - const [payloadSize, setPayloadSize] = useState('4'); - const [payloadUnit, setPayloadUnit] = useState('KB'); - const [appLogLevel, setAppLogLevel] = useState('INFO'); - const [agentLogLevel, setAgentLogLevel] = useState('INFO'); - const [metricsEnabled, setMetricsEnabled] = useState(true); - const [metricsInterval, setMetricsInterval] = useState('60'); - const [samplingRate, setSamplingRate] = useState('1.0'); - const [compressSuccess, setCompressSuccess] = useState(false); - const [replayEnabled, setReplayEnabled] = useState(true); - const [routeControlEnabled, setRouteControlEnabled] = useState(true); - - // Resources - const env = useMemo(() => environments.find((e) => e.id === envId), [environments, envId]); - const isProd = env?.production ?? false; - const defaults = env?.defaultContainerConfig ?? {}; - const [memoryLimit, setMemoryLimit] = useState(String(defaults.memoryLimitMb ?? 512)); - const [memoryReserve, setMemoryReserve] = useState(String(defaults.memoryReserveMb ?? '')); - const [cpuRequest, setCpuRequest] = useState(String(defaults.cpuRequest ?? 500)); - const [cpuLimit, setCpuLimit] = useState(String(defaults.cpuLimit ?? '')); - const [ports, setPorts] = useState(Array.isArray(defaults.exposedPorts) ? defaults.exposedPorts as number[] : []); - const [newPort, setNewPort] = useState(''); - const [envVars, setEnvVars] = useState<{ key: string; value: string }[]>([]); - const [appPort, setAppPort] = useState('8080'); - const [replicas, setReplicas] = useState('1'); - const [deployStrategy, setDeployStrategy] = useState('blue-green'); - const [stripPrefix, setStripPrefix] = useState(true); - const [sslOffloading, setSslOffloading] = useState(true); - const [runtimeType, setRuntimeType] = useState(String(defaults.runtimeType ?? 'auto')); - const [customArgs, setCustomArgs] = useState(String(defaults.customArgs ?? '')); - const [extraNetworks, setExtraNetworks] = useState(Array.isArray(defaults.extraNetworks) ? defaults.extraNetworks as string[] : []); - const [newNetwork, setNewNetwork] = useState(''); - - // Sensitive keys - const [sensitiveKeys, setSensitiveKeys] = useState([]); - const [newSensitiveKey, setNewSensitiveKey] = useState(''); - - const [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables' | 'sensitive-keys'>('monitoring'); - const [busy, setBusy] = useState(false); - const [step, setStep] = useState(''); - - // Reset resource defaults when environment changes - useEffect(() => { - const d = environments.find((e) => e.id === envId)?.defaultContainerConfig ?? {}; - setMemoryLimit(String(d.memoryLimitMb ?? 512)); - setMemoryReserve(String(d.memoryReserveMb ?? '')); - setCpuRequest(String(d.cpuRequest ?? 500)); - setCpuLimit(String(d.cpuLimit ?? '')); - setPorts(Array.isArray(d.exposedPorts) ? d.exposedPorts as number[] : []); - setRuntimeType(String(d.runtimeType ?? 'auto')); - setCustomArgs(String(d.customArgs ?? '')); - setExtraNetworks(Array.isArray(d.extraNetworks) ? d.extraNetworks as string[] : []); - }, [envId, environments]); - - useEffect(() => { - if (!slugEdited) setSlug(slugify(name)); - }, [name, slugEdited]); - - function addPort() { - const p = parseInt(newPort); - if (p && !ports.includes(p)) { setPorts([...ports, p]); setNewPort(''); } - } - - const canSubmit = name.trim() && slug.trim() && envId && (file || !deploy); - - async function handleSubmit() { - if (!canSubmit) return; - setBusy(true); - try { - // 1. Create app - setStep('Creating app...'); - const app = await createApp.mutateAsync({ envSlug: selectedEnv!, slug: slug.trim(), displayName: name.trim() }); - - // 2. Upload JAR (if provided) - let version: AppVersion | null = null; - if (file) { - setStep('Uploading JAR...'); - version = await uploadJar.mutateAsync({ envSlug: selectedEnv!, appSlug: app.slug, file }); - } - - // 3. Save container config - setStep('Saving configuration...'); - const containerConfig: Record = { - memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null, - memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null, - cpuRequest: cpuRequest ? parseInt(cpuRequest) : null, - cpuLimit: cpuLimit ? parseInt(cpuLimit) : null, - exposedPorts: ports, - customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])), - appPort: appPort ? parseInt(appPort) : 8080, - replicas: replicas ? parseInt(replicas) : 1, - deploymentStrategy: deployStrategy, - stripPathPrefix: stripPrefix, - sslOffloading: sslOffloading, - runtimeType: runtimeType, - customArgs: customArgs || null, - extraNetworks: extraNetworks, - }; - await updateContainerConfig.mutateAsync({ envSlug: selectedEnv!, appSlug: app.slug, config: containerConfig }); - - // 4. Save agent config (will be pushed to agent on first connect) - setStep('Saving monitoring config...'); - await updateAgentConfig.mutateAsync({ - config: { - application: slug.trim(), - version: 0, - engineLevel, - payloadCaptureMode: payloadCapture, - applicationLogLevel: appLogLevel, - agentLogLevel, - metricsEnabled, - samplingRate: parseFloat(samplingRate) || 1.0, - compressSuccess, - tracedProcessors: {}, - taps: [], - tapVersion: 0, - routeRecording: {}, - sensitiveKeys: sensitiveKeys.length > 0 ? sensitiveKeys : undefined, - }, - environment: selectedEnv!, - }); - - // 5. Deploy (if requested and JAR was uploaded) - if (deploy && version) { - setStep('Starting deployment...'); - await createDeployment.mutateAsync({ envSlug: selectedEnv!, appSlug: app.slug, appVersionId: version.id }); - } - - toast({ title: deploy ? 'App created and deployed' : 'App created', description: name.trim(), variant: 'success' }); - navigate(`/apps/${app.slug}`); - } catch (e) { - toast({ title: 'Failed: ' + step, description: e instanceof Error ? e.message : 'Unknown error', variant: 'error', duration: 86_400_000 }); - } finally { - setBusy(false); - setStep(''); - } - } - - return ( -
-
-
-

Create Application

-
Configure and deploy a new application
-
-
- - -
-
- - {step &&
{step}
} - - {/* Identity Section */} -
- Identity & Artifact -
- Application Name - setName(e.target.value)} placeholder="e.g. Payment Gateway" disabled={busy} /> - - External URL - {(() => { - const domain = String(defaults.routingDomain ?? ''); - const envSlug = env?.slug ?? '...'; - const appSlug = slug || '...'; - if (defaults.routingMode === 'subdomain' && domain) { - return `https://${appSlug}-${envSlug}.${domain}/`; - } - const base = domain ? `https://${domain}` : window.location.origin; - return `${base}/${envSlug}/${appSlug}/`; - })()} - - Environment - setFile(e.target.files?.[0] ?? null)} /> - - {file && {file.name} ({formatBytes(file.size)})} -
- - Deploy -
- setDeploy(!deploy)} disabled={busy} /> - - {deploy ? 'Deploy immediately after creation' : 'Create only (deploy later)'} - -
-
-
- - {/* Config Tabs */} - setConfigTab(v as typeof configTab)} - /> - - {configTab === 'variables' && ( -
- Variables - -
- )} - - {configTab === 'monitoring' && ( -
- Monitoring -
- Engine Level - setPayloadCapture(e.target.value)} - options={[{ value: 'NONE', label: 'NONE' }, { value: 'INPUT', label: 'INPUT' }, { value: 'OUTPUT', label: 'OUTPUT' }, { value: 'BOTH', label: 'BOTH' }]} /> - - Max Payload Size -
- setPayloadSize(e.target.value)} className={styles.inputMd} placeholder="e.g. 4" /> - setAppLogLevel(e.target.value)} - options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} /> - - Agent Log Level - setMetricsInterval(e.target.value)} className={styles.inputXs} placeholder="60" /> - s -
- - Sampling Rate - setSamplingRate(e.target.value)} className={styles.inputLg} placeholder="1.0" /> - - Compress Success -
- !busy && setCompressSuccess(!compressSuccess)} disabled={busy} /> - {compressSuccess ? 'Enabled' : 'Disabled'} -
- - Replay -
- !busy && setReplayEnabled(!replayEnabled)} disabled={busy} /> - {replayEnabled ? 'Enabled' : 'Disabled'} -
- - Route Control -
- !busy && setRouteControlEnabled(!routeControlEnabled)} disabled={busy} /> - {routeControlEnabled ? 'Enabled' : 'Disabled'} -
- -
-
- )} - - {configTab === 'resources' && ( -
- Container Resources -
- Runtime Type - setCustomArgs(e.target.value)} - placeholder="-Xmx256m -Dfoo=bar" className={styles.inputLg} /> - - {runtimeType === 'native' ? 'Arguments passed to the native binary' : 'Additional JVM arguments appended to the start command'} - -
- - Memory Limit -
- setMemoryLimit(e.target.value)} className={styles.inputLg} placeholder="e.g. 512" /> - MB -
- - Memory Reserve -
-
- setMemoryReserve(e.target.value)} placeholder="e.g. 256" className={styles.inputLg} /> - MB -
- {!isProd && Available in production environments only} -
- - CPU Request - setCpuRequest(e.target.value)} className={styles.inputLg} placeholder="e.g. 500 millicores" /> - - CPU Limit -
- setCpuLimit(e.target.value)} placeholder="e.g. 1000" className={styles.inputLg} /> - millicores -
- - Exposed Ports -
- {ports.map((p) => ( - - {p} - - - ))} - setNewPort(e.target.value)} - onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addPort(); } }} /> -
- - App Port - setAppPort(e.target.value)} className={styles.inputLg} placeholder="e.g. 8080" /> - - Replicas - setReplicas(e.target.value)} className={styles.inputSm} type="number" placeholder="1" /> - - Deploy Strategy - setNewNetwork(e.target.value)} - onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); const v = newNetwork.trim(); if (v && !extraNetworks.includes(v)) { setExtraNetworks([...extraNetworks, v]); setNewNetwork(''); } } }} /> -
- Additional Docker networks to join (e.g., monitoring, prometheus) - - - - )} - - {configTab === 'sensitive-keys' && ( -
-
- - Agent built-in defaults -
-
- {['Authorization', 'Cookie', 'Set-Cookie', 'X-API-Key', 'X-Auth-Token', 'Proxy-Authorization'].map((key) => ( - - ))} -
- - {globalKeys.length > 0 && ( - <> -
-
- Global keys (enforced) - {globalKeys.length} -
-
- {globalKeys.map((key) => ( - - ))} -
- - )} - -
-
- Application-specific keys - {sensitiveKeys.length > 0 && {sensitiveKeys.length}} -
- -
- {sensitiveKeys.map((k, i) => ( - !busy && setSensitiveKeys(sensitiveKeys.filter((_, idx) => idx !== i))} /> - ))} - {sensitiveKeys.length === 0 && ( - No app-specific keys — agents use built-in defaults{globalKeys.length > 0 ? ' and global keys' : ''} - )} -
- -
- setNewSensitiveKey(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const v = newSensitiveKey.trim(); - if (v && !sensitiveKeys.some((k) => k.toLowerCase() === v.toLowerCase())) { - setSensitiveKeys([...sensitiveKeys, v]); - setNewSensitiveKey(''); - } - } - }} - placeholder="Add key or glob pattern (e.g. *password*)" - disabled={busy} - /> - -
- -
- - - The final masking configuration is: agent defaults + global keys + app-specific keys. - Supports exact header names and glob patterns. - -
-
- )} - - ); -} - -// ═══════════════════════════════════════════════════════════════════ -// DETAIL VIEW -// ═══════════════════════════════════════════════════════════════════ - -function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: string; environments: Environment[]; selectedEnv: string | undefined }) { - const { toast } = useToast(); - const navigate = useNavigate(); - const { data: envApps = [] } = useApps(selectedEnv); - const app = useMemo(() => envApps.find((a) => a.slug === appSlug), [envApps, appSlug]); - const { data: catalogApps } = useCatalog(selectedEnv); - const catalogEntry = useMemo(() => (catalogApps ?? []).find((c: CatalogApp) => c.slug === appSlug), [catalogApps, appSlug]); - const { data: versions = [] } = useAppVersions(selectedEnv, appSlug); - const { data: deployments = [] } = useDeployments(selectedEnv, appSlug); - const uploadJar = useUploadJar(); - const createDeployment = useCreateDeployment(); - const stopDeployment = useStopDeployment(); - const deleteApp = useDeleteApp(); - const fileInputRef = useRef(null); - const [subTab, setSubTab] = useState<'overview' | 'config'>('config'); - const [deleteConfirm, setDeleteConfirm] = useState(false); - const [stopTarget, setStopTarget] = useState<{ id: string; name: string } | null>(null); - - const envMap = useMemo(() => new Map(environments.map((e) => [e.id, e])), [environments]); - const sortedVersions = useMemo(() => [...versions].sort((a, b) => b.version - a.version), [versions]); - - if (!app) { - return ( -
- -
- -
-
- ); - } - - const env = envMap.get(app.environmentId); - - async function handleUpload(e: React.ChangeEvent) { - const file = e.target.files?.[0]; - if (!file) return; - try { - const v = await uploadJar.mutateAsync({ envSlug: selectedEnv!, appSlug, file }); - toast({ title: `Version ${v.version} uploaded`, description: file.name, variant: 'success' }); - } catch { toast({ title: 'Failed to upload JAR', variant: 'error', duration: 86_400_000 }); } - if (fileInputRef.current) fileInputRef.current.value = ''; - } - - async function handleDeploy(versionId: string) { - try { - await createDeployment.mutateAsync({ envSlug: selectedEnv!, appSlug, appVersionId: versionId }); - toast({ title: 'Deployment started', variant: 'success' }); - } catch { toast({ title: 'Failed to deploy application', variant: 'error', duration: 86_400_000 }); } - } - - function handleStop(deploymentId: string) { - setStopTarget({ id: deploymentId, name: app?.displayName ?? appSlug }); - } - - async function confirmStop() { - if (!stopTarget) return; - try { - await stopDeployment.mutateAsync({ envSlug: selectedEnv!, appSlug, deploymentId: stopTarget.id }); - toast({ title: 'Deployment stopped', variant: 'warning' }); - } catch { toast({ title: 'Failed to stop deployment', variant: 'error', duration: 86_400_000 }); } - setStopTarget(null); - } - - async function handleDelete() { - try { - await deleteApp.mutateAsync({ envSlug: selectedEnv!, appSlug }); - toast({ title: 'App deleted', variant: 'warning' }); - navigate('/apps'); - } catch { toast({ title: 'Delete failed', variant: 'error', duration: 86_400_000 }); } - } - - return ( -
-
-
-

- {catalogEntry && ( - - - - )} - {app.displayName} -

-
- {app.slug} · - {catalogEntry?.deployment && ( - <> · - )} -
-
-
- - - -
-
- - setSubTab(v as typeof subTab)} - /> - - {subTab === 'overview' && ( - - )} - {subTab === 'config' && ( - - )} - - setDeleteConfirm(false)} - onConfirm={handleDelete} - message={`Delete app "${app.displayName}"? All versions and deployments will be removed. This cannot be undone.`} - confirmText={app.slug} - loading={deleteApp.isPending} - /> - setStopTarget(null)} - onConfirm={confirmStop} - title="Stop deployment?" - description={`Stop deployment for "${stopTarget?.name}"? This will take the service offline.`} - confirmLabel="Stop" - variant="warning" - /> -
- ); -} - -// ═══════════════════════════════════════════════════════════════════ -// OVERVIEW SUB-TAB -// ═══════════════════════════════════════════════════════════════════ - -function OverviewSubTab({ app, deployments, versions, environments, envMap, selectedEnv, onDeploy, onStop }: { - app: App; deployments: Deployment[]; versions: AppVersion[]; - environments: Environment[]; envMap: Map; - selectedEnv: string | undefined; - onDeploy: (versionId: string, envId: string) => void; - onStop: (deploymentId: string) => void; -}) { - // Determine which env slug is selected - const selectedEnvId = useMemo( - () => selectedEnv ? environments.find((e) => e.slug === selectedEnv)?.id : undefined, - [environments, selectedEnv], - ); - - type DeploymentRow = Deployment & { - dEnv: Environment | undefined; - version: AppVersion | undefined; - isSelectedEnv: boolean; - canAct: boolean; - canStart: boolean; - configChanged: boolean; - url: string; - }; - - const deploymentRows: DeploymentRow[] = useMemo(() => deployments.map((d) => { - const dEnv = envMap.get(d.environmentId); - const version = versions.find((v) => v.id === d.appVersionId); - const isSelectedEnv = !selectedEnvId || d.environmentId === selectedEnvId; - const canAct = isSelectedEnv && (d.status === 'RUNNING' || d.status === 'STARTING'); - const canStart = isSelectedEnv && d.status === 'STOPPED'; - const configChanged = canAct && !!d.deployedAt && new Date(app.updatedAt) > new Date(d.deployedAt); - const url = dEnv ? `/${dEnv.slug}/${app.slug}/` : ''; - return { ...d, dEnv, version, isSelectedEnv, canAct, canStart, configChanged, url }; - }), [deployments, envMap, versions, selectedEnvId, app]); - - const deploymentColumns: Column[] = useMemo(() => [ - { key: 'environmentId', header: 'Environment', render: (_v, row) => ( - - - - )}, - { key: 'appVersionId', header: 'Version', render: (_v, row) => ( - - - - )}, - { key: 'status', header: 'Status', render: (_v, row) => ( -
- - -
- )}, - { key: 'replicaStates', header: 'Replicas', render: (_v, row) => ( - - {row.replicaStates && row.replicaStates.length > 0 - ? {row.replicaStates.filter((r) => r.status === 'RUNNING').length}/{row.replicaStates.length} - : <>{'—'}} - - )}, - { key: 'url' as any, header: 'URL', render: (_v, row) => ( - - {row.status === 'RUNNING' - ? {row.url} - : {row.url}} - - )}, - { key: 'deployedAt', header: 'Deployed', render: (_v, row) => ( - - {row.deployedAt ? timeAgo(row.deployedAt) : '—'} - - )}, - { key: 'actions' as any, header: '', render: (_v, row) => ( -
- {row.configChanged && } - {row.canAct && } - {row.canStart && } - {!row.isSelectedEnv && switch env to manage} -
- )}, - ], [onDeploy, onStop]); - - return ( - <> -
-
- Deployments -
- {deploymentRows.length === 0 - ? - : columns={deploymentColumns} data={deploymentRows} flush /> - } -
- {deployments.filter((d) => d.deployStage || d.status === 'FAILED').map((d) => ( -
- {d.containerName} - - e.id === d.environmentId)?.slug ?? ''} /> -
- ))} - - Versions ({versions.length}) - {versions.length === 0 && } - {versions.map((v) => ( - onDeploy(v.id, envId)} /> - ))} - - ); -} - -function VersionRow({ version, environments, onDeploy }: { version: AppVersion; environments: Environment[]; onDeploy: (envId: string) => void }) { - const [deployEnv, setDeployEnv] = useState(''); - return ( -
- - {version.jarFilename} ({formatBytes(version.jarSizeBytes)}) - {version.jarChecksum.substring(0, 8)} - {timeAgo(version.uploadedAt)} - - -
- ); -} - -// ═══════════════════════════════════════════════════════════════════ -// CONFIGURATION SUB-TAB -// ═══════════════════════════════════════════════════════════════════ - -interface TracedTapRow { id: string; processorId: string; captureMode: string | null; taps: TapDefinition[]; } -interface RouteRecordingRow { id: string; routeId: string; recording: boolean; } - -function ConfigSubTab({ app, environment }: { app: App; environment?: Environment }) { - const { toast } = useToast(); - const navigate = useNavigate(); - const envSlug = environment?.slug; - const { data: agentConfig } = useApplicationConfig(app.slug, envSlug); - const updateAgentConfig = useUpdateApplicationConfig(); - const updateContainerConfig = useUpdateContainerConfig(); - const { data: catalog } = useCatalog(); - const { data: processorToRoute = {} } = useProcessorRouteMapping(app.slug, envSlug); - const isProd = environment?.production ?? false; - const [editing, setEditing] = useState(false); - const [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables' | 'traces' | 'recording'>('monitoring'); - - const appRoutes: CatalogRoute[] = useMemo(() => { - if (!catalog) return []; - const entry = (catalog as CatalogApp[]).find((e) => e.slug === app.slug); - return entry?.routes ?? []; - }, [catalog, app.slug]); - - // Agent config state - const [engineLevel, setEngineLevel] = useState('REGULAR'); - const [payloadCapture, setPayloadCapture] = useState('BOTH'); - const [payloadSize, setPayloadSize] = useState('4'); - const [payloadUnit, setPayloadUnit] = useState('KB'); - const [appLogLevel, setAppLogLevel] = useState('INFO'); - const [agentLogLevel, setAgentLogLevel] = useState('INFO'); - const [metricsEnabled, setMetricsEnabled] = useState(true); - const [metricsInterval, setMetricsInterval] = useState('60'); - const [samplingRate, setSamplingRate] = useState('1.0'); - const [replayEnabled, setReplayEnabled] = useState(true); - const [routeControlEnabled, setRouteControlEnabled] = useState(true); - const [compressSuccess, setCompressSuccess] = useState(false); - const [tracedDraft, setTracedDraft] = useState>({}); - const [routeRecordingDraft, setRouteRecordingDraft] = useState>({}); - - // Container config state - const defaults = environment?.defaultContainerConfig ?? {}; - const merged = useMemo(() => ({ ...defaults, ...app.containerConfig }), [defaults, app.containerConfig]); - const [memoryLimit, setMemoryLimit] = useState('512'); - const [memoryReserve, setMemoryReserve] = useState(''); - const [cpuRequest, setCpuRequest] = useState('500'); - const [cpuLimit, setCpuLimit] = useState(''); - const [ports, setPorts] = useState([]); - const [newPort, setNewPort] = useState(''); - const [envVars, setEnvVars] = useState<{ key: string; value: string }[]>([]); - const [appPort, setAppPort] = useState('8080'); - const [replicas, setReplicas] = useState('1'); - const [deployStrategy, setDeployStrategy] = useState('blue-green'); - const [stripPrefix, setStripPrefix] = useState(true); - const [sslOffloading, setSslOffloading] = useState(true); - const [runtimeType, setRuntimeType] = useState(String(merged.runtimeType ?? 'auto')); - const [customArgs, setCustomArgs] = useState(String(merged.customArgs ?? '')); - const [extraNetworks, setExtraNetworks] = useState(Array.isArray(merged.extraNetworks) ? merged.extraNetworks as string[] : []); - const [newNetwork, setNewNetwork] = useState(''); - - // Versions query for runtime detection hints - const { data: versions = [] } = useAppVersions(environment?.slug, app.slug); - const latestVersion = versions?.[0] ?? null; - - // Sync from server data - const syncFromServer = useCallback(() => { - if (agentConfig) { - setEngineLevel(agentConfig.engineLevel ?? 'REGULAR'); - setPayloadCapture(agentConfig.payloadCaptureMode ?? 'BOTH'); - setPayloadSize('4'); setPayloadUnit('KB'); - setAppLogLevel(agentConfig.applicationLogLevel ?? 'INFO'); - setAgentLogLevel(agentConfig.agentLogLevel ?? 'INFO'); - setMetricsEnabled(agentConfig.metricsEnabled); - setSamplingRate(String(agentConfig.samplingRate)); - setCompressSuccess(agentConfig.compressSuccess); - setTracedDraft({ ...agentConfig.tracedProcessors }); - setRouteRecordingDraft({ ...agentConfig.routeRecording }); - } - setMemoryLimit(String(merged.memoryLimitMb ?? 512)); - setMemoryReserve(String(merged.memoryReserveMb ?? '')); - setCpuRequest(String(merged.cpuRequest ?? 500)); - setCpuLimit(String(merged.cpuLimit ?? '')); - setPorts(Array.isArray(merged.exposedPorts) ? merged.exposedPorts as number[] : []); - const vars = merged.customEnvVars as Record | undefined; - setEnvVars(vars ? Object.entries(vars).map(([key, value]) => ({ key, value })) : []); - setAppPort(String(merged.appPort ?? 8080)); - setReplicas(String(merged.replicas ?? 1)); - setDeployStrategy(String(merged.deploymentStrategy ?? 'blue-green')); - setStripPrefix(merged.stripPathPrefix !== false); - setSslOffloading(merged.sslOffloading !== false); - setRuntimeType(String(merged.runtimeType ?? 'auto')); - setCustomArgs(String(merged.customArgs ?? '')); - setExtraNetworks(Array.isArray(merged.extraNetworks) ? merged.extraNetworks as string[] : []); - }, [agentConfig, merged]); - - useEffect(() => { syncFromServer(); }, [syncFromServer]); - - function handleCancel() { - syncFromServer(); - setEditing(false); - } - - function updateTracedProcessor(processorId: string, mode: string) { - setTracedDraft((prev) => applyTracedProcessorUpdate(prev, processorId, mode)); - } - - function updateRouteRecording(routeId: string, recording: boolean) { - setRouteRecordingDraft((prev) => applyRouteRecordingUpdate(prev, routeId, recording)); - } - - async function handleSave() { - // Save agent config - if (agentConfig) { - try { - await updateAgentConfig.mutateAsync({ - config: { - ...agentConfig, - engineLevel, payloadCaptureMode: payloadCapture, - applicationLogLevel: appLogLevel, agentLogLevel, - metricsEnabled, samplingRate: parseFloat(samplingRate) || 1.0, - compressSuccess, - tracedProcessors: tracedDraft, - routeRecording: routeRecordingDraft, - }, - environment: environment?.slug, - }); - } catch { toast({ title: 'Failed to save agent config', variant: 'error', duration: 86_400_000 }); return; } - } - - // Save container config - const containerConfig: Record = { - memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null, - memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null, - cpuRequest: cpuRequest ? parseInt(cpuRequest) : null, - cpuLimit: cpuLimit ? parseInt(cpuLimit) : null, - exposedPorts: ports, - customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])), - appPort: appPort ? parseInt(appPort) : 8080, - replicas: replicas ? parseInt(replicas) : 1, - deploymentStrategy: deployStrategy, - stripPathPrefix: stripPrefix, - sslOffloading: sslOffloading, - runtimeType: runtimeType, - customArgs: customArgs || null, - extraNetworks: extraNetworks, - }; - try { - await updateContainerConfig.mutateAsync({ envSlug: environment?.slug ?? '', appSlug: app.slug, config: containerConfig }); - toast({ title: 'Configuration saved', description: 'Redeploy to apply changes to running deployments.', variant: 'success' }); - setEditing(false); - } catch { toast({ title: 'Failed to save container config', variant: 'error', duration: 86_400_000 }); } - } - - function addPort() { - const p = parseInt(newPort); - if (p && !ports.includes(p)) { setPorts([...ports, p]); setNewPort(''); } - } - - // Traces & Taps - const tracedTapRows: TracedTapRow[] = useMemo(() => { - const traced = editing ? tracedDraft : (agentConfig?.tracedProcessors ?? {}); - const taps = agentConfig?.taps ?? []; - const pids = new Set([...Object.keys(traced), ...taps.map(t => t.processorId)]); - return Array.from(pids).sort().map(pid => ({ id: pid, processorId: pid, captureMode: traced[pid] ?? null, taps: taps.filter(t => t.processorId === pid) })); - }, [editing, tracedDraft, agentConfig?.tracedProcessors, agentConfig?.taps]); - - const tracedCount = useMemo(() => Object.keys(editing ? tracedDraft : (agentConfig?.tracedProcessors ?? {})).length, [editing, tracedDraft, agentConfig?.tracedProcessors]); - const tapCount = agentConfig?.taps?.length ?? 0; - - const tracedTapColumns: Column[] = useMemo(() => [ - { key: 'route' as any, header: 'Route', render: (_v: unknown, row: TracedTapRow) => { - const routeId = processorToRoute[row.processorId]; - return routeId ? {routeId} : ; - }}, - { key: 'processorId', header: 'Processor', render: (_v: unknown, row: TracedTapRow) => {row.processorId} }, - { - key: 'captureMode', header: 'Capture', - render: (_v: unknown, row: TracedTapRow) => { - if (row.captureMode === null) return ; - if (editing) return ( - - ); - return ; - }, - }, - { - key: 'taps', header: 'Taps', - render: (_v: unknown, row: TracedTapRow) => row.taps.length === 0 - ? - :
{row.taps.map(t => ( - - ))}
, - }, - ...(editing ? [{ - key: '_remove' as const, header: '', width: '36px', - render: (_v: unknown, row: TracedTapRow) => row.captureMode === null ? null : ( - - ), - }] : []), - ], [editing, processorToRoute]); - - // Route Recording - const routeRecordingRows: RouteRecordingRow[] = useMemo(() => { - const rec = editing ? routeRecordingDraft : (agentConfig?.routeRecording ?? {}); - return appRoutes.map(r => ({ id: r.routeId, routeId: r.routeId, recording: rec[r.routeId] !== false })); - }, [editing, routeRecordingDraft, agentConfig?.routeRecording, appRoutes]); - - const recordingCount = routeRecordingRows.filter(r => r.recording).length; - - const routeRecordingColumns: Column[] = useMemo(() => [ - { key: 'routeId', header: 'Route', render: (_v: unknown, row: RouteRecordingRow) => {row.routeId} }, - { key: 'recording', header: 'Recording', width: '100px', render: (_v: unknown, row: RouteRecordingRow) => { if (editing) updateRouteRecording(row.routeId, !row.recording); }} disabled={!editing} /> }, - ], [editing, routeRecordingDraft]); - - return ( - <> - {!editing && ( -
- Configuration is read-only. Enter edit mode to make changes. - -
- )} - {editing && ( -
- Editing configuration. Changes are not saved until you click Save. -
- - -
-
- )} - - setConfigTab(v as typeof configTab)} - /> - - {configTab === 'variables' && ( -
- Variables - -
- )} - - {configTab === 'monitoring' && ( -
- Monitoring -
- Engine Level - setPayloadCapture(e.target.value)} - options={[{ value: 'NONE', label: 'NONE' }, { value: 'INPUT', label: 'INPUT' }, { value: 'OUTPUT', label: 'OUTPUT' }, { value: 'BOTH', label: 'BOTH' }]} /> - - Max Payload Size -
- setPayloadSize(e.target.value)} className={styles.inputMd} /> - setAppLogLevel(e.target.value)} - options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} /> - - Agent Log Level - setMetricsInterval(e.target.value)} className={styles.inputXs} /> - s -
- - Sampling Rate - setSamplingRate(e.target.value)} className={styles.inputLg} /> - - Compress Success -
- editing && setCompressSuccess(!compressSuccess)} disabled={!editing} /> - {compressSuccess ? 'Enabled' : 'Disabled'} -
- - Replay -
- editing && setReplayEnabled(!replayEnabled)} disabled={!editing} /> - {replayEnabled ? 'Enabled' : 'Disabled'} -
- - Route Control -
- editing && setRouteControlEnabled(!routeControlEnabled)} disabled={!editing} /> - {routeControlEnabled ? 'Enabled' : 'Disabled'} -
-
-
- )} - - {configTab === 'traces' && ( -
- Traces & Taps - {tracedCount} traced · {tapCount} taps - {tracedTapRows.length > 0 - ? columns={tracedTapColumns} data={tracedTapRows} pageSize={20} flush /> - : } -
- )} - - {configTab === 'recording' && ( -
- Route Recording - {recordingCount} of {routeRecordingRows.length} routes recording - {routeRecordingRows.length > 0 - ? columns={routeRecordingColumns} data={routeRecordingRows} pageSize={20} flush /> - : } -
- )} - - {configTab === 'resources' && ( -
- Container Resources -
- Runtime Type -
- setCustomArgs(e.target.value)} - placeholder="-Xmx256m -Dfoo=bar" className={styles.inputLg} /> - - {runtimeType === 'native' ? 'Arguments passed to the native binary' : 'Additional JVM arguments appended to the start command'} - -
- - Memory Limit -
- setMemoryLimit(e.target.value)} className={styles.inputLg} /> - MB -
- - Memory Reserve -
-
- setMemoryReserve(e.target.value)} placeholder="---" className={styles.inputLg} /> - MB -
- {!isProd && Available in production environments only} -
- - CPU Request - setCpuRequest(e.target.value)} className={styles.inputLg} /> - - CPU Limit -
- setCpuLimit(e.target.value)} placeholder="e.g. 1000" className={styles.inputLg} /> - millicores -
- - Exposed Ports -
- {ports.map((p) => ( - - {p} - - - ))} - setNewPort(e.target.value)} - onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addPort(); } }} /> -
- - App Port - setAppPort(e.target.value)} className={styles.inputLg} /> - - Replicas - setReplicas(e.target.value)} className={styles.inputSm} type="number" /> - - Deploy Strategy - setNewNetwork(e.target.value)} - onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); const v = newNetwork.trim(); if (v && !extraNetworks.includes(v)) { setExtraNetworks([...extraNetworks, v]); setNewNetwork(''); } } }} /> -
- Additional Docker networks to join (e.g., monitoring, prometheus) -
- - - )} - - ); -}