feat(frontend): slice 5 — migrate Settings + Actions to shadcn/Tailwind
Web UI rework. Form-heavy pair (controlled useState parity, no form lib): - pages/Settings.tsx off @mui: monitoring-machine CRUD, SSH-key mgmt, SSH test/validation feedback, danger-zone reset (ConfirmDialog), tabs - pages/Actions.tsx off @mui: saved-task editor, machine selection, run history, tabs - Both reuse migrated shared components (SectionCard/SelectionRailCard/ TabbedCard/HoverEditButton/ConfirmDialog/DialogFooter) as before - Behavioral tests added (mocked hooks; no live SSH) Gate: build + lint + test green (19 files / 39 tests).
This commit is contained in:
@@ -616,3 +616,151 @@ Overall change `applyState` is still reported **blocked** by the status engine
|
||||
Slice 4 migration itself — `design.md` §1 provided the authoritative component
|
||||
mapping and `actionContext` is `repo-local` with `allowedEditRoots` covering the
|
||||
workspace. Should be resolved before `sdd-verify`/archive, per the slice-3 note.
|
||||
|
||||
## Slice 5 — Settings + Actions (form-heavy pair) — COMPLETE
|
||||
|
||||
Migrated the two form-heavy pages off `@mui/material` onto the shadcn/Tailwind
|
||||
design system per `design.md` §1. All 5 Slice-5 tasks in `tasks.md` are now
|
||||
marked `- [x]`. **No form library introduced** — every field stays controlled
|
||||
`useState`, exactly as before.
|
||||
|
||||
### Completed tasks (persisted checkboxes updated)
|
||||
|
||||
- [x] **`frontend/src/pages/Actions.tsx`** — MUI-free. `Tab/Tabs` → shadcn `Tabs`/
|
||||
`TabsList`/`TabsTrigger` (vertical orientation in the saved-actions rail);
|
||||
`FormControl`/`InputLabel`/`Select`/`MenuItem` → shadcn `Select` family (with a
|
||||
`NONE = "__none__"` sentinel for the empty "None" option, since Radix Select
|
||||
disallows empty-string item values); `Divider` → `Separator`; `Dialog` family →
|
||||
shadcn `Dialog`. Saved-task editor (shell/python `Textarea`, default-machine
|
||||
`Select`), run-machine selection, and the recent-runs list are preserved.
|
||||
Reuses `SelectionRailCard`, `SectionCard` (detail + empty states),
|
||||
`HoverEditButton`, `DialogFooter`. `TextField`→`Input`+`Label` via a file-local
|
||||
`FormField` helper.
|
||||
- [x] **`frontend/src/pages/Settings.tsx`** — MUI-free. MUI `Grid` → 12-col CSS
|
||||
grid (`grid grid-cols-12` + `col-span-*`); `Switch` → shadcn `Switch`
|
||||
(`onCheckedChange`); `Checkbox`+`FormControlLabel` → `Checkbox` + native
|
||||
`<label>`/`Label` rows; `Tab` → `TabsTrigger` inside `TabbedCard`; `Select` →
|
||||
shadcn `Select` (incl. the SSH-key picker with the `NONE` sentinel);
|
||||
`Dialog`/`DialogTitle`/`DialogContent` → shadcn `Dialog`. Monitoring-machine CRUD
|
||||
(rail + detail + edit dialog), SSH-key management (rail + SectionCard editor +
|
||||
generate/save/delete), SSH test/validation feedback (status line + success/error
|
||||
`Alert`s, exact message strings preserved incl. the protocol-banner /
|
||||
auth-failed / generic branches), and danger-zone database reset (3 acks + typed
|
||||
confirm phrase gating `confirmDisabled`, via the shared `ConfirmDialog`) are all
|
||||
preserved. Reuses `SectionCard`, `SelectionRailCard`, `TabbedCard`,
|
||||
`HoverEditButton`, `DialogFooter`, `ConfirmDialog`.
|
||||
- [x] **Form-behavior parity** — all hook usage (`useMonitoringSettings`,
|
||||
`useSSHKeys`, `useSave*`, `useDelete*`, `useGenerateSSHKey`, `useTestMonitoringMachineSSH`,
|
||||
`useResetLocalDatabase`, `useTasks`, `useTaskRuns`, `useRunTask`) and controlled
|
||||
`useState` drafts are unchanged. No form library.
|
||||
- [x] **Tests** — `src/pages/__tests__/Settings.test.tsx` (renders machine list
|
||||
from mocked store; saves a renamed machine via the editor dialog; deletes a
|
||||
machine through the confirm dialog) and `src/pages/__tests__/Actions.test.tsx`
|
||||
(empty state + create/save task via editor; `Run action` button disabled until a
|
||||
run machine is selected). Hooks mocked with `vi.mock`; no live SSH needed.
|
||||
- [x] **Exit gate** — `npm run build` + `npm run lint` + `npm test` all exit 0;
|
||||
`npm run test:node` (`node --test tests/*.test.mjs`) green (4/4). Verified
|
||||
below.
|
||||
|
||||
### Files changed (this slice)
|
||||
|
||||
- `frontend/src/pages/Settings.tsx` (rewrite; MUI → shadcn/Tailwind)
|
||||
- `frontend/src/pages/Actions.tsx` (rewrite; MUI → shadcn/Tailwind)
|
||||
- `frontend/src/pages/__tests__/Settings.test.tsx` (new)
|
||||
- `frontend/src/pages/__tests__/Actions.test.tsx` (new)
|
||||
|
||||
No other page or `components/*` file was edited (scope-clean confirmed via
|
||||
`git status`). Shared components (`SectionCard`, `SelectionRailCard`,
|
||||
`TabbedCard`, `HoverEditButton`, `ConfirmDialog`, `DialogFooter`) were reused
|
||||
unchanged.
|
||||
|
||||
### Gate results (run from `frontend/`)
|
||||
|
||||
| Command | Result | Notes |
|
||||
|---|---|---|
|
||||
| `npm run build` (`tsc -b && vite build`) | **pass (exit 0)** | 3313 modules; pre-existing >500 kB chunk warning only. |
|
||||
| `npm run lint` (`eslint .`) | **pass (exit 0)** | 0 errors. 2 pre-existing `react-hooks/exhaustive-deps` warnings live in `UsersPage.impl.tsx` (slice 6, out of scope). |
|
||||
| `npm test` (`vitest run`) | **pass (exit 0)** | 19 files / 39 tests, including the 5 new Settings+Actions tests. |
|
||||
| `npm run test:node` (`node --test tests/*.test.mjs`) | **pass (exit 0)** | 4/4 node suites (untouched). |
|
||||
| `grep @mui/(material\|icons-material\|x-data-grid)` over both pages | **BOTH-MUI-FREE** | Hard gate satisfied. |
|
||||
|
||||
> Note on `node --test tests`: the literal `node --test tests` invocation in the
|
||||
> tasks.md exit-gate line mis-resolves `tests` as a module entry (`Cannot find
|
||||
> module '.../tests'`). The working command is the npm script
|
||||
> `test:node` = `node --test tests/*.test.mjs` (matches slice-1's harness), which
|
||||
> is green. `npm test` covers the component suite.
|
||||
|
||||
### Deviations from design / notes
|
||||
|
||||
- **`Alert` variant surface:** the shadcn `Alert` primitive only exposes
|
||||
`default` + `destructive`. MUI `severity="info"/"success"/"warning"` map to the
|
||||
default `Alert` and `severity="error"` maps to `destructive`. All SSH
|
||||
validation / info / warning **message text is preserved verbatim** (behavior
|
||||
parity); only the severity→color cue is flattened to the two available Alert
|
||||
variants.
|
||||
- **Badge cues:** MUI `Chip variant="outlined"` → `Badge variant="outline"`; the
|
||||
danger-zone "Destructive" chip and destructive buttons → `Badge`/`Button
|
||||
variant="destructive"`. Status chips (enabled/disabled/service counts/run
|
||||
status) use `outline`. No `success` cue was needed at these call sites (run
|
||||
status is a free-text string), consistent with design §2.3.
|
||||
- **Radix Select empty value:** `NONE = "__none__"` sentinel is converted to `""`
|
||||
at the draft boundary for both the SSH-key picker (Settings) and the
|
||||
default-machine picker (Actions). Required because Radix Select rejects
|
||||
empty-string item values.
|
||||
- **Hover-reveal edit affordance:** `HoverEditButton` (not editable this slice)
|
||||
ships with a baked `opacity-0` + `rail-edit` class. Since the MUI
|
||||
`&:hover .rail-edit` sx rules are gone, each rail row now carries a Tailwind
|
||||
`group` + arbitrary-variant `group-hover:[&_.rail-edit]:opacity-100` to restore
|
||||
the hover-reveal (verified the utility is generated by the Tailwind v4 build).
|
||||
- **Dialog widths:** MUI `maxWidth` → `sm:max-w-4xl` (machine editor), `sm:max-w-2xl`
|
||||
(task editor), `sm:max-w-md` (danger-zone reset). A `DialogDescription` was
|
||||
added to each shadcn dialog (a11y + avoids the Radix "missing description" dev
|
||||
warning) without changing behavior.
|
||||
|
||||
### SSH-validation / form behavior worth flagging
|
||||
|
||||
- The `validateMachineSSH` flow, its `try/catch`, the `known_hosts_updated` →
|
||||
status-string branch, and the three lowered-message status branches
|
||||
(protocol-banner / auth-failed / generic) are byte-for-byte preserved.
|
||||
- `saveMachineDraft` still clears SSH validation, awaits `mutateAsync`, then closes
|
||||
the dialog and resets the draft to `emptyMachine(mode)`.
|
||||
- The danger-zone reset `canSubmit` gate (exact phrase match + all 3 acks) is
|
||||
preserved and wired to `DialogFooter.confirmDisabled`.
|
||||
|
||||
### Remaining tasks (exact unchecked `- [ ]` lines)
|
||||
|
||||
All Slice-5 lines are `[x]`. The remaining 28 unchecked lines are Slice 6
|
||||
(Users: table/drawer/compose + 9 lucide icons), Slice 7 (`DataTable` + Media +
|
||||
FileBrowser off `@mui/x-data-grid`), and Slice 8 (package.json cleanup + grep
|
||||
gates + `docs/REQUIREMENTS.md`). No slice-5 work remains.
|
||||
|
||||
### Workload / PR boundary
|
||||
|
||||
Single slice, under the 400-line added budget: ~1097 insertions / ~1337 deletions
|
||||
across the two page rewrites (net −240 — the migration is more compact) + 2 new
|
||||
test files (~150 lines). The parent owns the commit/PR; nothing committed here.
|
||||
|
||||
### Top risk for slice 6
|
||||
|
||||
**`UsersPage.impl.tsx`** is the largest consumer (25 MUI components + 9
|
||||
`@mui/icons-material` icons + `Drawer`→`Sheet` + rich-text compose with
|
||||
file attachments + selection-across-pagination). Per `design.md` §8 it has a
|
||||
**high sub-split likelihood**; expect a 6a (directory table + selection + drawer)
|
||||
→ 6b (compose dialog + formatting actions + attachments) split to stay ≤400
|
||||
lines. The icon-name pin (`Close→X`, `AttachFile→Paperclip`, `FormatBold→Bold`,
|
||||
`FormatItalic→Italic`, `Link→Link`, `FormatListBulleted→List`, `MailOutlined→Mail`,
|
||||
`Send→Send`, `DeleteOutlined→Trash2`) must be verified at the lucide-react
|
||||
`^1.14.0` pin before authoring. Selection-across-pagination semantics
|
||||
(`selectedUserIds` surviving paging/filtering) and the rich-text compose behavior
|
||||
(queue-status polling + `FormData` attachments + `useSendUserMessage`) are the
|
||||
behavior-parity surfaces to guard.
|
||||
|
||||
### Structured status note
|
||||
|
||||
Overall change `applyState` remains **blocked** per the status engine (domain
|
||||
specs missing/partial; legacy flat `spec.md`). This is a planning-completeness
|
||||
gap only — `design.md` §1 supplied the authoritative MUI→shadcn mapping and
|
||||
`actionContext` is `repo-local` with `allowedEditRoots: ["/home/user/Manage_01"]`.
|
||||
The parent explicitly delegated Slice 5 with a clear scope/delivery path, so this
|
||||
slice proceeded under that delegation. Should be resolved before
|
||||
`sdd-verify`/archive, per the slice-3/slice-4 notes.
|
||||
|
||||
Reference in New Issue
Block a user