diff --git a/web-admin/src/features/authentication/AvatarButton.svelte b/web-admin/src/features/authentication/AvatarButton.svelte index 9eb678d06e0..cb1b2f2b155 100644 --- a/web-admin/src/features/authentication/AvatarButton.svelte +++ b/web-admin/src/features/authentication/AvatarButton.svelte @@ -42,7 +42,7 @@ /> - {#if params.organization && params.project && params.dashboard} + {#if params.organization && params.project} - - Alerts - - - Reports - + {#if params.dashboard} + + Alerts + + + Reports + + {/if} {/if} diff --git a/web-admin/src/features/navigation/TopNavigationBar.svelte b/web-admin/src/features/navigation/TopNavigationBar.svelte index 2ad06decdcc..c5d44e601c1 100644 --- a/web-admin/src/features/navigation/TopNavigationBar.svelte +++ b/web-admin/src/features/navigation/TopNavigationBar.svelte @@ -4,6 +4,7 @@ import ExploreBookmarks from "@rilldata/web-admin/features/bookmarks/ExploreBookmarks.svelte"; import ShareDashboardPopover from "@rilldata/web-admin/features/dashboards/share/ShareDashboardPopover.svelte"; import ShareProjectPopover from "@rilldata/web-admin/features/projects/user-management/ShareProjectPopover.svelte"; + import { createAdminServiceGetProjectWithBearerToken } from "@rilldata/web-admin/features/public-urls/get-project-with-bearer-token"; import Rill from "@rilldata/web-common/components/icons/Rill.svelte"; import Breadcrumbs from "@rilldata/web-common/components/navigation/breadcrumbs/Breadcrumbs.svelte"; import type { PathOption } from "@rilldata/web-common/components/navigation/breadcrumbs/types"; @@ -15,6 +16,7 @@ import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; import { createAdminServiceGetCurrentUser, + createAdminServiceGetDeploymentCredentials, createAdminServiceListOrganizations as listOrgs, createAdminServiceListProjectsForOrganization as listProjects, type V1Organization, @@ -69,6 +71,43 @@ $: onPublicURLPage = isPublicURLPage($page); $: onOrgPage = isOrganizationPage($page); + // When "View As" is active, fetch deployment credentials for the mocked user. + // TanStack Query deduplicates by query key, so if the project layout already + // ran this query, we get instant cache hits with zero extra network calls. + $: 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, + }, + }, + ); + + // Use effective permissions when "View As" is active (from server) + // Otherwise fall back to the props passed from the root layout + $: effectiveManageProjectMembers = + $mockedProjectQuery.data?.projectPermissions?.manageProjectMembers ?? + manageProjectMembers; + $: effectiveCreateMagicAuthTokens = + $mockedProjectQuery.data?.projectPermissions?.createMagicAuthTokens ?? + createMagicAuthTokens; + $: loggedIn = !!$user.data?.user; $: rillLogoHref = !loggedIn ? "https://www.rilldata.com" : "/"; $: logoUrl = organizationLogoUrl; @@ -231,7 +270,7 @@ {/if} - {#if onProjectPage && manageProjectMembers} + {#if onProjectPage && effectiveManageProjectMembers} {/if} - + {/if} {/key} diff --git a/web-admin/src/features/view-as-user/viewAsUserStore.ts b/web-admin/src/features/view-as-user/viewAsUserStore.ts index a2f9a0f0fa4..6763bcd0dd4 100644 --- a/web-admin/src/features/view-as-user/viewAsUserStore.ts +++ b/web-admin/src/features/view-as-user/viewAsUserStore.ts @@ -1,4 +1,87 @@ -import { writable } from "svelte/store"; +import { writable, get } from "svelte/store"; import type { V1User } from "../../client"; +import { browser } from "$app/environment"; -export const viewAsUserStore = writable(null); +const STORAGE_KEY_PREFIX = "rill:viewAsUser:"; + +function getStorageKey(org: string, project: string): string { + return `${STORAGE_KEY_PREFIX}${org}/${project}`; +} + +function createViewAsUserStore() { + const store = writable(null); + let currentScope: { org: string; project: string } | null = null; + + return { + subscribe: store.subscribe, + + /** + * Initialize the store for a specific project scope. + * Loads persisted state from sessionStorage if available. + */ + initForProject(org: string, project: string): void { + if (!browser) return; + + currentScope = { org, project }; + const key = getStorageKey(org, project); + + try { + const stored = sessionStorage.getItem(key); + if (stored) { + const user = JSON.parse(stored) as V1User; + store.set(user); + } else { + store.set(null); + } + } catch { + store.set(null); + } + }, + + /** + * Set the view-as user for the current project scope. + */ + set(user: V1User | null): void { + store.set(user); + + if (!browser || !currentScope) return; + + const key = getStorageKey(currentScope.org, currentScope.project); + try { + if (user) { + sessionStorage.setItem(key, JSON.stringify(user)); + } else { + sessionStorage.removeItem(key); + } + } catch { + // Ignore storage errors + } + }, + + /** + * Clear the view-as state (e.g., when navigating away from project). + */ + clear(): void { + store.set(null); + + if (!browser || !currentScope) return; + + const key = getStorageKey(currentScope.org, currentScope.project); + try { + sessionStorage.removeItem(key); + } catch { + // Ignore storage errors + } + currentScope = null; + }, + + /** + * Get current value synchronously. + */ + get(): V1User | null { + return get(store); + }, + }; +} + +export const viewAsUserStore = createViewAsUserStore(); diff --git a/web-admin/src/routes/[organization]/[project]/+layout.svelte b/web-admin/src/routes/[organization]/[project]/+layout.svelte index 13be2cdfa1a..c983cb6b533 100644 --- a/web-admin/src/routes/[organization]/[project]/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/+layout.svelte @@ -27,6 +27,7 @@