Files
cameleer-server/docs/superpowers/specs/2026-04-23-checkpoints-grid-row-design.md
2026-04-23 16:51:08 +02:00

253 lines
9.8 KiB
Markdown

# 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:
1. Move the checkpoints section into the Identity & Artifact config grid as an in-grid row.
2. Format the Deployed-column sub-line to the user's locale (replaces the raw ISO string).
3. Remove the redundant `HistoryDisclosure` from 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`):
```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:**
```tsx
<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:
```ts
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:
```tsx
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:*
```css
.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:
```tsx
<td>
{d.deployedAt && timeAgo(d.deployedAt)}
<div className={styles.isoSubline}>{d.deployedAt}</div>
</td>
```
Replace with:
```tsx
<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 `.historyRow` rule (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`:
```tsx
<IdentitySection ...>
{app && (
<>
<CheckpointsTable ... />
{selectedDep && <CheckpointDetailDrawer ... />}
</>
)}
</IdentitySection>
```
After the change:
```tsx
<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|collapse/i`; add test asserting locale sub-line differs from raw ISO |
| `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 targets `screen.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 is `Collapse`; rows visible.
- One new test: render the table with `deployedAt: '2026-04-23T10:35:00Z'`, expand, grab the `.isoSubline` element, assert its text contains neither the raw ISO `T` nor `Z`, 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 PM` in `en-US`).
- Deployment tab no longer shows `▶ History (N)` below `Startup Logs`.
- `CheckpointDetailDrawer` still opens on row click (unaffected).
- Empty state: app with no checkpoints shows no Checkpoints row at all.
## 6. Non-goals
- No changes to `CheckpointDetailDrawer` layout 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 `CheckpointsTable` columns themselves.
## 7. Open questions
None — all resolved during brainstorming.