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>
6.8 KiB
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
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 theHelpModalopenprop.dropdownRef: RefObject<HTMLDivElement>— used to detect outside clicks.
-
Outside-click effect: registers a
mousedownlistener ondocumentwhile mounted; if the event target is not insidedropdownRef.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 onshowDropdownin 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 iflogout()throws (it never does —AuthContext.logoutswallows network errors by design). -
Language toggle:
i18n.changeLanguage(i18n.language === 'en' ? 'ua' : 'en'). Same two-language assumption asHelpModal(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):tolabelkeyperm/flightsnav.flightsFL/annotationsnav.annotationsANN/datasetnav.datasetDATASET/adminnav.adminADMSettings(/settings) is always rendered — no permission gate. Filter applied vianavItems.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 issm:hiddenand 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—Flighttype for the dropdown row.
- External:
react-router-dom(NavLink,useNavigate),react-i18next(useTranslation),react(useState,useRef,useEffect).
Consumers (intra-repo)
src/App.tsx— mounted insideProtectedRoute → 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 insrc/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 insrc/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 theadmin/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/adminroute URL can navigate there without going through the nav link. The backendadmin/service is the authority. Document insecurity_approach.md(Step 6).- The flight-selector dropdown displays every flight returned by
GET /api/flights?pageSize=1000— there is no permission check here. Ifflights/ever returns flights the user should not see, this component would leak them. Trust the backend filter. user?.emailis rendered raw in the header — React JSX-escapes it, so XSS via a malicious email is not possible at this layer, but validate that theadmin/service enforces email format.
Tests
None.
Notes / open questions
- Outside-click listener is always attached — slightly wasteful when
the dropdown is closed. Gating on
showDropdownwould also remove a closure capture. Defer to Step 8. - Dropdown is not keyboard-accessible: no
role="combobox", noaria-expanded, no Esc-to-close, focus does not move into the filter input on open beyondautoFocus(which works only the first time the input mounts). Flag against_docs/ui_design/README.mdkeyboard shortcuts for Step 4. - Mobile bottom nav duplicates
navItemstwice 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. /settingslink is unguarded byhasPermission. Per_docs/ui_design/README.mdsettings page is available to every authenticated user — confirmed intent.new Date(f.createdDate).toLocaleDateString()uses the browser's default locale, noti18n.language. Mild inconsistency; Step 8 cosmetic.flightscache truncation: the dropdown shows at most 1000 flights because of the hardcodedpageSize=1000inFlightContext. Documented as a flag there; Header inherits the limitation.