diff --git a/ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css b/ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css index f7869b64..f35d780e 100644 --- a/ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css +++ b/ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css @@ -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; +} diff --git a/ui/src/pages/AppsTab/AppDeploymentPage/Checkpoints.tsx b/ui/src/pages/AppsTab/AppDeploymentPage/Checkpoints.tsx new file mode 100644 index 00000000..3b447ce1 --- /dev/null +++ b/ui/src/pages/AppsTab/AppDeploymentPage/Checkpoints.tsx @@ -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 ( +
+ + {open && ( +
+ {checkpoints.length === 0 && ( +
No past deployments yet.
+ )} + {checkpoints.map((d) => { + const v = versionMap.get(d.appVersionId); + const jarAvailable = !!v; + return ( +
+ + + {d.deployedAt ? timeAgo(d.deployedAt) : '—'} + + {!jarAvailable && ( + archived, JAR unavailable + )} + +
+ ); + })} +
+ )} +
+ ); +} diff --git a/ui/src/pages/AppsTab/AppDeploymentPage/index.tsx b/ui/src/pages/AppsTab/AppDeploymentPage/index.tsx index 03bbb91b..a3ff1fb9 100644 --- a/ui/src/pages/AppsTab/AppDeploymentPage/index.tsx +++ b/ui/src/pages/AppsTab/AppDeploymentPage/index.tsx @@ -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' && ( + { + // TODO: wired in Task 10.3 (restore hydrates form from snapshot) + console.info('restore checkpoint', id); + }} + /> + )} ); }