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

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:

  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):

<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 .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:

<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 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.