feat(site/src/modules/dashboard/Navbar): replace proxy emoji with latency radio icon in trigger (#28128)

Updates the latency dropdown's collapsed views in the navbar:

- Removes the proxy emoji (`ExternalImage`) from the desktop trigger and
the mobile "Workspace proxy settings" row.
- Shows a lucide `RadioIcon` instead, colored via `getLatencyColor`
(matching the loading state used by the `Latency` component on desktop).
- Keeps the latency text in `content-primary`; only the icon carries the
latency color.

The expanded proxy lists are unchanged (they keep the proxy icon and
colored latency text).

Story changes:

- Added `ClosedWarningLatency` and `ClosedCriticalLatency` to cover the
icon color per latency level.
- Right-aligned the ProxyMenu story trigger to match its navbar
placement, so the end-aligned menu renders without collision shifting in
the story canvas.

> Generated by Coder Agents on behalf of @tracyjohnsonux.
This commit is contained in:
TJ
2026-08-17 18:41:27 +00:00
committed by GitHub
parent 5d746aa594
commit b4971bc49f
4 changed files with 142 additions and 26 deletions
@@ -11,6 +11,22 @@ import {
} from "#/testHelpers/entities";
import { MobileMenu } from "./MobileMenu";
const defaultProxyContextValue = {
latenciesLoaded: true,
proxy: {
preferredPathAppURL: "",
preferredWildcardHostname: "",
proxy: MockPrimaryWorkspaceProxy,
},
isLoading: false,
isFetched: true,
setProxy: fn(),
clearProxy: fn(),
refetchProxyLatencies: fn(),
proxyLatencies: MockProxyLatencies,
proxies: MockWorkspaceProxies,
};
const meta: Meta<typeof MobileMenu> = {
title: "modules/dashboard/MobileMenu",
parameters: {
@@ -21,21 +37,7 @@ const meta: Meta<typeof MobileMenu> = {
},
component: MobileMenu,
args: {
proxyContextValue: {
latenciesLoaded: true,
proxy: {
preferredPathAppURL: "",
preferredWildcardHostname: "",
proxy: MockPrimaryWorkspaceProxy,
},
isLoading: false,
isFetched: true,
setProxy: fn(),
clearProxy: fn(),
refetchProxyLatencies: fn(),
proxyLatencies: MockProxyLatencies,
proxies: MockWorkspaceProxies,
},
proxyContextValue: defaultProxyContextValue,
user: MockUserOwner,
supportLinks: MockSupportLinks,
onSignOut: fn(),
@@ -105,6 +107,53 @@ export const ProxySettings: Story = {
},
};
export const ProxyWarningLatency: Story = {
args: {
proxyContextValue: {
...defaultProxyContextValue,
proxyLatencies: {
...MockProxyLatencies,
[MockPrimaryWorkspaceProxy.id]: {
accurate: true,
latencyMS: 224,
at: new Date(),
nextHopProtocol: "h2",
},
},
},
},
};
export const ProxyCriticalLatency: Story = {
args: {
proxyContextValue: {
...defaultProxyContextValue,
proxyLatencies: {
...MockProxyLatencies,
[MockPrimaryWorkspaceProxy.id]: {
accurate: true,
latencyMS: 471,
at: new Date(),
nextHopProtocol: "h2",
},
},
},
},
};
export const ProxyNoLatency: Story = {
args: {
proxyContextValue: {
...defaultProxyContextValue,
proxyLatencies: Object.fromEntries(
Object.entries(MockProxyLatencies).filter(
([id]) => id !== MockPrimaryWorkspaceProxy.id,
),
),
},
},
};
export const UserSettings: Story = {
play: async ({ canvasElement }) => {
const user = userEvent.setup();
@@ -2,6 +2,7 @@ import {
ChevronRightIcon,
CircleHelpIcon,
MenuIcon,
RadioIcon,
XIcon,
} from "lucide-react";
import { type FC, useState } from "react";
@@ -26,6 +27,7 @@ import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
import { Latency } from "#/components/Latency/Latency";
import type { ProxyContextValue } from "#/contexts/ProxyContext";
import { cn } from "#/utils/cn";
import { getLatencyColor } from "#/utils/latency";
import {
AdminSettingsItems,
type AdminSettingsPermissions,
@@ -132,12 +134,19 @@ const ProxySettingsSub: FC<ProxySettingsSubProps> = ({ proxyContextValue }) => {
>
Workspace proxy settings:
<span className="leading-none flex items-center gap-1">
<ExternalImage
className="size-4"
src={selectedProxy.icon_url}
alt={selectedProxy.name}
<span className="sr-only">
Latency for {selectedProxy.display_name || selectedProxy.name}
</span>
<RadioIcon
aria-hidden="true"
className={cn("size-4", getLatencyColor(latency?.latencyMS))}
/>
<Latency
className={
latency?.latencyMS ? "text-content-primary" : undefined
}
latency={latency?.latencyMS}
/>
{latency && <Latency latency={latency.latencyMS} />}
</span>
<ChevronRightIcon
className={cn("ml-auto", open ? "rotate-90" : "")}
@@ -80,7 +80,9 @@ const meta: Meta<typeof ProxyMenu> = {
decorators: [
(Story) => (
<AuthProvider>
<Story />
<div className="flex justify-end">
<Story />
</div>
</AuthProvider>
),
withDesktopViewport,
@@ -103,6 +105,53 @@ type Story = StoryObj<typeof ProxyMenu>;
export const Closed: Story = {};
export const ClosedWarningLatency: Story = {
args: {
proxyContextValue: {
...defaultProxyContextValue,
proxyLatencies: {
...MockProxyLatencies,
[MockWorkspaceProxies[0].id]: {
accurate: true,
latencyMS: 224,
at: new Date(),
nextHopProtocol: "h2",
},
},
},
},
};
export const ClosedCriticalLatency: Story = {
args: {
proxyContextValue: {
...defaultProxyContextValue,
proxyLatencies: {
...MockProxyLatencies,
[MockWorkspaceProxies[0].id]: {
accurate: true,
latencyMS: 471,
at: new Date(),
nextHopProtocol: "h2",
},
},
},
},
};
export const ClosedNoLatency: Story = {
args: {
proxyContextValue: {
...defaultProxyContextValue,
proxyLatencies: Object.fromEntries(
Object.entries(MockProxyLatencies).filter(
([id]) => id !== MockWorkspaceProxies[0].id,
),
),
},
},
};
export const Opened: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -1,3 +1,4 @@
import { RadioIcon } from "lucide-react";
import { type FC, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
@@ -19,6 +20,7 @@ import { Latency } from "#/components/Latency/Latency";
import { Skeleton } from "#/components/Skeleton/Skeleton";
import type { ProxyContextValue } from "#/contexts/ProxyContext";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { getLatencyColor } from "#/utils/latency";
import { sortProxiesByLatency } from "./proxyUtils";
interface ProxyMenuProps {
@@ -80,14 +82,21 @@ export const ProxyMenu: FC<ProxyMenuProps> = ({ proxyContextValue }) => {
{selectedProxy ? (
<>
<ExternalImage
// 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}
<RadioIcon
aria-hidden="true"
className={getLatencyColor(
proxyLatencyLoading(selectedProxy)
? undefined
: latencies?.[selectedProxy.id]?.latencyMS,
)}
/>
<Latency
className={
latencies?.[selectedProxy.id]?.latencyMS
? "text-content-primary"
: undefined
}
latency={latencies?.[selectedProxy.id]?.latencyMS}
isLoading={proxyLatencyLoading(selectedProxy)}
/>