From 13f218d522c5bd64c7b407a409972db8cfd28d33 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:42:06 +0200 Subject: [PATCH] docs(plan): deployment page polish (9 TDD tasks) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-04-23-deployment-page-polish.md | 1359 +++++++++++++++++ 1 file changed, 1359 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-23-deployment-page-polish.md diff --git a/docs/superpowers/plans/2026-04-23-deployment-page-polish.md b/docs/superpowers/plans/2026-04-23-deployment-page-polish.md new file mode 100644 index 00000000..95598489 --- /dev/null +++ b/docs/superpowers/plans/2026-04-23-deployment-page-polish.md @@ -0,0 +1,1359 @@ +# Deployment Page Polish — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver five targeted UX improvements on the unified app deployment page: JAR-upload progress inside the primary button, sort + refresh on startup logs, a collapsible checkpoints table (default collapsed), a DS-styled replica dropdown, and reversed drawer tab order (Config first). + +**Architecture:** All changes are UI-only, confined to `ui/src/pages/AppsTab/AppDeploymentPage/*`, `ui/src/components/StartupLogPanel.*`, `ui/src/api/queries/admin/apps.ts` (`useUploadJar` switches to `XMLHttpRequest`), and `ui/src/api/queries/logs.ts` (`useStartupLogs` accepts a sort parameter). No backend or schema changes. + +**Tech Stack:** React 18, TypeScript, Vitest + React Testing Library, `@cameleer/design-system` components (Button, Select, Tabs, SectionHeader), `lucide-react` (`RefreshCw`), TanStack Query. + +**Spec:** `docs/superpowers/specs/2026-04-23-deployment-page-polish-design.md` + +--- + +## Files touched (summary) + +| Path | Change | +|------|--------| +| `ui/src/api/queries/admin/apps.ts` | `useUploadJar` rewrites the mutation function to use `XMLHttpRequest` and exposes an `onProgress` callback | +| `ui/src/api/queries/logs.ts` | `useStartupLogs` gains a 5th `sort` parameter | +| `ui/src/pages/AppsTab/AppDeploymentPage/PrimaryActionButton.tsx` | Adds `'uploading'` mode with a progress-overlay visual | +| `ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css` | Adds `.uploadBtnWrap`, `.uploadBtnFill`, `.checkpointsHeader` styles | +| `ui/src/pages/AppsTab/AppDeploymentPage/index.tsx` | Wires `uploadPct` state and feeds it into both upload call sites + the primary button | +| `ui/src/components/StartupLogPanel.tsx` | New header layout (sort ↑/↓ + refresh icon), local sort state, scroll-ref | +| `ui/src/components/StartupLogPanel.module.css` | Extends with `.headerRight`, `.scrollWrap`, `.ghostIconBtn` | +| `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.tsx` | Collapsible header + `useState(false)` default | +| `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.test.tsx` | Tests updated for collapsed-by-default behavior; stale empty-state assertion removed | +| `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointDetailDrawer/LogsPanel.tsx` | Native `` clashes with the rest of the drawer's DS styling. + +**Files:** +- Modify: `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointDetailDrawer/LogsPanel.tsx` + +- [ ] **Step 7.1: Replace native select with DS `Select`** + +Open `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointDetailDrawer/LogsPanel.tsx`. Replace the whole file contents with: + +```tsx +import { useMemo, useState } from 'react'; +import { Select } from '@cameleer/design-system'; +import { useInfiniteApplicationLogs } from '../../../../api/queries/logs'; +import type { Deployment } from '../../../../api/queries/admin/apps'; +import { instanceIdsFor } from './instance-id'; +import styles from './CheckpointDetailDrawer.module.css'; + +interface Props { + deployment: Deployment; + appSlug: string; + envSlug: string; +} + +export function LogsPanel({ deployment, appSlug, envSlug }: Props) { + const allInstanceIds = useMemo( + () => instanceIdsFor(deployment, envSlug, appSlug), + // eslint-disable-next-line react-hooks/exhaustive-deps + [deployment.id, deployment.replicaStates, envSlug, appSlug] + ); + + const [replicaFilter, setReplicaFilter] = useState<'all' | number>('all'); + const filteredInstanceIds = replicaFilter === 'all' + ? allInstanceIds + : allInstanceIds.filter((_, i) => i === replicaFilter); + + const logs = useInfiniteApplicationLogs({ + application: appSlug, + instanceIds: filteredInstanceIds, + isAtTop: true, + }); + + const replicaOptions = [ + { value: 'all', label: `all (${deployment.replicaStates.length})` }, + ...deployment.replicaStates.map((_, i) => ({ value: String(i), label: String(i) })), + ]; + + return ( +
+
+