import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { useParams, useNavigate, useLocation } from 'react-router'; import { Badge, Button, ConfirmDialog, DataTable, Input, MonoText, SectionHeader, Select, Spinner, StatusDot, Toggle, useToast, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { useEnvironmentStore } from '../../api/environment-store'; import { useEnvironments } from '../../api/queries/admin/environments'; import { useAllApps, 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 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 type { CatalogApp, CatalogRoute } from '../../api/queries/catalog'; import { DeploymentProgress } from '../../components/DeploymentProgress'; import styles from './AppsTab.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`; } function timeAgo(date: string): string { const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000); if (seconds < 60) return `${seconds}s ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; return `${Math.floor(hours / 24)}d ago`; } const STATUS_COLORS: Record = { RUNNING: 'running', STARTING: 'warning', FAILED: 'error', STOPPED: 'auto', DEGRADED: 'warning', STOPPING: 'auto', }; const DEPLOY_STATUS_DOT: Record = { RUNNING: 'live', STARTING: 'running', DEGRADED: 'stale', STOPPING: 'stale', STOPPED: 'dead', FAILED: 'error', }; function slugify(name: string): string { return name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .substring(0, 100); } export default function AppsTab() { const { appId } = useParams<{ appId?: string }>(); const location = useLocation(); const selectedEnv = useEnvironmentStore((s) => s.environment); const { data: environments = [] } = useEnvironments(); if (location.pathname.endsWith('/apps/new')) return ; if (appId) return ; return ; } // ═══════════════════════════════════════════════════════════════════ // LIST VIEW // ═══════════════════════════════════════════════════════════════════ function AppListView({ selectedEnv, environments }: { selectedEnv: string | undefined; environments: Environment[] }) { const navigate = useNavigate(); const { data: allApps = [], isLoading: allLoading } = useAllApps(); const envId = useMemo(() => environments.find((e) => e.slug === selectedEnv)?.id, [environments, selectedEnv]); const { data: envApps = [], isLoading: envLoading } = useApps(envId); const apps = selectedEnv ? envApps : allApps; const isLoading = selectedEnv ? envLoading : allLoading; const envMap = useMemo(() => new Map(environments.map((e) => [e.id, e])), [environments]); type AppRow = App & { id: string; envName: string }; const rows: AppRow[] = useMemo( () => apps.map((a) => ({ ...a, envName: envMap.get(a.environmentId)?.displayName ?? '?' })), [apps, envMap], ); const columns: Column[] = useMemo(() => [ { key: 'displayName', header: 'Name', sortable: true, render: (_v: unknown, row: AppRow) => (
{row.displayName}
{row.slug}
), }, ...(!selectedEnv ? [{ key: 'envName', header: 'Environment', sortable: true, render: (_v: unknown, row: AppRow) => , }] : []), { key: 'updatedAt', header: 'Updated', sortable: true, render: (_v: unknown, row: AppRow) => {timeAgo(row.updatedAt)}, }, { key: 'createdAt', header: 'Created', sortable: true, render: (_v: unknown, row: AppRow) => {new Date(row.createdAt).toLocaleDateString()}, }, ], [selectedEnv]); if (isLoading) return ; return (
navigate(`/apps/${row.slug}`)} />
); } // ═══════════════════════════════════════════════════════════════════ // 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 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 [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables'>('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[] : []); }, [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; async function handleSubmit() { if (!canSubmit) return; setBusy(true); try { // 1. Create app setStep('Creating app...'); const app = await createApp.mutateAsync({ environmentId: envId, slug: slug.trim(), displayName: name.trim() }); // 2. Upload JAR setStep('Uploading JAR...'); const version = await uploadJar.mutateAsync({ appId: app.slug, file: 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, }; await updateContainerConfig.mutateAsync({ appId: app.slug, config: containerConfig }); // 4. Save agent config (will be pushed to agent on first connect) setStep('Saving monitoring config...'); await updateAgentConfig.mutateAsync({ application: slug.trim(), version: 0, engineLevel, payloadCaptureMode: payloadCapture, applicationLogLevel: appLogLevel, agentLogLevel, metricsEnabled, samplingRate: parseFloat(samplingRate) || 1.0, compressSuccess, tracedProcessors: {}, taps: [], tapVersion: 0, routeRecording: {}, }); // 5. Deploy (if requested) if (deploy) { setStep('Starting deployment...'); await createDeployment.mutateAsync({ appId: app.slug, appVersionId: version.id, environmentId: envId }); } 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 /{env?.slug ?? '...'}/{slug || '...'}/ 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 */}
{configTab === 'variables' && ( <> {envVars.map((v, i) => (
{ const next = [...envVars]; next[i] = { ...v, key: e.target.value }; setEnvVars(next); }} className={styles.envVarKey} placeholder="KEY" /> { const next = [...envVars]; next[i] = { ...v, value: e.target.value }; setEnvVars(next); }} className={styles.envVarValue} placeholder="value" />
))} )} {configTab === '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)} style={{ width: 70 }} /> setAppLogLevel(e.target.value)} options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} /> Agent Log Level setMetricsInterval(e.target.value)} style={{ width: 50 }} /> s
Sampling Rate setSamplingRate(e.target.value)} style={{ width: 80 }} /> 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
Memory Limit
setMemoryLimit(e.target.value)} style={{ width: 80 }} /> MB
Memory Reserve
setMemoryReserve(e.target.value)} placeholder="---" style={{ width: 80 }} /> MB
{!isProd && Available in production environments only}
CPU Request setCpuRequest(e.target.value)} style={{ width: 80 }} /> CPU Limit
setCpuLimit(e.target.value)} placeholder="e.g. 1000" style={{ width: 80 }} /> 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)} style={{ width: 80 }} /> Replicas setReplicas(e.target.value)} style={{ width: 60 }} type="number" /> Deploy Strategy
{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} /> ); } // ═══════════════════════════════════════════════════════════════════ // 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], ); return ( <> Deployments {deployments.length === 0 &&

