Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions web-admin/src/features/authentication/AvatarButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
/>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{#if params.organization && params.project && params.dashboard}
{#if params.organization && params.project}
<ProjectAccessControls
organization={params.organization}
project={params.project}
Expand Down Expand Up @@ -71,16 +71,18 @@
</DropdownMenu.Sub>
</svelte:fragment>
</ProjectAccessControls>
<DropdownMenu.Item
href={`/${params.organization}/${params.project}/-/alerts`}
>
Alerts
</DropdownMenu.Item>
<DropdownMenu.Item
href={`/${params.organization}/${params.project}/-/reports`}
>
Reports
</DropdownMenu.Item>
{#if params.dashboard}
<DropdownMenu.Item
href={`/${params.organization}/${params.project}/-/alerts`}
>
Alerts
</DropdownMenu.Item>
<DropdownMenu.Item
href={`/${params.organization}/${params.project}/-/reports`}
>
Reports
</DropdownMenu.Item>
{/if}
{/if}

<ThemeToggle />
Expand Down
16 changes: 14 additions & 2 deletions web-admin/src/features/navigation/TopNavigationBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
} from "../../client";
import ViewAsUserChip from "../../features/view-as-user/ViewAsUserChip.svelte";
import { viewAsUserStore } from "../../features/view-as-user/viewAsUserStore";
import { effectiveProjectPermissionsStore } from "../../features/view-as-user/effectivePermissionsStore";
import CreateAlert from "../alerts/CreateAlert.svelte";
import { useAlerts } from "../alerts/selectors";
import AvatarButton from "../authentication/AvatarButton.svelte";
Expand Down Expand Up @@ -69,6 +70,15 @@
$: onPublicURLPage = isPublicURLPage($page);
$: onOrgPage = isOrganizationPage($page);

// Use effective permissions when "View As" is active (from server)
// Otherwise fall back to the props passed from the root layout
$: effectiveManageProjectMembers =
$effectiveProjectPermissionsStore?.manageProjectMembers ??
manageProjectMembers;
$: effectiveCreateMagicAuthTokens =
$effectiveProjectPermissionsStore?.createMagicAuthTokens ??
createMagicAuthTokens;

$: loggedIn = !!$user.data?.user;
$: rillLogoHref = !loggedIn ? "https://www.rilldata.com" : "/";
$: logoUrl = organizationLogoUrl;
Expand Down Expand Up @@ -231,7 +241,7 @@
{/if}
<!-- NOTE: only project admin and editor can manage project members -->
<!-- https://docs.rilldata.com/guide/administration/users-and-access/roles-permissions -->
{#if onProjectPage && manageProjectMembers}
{#if onProjectPage && effectiveManageProjectMembers}
<ShareProjectPopover
{organization}
{project}
Expand Down Expand Up @@ -264,7 +274,9 @@
{#if $alertsFlag}
<CreateAlert />
{/if}
<ShareDashboardPopover {createMagicAuthTokens} />
<ShareDashboardPopover
createMagicAuthTokens={effectiveCreateMagicAuthTokens}
/>
{/if}
</StateManagersProvider>
{/key}
Expand Down
10 changes: 10 additions & 0 deletions web-admin/src/features/view-as-user/effectivePermissionsStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,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
Copy Markdown
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.

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

37 changes: 36 additions & 1 deletion web-admin/src/routes/[organization]/[project]/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import { createAdminServiceGetProjectWithBearerToken } from "@rilldata/web-admin/features/public-urls/get-project-with-bearer-token";
import { cloudVersion } from "@rilldata/web-admin/features/telemetry/initCloudMetrics";
import { viewAsUserStore } from "@rilldata/web-admin/features/view-as-user/viewAsUserStore";
import { effectiveProjectPermissionsStore } from "@rilldata/web-admin/features/view-as-user/effectivePermissionsStore";
import ErrorPage from "@rilldata/web-common/components/ErrorPage.svelte";
import { metricsService } from "@rilldata/web-common/metrics/initMetrics";
import RuntimeProvider from "@rilldata/web-common/runtime-client/RuntimeProvider.svelte";
Expand Down Expand Up @@ -122,7 +123,41 @@
$: ({ data: mockedUserDeploymentCredentials } =
$mockedUserDeploymentCredentialsQuery);

/**
* When "View As" is active, fetch the project using the mocked user's JWT.
* This returns the impersonated user's `projectPermissions` from the server.
*/
$: mockedUserProjectQuery = createAdminServiceGetProjectWithBearerToken(
organization,
project,
mockedUserDeploymentCredentials?.accessToken ?? "",
undefined,
{
query: {
enabled: !!mockedUserDeploymentCredentials?.accessToken,
},
},
);

$: ({ data: projectData, error: projectError } = $projectQuery);

/**
* Compute effective project permissions.
* When "View As" is active, use the impersonated user's permissions (from server).
* Otherwise, use the actual user's permissions.
*/
$: 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
Copy Markdown
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.

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


$: deploymentStatus = projectData?.deployment?.status;
// A re-deploy triggers `DEPLOYMENT_STATUS_UPDATING` status. But we can still show the project UI.
$: isProjectAvailable =
Expand Down Expand Up @@ -168,7 +203,7 @@

{#if onProjectPage && deploymentStatus === V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING}
<ProjectTabs
projectPermissions={projectData.projectPermissions}
projectPermissions={effectiveProjectPermissions}
{organization}
{pathname}
{project}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,15 @@
);

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
Copy Markdown
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.

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

}
Copy link
Copy Markdown

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

errorStore.reset();

const changedDashboard =
Expand Down
Loading