Skip to content

Commit 7956d56

Browse files
committed
Extract MobileMenu
1 parent 1b0c075 commit 7956d56

File tree

2 files changed

+317
-359
lines changed

2 files changed

+317
-359
lines changed
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import type * as TypesGen from "api/typesGenerated";
2+
import type { ProxyContextValue } from "contexts/ProxyContext";
3+
import { useState, type FC } from "react";
4+
import { Link } from "react-router-dom";
5+
import { cn } from "utils/cn";
6+
import { Button } from "components/Button/Button";
7+
import {
8+
ChevronRightIcon,
9+
CircleHelpIcon,
10+
MenuIcon,
11+
XIcon,
12+
} from "lucide-react";
13+
import {
14+
DropdownMenu,
15+
DropdownMenuContent,
16+
DropdownMenuItem,
17+
DropdownMenuSeparator,
18+
DropdownMenuTrigger,
19+
} from "components/DropdownMenu/DropdownMenu";
20+
import { Avatar } from "components/Avatar/Avatar";
21+
import { Latency } from "components/Latency/Latency";
22+
import {
23+
Collapsible,
24+
CollapsibleContent,
25+
CollapsibleTrigger,
26+
} from "components/Collapsible/Collapsible";
27+
import { sortProxiesByLatency } from "./proxyUtils";
28+
import { displayError } from "components/GlobalSnackbar/utils";
29+
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
30+
31+
const itemStyles = {
32+
default: "px-9 h-10 no-underline",
33+
sub: "pl-12",
34+
open: "text-content-primary",
35+
};
36+
37+
type MobileMenuProps = {
38+
proxyContextValue?: ProxyContextValue;
39+
user?: TypesGen.User;
40+
supportLinks?: readonly TypesGen.LinkConfig[];
41+
docsHref: string;
42+
onSignOut: () => void;
43+
};
44+
45+
export const MobileMenu: FC<MobileMenuProps> = ({
46+
proxyContextValue,
47+
user,
48+
supportLinks,
49+
docsHref,
50+
onSignOut,
51+
}) => {
52+
const [open, setOpen] = useState(false);
53+
54+
return (
55+
<DropdownMenu open={open} onOpenChange={setOpen}>
56+
{open && (
57+
<div className="fixed inset-0 top-[72px] backdrop-blur-sm z-10 bg-content-primary/50" />
58+
)}
59+
<DropdownMenuTrigger asChild>
60+
<Button
61+
aria-label={open ? "Close menu" : "Open menu"}
62+
size="icon"
63+
variant="ghost"
64+
className="ml-auto md:hidden [&_svg]:size-6"
65+
>
66+
{open ? (
67+
<XIcon className="size-icon-lg" />
68+
) : (
69+
<MenuIcon className="size-icon-lg" />
70+
)}
71+
</Button>
72+
</DropdownMenuTrigger>
73+
<DropdownMenuContent
74+
className="w-screen border-0 border-b border-solid p-0 py-2"
75+
sideOffset={17}
76+
>
77+
<ProxySettingsSub proxyContextValue={proxyContextValue} />
78+
<DropdownMenuSeparator />
79+
<AdminSettingsSub />
80+
<DropdownMenuSeparator />
81+
<DropdownMenuItem asChild className={itemStyles.default}>
82+
<a href={docsHref} target="_blank" rel="noreferrer norefereer">
83+
Docs
84+
</a>
85+
</DropdownMenuItem>
86+
<DropdownMenuSeparator />
87+
<UserSettingsSub
88+
user={user}
89+
supportLinks={supportLinks}
90+
onSignOut={onSignOut}
91+
/>
92+
</DropdownMenuContent>
93+
</DropdownMenu>
94+
);
95+
};
96+
97+
type ProxySettingsSubProps = {
98+
proxyContextValue?: ProxyContextValue;
99+
};
100+
101+
const ProxySettingsSub: FC<ProxySettingsSubProps> = ({ proxyContextValue }) => {
102+
const selectedProxy = proxyContextValue?.proxy.proxy;
103+
const latency = selectedProxy
104+
? proxyContextValue?.proxyLatencies[selectedProxy?.id]
105+
: undefined;
106+
const [open, setOpen] = useState(false);
107+
108+
if (!selectedProxy) {
109+
return null;
110+
}
111+
112+
return (
113+
<Collapsible open={open} onOpenChange={setOpen}>
114+
<CollapsibleTrigger asChild>
115+
<DropdownMenuItem
116+
className={cn(itemStyles.default, open ? itemStyles.open : "")}
117+
onClick={(e) => {
118+
e.preventDefault();
119+
setOpen((prev) => !prev);
120+
}}
121+
>
122+
Workspace proxy settings:
123+
<span className="leading-[0px] flex items-center gap-1">
124+
<img
125+
className="w-4 h-4"
126+
src={selectedProxy.icon_url}
127+
alt={selectedProxy.name}
128+
/>
129+
{latency && <Latency latency={latency.latencyMS} />}
130+
</span>
131+
<ChevronRightIcon
132+
className={cn("ml-auto", open ? "rotate-90" : "")}
133+
/>
134+
</DropdownMenuItem>
135+
</CollapsibleTrigger>
136+
<CollapsibleContent>
137+
{proxyContextValue.proxies &&
138+
sortProxiesByLatency(
139+
proxyContextValue.proxies,
140+
proxyContextValue.proxyLatencies,
141+
).map((p) => {
142+
const latency = proxyContextValue.proxyLatencies[p.id];
143+
return (
144+
<DropdownMenuItem
145+
className={cn(itemStyles.default, itemStyles.sub)}
146+
key={p.id}
147+
onClick={(e) => {
148+
e.preventDefault();
149+
150+
if (!p.healthy) {
151+
displayError("Please select a healthy workspace proxy.");
152+
return;
153+
}
154+
155+
proxyContextValue.setProxy(p);
156+
setOpen(false);
157+
}}
158+
>
159+
<img className="w-4 h-4" src={p.icon_url} alt={p.name} />
160+
{p.display_name || p.name}
161+
{latency ? (
162+
<Latency latency={latency.latencyMS} />
163+
) : (
164+
<CircleHelpIcon className="ml-auto" />
165+
)}
166+
</DropdownMenuItem>
167+
);
168+
})}
169+
<DropdownMenuSeparator />
170+
<DropdownMenuItem
171+
asChild
172+
className={cn(itemStyles.default, itemStyles.sub)}
173+
>
174+
<Link to="/deployment/workspace-proxies">Proxy settings</Link>
175+
</DropdownMenuItem>
176+
<DropdownMenuItem
177+
className={cn(itemStyles.default, itemStyles.sub)}
178+
onClick={() => {
179+
proxyContextValue.refetchProxyLatencies();
180+
}}
181+
>
182+
Refresh latencies
183+
</DropdownMenuItem>
184+
</CollapsibleContent>
185+
</Collapsible>
186+
);
187+
};
188+
189+
const AdminSettingsSub: FC = () => {
190+
const [open, setOpen] = useState(false);
191+
192+
return (
193+
<Collapsible open={open} onOpenChange={setOpen}>
194+
<CollapsibleTrigger asChild>
195+
<DropdownMenuItem
196+
className={cn(itemStyles.default, open ? itemStyles.open : "")}
197+
onClick={(e) => {
198+
e.preventDefault();
199+
setOpen((prev) => !prev);
200+
}}
201+
>
202+
Admin settings
203+
<ChevronRightIcon
204+
className={cn("ml-auto", open ? "rotate-90" : "")}
205+
/>
206+
</DropdownMenuItem>
207+
</CollapsibleTrigger>
208+
<CollapsibleContent>
209+
<DropdownMenuItem
210+
asChild
211+
className={cn(itemStyles.default, itemStyles.sub)}
212+
>
213+
<Link to="/deployment/general">Deployment</Link>
214+
</DropdownMenuItem>
215+
<DropdownMenuItem
216+
asChild
217+
className={cn(itemStyles.default, itemStyles.sub)}
218+
>
219+
<Link to="/organizations">
220+
Organizations
221+
<FeatureStageBadge
222+
contentType="beta"
223+
size="sm"
224+
showTooltip={false}
225+
/>
226+
</Link>
227+
</DropdownMenuItem>
228+
<DropdownMenuItem
229+
asChild
230+
className={cn(itemStyles.default, itemStyles.sub)}
231+
>
232+
<Link to="/audit">Audit logs</Link>
233+
</DropdownMenuItem>
234+
<DropdownMenuItem
235+
asChild
236+
className={cn(itemStyles.default, itemStyles.sub)}
237+
>
238+
<Link to="/health">Healthcheck</Link>
239+
</DropdownMenuItem>
240+
</CollapsibleContent>
241+
</Collapsible>
242+
);
243+
};
244+
245+
type UserSettingsSubProps = {
246+
user?: TypesGen.User;
247+
supportLinks?: readonly TypesGen.LinkConfig[];
248+
onSignOut: () => void;
249+
};
250+
251+
const UserSettingsSub: FC<UserSettingsSubProps> = ({
252+
user,
253+
supportLinks,
254+
onSignOut,
255+
}) => {
256+
const [open, setOpen] = useState(false);
257+
258+
return (
259+
<Collapsible open={open} onOpenChange={setOpen}>
260+
<CollapsibleTrigger asChild>
261+
<DropdownMenuItem
262+
className={cn(itemStyles.default, open ? itemStyles.open : "")}
263+
onClick={(e) => {
264+
e.preventDefault();
265+
setOpen((prev) => !prev);
266+
}}
267+
>
268+
<Avatar
269+
src={user?.avatar_url}
270+
fallback={user?.name || user?.username}
271+
/>
272+
User settings
273+
<ChevronRightIcon
274+
className={cn("ml-auto", open ? "rotate-90" : "")}
275+
/>
276+
</DropdownMenuItem>
277+
</CollapsibleTrigger>
278+
<CollapsibleContent>
279+
<DropdownMenuItem
280+
asChild
281+
className={cn(itemStyles.default, itemStyles.sub)}
282+
>
283+
<Link to="/settings/account">Account</Link>
284+
</DropdownMenuItem>
285+
<DropdownMenuItem
286+
className={cn(itemStyles.default, itemStyles.sub)}
287+
onClick={onSignOut}
288+
>
289+
Sign out
290+
</DropdownMenuItem>
291+
{supportLinks && (
292+
<>
293+
<DropdownMenuSeparator />
294+
{supportLinks?.map((l) => (
295+
<DropdownMenuItem
296+
key={l.name}
297+
asChild
298+
className={cn(itemStyles.default, itemStyles.sub)}
299+
>
300+
<a href={l.target} target="_blank" rel="noreferrer">
301+
{l.name}
302+
</a>
303+
</DropdownMenuItem>
304+
))}
305+
</>
306+
)}
307+
</CollapsibleContent>
308+
</Collapsible>
309+
);
310+
};

0 commit comments

Comments
 (0)