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:
@@ -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;
|
||||
}
|
||||
|
||||
65
ui/src/pages/AppsTab/AppDeploymentPage/Checkpoints.tsx
Normal file
65
ui/src/pages/AppsTab/AppDeploymentPage/Checkpoints.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user