Skip to content

View as project home#8735

Merged
ericokuma merged 3 commits intomainfrom
cursor/PM-103-view-as-project-home-c194
Feb 25, 2026
Merged

View as project home#8735
ericokuma merged 3 commits intomainfrom
cursor/PM-103-view-as-project-home-c194

Conversation

@ericokuma
Copy link
Contributor

@ericokuma ericokuma commented Jan 30, 2026

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:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

Linear Issue: PM-103

Open in Cursor Open in Web


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 viewAsUserStore to track { user, sourceProject, isOrgLevel }, adds clearViewAsUser/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 new ViewAsUserOrgPopover that lists org members.

Written by Cursor Bugbot for commit 02243ff. This will update automatically on new commits. Configure here.

- 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
Copy link

cursor bot commented Jan 30, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@ericokuma ericokuma marked this pull request as ready for review February 13, 2026 20:55
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

Copy link
Contributor

@ericpgreen2 ericpgreen2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The store is a single writable<V1User | null> — 4 lines of code
  2. [project]/+layout.svelte calls GetDeploymentCredentials({ userId: mockedUserId }) — the server computes the correct runtime credentials for the impersonated user
  3. Those credentials flow into RuntimeProvider, and all data queries execute under the impersonated user's security policies, enforced server-side
  4. 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>
@cursor cursor bot force-pushed the cursor/PM-103-view-as-project-home-c194 branch from e680498 to b043ed8 Compare February 18, 2026 18:45
ericpgreen2

This comment was marked as duplicate.

Copy link
Contributor

@ericpgreen2 ericpgreen2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +10
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +84 to +92
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +149 to +159
$: 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,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Contributor

@ericpgreen2 ericpgreen2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three items from the previous review addressed cleanly:

  1. TanStack Query dedupeffectivePermissionsStore.ts deleted. TopNavigationBar now creates its own query observers that match the project layout's query keys, getting instant cache hits. Follows the ProjectAccessControls pattern.

  2. Navigation guard moved to project layoutonNavigate + onDestroy in +layout.svelte covers all project pages. Dashboard page no longer touches the store.

  3. sessionStorage persistence — Clean custom store API with initForProject/set/clear. The set(null) vs clear() distinction is well-designed: set(null) removes the value but keeps the project scope (used by ViewAsUserChip), while clear() 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 (GetDeploymentCredentialsGetProjectWithBearerToken → 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 };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

@ericokuma ericokuma merged commit 5e80f00 into main Feb 25, 2026
11 checks passed
@ericokuma ericokuma deleted the cursor/PM-103-view-as-project-home-c194 branch February 25, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants