mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 09:21:10 +00:00
[AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs
Captures the full output of autodev existing-code Phase A through Step 4 (Code Testability Revision) for the Azaion UI workspace: - Step 1 Document: _docs/02_document/ (FINAL_report, architecture, glossary, components/, modules/, diagrams/, system-flows, module-layout) plus _docs/00_problem/ + _docs/01_solution/ + _docs/legacy/ + _docs/how_to_test + README. - Step 2 Architecture Baseline: architecture_compliance_baseline.md. - Step 3 Test Spec: _docs/02_document/tests/ (environment, test-data, blackbox/performance/resilience/security/ resource-limit tests, traceability-matrix), enum_spec_snapshot, expected_results/results_report.md (98 rows), plus the run-tests.sh + run-performance-tests.sh runners. - Step 4 Code Testability Revision: 01-testability-refactoring/ run dir (list-of-changes C01-C07, deferred_to_refactor, analysis/research_findings + refactoring_roadmap) and the 7 child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/ plus _dependencies_table.md. - _docs/_autodev_state.md pins the cursor at Step 4 / refactor Phase 4 entry so /autodev resumes cleanly. Epic AZ-447 (UI testability gates) tracks the 7 child tasks that will land in subsequent commits. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
# Module: `src/components/Header.tsx`
|
||||
|
||||
> **Source**: `src/components/Header.tsx` (133 lines)
|
||||
> **Topo batch**: B4 (depends on B3: `auth/AuthContext`, `components/FlightContext`, `components/HelpModal`, `types/index`)
|
||||
|
||||
## Purpose
|
||||
|
||||
The persistent top navigation bar of the authenticated SPA. Combines
|
||||
five concerns into one component: brand mark, currently-selected-flight
|
||||
dropdown, top-level navigation links (permission-gated), session info
|
||||
(user email + Logout), language toggle (EN ↔ UA), and a `?` button that
|
||||
opens `HelpModal`. Also renders a duplicate bottom-nav on the mobile
|
||||
breakpoint. Replaces the legacy WPF top ribbon + window chrome
|
||||
(`_docs/legacy/wpf-era.md` §4 / §"What survived").
|
||||
|
||||
## Public interface
|
||||
|
||||
```ts
|
||||
export default function Header(): JSX.Element
|
||||
```
|
||||
|
||||
No props — Header reads everything it needs from `AuthContext`,
|
||||
`FlightContext`, and `react-i18next`. Mounted exactly once in `App.tsx`
|
||||
above the protected route tree.
|
||||
|
||||
## Internal logic
|
||||
|
||||
- **Local state**:
|
||||
- `showDropdown: boolean` — flight selector open/closed.
|
||||
- `filter: string` — text filter for the flight selector list.
|
||||
- `showHelp: boolean` — controls the `HelpModal` `open` prop.
|
||||
- `dropdownRef: RefObject<HTMLDivElement>` — used to detect outside
|
||||
clicks.
|
||||
- **Outside-click effect**: registers a `mousedown` listener on `document`
|
||||
while mounted; if the event target is not inside `dropdownRef.current`,
|
||||
closes the dropdown. Listener is removed on unmount. (Always-on, even
|
||||
while the dropdown is closed — cheap, but not the most surgical
|
||||
pattern; flag to consider gating on `showDropdown` in Step 8.)
|
||||
- **Filtered flights**: `flights.filter(f => f.name.toLowerCase().includes(filter.toLowerCase()))`
|
||||
— case-insensitive substring match on the flight name. Empty filter
|
||||
shows everything.
|
||||
- **Logout**: `await logout(); navigate('/login')`. Always navigates,
|
||||
even if `logout()` throws (it never does — `AuthContext.logout` swallows
|
||||
network errors by design).
|
||||
- **Language toggle**: `i18n.changeLanguage(i18n.language === 'en' ? 'ua' : 'en')`.
|
||||
Same two-language assumption as `HelpModal` (treats every non-`'ua'`
|
||||
value as English-equivalent). The toggle button label is the
|
||||
*target* language ("UA" while EN is active, "EN" while UA is active).
|
||||
- **Permission-gated nav** (`navItems`):
|
||||
|
||||
| `to` | `label` key | `perm` |
|
||||
|---|---|---|
|
||||
| `/flights` | `nav.flights` | `FL` |
|
||||
| `/annotations` | `nav.annotations` | `ANN` |
|
||||
| `/dataset` | `nav.dataset` | `DATASET` |
|
||||
| `/admin` | `nav.admin` | `ADM` |
|
||||
|
||||
`Settings` (`/settings`) is always rendered — no permission gate.
|
||||
Filter applied via `navItems.filter(n => hasPermission(n.perm))`.
|
||||
- **Layout**: a single `<header>` containing brand → flight dropdown →
|
||||
primary nav (`hidden sm:flex`) → spacer (`flex-1`) → user email
|
||||
(`hidden sm:block`) → language toggle → `?` → `⚙` (settings link) →
|
||||
logout button. A second `<nav>` element below is `sm:hidden` and
|
||||
positions itself fixed at the bottom of the viewport — the mobile
|
||||
nav. Both navs share the same filter logic.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Internal**:
|
||||
- `../auth/AuthContext` — `useAuth` (user, logout, hasPermission).
|
||||
- `./FlightContext` — `useFlight` (flights, selectedFlight, selectFlight).
|
||||
- `./HelpModal` — opens on `?` click.
|
||||
- `../types` — `Flight` type for the dropdown row.
|
||||
- **External**: `react-router-dom` (`NavLink`, `useNavigate`),
|
||||
`react-i18next` (`useTranslation`),
|
||||
`react` (`useState`, `useRef`, `useEffect`).
|
||||
|
||||
## Consumers (intra-repo)
|
||||
|
||||
- `src/App.tsx` — mounted inside `ProtectedRoute → FlightProvider`.
|
||||
|
||||
No other intra-repo consumer; `Header` is a top-level chrome component.
|
||||
|
||||
## Data models
|
||||
|
||||
The `navItems` array is a module-private literal of
|
||||
`{ to: string; label: string; perm: string }`. The filtered `flights`
|
||||
list is `Flight[]` (from `types/index.ts`).
|
||||
|
||||
## Configuration
|
||||
|
||||
- **i18n keys consumed**: `nav.flights`, `nav.annotations`, `nav.dataset`,
|
||||
`nav.admin`, `nav.logout`. (Verified to exist in `src/i18n/en.json`.)
|
||||
The filter placeholder ("Filter…") and the empty-state ("— Select
|
||||
Flight —", "No flights") are hardcoded strings — flag for Step 4.
|
||||
- **Tailwind tokens**: `bg-az-header`, `border-az-border`, `bg-az-panel`,
|
||||
`text-az-text`, `text-az-muted`, `text-az-orange`, `text-az-red`,
|
||||
`bg-az-bg`. Defined in `src/index.css`.
|
||||
- **Breakpoint**: `sm:` from Tailwind defaults to 640px — matches the
|
||||
responsive-breakpoint spec in `_docs/ui_design/README.md`.
|
||||
- **Permission strings**: `FL`, `ANN`, `DATASET`, `ADM`. Backend-defined
|
||||
by the `admin/` service; treated as opaque strings here.
|
||||
|
||||
## External integrations
|
||||
|
||||
None directly. Triggers `logout()` which calls `POST /api/admin/auth/logout`
|
||||
inside `AuthContext`, and `selectFlight()` which calls `PUT /api/annotations/settings/user`
|
||||
inside `FlightContext`.
|
||||
|
||||
## Security
|
||||
|
||||
- `hasPermission(perm)` is **client-side advisory** — a user with the
|
||||
`/admin` route URL can navigate there without going through the nav
|
||||
link. The backend `admin/` service is the authority. Document in
|
||||
`security_approach.md` (Step 6).
|
||||
- The flight-selector dropdown displays every flight returned by
|
||||
`GET /api/flights?pageSize=1000` — there is no permission check here.
|
||||
If `flights/` ever returns flights the user should not see, this
|
||||
component would leak them. Trust the backend filter.
|
||||
- `user?.email` is rendered raw in the header — React JSX-escapes it,
|
||||
so XSS via a malicious email is not possible at this layer, but
|
||||
validate that the `admin/` service enforces email format.
|
||||
|
||||
## Tests
|
||||
|
||||
None.
|
||||
|
||||
## Notes / open questions
|
||||
|
||||
- **Outside-click listener is always attached** — slightly wasteful when
|
||||
the dropdown is closed. Gating on `showDropdown` would also remove a
|
||||
closure capture. Defer to Step 8.
|
||||
- **Dropdown is not keyboard-accessible**: no `role="combobox"`, no
|
||||
`aria-expanded`, no Esc-to-close, focus does not move into the filter
|
||||
input on open beyond `autoFocus` (which works only the first time
|
||||
the input mounts). Flag against `_docs/ui_design/README.md` keyboard
|
||||
shortcuts for Step 4.
|
||||
- **Mobile bottom nav duplicates `navItems`** twice in source — DRY win
|
||||
by extracting a `<NavSet items={...}/>` subcomponent, but a deferral
|
||||
candidate.
|
||||
- **Hardcoded English strings** ("AZAION", "— Select Flight —",
|
||||
"Filter...", "No flights"). Brand mark is intentional; the others
|
||||
are content and should move to `nav.*` keys. Step 4 candidate.
|
||||
- **`/settings` link** is unguarded by `hasPermission`. Per
|
||||
`_docs/ui_design/README.md` settings page is available to every
|
||||
authenticated user — confirmed intent.
|
||||
- **`new Date(f.createdDate).toLocaleDateString()`** uses the browser's
|
||||
default locale, not `i18n.language`. Mild inconsistency; Step 8
|
||||
cosmetic.
|
||||
- **`flights` cache truncation**: the dropdown shows at most 1000
|
||||
flights because of the hardcoded `pageSize=1000` in `FlightContext`.
|
||||
Documented as a flag there; Header inherits the limitation.
|
||||
Reference in New Issue
Block a user