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:
@@ -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);
|
||||||
|
|||||||
@@ -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 & Taps</SectionHeader>
|
||||||
<span className={styles.sectionSummary}>{tracedCount} traced · {tapCount} taps</span>
|
<span className={styles.sectionSummary}>{tracedCount} traced · {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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user