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,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 & 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