feat(ui): CheckpointsTable component (replaces row list)

Full-width table with Version / JAR / Deployed-by / Deployed / Strategy /
Outcome columns, pagination cap (jarRetentionCount, default 10), pruned-JAR
archived state, empty state, and row-click onSelect handler. 8/8 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-23 13:15:30 +02:00
parent 48217e0034
commit 1b4b522233
3 changed files with 270 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
import { useState } from 'react';
import { Badge } from '@cameleer/design-system';
import type { Deployment, AppVersion } from '../../../api/queries/admin/apps';
import { timeAgo } from '../../../utils/format-utils';
import styles from './AppDeploymentPage.module.css';
const FALLBACK_CAP = 10;
interface CheckpointsTableProps {
deployments: Deployment[];
versions: AppVersion[];
currentDeploymentId: string | null;
jarRetentionCount: number | null;
onSelect: (deploymentId: string) => void;
}
export function CheckpointsTable({
deployments,
versions,
currentDeploymentId,
jarRetentionCount,
onSelect,
}: CheckpointsTableProps) {
const [expanded, setExpanded] = useState(false);
const versionMap = new Map(versions.map((v) => [v.id, v]));
const checkpoints = deployments
.filter((d) => d.deployedConfigSnapshot && d.id !== currentDeploymentId)
.sort((a, b) => (b.deployedAt ?? '').localeCompare(a.deployedAt ?? ''));
if (checkpoints.length === 0) {
return <div className={styles.checkpointEmpty}>No past deployments yet.</div>;
}
const cap = (jarRetentionCount ?? 0) > 0 ? jarRetentionCount! : FALLBACK_CAP;
const visible = expanded ? checkpoints : checkpoints.slice(0, cap);
const hidden = checkpoints.length - visible.length;
return (
<div className={styles.checkpointsTable}>
<table>
<thead>
<tr>
<th>Version</th>
<th>JAR</th>
<th>Deployed by</th>
<th>Deployed</th>
<th>Strategy</th>
<th>Outcome</th>
<th aria-label="open"></th>
</tr>
</thead>
<tbody>
{visible.map((d) => {
const v = versionMap.get(d.appVersionId);
const archived = !v;
const strategyLabel =
d.deploymentStrategy === 'BLUE_GREEN' ? 'blue/green' : 'rolling';
return (
<tr
key={d.id}
className={archived ? styles.checkpointArchived : undefined}
onClick={() => onSelect(d.id)}
>
<td>
<Badge label={v ? `v${v.version}` : '?'} color="auto" />
</td>
<td className={styles.jarCell}>
{v ? (
<span className={styles.jarName}>{v.jarFilename}</span>
) : (
<>
<span className={styles.jarStrike}>JAR pruned</span>
<div className={styles.archivedHint}>archived JAR pruned</div>
</>
)}
</td>
<td>
{d.createdBy ?? <span className={styles.muted}></span>}
</td>
<td>
{d.deployedAt && timeAgo(d.deployedAt)}
<div className={styles.isoSubline}>{d.deployedAt}</div>
</td>
<td>
<span className={styles.strategyPill}>{strategyLabel}</span>
</td>
<td>
<span
className={`${styles.outcomePill} ${styles[`outcome-${d.status}` as keyof typeof styles]}`}
>
{d.status}
</span>
</td>
<td className={styles.chevron}></td>
</tr>
);
})}
</tbody>
</table>
{hidden > 0 && !expanded && (
<button
type="button"
className={styles.showOlderBtn}
onClick={() => setExpanded(true)}
>
Show older ({hidden}) archived, postmortem only
</button>
)}
</div>
);
}