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>
|
|||
|
|
);
|
|||
|
|
}
|