mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user