Conversation
- Modified AvatarButton.svelte to show 'View As' option on all project pages, not just dashboard pages (removed dashboard param requirement) - Updated navigation behavior to preserve 'View As' state when navigating within the same project, only clearing when switching projects - Alerts/Reports links in avatar menu now only show when on a dashboard page Resolves PM-103 Co-authored-by: eric.okuma <eric.okuma@rilldata.com>
|
Cursor Agent can help with this pull request. Just |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
This is the final PR Bugbot will review for you during this billing cycle
Your free Bugbot reviews will reset on March 4
Details
You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
| from.params.project !== to.params.project; | ||
| if (changedProject) { | ||
| clearViewAsUser(); | ||
| } |
There was a problem hiding this comment.
onNavigate clears org-level view-as on project change
High Severity
The onNavigate callback in the explore page calls clearViewAsUser() unconditionally when the project changes, without checking whether the view-as state has isOrgLevel: true. This contradicts the design in isViewAsValidForProject (which returns true for org-level) and the TopNavigationBar reactive block (which skips clearing when isOrgLevel is true). When an org admin navigates from one project's dashboard to another, the org-level view-as state is incorrectly wiped out.
Additional Locations (2)
ericpgreen2
left a comment
There was a problem hiding this comment.
The existing view-as architecture
The current "View As" implementation has an elegant design worth understanding before extending it. The key principle: the server does all the permission work.
When a user activates "View As" on a dashboard today:
- The store is a single
writable<V1User | null>— 4 lines of code [project]/+layout.sveltecallsGetDeploymentCredentials({ userId: mockedUserId })— the server computes the correct runtime credentials for the impersonated user- Those credentials flow into
RuntimeProvider, and all data queries execute under the impersonated user's security policies, enforced server-side - On navigation, the store resets
There is zero client-side permission logic. The server is the single source of truth.
Getting the impersonated user's permissions
The JWT returned by GetDeploymentCredentials can be used to call GetProject (via the existing GetProjectWithBearerToken pattern). This returns projectPermissions for the impersonated user — computed by the server, not reconstructed on the client. The UI is already correctly gated on projectPermissions, so this is all you need.
This means getViewAsUserPermissions.ts (the 206-line client-side role-to-permission mapping) shouldn't exist. It duplicates the server's permission model, will silently drift if roles or permissions change, and is unnecessary — the server already provides this answer through the existing API.
Scope: project-level only
GetDeploymentCredentials is project-scoped — it takes org and project. Org-level impersonation (persisting view-as across projects) would require backend capabilities that don't exist today. This PR should be scoped to project-level view-as only, without introducing org-level concepts (ViewAsUserOrgPopover, __org_level__ sentinel, isOrgLevel flag).
What this PR should focus on
The valuable change is making "View As" available from the avatar menu on all project pages (not just dashboards), and persisting the state when navigating within a project. That builds naturally on the existing architecture.
Use the existing GetProjectWithBearerToken pattern to fetch the impersonated user's projectPermissions from the server, rather than reconstructing permissions client-side. Changes: - Project layout calls GetProjectWithBearerToken with the mocked user's JWT (from GetDeploymentCredentials) to get their actual permissions - Created effectivePermissionsStore to share permissions with TopNavBar - ProjectTabs uses effectiveProjectPermissions (impersonated user's permissions when View As is active) - TopNavigationBar uses effective permissions for Share button visibility This follows the existing architecture where the server is the single source of truth for permissions, rather than duplicating permission logic on the client. Co-authored-by: ericokuma <ericokuma@users.noreply.github.com>
e680498 to
b043ed8
Compare
ericpgreen2
left a comment
There was a problem hiding this comment.
Nice work extending View As to all project pages — the server-side permissions approach (fetching the impersonated user's actual permissions via GetProjectWithBearerToken) is the right pattern.
A few things to address before merging:
1. effectiveProjectPermissionsStore — use TanStack Query dedup instead
The global store exists because TopNavigationBar lives in the root layout and can't receive data from the project layout (SvelteKit child-to-parent boundary). But we can solve this without a side-channel store by having TopNavigationBar create its own query observers for the impersonated user's permissions.
The project layout already runs GetDeploymentCredentials and GetProjectWithBearerToken — TanStack Query deduplicates by query key, so TopNavigationBar's observers get instant cache hits with zero extra network calls. This is the same pattern ProjectAccessControls already uses (it independently queries GetProject even though the layout does too).
In TopNavigationBar, this would look like:
$: mockedUserId = $viewAsUserStore?.id;
$: mockedCredentialsQuery = createAdminServiceGetDeploymentCredentials(
organization, project, { userId: mockedUserId },
{ query: { enabled: !!mockedUserId && !!organization && !!project } },
);
$: mockedProjectQuery = createAdminServiceGetProjectWithBearerToken(
organization, project,
$mockedCredentialsQuery.data?.accessToken ?? "",
undefined,
{ query: { enabled: !!$mockedCredentialsQuery.data?.accessToken } },
);
$: effectiveManageProjectMembers =
$mockedProjectQuery.data?.projectPermissions?.manageProjectMembers
?? manageProjectMembers;
$: effectiveCreateMagicAuthTokens =
$mockedProjectQuery.data?.projectPermissions?.createMagicAuthTokens
?? createMagicAuthTokens;Then delete effectivePermissionsStore.ts and remove the .set() calls from the project layout. This follows the frontend style guide principle: "Put Query Observers in the component that needs the data." The future ProjectProvider refactor (#8603) will simplify this further — these observers would be replaced with provider context reads.
2. View As state leaks across projects from the home page
The onNavigate guard that clears viewAsUserStore only lives in explore/[dashboard]/+page.svelte. If a user activates View As on the project home page and navigates to a different project, the dashboard page's guard is never mounted — state persists.
Fix: move the guard to the project layout ([organization]/[project]/+layout.svelte) so it covers all project pages, then remove the duplicate from the dashboard page. Also add onDestroy(() => viewAsUserStore.set(null)) so navigating away from all project pages (e.g. to an org page) clears the state.
3. Persist View As state to sessionStorage
Currently, a page refresh loses the impersonated user, which makes it hard to test across multiple pages. Consider persisting to sessionStorage scoped to {org}/{project}:
- Survives page refresh (fixes the UX papercut)
- Auto-clears when the tab closes (no "forgot I was impersonating" risk)
- Tab-scoped, so admins can compare two users in separate tabs
- No TTL management needed
| import { writable } from "svelte/store"; | ||
| import type { V1ProjectPermissions } from "../../client"; | ||
|
|
||
| /** | ||
| * Store for effective project permissions when "View As" is active. | ||
| * When null, the actual user's permissions should be used. | ||
| * When set, these are the impersonated user's permissions (from server). | ||
| */ | ||
| export const effectiveProjectPermissionsStore = | ||
| writable<V1ProjectPermissions | null>(null); |
There was a problem hiding this comment.
This store can be eliminated — see the top-level comment for the TanStack Query dedup approach. When TopNavigationBar creates its own query observers (matching the same query keys the project layout uses), TanStack gives it instant cache hits. No side-channel store needed. This also follows the existing ProjectAccessControls pattern, which independently queries GetProject.
| onNavigate(({ from, to }) => { | ||
| viewAsUserStore.set(null); | ||
| // Only clear "View As" state when navigating outside of the current project | ||
| const changedProject = | ||
| !from || | ||
| !to || | ||
| from.params.organization !== to.params.organization || | ||
| from.params.project !== to.params.project; | ||
| if (changedProject) { | ||
| viewAsUserStore.set(null); |
There was a problem hiding this comment.
This navigation guard should move to the project layout (+layout.svelte) so it covers all project pages — home, settings, status, etc. With View As now available on the home page, activating it there and navigating to a different project won't trigger this guard since the dashboard page is never mounted.
| $: effectiveProjectPermissions = | ||
| mockedUserId && $mockedUserProjectQuery.data?.projectPermissions | ||
| ? $mockedUserProjectQuery.data.projectPermissions | ||
| : projectData?.projectPermissions; | ||
|
|
||
| // Update the global store so TopNavigationBar can access effective permissions | ||
| $: effectiveProjectPermissionsStore.set( | ||
| mockedUserId && $mockedUserProjectQuery.data?.projectPermissions | ||
| ? $mockedUserProjectQuery.data.projectPermissions | ||
| : null, | ||
| ); |
There was a problem hiding this comment.
The condition mockedUserId && $mockedUserProjectQuery.data?.projectPermissions is duplicated between the local variable above and this store update. If you keep this pattern, compute once and reuse. But see the top-level comment for an approach that eliminates these lines entirely.
…o sessionStorage
- Remove effectiveProjectPermissionsStore and use TanStack Query
deduplication in TopNavigationBar instead (matching ProjectAccessControls pattern)
- Move onNavigate guard from dashboard page to project layout to cover all project pages
- Add onDestroy cleanup when unmounting project layout
- Persist View As state to sessionStorage scoped to {org}/{project} for
refresh survival without 'forgot I was impersonating' risk
Co-authored-by: ericokuma <ericokuma@users.noreply.github.com>
ericpgreen2
left a comment
There was a problem hiding this comment.
All three items from the previous review addressed cleanly:
-
TanStack Query dedup —
effectivePermissionsStore.tsdeleted. TopNavigationBar now creates its own query observers that match the project layout's query keys, getting instant cache hits. Follows theProjectAccessControlspattern. -
Navigation guard moved to project layout —
onNavigate+onDestroyin+layout.sveltecovers all project pages. Dashboard page no longer touches the store. -
sessionStorage persistence — Clean custom store API with
initForProject/set/clear. Theset(null)vsclear()distinction is well-designed:set(null)removes the value but keeps the project scope (used by ViewAsUserChip), whileclear()tears down the scope entirely (used when leaving the project).
Two suggestions for follow-up:
Consider extracting the View As query chain into a shared selector
The 3-query chain (GetDeploymentCredentials → GetProjectWithBearerToken → effective permissions) is now duplicated between the project layout and TopNavigationBar. A shared selector like useViewAsProjectPermissions(org, project) would deduplicate this, shave ~30 lines from the layout (which is now 265 lines — 1.75x the next-largest layout), and make future View As consumers easier to build. Not blocking, but worth considering.
Add e2e tests for View As
View As is a security-adjacent feature (impersonation + permission gating) with zero test coverage. The Playwright infrastructure is well-suited for this — adminPage fixtures, role-based patterns, and state persistence testing are all established (see bookmarks.spec.ts). Key scenarios:
- Admin sees View As UI on project home (not just dashboards)
- View As state persists across page refresh
- View As state clears when navigating to a different project
- Effective permissions update correctly (e.g., Share button hidden for viewer)
Note: the viewerPage fixture is currently a stub (playwright/.auth/viewer.json is never created in setup.ts). Wiring that up would be a prerequisite for permission-based assertions. Could be a separate PR.
| initForProject(org: string, project: string): void { | ||
| if (!browser) return; | ||
|
|
||
| currentScope = { org, project }; |
There was a problem hiding this comment.
Minor: initForProject is called from a reactive block that re-fires on any $page change (not just org/project changes). An early return when the scope hasn't changed would avoid redundant sessionStorage reads:
if (currentScope?.org === org && currentScope?.project === project) return;

Extends the "View As" functionality to all project-level pages, including Project Home (PM-103).
The "View As" option is now available from the avatar dropdown on any project page, not just dashboard-specific pages. The selected user state will persist when navigating between pages within the same project, providing a consistent impersonation experience. This leverages the existing project-level runtime credential switching infrastructure.
Checklist:
Linear Issue: PM-103
Note
Medium Risk
Touches global navigation/state for impersonation and changes when credentials are cleared/persisted across routes, which can cause confusing access context if edge cases are missed, but it’s limited to UI/state management with explicit validity checks.
Overview
Extends "View As" impersonation to be available from the avatar menu on any project page (not just dashboards) and makes the active state persist while navigating within the same project.
Refactors
viewAsUserStoreto track{ user, sourceProject, isOrgLevel }, addsclearViewAsUser/isViewAsValidForProject, and updates navigation/chip rendering to clear or hide invalid project-scoped impersonations; org admins can also start an org-level view-as from org pages via a newViewAsUserOrgPopoverthat lists org members.Written by Cursor Bugbot for commit 02243ff. This will update automatically on new commits. Configure here.