feat(frontend): rbac token notice for the engine#5028
Conversation
PR #5028 Review — feat(frontend): rbac token notice for the engineOverviewThis PR does two related things:
What's Good
Issues and Suggestions1. Mixed header casing in All other headers in the same object use title-case ( 2. Undocumented new headers in the replay request
3. In 4. Previously the indicator sat alongside the link in a flex wrapper. Now it is a child of the link element, so clicks on the indicator navigate to the Settings route. This may be intentional, but if the indicator is supposed to open a separate popover, nesting it inside the link may swallow the click. 5. Empty-string fallback in
Minor
Overall this is a clean, well-scoped change. The flag rename is mechanical and consistent, and the Enterprise RBAC notice fills a real gap in the UX. The header casing and undocumented replay headers are the only items worth a follow-up look. |
2e537d4 to
b166ca6
Compare
331cbe7 to
da858a7
Compare
b166ca6 to
f3897b9
Compare
There was a problem hiding this comment.
Code Review: feat(frontend): rbac token notice for the engine
Summary of changes: (1) Renames multitenancy to platform across the frontend with a backward-compatible alias. (2) Adds a new acl feature flag for enterprise deployments that have ACL but no cloud platform. (3) Adds a PublishableTokenNotice UI component with a Ladle story. (4) Updates the engine credentials dialog with enterprise-specific copy. (5) Fixes useEngineToken to use the query system instead of raw localStorage.
Issues
Stale import in accessors.ts
useEngineNamespaceDataProvider is still imported on line 7 of frontend/src/queries/accessors.ts but is no longer used anywhere in that file — the block that called useEngineNamespaceDataProvider().engineAdminTokenQueryOptions() was removed in this PR. This will produce a lint/type-check warning and should be removed.
// Remove this import — nothing in accessors.ts calls it after this PR
import {
useCloudNamespaceDataProvider,
useEngineCompatDataProvider,
useEngineNamespaceDataProvider, // <-- unused
} from "@/components/actors";Duplicated isEnterprise computation
const isEnterprise = features.acl && !features.platform is computed identically in two separate files:
frontend/src/app/dialogs/provide-engine-credentials-frame.tsx(line 16)frontend/src/app/forms/engine-credentials-form.tsx(line 14)
These two files are closely coupled (the dialog renders the form), so this is low-risk today, but duplicated feature-flag predicates tend to drift. Consider either exporting a named flag from features.ts directly, or re-exporting the constant from engine-credentials-form.tsx into the dialog that uses it. The needsManualPublishableToken constant in env-variables.tsx (which is the same expression under a different name) adds a third variant. A single exported features.enterpriseAcl or similar would make the intent clear and eliminate the surface area.
Observations (non-blocking)
acl defaults to on in a full cloud build
In features.ts, isEnabled returns true when no env var is set (enabled === null). Because const acl = isEnabled("acl") || platform, features.acl is always true in a full cloud (no-flag) build. That evaluates needsManualPublishableToken = features.acl && !features.platform to false in the cloud path (since platform is also true there), which is the correct behavior. However, an operator who sets VITE_FEATURE_FLAGS=auth explicitly gets acl=false unless they also include acl in the flag list, which would hide the publishable-token notice from an ACL-enabled self-hosted build. If that scenario is expected, document acl alongside the other recognized flag strings.
Changelog link rel inconsistency
The changelog <a> links in layout.tsx (lines 301 and 335) use rel="noopener" without noreferrer. All other external links in the same file use rel="noopener noreferrer". This is a pre-existing inconsistency, but the reformatting in this PR makes it more visible; it would be low effort to fix in the same change.
useEngineToken race window in non-platform path
The old path used useLocalStorage, which reads synchronously. The new path uses useQuery(useEngineCompatDataProvider().engineAdminTokenQueryOptions()), which returns undefined until the query resolves. The function returns data || "", so the caller gets an empty string while loading. This matches the previous behavior when the key was absent, and the downstream useActorEngineContext memo passes the value through unconditionally, so there is a brief window on first render where an empty token may be used. Worth a quick check that the actor engine context guards against connecting with an empty token before the query settles.
Positive notes
- The
platformalias formultitenancy(isEnabled("platform") || isEnabled("multitenancy")) is a clean backward-compatible approach for a rename without a flag-day migration. - Moving
useEngineTokenoffuseLocalStorageand onto the query system is the right direction; it makes the token lifecycle consistent with the rest of the credential queries. - The Ladle story covers all three meaningful configurations (plain OSS, cloud, enterprise ACL) plus an isolated notice story, which matches the coverage guidance in
frontend/CLAUDE.md. - The
PublishableTokenNoticeplaceholder<PUBLISHABLE_TOKEN>in the DSN value is a sensible degraded-state UX; the notice makes the manual step explicit rather than silently showing a broken URL.
f3897b9 to
1fbf3fd
Compare

Description
Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
Type of change
How Has This Been Tested?
Please describe the tests that you ran to verify your changes.
Checklist: