mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: refactor <ProxyMenu /> (#21807)
This pull-request takes the old `<ProxyMenu />` in the header and makes it so that we're inline with the latest and greatest of components from codebase rather than MUI. Furthermore, we're reintroducing the `<DropdownRadioGroup />` and `<DropdownRadioItem />` components. <img width="3516" height="2390" alt="CleanShot 2026-01-31 at 13 49 28@2x" src="https://github.com/user-attachments/assets/7f8de8e9-7645-446e-9495-0b20194cc759" /> ### Preview | Old | New | | --- | --- | | <img width="418" height="499" alt="LATENCY_OLD" src="https://github.com/user-attachments/assets/86e9166a-7045-48c9-91f1-4593f85274d4" /> | <img width="418" height="499" alt="LATENCY_NEW" src="https://github.com/user-attachments/assets/a1cf80af-d11f-4bc7-99fd-a41c54a7b153" /> |
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import { Check, ChevronRight } from "lucide-react";
|
||||
import {
|
||||
type ComponentPropsWithoutRef,
|
||||
type ElementRef,
|
||||
@@ -26,7 +26,7 @@ const _DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const _DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const _DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
@@ -154,7 +154,7 @@ const DropdownMenuCheckboxItem = forwardRef<
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = forwardRef<
|
||||
export const DropdownMenuRadioItem = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
@@ -162,19 +162,20 @@ const DropdownMenuRadioItem = forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
[
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors",
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none transition-colors",
|
||||
"focus:bg-surface-secondary focus:text-content-primary data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"data-[state=checked]:bg-surface-secondary data-[state=checked]:text-content-primary",
|
||||
],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
{children}
|
||||
<span className="absolute top-3.5 right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { Abbr } from "components/Abbr/Abbr";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "components/DropdownMenu/DropdownMenu";
|
||||
import { displayError } from "components/GlobalSnackbar/utils";
|
||||
import { Latency } from "components/Latency/Latency";
|
||||
import type { ProxyContextValue } from "contexts/ProxyContext";
|
||||
import { useAuthenticated } from "hooks";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { type FC, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { type FC, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { sortProxiesByLatency } from "./proxyUtils";
|
||||
|
||||
interface ProxyMenuProps {
|
||||
@@ -21,14 +26,11 @@ interface ProxyMenuProps {
|
||||
}
|
||||
|
||||
export const ProxyMenu: FC<ProxyMenuProps> = ({ proxyContextValue }) => {
|
||||
const theme = useTheme();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [refetchDate, setRefetchDate] = useState<Date>();
|
||||
const selectedProxy = proxyContextValue.proxy.proxy;
|
||||
const refreshLatencies = proxyContextValue.refetchProxyLatencies;
|
||||
const closeMenu = () => setIsOpen(false);
|
||||
const navigate = useNavigate();
|
||||
const closeMenu = () => setOpen(false);
|
||||
const latencies = proxyContextValue.proxyLatencies;
|
||||
const isLoadingLatencies = Object.keys(latencies).length === 0;
|
||||
const isLoading = proxyContextValue.isLoading || isLoadingLatencies;
|
||||
@@ -69,163 +71,121 @@ export const ProxyMenu: FC<ProxyMenuProps> = ({ proxyContextValue }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
ref={buttonRef}
|
||||
onClick={() => setIsOpen(true)}
|
||||
size="lg"
|
||||
>
|
||||
<span css={{ ...visuallyHidden }}>
|
||||
Latency for {selectedProxy?.display_name ?? "your region"}
|
||||
</span>
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="lg">
|
||||
<span css={{ ...visuallyHidden }}>
|
||||
Latency for {selectedProxy?.display_name ?? "your region"}
|
||||
</span>
|
||||
|
||||
{selectedProxy ? (
|
||||
<>
|
||||
<img
|
||||
// Empty alt text used because we don't want to double up on
|
||||
// screen reader announcements from visually-hidden span
|
||||
alt=""
|
||||
src={selectedProxy.icon_url}
|
||||
/>
|
||||
{selectedProxy ? (
|
||||
<>
|
||||
<img
|
||||
// Empty alt text used because we don't want to double up on
|
||||
// screen reader announcements from visually-hidden span
|
||||
alt=""
|
||||
src={selectedProxy.icon_url}
|
||||
/>
|
||||
|
||||
<Latency
|
||||
latency={latencies?.[selectedProxy.id]?.latencyMS}
|
||||
isLoading={proxyLatencyLoading(selectedProxy)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
"Select Proxy"
|
||||
)}
|
||||
|
||||
<ChevronDownIcon className="text-content-primary !size-icon-sm" />
|
||||
</Button>
|
||||
|
||||
<Menu
|
||||
open={isOpen}
|
||||
anchorEl={buttonRef.current}
|
||||
onClick={closeMenu}
|
||||
onClose={closeMenu}
|
||||
css={{ "& .MuiMenu-paper": { paddingTop: 8, paddingBottom: 8 } }}
|
||||
// autoFocus here does not affect modal focus; it affects whether the
|
||||
// first item in the list will get auto-focus when the menu opens. Have
|
||||
// to turn this off because otherwise, screen readers will skip over all
|
||||
// the descriptive text and will only have access to the latency options
|
||||
autoFocus={false}
|
||||
className="z-0"
|
||||
>
|
||||
{proxyContextValue.proxies &&
|
||||
proxyContextValue.proxies.length > 1 && [
|
||||
<div
|
||||
key="description"
|
||||
css={{
|
||||
width: "100%",
|
||||
maxWidth: "320px",
|
||||
fontSize: 14,
|
||||
padding: 16,
|
||||
lineHeight: "140%",
|
||||
}}
|
||||
>
|
||||
<h4
|
||||
tabIndex={-1}
|
||||
css={{
|
||||
fontSize: "inherit",
|
||||
fontWeight: 600,
|
||||
lineHeight: "inherit",
|
||||
margin: 0,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Select a region nearest to you
|
||||
</h4>
|
||||
|
||||
<p
|
||||
css={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "inherit",
|
||||
marginTop: 0.5,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
Workspace proxies improve terminal and web app connections to
|
||||
workspaces. This does not apply to{" "}
|
||||
<Abbr title="Command-Line Interface" pronunciation="initialism">
|
||||
CLI
|
||||
</Abbr>{" "}
|
||||
connections. A region must be manually selected, otherwise the
|
||||
default primary region will be used.
|
||||
</p>
|
||||
</div>,
|
||||
|
||||
<Divider key="divider" />,
|
||||
]}
|
||||
|
||||
{proxyContextValue.proxies &&
|
||||
sortProxiesByLatency(proxyContextValue.proxies, latencies).map(
|
||||
(proxy) => (
|
||||
<MenuItem
|
||||
key={proxy.id}
|
||||
selected={proxy.id === selectedProxy?.id}
|
||||
className="text-sm"
|
||||
onClick={() => {
|
||||
if (!proxy.healthy) {
|
||||
displayError("Please select a healthy workspace proxy.");
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
proxyContextValue.setProxy(proxy);
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-6 items-center w-full">
|
||||
<div className="leading-[0] size-[14px]">
|
||||
<img
|
||||
src={proxy.icon_url}
|
||||
alt=""
|
||||
className="object-fit size-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{proxy.display_name}
|
||||
|
||||
<Latency
|
||||
className="ml-auto"
|
||||
latency={latencies?.[proxy.id]?.latencyMS}
|
||||
isLoading={proxyLatencyLoading(proxy)}
|
||||
/>
|
||||
</div>
|
||||
</MenuItem>
|
||||
),
|
||||
<Latency
|
||||
latency={latencies?.[selectedProxy.id]?.latencyMS}
|
||||
isLoading={proxyLatencyLoading(selectedProxy)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
"Select Proxy"
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
{Boolean(permissions.editWorkspaceProxies) && (
|
||||
<MenuItem
|
||||
css={{ fontSize: 14 }}
|
||||
onClick={() => {
|
||||
navigate("/deployment/workspace-proxies");
|
||||
}}
|
||||
<ChevronDownIcon className="text-content-primary !size-icon-lg" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-80">
|
||||
{proxyContextValue.proxies && proxyContextValue.proxies.length > 1 && (
|
||||
<DropdownMenuItem
|
||||
disabled
|
||||
className="flex flex-col gap-1 items-start data-[disabled]:opacity-100"
|
||||
>
|
||||
Proxy settings
|
||||
</MenuItem>
|
||||
<div className="text-content-primary font-semibold text-left">
|
||||
Select a region nearest to you
|
||||
</div>
|
||||
<div className="text-xs text-content-secondary leading-relaxed">
|
||||
Workspace proxies improve terminal and web app connections to
|
||||
workspaces. This does not apply to{" "}
|
||||
<Abbr title="Command-Line Interface" pronunciation="initialism">
|
||||
CLI
|
||||
</Abbr>{" "}
|
||||
connections. A region must be manually selected, otherwise the
|
||||
default primary region will be used.
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
css={{ fontSize: 14 }}
|
||||
{proxyContextValue.proxies && proxyContextValue.proxies.length > 1 && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
|
||||
{proxyContextValue.proxies && (
|
||||
<DropdownMenuRadioGroup value={selectedProxy?.id}>
|
||||
{sortProxiesByLatency(proxyContextValue.proxies, latencies).map(
|
||||
(proxy) => (
|
||||
<DropdownMenuRadioItem
|
||||
value={proxy.id}
|
||||
key={proxy.id}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (!proxy.healthy) {
|
||||
displayError("Please select a healthy workspace proxy.");
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
proxyContextValue.setProxy(proxy);
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-3 items-center w-full">
|
||||
<div className="leading-none size-4">
|
||||
<img
|
||||
src={proxy.icon_url}
|
||||
alt=""
|
||||
className="object-contain size-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{proxy.display_name}
|
||||
|
||||
<Latency
|
||||
className="ml-auto"
|
||||
latency={latencies?.[proxy.id]?.latencyMS}
|
||||
isLoading={proxyLatencyLoading(proxy)}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuRadioItem>
|
||||
),
|
||||
)}
|
||||
</DropdownMenuRadioGroup>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{Boolean(permissions.editWorkspaceProxies) && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/deployment/workspace-proxies">
|
||||
<span>Proxy settings</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
// Stop the menu from closing
|
||||
e.stopPropagation();
|
||||
// Refresh the latencies.
|
||||
e.preventDefault();
|
||||
const refetchDate = refreshLatencies();
|
||||
setRefetchDate(refetchDate);
|
||||
}}
|
||||
>
|
||||
Refresh Latencies
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
Refresh latencies
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user