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>
113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
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>
|
||
);
|
||
}
|