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;
|
white-space: nowrap;
|
||||||
border: 0;
|
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 { useParams, useLocation } from 'react-router';
|
||||||
import { useEnvironmentStore } from '../../../api/environment-store';
|
import { useEnvironmentStore } from '../../../api/environment-store';
|
||||||
import { useEnvironments } from '../../../api/queries/admin/environments';
|
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 { PageLoader } from '../../../components/PageLoader';
|
||||||
import { IdentitySection } from './IdentitySection';
|
import { IdentitySection } from './IdentitySection';
|
||||||
|
import { Checkpoints } from './Checkpoints';
|
||||||
import { deriveAppName } from './utils/deriveAppName';
|
import { deriveAppName } from './utils/deriveAppName';
|
||||||
import styles from './AppDeploymentPage.module.css';
|
import styles from './AppDeploymentPage.module.css';
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ export default function AppDeploymentPage() {
|
|||||||
const env = environments.find((e) => e.slug === selectedEnv);
|
const env = environments.find((e) => e.slug === selectedEnv);
|
||||||
const { data: versions = [] } = useAppVersions(selectedEnv, app?.slug);
|
const { data: versions = [] } = useAppVersions(selectedEnv, app?.slug);
|
||||||
const currentVersion = versions.slice().sort((a, b) => b.version - a.version)[0] ?? null;
|
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
|
// Form state
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
@@ -61,6 +64,17 @@ export default function AppDeploymentPage() {
|
|||||||
onStagedJarChange={setStagedJar}
|
onStagedJarChange={setStagedJar}
|
||||||
deploying={false}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user