ui(deploy): Checkpoints disclosure (hides current deployment, flags pruned JARs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 22:51:39 +02:00
parent 00c7c0cd71
commit 08efdfa9c5
3 changed files with 121 additions and 1 deletions

View File

@@ -53,3 +53,44 @@
white-space: nowrap;
border: 0;
}
.checkpointsRow {
margin-top: 8px;
}
.disclosureToggle {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 13px;
padding: 4px 0;
}
.checkpointList {
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px 0 0 12px;
}
.checkpointRow {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
}
.checkpointMeta {
color: var(--text-muted);
}
.checkpointArchived {
color: var(--warning);
font-size: 12px;
}
.checkpointEmpty {
color: var(--text-muted);
font-size: 13px;
}

View File

@@ -0,0 +1,65 @@
import { useState } from 'react';
import { Button, 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';
interface CheckpointsProps {
deployments: Deployment[];
versions: AppVersion[];
currentDeploymentId: string | null;
onRestore: (deploymentId: string) => void;
}
export function Checkpoints({ deployments, versions, currentDeploymentId, onRestore }: CheckpointsProps) {
const [open, setOpen] = useState(false);
const versionMap = new Map(versions.map((v) => [v.id, v]));
// Only successful deployments (RUNNING with a deployedAt). Exclude the currently-running one.
const checkpoints = deployments
.filter((d) => d.deployedAt && d.status === 'RUNNING' && d.id !== currentDeploymentId)
.sort((a, b) => (b.deployedAt ?? '').localeCompare(a.deployedAt ?? ''));
return (
<div className={styles.checkpointsRow}>
<button
type="button"
className={styles.disclosureToggle}
onClick={() => setOpen(!open)}
>
{open ? '▼' : '▶'} Checkpoints ({checkpoints.length})
</button>
{open && (
<div className={styles.checkpointList}>
{checkpoints.length === 0 && (
<div className={styles.checkpointEmpty}>No past deployments yet.</div>
)}
{checkpoints.map((d) => {
const v = versionMap.get(d.appVersionId);
const jarAvailable = !!v;
return (
<div key={d.id} className={styles.checkpointRow}>
<Badge label={v ? `v${v.version}` : '?'} color="auto" />
<span className={styles.checkpointMeta}>
{d.deployedAt ? timeAgo(d.deployedAt) : '—'}
</span>
{!jarAvailable && (
<span className={styles.checkpointArchived}>archived, JAR unavailable</span>
)}
<Button
size="sm"
variant="ghost"
disabled={!jarAvailable}
title={!jarAvailable ? 'JAR was pruned by the environment retention policy' : undefined}
onClick={() => onRestore(d.id)}
>
Restore
</Button>
</div>
);
})}
</div>
)}
</div>
);
}

View File

@@ -2,9 +2,10 @@ import { useState, useEffect, useRef } from 'react';
import { useParams, useLocation } from 'react-router';
import { useEnvironmentStore } from '../../../api/environment-store';
import { useEnvironments } from '../../../api/queries/admin/environments';
import { useApps, useAppVersions } from '../../../api/queries/admin/apps';
import { useApps, useAppVersions, useDeployments } from '../../../api/queries/admin/apps';
import { PageLoader } from '../../../components/PageLoader';
import { IdentitySection } from './IdentitySection';
import { Checkpoints } from './Checkpoints';
import { deriveAppName } from './utils/deriveAppName';
import styles from './AppDeploymentPage.module.css';
@@ -21,6 +22,8 @@ export default function AppDeploymentPage() {
const env = environments.find((e) => e.slug === selectedEnv);
const { data: versions = [] } = useAppVersions(selectedEnv, app?.slug);
const currentVersion = versions.slice().sort((a, b) => b.version - a.version)[0] ?? null;
const { data: deployments = [] } = useDeployments(selectedEnv, app?.slug);
const currentDeployment = deployments.find((d) => d.status === 'RUNNING') ?? null;
// Form state
const [name, setName] = useState('');
@@ -61,6 +64,17 @@ export default function AppDeploymentPage() {
onStagedJarChange={setStagedJar}
deploying={false}
/>
{mode === 'deployed' && (
<Checkpoints
deployments={deployments}
versions={versions}
currentDeploymentId={currentDeployment?.id ?? null}
onRestore={(id) => {
// TODO: wired in Task 10.3 (restore hydrates form from snapshot)
console.info('restore checkpoint', id);
}}
/>
)}
</div>
);
}