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,6 +292,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
{step && <div className={styles.stepIndicator}>{step}</div>} {step && <div className={styles.stepIndicator}>{step}</div>}
{/* Identity Section */} {/* Identity Section */}
<div className={sectionStyles.section}>
<SectionHeader>Identity & Artifact</SectionHeader> <SectionHeader>Identity & Artifact</SectionHeader>
<div className={styles.configGrid}> <div className={styles.configGrid}>
<span className={styles.configLabel}>Application Name</span> <span className={styles.configLabel}>Application Name</span>
@@ -321,6 +324,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
</span> </span>
</div> </div>
</div> </div>
</div>
{/* Config Tabs */} {/* Config Tabs */}
<Tabs <Tabs
@@ -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,10 +353,12 @@ 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={sectionStyles.section}>
<SectionHeader>Monitoring</SectionHeader>
<div className={styles.configGrid}> <div className={styles.configGrid}>
<span className={styles.configLabel}>Engine Level</span> <span className={styles.configLabel}>Engine Level</span>
<Select disabled={busy} value={engineLevel} onChange={(e) => setEngineLevel(e.target.value)} <Select disabled={busy} value={engineLevel} onChange={(e) => setEngineLevel(e.target.value)}
@@ -406,10 +413,11 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
<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],
); );
return ( type DeploymentRow = Deployment & {
<> dEnv: Environment | undefined;
<SectionHeader>Deployments</SectionHeader> version: AppVersion | undefined;
{deployments.length === 0 && <p className={styles.emptyNote}>No deployments yet.</p>} isSelectedEnv: boolean;
{deployments.length > 0 && ( canAct: boolean;
<table className={styles.table}> canStart: boolean;
<thead> configChanged: boolean;
<tr> url: string;
<th>Environment</th> };
<th>Version</th>
<th>Status</th> const deploymentRows: DeploymentRow[] = useMemo(() => deployments.map((d) => {
<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 dEnv = envMap.get(d.environmentId);
const version = versions.find((v) => v.id === d.appVersionId); const version = versions.find((v) => v.id === d.appVersionId);
const isSelectedEnv = !selectedEnvId || d.environmentId === selectedEnvId; const isSelectedEnv = !selectedEnvId || d.environmentId === selectedEnvId;
const canAct = isSelectedEnv && (d.status === 'RUNNING' || d.status === 'STARTING'); const canAct = isSelectedEnv && (d.status === 'RUNNING' || d.status === 'STARTING');
const canStart = isSelectedEnv && d.status === 'STOPPED'; const canStart = isSelectedEnv && d.status === 'STOPPED';
const configChanged = canAct && d.deployedAt && new Date(app.updatedAt) > new Date(d.deployedAt); const configChanged = canAct && !!d.deployedAt && new Date(app.updatedAt) > new Date(d.deployedAt);
const url = dEnv ? `/${dEnv.slug}/${app.slug}/` : ''; 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 (
<tr key={d.id} className={!isSelectedEnv ? styles.mutedRow : undefined}> <>
<td> <div className={tableStyles.tableSection}>
<Badge label={dEnv?.displayName ?? '?'} color={dEnv?.production ? 'error' : 'auto'} /> <div className={tableStyles.tableHeader}>
</td> <span className={tableStyles.tableTitle}>Deployments</span>
<td><Badge label={version ? `v${version.version}` : '?'} color="auto" /></td> </div>
<td className={styles.cellFlex}> {deploymentRows.length === 0
<StatusDot variant={DEPLOY_STATUS_DOT[d.status] ?? 'dead'} /> ? <p className={styles.emptyNote} style={{ padding: '12px 16px', margin: 0 }}>No deployments yet.</p>
<Badge label={d.status} color={STATUS_COLORS[d.status] ?? 'auto'} /> : <DataTable<DeploymentRow> columns={deploymentColumns} data={deploymentRows} flush />
</td> }
<td> </div>
{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,10 +989,12 @@ 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={sectionStyles.section}>
<SectionHeader>Monitoring</SectionHeader>
<div className={styles.configGrid}> <div className={styles.configGrid}>
<span className={styles.configLabel}>Engine Level</span> <span className={styles.configLabel}>Engine Level</span>
<Select disabled={!editing} value={engineLevel} onChange={(e) => setEngineLevel(e.target.value)} <Select disabled={!editing} value={engineLevel} onChange={(e) => setEngineLevel(e.target.value)}
@@ -1024,29 +1049,31 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
<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>
)} )}
</> </>
); );