Skip to content

Latest commit

 

History

46 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

next-gen-ui home page

next-gen-ui

KBase 2.0 web app. Vite + React 19 + TypeScript, file-based TanStack Router with TanStack Query, ORCID-only auth against the kbase auth service. Source repo for the KBase design system.


Quick start

git clone <this-repo>
cd next-gen-ui
nvm use                 # Node 22 (see .nvmrc)
npm install
npm run dev             # http://localhost:3000

The dev server binds on 0.0.0.0 so it's reachable from non-localhost hosts on your network. Set VITE_DEV_ALLOWED_HOSTS (comma-separated; leading dot is Vite's subdomain wildcard) in .env.development.local to add hosts.


Scripts

Script What it does
npm run dev Vite dev server, port 3000, HMR.
npm run build Type-check route tree, bundle to dist/.
npm run build:design-system Build the @kbase/design-system package to dist-design-system/.
npm run preview Serve the built dist/ locally (smoke-test the prod bundle).
npm run typecheck tsc --noEmit.
npm run lint ESLint (flat config in eslint.config.js).
npm run lint:fix ESLint with autofix.
npm run format Prettier write.
npm run format:check Prettier check (CI uses this).
npm test Vitest, single run (CI uses this).
npm run test:watch Vitest watch mode.

CI runs lint → format:check → build → route-tree drift check → typecheck → test. The route tree (src/routeTree.gen.ts) is committed; the drift step fails CI if npm run build produces a diff against the committed file.


Tech stack

  • Build / dev: Vite 8.
  • Framework: React 19 (StrictMode-on in dev).
  • Routing: TanStack Router, file-based, codegen via the Vite plugin (autoCodeSplitting: true). Test files (*.test.tsx) are excluded from the route tree.
  • Server state: TanStack Query 5. One QueryClient lives in main.tsx and is exposed to the router via createRouter({ context: { queryClient } }) so route loaders can call context.queryClient.ensureQueryData(...).
  • Schema validation: Zod at every network boundary. No untyped network responses leak into the app.
  • Auth: ORCID-only via the kbase auth service; session token in a .kbase.us cookie shared with the legacy UI / narratives. Implementation lives at src/api/auth/. See src/api/auth/README.md for the wire contract, flow, and trade-offs.
  • UI primitives: KBase design system at src/design-system/ (in-repo alias @kbase/design-system); also published as @kbase/design-system on GitHub Packages npm. See src/design-system/README.md.
  • Icons: @phosphor-icons/react.
  • Tests: Vitest + Testing Library, jsdom environment, MSW for HTTP stubbing.

Project layout

src/
  main.tsx                 React + Router bootstrap, QueryClient setup,
                           cross-tab auth sync, design-system token CSS.
  styles.css               App-shell styling (sidebar/header, .link, login bits).
  routeTree.gen.ts         Generated by the TanStack Router plugin. Do not edit.

  routes/                  File-based routes. The path on disk = the URL.
    __root.tsx             Root route. Auth gate (beforeLoad), error +
                           pending boundaries, layout split between
                           AppLayout (sidebar+header) and AuthLayout (no chrome).
    index.tsx              `/`        KBase 2.0 home page. Signed-out visitors
                                      are sent to `/portals` instead of `/login`.
    account.tsx            `/account` Identity + sessions.
    portals.tsx            `/portals` Public. Published-portals gallery.
    portals.module.css     Styles for the above, scoped to that route.
    design-system.tsx      `/design-system` Hidden. Design-system showcase.
    login/
      index.tsx            `/login`           Public. ORCID button + dev token paste.
      continue.tsx         `/login/continue`  Public. OAuth callback.

  api/auth/                ORCID auth subsystem. See README in this dir.
  design-system/           KBase design system (canonical source). See README.
  test/setup.ts            jsdom + jest-dom + MSW server lifecycle.

Adding a route

Drop a file under src/routes/. The URL is the path on disk. The TanStack plugin regenerates routeTree.gen.ts on dev / build.

  • Private route (the default): just create the file. The root gate in __root.tsx protects everything not in PUBLIC_ROUTES.
  • Public route: create the file and add its pathname to PUBLIC_ROUTES in src/routes/__root.tsx (e.g. /login, /login/continue).

Set the header title via staticData: { title: '…' } on the route's createFileRoute config. The app shell pulls that from the deepest matched route.


Environment variables

Two sets, and the distinction matters.

Dev-time VITE_*, read from .env* files and inlined by Vite. These configure npm run dev only — a production image ignores them.

Var Default (in code) Notes
VITE_AUTH_ORIGIN https://kbase.us Auth service origin. Empty means relative paths through the dev proxy.
VITE_COOKIE_DOMAIN unset Optional. .kbase.us for prod-like deploys; leave unset locally.
VITE_DEV_ALLOWED_HOSTS unset Comma-separated; leading dot is Vite's subdomain wildcard. For non-localhost dev hosts.

Runtime, read by the container's entrypoint and rendered into nginx.conf and index.html at start. Set these on the workload; no rebuild is involved.

Unset and empty mean the same thing.

Var Not set means
AUTH_ORIGIN no auth service in this deployment
COOKIE_DOMAIN derive from the current host
IDP_ORIGINS https://orcid.org (space-separated, for form-action)

Leaving AUTH_ORIGIN out is a supported deployment, not a broken one: public routes serve normally and sign-in reports itself as unavailable rather than posting to a path that does not exist. That is the initial rollout, before an auth route exists.