No deployments yet.

} {deployments.length > 0 && ( {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 ( ); })}
Environment Version Status Replicas URL Deployed Actions
{d.replicaStates && d.replicaStates.length > 0 ? ( {d.replicaStates.filter((r) => r.status === 'RUNNING').length}/{d.replicaStates.length} ) : '—'} {d.status === 'RUNNING' ? ( {url} ) : ( {url} )} {d.deployedAt ? timeAgo(d.deployedAt) : '—'} {configChanged && } {canAct && } {canStart && } {!isSelectedEnv && switch env to manage}
)} {deployments.filter((d) => d.deployStage).map((d) => (
{d.containerName}
))} Versions ({versions.length}) {versions.length === 0 &&

No versions uploaded yet.

} {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 { data: agentConfig } = useApplicationConfig(app.slug); const updateAgentConfig = useUpdateApplicationConfig(); const updateContainerConfig = useUpdateContainerConfig(); const { data: catalog } = useCatalog(); const { data: processorToRoute = {} } = useProcessorRouteMapping(app.slug); 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); // 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); }, [agentConfig, merged]); useEffect(() => { syncFromServer(); }, [syncFromServer]); function handleCancel() { syncFromServer(); setEditing(false); } function updateTracedProcessor(processorId: string, mode: string) { setTracedDraft((prev) => { if (mode === 'REMOVE') { const next = { ...prev }; delete next[processorId]; return next; } return { ...prev, [processorId]: mode }; }); } function updateRouteRecording(routeId: string, recording: boolean) { setRouteRecordingDraft((prev) => ({ ...prev, [routeId]: recording })); } async function handleSave() { // Save agent config if (agentConfig) { try { await updateAgentConfig.mutateAsync({ ...agentConfig, engineLevel, payloadCaptureMode: payloadCapture, applicationLogLevel: appLogLevel, agentLogLevel, metricsEnabled, samplingRate: parseFloat(samplingRate) || 1.0, compressSuccess, tracedProcessors: tracedDraft, routeRecording: routeRecordingDraft, }); } 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, }; try { await updateContainerConfig.mutateAsync({ appId: 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.
)}
{configTab === 'variables' && ( <> {envVars.map((v, i) => (
{ const next = [...envVars]; next[i] = { ...v, key: e.target.value }; setEnvVars(next); }} className={styles.envVarKey} placeholder="KEY" /> { const next = [...envVars]; next[i] = { ...v, value: e.target.value }; setEnvVars(next); }} className={styles.envVarValue} placeholder="value" />
))} {editing && ( )} {envVars.length === 0 && !editing &&

No environment variables configured.

} )} {configTab === '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)} style={{ width: 70 }} /> setAppLogLevel(e.target.value)} options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} /> Agent Log Level setMetricsInterval(e.target.value)} style={{ width: 50 }} /> s
Sampling Rate setSamplingRate(e.target.value)} style={{ width: 80 }} /> 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' && ( <> {tracedCount} traced · {tapCount} taps {tracedTapRows.length > 0 ? columns={tracedTapColumns} data={tracedTapRows} pageSize={20} flush /> :

No processor traces or taps configured.

} )} {configTab === 'recording' && ( <> {recordingCount} of {routeRecordingRows.length} routes recording {routeRecordingRows.length > 0 ? columns={routeRecordingColumns} data={routeRecordingRows} pageSize={20} flush /> :

No routes found for this application.

} )} {configTab === 'resources' && ( <> {/* Container Resources */} Container Resources
Memory Limit
setMemoryLimit(e.target.value)} style={{ width: 80 }} /> MB
Memory Reserve
setMemoryReserve(e.target.value)} placeholder="---" style={{ width: 80 }} /> MB
{!isProd && Available in production environments only}
CPU Request setCpuRequest(e.target.value)} style={{ width: 80 }} /> CPU Limit
setCpuLimit(e.target.value)} placeholder="e.g. 1000" style={{ width: 80 }} /> 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)} style={{ width: 80 }} /> Replicas setReplicas(e.target.value)} style={{ width: 60 }} type="number" /> Deploy Strategy