Skip to main content

Design System

What is Eos.Web?

Eos.Web (frontend/src/eos-web) is the Operations frontend — a React + TypeScript single-page application built with Vite. It's a login page followed by a logged-in shell:

  • A collapsible sidebar with a favorites section and a searchable full menu tree below it, loaded from GET /api/operations/menu and /menu/favorites. When collapsed, it switches to an icon-rail + hover-flyout mode.
  • A top bar with the company logo, the current branch (with a country flag), and a user pill with a Settings/Logout dropdown.
  • A dashboard body that's intentionally empty until module pages exist.

Routing, API calls, and auth

  • Routing is react-router-dom: /login is public, / is gated behind RequireAuth.
  • API calls go through an axios instance (src/api/client.ts) whose baseURL comes from the Vite env var VITE_API_BASE_URL — this is never hardcoded.
  • JWT storage: the access token lives only in an httpOnly/Secure cookie set by the backend's AuthCookieWriter — never in localStorage or anywhere else JS can read it. client.ts sets withCredentials: true so the browser sends/receives it automatically; there is no client-side JWT decode. AuthContext hydrates its CurrentUser state from GET api/auth/me instead (on mount and after login) — the server independently validates the token and derives identity from its own claims. A single in-flight refresh promise (refreshAccessToken()) is shared across concurrent 401s so N simultaneous failing requests trigger exactly one POST api/auth/refresh, not N; on refresh failure the app forces a redirect to /login.
note

Passwords are never logged. This section previously described localStorage + client-side jwt-decode — that was replaced by the httpOnly-cookie model above; if you find lingering references to tokenStorage.ts/jwt-decode elsewhere, they're stale.

Api.External is a separate backend — do not call it with apiClient