src/config.ts reads the rendered values from <meta name="config:*"> tags. An absent tag means a dev build, and falls back to the VITE_* value; a tag still holding its __PLACEHOLDER__ means a container whose operator did not set that variable, and reads as not configured.

.env.example is the documentation; .env.development overrides for npm run dev and is committed. Personal overrides go in .env.development.local (gitignored).

The auth-related env vars have deeper context in src/api/auth/README.md.


Build & deploy

Deploying on Rancher2: DEPLOYING.md.

docker build -t frontend .
docker run -e AUTH_ORIGIN=https://kbase.us -p 8080:8080 frontend

No build args. The image is environment-agnostic: the same bytes run on CI, staging, and prod, and get promoted by tag. Deploy config is rendered when the container starts, not baked into the bundle, so the artifact you tested is the artifact you ship.

Multi-stage layout:

  1. deps: npm ci against package*.json with the npm cache mounted.
  2. build: npm run builddist/, plus .csp-script-hash.
  3. runtime: nginxinc/nginx-unprivileged:1.27-alpine on port 8080, serves dist/ with the SPA fallback.

At start, docker-entrypoint.d/05-render-config.sh renders both nginx.conf and index.html from pristine templates kept outside the doc root. From templates rather than in place so the render is idempotent — the entrypoint runs again on every restart, and an in-place edit would consume its own placeholders. It exits non-zero if a __PLACEHOLDER__ survives where a real value was required — a stray one in a CSP is a broken policy — so a container that cannot be configured correctly does not serve traffic.

AUTH_ORIGIN lands in two places at once — the CSP's connect-src and form-action, and the <meta name="config:auth-origin"> the bundle reads. That coupling is the point: a bundle calling one origin while the CSP allows another produces a blocked request with a confusing console message.

nginx.conf sets:

  • Content-Security-Policy: connect-src is 'self' <AUTH_ORIGIN>; form-action is 'self' <AUTH_ORIGIN> <IDP_ORIGINS> (the IDP entry is required because modern browsers check form-action against every redirect target, and the ORCID hop falls under this); script-src is 'self' '<sha256 of the theme-init script>' (that script is inlined into index.html, and CSP blocks inline script unless it is named by hash; the hash is computed at build time by the theme-init plugin in vite.config.ts and written to .csp-script-hash for the container entrypoint, so editing the script cannot leave a stale hash behind); font-src is 'self' data:, because Vite inlines font subsets under 4 KB as data: URIs while the rest ship as files. The fonts are self-hosted via @fontsource, so no Google Fonts origin is allowlisted.
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • X-Frame-Options: DENY
  • Long-lived Cache-Control: public, immutable for /assets/* (Vite's fingerprinted bundles); no-cache for index.html.

The full security header set is repeated in every location block. add_header doesn't inherit when a child location defines its own. The repetition is fenced with # --- BEGIN security headers (keep in sync) --- … # --- END --- comments so PR diffs surface drift.

CI / CD

  • .github/workflows/ci.yml: runs on every push to main and every PR. Lint → format check → build (+ route-tree drift check) → typecheck → test.
  • .github/workflows/docker.yml: builds the multi-arch (linux/amd64, linux/arm64) image and pushes to GHCR with build provenance attestation. Tags:
    • PRs from this same repo → pr-<n>, prerelease, sha-<short>.
    • GitHub Releases → semver (x.y.z, x.y, x) plus latest if not a prerelease.
    • Fork PRs do not trigger a push (avoids leaking write-scoped tokens to untrusted code).
    • workflow_dispatch is allowed for manual rebuilds.
  • .github/workflows/design-system.yml: builds the @kbase/design-system package on every PR and push to main; uploads the .tgz as a workflow artifact. On a release with a ds-v* tag, derives the version from the tag, publishes to GitHub Packages npm, and attaches the tarball to the release. Independent of the app's vX.Y.Z releases. Details: src/design-system/README.md#release.

Testing

Vitest + Testing Library, jsdom environment. MSW (msw) is the default network stub; spin up a server in src/test/setup.ts so every test runs against the same handler set.

Conventions:

  • Test files are colocated with the unit under test (foo.test.ts, foo.test.tsx). The router plugin is configured to ignore them.
  • *.test.tsx for components, *.test.ts for pure functions.
  • Prefer Testing Library queries over implementation details; assert on roles/labels, not class names.
  • Stub the auth service via MSW. For route-level tests, use createMemoryHistory + a fresh QueryClient so cache state doesn't bleed across tests.

The auth subsystem's high-leverage coverage is summarised in src/api/auth/README.md.


Conventions

  • TypeScript strict. No any in committed code. Use Zod-derived types at boundaries.
  • No global mutable state. The QueryClient is dependency-injected via the router context.
  • Don't mix layers in api/. Each subsystem under src/api/ is schemas → client → cookie → queries → hooks → index, each importing only from layers below. New backends (workspace, narrative, …) get their own sibling directory with the same shape.
  • Route styles in a sibling *.module.css, app-shell styling in styles.css, design-system tokens for everything else.

Troubleshooting

npm run dev won't load, "host not allowed": Set VITE_DEV_ALLOWED_HOSTS in .env.development.local (comma-separated; leading dot is Vite's subdomain wildcard).

Auth-related issues (cookie not landing, 403 on sessions, Bearer 401, etc.). See the troubleshooting section in src/api/auth/README.md.


Subsystem docs

  • src/api/auth/README.md: wire contract, login flow, safeRedirect, cross-tab sync, token storage trade-off, local dev, troubleshooting.
  • DEPLOYING.md: Rancher2 deployment — image tags, container facts, runtime configuration, and what the Ingress has to do.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages