Files
ui/_docs/02_document/modules/src__components__Header.md
T
Oleksandr Bezdieniezhnykh 510df68bcf [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>
2026-05-11 00:38:49 +03:00

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 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/AuthContextuseAuth (user, logout, hasPermission).
    • ./FlightContextuseFlight (flights, selectedFlight, selectFlight).
    • ./HelpModal — opens on ? click.
    • ../typesFlight 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.