fix: create scrollable proxy menu list (#23764)

This allows the proxy menu to scroll for very large numbers of proxies
in the menu.

<img width="334" height="802" alt="screenshot"
src="https://github.com/user-attachments/assets/f0e45b9c-5b77-43da-b566-28d0572fd56b"
/>
This commit is contained in:
Jaayden Halko
2026-03-30 09:39:30 +01:00
committed by GitHub
parent bc5e4b5d54
commit 0e78156bcd
2 changed files with 104 additions and 42 deletions
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn, userEvent, within } from "storybook/test";
import { getAuthorizationKey } from "#/api/queries/authCheck";
import type * as TypesGen from "#/api/typesGenerated";
import { AuthProvider } from "#/contexts/auth/AuthProvider";
import { getPreferredProxy } from "#/contexts/ProxyContext";
import { permissionChecks } from "#/modules/permissions";
@@ -14,6 +15,50 @@ import {
import { withDesktopViewport } from "#/testHelpers/storybook";
import { ProxyMenu } from "./ProxyMenu";
const buildProxies = (count: number): TypesGen.WorkspaceProxy[] => {
const seedProxy = MockWorkspaceProxies[0];
const proxies: TypesGen.WorkspaceProxy[] = [];
for (let index = 0; index < count; index++) {
const suffix = String(index + 1).padStart(12, "0");
const id = `10000000-0000-4000-8000-${suffix}`;
const isHealthy = index % 7 !== 0;
proxies.push({
...seedProxy,
id,
name: `region-${index + 1}`,
display_name: `Region ${index + 1}`,
healthy: isHealthy,
});
}
return proxies;
};
const buildLatencies = (
proxies: TypesGen.WorkspaceProxy[],
): typeof MockProxyLatencies => {
const latencies: typeof MockProxyLatencies = {};
for (const [index, proxy] of proxies.entries()) {
if (!proxy.healthy) {
continue;
}
latencies[proxy.id] = {
accurate: true,
latencyMS: 20 + index * 3,
at: new Date(),
nextHopProtocol: "h2",
};
}
return latencies;
};
const manyProxies = buildProxies(45);
const defaultProxyContextValue = {
latenciesLoaded: true,
proxyLatencies: MockProxyLatencies,
@@ -77,3 +122,18 @@ export const SingleProxy: Story = {
await userEvent.click(canvas.getByRole("button"));
},
};
export const ManyProxiesOpened: Story = {
args: {
proxyContextValue: {
...defaultProxyContextValue,
proxies: manyProxies,
proxyLatencies: buildLatencies(manyProxies),
proxy: getPreferredProxy(manyProxies, undefined),
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button"));
},
};
+44 -42
View File
@@ -124,51 +124,53 @@ export const ProxyMenu: FC<ProxyMenuProps> = ({ proxyContextValue }) => {
)}
{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) {
toast.error(
`Failed to select proxy "${proxy.display_name}".`,
{
description:
"Please select a healthy workspace proxy.",
},
);
closeMenu();
return;
}
<div className="max-h-[calc(100vh-22rem)] -mr-2 pr-2 overflow-y-auto [scrollbar-width:thin] [scrollbar-color:hsl(var(--surface-quaternary))_transparent]">
<DropdownMenuRadioGroup value={selectedProxy?.id}>
{sortProxiesByLatency(proxyContextValue.proxies, latencies).map(
(proxy) => (
<DropdownMenuRadioItem
value={proxy.id}
key={proxy.id}
onClick={(e) => {
e.preventDefault();
if (!proxy.healthy) {
toast.error(
`Failed to select proxy "${proxy.display_name}".`,
{
description:
"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"
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>
{proxy.display_name}
<Latency
className="ml-auto"
latency={latencies?.[proxy.id]?.latencyMS}
isLoading={proxyLatencyLoading(proxy)}
/>
</div>
</DropdownMenuRadioItem>
),
)}
</DropdownMenuRadioGroup>
</DropdownMenuRadioItem>
),
)}
</DropdownMenuRadioGroup>
</div>
)}
<DropdownMenuSeparator />