Skip to content

Commit 0b0f66d

Browse files
committed
feat(webapp): add an account menu to the side menu header
- Add an Account menu (user avatar button) to the top of the side menu, between the organization menu and where the impersonation button used to be - The menu links to Profile, Personal Access Tokens, and Security, plus Logout - For admins it adds an Admin dashboard / Stop impersonating item with the cmd+esc shortcut shown inline, replacing the old permanent admin/impersonation button - Move Logout out of the organization menu into the account menu, and add a divider above Switch organization
1 parent 24557d8 commit 0b0f66d

1 file changed

Lines changed: 115 additions & 28 deletions

File tree

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 115 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
ExclamationTriangleIcon,
55
} from "@heroicons/react/24/outline";
66
import { LinkIcon } from "@heroicons/react/24/solid";
7-
import { useFetcher, useNavigation } from "@remix-run/react";
7+
import { useFetcher, useNavigate, useNavigation, useSubmit } from "@remix-run/react";
88
import { LayoutGroup, motion } from "framer-motion";
99
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
1010
import { AIChatIcon } from "~/assets/icons/AIChatIcon";
@@ -37,17 +37,20 @@ import { LogsIcon } from "~/assets/icons/LogsIcon";
3737
import { PlusIcon } from "~/assets/icons/PlusIcon";
3838
import { QueuesIcon } from "~/assets/icons/QueuesIcon";
3939
import { RunsIcon } from "~/assets/icons/RunsIcon";
40+
import { ShieldIcon } from "~/assets/icons/ShieldIcon";
4041
import { SlidersIcon } from "~/assets/icons/SlidersIcon";
4142
import { TasksIcon } from "~/assets/icons/TasksIcon";
4243
import { UsageIcon } from "~/assets/icons/UsageIcon";
4344
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";
4445
import { CreditCardIcon } from "~/assets/icons/CreditCardIcon";
46+
import { UserCrossIcon } from "~/assets/icons/UserCrossIcon";
4547
import { UserGroupIcon } from "~/assets/icons/UserGroupIcon";
4648
import { RolesIcon } from "~/assets/icons/RolesIcon";
4749
import { PadlockIcon } from "~/assets/icons/PadlockIcon";
4850
import { SlackIcon } from "~/assets/icons/SlackIcon";
4951
import { VercelLogo } from "~/components/integrations/VercelLogo";
5052
import { Avatar } from "~/components/primitives/Avatar";
53+
import { UserProfilePhoto } from "~/components/UserProfilePhoto";
5154
import { type MatchedEnvironment } from "~/hooks/useEnvironment";
5255
import { useFeatureFlags } from "~/hooks/useFeatureFlags";
5356
import { useFeatures } from "~/hooks/useFeatures";
@@ -67,6 +70,8 @@ import { IncidentStatusPanel, useIncidentStatus } from "~/routes/resources.incid
6770
import { cn } from "~/utils/cn";
6871
import {
6972
accountPath,
73+
accountSecurityPath,
74+
personalAccessTokensPath,
7075
adminPath,
7176
branchesPath,
7277
concurrencyPath,
@@ -110,7 +115,6 @@ import {
110115
import { FreePlanUsage } from "../billing/FreePlanUsage";
111116
import { ConnectionIcon, DevPresencePanel, useDevPresence } from "../DevPresence";
112117
import { AlphaBadge, NewBadge } from "../FeatureBadges";
113-
import { ImpersonationBanner } from "../ImpersonationBanner";
114118
import { Button, ButtonContent, LinkButton } from "../primitives/Buttons";
115119
import { Dialog, DialogTrigger } from "../primitives/Dialog";
116120
import { Paragraph } from "../primitives/Paragraph";
@@ -305,30 +309,9 @@ export function SideMenu({
305309
isCollapsed={isCollapsed}
306310
/>
307311
</div>
308-
{isAdmin && !user.isImpersonating ? (
309-
<CollapsibleElement isCollapsed={isCollapsed}>
310-
<TooltipProvider disableHoverableContent={true}>
311-
<Tooltip>
312-
<TooltipTrigger>
313-
<LinkButton
314-
variant="minimal/medium"
315-
to={adminPath()}
316-
TrailingIcon={HomeIcon}
317-
trailingIconClassName="h-4.5 w-4.5"
318-
className="h-8 w-8"
319-
/>
320-
</TooltipTrigger>
321-
<TooltipContent side="bottom" className={"text-xs"}>
322-
Admin dashboard
323-
</TooltipContent>
324-
</Tooltip>
325-
</TooltipProvider>
326-
</CollapsibleElement>
327-
) : isAdmin && user.isImpersonating ? (
328-
<CollapsibleElement isCollapsed={isCollapsed}>
329-
<ImpersonationBanner />
330-
</CollapsibleElement>
331-
) : null}
312+
<CollapsibleElement isCollapsed={isCollapsed}>
313+
<AccountMenu isAdmin={isAdmin} isImpersonating={user.isImpersonating} />
314+
</CollapsibleElement>
332315
</div>
333316
<div
334317
className={cn(
@@ -975,6 +958,8 @@ function OrgSelector({
975958
/>
976959
)}
977960
<Integrations organization={organization} />
961+
</div>
962+
<div className="border-t border-charcoal-700 p-1">
978963
{organizations.length > 1 ? (
979964
<SwitchOrganizations organizations={organizations} organization={organization} />
980965
) : (
@@ -987,14 +972,116 @@ function OrgSelector({
987972
/>
988973
)}
989974
</div>
990-
<div className="border-t border-charcoal-700 p-1">
975+
</PopoverContent>
976+
</Popover>
977+
);
978+
}
979+
980+
function AccountMenu({ isAdmin, isImpersonating }: { isAdmin: boolean; isImpersonating: boolean }) {
981+
const [isOpen, setIsOpen] = useState(false);
982+
const navigation = useNavigation();
983+
const navigate = useNavigate();
984+
const submit = useSubmit();
985+
986+
useEffect(() => {
987+
setIsOpen(false);
988+
}, [navigation.location?.pathname]);
989+
990+
const stopImpersonating = () =>
991+
submit(null, { action: "/resources/impersonation", method: "delete" });
992+
993+
useShortcutKeys({
994+
shortcut: isAdmin
995+
? { modifiers: ["mod"], key: "esc", enabledOnInputElements: true }
996+
: undefined,
997+
action: () => {
998+
if (isImpersonating) {
999+
stopImpersonating();
1000+
} else {
1001+
navigate(adminPath());
1002+
}
1003+
},
1004+
});
1005+
1006+
return (
1007+
<Popover onOpenChange={(open) => setIsOpen(open)} open={isOpen}>
1008+
<SimpleTooltip
1009+
button={
1010+
<PopoverTrigger className="group flex size-8 items-center justify-center rounded transition-colors hover:bg-charcoal-750 focus-custom">
1011+
<UserProfilePhoto className="size-6" />
1012+
</PopoverTrigger>
1013+
}
1014+
content="Account"
1015+
side="bottom"
1016+
sideOffset={8}
1017+
disableHoverableContent
1018+
/>
1019+
<PopoverContent
1020+
className="min-w-[16rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
1021+
side="bottom"
1022+
sideOffset={4}
1023+
align="start"
1024+
style={{ maxHeight: `calc(var(--radix-popover-content-available-height) - 10vh)` }}
1025+
>
1026+
{isAdmin && (
1027+
<div className="flex flex-col gap-1 border-b border-charcoal-700 p-1">
1028+
{isImpersonating ? (
1029+
<PopoverMenuItem
1030+
title={
1031+
<div className="flex w-full items-center justify-between">
1032+
<span className="text-amber-400">Stop impersonating</span>
1033+
<span className="flex items-center gap-1">
1034+
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
1035+
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
1036+
</span>
1037+
</div>
1038+
}
1039+
icon={UserCrossIcon}
1040+
onClick={stopImpersonating}
1041+
leadingIconClassName={cn(SIDE_MENU_POPOVER_ITEM_ICON, "text-amber-400")}
1042+
className={SIDE_MENU_POPOVER_ITEM_LABEL}
1043+
/>
1044+
) : (
1045+
<PopoverMenuItem
1046+
to={adminPath()}
1047+
title={
1048+
<div className="flex w-full items-center justify-between">
1049+
<span>Admin dashboard</span>
1050+
<span className="flex items-center gap-1">
1051+
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
1052+
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
1053+
</span>
1054+
</div>
1055+
}
1056+
icon={HomeIcon}
1057+
leadingIconClassName={SIDE_MENU_POPOVER_ITEM_ICON}
1058+
className={SIDE_MENU_POPOVER_ITEM_LABEL}
1059+
/>
1060+
)}
1061+
</div>
1062+
)}
1063+
<div className="flex flex-col gap-1 p-1">
9911064
<PopoverMenuItem
9921065
to={accountPath()}
993-
title="Account"
1066+
title="Profile"
9941067
icon={AvatarCircleIcon}
9951068
leadingIconClassName={SIDE_MENU_POPOVER_ITEM_ICON}
9961069
className={SIDE_MENU_POPOVER_ITEM_LABEL}
9971070
/>
1071+
<PopoverMenuItem
1072+
to={personalAccessTokensPath()}
1073+
title="Personal Access Tokens"
1074+
icon={ShieldIcon}
1075+
leadingIconClassName={SIDE_MENU_POPOVER_ITEM_ICON}
1076+
className={SIDE_MENU_POPOVER_ITEM_LABEL}
1077+
/>
1078+
<PopoverMenuItem
1079+
to={accountSecurityPath()}
1080+
title="Security"
1081+
icon={PadlockIcon}
1082+
leadingIconClassName={SIDE_MENU_POPOVER_ITEM_ICON}
1083+
className={SIDE_MENU_POPOVER_ITEM_LABEL}
1084+
/>
9981085
</div>
9991086
<div className="border-t border-charcoal-700 p-1">
10001087
<PopoverMenuItem

0 commit comments

Comments
 (0)