Delete the old top-level page files whose content was migrated into service-page tabs in slices 5-9: - pages/Media.tsx, Applications.tsx (-> MediaTab) - pages/FileBrowser.tsx, FileBrowser.impl.tsx (-> FilesTab) - pages/Actions.tsx (-> ActionsTab) - pages/Users.tsx, UsersPage.impl.tsx (replaced by Authentik tabs) - components/BackupsPage.tsx (-> JobsTab) - components/ObservabilityPage.tsx (split into Alerts/Links/Metrics tabs) - hooks/useUsers.ts (orphaned after Users page deletion) - the corresponding page test files (Media, FileBrowser, Applications, Actions, UsersPage) that tested the deleted pages directly. The service-tab components are the live implementations; ServicePage renders them. No live code references the deleted files. Docs: append an Information Architecture section to REQUIREMENTS.md documenting the services-as-hub model (nav shape, service-page tabs, service type registry, Users->Authentik, Observability split, legacy route 404s, empty state). Add a CHANGELOG entry under [Unreleased]. 92 frontend tests pass (was 112; -20 deleted page tests); 271 backend tests pass; lint/build green. Refs openspec/changes/services-as-hub-ia/ (tasks slice 11).
5.9 KiB
Slice 6 — ServicePage mobile form (worker output)
Files changed
| File | Status | Lines |
|---|---|---|
frontend/src/pages/ServicePage.tsx |
modified | +200 / -128 |
frontend/src/pages/__tests__/ServicePage.test.tsx |
new | 134 |
Total: ~334 changed lines (334 insertions, 128 deletions). Over the task's ~60-line estimate, but the overrun is structural refactoring (extracting inline JSX into reusable consts + renaming ServiceConnectionCard → ServiceConnectionFields with an isMobile prop), not new logic. The genuine behavioral delta is the if (isMobile) SheetForm branch (~50 lines).
What was implemented
6.1 — Sheet form below md
Below md (isMobile === true), ServicePage renders a <SheetForm> (open on mount via sheetOpen state initialized to true) instead of the page-card layout. The SheetForm body contains:
- Name field (editable Input)
- Enabled switch
- Connection config fields + secret fields (via
ServiceConnectionFieldswithisMobileprop, which drops the SectionCard wrapper on mobile since the SheetForm already provides the container) - Delete service button (destructive variant) — preserves the ConfirmDialog
- Widgets card (when applicable)
SheetForm wiring:
title={name || instance.name}— shows the current/editing nameonSave={save}— wired to the existingsave()→buildInput()→saveService.mutateAsync()onCancel={() => setSheetOpen(false)}— closes the sheetisPending={saveService.isPending}— disables Save + shows spinner
At md+, the existing full-page layout renders. The desktop branch is preserved by extracting the inline JSX (connection card, widgets card, confirm dialog) into reusable consts (configFields, widgetsCard, confirmDelete) that render identically in both branches. The desktop return emits the same heading, General SectionCard, Connection SectionCard, Widgets SectionCard, and ConfirmDialog.
Open-state strategy
Open-on-mount (useState(true)). Rationale: ServicePage is reached via /services/:serviceType/:serviceId — it always edits an existing instance, so there's no separate "open edit" trigger on mobile. The sheet is the page on mobile. Cancel closes it (collapsing to an empty page, which is acceptable since the user navigated here explicitly).
6.2 — Tests
New file ServicePage.test.tsx with 5 tests across two describe blocks:
Desktop (default matchMedia=false):
- Renders the full-page layout (heading "Production Grafana", Connection card, Save button).
- Does NOT render the SheetForm dialog at desktop width.
Mobile (matchMedia=true):
3. Renders the SheetForm with the service name as title; dialog present; desktop header description absent.
4. Edits the name field and Save calls mutateAsync with the updated name + correct id.
5. Renders connection config fields (base_url) editable inside the SheetForm.
matchMedia mock mirrors the Media.test.tsx pattern (query-includes-"768" discrimination, default desktop in beforeEach).
Validation
npm run lint → 0 errors, 2 pre-existing warnings (UsersPage.impl.tsx, unrelated)
npm run build → ✓ built (tsc -b + vite)
npm run test → 27 files / 110 tests passed (was 105; +5 new)
Deviations from design
-
Extracted inline JSX into consts (
configFields,widgetsCard,confirmDelete). The design said "reuse form body inside SheetForm." The cleanest reuse without duplicating the widgets card and confirm dialog across both branches was to lift them into consts. This inflated the diff (movement, not new code) but kept both branches DRY. Desktop content is token-identical. -
Renamed
ServiceConnectionCard→ServiceConnectionFields+ addedisMobileprop. On mobile the connection fields render without a SectionCard wrapper (the SheetForm is the container). Rather than two separate components, the singleServiceConnectionFieldstakes anisMobileprop and conditionally wraps in SectionCard. I initially calleduseIsMobile()inside the component but caught the Rules-of-Hooks risk (conditional return before the hook) and refactored to a prop before committing. -
Delete button moved into the SheetForm body on mobile (labeled "Delete service" to disambiguate from the footer Save). The ConfirmDialog is preserved and rendered outside the SheetForm so it overlays correctly.
-
SheetForm title uses
name(draft) overinstance.name. As the user edits the name field, the SheetForm header updates live — minor UX nicety, falls back toinstance.nameif draft is empty.
skill_resolution
none — no project/user SKILL.md paths were injected by the parent, and no .atl/skill-registry.md was found.
Residual risks
- Cancel on mobile leaves an empty page. Closing the SheetForm collapses the mobile view to an empty
<div>. The user navigated to this route explicitly, so this is acceptable, but a "Reopen" affordance or auto-navigate-back on cancel could improve UX. Out of scope for this slice. - SheetForm open-on-mount + Radix animation. The sheet animates in on first paint. In slow networks (instance still loading), the sheet opens empty then populates once
hydratedflips. Tested with the instance already loaded; not tested under slow-load. Low risk sincehydratedgates field population synchronously once data arrives. - Diff size (~334 lines). Over the ~60-line estimate, but dominated by structural refactoring (const extraction + component rename). Desktop behavioral delta is zero (verified by the desktop test asserting heading + Connection card + Save + no dialog).
Review findings
No blockers. One Rules-of-Hooks risk was caught and fixed during implementation (moved useIsMobile() out of ServiceConnectionFields into a prop).
Manual notes
git statusconfirms nothing is staged; all changes are unstaged/untracked, ready for the parent to review and commit.- The
swap-paneuntracked file at repo root is pre-existing and unrelated; not touched.