9.8 KiB
Checkpoints in the Identity grid + locale time + remove History — design
Date: 2026-04-23
Scope: three targeted UX changes on the unified app deployment page, follow-up to 2026-04-23-deployment-page-polish-design.md.
Status: Draft — pending user review.
1. Motivation
The previous polish shipped a collapsible CheckpointsTable as a standalone section below the Identity & Artifact block. That made the visual hierarchy noisy — Checkpoints became a third section between Identity and the config tabs, competing for attention. The proper home for "how many past deployments exist and what were they" is inside the Identity panel, as one more row in its config grid.
Three changes:
- Move the checkpoints section into the Identity & Artifact config grid as an in-grid row.
- Format the Deployed-column sub-line to the user's locale (replaces the raw ISO string).
- Remove the redundant
HistoryDisclosurefrom the Deployment tab — the checkpoints table covers the same information and the per-deployment log drill-down now lives in the drawer.
2. Design
2.1 Checkpoints row in the Identity config grid
Current structure (IdentitySection.tsx):
<div className={styles.section}>
<SectionHeader>Identity & Artifact</SectionHeader>
<div className={styles.configGrid}>
... label + value cells (Application Name, Slug, Environment, External URL, Current Version, Application JAR) ...
</div>
{children} {/* CheckpointsTable + CheckpointDetailDrawer currently render here */}
</div>
New structure:
<div className={styles.section}>
<SectionHeader>Identity & Artifact</SectionHeader>
<div className={styles.configGrid}>
... existing label + value cells ...
{checkpointsSlot} {/* NEW: rendered as direct grid children via React.Fragment */}
</div>
{children} {/* still used — for the portal-rendered CheckpointDetailDrawer */}
</div>
Slot contract. IdentitySection gains a new prop:
interface IdentitySectionProps {
// ... existing props ...
checkpointsSlot?: ReactNode;
children?: ReactNode;
}
checkpointsSlot is expected to be a React.Fragment whose children are grid-direct cells (spans / divs). React fragments are transparent to CSS grid, so the inner elements become direct children of configGrid and flow into grid cells like the existing rows.
CheckpointsTable rewrite. Instead of wrapping itself in <div className={styles.checkpointsSection}>, the component returns a Fragment of grid-ready children:
if (checkpoints.length === 0) {
return null;
}
return (
<>
<span className={styles.configLabel}>Checkpoints</span>
<div className={styles.checkpointsTriggerCell}>
<button
type="button"
className={styles.checkpointsTrigger}
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
>
<span className={styles.checkpointsChevron}>{open ? '\u25BE' : '\u25B8'}</span>
{open ? 'Collapse' : 'Expand'} ({checkpoints.length})
</button>
</div>
{open && (
<div className={styles.checkpointsTableFullRow}>
<table>...</table>
{hidden > 0 && !expanded && (
<button type="button" className={styles.showOlderBtn} onClick={...}>
Show older (N) — archived, postmortem only
</button>
)}
</div>
)}
</>
);
Why this layout.
- The trigger button sits in the value column (180px label + 1fr value). When closed, the row reads
Checkpoints ▸ Expand (5). - When opened, a third grid child appears: a div that spans both columns (
grid-column: 1 / -1) containing the<table>+ optional "Show older" button. This gives the 7-column table the full grid width so columns don't crush. - The trigger remains in the value cell of the label row above — collapse/expand stays attached to its label.
CSS changes (AppDeploymentPage.module.css):
Add:
.checkpointsTriggerCell {
display: flex;
align-items: center;
}
.checkpointsTrigger {
display: inline-flex;
align-items: center;
gap: 6px;
background: none;
border: none;
padding: 0;
color: var(--text-primary);
cursor: pointer;
font: inherit;
text-align: left;
}
.checkpointsTrigger:hover {
color: var(--amber);
}
.checkpointsTableFullRow {
grid-column: 1 / -1;
margin-top: 4px;
}
Remove (no longer referenced):
.checkpointsSection.checkpointsHeader+.checkpointsHeader:hover.checkpointsCount
Keep: .checkpointsChevron (still used by the trigger for the arrow). .checkpointsTable, .jarCell, .jarName, .jarStrike, .archivedHint, .isoSubline, .muted, .strategyPill, .outcomePill, .outcome-*, .chevron, .showOlderBtn, .checkpointArchived — all still referenced by the table body.
Also remove (cleanup — unrelated dead weight from the retired Checkpoints.tsx row-list view, safe to delete because no TSX references remain):
.checkpointsRow.disclosureToggle.checkpointList.checkpointRow.checkpointMeta- Standalone
.checkpointArchived { color: var(--warning); font-size: 12px; }(the table-row variant.checkpointsTable tr.checkpointArchived { opacity: 0.55; }stays) .historyRow(see §2.3)
2.2 Deployed-column locale sub-line
In CheckpointsTable.tsx, the Deployed <td> currently renders:
<td>
{d.deployedAt && timeAgo(d.deployedAt)}
<div className={styles.isoSubline}>{d.deployedAt}</div>
</td>
Replace with:
<td>
{d.deployedAt && timeAgo(d.deployedAt)}
<div className={styles.isoSubline}>
{d.deployedAt && new Date(d.deployedAt).toLocaleString()}
</div>
</td>
new Date(iso).toLocaleString() uses the browser's resolved locale via the Intl API. No locale plumbing, no new util.
Primary "5h ago" display stays unchanged.
2.3 Remove the History disclosure from the Deployment tab
HistoryDisclosure.tsx renders a collapsible DataTable + nested StartupLogPanel. It duplicates information now surfaced via CheckpointsTable + CheckpointDetailDrawer (which has its own LogsPanel).
Changes:
- Delete
ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/HistoryDisclosure.tsx. ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/DeploymentTab.tsx— remove the import and the<HistoryDisclosure ... />render at the bottom of the tab.ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css— drop the.historyRowrule (covered in §2.1's CSS cleanup list).
3. Page wiring
ui/src/pages/AppsTab/AppDeploymentPage/index.tsx currently passes the table + drawer together as children to IdentitySection:
<IdentitySection ...>
{app && (
<>
<CheckpointsTable ... />
{selectedDep && <CheckpointDetailDrawer ... />}
</>
)}
</IdentitySection>
After the change:
<IdentitySection
...
checkpointsSlot={app ? <CheckpointsTable ... /> : undefined}
>
{app && selectedDep && <CheckpointDetailDrawer ... />}
</IdentitySection>
The drawer continues to pass through as children because SideDrawer uses createPortal — it can live at any DOM depth, but conceptually sits outside the Identity grid so it doesn't become a stray grid cell.
4. Files touched
| Path | Change |
|---|---|
ui/src/pages/AppsTab/AppDeploymentPage/IdentitySection.tsx |
Add checkpointsSlot?: ReactNode; render inside configGrid after JAR row |
ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.tsx |
Return React.Fragment of grid-ready children; replace header wrapper with checkpointsTrigger button; locale sub-line in Deployed cell |
ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.test.tsx |
Update expand() helper to target `/expand |
ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css |
Add .checkpointsTriggerCell, .checkpointsTrigger, .checkpointsTableFullRow; remove obsolete classes listed in §2.1 |
ui/src/pages/AppsTab/AppDeploymentPage/index.tsx |
Split checkpointsSlot out of children; drawer stays in children |
ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/DeploymentTab.tsx |
Remove HistoryDisclosure import + render |
ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/HistoryDisclosure.tsx |
Delete |
5. Testing
Unit (vitest + RTL):
- Update
CheckpointsTable.test.tsx:expand()helper targetsscreen.getByRole('button', { name: /expand|collapse/i }).- The "defaults to collapsed" test asserts the trigger button exists and reads
Expand (1); rows hidden. - The "clicking header expands" test clicks the button (now labeled
Expand); after click, button label isCollapse; rows visible. - One new test: render the table with
deployedAt: '2026-04-23T10:35:00Z', expand, grab the.isoSublineelement, assert its text contains neither the raw ISOTnorZ, i.e. it was parsed into a localized form. (Avoids asserting the exact string — CI locales vary.)
Manual smoke:
- Page loads →
Checkpoints | ▸ Expand (N)as a grid row under Application JAR. Collapsed by default. - Click trigger → text swaps to
▾ Collapse (N); table appears below, spanning full grid width. - Deployed column sub-line shows a local-format date/time (e.g.
4/23/2026, 12:35:00 PMinen-US). - Deployment tab no longer shows
▶ History (N)belowStartup Logs. CheckpointDetailDrawerstill opens on row click (unaffected).- Empty state: app with no checkpoints shows no Checkpoints row at all.
6. Non-goals
- No changes to
CheckpointDetailDrawerlayout or behavior. - No changes to
timeAgo(other components still use it). - No new locale-formatting helpers;
toLocaleString()inline at the one callsite. - Not touching primary Deployed column display (keeps "5h ago").
- No changes to the
CheckpointsTablecolumns themselves.
7. Open questions
None — all resolved during brainstorming.