feat(admin-panel): integrated admin panel — WIP (docs + v0 core)#4145
feat(admin-panel): integrated admin panel — WIP (docs + v0 core)#4145KvizadSaderah wants to merge 35 commits into
Conversation
|
Warning Review limit reached
Next review available in: 35 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughAdds an embedded React/Refine admin panel served at ChangesIntegrated admin panel
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
232ffab to
b0280c0
Compare
| ## Considered Options | ||
|
|
||
| - **Option A**: Embed the panel in the gears-rust monorepo; build the SPA into static assets served by the example server under `/cf/admin`. | ||
| - **Option B**: Develop the panel in a separate `constructorfabric/` repository; run it as a sidecar via a `make admin` target. |
There was a problem hiding this comment.
Did you consider an idea to store all the static files in a dedicated constructorfabric/repo and have somewhere a pre-built version that can parse a config or openapi spec and be loaded by simple Rust app and make example from somewhere?
I'm not sure it's good to have so much *.ts code in apps/admin-panel, because then we would need to have similar code in other projects, like imagine I have another gears repo project and I want to enable admin panel there as well
For example in Django, you do not need to copy the code of admin panel to every repo but yet you can easily run it for any django project
There was a problem hiding this comment.
Update after digging into what the aggregated spec can actually express:
Most of what I thought needed a new mechanism is already discoverable from /cf/openapi.json — field types/required/readOnly (component schemas), custom actions (e.g. POST …/suspend and …/unsuspend are real operations), and tenant-scope (the {tenant_id} path parameter). The panel now derives those directly; 8 of 9 resources already source their fields from the spec.
What's genuinely left is pure presentation — list columns, labels, grouping, confirm/safety level, an action's button label. That's a UX concern, not the API contract, so pushing it into OpenAPI vendor extensions would leak presentation into the API layer. So the plan is a split:
- OpenAPI → everything API-intrinsic (fields, operations, tenant-scope).
- A small panel-side config → presentation overrides only — Django's
admin.py-style registration, not copied panel code.
This makes the SPA generic with no per-project TS and no framework changes — I'm dropping the hardcoded registry.ts. The x-cf-admin-* extension idea (option b) stays available as a later optimization if something genuinely API-intrinsic turns up, but it isn't needed for this.
The one thing I'd value your steer on is distribution: keep the pre-built panel + a thin Rust loader (mount_admin_spa() already exists) and extract the SPA source to a dedicated constructorfabric/ repo as a follow-up — does that match what you had in mind?
…e panel Reviewer (constructorfabric#4145 r3495062634) reopened ADR-0001's placement decision: embedding the SPA's TypeScript under apps/admin-panel forces every gears-rust project to copy it. Agreed target is a generic, reusable panel driven at runtime by /cf/openapi.json + gear-emitted metadata, shipped pre-built with zero per-project TS, eventually extracted to a dedicated constructorfabric/ repo. Add dated revision notes (2026-06-30) to ADR-0001 (placement), ADR-0003 (discovery promoted from deferred to v1), and DESIGN.md. Original Option A analysis retained as the v0 record. Metadata transport (config / x-cf-admin-* extensions / descriptor endpoint) left open pending reviewer. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Add the architecture documentation set for the integrated admin panel under docs/arch/admin-panel/, following the studio SDLC templates: - PRD.md: requirements, actors, scope, FR/NFR (v0=p1), interfaces - DESIGN.md: architecture, component model, admin-context contract, topology - ADR/0001: embed in monorepo, serve SPA from example server at /cf/admin - ADR/0002: React + Refine + Vite + Ant Design frontend - ADR/0003: OpenAPI-first discovery + hardcoded v0 resource registry - ADR/0004: static two-role stub + dedicated admin-context endpoint Part of constructorfabric#4144. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Add config/admin.yaml and a `make admin` target to bring up the example server for admin-panel development. The config enables bearer-token auth and runs static-authn-plugin in static_tokens mode with two NON-PRODUCTION dev tokens: - platform-admin-dev-token -> subject_type=platform_admin (root tenant) - tenant-admin-dev-token -> subject_type=tenant_admin (engineering tenant) This realizes the v0 admin-role stub (cpt-admin-panel-fr-role-stub) without touching the security model: the role marker rides on subject_type, which the admin-context endpoint will read to select the admin mode. IdP is left unrequired so user management surfaces as unavailable per the PRD. Verified: GET /me returns the expected subject_type per token; missing or unknown tokens return 401. Part of constructorfabric#4144. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Add GET /account-management/v1/admin/context: a richer sibling of /me that projects the authenticated SecurityContext plus a derived admin mode and coarse capability hints for the admin panel's startup (cpt-admin-panel-fr-admin-context, cpt-admin-panel-interface-admin-context). Mode and capabilities derive from the subject_type role marker set by the non-production static auth stub (platform_admin -> platform, anything else -> least-privileged tenant). non_production_auth flags when the stub is in effect. The backend remains the final authority; capabilities are UI hints only. Enabled gears are intentionally omitted (the frontend reads them from the gear orchestrator), keeping this endpoint self-contained in AM. /me is unchanged. Pure context projection, no service or I/O. Adds AdminContextDto, the handler, route registration, and unit tests covering the platform, tenant, unknown, and absent role-marker cases. Verified: endpoint returns the expected mode/capabilities per token; missing or unknown tokens return 401. Part of constructorfabric#4144. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Add the admin-panel SPA under apps/admin-panel/ (React + Refine + Vite + Ant Design, TypeScript), realizing the v0 admin shell, current-context view, and capability-driven navigation (cpt-admin-panel-fr-admin-shell, cpt-admin-panel-fr-admin-context, cpt-admin-panel-fr-generated-screens). - authProvider: bearer-token login over the platform auth flow; the login screen offers the two non-production dev roles. - dataProvider: minimal Gears provider over /cf with a hardcoded v0 resource map (tenants, resource-groups, types, gears) and RFC-9457 error normalization; OpenAPI/registry-driven generation lands later (ADR-0003). - accessControlProvider: can() maps Refine actions to admin-context capability hints (UI gating only; backend stays authoritative). - ContextView: principal, tenant, admin mode, capabilities, and the enabled gears summary (from the gear orchestrator), with a non-production banner. - TenantList + GenericList: list screens that degrade gracefully on backend errors. Dev: `npm run dev` serves the SPA at :5173 and proxies /cf to the example server (`make admin`, :8087). Production base is /cf/admin/ for serving from the example server. Verified: build passes; login fetches admin context; platform/tenant roles resolve the expected mode and capabilities. Part of constructorfabric#4144. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
The admin config could not seed the AM platform root: the bootstrap saga failed first on an unregistered tenant type, then on the noop IdP's missing provision_tenant, leaving the tenants table empty (every tenant view 404'd). Fix the admin.yaml stack: - Register the platform and customer tenant types under types-registry.config.entities so the AM tenant-type checker validates the bootstrap root type. - Enable the static-idp-plugin (feature static-idp) and set idp.required so the bootstrap saga's provision_tenant step succeeds and the root persists. - Home the tenant-admin dev token at the seeded platform root (AM assigns child tenant ids, so a child id is not stable across a fresh db); the platform-vs-tenant distinction is still shown via admin_mode/capabilities. Verified: bootstrap saga completes, the root is seeded, and both dev roles get 200 from the tenant children endpoint. Part of constructorfabric#4144. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Replace the read-only v0 data provider with a metadata-driven resource registry (apps/admin-panel/src/resources): each manageable object is described once — paths, fields (with per-view visibility and create-time immutability), capabilities, safety level, tenant scope, and custom actions. The data provider and the generated List/Show/Create/Edit screens are driven entirely off these descriptors, so new resources are added by appending a descriptor rather than editing the core app. Covers full CRUD for tenants and resource-groups, read-only types/gears, and custom actions: tenant suspend/unsuspend/soft-delete and conversion approve/reject/cancel — each capability-gated in the UI and confirmed for destructive operations, with the backend remaining the final authority. Verified live against config/admin.yaml: tenant create/patch/suspend/ unsuspend/delete and the list endpoints for every resource. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
ADR-0003 now notes what shipped (curated descriptor registry + CRUD/ actions) vs what remains deferred (runtime OpenAPI field derivation, gear-contributed descriptors). Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Add testing/e2e/gears/admin covering GET /account-management/v1/admin/ context: 401 when unauthenticated, the default (untyped) token projecting least-privileged tenant mode, and the platform_admin / tenant_admin role markers projecting platform vs tenant mode with the expected capability sets. Adds two subject_type-typed static tokens to config/e2e-local.yaml to drive the role projection (homed at the platform root). Verified locally: make e2e-local E2E_TARGET=testing/e2e/gears/admin/ -> passed. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Add docs/web-docs/guides/admin-panel.md (two admin modes, the admin- context endpoint, running it locally, and the descriptor-driven resource model) plus its guides-index entry, and a README 'Admin panel' section pointing at make admin and the design set. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Add a tenant Users resource (list/create/delete via the account- management IdP passthrough; no get-one/update on the API, so the descriptor advertises only those verbs and the UI degrades gracefully). Add users:read/users:write capability hints to the platform and tenant admin-context projections so the screen is capability-gated, and extend the admin-context e2e to assert them. Verified live: list/create/delete against config/admin.yaml; e2e passes. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Add a Tenant metadata resource (list/read/upsert/delete) for the home tenant. Metadata entries are keyed by their GTS type_id and upserted via PUT with the bare value as the body, so the descriptor model gains two generic hooks: createKeyField (create is an upsert to the entry path with the id taken from a form field) and bodyField (send only that field's value as a transparent payload). No new per-resource provider code. Verified live against config/admin.yaml: register schema -> PUT value -> list -> get-one -> delete (204). Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Add a local Playwright smoke (apps/admin-panel/tests/smoke.spec.ts) that signs in as the platform-admin dev role and asserts the descriptor-driven console renders end-to-end: the admin-context view (mode, capabilities, non-production banner), navigation to every registered resource's list screen, and the generated tenant create form. Requires the Gears backend on :8087 (make admin); Playwright starts the Vite dev server itself. Not wired into the Rust CI gate (needs a browser + running backend). Verified: npx playwright test -> 1 passed. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
The tenant_type is a GTS chain (the API rejects anything not starting with gts.), but the create form offered a bare text field with no default, so a plain word like "user" failed with a 400. Pre-fill the form with the registered customer tenant-type id, and surface the real HTTP status on ApiError (statusCode) so failed-write notifications show the actual code instead of "undefined". Verified in-browser: create now succeeds and the row appears in the list. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Replace the flat tenant list with a hierarchy tree (the v0 "tenants
list/tree/detail" item): the home tenant is the root and each node's
children load lazily via /tenants/{id}/children on expand. Reuses the
tenants descriptor for columns, row actions, and navigation, and is wired
through a per-resource list-override hook (the ADR-0003 override escape
hatch) so other resources keep the generated list.
Verified in-browser: root -> child -> grandchild expand and load; row
actions (view/edit/delete/suspend) render per node.
Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Surface OAGW egress upstreams and routes as read-only resources (list + detail). Their bodies are deeply nested (endpoints, auth, headers, plugins, rate limits, CORS, match rules), so create/update is deferred to a dedicated config editor rather than a guessed flat form. Make accessControlProvider descriptor-driven: a verb is gated only when the descriptor declares a capability for it, so these capability-less read-only views are ungated while existing gated resources are unchanged. Add both to the Playwright smoke's navigation set. Verified live: /oagw/v1/upstreams and /routes return 200; smoke passes. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Replace the tenant create form's free-text Type field with a searchable select whose options are the registered tenant types (loaded from the types registry, filtered to the tenant_type GTS prefix). Adds a generic options hook to the field model + an AsyncSelect control, so any field can become a backend-backed select. Also auto-expand the tenant tree's root so children — including a just-created tenant — are visible immediately. Verified in-browser: Type shows [platform, customer], create succeeds and the new tenant appears under the expanded root. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Add an optional api-gateway config flag `admin_spa_dir`: when set, the
gateway serves the built admin-panel SPA (dist/) from that path at
`{prefix}/admin` (e.g. /cf/admin) via tower-http ServeDir.
The SPA is mounted on the top-level router, OUTSIDE the auth/middleware
stack, so the static assets are public (no bearer) while the SPA's own
API calls still hit the prefixed, authenticated routes. Unmatched paths
fall back to index.html with a 200 so client-side routes deep-link and
survive a refresh (`fallback`, not `not_found_service`, to avoid the
SetStatus(404) wrapper). The flag defaults to unset, so all other
deployments are unaffected.
`make admin` now builds the SPA first (npm install && npm run build) and
admin.yaml points `admin_spa_dir` at apps/admin-panel/dist, so the
server serves the panel without a separate vite dev process.
Signed-off-by: KvizadSaderah <kvizad@gmail.com>
make admin now builds and serves the SPA from the example server; explain the admin_spa_dir api-gateway config flag and that static assets are served outside the auth stack with an index.html fallback for client-side routes. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
…e panel Reviewer (constructorfabric#4145 r3495062634) reopened ADR-0001's placement decision: embedding the SPA's TypeScript under apps/admin-panel forces every gears-rust project to copy it. Agreed target is a generic, reusable panel driven at runtime by /cf/openapi.json + gear-emitted metadata, shipped pre-built with zero per-project TS, eventually extracted to a dedicated constructorfabric/ repo. Add dated revision notes (2026-06-30) to ADR-0001 (placement), ADR-0003 (discovery promoted from deferred to v1), and DESIGN.md. Original Option A analysis retained as the v0 record. Metadata transport (config / x-cf-admin-* extensions / descriptor endpoint) left open pending reviewer. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
ADR-0003 (revised): start shrinking the hand-curated registry by reading the gateway-aggregated /cf/openapi.json at boot and deriving each resource's field types, required, and readOnly from its component schema. The curated descriptor now carries only what OpenAPI can't express (visibility, labels, relations, the GTS type select, create-only, tag render override); derived facts fill the rest, and derived-only fields are appended. - resources/openapi.ts: pure schemaToFields mapper (utoipa shape: uuid/ date-time formats, ["T","null"] nullable, root required[], readOnly, $ref/object/array -> json), cached spec fetch, idempotent best-effort enrichRegistry run from the auth gate (login/check) before any render. - types.ts: optional `schema` anchor on ResourceDescriptor. - registry.ts: tenants -> TenantDto, resource-groups -> GroupDto; drop redundant per-field type/required/readOnly, keep presentation overrides. Verified against the live aggregated spec: TenantDto/GroupDto resolve and map correctly; typecheck + production build green. (Schema names are the runtime utoipa DTO names, e.g. GroupDto, not the stale per-gear doc names.) Signed-off-by: KvizadSaderah <kvizad@gmail.com>
The Types/GTS get-one endpoint keys on the GTS id; passing the internal
uuid 404s ("no entity with GTS ID: <uuid>") and React Query retries it in a
loop. Set idField to gts_id and url-encode it in the get-one path.
Signed-off-by: KvizadSaderah <kvizad@gmail.com>
mount_admin_spa fell back to index.html (200) for every unmatched path, including missing hashed /assets/*. A browser holding a stale cached index.html then requested a removed asset hash, got the HTML shell as a "module script", failed the MIME check, and rendered blank. Fix: 404 missing /assets/* instead of returning the shell; serve the shell with Cache-Control: no-cache; and route the entry-point directory request through the fallback (append_index_html_on_directories(false)) so even `/admin/` carries no-cache rather than being served straight from disk. The cached shell can no longer keep pointing at an asset hash a rebuild dropped. Hashed assets stay immutable and disk-streamed. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
…sources Roll the runtime OpenAPI field-discovery (introduced for tenants/resource- groups) onto the remaining resources whose list/detail DTO has a named component schema: conversions (OwnConversionRequestDto), users (UserDto), tenant-metadata (TenantMetadataEntryDto), types (GtsEntityDto), egress upstreams (UpstreamResponse) and routes (RouteResponse). Eight of nine resources now source field type/required/readOnly from the gateway-aggregated spec; only gears stays hand-curated (its list returns an unnamed Vec element with no component schema). Each descriptor keeps only the presentation the spec can't express (tag render, labels, list visibility, create-only, relations); pure schema- duplicate field entries are dropped and reappear via derivation. This is the genericity step the reviewer asked for: in another gears-rust project the field set comes from that project's own OpenAPI, with no hand-written field TS. Net registry LOC is flat (anchor comments offset the removed entries); the win is field truth moving to the API, not line count. Verified against the live aggregated /cf/openapi.json (all eight schema names resolve) and the merge result (every dropped field reappears, no orphaned curated field); typecheck + production build green. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
…-0003)
Re-examining what the aggregated /cf/openapi.json expresses closes the
previously-open metadata-transport question without a new backend mechanism.
Split discovery by the nature of each fact:
- API-intrinsic (field types/required/readOnly, CRUD verbs, custom actions
such as POST .../suspend, tenant-scope from the {tenant_id} path param) is
derived from OpenAPI at runtime.
- Presentation-only (list columns, labels, grouping, confirm/safety level,
action labels, irregular-list hints) lives in a small panel-side config —
Django admin.py-style registration, not copied panel code. Encoding it in
OpenAPI vendor extensions would leak presentation into the API contract.
Consequence: the SPA becomes fully generic with no per-project TypeScript and
no framework changes; the hardcoded registry.ts is dropped. The x-cf-admin-*
vendor-extension idea (Option-B transport) is retained only as a later
optimization, not a prerequisite. Distribution (pre-built artifact + thin
loader, later extracted to a dedicated repo) is tracked in ADR-0001.
The op-derivation algorithm was validated against the live aggregated spec for
all nine resources; the edge cases it surfaced (irregular tenant list via
/children, PATCH-status conversion actions, read-only types despite a POST
route, POST-only-leaf action detection) map exactly onto the config surface
above.
Signed-off-by: KvizadSaderah <kvizad@gmail.com>
… paths
Resource routes are API-intrinsic, so derive them from the aggregated
/cf/openapi.json instead of hand-writing a path builder per resource
(ADR-0003 concern-split). Each descriptor now declares a `basePath` (its
collection path in the spec) and the standard list/one/create/update/remove
paths are built at boot from the operations the spec actually exposes; the
API is the single source of truth for which verbs exist.
New: resources/openapiOps.ts — `deriveOps(spec, basePath)` (item path =
base/{param}; list/create on the collection, one/update/remove on the item;
custom actions detected as POST-only leaves), a `resolvePath` that fills the
trailing record id and any earlier tenant param from context, and
`buildPaths` applying list overrides + verb suppression.
Descriptors keep only what the spec can't express (presentation + policy):
- `listPath` for the irregular tenants list (home-tenant `/children`);
- `suppressVerbs` where policy offers less than the API (conversions edit
state via actions, not a generic form); a `read-only` safety level
suppresses writes automatically (types, egress upstreams/routes);
- custom actions (suspend/unsuspend, approve/reject/cancel) stay curated —
their label/safety/confirm/visibility are presentation, not API contract.
Registry resolution moved to a blocking bootstrap in main.tsx: the spec is
public, so it is fetched and the registry resolved (paths + fields) before the
App module evaluates its route/nav tables. dataProvider reads paths via a
`resourcePaths()` accessor; route/button visibility optional-chains on the
resolved paths.
No framework changes and no per-project TypeScript for the wiring — adding a
resource in another gears project is a descriptor with a basePath plus a few
presentation lines. registry.ts drops the hand-written path functions
(354->335 LOC, and the removed lines are the actual per-resource logic).
Verified live against config/admin.yaml on :8087 via the Playwright smoke:
login, context, all nine resource lists render through the derived paths
(including tenant-scoped /tenants/{home}/conversions|users|metadata, which
exercises the resolver's tenant substitution), and the tenant create form
renders with its async type select. tsc + production build green. Fixed a
stale smoke assertion (getByLabel on an async Select that carries no label
association) to assert the rendered combobox + loaded default.
Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Vite sets BASE_URL to "/cf/admin/" (trailing slash) and the router used it
verbatim as the BrowserRouter basename. react-router v6 does not match a
no-trailing-slash location ("/cf/admin") against a trailing-slash basename,
so hitting the panel at /cf/admin (the natural URL) rendered a blank page with
no console error; only /cf/admin/ worked. Strip the trailing slash from the
basename so both forms match.
Verified headless against the prod-served build on :8087: /cf/admin,
/cf/admin/, and a deep route (/cf/admin/tenants) all render and redirect to
login. (Note: the api-gateway caches the SPA shell in memory, so a server
restart is required to pick up a rebuilt dist — asset hashes change.)
Signed-off-by: KvizadSaderah <kvizad@gmail.com>
The shell declared no icon, so browsers issued the default /favicon.ico request, which resolves to the origin root (outside the /cf/admin base) and returns 404 — a spurious console error on every load. Add an inline SVG data-URI favicon so no separate request is made. Verified headless against the prod-served build: the panel loads at /cf/admin with zero responses >= 400. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Complete the concern-split (ADR-0003): per-resource registration is now DATA,
not TypeScript. src/resources/admin.config.json declares each resource; a
generic interpreter (configLoader.ts) turns it into the runtime descriptors,
building the few behavioral bits declaratively — path/list templates with
{tenant}/{id} placeholders, static-body actions with a visibleWhen predicate,
create-form defaults, and field option loaders referenced by name
(optionsSource -> a reusable loader, not per-resource code).
registry.ts drops from ~336 hand-written lines to ~46 (config import + build +
icons). The API-intrinsic parts (field types, CRUD verbs, tenant-scope) still
fill in at boot from the aggregated OpenAPI spec. Net: registering an admin
resource — here or in another gears-rust project — is a JSON edit with zero
TypeScript, which is the Django-admin-style genericity the reviewer asked for.
Behavior-preserving: descriptor shape and all screens are unchanged. Verified
against the prod build on :8087 — the Playwright smoke (login, context, all
nine config-driven resource lists via derived paths, tenant create form with
the async type select) passes; tsc + production build green.
Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Bring DESIGN.md and the web-docs guide in line with the implemented model: resources are declared in admin.config.json (data), API-intrinsic facts (fields, CRUD routes, tenant-scope) derive from the aggregated OpenAPI spec, and adding a resource is a JSON edit with no per-project TypeScript. DESIGN gets a 2026-07-02 revision note superseding the curated-in-app-registry wording; the web-docs 'add a resource' example is now JSON and the deferred list is updated (runtime discovery + JSON registration done; repo extraction, production role model, raw-DB fallback remain). Signed-off-by: KvizadSaderah <kvizad@gmail.com>
Re-add the /account-management/v1/admin/context path and AdminContextDto to the aggregated spec after rebasing onto upstream/main. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
3bfcdfc to
b5abeb5
Compare
Signed-off-by: KvizadSaderah <kvizad@gmail.com>
…on as follow-up The reviewer's reusability concern is met in place (declarative JSON registration + runtime OpenAPI derivation, zero per-project TS). Deliverable 4 delegates placement to this ADR, so decide it rather than block: v0 ships embedded, repo extraction to constructorfabric/ is tracked as a follow-up. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
|
@Artifizer rebased onto the latest On the reusability concern (your main point): resolved in place. Resource registration moved to a declarative On distribution: since deliverable 4 delegates placement to our ADRs, I've decided it rather than hold the PR — v0 ships embedded in the monorepo; extraction to a dedicated Moving the PR out of draft for review. |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (3)
apps/admin-panel/src/resources/openapiOps.ts (1)
138-141: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
listpath silently returns empty string when no list operation is derived.Unlike
one,create,update, andremove— which use theenabled()guard and returnundefinedwhen no template exists —listalways returns a function. Whent.listis undefined (no GET on the basePath in the OpenAPI spec) and noopts.listPathoverride is provided,resolvePath(t.list ?? "", ctx)returns"". The downstreamdataProvider.getListwould then callapiFetch(""), hitting an unintended endpoint.Consider making
listoptional inResourcePaths(consistent with the other verbs) and applying the sameenabled("list")guard, or throw a descriptive error at build time so misconfigurations fail fast.♻️ Suggested fix: make
listoptional and guard withenabled()In
types.ts:export interface ResourcePaths { - list: (ctx: AdminContext) => string; + list?: (ctx: AdminContext) => string; one?: (ctx: AdminContext, id: string) => string;In
openapiOps.ts:return { list: opts.listPath ? opts.listPath - : (ctx) => resolvePath(t.list ?? "", ctx), + : enabled("list") ? (ctx) => resolvePath(t.list!, ctx) : undefined, one: enabled("one") ? (ctx, id) => resolvePath(t.one!, ctx, id) : undefined,In
dataProvider.ts(add guard mirroring the other verbs):getList: async ({ resource }) => { const d = getDescriptor(resource); - const payload = await apiFetch<unknown>(resourcePaths(d).list(requireContext())); + const list = resourcePaths(d).list; + if (!list) unsupported("getList", resource); + const payload = await apiFetch<unknown>(list(requireContext()));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/admin-panel/src/resources/openapiOps.ts` around lines 138 - 141, Update the list path construction in openapiOps to use the same enabled("list") guard as one, create, update, and remove, returning undefined when neither opts.listPath nor the list template exists instead of resolving an empty path. Make ResourcePaths.list optional and add the corresponding guard in dataProvider wherever list paths are consumed, preserving the existing behavior when a list operation is configured.apps/admin-panel/src/pages/ResourceActions.tsx (1)
68-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer
onClickon theButtonover a wrapping clickable<span>.The non-destructive branch wraps a no-op
Buttonin a<span onClick>. It works via click bubbling, but attachingonClickdirectly to theButtonis more idiomatic and avoids the extra clickable wrapper.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/admin-panel/src/pages/ResourceActions.tsx` around lines 68 - 72, Update the non-destructive branch in ResourceActions to attach the existing run(a) handler directly to the Button component instead of wrapping it in a clickable span. Remove the wrapper while preserving the button rendering and key assignment behavior.apps/admin-panel/src/dataProvider.ts (1)
69-74: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
totalis the current page length; cursor pages won't paginate.
getListignores Refine's pagination/sort/filter params and returnstotal: data.length. For the{ items, page_info }cursor envelope this under-reports the real total, so Refine's pager treats the first response as the entire set and never advances. If v0 intentionally shows a single unpaginated page, consider disabling pagination on the consuminguseListto avoid a misleading pager; otherwise threadpage_infothrough.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/admin-panel/src/dataProvider.ts` around lines 69 - 74, The getList method currently reports the current page length as total and does not handle cursor pagination. Update getList and the related normalizeList/page_info flow to preserve cursor metadata and return the actual total or cursor-aware pagination information expected by Refine; if this provider intentionally remains single-page, disable pagination in its consuming useList instead of exposing a misleading pager.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/admin-panel/src/authProvider.ts`:
- Line 20: Replace the localStorage persistence in the auth token flow around
TOKEN_STORAGE_KEY with a non-readable session mechanism: prefer
backend-coordinated httpOnly, secure, appropriately scoped cookies, or use
in-memory access-token storage with refresh-token rotation if cookie support is
unavailable. Remove reads and writes that persist bearer tokens in localStorage
while preserving the existing authentication behavior.
In `@apps/admin-panel/src/pages/TenantTree.tsx`:
- Around line 21-39: Thread the dynamic idKey from idField(d) through
withPlaceholder, isPlaceholder, and injectChildren, using r[idKey] for all ID
comparisons and placeholder generation instead of hardcoded r.id. Update every
affected call site, including onExpand-related tree updates, and change the Ant
Table rowKey from "id" to idKey so non-default primary keys work consistently.
- Around line 83-93: Update the onExpand handler to wrap fetchChildren(id) and
the subsequent child injection in try-catch, following the error-handling
pattern used by load. On failure, call setError with the caught error and
prevent the rejection from escaping, while preserving the existing expansion and
successful child-loading behavior.
In `@apps/admin-panel/src/resources/admin.config.json`:
- Line 40: Update the tenant resource base paths used by deriveOps() to use the
OpenAPI placeholder name {tenant_id} instead of {tenant}, including conversions,
users, and metadata entries in admin.config.json. Preserve the existing path
structure so exact matching populates paths and buildPaths() produces the
correct list URLs.
In `@docs/arch/admin-panel/ADR/0002-cpt-admin-panel-adr-frontend-framework.md`:
- Line 60: The ADR’s Data description incorrectly claims a generated OpenAPI
client. Update the line describing the custom Refine data provider to reflect
that apps/admin-panel/src/dataProvider.ts and adminContext.ts call apiFetch
directly, with OpenAPI processing handled at runtime elsewhere.
In `@docs/arch/admin-panel/ADR/0003-cpt-admin-panel-adr-resource-discovery.md`:
- Around line 62-84: Synchronize both architecture documents with the
implemented discovery contracts: in
docs/arch/admin-panel/ADR/0003-cpt-admin-panel-adr-resource-discovery.md lines
62-84, replace the obsolete hardcoded-registry Option A outcome and deferred
runtime-OpenAPI status with the current runtime OpenAPI metadata plus
declarative admin.config.json architecture; in docs/arch/admin-panel/DESIGN.md
lines 37-41, describe admin.config.json and runtime OpenAPI as the current
discovery sources. Update related consequences or status statements in the ADR
so they no longer claim the registry is the active source or runtime parsing is
deferred.
In `@docs/arch/admin-panel/DESIGN.md`:
- Around line 353-370: Make the admin-context contract consistent across all
specified documents: in docs/arch/admin-panel/DESIGN.md lines 353-370, document
/account-management/v1/admin/context with the frontend’s actual flat
account-management DTO and describe enabled gears as retrieved separately; in
docs/arch/admin-panel/ADR/0004-cpt-admin-panel-adr-admin-role-and-context.md
lines 59-63, remove the claim that the context endpoint returns enabled gears;
in docs/arch/admin-panel/PRD.md lines 352-356, specify the separate
enabled-gears request.
In `@docs/arch/admin-panel/PRD.md`:
- Around line 576-584: Update the “Open Questions” section to remove or mark as
resolved the decisions already settled by the PR objectives and ADR revisions,
specifically implementation placement, admin-context ownership, discovery
strategy, and frontend-stack selection. Retain only genuinely pending questions,
including production admin-mode representation and authentication for
non-auth_disabled deployments.
In `@README.md`:
- Around line 100-116: Update the Admin panel guide link in the README to
reference docs/web-docs/build-with-gears/admin-panel.md instead of the outdated
path, leaving the surrounding admin panel documentation unchanged.
---
Nitpick comments:
In `@apps/admin-panel/src/dataProvider.ts`:
- Around line 69-74: The getList method currently reports the current page
length as total and does not handle cursor pagination. Update getList and the
related normalizeList/page_info flow to preserve cursor metadata and return the
actual total or cursor-aware pagination information expected by Refine; if this
provider intentionally remains single-page, disable pagination in its consuming
useList instead of exposing a misleading pager.
In `@apps/admin-panel/src/pages/ResourceActions.tsx`:
- Around line 68-72: Update the non-destructive branch in ResourceActions to
attach the existing run(a) handler directly to the Button component instead of
wrapping it in a clickable span. Remove the wrapper while preserving the button
rendering and key assignment behavior.
In `@apps/admin-panel/src/resources/openapiOps.ts`:
- Around line 138-141: Update the list path construction in openapiOps to use
the same enabled("list") guard as one, create, update, and remove, returning
undefined when neither opts.listPath nor the list template exists instead of
resolving an empty path. Make ResourcePaths.list optional and add the
corresponding guard in dataProvider wherever list paths are consumed, preserving
the existing behavior when a list operation is configured.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b89b313c-2d8a-4ad1-903d-883042c9f274
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lockapps/admin-panel/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (55)
Cargo.tomlMakefileREADME.mdapps/admin-panel/.gitignoreapps/admin-panel/index.htmlapps/admin-panel/package.jsonapps/admin-panel/playwright.config.tsapps/admin-panel/src/App.tsxapps/admin-panel/src/accessControlProvider.tsapps/admin-panel/src/adminContext.tsapps/admin-panel/src/authProvider.tsapps/admin-panel/src/config.tsapps/admin-panel/src/dataProvider.tsapps/admin-panel/src/httpClient.tsapps/admin-panel/src/main.tsxapps/admin-panel/src/pages/ContextView.tsxapps/admin-panel/src/pages/Login.tsxapps/admin-panel/src/pages/ResourceActions.tsxapps/admin-panel/src/pages/ResourceForm.tsxapps/admin-panel/src/pages/ResourceList.tsxapps/admin-panel/src/pages/ResourceShow.tsxapps/admin-panel/src/pages/TenantTree.tsxapps/admin-panel/src/resources/admin.config.jsonapps/admin-panel/src/resources/configLoader.tsapps/admin-panel/src/resources/fields.tsxapps/admin-panel/src/resources/index.tsapps/admin-panel/src/resources/openapi.tsapps/admin-panel/src/resources/openapiOps.tsapps/admin-panel/src/resources/registry.tsapps/admin-panel/src/resources/types.tsapps/admin-panel/src/vite-env.d.tsapps/admin-panel/tests/smoke.spec.tsapps/admin-panel/tsconfig.jsonapps/admin-panel/vite.config.tsconfig/admin.yamlconfig/e2e-local.yamldocs/api/api.jsondocs/arch/admin-panel/ADR/0001-cpt-admin-panel-adr-placement-and-delivery.mddocs/arch/admin-panel/ADR/0002-cpt-admin-panel-adr-frontend-framework.mddocs/arch/admin-panel/ADR/0003-cpt-admin-panel-adr-resource-discovery.mddocs/arch/admin-panel/ADR/0004-cpt-admin-panel-adr-admin-role-and-context.mddocs/arch/admin-panel/DESIGN.mddocs/arch/admin-panel/PRD.mddocs/web-docs/build-with-gears/admin-panel.mdgears/system/account-management/account-management/src/api/rest/dto.rsgears/system/account-management/account-management/src/api/rest/handlers/admin_context.rsgears/system/account-management/account-management/src/api/rest/handlers/admin_context_tests.rsgears/system/account-management/account-management/src/api/rest/handlers/mod.rsgears/system/account-management/account-management/src/api/rest/routes/admin_context.rsgears/system/account-management/account-management/src/api/rest/routes/mod.rsgears/system/api-gateway/src/config.rsgears/system/api-gateway/src/gear.rstesting/e2e/gears/admin/__init__.pytesting/e2e/gears/admin/conftest.pytesting/e2e/gears/admin/test_admin_context.py
| error: { name: "LoginError", message: "A bearer token is required" }, | ||
| }; | ||
| } | ||
| localStorage.setItem(TOKEN_STORAGE_KEY, token); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Bearer token stored in localStorage is vulnerable to XSS.
Static analysis flagged this as CWE-312. Any script executing on the page can read the token from localStorage, and it persists across sessions. While the PR scopes this to a non-production dev setup, the pattern will need to change before any production deployment. Consider migrating to httpOnly cookie-based session management (requires backend coordination) or an in-memory token with refresh-token rotation for production use.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/admin-panel/src/authProvider.ts` at line 20, Replace the localStorage
persistence in the auth token flow around TOKEN_STORAGE_KEY with a non-readable
session mechanism: prefer backend-coordinated httpOnly, secure, appropriately
scoped cookies, or use in-memory access-token storage with refresh-token
rotation if cookie support is unavailable. Remove reads and writes that persist
bearer tokens in localStorage while preserving the existing authentication
behavior.
Source: Linters/SAST tools
There was a problem hiding this comment.
Acknowledged, and intentional for v0. The bearer token here is a non-production dev-stub token from the static-auth plugin (the UI marks it as such with a demo banner). The design already defers the production auth model — when a real IdP/session flow replaces the stub, token storage should move to an httpOnly cookie (or in-memory + silent refresh) rather than localStorage. Tracking it as a hardening item for the production auth work, not a v0 blocker.
There was a problem hiding this comment.
@KvizadSaderah, that makes sense — thanks for the context. Given the non-production dev-stub token and the documented plan to move to httpOnly cookies or in-memory + refresh rotation when the real auth model lands, this is reasonable to defer for v0. I'll leave it as-is for now.
…esources resolve
deriveOps() matched a descriptor's basePath against the spec by exact string,
so tenant-scoped resources whose config basePath uses the panel's {tenant}
placeholder never lined up with the spec's {tenant_id} paths — leaving
conversions/users/tenant-metadata with empty derived routes. Collapse every
path parameter to a wildcard before matching (the returned templates are the
spec's real keys, so resolvePath still fills the real params).
Also surface a failed lazy child-fetch in the tenant tree instead of silently
swallowing the rejection.
Signed-off-by: KvizadSaderah <kvizad@gmail.com>
…ed contracts - admin-context is a flat AdminContextDto (subject, home tenant, mode, capabilities); enabled gears are read separately from the gear orchestrator, not returned by the endpoint (DESIGN, ADR-0004, PRD). - record the implemented discovery: declarative JSON config + runtime OpenAPI, no in-app registry (ADR-0003 revision); data provider uses a hand-written apiFetch, not a generated client (ADR-0002). - mark resolved open questions in the PRD; fix the moved web-docs guide link. Signed-off-by: KvizadSaderah <kvizad@gmail.com>
WIP / draft for #4144 — integrated admin panel. ~45% complete; opening early for visibility and feedback on the open questions below.
Done
docs/arch/admin-panel/: PRD, DESIGN, ADRs 0001–0004 (placement, frontend=Refine, OpenAPI-first discovery, admin role+context).config/admin.yaml+make admin(static_tokens:platform-admin-dev-token,tenant-admin-dev-token).GET /account-management/v1/admin/context— principal, tenant, admin mode, capabilities (derived fromsubject_type);/meunchanged. Unit-tested.apps/admin-panel/(React + Refine + Vite + Ant Design): login (2 dev roles), context view (principal/mode/capabilities + enabled gears + non-prod banner), tenants/resource-groups/gears list screens, RFC-9457 error normalization. Dev:npm run devproxies/cftomake admin.Not done yet
/cf/admin(dev uses Vite).make admin-seed).Notes
Open questions (for reviewers)
GET /tenantslist for platform admin.auth_disableddeployment.Part of #4144.
Summary by CodeRabbit
/cf/admin.