apiClient targets Logiswift.Eos.Api (the internal Operations API) and authenticates via the httpOnly cookie above — no Authorization header is set. Logiswift.Eos.Api.External is a separate ASP.NET Core project with its own base URL and an OAuth2 bearer-token auth model; Eos.Web does not currently call it. If a future feature needs to, build a separate Axios instance with OAuth2 token handling — copying src/api/*.ts patterns as-is would silently produce unauthenticated requests against the wrong auth model.

Design system — vendored, not Ant Design, for the shell/login

The shell (AppShell/TopBar/SideNav/AppBreadcrumb) and the login page are styled with a vendored CSS design system (src/styles/tokens.css + app.css), not Ant Design — ported from a sister project (LSCoreNext) that built the same system against the same legacy visual language.

antd is not a dependency of this project — don't add it without a deliberate decision. Every page, including the ~12 Reference CRUD pages' tables/ forms (Cities, Countries, Branches, Companies, Clients, Warehouses, Departments, Equipment, ForumTypes, Queues, TrCodes, Addresses), is styled with the same vendored system and custom components (DataGrid, Field, RightPanel, ConfirmDialog).

:::note Previously used antd — since ported off it This page previously described antd as a real dependency still used by the 12 CRUD pages. That's no longer true — those pages have since been ported onto the same vendored DataGrid/Field components as the shell. If you find antd imports remaining anywhere, that's drift to flag, not the current standard. :::

:::danger Rule: style with the real classes already in app.css/tokens.css — never invent a parallel one The source project learned this the hard way. Components built by guessing class names before actually reading the CSS produced zero real styling and had to be rewritten wholesale — one entire CSS file was deleted as fully superseded once the real, already-working design system was actually read. Before naming a new component-scoped class, check whether one already exists. :::

Re-vendoring

If the design system changes upstream, re-copy tokens.css/app.css deliberately. This is a vendored copy, not a fork with its own drifting values.

Grids load only the requested page from the server

table.grid has no built-in limit — unlike antd's Table, which defaults to 10 rows/page. This is the standing default for every grid going forward: build new grids this way unless explicitly told otherwise for that specific case.

:::caution Real incident: CitiesPage hard-crashed the browser CitiesPage initially shipped fetching the full MAS_CITY table and slicing it client-side. Because antd's Table had always paginated at 10 rows/page by default, nothing had ever actually rendered a whole large table into the DOM before — removing antd removed that protection without anyone deciding to. Against real (world-cities-sized) data, this hard-crashed the browser tab.

The fix was later upgraded from "paginate client-side" to real server-side pagination: the API returns only the requested page, and every page/page-size/ sort change is a new network request, not a re-slice of an already-fetched array. :::

The full shape of this pattern — PagedResult<T>, usePagedList, and the server-driven DataGrid with its page-size selector — is documented on the Pagination page under Backend Patterns.

Shared components

src/components/ holds a set of UI-kit-agnostic shared components — reuse these rather than building a one-off styled element:

  • Button
  • PageHeader
  • Modal
  • ConfirmDialog
  • EmptyState
  • LoadingSkeleton
  • ErrorBoundary
  • StatusBadge
  • AppBreadcrumb
  • MenuIcon
  • Toast / ToastProvider

MenuIcon always falls back to a generic icon today — Eos's menu API has no IconName column yet (a future backend enhancement, not built), so don't wire up per-item icons until that data exists.

Data fetching: plain useEffect/useState by default, TanStack Query for order-tracking pages

The shell, login page, and every standard CRUD page (list + create/edit + delete) use the existing axios + useEffect/useState pattern — this stays the default. Don't add TanStack Query to a CRUD page without a specific reason.

MasterOrderDetailPage/MasterOrdersPage/TransportOrderDetailPage/ TransportOrdersPage are the one deliberate exception: they're on @tanstack/react-query (QueryClientProvider wraps App.tsx, src/lib/queryClient.ts holds the shared QueryClient), adopted once the pain of hand-rolled polling + no cross-tab staleness handling became real. No SignalR/ live-push exists yet, so these pages poll instead:

  • refetchInterval: OPERATIONAL_POLL_INTERVAL_MS (pollingIntervals.ts, 45s) is the shared poll cadence on both detail pages and on usePagedListQuery (src/lib/usePagedListQuery.ts, the TanStack counterpart to usePagedList used only by these two grids) — don't pick a different number per page. refetchOnWindowFocus is also enabled globally, so switching back to a stale tab revalidates immediately instead of waiting for the next interval tick.
  • A background poll/focus-refetch failure stays silentisError flips true on any failed fetch, including a background refetch that still has good cached data, so every consumer gates notFound/error on data === undefined rather than on isError alone. A real first-load failure (data still undefined) still surfaces normally.
  • useBoundedPolling still exists and is still used internally by usePagedList for every other CRUD grid's optional pollIntervalMs — not dead code.

This is not an app-wide TanStack Query adoption — weigh the same trigger (real polling/staleness pain, not just "this page fetches data") before adding it elsewhere.

Route-level code splitting

Every page component in App.tsx (~30 pages) is wrapped in React.lazy(), with Routes wrapped in a single Suspense using the existing LoadingSkeleton as fallback — each page now emits its own build chunk instead of bundling into one index.js. Layout/provider components (AppShell, AuthProvider, RequireAuth, RouteErrorBoundary) stay eager since they're needed on every route. Add new page components the same way — lazy(() => import('./pages/NewPage')) — rather than a plain static import.

Shared DTOs come from eos-contracts, not src/types

API-boundary DTO types live in the eos-contracts package (repo root, its own package.json/tsconfig) rather than src/types/ — consumed today via a plain file: npm dependency, matching this convention for any new shared DTO rather than adding a fresh type file under src/types/. eos-contracts is also published to an Azure Artifacts npm feed for potential out-of-repo consumers, but eos-web deliberately stays on the file: reference until a real out-of-repo consumer exists — don't switch it to the published version without a reason to.

Branch-level date/number formatting

Order pages format dates/numbers using the current order's own branch locale (MAS_BRANCH's DateFormat/CultureCode), not the browser's locale or a hardcoded format — real branches genuinely diverge (see branch/format.ts, branch/useBranchFormat.ts, branch/BranchContext.tsx, and the BranchDateField component). Shipped so far on the MasterOrder pages (detail, form, create wizard, attachments panel) and the TransportOrder/Scenario-log pages that touch order dates — not yet extended to the rest of Eos.Web. Follow the same useBranchFormat/BranchDateField pattern when a new page needs to render an order-scoped date/number rather than inventing a parallel formatter.