Rewrite the Service Management Portal as a React SPA - #28
Open
Paul Lizer (paullizer) wants to merge 2 commits into
Open
Rewrite the Service Management Portal as a React SPA#28Paul Lizer (paullizer) wants to merge 2 commits into
Paul Lizer (paullizer) wants to merge 2 commits into
Conversation
Replace the Flask/Jinja/Bootstrap portal with a React 18 + TypeScript single-page app built with Vite and Tailwind CSS v4, using a custom glassmorphism design system. Flask stays, but as a backend-for-frontend. The MSAL confidential client flow is unchanged: the access token lives in the Flask session and never reaches the browser, so no Entra app registration changes were needed. The route modules now return JSON under /api/ui/*, and Flask serves the built bundle for every non-API path so bookmarked deep links and hard refreshes still resolve. Flask-WTF CSRF protection is preserved; the client reads the token from /api/ui/session and returns it as an X-CSRFToken header. All 14 pages are ported. The 37 hand-authored inline SVG icons carry over, so the portal still has no icon-font dependency. Fonts are the system stack and every asset is bundled locally, which keeps the no-CDN requirement for Azure Government, sovereign and air-gapped clouds intact at runtime. Deleted: 20 Jinja templates, the vendored Bootstrap 5.3 distribution, static/css/app.css, static/js/app.js, and route_user.py, whose only job was rendering the profile page from session claims. Behaviour changes worth knowing: - History filters move from the Flask session into the URL. A filtered view is now bookmarkable and shareable, two browser tabs no longer clobber each other's criteria, and three near-identical POST-redirect-GET blocks are gone. The client and the BFF clamp page and per_page identically. - Flash messages become toasts in an aria-live region. - No-JavaScript support is lost. The old portal degraded to working HTML forms; a single-page app cannot. Fixes found while testing the rewrite: - NavLink overrode the explicit aria-current, so Scaling Management was not highlighted on /scaling/log. This is the same bug the scaling_endpoints list in base.html existed to prevent, so the nav now does its own section matching with a plain Link. - The confirm dialog's focus trap filtered candidates on offsetParent, which collapsed the list to a single element and stopped Tab wrapping. - .dockerignore patterns only matched at the build context root, so a nested node_modules or .venv would have been copied into the image. flask_session is now excluded too, because those files hold live access tokens. - /api/ui/session reported authenticated: true with an all-null user when session['user'] was not a mapping. Build and test: - front_end/Dockerfile is multi-stage. A node:22-alpine stage runs npm ci and npm run build, and only the compiled bundle is copied into the Python image. Nothing generated is committed; package-lock.json is, so npm ci is reproducible. Note that building the image now requires npm registry access, which is documented in deploy/DEPLOYMENT.md. - The pytest suite is rewritten against the JSON contract, and a Vitest and React Testing Library suite covers the client. Both run in CI. Everything the previous suite protected is still covered, on whichever side now owns it. App.test.tsx mounts the real app against a stubbed BFF and walks every authenticated route. pytest: 134 passed. Vitest: 103 passed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
|
||
|
|
||
| def json_error(message, status=502, **extra): | ||
| return jsonify({"error": message, **extra}), status |
The three tests that assert Flask serves the built bundle read static/dist/index.html, which is a build artifact. They passed locally only because a build happened to be present, and failed in the Python CI job, which has no Node toolchain and never produces one. Add a spa_bundle fixture that supplies a stand-in shell when no build exists, and leaves a real build untouched. These tests are about Flask's routing and headers rather than the bundle's contents, so they now state that dependency instead of inheriting it, and the Python suite stays free of Node. Also covers the branch this exposed: a missing bundle returns a 500 that names the command to run, which was previously untested. pytest: 135 passed, both with and without a build present. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the Flask/Jinja/Bootstrap portal with a React 18 + TypeScript single-page app built with Vite and Tailwind CSS v4, using a custom glassmorphism design system.
Architecture
Flask stays, but as a backend-for-frontend.
flowchart LR B[Browser<br/>React SPA] -->|session cookie<br/>+ X-CSRFToken| F[Flask BFF] F -->|/login /getAToken /logout| E[Entra ID] F -->|Bearer token from<br/>server-side session| A[Broker API] F -->|static/dist| Broute_*.pymodules now return JSON under/api/ui/*. Flask serves the built bundle for every non-API path, so bookmarked deep links and hard refreshes still resolve./api/ui/sessionand returns it as anX-CSRFTokenheader, whichFlask-WTFalready accepts.Client routes mirror the URLs the Jinja portal served, so existing bookmarks and runbook links keep working.
Scope
front_end/web/— 70 files. React Router + TanStack Query, all 14 pages, 37 inline SVG icons, design system in one CSS file.app.py, all route modules,function_authentication.py,function_api.py,Dockerfile, the pytest suite,front_end/README.md.app.css,app.js,route_user.py.154 files changed: +13,197 / −63,782.
Behaviour changes worth reviewing
page/per_pageidentically so they can't disagree.aria-liveregion.Bugs found and fixed while testing the rewrite
NavLinkoverrode the explicitaria-current, so "Scaling Management" was not highlighted on/scaling/log. This is the same bug thescaling_endpointslist inbase.htmlexisted to prevent, so the nav now does its own section matching with a plainLink.offsetParent, collapsing the list to one element and stopping Tab from wrapping..dockerignorepatterns only matched at the build context root, so a nestednode_modulesor.venvwould have been copied into the image.flask_sessionis now excluded too, because those files hold live access tokens./api/ui/sessionreportedauthenticated: truewith an all-null user whensession['user']was not a mapping.Accessibility
Glassmorphism is easy to make unreadable, so these are enforced rather than assumed:
prefers-reduced-transparencyandprefers-reduced-motionfall back to solid surfaces, and there is a matching@supports not (backdrop-filter: ...)fallback.Badge.test.tsx.:focus-visiblering, and the pre-paint theme script are all retained.No-CDN policy
Still intact at runtime: everything is bundled locally, fonts are the system stack, and icons are inline SVG. Audited the built output — the only external strings are
data:URIs and library license banners.front_end/Dockerfileis now multi-stage: anode:22-alpinestage runsnpm ci && npm run build, and only the compiled bundle is copied into the Python image. That meansaz acr buildnow needs npm registry access. Documented indeploy/DEPLOYMENT.mdwith internal-mirror guidance. Nothing generated is committed;package-lock.jsonis, sonpm ciis reproducible and the dependency set is reviewable.Testing
Everything the previous suite protected is still covered, on whichever side now owns it.
tsc --buildnpm run buildYYYY-MM-DD→MM/DD/YYYY), ignore-filter semantics, pagination parameters, the legacy bare-list fallback, dashboard summary preference and degradation, CSRF, and the error-status mapping.App.test.tsxmounts the real app against a stubbed BFF and walks every authenticated route, so a page that throws on mount or misuses a hook fails in CI rather than in a browser.CI gains a Node job (install, typecheck, Vitest, production build) alongside the existing pytest job. The Python job deliberately needs no Node toolchain: a
spa_bundlefixture supplies a stand-in shell for the tests that assert Flask serves the bundle, so they state that dependency rather than inheriting it from whether someone happened to run a build. The second commit fixes exactly that — the first push failed CI for this reason, which also surfaced an untested branch: a missing bundle now returns a 500 naming the command to run.Not touched
api/,deploy/bicep/, app registrations, redirect URIs, scopes, and the database schema.