@@ -4,7 +4,14 @@ import {
44 ExclamationTriangleIcon ,
55} from "@heroicons/react/24/outline" ;
66import { EllipsisHorizontalIcon } from "@heroicons/react/20/solid" ;
7- import { useFetcher , useNavigation , useRevalidator , useSubmit } from "@remix-run/react" ;
7+ import {
8+ Form ,
9+ useFetcher ,
10+ useLocation ,
11+ useNavigation ,
12+ useRevalidator ,
13+ useSubmit ,
14+ } from "@remix-run/react" ;
815import { LayoutGroup , motion } from "framer-motion" ;
916import {
1017 type CSSProperties ,
@@ -33,6 +40,8 @@ import { DeploymentsIcon } from "~/assets/icons/DeploymentsIcon";
3340import { DialIcon } from "~/assets/icons/DialIcon" ;
3441import { DropdownIcon } from "~/assets/icons/DropdownIcon" ;
3542import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons" ;
43+ import { EyeClosedIcon } from "~/assets/icons/EyeClosedIcon" ;
44+ import { EyeOpenIcon } from "~/assets/icons/EyeOpenIcon" ;
3645import { FolderClosedIcon } from "~/assets/icons/FolderClosedIcon" ;
3746import { FolderOpenIcon } from "~/assets/icons/FolderOpenIcon" ;
3847import { GlobeLinesIcon } from "~/assets/icons/GlobeLinesIcon" ;
@@ -69,7 +78,7 @@ import { type MatchedOrganization } from "~/hooks/useOrganizations";
6978import { type MatchedProject } from "~/hooks/useProject" ;
7079import { useShortcutKeys } from "~/hooks/useShortcutKeys" ;
7180import { useShowSelfServe } from "~/hooks/useShowSelfServe" ;
72- import { useHasAdminAccess } from "~/hooks/useUser" ;
81+ import { useHasAdminAccess , useIsViewingAsUser } from "~/hooks/useUser" ;
7382import { type UserWithDashboardPreferences } from "~/models/user.server" ;
7483import {
7584 useCurrentPlan ,
@@ -389,6 +398,7 @@ export function SideMenu({
389398 const { isConnected } = useDevPresence ( ) ;
390399 const isFreeUser = currentPlan ?. v3Subscription ?. isPaying === false ;
391400 const isAdmin = useHasAdminAccess ( ) ;
401+ const isViewingAsUser = useIsViewingAsUser ( ) ;
392402 const { isManagedCloud } = useFeatures ( ) ;
393403 const featureFlags = useFeatureFlags ( ) ;
394404 const incidentStatus = useIncidentStatus ( ) ;
@@ -785,7 +795,7 @@ export function SideMenu({
785795 // user's saved order/hidden preferences are applied at render below.
786796 const staticSections : SideMenuSectionConfig [ ] = [ ] ;
787797
788- if ( user . admin || user . isImpersonating || featureFlags . hasAiAccess ) {
798+ if ( isAdmin || featureFlags . hasAiAccess ) {
789799 staticSections . push ( {
790800 id : "ai" ,
791801 title : "AI" ,
@@ -813,12 +823,12 @@ export function SideMenu({
813823 } ) ;
814824 }
815825
816- if ( user . admin || user . isImpersonating || featureFlags . hasQueryAccess ) {
826+ if ( isAdmin || featureFlags . hasQueryAccess ) {
817827 staticSections . push ( {
818828 id : "metrics" ,
819829 title : "Observability" ,
820830 items : [
821- ...( user . admin || user . isImpersonating || featureFlags . hasLogsPageAccess
831+ ...( isAdmin || featureFlags . hasLogsPageAccess
822832 ? [
823833 {
824834 id : "logs" ,
@@ -1048,7 +1058,13 @@ export function SideMenu({
10481058 style = { initialStyleRef . current }
10491059 className = { cn (
10501060 "relative h-full border-r bg-background-bright" ,
1051- user . isImpersonating ? IMPERSONATION_ACCENT . border : "border-grid-bright"
1061+ // The accent is the loudest "you are not this user" tell, so "view as user" drops it too —
1062+ // the point of the mode is a dashboard that looks exactly like the user's. The account
1063+ // menu's "Stop impersonating" and the toggle itself stay on raw impersonation, so there is
1064+ // still a way back out (as does the ⌘⌥A shortcut in <GlobalShortcuts>).
1065+ user . isImpersonating && ! isViewingAsUser
1066+ ? IMPERSONATION_ACCENT . border
1067+ : "border-grid-bright"
10521068 ) }
10531069 >
10541070 < ResizeHandle
@@ -1832,24 +1848,29 @@ function AccountMenuItems({
18321848
18331849 return (
18341850 < >
1835- { isAdmin && (
1851+ { /* "Stop impersonating" and the view-as-user toggle key off raw impersonation, not `isAdmin`:
1852+ with "view as user" on, `isAdmin` is false and these are the only ways back out. */ }
1853+ { ( isImpersonating || isAdmin ) && (
18361854 < div className = "flex flex-col gap-1 border-b border-grid-bright p-1" >
18371855 { isImpersonating ? (
1838- < PopoverMenuItem
1839- title = {
1840- < div className = "flex w-full items-center justify-between" >
1841- < span className = { IMPERSONATION_ACCENT . text } > Stop impersonating</ span >
1842- < ShortcutKey
1843- shortcut = { { modifiers : [ "mod" , "alt" ] , key : "a" } }
1844- variant = "medium/bright"
1845- />
1846- </ div >
1847- }
1848- icon = { UserCrossIcon }
1849- onClick = { stopImpersonating }
1850- leadingIconClassName = { cn ( SIDE_MENU_POPOVER_ITEM_ICON , IMPERSONATION_ACCENT . text ) }
1851- className = { SIDE_MENU_POPOVER_ITEM_LABEL }
1852- />
1856+ < >
1857+ < PopoverMenuItem
1858+ title = {
1859+ < div className = "flex w-full items-center justify-between" >
1860+ < span className = { IMPERSONATION_ACCENT . text } > Stop impersonating</ span >
1861+ < ShortcutKey
1862+ shortcut = { { modifiers : [ "mod" , "alt" ] , key : "a" } }
1863+ variant = "medium/bright"
1864+ />
1865+ </ div >
1866+ }
1867+ icon = { UserCrossIcon }
1868+ onClick = { stopImpersonating }
1869+ leadingIconClassName = { cn ( SIDE_MENU_POPOVER_ITEM_ICON , IMPERSONATION_ACCENT . text ) }
1870+ className = { SIDE_MENU_POPOVER_ITEM_LABEL }
1871+ />
1872+ < ViewAsUserMenuItem />
1873+ </ >
18531874 ) : (
18541875 < PopoverMenuItem
18551876 to = { adminPath ( ) }
@@ -1906,6 +1927,30 @@ function AccountMenuItems({
19061927 ) ;
19071928}
19081929
1930+ /**
1931+ * Toggles the display-only "view as user" mode for the current impersonation session, so an admin
1932+ * can see the dashboard the way the impersonated user sees it. `reloadDocument` forces a full
1933+ * navigation, so every loader re-runs under the updated cookie instead of reusing cached data.
1934+ */
1935+ function ViewAsUserMenuItem ( ) {
1936+ const isViewingAsUser = useIsViewingAsUser ( ) ;
1937+ const location = useLocation ( ) ;
1938+
1939+ return (
1940+ < Form method = "post" action = "/resources/impersonation/view-as" reloadDocument >
1941+ < input type = "hidden" name = "viewAsUser" value = { isViewingAsUser ? "false" : "true" } />
1942+ < input type = "hidden" name = "redirectTo" value = { `${ location . pathname } ${ location . search } ` } />
1943+ < PopoverMenuItem
1944+ type = "submit"
1945+ title = { isViewingAsUser ? "Show admin UI" : "View as user" }
1946+ icon = { isViewingAsUser ? EyeClosedIcon : EyeOpenIcon }
1947+ leadingIconClassName = { cn ( SIDE_MENU_POPOVER_ITEM_ICON , IMPERSONATION_ACCENT . text ) }
1948+ className = { SIDE_MENU_POPOVER_ITEM_LABEL }
1949+ />
1950+ </ Form >
1951+ ) ;
1952+ }
1953+
19091954function AccountMenu ( { isAdmin, isImpersonating } : { isAdmin : boolean ; isImpersonating : boolean } ) {
19101955 const [ isOpen , setIsOpen ] = useState ( false ) ;
19111956 const navigation = useNavigation ( ) ;
0 commit comments