dd778d8850
Web UI rework (openspec/changes/web-ui-rework). Foundation slice: - Add @tanstack/react-table; remove orphaned recharts, d3 - Vendor shadcn primitives: tabs table dialog input label checkbox switch progress separator avatar textarea dropdown-menu scroll-area - Add Vitest + @testing-library/react + jsdom harness; npm test script - Delete no-op theme.ts shim; remove getAppTheme references - Smoke test proves the harness Gate: build + lint + test green.
222 lines
18 KiB
Markdown
222 lines
18 KiB
Markdown
# Exploration — web-ui-rework
|
||
|
||
> Phase: **explore**. Evidence-grounded mapping of the rework surface. No code changes.
|
||
> Recovered by the orchestrator after the `sdd-explore` subagent hit an MCP-bridge heap OOM;
|
||
> all figures below were re-derived directly from source with multiline-aware parsing and verified with `grep`.
|
||
|
||
## 1. Scope and non-goals
|
||
|
||
### In scope
|
||
|
||
- Finish the **MUI v9 → shadcn/ui + Tailwind CSS + lucide-react** migration for every remaining `@mui/*` consumer in `frontend/src`.
|
||
- Deliver a **visual/UX redesign** on top of the migrated primitives, consistent with the already-migrated style references: `frontend/src/App.tsx` (shell + sidebar) and `frontend/src/components/ObservabilityPage.tsx`.
|
||
- Align the UI to the **new observability model**: Manage is a *thin dashboard*; charts/metrics/logs live in external, decoupled Grafana. In-app surfaces show Alertmanager alerts + Prometheus target health + Grafana deep-links only.
|
||
- Remove orphaned charting deps (`recharts`, `d3`) and the no-op `theme.ts` shim.
|
||
- Add the missing shadcn primitives and `@tanstack/react-table` needed for the migration.
|
||
- Update `docs/REQUIREMENTS.md` to reflect the UX/architecture change (per `AGENTS.md`).
|
||
|
||
### Non-goals
|
||
|
||
- **No in-app charting.** Do not re-introduce recharts/d3 or build custom charts; metrics visualisation stays in Grafana.
|
||
- **No new design system.** Use shadcn/ui + the existing Tailwind v4 `@theme` token system; do not adopt a different component library.
|
||
- **No backend API changes** as part of this rework. The data contracts in `frontend/src/types/*` are unchanged unless a UI simplification forces one, which must then be flagged separately.
|
||
- **No MUI retention.** `@mui/material`, `@mui/icons-material`, `@mui/x-data-grid`, `@emotion/react`, `@emotion/styled` are all removed by the final slice.
|
||
- **No auth/routing model change.** OIDC flow (`auth.ts`, `react-oidc-context`) and `react-router-dom` structure stay as-is; only nav *items*/IA may shift.
|
||
|
||
## 2. Extended component inventory (verified from source)
|
||
|
||
Parsing note: MUI imports are multiline (`import {\n Box,\n Card,\n} from "@mui/material"`); figures below use a brace-bounded multiline parser and were cross-checked with `grep -rlE '@mui/(material|icons-material|x-data-grid)' src`.
|
||
|
||
### 2a. `@mui/material` — 22 files, 37 distinct components
|
||
|
||
| File | # | Components |
|
||
|---|---|---|
|
||
| `pages/UsersPage.impl.tsx` | 25 | Alert, Avatar, Box, Button, Checkbox, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, Drawer, IconButton, LinearProgress, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Tooltip, Typography |
|
||
| `pages/Dashboard.tsx` | 20 | Alert, Box, Button, Card, CardContent, Chip, Dialog, DialogContent, DialogTitle, FormControl, FormControlLabel, FormHelperText, Grid, InputLabel, MenuItem, Select, Stack, Switch, TextField, Typography |
|
||
| `pages/Actions.tsx` | 19 | Alert, Box, Button, Card, CardContent, Chip, Dialog, DialogContent, DialogTitle, Divider, FormControl, InputLabel, MenuItem, Select, Stack, Tab, Tabs, TextField, Typography |
|
||
| `pages/Settings.tsx` | 18 | Alert, Box, Button, Card, CardContent, Checkbox, Chip, Dialog, DialogContent, DialogTitle, FormControlLabel, Grid, MenuItem, Stack, Switch, Tab, TextField, Typography |
|
||
| `pages/FileBrowser.impl.tsx` | 15 | Alert, Box, Button, Card, CardContent, Chip, FormControl, Grid, InputLabel, MenuItem, Select, Stack, Tab, TextField, Typography |
|
||
| `pages/Media.tsx` | 14 | Alert, Box, Button, Card, CardContent, FormControl, Grid, InputLabel, LinearProgress, MenuItem, Select, Stack, TextField, Typography |
|
||
| `components/BackupRunsTable.tsx` | 12 | Chip, FormControl, InputLabel, MenuItem, Paper, Select, Table, TableBody, TableCell, TableContainer, TableHead, TableRow |
|
||
| `components/SessionActivityPanel.tsx` | 10 | Button, Chip, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography |
|
||
| `components/BackupAlertsTable.tsx` | 9 | Button, Chip, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow |
|
||
| `pages/Applications.tsx` | 9 | Alert, Box, Card, CardContent, Chip, Grid, Stack, Tab, Typography |
|
||
| `components/BackupJobsTable.tsx` | 8 | Chip, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow |
|
||
| `components/DiskSpaceCard.tsx` | 7 | Box, Card, CardContent, Grid, LinearProgress, Stack, Typography |
|
||
| `components/BackupDashboardWidget.tsx` | 5 | Box, Card, CardContent, Chip, Typography |
|
||
| `components/ConfirmDialog.tsx` | 5 | Dialog, DialogContent, DialogTitle, Stack, Typography |
|
||
| `components/LibraryOverview.tsx` | 5 | Card, CardContent, Grid, Stack, Typography |
|
||
| `components/SectionCard.tsx` | 5 | Box, Card, CardContent, Stack, Typography |
|
||
| `components/BackupsPage.tsx` | 4 | Box, Tab, Tabs, Typography |
|
||
| `components/SelectionRailCard.tsx` | 4 | Box, Card, CardContent, Typography |
|
||
| `components/TabbedCard.tsx` | 4 | Box, Card, CardContent, Tabs |
|
||
| `components/DialogFooter.tsx` | 3 | Box, Button, DialogActions |
|
||
| `components/MetricCard.tsx` | 3 | Card, CardContent, Typography |
|
||
| `components/HoverEditButton.tsx` | 1 | IconButton |
|
||
|
||
**Component frequency (migration sizing):** Typography 16 · Box 14 · Card/CardContent 13 · Chip 11 · Stack 11 · Button 9 · Grid 7 · Alert 7 · MenuItem 6 · TextField 6 · Paper/Table*/FormControl/InputLabel/Select/Tab 5 · Dialog/DialogContent/DialogTitle 5 · Tabs 3 · LinearProgress 3 · DialogActions/IconButton/Divider/FormControlLabel/Switch/Checkbox 2 · FormHelperText/Avatar/Drawer/Tooltip 1.
|
||
|
||
### 2b. `@mui/x-data-grid` — 2 files (DataGrid)
|
||
|
||
- `pages/Media.tsx` — `DataGrid`, `GridColDef`
|
||
- `pages/FileBrowser.impl.tsx` — `DataGrid`, `GridColDef`, `GridRowSelectionModel`
|
||
|
||
### 2c. `@mui/icons-material` — 2 files, 10 distinct icons (default-import-per-icon style)
|
||
|
||
| File | Icons → lucide-react equivalent |
|
||
|---|---|
|
||
| `components/HoverEditButton.tsx` | `EditOutlined` → `Pencil` |
|
||
| `pages/UsersPage.impl.tsx` | `Close`→`X` · `AttachFile`→`Paperclip` · `FormatBold`→`Bold` · `FormatItalic`→`Italic` · `Link`→`Link` · `FormatListBulleted`→`List` · `MailOutlined`→`Mail` · `Send`→`Send` · `DeleteOutlined`→`Trash2` |
|
||
|
||
> **lucide caveat:** `package.json` pins `lucide-react: ^1.14.0`, an unusual major. The named icons above (`Pencil`, `X`, `Paperclip`, `Bold`, `Italic`, `Link`, `List`, `Mail`, `Send`, `Trash2`) are standard lucide exports, but the version pin must be verified to export them before the UsersPage slice.
|
||
|
||
### 2d. Orphaned / unused deps (verified zero imports)
|
||
|
||
- `recharts: ^3.8.1` — **no `from "recharts"` anywhere.** Removable.
|
||
- `d3: ^7.9.0` — **no `from "d3"` / `from "d3-*"` anywhere.** Removable.
|
||
- `frontend/src/theme.ts` — no-op shim (`getAppTheme` returns `{}`); safe to delete once no caller remains.
|
||
|
||
### 2e. Already-migrated style references (the design targets)
|
||
|
||
- `frontend/src/App.tsx` — shell: fixed sidebar (`w-16`/`w-60` collapse), `TopBar` (sticky, backdrop-blur, version chips), `MobileDrawer` (shadcn `Sheet`), `useDarkMode` toggles `dark` class on `<html>`.
|
||
- `frontend/src/components/ObservabilityPage.tsx` (575 lines) — uses shadcn `Card/Badge/Alert/Button/Select/Skeleton/Collapsible` + 12 lucide icons; Grafana deep-links via kiosk URLs (`/d/node-exporter-overview/...?kiosk&var-instance=…`) and Explore.
|
||
|
||
## 3. MUI → shadcn/ui + Tailwind mapping table
|
||
|
||
| MUI | shadcn/ui + Tailwind | Primitive status |
|
||
|---|---|---|
|
||
| `Card` / `CardContent` | `Card` / `CardContent` (`@/components/ui/card`) | **present** |
|
||
| `Typography` | semantic `<h1>`–`<h6>`/`<p>`/`<span>` + Tailwind `text-* font-*` | none needed |
|
||
| `Box` | `<div>` + flex/grid utilities | none needed |
|
||
| `Stack` | `<div className="flex flex-col gap-*">` (or `flex-row`) | none needed |
|
||
| `Grid` | CSS grid `grid grid-cols-* gap-*` w/ responsive `md:`/`lg:` | none needed |
|
||
| `Chip` | `Badge` (`@/components/ui/badge`) | **present** |
|
||
| `Button` | `Button` (`@/components/ui/button`) | **present** |
|
||
| `IconButton` | `Button variant="ghost" size="icon"` (pattern already used in shell) | **present** |
|
||
| `Tooltip` | `Tooltip` (`@/components/ui/tooltip`) | **present** |
|
||
| `Alert` | `Alert`/`AlertTitle`/`AlertDescription` | **present** |
|
||
| `Select`/`MenuItem`/`FormControl`/`InputLabel` | `Select` family (`@/components/ui/select`) | **present** |
|
||
| `Paper` | `<div className="rounded-lg border bg-card">` or `Card` | none needed |
|
||
| `Table`/`TableHead`/`TableBody`/`TableRow`/`TableCell`/`TableContainer` | shadcn `Table` family | **add** |
|
||
| `Tabs`/`Tab` | shadcn `Tabs` (`TabsList`/`TabsTrigger`/`TabsContent`) | **add** |
|
||
| `Dialog`/`DialogTitle`/`DialogContent`/`DialogActions` | shadcn `Dialog` (+ `DialogFooter`, `DialogHeader`) | **add** |
|
||
| `TextField` | `Input` (+ `Label`) | **add** |
|
||
| `Checkbox` | `Checkbox` | **add** |
|
||
| `Switch` | `Switch` | **add** |
|
||
| `LinearProgress` | `Progress` | **add** |
|
||
| `Divider` | `Separator` | **add** |
|
||
| `Avatar` | `Avatar` | **add** |
|
||
| `Drawer` | `Sheet` (side="right") — already used for mobile nav | **present** |
|
||
| `FormControlLabel` / `FormHelperText` | `Label` + `<p className="text-xs text-muted-foreground">` | **add** `Label` |
|
||
| `DataGrid` (`@mui/x-data-grid`) | TanStack Table + shadcn `Table` (see §4) | **add** `@tanstack/react-table` |
|
||
|
||
## 4. DataGrid decision — TanStack Table (KEY TECHNICAL RISK)
|
||
|
||
**Recommendation:** migrate both DataGrid usages to **`@tanstack/react-table`** (headless) styled with the shadcn `Table` primitive. Rationale: headless (no opinionated styling, fits Tailwind), first-class sorting/filtering/pagination/selection/column-visibility, actively maintained, and the project already uses `@tanstack/react-query` so the dependency family is consistent.
|
||
|
||
**Feature parity that MUST be preserved** (derived from source):
|
||
|
||
| Page | Columns (`field`) | Features in use |
|
||
|---|---|---|
|
||
| `pages/Media.tsx` | title, series, season, episode, type, year, runtime_min, size, bitrate, hdr, video, resolution, date_added, library, path | pagination, row selection, row click (opens file browser), column visibility management, stable row id |
|
||
| `pages/FileBrowser.impl.tsx` | type, name, ext, size, modified | row selection, row click (selects file for ffprobe), column visibility management |
|
||
|
||
**Open parity questions for the proposal phase:**
|
||
|
||
- Does the redesign keep client-side column *resizing*, or only *visibility*? (Source scan shows visibility; resizing usage is ambiguous and must be confirmed against the live DataGrid props.)
|
||
- Sorting/filtering: Media has external controls already (its own filter UI); decide whether TanStack sorting is added or deferred to keep slice size ≤400 lines.
|
||
|
||
This is the single highest-risk slice; it is sequenced **last** so the shared Table primitive and design tokens are already settled.
|
||
|
||
## 5. shadcn primitives gap list
|
||
|
||
Add via `npx shadcn@latest add <name>` (project already initialised — `components.json`, `@/components/ui`, `cn()` in `src/lib/utils.ts`):
|
||
|
||
```
|
||
tabs table dialog input label checkbox switch progress separator avatar textarea dropdown-menu scroll-area
|
||
```
|
||
|
||
Plus dependency: `npm i @tanstack/react-table`.
|
||
|
||
Already present (do not re-add): `button tooltip sheet card badge alert select skeleton collapsible`.
|
||
|
||
## 6. Design tokens, theming, and redesign direction
|
||
|
||
**Current tokens (`frontend/src/index.css`, Tailwind v4 `@theme`)** — already a complete shadcn-style system:
|
||
|
||
- Font: Inter (300–700 via Google Fonts).
|
||
- Light: background `#fafafa`, foreground `#0f172a`, card `#ffffff`, primary `#4f8cff`, border `#e2e8f0`, muted-foreground `#64748b`.
|
||
- `.dark`: background `#0f172a`, card `#1e293b`, border `#334155`, muted-foreground `#94a3b8`.
|
||
- Radius `0.625rem`. Chart palette `chart-1..5` defined but **unused** (no charting) — keep for Grafana-link color cues, or drop.
|
||
- Dark mode: `useDarkMode()` in `App.tsx` toggles `.dark` on `<html>`; `theme.ts` is a no-op shim.
|
||
|
||
**Config:** `frontend/tailwind.config.cjs` is minimal (`content` glob only) — correct for Tailwind v4 which reads tokens from `@theme` in CSS. No JS theme config to migrate.
|
||
|
||
**Redesign direction (consistent with shell + ObservabilityPage):**
|
||
|
||
- **Density:** comfortable, not cramped. Page padding `p-4 md:p-6` (matches `<main>` in `App.tsx`). Card gap `gap-4`. Section spacing via `SectionCard` replacement.
|
||
- **Typography ramp:** headings `text-lg font-semibold` (page title, per `TopBar`), section titles `text-base font-semibold`, body `text-sm`, captions `text-xs text-muted-foreground`.
|
||
- **Color semantics:** status uses Badge variants — `default` (info), `secondary` (neutral), `destructive` (error/firing alert), and a success variant to add for healthy/OK (currently only ObservabilityPage has health semantics). Keep primary blue `#4f8cff`.
|
||
- **Surfaces:** prefer `Card` over ad-hoc `Paper`; one border weight (`border-border`), one radius (`rounded-lg`).
|
||
- **Layout primitive:** replace MUI `Grid`/`Stack` with a small set of Tailwind patterns documented in the design doc so all 22 files stay consistent across slices.
|
||
|
||
## 7. Information architecture & navigation
|
||
|
||
**Current nav (`App.tsx` `navItems`):** Dashboard (`/`), Observability (`/observability`), Media (`/applications`), Files (`/files`), Users (`/users`), Actions (`/actions`), Settings (`/settings`).
|
||
|
||
**Routing facts:**
|
||
|
||
- `/monitoring` → `<Navigate to="/observability" replace />` (legacy redirect).
|
||
- `/media` → `Applications` (alias).
|
||
- `/backups` route **exists** (`BackupsPage`) but is **not in the sidebar** — reachable only by URL or via the Dashboard widget's deep-link.
|
||
|
||
**IA recommendations for the proposal:**
|
||
|
||
- Surface **Backups** in the nav (currently hidden) — either its own item or folded into a "Media ops" group.
|
||
- Reconcile the **Media ↔ Applications** naming (nav says "Media", route is `/applications`, page component is `Applications`); pick one name.
|
||
- Keep Observability as a first-class top-level item (it is the post-decoupling home for status + Grafana links).
|
||
|
||
## 8. Observability-decoupling implications for the redesign
|
||
|
||
- **Stays in-app:** Alertmanager alerts list, Prometheus target health table, monitoring-machine status, Grafana deep-link cards (kiosk iframe URLs + Explore). All already implemented in `ObservabilityPage.tsx`.
|
||
- **Becomes / stays a Grafana deep-link:** any metric chart, time-series, log stream. The `chart-1..5` tokens and orphaned `recharts`/`d3` deps are leftovers from the removed in-app Monitoring UI — removing them codifies the decoupling.
|
||
- **Design consequence:** no chart components need to be designed; "metric" surfaces become either a number (MetricCard) or an outbound Grafana link. The redesign should make the **external-link affordance** consistent (lucide `ExternalLink`, already used in `ObservabilityPage`).
|
||
|
||
## 9. Slice plan preview (Force-chained, ≤400 changed lines/PR)
|
||
|
||
Sequenced so each slice leaves `npm run build` + `npm run lint` green and is independently reviewable:
|
||
|
||
1. **Foundation** — add shadcn primitives (§5), add `@tanstack/react-table`, remove `recharts`+`d3` from `package.json`, delete `theme.ts`. Enables all later slices.
|
||
2. **Shared components** — migrate the reusable building blocks first to prevent drift: `SectionCard`, `SelectionRailCard`, `TabbedCard`, `MetricCard`, `DiskSpaceCard`, `HoverEditButton`, `DialogFooter`, `ConfirmDialog`, `LibraryOverview`, `NowPlaying`/`SessionActivityPanel`.
|
||
3. **Backups cluster** — `BackupAlertsTable`, `BackupJobsTable`, `BackupRunsTable`, `BackupsPage`, `BackupDashboardWidget` (+ surface `/backups` in nav).
|
||
4. **Dashboard + Applications** — `Dashboard.tsx`, `Applications.tsx`.
|
||
5. **Settings + Actions** — `Settings.tsx`, `Actions.tsx`.
|
||
6. **Users** — `UsersPage.impl.tsx` (largest MUI consumer: 25 components + 9 icons + Drawer + Table + rich-text compose). May sub-split.
|
||
7. **DataGrid → TanStack Table** — `Media.tsx` + `FileBrowser.impl.tsx` (highest risk; sequenced last).
|
||
8. **Cleanup + docs** — remove `@mui/*` + `@emotion/*` deps, final typecheck/lint pass, update `docs/REQUIREMENTS.md`.
|
||
|
||
~8 slices; slices 6 and 7 are the most likely to need sub-splitting at apply time to stay under 400 lines.
|
||
|
||
## 10. Risks & open questions for the proposal
|
||
|
||
1. **DataGrid parity (high)** — TanStack Table must reproduce pagination/selection/row-click/column-visibility; resizing/sorting scope undecided (§4).
|
||
2. **UsersPage size (high)** — single largest migration; rich-text compose UI with formatting actions, attachments, drawer, table. Needs its own careful slice (possibly split).
|
||
3. **Cross-slice visual drift (medium)** — 22 files migrated over 8 slices; mitigated by doing shared components (slice 2) and documenting the Tailwind layout patterns in the design doc first.
|
||
4. **lucide-react v1 pin (low–medium)** — unusual major (`^1.14.0`); verify all 10 mapped icon names exist before the UsersPage slice.
|
||
5. **Form patterns (low)** — current forms are uncontrolled/`useState`; keep parity, no form library introduction (non-goal).
|
||
6. **IA decisions (product)** — Backups nav placement, Media/Applications naming (§7).
|
||
7. **Testing coverage (medium)** — see §11; no component test harness exists today.
|
||
|
||
## 11. Testing strategy note
|
||
|
||
- `frontend/package.json` has **no `test` script**. Capability today:
|
||
- `npm run build` → `tsc -b && vite build` (typecheck + build).
|
||
- `npm run lint` → `eslint .`.
|
||
- `node --test` on `frontend/tests/*.mjs` (`users.test.mjs`, `userState.test.mjs`) — pure-JS transform tests, run directly, not wired into npm.
|
||
- `openspec/config.yaml` declares **no strict-TDD flag**.
|
||
- **Implication:** "green" for each slice = `tsc -b` clean + `eslint` clean + existing `node --test` suites pass + manual smoke of the affected page. The proposal should decide whether to introduce a minimal component test harness (e.g. Vitest + Testing Library) as slice 1 work, or proceed on static + smoke evidence only.
|
||
|
||
---
|
||
|
||
**Status:** explore complete. **Recommended next phase:** `proposal` (with a product-question round before locking, given redesign + IA ambiguity).
|