fix: wrap app config in section cards, replace manual table with DataTable

- Add sectionStyles and tableStyles imports to AppsTab.tsx
- Wrap CreateAppView identity section and each config tab (Monitoring,
  Resources, Variables) in sectionStyles.section cards
- Wrap ConfigSubTab config tabs (Monitoring, Resources, Variables,
  Traces & Taps, Route Recording) in sectionStyles.section cards
- Replace manual <table> in OverviewSubTab with DataTable inside a
  tableStyles.tableSection card wrapper; pre-compute enriched row data
  via useMemo; handle muted non-selected-env rows via inline opacity
- Remove unused .table, .table th, .table td, .table tr:hover td, and
  .mutedRow CSS rules from AppsTab.module.css

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-09 18:28:11 +02:00
parent ba53f91f4a
commit 3f9fd44ea5
2 changed files with 213 additions and 218 deletions

View File

@@ -119,38 +119,6 @@
gap: 8px; gap: 8px;
} }
/* Table */
.table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
.table th {
text-align: left;
padding: 8px 12px;
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--border-subtle);
}
.table td {
padding: 10px 12px;
border-bottom: 1px solid var(--border-subtle);
font-size: 13px;
vertical-align: middle;
}
.table tr:hover td {
background: var(--bg-hover);
}
.mutedRow td {
opacity: 0.45;
}
.mutedMono { .mutedMono {
font-family: var(--font-mono); font-family: var(--font-mono);

View File

@@ -40,6 +40,8 @@ import { DeploymentProgress } from '../../components/DeploymentProgress';
import { timeAgo } from '../../utils/format-utils'; import { timeAgo } from '../../utils/format-utils';
import { applyTracedProcessorUpdate, applyRouteRecordingUpdate } from '../../utils/config-draft-utils'; import { applyTracedProcessorUpdate, applyRouteRecordingUpdate } from '../../utils/config-draft-utils';
import styles from './AppsTab.module.css'; import styles from './AppsTab.module.css';
import sectionStyles from '../../styles/section-card.module.css';
import tableStyles from '../../styles/table-section.module.css';
function formatBytes(bytes: number): string { function formatBytes(bytes: number): string {
if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`; if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`;
@@ -290,35 +292,37 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
{step && <div className={styles.stepIndicator}>{step}</div>} {step && <div className={styles.stepIndicator}>{step}</div>}
{/* Identity Section */} {/* Identity Section */}
<SectionHeader>Identity & Artifact</SectionHeader> <div className={sectionStyles.section}>
<div className={styles.configGrid}> <SectionHeader>Identity & Artifact</SectionHeader>
<span className={styles.configLabel}>Application Name</span> <div className={styles.configGrid}>
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Payment Gateway" disabled={busy} /> <span className={styles.configLabel}>Application Name</span>
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Payment Gateway" disabled={busy} />
<span className={styles.configLabel}>External URL</span> <span className={styles.configLabel}>External URL</span>
<MonoText size="sm">/{env?.slug ?? '...'}/{slug || '...'}/</MonoText> <MonoText size="sm">/{env?.slug ?? '...'}/{slug || '...'}/</MonoText>
<span className={styles.configLabel}>Environment</span> <span className={styles.configLabel}>Environment</span>
<Select value={envId} onChange={(e) => setEnvId(e.target.value)} disabled={busy} <Select value={envId} onChange={(e) => setEnvId(e.target.value)} disabled={busy}
options={environments.filter((e) => e.enabled).map((e) => ({ value: e.id, label: `${e.displayName} (${e.slug})` }))} /> options={environments.filter((e) => e.enabled).map((e) => ({ value: e.id, label: `${e.displayName} (${e.slug})` }))} />
<span className={styles.configLabel}>Application JAR</span> <span className={styles.configLabel}>Application JAR</span>
<div className={styles.fileRow}> <div className={styles.fileRow}>
<input ref={fileInputRef} type="file" accept=".jar" <input ref={fileInputRef} type="file" accept=".jar"
className={styles.visuallyHidden} className={styles.visuallyHidden}
onChange={(e) => setFile(e.target.files?.[0] ?? null)} /> onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
<Button size="sm" variant="secondary" type="button" onClick={() => fileInputRef.current?.click()} disabled={busy}> <Button size="sm" variant="secondary" type="button" onClick={() => fileInputRef.current?.click()} disabled={busy}>
{file ? 'Change file' : 'Select JAR'} {file ? 'Change file' : 'Select JAR'}
</Button> </Button>
{file && <span className={styles.fileName}>{file.name} ({formatBytes(file.size)})</span>} {file && <span className={styles.fileName}>{file.name} ({formatBytes(file.size)})</span>}
</div> </div>
<span className={styles.configLabel}>Deploy</span> <span className={styles.configLabel}>Deploy</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Toggle checked={deploy} onChange={() => setDeploy(!deploy)} disabled={busy} /> <Toggle checked={deploy} onChange={() => setDeploy(!deploy)} disabled={busy} />
<span className={deploy ? styles.toggleEnabled : styles.toggleDisabled}> <span className={deploy ? styles.toggleEnabled : styles.toggleDisabled}>
{deploy ? 'Deploy immediately after creation' : 'Create only (deploy later)'} {deploy ? 'Deploy immediately after creation' : 'Create only (deploy later)'}
</span> </span>
</div>
</div> </div>
</div> </div>
@@ -334,7 +338,8 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
/> />
{configTab === 'variables' && ( {configTab === 'variables' && (
<> <div className={sectionStyles.section}>
<SectionHeader>Variables</SectionHeader>
{envVars.map((v, i) => ( {envVars.map((v, i) => (
<div key={i} className={styles.envVarRow}> <div key={i} className={styles.envVarRow}>
<Input disabled={busy} value={v.key} onChange={(e) => { <Input disabled={busy} value={v.key} onChange={(e) => {
@@ -348,68 +353,71 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
</div> </div>
))} ))}
<Button size="sm" variant="secondary" disabled={busy} onClick={() => setEnvVars([...envVars, { key: '', value: '' }])}>+ Add Variable</Button> <Button size="sm" variant="secondary" disabled={busy} onClick={() => setEnvVars([...envVars, { key: '', value: '' }])}>+ Add Variable</Button>
</> </div>
)} )}
{configTab === 'monitoring' && ( {configTab === 'monitoring' && (
<div className={styles.configGrid}> <div className={sectionStyles.section}>
<span className={styles.configLabel}>Engine Level</span> <SectionHeader>Monitoring</SectionHeader>
<Select disabled={busy} value={engineLevel} onChange={(e) => setEngineLevel(e.target.value)} <div className={styles.configGrid}>
options={[{ value: 'NONE', label: 'NONE' }, { value: 'MINIMAL', label: 'MINIMAL' }, { value: 'REGULAR', label: 'REGULAR' }, { value: 'COMPLETE', label: 'COMPLETE' }]} /> <span className={styles.configLabel}>Engine Level</span>
<Select disabled={busy} value={engineLevel} onChange={(e) => setEngineLevel(e.target.value)}
options={[{ value: 'NONE', label: 'NONE' }, { value: 'MINIMAL', label: 'MINIMAL' }, { value: 'REGULAR', label: 'REGULAR' }, { value: 'COMPLETE', label: 'COMPLETE' }]} />
<span className={styles.configLabel}>Payload Capture</span> <span className={styles.configLabel}>Payload Capture</span>
<Select disabled={busy} value={payloadCapture} onChange={(e) => setPayloadCapture(e.target.value)} <Select disabled={busy} value={payloadCapture} onChange={(e) => setPayloadCapture(e.target.value)}
options={[{ value: 'NONE', label: 'NONE' }, { value: 'INPUT', label: 'INPUT' }, { value: 'OUTPUT', label: 'OUTPUT' }, { value: 'BOTH', label: 'BOTH' }]} /> options={[{ value: 'NONE', label: 'NONE' }, { value: 'INPUT', label: 'INPUT' }, { value: 'OUTPUT', label: 'OUTPUT' }, { value: 'BOTH', label: 'BOTH' }]} />
<span className={styles.configLabel}>Max Payload Size</span> <span className={styles.configLabel}>Max Payload Size</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Input disabled={busy} value={payloadSize} onChange={(e) => setPayloadSize(e.target.value)} className={styles.inputMd} /> <Input disabled={busy} value={payloadSize} onChange={(e) => setPayloadSize(e.target.value)} className={styles.inputMd} />
<Select disabled={busy} value={payloadUnit} onChange={(e) => setPayloadUnit(e.target.value)} className={styles.inputXl} <Select disabled={busy} value={payloadUnit} onChange={(e) => setPayloadUnit(e.target.value)} className={styles.inputXl}
options={[{ value: 'bytes', label: 'bytes' }, { value: 'KB', label: 'KB' }, { value: 'MB', label: 'MB' }]} /> options={[{ value: 'bytes', label: 'bytes' }, { value: 'KB', label: 'KB' }, { value: 'MB', label: 'MB' }]} />
</div> </div>
<span className={styles.configLabel}>App Log Level</span> <span className={styles.configLabel}>App Log Level</span>
<Select disabled={busy} value={appLogLevel} onChange={(e) => setAppLogLevel(e.target.value)} <Select disabled={busy} value={appLogLevel} onChange={(e) => setAppLogLevel(e.target.value)}
options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} /> options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} />
<span className={styles.configLabel}>Agent Log Level</span> <span className={styles.configLabel}>Agent Log Level</span>
<Select disabled={busy} value={agentLogLevel} onChange={(e) => setAgentLogLevel(e.target.value)} <Select disabled={busy} value={agentLogLevel} onChange={(e) => setAgentLogLevel(e.target.value)}
options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} /> options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} />
<span className={styles.configLabel}>Metrics</span> <span className={styles.configLabel}>Metrics</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Toggle checked={metricsEnabled} onChange={() => !busy && setMetricsEnabled(!metricsEnabled)} disabled={busy} /> <Toggle checked={metricsEnabled} onChange={() => !busy && setMetricsEnabled(!metricsEnabled)} disabled={busy} />
<span className={metricsEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{metricsEnabled ? 'Enabled' : 'Disabled'}</span> <span className={metricsEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{metricsEnabled ? 'Enabled' : 'Disabled'}</span>
<span className={styles.cellMeta} style={{ marginLeft: 8 }}>Interval</span> <span className={styles.cellMeta} style={{ marginLeft: 8 }}>Interval</span>
<Input disabled={busy} value={metricsInterval} onChange={(e) => setMetricsInterval(e.target.value)} className={styles.inputXs} /> <Input disabled={busy} value={metricsInterval} onChange={(e) => setMetricsInterval(e.target.value)} className={styles.inputXs} />
<span className={styles.cellMeta}>s</span> <span className={styles.cellMeta}>s</span>
</div> </div>
<span className={styles.configLabel}>Sampling Rate</span> <span className={styles.configLabel}>Sampling Rate</span>
<Input disabled={busy} value={samplingRate} onChange={(e) => setSamplingRate(e.target.value)} className={styles.inputLg} /> <Input disabled={busy} value={samplingRate} onChange={(e) => setSamplingRate(e.target.value)} className={styles.inputLg} />
<span className={styles.configLabel}>Compress Success</span> <span className={styles.configLabel}>Compress Success</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Toggle checked={compressSuccess} onChange={() => !busy && setCompressSuccess(!compressSuccess)} disabled={busy} /> <Toggle checked={compressSuccess} onChange={() => !busy && setCompressSuccess(!compressSuccess)} disabled={busy} />
<span className={compressSuccess ? styles.toggleEnabled : styles.toggleDisabled}>{compressSuccess ? 'Enabled' : 'Disabled'}</span> <span className={compressSuccess ? styles.toggleEnabled : styles.toggleDisabled}>{compressSuccess ? 'Enabled' : 'Disabled'}</span>
</div> </div>
<span className={styles.configLabel}>Replay</span> <span className={styles.configLabel}>Replay</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Toggle checked={replayEnabled} onChange={() => !busy && setReplayEnabled(!replayEnabled)} disabled={busy} /> <Toggle checked={replayEnabled} onChange={() => !busy && setReplayEnabled(!replayEnabled)} disabled={busy} />
<span className={replayEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{replayEnabled ? 'Enabled' : 'Disabled'}</span> <span className={replayEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{replayEnabled ? 'Enabled' : 'Disabled'}</span>
</div> </div>
<span className={styles.configLabel}>Route Control</span> <span className={styles.configLabel}>Route Control</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Toggle checked={routeControlEnabled} onChange={() => !busy && setRouteControlEnabled(!routeControlEnabled)} disabled={busy} /> <Toggle checked={routeControlEnabled} onChange={() => !busy && setRouteControlEnabled(!routeControlEnabled)} disabled={busy} />
<span className={routeControlEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{routeControlEnabled ? 'Enabled' : 'Disabled'}</span> <span className={routeControlEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{routeControlEnabled ? 'Enabled' : 'Disabled'}</span>
</div>
</div> </div>
</div> </div>
)} )}
{configTab === 'resources' && ( {configTab === 'resources' && (
<> <div className={sectionStyles.section}>
<SectionHeader>Container Resources</SectionHeader> <SectionHeader>Container Resources</SectionHeader>
<div className={styles.configGrid}> <div className={styles.configGrid}>
<span className={styles.configLabel}>Memory Limit</span> <span className={styles.configLabel}>Memory Limit</span>
@@ -472,7 +480,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
<span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span> <span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span>
</div> </div>
</div> </div>
</> </div>
)} )}
</div> </div>
); );
@@ -615,70 +623,84 @@ function OverviewSubTab({ app, deployments, versions, environments, envMap, sele
[environments, selectedEnv], [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<DeploymentRow>[] = useMemo(() => [
{ key: 'environmentId', header: 'Environment', render: (_v, row) => (
<span style={{ opacity: row.isSelectedEnv ? 1 : 0.45 }}>
<Badge label={row.dEnv?.displayName ?? '?'} color={row.dEnv?.production ? 'error' : 'auto'} />
</span>
)},
{ key: 'appVersionId', header: 'Version', render: (_v, row) => (
<span style={{ opacity: row.isSelectedEnv ? 1 : 0.45 }}>
<Badge label={row.version ? `v${row.version.version}` : '?'} color="auto" />
</span>
)},
{ key: 'status', header: 'Status', render: (_v, row) => (
<div className={styles.cellFlex} style={{ opacity: row.isSelectedEnv ? 1 : 0.45 }}>
<StatusDot variant={DEPLOY_STATUS_DOT[row.status] ?? 'dead'} />
<Badge label={row.status} color={STATUS_COLORS[row.status] ?? 'auto'} />
</div>
)},
{ key: 'replicaStates', header: 'Replicas', render: (_v, row) => (
<span style={{ opacity: row.isSelectedEnv ? 1 : 0.45 }}>
{row.replicaStates && row.replicaStates.length > 0
? <span className={styles.cellMeta}>{row.replicaStates.filter((r) => r.status === 'RUNNING').length}/{row.replicaStates.length}</span>
: <>{'—'}</>}
</span>
)},
{ key: 'url' as any, header: 'URL', render: (_v, row) => (
<span style={{ opacity: row.isSelectedEnv ? 1 : 0.45 }}>
{row.status === 'RUNNING'
? <MonoText size="xs">{row.url}</MonoText>
: <span className={styles.mutedMono}>{row.url}</span>}
</span>
)},
{ key: 'deployedAt', header: 'Deployed', render: (_v, row) => (
<span className={styles.cellMeta} style={{ opacity: row.isSelectedEnv ? 1 : 0.45 }}>
{row.deployedAt ? timeAgo(row.deployedAt) : '—'}
</span>
)},
{ key: 'actions' as any, header: '', render: (_v, row) => (
<div className={styles.cellFlex} style={{ justifyContent: 'flex-end', gap: 4 }}>
{row.configChanged && <Button size="sm" variant="primary" onClick={() => onDeploy(row.appVersionId, row.environmentId)}>Redeploy</Button>}
{row.canAct && <Button size="sm" variant="danger" onClick={() => onStop(row.id)}>Stop</Button>}
{row.canStart && <Button size="sm" variant="secondary" onClick={() => onDeploy(row.appVersionId, row.environmentId)}>Start</Button>}
{!row.isSelectedEnv && <span className={styles.envHint}>switch env to manage</span>}
</div>
)},
], [onDeploy, onStop]);
return ( return (
<> <>
<SectionHeader>Deployments</SectionHeader> <div className={tableStyles.tableSection}>
{deployments.length === 0 && <p className={styles.emptyNote}>No deployments yet.</p>} <div className={tableStyles.tableHeader}>
{deployments.length > 0 && ( <span className={tableStyles.tableTitle}>Deployments</span>
<table className={styles.table}> </div>
<thead> {deploymentRows.length === 0
<tr> ? <p className={styles.emptyNote} style={{ padding: '12px 16px', margin: 0 }}>No deployments yet.</p>
<th>Environment</th> : <DataTable<DeploymentRow> columns={deploymentColumns} data={deploymentRows} flush />
<th>Version</th> }
<th>Status</th> </div>
<th>Replicas</th>
<th>URL</th>
<th>Deployed</th>
<th style={{ textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{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 (
<tr key={d.id} className={!isSelectedEnv ? styles.mutedRow : undefined}>
<td>
<Badge label={dEnv?.displayName ?? '?'} color={dEnv?.production ? 'error' : 'auto'} />
</td>
<td><Badge label={version ? `v${version.version}` : '?'} color="auto" /></td>
<td className={styles.cellFlex}>
<StatusDot variant={DEPLOY_STATUS_DOT[d.status] ?? 'dead'} />
<Badge label={d.status} color={STATUS_COLORS[d.status] ?? 'auto'} />
</td>
<td>
{d.replicaStates && d.replicaStates.length > 0 ? (
<span className={styles.cellMeta}>
{d.replicaStates.filter((r) => r.status === 'RUNNING').length}/{d.replicaStates.length}
</span>
) : '—'}
</td>
<td>
{d.status === 'RUNNING' ? (
<MonoText size="xs">{url}</MonoText>
) : (
<span className={styles.mutedMono}>{url}</span>
)}
</td>
<td><span className={styles.cellMeta}>{d.deployedAt ? timeAgo(d.deployedAt) : '—'}</span></td>
<td style={{ textAlign: 'right', display: 'flex', gap: 4, justifyContent: 'flex-end' }}>
{configChanged && <Button size="sm" variant="primary" onClick={() => onDeploy(d.appVersionId, d.environmentId)}>Redeploy</Button>}
{canAct && <Button size="sm" variant="danger" onClick={() => onStop(d.id)}>Stop</Button>}
{canStart && <Button size="sm" variant="secondary" onClick={() => onDeploy(d.appVersionId, d.environmentId)}>Start</Button>}
{!isSelectedEnv && <span className={styles.envHint}>switch env to manage</span>}
</td>
</tr>
);
})}
</tbody>
</table>
)}
{deployments.filter((d) => d.deployStage).map((d) => ( {deployments.filter((d) => d.deployStage).map((d) => (
<div key={`progress-${d.id}`} style={{ marginBottom: 8 }}> <div key={`progress-${d.id}`} style={{ marginBottom: 8 }}>
<span className={styles.cellMeta}>{d.containerName}</span> <span className={styles.cellMeta}>{d.containerName}</span>
@@ -949,7 +971,8 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
/> />
{configTab === 'variables' && ( {configTab === 'variables' && (
<> <div className={sectionStyles.section}>
<SectionHeader>Variables</SectionHeader>
{envVars.map((v, i) => ( {envVars.map((v, i) => (
<div key={i} className={styles.envVarRow}> <div key={i} className={styles.envVarRow}>
<Input disabled={!editing} value={v.key} onChange={(e) => { <Input disabled={!editing} value={v.key} onChange={(e) => {
@@ -966,87 +989,91 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
<Button size="sm" variant="secondary" onClick={() => setEnvVars([...envVars, { key: '', value: '' }])}>+ Add Variable</Button> <Button size="sm" variant="secondary" onClick={() => setEnvVars([...envVars, { key: '', value: '' }])}>+ Add Variable</Button>
)} )}
{envVars.length === 0 && !editing && <p className={styles.emptyNote}>No environment variables configured.</p>} {envVars.length === 0 && !editing && <p className={styles.emptyNote}>No environment variables configured.</p>}
</> </div>
)} )}
{configTab === 'monitoring' && ( {configTab === 'monitoring' && (
<div className={styles.configGrid}> <div className={sectionStyles.section}>
<span className={styles.configLabel}>Engine Level</span> <SectionHeader>Monitoring</SectionHeader>
<Select disabled={!editing} value={engineLevel} onChange={(e) => setEngineLevel(e.target.value)} <div className={styles.configGrid}>
options={[{ value: 'NONE', label: 'NONE' }, { value: 'MINIMAL', label: 'MINIMAL' }, { value: 'REGULAR', label: 'REGULAR' }, { value: 'COMPLETE', label: 'COMPLETE' }]} /> <span className={styles.configLabel}>Engine Level</span>
<Select disabled={!editing} value={engineLevel} onChange={(e) => setEngineLevel(e.target.value)}
options={[{ value: 'NONE', label: 'NONE' }, { value: 'MINIMAL', label: 'MINIMAL' }, { value: 'REGULAR', label: 'REGULAR' }, { value: 'COMPLETE', label: 'COMPLETE' }]} />
<span className={styles.configLabel}>Payload Capture</span> <span className={styles.configLabel}>Payload Capture</span>
<Select disabled={!editing} value={payloadCapture} onChange={(e) => setPayloadCapture(e.target.value)} <Select disabled={!editing} value={payloadCapture} onChange={(e) => setPayloadCapture(e.target.value)}
options={[{ value: 'NONE', label: 'NONE' }, { value: 'INPUT', label: 'INPUT' }, { value: 'OUTPUT', label: 'OUTPUT' }, { value: 'BOTH', label: 'BOTH' }]} /> options={[{ value: 'NONE', label: 'NONE' }, { value: 'INPUT', label: 'INPUT' }, { value: 'OUTPUT', label: 'OUTPUT' }, { value: 'BOTH', label: 'BOTH' }]} />
<span className={styles.configLabel}>Max Payload Size</span> <span className={styles.configLabel}>Max Payload Size</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Input disabled={!editing} value={payloadSize} onChange={(e) => setPayloadSize(e.target.value)} className={styles.inputMd} /> <Input disabled={!editing} value={payloadSize} onChange={(e) => setPayloadSize(e.target.value)} className={styles.inputMd} />
<Select disabled={!editing} value={payloadUnit} onChange={(e) => setPayloadUnit(e.target.value)} className={styles.inputXl} <Select disabled={!editing} value={payloadUnit} onChange={(e) => setPayloadUnit(e.target.value)} className={styles.inputXl}
options={[{ value: 'bytes', label: 'bytes' }, { value: 'KB', label: 'KB' }, { value: 'MB', label: 'MB' }]} /> options={[{ value: 'bytes', label: 'bytes' }, { value: 'KB', label: 'KB' }, { value: 'MB', label: 'MB' }]} />
</div> </div>
<span className={styles.configLabel}>App Log Level</span> <span className={styles.configLabel}>App Log Level</span>
<Select disabled={!editing} value={appLogLevel} onChange={(e) => setAppLogLevel(e.target.value)} <Select disabled={!editing} value={appLogLevel} onChange={(e) => setAppLogLevel(e.target.value)}
options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} /> options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} />
<span className={styles.configLabel}>Agent Log Level</span> <span className={styles.configLabel}>Agent Log Level</span>
<Select disabled={!editing} value={agentLogLevel} onChange={(e) => setAgentLogLevel(e.target.value)} <Select disabled={!editing} value={agentLogLevel} onChange={(e) => setAgentLogLevel(e.target.value)}
options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} /> options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} />
<span className={styles.configLabel}>Metrics</span> <span className={styles.configLabel}>Metrics</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Toggle checked={metricsEnabled} onChange={() => editing && setMetricsEnabled(!metricsEnabled)} disabled={!editing} /> <Toggle checked={metricsEnabled} onChange={() => editing && setMetricsEnabled(!metricsEnabled)} disabled={!editing} />
<span className={metricsEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{metricsEnabled ? 'Enabled' : 'Disabled'}</span> <span className={metricsEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{metricsEnabled ? 'Enabled' : 'Disabled'}</span>
<span className={styles.cellMeta} style={{ marginLeft: 8 }}>Interval</span> <span className={styles.cellMeta} style={{ marginLeft: 8 }}>Interval</span>
<Input disabled={!editing} value={metricsInterval} onChange={(e) => setMetricsInterval(e.target.value)} className={styles.inputXs} /> <Input disabled={!editing} value={metricsInterval} onChange={(e) => setMetricsInterval(e.target.value)} className={styles.inputXs} />
<span className={styles.cellMeta}>s</span> <span className={styles.cellMeta}>s</span>
</div> </div>
<span className={styles.configLabel}>Sampling Rate</span> <span className={styles.configLabel}>Sampling Rate</span>
<Input disabled={!editing} value={samplingRate} onChange={(e) => setSamplingRate(e.target.value)} className={styles.inputLg} /> <Input disabled={!editing} value={samplingRate} onChange={(e) => setSamplingRate(e.target.value)} className={styles.inputLg} />
<span className={styles.configLabel}>Compress Success</span> <span className={styles.configLabel}>Compress Success</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Toggle checked={compressSuccess} onChange={() => editing && setCompressSuccess(!compressSuccess)} disabled={!editing} /> <Toggle checked={compressSuccess} onChange={() => editing && setCompressSuccess(!compressSuccess)} disabled={!editing} />
<span className={compressSuccess ? styles.toggleEnabled : styles.toggleDisabled}>{compressSuccess ? 'Enabled' : 'Disabled'}</span> <span className={compressSuccess ? styles.toggleEnabled : styles.toggleDisabled}>{compressSuccess ? 'Enabled' : 'Disabled'}</span>
</div> </div>
<span className={styles.configLabel}>Replay</span> <span className={styles.configLabel}>Replay</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Toggle checked={replayEnabled} onChange={() => editing && setReplayEnabled(!replayEnabled)} disabled={!editing} /> <Toggle checked={replayEnabled} onChange={() => editing && setReplayEnabled(!replayEnabled)} disabled={!editing} />
<span className={replayEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{replayEnabled ? 'Enabled' : 'Disabled'}</span> <span className={replayEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{replayEnabled ? 'Enabled' : 'Disabled'}</span>
</div> </div>
<span className={styles.configLabel}>Route Control</span> <span className={styles.configLabel}>Route Control</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Toggle checked={routeControlEnabled} onChange={() => editing && setRouteControlEnabled(!routeControlEnabled)} disabled={!editing} /> <Toggle checked={routeControlEnabled} onChange={() => editing && setRouteControlEnabled(!routeControlEnabled)} disabled={!editing} />
<span className={routeControlEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{routeControlEnabled ? 'Enabled' : 'Disabled'}</span> <span className={routeControlEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{routeControlEnabled ? 'Enabled' : 'Disabled'}</span>
</div>
</div> </div>
</div> </div>
)} )}
{configTab === 'traces' && ( {configTab === 'traces' && (
<> <div className={sectionStyles.section}>
<SectionHeader>Traces &amp; Taps</SectionHeader>
<span className={styles.sectionSummary}>{tracedCount} traced &middot; {tapCount} taps</span> <span className={styles.sectionSummary}>{tracedCount} traced &middot; {tapCount} taps</span>
{tracedTapRows.length > 0 {tracedTapRows.length > 0
? <DataTable<TracedTapRow> columns={tracedTapColumns} data={tracedTapRows} pageSize={20} flush /> ? <DataTable<TracedTapRow> columns={tracedTapColumns} data={tracedTapRows} pageSize={20} flush />
: <p className={styles.emptyNote}>No processor traces or taps configured.</p>} : <p className={styles.emptyNote}>No processor traces or taps configured.</p>}
</> </div>
)} )}
{configTab === 'recording' && ( {configTab === 'recording' && (
<> <div className={sectionStyles.section}>
<SectionHeader>Route Recording</SectionHeader>
<span className={styles.sectionSummary}>{recordingCount} of {routeRecordingRows.length} routes recording</span> <span className={styles.sectionSummary}>{recordingCount} of {routeRecordingRows.length} routes recording</span>
{routeRecordingRows.length > 0 {routeRecordingRows.length > 0
? <DataTable<RouteRecordingRow> columns={routeRecordingColumns} data={routeRecordingRows} pageSize={20} flush /> ? <DataTable<RouteRecordingRow> columns={routeRecordingColumns} data={routeRecordingRows} pageSize={20} flush />
: <p className={styles.emptyNote}>No routes found for this application.</p>} : <p className={styles.emptyNote}>No routes found for this application.</p>}
</> </div>
)} )}
{configTab === 'resources' && ( {configTab === 'resources' && (
<> <div className={sectionStyles.section}>
{/* Container Resources */}
<SectionHeader>Container Resources</SectionHeader> <SectionHeader>Container Resources</SectionHeader>
<div className={styles.configGrid}> <div className={styles.configGrid}>
<span className={styles.configLabel}>Memory Limit</span> <span className={styles.configLabel}>Memory Limit</span>
@@ -1109,7 +1136,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
<span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span> <span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span>
</div> </div>
</div> </div>
</> </div>
)} )}
</> </>
); );