diff --git a/site/.storybook/preview.jsx b/site/.storybook/preview.jsx index 9953c0533e5d6..17e6113508fcc 100644 --- a/site/.storybook/preview.jsx +++ b/site/.storybook/preview.jsx @@ -64,6 +64,14 @@ export const parameters = { }, type: "tablet", }, + iphone12: { + name: "iPhone 12", + styles: { + height: "844px", + width: "390px", + }, + type: "mobile", + }, terminal: { name: "Terminal", styles: { diff --git a/site/src/components/Breadcrumb/Breadcrumb.tsx b/site/src/components/Breadcrumb/Breadcrumb.tsx index cd6625a42cca3..35f90d30a5d7b 100644 --- a/site/src/components/Breadcrumb/Breadcrumb.tsx +++ b/site/src/components/Breadcrumb/Breadcrumb.tsx @@ -28,7 +28,7 @@ export const BreadcrumbList = forwardRef<
    = forwardRef< - HTMLButtonElement, - ButtonProps ->(({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - return ( - - ); -}); +export const Button = forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); diff --git a/site/src/components/DropdownMenu/DropdownMenu.tsx b/site/src/components/DropdownMenu/DropdownMenu.tsx index 8008ea9d6c27e..c924317b20f87 100644 --- a/site/src/components/DropdownMenu/DropdownMenu.tsx +++ b/site/src/components/DropdownMenu/DropdownMenu.tsx @@ -7,10 +7,12 @@ */ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; -import { Check, ChevronRight, Circle } from "lucide-react"; +import { Button } from "components/Button/Button"; +import { Check, ChevronDownIcon, ChevronRight, Circle } from "lucide-react"; import { type ComponentPropsWithoutRef, type ElementRef, + type FC, type HTMLAttributes, forwardRef, } from "react"; @@ -196,7 +198,7 @@ export const DropdownMenuSeparator = forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx index fb43291dd48a6..1aa749e83edf4 100644 --- a/site/src/contexts/ProxyContext.tsx +++ b/site/src/contexts/ProxyContext.tsx @@ -15,6 +15,8 @@ import { import { useQuery } from "react-query"; import { type ProxyLatencyReport, useProxyLatency } from "./useProxyLatency"; +export type Proxies = readonly Region[] | readonly WorkspaceProxy[]; +export type ProxyLatencies = Record; export interface ProxyContextValue { // proxy is **always** the workspace proxy that should be used. // The 'proxy.selectedProxy' field is the proxy being used and comes from either: @@ -43,7 +45,7 @@ export interface ProxyContextValue { // WorkspaceProxy[] is returned if the user is an admin. WorkspaceProxy extends Region with // more information about the proxy and the status. More information includes the error message if // the proxy is unhealthy. - proxies?: readonly Region[] | readonly WorkspaceProxy[]; + proxies?: Proxies; // isFetched is true when the 'proxies' api call is complete. isFetched: boolean; isLoading: boolean; @@ -51,7 +53,7 @@ export interface ProxyContextValue { // proxyLatencies is a map of proxy id to latency report. If the proxyLatencies[proxy.id] is undefined // then the latency has not been fetched yet. Calculations happen async for each proxy in the list. // Refer to the returned report for a given proxy for more information. - proxyLatencies: Record; + proxyLatencies: ProxyLatencies; // refetchProxyLatencies will trigger refreshing of the proxy latencies. By default the latencies // are loaded once. refetchProxyLatencies: () => Date; diff --git a/site/src/index.css b/site/src/index.css index c97e827b98a0f..5f690b5616bca 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -70,4 +70,17 @@ * { @apply border-border; } + + /* + By default, Radix adds a margin to the `body` element when a dropdown is displayed, + causing some shifting when the dropdown has a full-width size, as is the case with the mobile menu. + To prevent this, we need to apply the styles below. + + There’s a related issue on GitHub: Radix UI Primitives Issue #3251 + https://github.com/radix-ui/primitives/issues/3251 + */ + html body[data-scroll-locked] { + --removed-body-scroll-bar-size: 0 !important; + margin-right: 0 !important; + } } diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx index dc4b1b4d92fde..d1a75c02cd315 100644 --- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -1,7 +1,6 @@ import { type Interpolation, type Theme, css, useTheme } from "@emotion/react"; -import Button from "@mui/material/Button"; import MenuItem from "@mui/material/MenuItem"; -import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; +import { Button } from "components/Button/Button"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Popover, @@ -9,6 +8,7 @@ import { PopoverTrigger, usePopover, } from "components/deprecated/Popover/Popover"; +import { ChevronDownIcon } from "lucide-react"; import { linkToAuditing } from "modules/navigation"; import type { FC } from "react"; import { NavLink } from "react-router-dom"; @@ -16,7 +16,6 @@ import { NavLink } from "react-router-dom"; interface DeploymentDropdownProps { canViewDeployment: boolean; canViewOrganizations: boolean; - canViewAllUsers: boolean; canViewAuditLog: boolean; canViewHealth: boolean; } @@ -24,7 +23,6 @@ interface DeploymentDropdownProps { export const DeploymentDropdown: FC = ({ canViewDeployment, canViewOrganizations, - canViewAllUsers, canViewAuditLog, canViewHealth, }) => { @@ -34,7 +32,6 @@ export const DeploymentDropdown: FC = ({ !canViewAuditLog && !canViewOrganizations && !canViewDeployment && - !canViewAllUsers && !canViewHealth ) { return null; @@ -43,17 +40,9 @@ export const DeploymentDropdown: FC = ({ return ( - @@ -70,7 +59,6 @@ export const DeploymentDropdown: FC = ({ diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx new file mode 100644 index 0000000000000..19c66c14b38a7 --- /dev/null +++ b/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx @@ -0,0 +1,146 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn, userEvent, within } from "@storybook/test"; +import { PointerEventsCheckLevel } from "@testing-library/user-event"; +import type { FC } from "react"; +import { chromaticWithTablet } from "testHelpers/chromatic"; +import { + MockPrimaryWorkspaceProxy, + MockProxyLatencies, + MockSupportLinks, + MockUser, + MockUser2, + MockWorkspaceProxies, +} from "testHelpers/entities"; +import { MobileMenu } from "./MobileMenu"; + +const meta: Meta = { + title: "modules/dashboard/MobileMenu", + parameters: { + layout: "fullscreen", + viewport: { + defaultViewport: "iphone12", + }, + }, + component: MobileMenu, + args: { + proxyContextValue: { + proxy: { + preferredPathAppURL: "", + preferredWildcardHostname: "", + proxy: MockPrimaryWorkspaceProxy, + }, + isLoading: false, + isFetched: true, + setProxy: fn(), + clearProxy: fn(), + refetchProxyLatencies: fn(), + proxyLatencies: MockProxyLatencies, + proxies: MockWorkspaceProxies, + }, + user: MockUser, + supportLinks: MockSupportLinks, + docsHref: "https://coder.com/docs", + onSignOut: fn(), + isDefaultOpen: true, + canViewAuditLog: true, + canViewDeployment: true, + canViewHealth: true, + canViewOrganizations: true, + }, + decorators: [withNavbarMock], +}; + +export default meta; +type Story = StoryObj; + +export const Closed: Story = { + args: { + isDefaultOpen: false, + }, +}; + +export const Admin: Story = { + play: openAdminSettings, +}; + +export const Auditor: Story = { + args: { + user: MockUser2, + canViewAuditLog: true, + canViewDeployment: false, + canViewHealth: false, + canViewOrganizations: false, + }, + play: openAdminSettings, +}; + +export const OrgAdmin: Story = { + args: { + user: MockUser2, + canViewAuditLog: true, + canViewDeployment: false, + canViewHealth: false, + canViewOrganizations: true, + }, + play: openAdminSettings, +}; + +export const Member: Story = { + args: { + user: MockUser2, + canViewAuditLog: false, + canViewDeployment: false, + canViewHealth: false, + canViewOrganizations: false, + }, +}; + +export const ProxySettings: Story = { + play: async ({ canvasElement }) => { + const user = setupUser(); + const body = within(canvasElement.ownerDocument.body); + const menuItem = await body.findByRole("menuitem", { + name: /workspace proxy settings/i, + }); + await user.click(menuItem); + }, +}; + +export const UserSettings: Story = { + play: async ({ canvasElement }) => { + const user = setupUser(); + const body = within(canvasElement.ownerDocument.body); + const menuItem = await body.findByRole("menuitem", { + name: /user settings/i, + }); + await user.click(menuItem); + }, +}; + +function withNavbarMock(Story: FC) { + return ( +
    + +
    + ); +} + +function setupUser() { + // It seems the dropdown component is disabling pointer events, which is + // causing Testing Library to throw an error. As a workaround, we can + // disable the pointer events check. + return userEvent.setup({ + pointerEventsCheck: PointerEventsCheckLevel.Never, + }); +} + +async function openAdminSettings({ + canvasElement, +}: { canvasElement: HTMLElement }) { + const user = setupUser(); + const body = within(canvasElement.ownerDocument.body); + const menuItem = await body.findByRole("menuitem", { + name: /admin settings/i, + }); + await user.click(menuItem); +} diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.tsx new file mode 100644 index 0000000000000..e04bb7328d78c --- /dev/null +++ b/site/src/modules/dashboard/Navbar/MobileMenu.tsx @@ -0,0 +1,339 @@ +import type * as TypesGen from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { Button } from "components/Button/Button"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "components/Collapsible/Collapsible"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; +import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { Latency } from "components/Latency/Latency"; +import type { ProxyContextValue } from "contexts/ProxyContext"; +import { + ChevronRightIcon, + CircleHelpIcon, + MenuIcon, + XIcon, +} from "lucide-react"; +import { type FC, useState } from "react"; +import { Link } from "react-router-dom"; +import { cn } from "utils/cn"; +import { sortProxiesByLatency } from "./proxyUtils"; + +const itemStyles = { + default: "px-9 h-10 no-underline", + sub: "pl-12", + open: "text-content-primary", +}; + +type MobileMenuPermissions = { + canViewDeployment: boolean; + canViewOrganizations: boolean; + canViewAuditLog: boolean; + canViewHealth: boolean; +}; + +type MobileMenuProps = MobileMenuPermissions & { + proxyContextValue?: ProxyContextValue; + user?: TypesGen.User; + supportLinks?: readonly TypesGen.LinkConfig[]; + docsHref: string; + onSignOut: () => void; + isDefaultOpen?: boolean; // Useful for storybook +}; + +export const MobileMenu: FC = ({ + isDefaultOpen, + proxyContextValue, + user, + supportLinks, + docsHref, + onSignOut, + ...permissions +}) => { + const [open, setOpen] = useState(isDefaultOpen); + const hasSomePermission = Object.values(permissions).some((p) => p); + + return ( + + {open && ( +
    + )} + + + + + + + {hasSomePermission && ( + <> + + + + )} + + + + Docs + + + + + + + ); +}; + +type ProxySettingsSubProps = { + proxyContextValue?: ProxyContextValue; +}; + +const ProxySettingsSub: FC = ({ proxyContextValue }) => { + const selectedProxy = proxyContextValue?.proxy.proxy; + const latency = selectedProxy + ? proxyContextValue?.proxyLatencies[selectedProxy?.id] + : undefined; + const [open, setOpen] = useState(false); + + if (!selectedProxy) { + return null; + } + + return ( + + + { + e.preventDefault(); + setOpen((prev) => !prev); + }} + > + Workspace proxy settings: + + {selectedProxy.name} + {latency && } + + + + + + {proxyContextValue.proxies && + sortProxiesByLatency( + proxyContextValue.proxies, + proxyContextValue.proxyLatencies, + ).map((p) => { + const latency = proxyContextValue.proxyLatencies[p.id]; + return ( + { + e.preventDefault(); + + if (!p.healthy) { + displayError("Please select a healthy workspace proxy."); + return; + } + + proxyContextValue.setProxy(p); + setOpen(false); + }} + > + {p.name} + {p.display_name || p.name} + {latency ? ( + + ) : ( + + )} + + ); + })} + + + Proxy settings + + { + proxyContextValue.refetchProxyLatencies(); + }} + > + Refresh latencies + + + + ); +}; + +const AdminSettingsSub: FC = ({ + canViewDeployment, + canViewOrganizations, + canViewAuditLog, + canViewHealth, +}) => { + const [open, setOpen] = useState(false); + + return ( + + + { + e.preventDefault(); + setOpen((prev) => !prev); + }} + > + Admin settings + + + + + {canViewDeployment && ( + + Deployment + + )} + {canViewOrganizations && ( + + + Organizations + + + + )} + {canViewAuditLog && ( + + Audit logs + + )} + {canViewHealth && ( + + Healthcheck + + )} + + + ); +}; + +type UserSettingsSubProps = { + user?: TypesGen.User; + supportLinks?: readonly TypesGen.LinkConfig[]; + onSignOut: () => void; +}; + +const UserSettingsSub: FC = ({ + user, + supportLinks, + onSignOut, +}) => { + const [open, setOpen] = useState(false); + + return ( + + + { + e.preventDefault(); + setOpen((prev) => !prev); + }} + > + + User settings + + + + + + Account + + + Sign out + + {supportLinks && ( + <> + + {supportLinks?.map((l) => ( + + + {l.name} + + + ))} + + )} + + + ); +}; diff --git a/site/src/modules/dashboard/Navbar/Navbar.test.tsx b/site/src/modules/dashboard/Navbar/Navbar.test.tsx index e01a1506f4c9c..aa9a2c0400e10 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.test.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.test.tsx @@ -7,7 +7,6 @@ import { MockMemberPermissions, } from "testHelpers/entities"; import { server } from "testHelpers/server"; -import { Language } from "./NavbarView"; /** * The LicenseBanner, mounted above the AppRouter, fetches entitlements. Thus, to test their @@ -24,13 +23,7 @@ describe("Navbar", () => { render(); const deploymentMenu = await screen.findByText("Admin settings"); await userEvent.click(deploymentMenu); - await waitFor( - () => { - const link = screen.getByText(Language.audit); - expect(link).toBeDefined(); - }, - { timeout: 2000 }, - ); + await screen.findByText("Audit Logs"); }); it("does not show Audit Log link when not entitled", async () => { @@ -41,8 +34,7 @@ describe("Navbar", () => { await userEvent.click(deploymentMenu); await waitFor( () => { - const link = screen.queryByText(Language.audit); - expect(link).toBe(null); + expect(screen.queryByText("Audit Logs")).not.toBeInTheDocument(); }, { timeout: 2000 }, ); @@ -64,8 +56,7 @@ describe("Navbar", () => { render(); await waitFor( () => { - const link = screen.queryByText("Deployment"); - expect(link).toBe(null); + expect(screen.queryByText("Deployment")).not.toBeInTheDocument(); }, { timeout: 2000 }, ); diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index 5a3e86832ee43..5c3ccb72ff97e 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.tsx @@ -20,7 +20,6 @@ export const Navbar: FC = () => { const canViewDeployment = Boolean(permissions.viewDeploymentValues); const canViewOrganizations = Boolean(permissions.editAnyOrganization) && showOrganizations; - const canViewAllUsers = Boolean(permissions.viewAllUsers); const proxyContextValue = useProxy(); const canViewHealth = canViewDeployment; @@ -33,10 +32,10 @@ export const Navbar: FC = () => { onSignOut={signOut} canViewDeployment={canViewDeployment} canViewOrganizations={canViewOrganizations} - canViewAllUsers={canViewAllUsers} canViewHealth={canViewHealth} canViewAuditLog={canViewAuditLog} proxyContextValue={proxyContextValue} + docsHref={appearance.docs_url} /> ); }; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx index 6ac0e51087dfa..ae13c7fcc9129 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx @@ -11,7 +11,6 @@ const meta: Meta = { component: NavbarView, args: { user: MockUser, - canViewAllUsers: true, canViewAuditLog: true, canViewDeployment: true, canViewHealth: true, @@ -35,7 +34,6 @@ export const ForAdmin: Story = { export const ForAuditor: Story = { args: { user: MockUser2, - canViewAllUsers: false, canViewAuditLog: true, canViewDeployment: false, canViewHealth: false, @@ -52,7 +50,6 @@ export const ForAuditor: Story = { export const ForOrgAdmin: Story = { args: { user: MockUser2, - canViewAllUsers: false, canViewAuditLog: true, canViewDeployment: false, canViewHealth: false, @@ -69,7 +66,6 @@ export const ForOrgAdmin: Story = { export const ForMember: Story = { args: { user: MockUser2, - canViewAllUsers: false, canViewAuditLog: false, canViewDeployment: false, canViewHealth: false, diff --git a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx index 3dd4251385e20..7b51561ddea5a 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx @@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event"; import type { ProxyContextValue } from "contexts/ProxyContext"; import { MockPrimaryWorkspaceProxy, MockUser } from "testHelpers/entities"; import { renderWithAuth } from "testHelpers/renderHelpers"; -import { NavbarView, Language as navLanguage } from "./NavbarView"; +import { NavbarView } from "./NavbarView"; const proxyContextValue: ProxyContextValue = { proxy: { @@ -25,76 +25,75 @@ describe("NavbarView", () => { it("workspaces nav link has the correct href", async () => { renderWithAuth( , ); - const workspacesLink = await screen.findByText(navLanguage.workspaces); - expect((workspacesLink as HTMLAnchorElement).href).toContain("/workspaces"); + const workspacesLink = + await screen.findByText(/workspaces/i); + expect(workspacesLink.href).toContain("/workspaces"); }); it("templates nav link has the correct href", async () => { renderWithAuth( , ); - const templatesLink = await screen.findByText(navLanguage.templates); - expect((templatesLink as HTMLAnchorElement).href).toContain("/templates"); + const templatesLink = + await screen.findByText(/templates/i); + expect(templatesLink.href).toContain("/templates"); }); it("audit nav link has the correct href", async () => { renderWithAuth( , ); const deploymentMenu = await screen.findByText("Admin settings"); await userEvent.click(deploymentMenu); - const auditLink = await screen.findByText(navLanguage.audit); - expect((auditLink as HTMLAnchorElement).href).toContain("/audit"); + const auditLink = await screen.findByText(/audit logs/i); + expect(auditLink.href).toContain("/audit"); }); it("deployment nav link has the correct href", async () => { renderWithAuth( , ); const deploymentMenu = await screen.findByText("Admin settings"); await userEvent.click(deploymentMenu); - const deploymentSettingsLink = await screen.findByText( - navLanguage.deployment, - ); - expect((deploymentSettingsLink as HTMLAnchorElement).href).toContain( - "/deployment/general", - ); + const deploymentSettingsLink = + await screen.findByText(/deployment/i); + expect(deploymentSettingsLink.href).toContain("/deployment/general"); }); }); diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 662e39ca9d02c..ec3a1c690bb60 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -1,239 +1,134 @@ -import { type Interpolation, type Theme, css, useTheme } from "@emotion/react"; -import MenuIcon from "@mui/icons-material/Menu"; -import Drawer from "@mui/material/Drawer"; -import IconButton from "@mui/material/IconButton"; import type * as TypesGen from "api/typesGenerated"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { CoderIcon } from "components/Icons/CoderIcon"; import type { ProxyContextValue } from "contexts/ProxyContext"; -import { type FC, useState } from "react"; +import type { FC } from "react"; import { NavLink, useLocation } from "react-router-dom"; -import { navHeight } from "theme/constants"; +import { cn } from "utils/cn"; import { DeploymentDropdown } from "./DeploymentDropdown"; +import { MobileMenu } from "./MobileMenu"; import { ProxyMenu } from "./ProxyMenu"; import { UserDropdown } from "./UserDropdown/UserDropdown"; export interface NavbarViewProps { logo_url?: string; user?: TypesGen.User; + docsHref: string; buildInfo?: TypesGen.BuildInfoResponse; supportLinks?: readonly TypesGen.LinkConfig[]; onSignOut: () => void; canViewDeployment: boolean; canViewOrganizations: boolean; - canViewAllUsers: boolean; canViewAuditLog: boolean; canViewHealth: boolean; proxyContextValue?: ProxyContextValue; } -export const Language = { - workspaces: "Workspaces", - templates: "Templates", - users: "Users", - audit: "Audit Logs", - deployment: "Deployment", -}; - -interface NavItemsProps { - className?: string; -} - -const NavItems: FC = ({ className }) => { - const location = useLocation(); - const theme = useTheme(); - - return ( - - ); +const linkStyles = { + default: + "text-sm font-medium text-content-secondary no-underline block h-full px-2 flex items-center hover:text-content-primary transition-colors", + active: "text-content-primary", }; export const NavbarView: FC = ({ user, logo_url, + docsHref, buildInfo, supportLinks, onSignOut, canViewDeployment, canViewOrganizations, - canViewAllUsers, canViewHealth, canViewAuditLog, proxyContextValue, }) => { - const theme = useTheme(); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); - return ( - + + +
    ); }; -const styles = { - desktopNavItems: (theme) => css` - display: none; - - ${theme.breakpoints.up("md")} { - display: flex; - } - `, - mobileMenuButton: (theme) => css` - ${theme.breakpoints.up("md")} { - display: none; - } - `, - navMenus: (theme) => ({ - display: "flex", - gap: 16, - alignItems: "center", - paddingRight: 16, - - [theme.breakpoints.up("md")]: { - marginLeft: "auto", - }, - }), - wrapper: (theme) => css` - position: relative; - display: flex; - justify-content: space-between; - align-items: center; - - ${theme.breakpoints.up("md")} { - justify-content: flex-start; - } - `, - drawerHeader: { - padding: 16, - paddingTop: 32, - paddingBottom: 32, - }, - logo: (theme) => css` - align-items: center; - display: flex; - height: ${navHeight}px; - color: ${theme.palette.text.primary}; - padding: 16px; - - // svg is for the Coder logo, img is for custom images - & svg, - & img { - height: 100%; - object-fit: contain; - } - `, - drawerLogo: { - padding: 0, - maxHeight: 40, - }, - link: (theme) => css` - align-items: center; - color: ${theme.palette.text.secondary}; - display: flex; - flex: 1; - font-size: 16px; - padding: 12px 16px; - text-decoration: none; - transition: background-color 0.15s ease-in-out; - - &.active { - color: ${theme.palette.text.primary}; - font-weight: 500; - } +interface NavItemsProps { + className?: string; +} - &:hover { - background-color: ${theme.experimental.l2.hover.background}; - } +const NavItems: FC = ({ className }) => { + const location = useLocation(); - ${theme.breakpoints.up("md")} { - height: ${navHeight}px; - padding: 0 24px; - } - `, -} satisfies Record>; + return ( + + ); +}; diff --git a/site/src/modules/dashboard/Navbar/ProxyMenu.tsx b/site/src/modules/dashboard/Navbar/ProxyMenu.tsx index 2d14ce3daee56..5345d3db9cdae 100644 --- a/site/src/modules/dashboard/Navbar/ProxyMenu.tsx +++ b/site/src/modules/dashboard/Navbar/ProxyMenu.tsx @@ -1,6 +1,4 @@ import { useTheme } from "@emotion/react"; -import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined"; -import Button from "@mui/material/Button"; import Divider from "@mui/material/Divider"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; @@ -8,13 +6,15 @@ 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 { displayError } from "components/GlobalSnackbar/utils"; import { Latency } from "components/Latency/Latency"; import type { ProxyContextValue } from "contexts/ProxyContext"; import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { ChevronDownIcon } from "lucide-react"; import { type FC, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { BUTTON_SM_HEIGHT } from "theme/constants"; +import { sortProxiesByLatency } from "./proxyUtils"; interface ProxyMenuProps { proxyContextValue: ProxyContextValue; @@ -62,7 +62,7 @@ export const ProxyMenu: FC = ({ proxyContextValue }) => { return ( ); @@ -71,13 +71,10 @@ export const ProxyMenu: FC = ({ proxyContextValue }) => { return ( <> = ({ proxyContextValue }) => { ]} {proxyContextValue.proxies && - [...proxyContextValue.proxies] - .sort((a, b) => { - const latencyA = - latencies?.[a.id]?.latencyMS ?? Number.POSITIVE_INFINITY; - const latencyB = - latencies?.[b.id]?.latencyMS ?? Number.POSITIVE_INFINITY; - return latencyA - latencyB; - }) - .map((proxy) => ( + sortProxiesByLatency(proxyContextValue.proxies, latencies).map( + (proxy) => ( = ({ proxyContextValue }) => { /> - ))} + ), + )} diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx index 6fb7428bb0dc1..6fc41fe7232ec 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx @@ -1,15 +1,12 @@ -import { type Interpolation, type Theme, css, useTheme } from "@emotion/react"; -import Badge from "@mui/material/Badge"; +import { useTheme } from "@emotion/react"; import type * as TypesGen from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; -import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { Popover, PopoverContent, PopoverTrigger, } from "components/deprecated/Popover/Popover"; import { type FC, useState } from "react"; -import { navHeight } from "theme/constants"; import { UserDropdownContent } from "./UserDropdownContent"; export interface UserDropdownProps { @@ -31,20 +28,11 @@ export const UserDropdown: FC = ({ return ( - @@ -68,24 +56,3 @@ export const UserDropdown: FC = ({ ); }; - -const styles = { - button: css` - background: none; - border: 0; - cursor: pointer; - height: ${navHeight}px; - padding: 12px 0; - - &:hover { - background-color: transparent; - } - `, - - badgeContainer: { - display: "flex", - alignItems: "center", - minWidth: 0, - maxWidth: 300, - }, -} satisfies Record>; diff --git a/site/src/modules/dashboard/Navbar/proxyUtils.tsx b/site/src/modules/dashboard/Navbar/proxyUtils.tsx new file mode 100644 index 0000000000000..57afadb7fbdd9 --- /dev/null +++ b/site/src/modules/dashboard/Navbar/proxyUtils.tsx @@ -0,0 +1,12 @@ +import type { Proxies, ProxyLatencies } from "contexts/ProxyContext"; + +export function sortProxiesByLatency( + proxies: Proxies, + latencies: ProxyLatencies, +) { + return proxies.toSorted((a, b) => { + const latencyA = latencies?.[a.id]?.latencyMS ?? Number.POSITIVE_INFINITY; + const latencyB = latencies?.[b.id]?.latencyMS ?? Number.POSITIVE_INFINITY; + return latencyA - latencyB; + }); +} diff --git a/site/src/modules/management/DeploymentSettingsLayout.tsx b/site/src/modules/management/DeploymentSettingsLayout.tsx index 2a0a999ac9f3d..65c2e70ea3333 100644 --- a/site/src/modules/management/DeploymentSettingsLayout.tsx +++ b/site/src/modules/management/DeploymentSettingsLayout.tsx @@ -39,7 +39,7 @@ const DeploymentSettingsLayout: FC = () => {
    -
    +
    diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index 484a6dd8a65e8..aa586e877d6e0 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -109,7 +109,7 @@ const OrganizationSettingsLayout: FC = () => {
    -
    +
    diff --git a/site/tailwind.config.js b/site/tailwind.config.js index 61d35ee0338aa..389ffb22fe96a 100644 --- a/site/tailwind.config.js +++ b/site/tailwind.config.js @@ -8,6 +8,11 @@ module.exports = { important: ["#root", "#storybook-root"], theme: { extend: { + size: { + "icon-lg": "1.5rem", + "icon-sm": "1.125rem", + "icon-xs": "0.875rem", + }, fontSize: { "2xs": ["0.625rem", "0.875rem"], sm: ["0.875rem", "1.5rem"],