From 3900600c91dd62981cc4e86fc7afd1e93efa3bb8 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 26 Dec 2024 15:33:37 +0000 Subject: [PATCH 01/13] Refactor navbar on desktop --- site/src/components/Button/Button.tsx | 35 ++- .../components/DropdownMenu/DropdownMenu.tsx | 24 +- .../dashboard/Navbar/DeploymentDropdown.tsx | 14 +- site/src/modules/dashboard/Navbar/Navbar.tsx | 1 + .../dashboard/Navbar/NavbarView.test.tsx | 4 + .../modules/dashboard/Navbar/NavbarView.tsx | 288 ++++++++---------- .../modules/dashboard/Navbar/ProxyMenu.tsx | 11 +- .../Navbar/UserDropdown/UserDropdown.tsx | 45 +-- 8 files changed, 179 insertions(+), 243 deletions(-) diff --git a/site/src/components/Button/Button.tsx b/site/src/components/Button/Button.tsx index b0a460eca8a3c..2d99d9d2cc6af 100644 --- a/site/src/components/Button/Button.tsx +++ b/site/src/components/Button/Button.tsx @@ -13,7 +13,8 @@ export const buttonVariants = cva( text-sm font-semibold font-medium cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link disabled:pointer-events-none disabled:text-content-disabled - [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`, + [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 + px-3 py-2`, { variants: { variant: { @@ -26,9 +27,11 @@ export const buttonVariants = cva( warning: "border border-border-error text-content-primary bg-surface-error hover:bg-transparent", }, + size: { - default: "h-9 px-3 py-2", - sm: "h-8 px-2 text-xs", + lg: "h-10", + default: "h-9", + sm: "h-8 px-2 py-1.5 text-xs", }, }, defaultVariants: { @@ -44,16 +47,16 @@ export interface ButtonProps asChild?: boolean; } -export const Button: FC = 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 ( + + ); + }, +); +Button.displayName = "Button"; diff --git a/site/src/components/DropdownMenu/DropdownMenu.tsx b/site/src/components/DropdownMenu/DropdownMenu.tsx index 8008ea9d6c27e..1075d5c65c9be 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"; @@ -214,3 +216,23 @@ export const DropdownMenuShortcut = ({ ); }; DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export interface DropdownMenuButtonProps + extends ComponentPropsWithoutRef {} + +export const DropdownMenuButton = forwardRef< + HTMLButtonElement, + DropdownMenuButtonProps +>(({ children, ...props }, ref) => { + return ( + + ); +}); +DropdownMenuButton.displayName = "DropdownMenuButton"; + +export const DropdownMenuChevronDown: FC = () => { + return ; +}; diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx index dc4b1b4d92fde..a85f1c7c2fec0 100644 --- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -2,6 +2,7 @@ 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 { DropdownMenuButton } from "components/DropdownMenu/DropdownMenu"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Popover, @@ -43,18 +44,7 @@ export const DeploymentDropdown: FC = ({ return ( - + Admin settings { canViewHealth={canViewHealth} canViewAuditLog={canViewAuditLog} proxyContextValue={proxyContextValue} + docsHref={appearance.docs_url} /> ); }; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx index 3dd4251385e20..826115eb6e552 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx @@ -25,6 +25,7 @@ describe("NavbarView", () => { it("workspaces nav link has the correct href", async () => { renderWithAuth( { it("templates nav link has the correct href", async () => { renderWithAuth( { it("audit nav link has the correct href", async () => { renderWithAuth( { it("deployment nav link has the correct href", async () => { renderWithAuth( void; @@ -27,46 +35,16 @@ export interface NavbarViewProps { 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 linkClassNames = { + 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, @@ -77,163 +55,137 @@ export const NavbarView: FC = ({ canViewAuditLog, proxyContextValue, }) => { - const theme = useTheme(); const [isDrawerOpen, setIsDrawerOpen] = useState(false); return ( - ); }; - -const styles = { - mobileMenuButton: (theme) => css` - ${theme.breakpoints.up("md")} { - display: none; - } - `, - - drawerHeader: { - padding: 16, - paddingTop: 32, - paddingBottom: 32, - }, - drawerLogo: { - padding: 0, - maxHeight: 40, - }, -} satisfies Record>; From 072a6d8fc76685f7600c5157dbb25e995a85c7b8 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 26 Dec 2024 20:44:38 +0000 Subject: [PATCH 04/13] Finish mobile menu --- .../components/DropdownMenu/DropdownMenu.tsx | 2 +- site/src/contexts/ProxyContext.tsx | 6 +- site/src/index.css | 6 + .../modules/dashboard/Navbar/NavbarView.tsx | 355 ++++++++++++++++-- .../modules/dashboard/Navbar/ProxyMenu.tsx | 15 +- .../modules/dashboard/Navbar/proxyUtils.tsx | 12 + 6 files changed, 345 insertions(+), 51 deletions(-) create mode 100644 site/src/modules/dashboard/Navbar/proxyUtils.tsx diff --git a/site/src/components/DropdownMenu/DropdownMenu.tsx b/site/src/components/DropdownMenu/DropdownMenu.tsx index 1075d5c65c9be..3410250bc6b31 100644 --- a/site/src/components/DropdownMenu/DropdownMenu.tsx +++ b/site/src/components/DropdownMenu/DropdownMenu.tsx @@ -198,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..f4fd77b7db4e5 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -70,4 +70,10 @@ * { @apply border-border; } + + /** Related to 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/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index d5672ccbc3bfa..f279ddfd04b26 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -2,22 +2,36 @@ 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 } from "react"; -import { NavLink, useLocation } from "react-router-dom"; +import { useState, type FC } from "react"; +import { Link, NavLink, useLocation } from "react-router-dom"; import { DeploymentDropdown } from "./DeploymentDropdown"; import { ProxyMenu } from "./ProxyMenu"; import { UserDropdown } from "./UserDropdown/UserDropdown"; import { cn } from "utils/cn"; import { Button } from "components/Button/Button"; -import { ChevronRightIcon, MenuIcon } from "lucide-react"; +import { + ChevronRightIcon, + CircleHelpIcon, + MenuIcon, + XIcon, +} from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "components/DropdownMenu/DropdownMenu"; import { Avatar } from "components/Avatar/Avatar"; import { Latency } from "components/Latency/Latency"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "components/Collapsible/Collapsible"; +import { sortProxiesByLatency } from "./proxyUtils"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; export interface NavbarViewProps { logo_url?: string; @@ -40,8 +54,11 @@ const linkClassNames = { active: "text-content-primary", }; -const mobileDropdownItemClassName = - "px-9 h-[60px] border-0 border-b border-solid"; +const mobileDropdownItemClassNames = { + default: "px-9 h-10 no-underline", + sub: "pl-12", + open: "text-content-primary", +}; export const NavbarView: FC = ({ user, @@ -101,7 +118,13 @@ export const NavbarView: FC = ({ )} - + ); }; @@ -109,58 +132,314 @@ export const NavbarView: FC = ({ type MobileMenuProps = { proxyContextValue?: ProxyContextValue; user?: TypesGen.User; + supportLinks?: readonly TypesGen.LinkConfig[]; + docsHref: string; + onSignOut: () => void; }; -const MobileMenu: FC = ({ proxyContextValue, user }) => { - const selectedProxy = proxyContextValue?.proxy.proxy; - const latency = selectedProxy - ? proxyContextValue?.proxyLatencies[selectedProxy?.id] - : undefined; +const MobileMenu: FC = ({ + proxyContextValue, + user, + supportLinks, + docsHref, + onSignOut, +}) => { + const [open, setOpen] = useState(false); return ( - + + {open && ( +
+ )} - - {selectedProxy && ( - - Workspace proxy settings: - - {selectedProxy.name} - {latency && } - - - - )} - + + + + + + + + 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 = () => { + const [open, setOpen] = useState(false); + + return ( + + + { + e.preventDefault(); + setOpen((prev) => !prev); + }} + > Admin settings - + - - Docs + + + + Deployment + + + + Organizations + + + + + Audit logs - + + 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/ProxyMenu.tsx b/site/src/modules/dashboard/Navbar/ProxyMenu.tsx index bb0a303e878d7..d04bd483407b0 100644 --- a/site/src/modules/dashboard/Navbar/ProxyMenu.tsx +++ b/site/src/modules/dashboard/Navbar/ProxyMenu.tsx @@ -13,6 +13,7 @@ import type { ProxyContextValue } from "contexts/ProxyContext"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { type FC, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { sortProxiesByLatency } from "./proxyUtils"; interface ProxyMenuProps { proxyContextValue: ProxyContextValue; @@ -163,15 +164,8 @@ export const ProxyMenu: FC = ({ 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/proxyUtils.tsx b/site/src/modules/dashboard/Navbar/proxyUtils.tsx new file mode 100644 index 0000000000000..674c62ef38f1e --- /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].sort((a, b) => { + const latencyA = latencies?.[a.id]?.latencyMS ?? Number.POSITIVE_INFINITY; + const latencyB = latencies?.[b.id]?.latencyMS ?? Number.POSITIVE_INFINITY; + return latencyA - latencyB; + }); +} From 2a793169d45832ac6ba068f63eda2b4f9874d889 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 27 Dec 2024 13:26:57 +0000 Subject: [PATCH 05/13] Add icon sizes --- site/src/components/DropdownMenu/DropdownMenu.tsx | 2 +- site/tailwind.config.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/site/src/components/DropdownMenu/DropdownMenu.tsx b/site/src/components/DropdownMenu/DropdownMenu.tsx index 3410250bc6b31..ab3866bd4e951 100644 --- a/site/src/components/DropdownMenu/DropdownMenu.tsx +++ b/site/src/components/DropdownMenu/DropdownMenu.tsx @@ -234,5 +234,5 @@ export const DropdownMenuButton = forwardRef< DropdownMenuButton.displayName = "DropdownMenuButton"; export const DropdownMenuChevronDown: FC = () => { - return ; + return ; }; 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"], From 1b0c075aee32d66a9de4059b7ac9cb67e17d6714 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 27 Dec 2024 13:29:42 +0000 Subject: [PATCH 06/13] Adjust icon sizes --- site/src/modules/dashboard/Navbar/NavbarView.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index f279ddfd04b26..576e00ef3449d 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -158,7 +158,11 @@ const MobileMenu: FC = ({ variant="ghost" className="ml-auto md:hidden [&_svg]:size-6" > - {open ? : } + {open ? ( + + ) : ( + + )} = ({ Account Date: Fri, 27 Dec 2024 13:37:30 +0000 Subject: [PATCH 07/13] Extract MobileMenu --- .../modules/dashboard/Navbar/MobileMenu.tsx | 310 +++++++++++++++ .../modules/dashboard/Navbar/NavbarView.tsx | 366 +----------------- 2 files changed, 317 insertions(+), 359 deletions(-) create mode 100644 site/src/modules/dashboard/Navbar/MobileMenu.tsx diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.tsx new file mode 100644 index 0000000000000..02735bd0ebd73 --- /dev/null +++ b/site/src/modules/dashboard/Navbar/MobileMenu.tsx @@ -0,0 +1,310 @@ +import type * as TypesGen from "api/typesGenerated"; +import type { ProxyContextValue } from "contexts/ProxyContext"; +import { useState, type FC } from "react"; +import { Link } from "react-router-dom"; +import { cn } from "utils/cn"; +import { Button } from "components/Button/Button"; +import { + ChevronRightIcon, + CircleHelpIcon, + MenuIcon, + XIcon, +} from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; +import { Avatar } from "components/Avatar/Avatar"; +import { Latency } from "components/Latency/Latency"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "components/Collapsible/Collapsible"; +import { sortProxiesByLatency } from "./proxyUtils"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; + +const itemStyles = { + default: "px-9 h-10 no-underline", + sub: "pl-12", + open: "text-content-primary", +}; + +type MobileMenuProps = { + proxyContextValue?: ProxyContextValue; + user?: TypesGen.User; + supportLinks?: readonly TypesGen.LinkConfig[]; + docsHref: string; + onSignOut: () => void; +}; + +export const MobileMenu: FC = ({ + proxyContextValue, + user, + supportLinks, + docsHref, + onSignOut, +}) => { + const [open, setOpen] = useState(false); + + return ( + + {open && ( +
+ )} + + + + + + + + + + + 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 = () => { + const [open, setOpen] = useState(false); + + return ( + + + { + e.preventDefault(); + setOpen((prev) => !prev); + }} + > + Admin settings + + + + + + Deployment + + + + Organizations + + + + + Audit logs + + + 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/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 576e00ef3449d..47d0012f67795 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -2,36 +2,13 @@ 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 { useState, type FC } from "react"; -import { Link, NavLink, useLocation } from "react-router-dom"; +import type { FC } from "react"; +import { NavLink, useLocation } from "react-router-dom"; import { DeploymentDropdown } from "./DeploymentDropdown"; import { ProxyMenu } from "./ProxyMenu"; import { UserDropdown } from "./UserDropdown/UserDropdown"; import { cn } from "utils/cn"; -import { Button } from "components/Button/Button"; -import { - ChevronRightIcon, - CircleHelpIcon, - MenuIcon, - XIcon, -} from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "components/DropdownMenu/DropdownMenu"; -import { Avatar } from "components/Avatar/Avatar"; -import { Latency } from "components/Latency/Latency"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "components/Collapsible/Collapsible"; -import { sortProxiesByLatency } from "./proxyUtils"; -import { displayError } from "components/GlobalSnackbar/utils"; -import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; +import { MobileMenu } from "./MobileMenu"; export interface NavbarViewProps { logo_url?: string; @@ -48,18 +25,12 @@ export interface NavbarViewProps { proxyContextValue?: ProxyContextValue; } -const linkClassNames = { +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", }; -const mobileDropdownItemClassNames = { - default: "px-9 h-10 no-underline", - sub: "pl-12", - open: "text-content-primary", -}; - export const NavbarView: FC = ({ user, logo_url, @@ -100,7 +71,7 @@ export const NavbarView: FC = ({ /> = ({ ); }; -type MobileMenuProps = { - proxyContextValue?: ProxyContextValue; - user?: TypesGen.User; - supportLinks?: readonly TypesGen.LinkConfig[]; - docsHref: string; - onSignOut: () => void; -}; - -const MobileMenu: FC = ({ - proxyContextValue, - user, - supportLinks, - docsHref, - onSignOut, -}) => { - const [open, setOpen] = useState(false); - - return ( - - {open && ( -
- )} - - - - - - - - - - - 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 = () => { - const [open, setOpen] = useState(false); - - return ( - - - { - e.preventDefault(); - setOpen((prev) => !prev); - }} - > - Admin settings - - - - - - Deployment - - - - Organizations - - - - - Audit logs - - - 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} - - - ))} - - )} - - - ); -}; - interface NavItemsProps { className?: string; } @@ -460,10 +114,7 @@ const NavItems: FC = ({ className }) => { if (location.pathname.startsWith("/@")) { isActive = true; } - return cn( - linkClassNames.default, - isActive ? linkClassNames.active : "", - ); + return cn(linkStyles.default, isActive ? linkStyles.active : ""); }} to="/workspaces" > @@ -471,10 +122,7 @@ const NavItems: FC = ({ className }) => { { - return cn( - linkClassNames.default, - isActive ? linkClassNames.active : "", - ); + return cn(linkStyles.default, isActive ? linkStyles.active : ""); }} to="/templates" > From 7f9a95d5ffc606acff71cb0a1b6c804a35de5887 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 27 Dec 2024 14:54:11 +0000 Subject: [PATCH 08/13] Add stories for mobile menu --- site/.storybook/preview.jsx | 8 + site/src/components/Button/Button.tsx | 3 +- .../dashboard/Navbar/DeploymentDropdown.tsx | 6 - .../dashboard/Navbar/MobileMenu.stories.tsx | 146 ++++++++++++++++++ .../modules/dashboard/Navbar/MobileMenu.tsx | 101 +++++++----- site/src/modules/dashboard/Navbar/Navbar.tsx | 2 - .../dashboard/Navbar/NavbarView.stories.tsx | 4 - .../modules/dashboard/Navbar/NavbarView.tsx | 7 +- 8 files changed, 225 insertions(+), 52 deletions(-) create mode 100644 site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx 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/Button/Button.tsx b/site/src/components/Button/Button.tsx index a3ab448164968..133e8ca8aa294 100644 --- a/site/src/components/Button/Button.tsx +++ b/site/src/components/Button/Button.tsx @@ -26,7 +26,8 @@ export const buttonVariants = cva( "border-none bg-transparent text-content-secondary hover:text-content-primary", warning: "border border-border-error text-content-primary bg-surface-error hover:bg-transparent", - ghost: "bg-transparent border-0 hover:bg-surface-secondary", + ghost: + "text-content-primary bg-transparent border-0 hover:bg-surface-secondary", }, size: { diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx index a85f1c7c2fec0..6bbc2489a4ae5 100644 --- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -1,7 +1,5 @@ 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 { DropdownMenuButton } from "components/DropdownMenu/DropdownMenu"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { @@ -17,7 +15,6 @@ import { NavLink } from "react-router-dom"; interface DeploymentDropdownProps { canViewDeployment: boolean; canViewOrganizations: boolean; - canViewAllUsers: boolean; canViewAuditLog: boolean; canViewHealth: boolean; } @@ -25,7 +22,6 @@ interface DeploymentDropdownProps { export const DeploymentDropdown: FC = ({ canViewDeployment, canViewOrganizations, - canViewAllUsers, canViewAuditLog, canViewHealth, }) => { @@ -35,7 +31,6 @@ export const DeploymentDropdown: FC = ({ !canViewAuditLog && !canViewOrganizations && !canViewDeployment && - !canViewAllUsers && !canViewHealth ) { return null; @@ -60,7 +55,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..ff696b6b17718 --- /dev/null +++ b/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx @@ -0,0 +1,146 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { chromaticWithTablet } from "testHelpers/chromatic"; +import { MobileMenu } from "./MobileMenu"; +import { + MockPrimaryWorkspaceProxy, + MockProxyLatencies, + MockSupportLinks, + MockUser, + MockUser2, + MockWorkspaceProxies, +} from "testHelpers/entities"; +import { fn, userEvent, within } from "@storybook/test"; +import { PointerEventsCheckLevel } from "@testing-library/user-event"; +import type { FC } from "react"; + +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 index 02735bd0ebd73..0d0c7b463164d 100644 --- a/site/src/modules/dashboard/Navbar/MobileMenu.tsx +++ b/site/src/modules/dashboard/Navbar/MobileMenu.tsx @@ -34,22 +34,33 @@ const itemStyles = { open: "text-content-primary", }; -type MobileMenuProps = { +type MobileMenuProps = MobileMenuPermissions & { proxyContextValue?: ProxyContextValue; user?: TypesGen.User; supportLinks?: readonly TypesGen.LinkConfig[]; docsHref: string; onSignOut: () => void; + isDefaultOpen?: boolean; // Useful for storybook +}; + +type MobileMenuPermissions = { + canViewDeployment: boolean; + canViewOrganizations: boolean; + canViewAuditLog: boolean; + canViewHealth: boolean; }; export const MobileMenu: FC = ({ + isDefaultOpen, proxyContextValue, user, supportLinks, docsHref, onSignOut, + ...permissions }) => { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(isDefaultOpen); + const hasSomePermission = Object.values(permissions).some((p) => p); return ( @@ -75,8 +86,13 @@ export const MobileMenu: FC = ({ sideOffset={17} > - - + + {hasSomePermission && ( + <> + + + + )} @@ -186,7 +202,12 @@ const ProxySettingsSub: FC = ({ proxyContextValue }) => { ); }; -const AdminSettingsSub: FC = () => { +const AdminSettingsSub: FC = ({ + canViewDeployment, + canViewOrganizations, + canViewAuditLog, + canViewHealth, +}) => { const [open, setOpen] = useState(false); return ( @@ -206,37 +227,45 @@ const AdminSettingsSub: FC = () => { - - Deployment - - - - Organizations - - - - - Audit logs - - - Healthcheck - + {canViewDeployment && ( + + Deployment + + )} + {canViewOrganizations && ( + + + Organizations + + + + )} + {canViewAuditLog && ( + + Audit logs + + )} + {canViewHealth && ( + + Healthcheck + + )} ); diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index 426eb43fefe77..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,7 +32,6 @@ export const Navbar: FC = () => { onSignOut={signOut} canViewDeployment={canViewDeployment} canViewOrganizations={canViewOrganizations} - canViewAllUsers={canViewAllUsers} canViewHealth={canViewHealth} canViewAuditLog={canViewAuditLog} proxyContextValue={proxyContextValue} 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.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 47d0012f67795..83dc4df8aa956 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -19,7 +19,6 @@ export interface NavbarViewProps { onSignOut: () => void; canViewDeployment: boolean; canViewOrganizations: boolean; - canViewAllUsers: boolean; canViewAuditLog: boolean; canViewHealth: boolean; proxyContextValue?: ProxyContextValue; @@ -40,7 +39,6 @@ export const NavbarView: FC = ({ onSignOut, canViewDeployment, canViewOrganizations, - canViewAllUsers, canViewHealth, canViewAuditLog, proxyContextValue, @@ -66,7 +64,6 @@ export const NavbarView: FC = ({ canViewAuditLog={canViewAuditLog} canViewOrganizations={canViewOrganizations} canViewDeployment={canViewDeployment} - canViewAllUsers={canViewAllUsers} canViewHealth={canViewHealth} /> @@ -95,6 +92,10 @@ export const NavbarView: FC = ({ supportLinks={supportLinks} docsHref={docsHref} onSignOut={onSignOut} + canViewAuditLog={canViewAuditLog} + canViewOrganizations={canViewOrganizations} + canViewDeployment={canViewDeployment} + canViewHealth={canViewHealth} />
); From 7e11d46c2d300c03c8aea9ec7de424aa36e814dd Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 27 Dec 2024 14:56:41 +0000 Subject: [PATCH 09/13] Fix fmt and lint --- site/src/index.css | 6 ++-- .../dashboard/Navbar/MobileMenu.stories.tsx | 8 ++--- .../modules/dashboard/Navbar/MobileMenu.tsx | 32 +++++++++---------- .../dashboard/Navbar/NavbarView.test.tsx | 4 --- .../modules/dashboard/Navbar/NavbarView.tsx | 4 +-- 5 files changed, 25 insertions(+), 29 deletions(-) diff --git a/site/src/index.css b/site/src/index.css index f4fd77b7db4e5..187906b995c2f 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -73,7 +73,7 @@ /** Related to https://github.com/radix-ui/primitives/issues/3251 */ html body[data-scroll-locked] { - --removed-body-scroll-bar-size: 0 !important; - margin-right: 0 !important; - } + --removed-body-scroll-bar-size: 0 !important; + margin-right: 0 !important; + } } diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx index ff696b6b17718..19c66c14b38a7 100644 --- a/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx +++ b/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx @@ -1,6 +1,8 @@ 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 { MobileMenu } from "./MobileMenu"; import { MockPrimaryWorkspaceProxy, MockProxyLatencies, @@ -9,9 +11,7 @@ import { MockUser2, MockWorkspaceProxies, } from "testHelpers/entities"; -import { fn, userEvent, within } from "@storybook/test"; -import { PointerEventsCheckLevel } from "@testing-library/user-event"; -import type { FC } from "react"; +import { MobileMenu } from "./MobileMenu"; const meta: Meta = { title: "modules/dashboard/MobileMenu", diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.tsx index 0d0c7b463164d..a0368d76ac3ec 100644 --- a/site/src/modules/dashboard/Navbar/MobileMenu.tsx +++ b/site/src/modules/dashboard/Navbar/MobileMenu.tsx @@ -1,15 +1,11 @@ import type * as TypesGen from "api/typesGenerated"; -import type { ProxyContextValue } from "contexts/ProxyContext"; -import { useState, type FC } from "react"; -import { Link } from "react-router-dom"; -import { cn } from "utils/cn"; +import { Avatar } from "components/Avatar/Avatar"; import { Button } from "components/Button/Button"; import { - ChevronRightIcon, - CircleHelpIcon, - MenuIcon, - XIcon, -} from "lucide-react"; + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "components/Collapsible/Collapsible"; import { DropdownMenu, DropdownMenuContent, @@ -17,16 +13,20 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "components/DropdownMenu/DropdownMenu"; -import { Avatar } from "components/Avatar/Avatar"; +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 { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "components/Collapsible/Collapsible"; + 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"; -import { displayError } from "components/GlobalSnackbar/utils"; -import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; const itemStyles = { default: "px-9 h-10 no-underline", diff --git a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx index ec0962a756f00..979fe264040d3 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx @@ -31,7 +31,6 @@ describe("NavbarView", () => { onSignOut={noop} canViewDeployment canViewOrganizations - canViewAllUsers canViewHealth canViewAuditLog />, @@ -49,7 +48,6 @@ describe("NavbarView", () => { onSignOut={noop} canViewDeployment canViewOrganizations - canViewAllUsers canViewHealth canViewAuditLog />, @@ -67,7 +65,6 @@ describe("NavbarView", () => { onSignOut={noop} canViewDeployment canViewOrganizations - canViewAllUsers canViewHealth canViewAuditLog />, @@ -87,7 +84,6 @@ describe("NavbarView", () => { onSignOut={noop} canViewDeployment canViewOrganizations - canViewAllUsers canViewHealth canViewAuditLog />, diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 83dc4df8aa956..ec3a1c690bb60 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -4,11 +4,11 @@ import { CoderIcon } from "components/Icons/CoderIcon"; import type { ProxyContextValue } from "contexts/ProxyContext"; import type { FC } from "react"; import { NavLink, useLocation } from "react-router-dom"; +import { cn } from "utils/cn"; import { DeploymentDropdown } from "./DeploymentDropdown"; +import { MobileMenu } from "./MobileMenu"; import { ProxyMenu } from "./ProxyMenu"; import { UserDropdown } from "./UserDropdown/UserDropdown"; -import { cn } from "utils/cn"; -import { MobileMenu } from "./MobileMenu"; export interface NavbarViewProps { logo_url?: string; From 07ce6775879343d3177ebf0cc9d4be04ec255bb9 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 30 Dec 2024 13:20:41 +0000 Subject: [PATCH 10/13] Apply PR review suggestions --- site/src/components/Button/Button.tsx | 3 +-- site/src/index.css | 9 +++++++- .../modules/dashboard/Navbar/MobileMenu.tsx | 16 +++++++------- .../modules/dashboard/Navbar/Navbar.test.tsx | 14 +++---------- .../dashboard/Navbar/NavbarView.test.tsx | 21 ++++++++++--------- .../modules/dashboard/Navbar/proxyUtils.tsx | 2 +- 6 files changed, 32 insertions(+), 33 deletions(-) diff --git a/site/src/components/Button/Button.tsx b/site/src/components/Button/Button.tsx index 133e8ca8aa294..4eeada4696b5a 100644 --- a/site/src/components/Button/Button.tsx +++ b/site/src/components/Button/Button.tsx @@ -4,7 +4,7 @@ */ import { Slot } from "@radix-ui/react-slot"; import { type VariantProps, cva } from "class-variance-authority"; -import { type FC, forwardRef } from "react"; +import { forwardRef } from "react"; import { cn } from "utils/cn"; export const buttonVariants = cva( @@ -62,4 +62,3 @@ export const Button = forwardRef( ); }, ); -Button.displayName = "Button"; diff --git a/site/src/index.css b/site/src/index.css index 187906b995c2f..5f690b5616bca 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -71,7 +71,14 @@ @apply border-border; } - /** Related to https://github.com/radix-ui/primitives/issues/3251 */ + /* + 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/MobileMenu.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.tsx index a0368d76ac3ec..e04bb7328d78c 100644 --- a/site/src/modules/dashboard/Navbar/MobileMenu.tsx +++ b/site/src/modules/dashboard/Navbar/MobileMenu.tsx @@ -34,6 +34,13 @@ const itemStyles = { open: "text-content-primary", }; +type MobileMenuPermissions = { + canViewDeployment: boolean; + canViewOrganizations: boolean; + canViewAuditLog: boolean; + canViewHealth: boolean; +}; + type MobileMenuProps = MobileMenuPermissions & { proxyContextValue?: ProxyContextValue; user?: TypesGen.User; @@ -43,13 +50,6 @@ type MobileMenuProps = MobileMenuPermissions & { isDefaultOpen?: boolean; // Useful for storybook }; -type MobileMenuPermissions = { - canViewDeployment: boolean; - canViewOrganizations: boolean; - canViewAuditLog: boolean; - canViewHealth: boolean; -}; - export const MobileMenu: FC = ({ isDefaultOpen, proxyContextValue, @@ -136,7 +136,7 @@ const ProxySettingsSub: FC = ({ proxyContextValue }) => { }} > Workspace proxy settings: - + { render(); const deploymentMenu = await screen.findByText("Admin settings"); await userEvent.click(deploymentMenu); - await waitFor( - () => { - const link = screen.getByText("Audit Logs"); - expect(link).toBeDefined(); - }, - { timeout: 2000 }, - ); + await screen.findByText("Audit Logs"); }); it("does not show Audit Log link when not entitled", async () => { @@ -40,8 +34,7 @@ describe("Navbar", () => { await userEvent.click(deploymentMenu); await waitFor( () => { - const link = screen.queryByText("Audit Logs"); - expect(link).toBe(null); + expect(screen.queryByText("Audit Logs")).not.toBeInTheDocument(); }, { timeout: 2000 }, ); @@ -63,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/NavbarView.test.tsx b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx index 979fe264040d3..7b51561ddea5a 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx @@ -35,8 +35,9 @@ describe("NavbarView", () => { canViewAuditLog />, ); - const workspacesLink = await screen.findByText("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 () => { @@ -52,8 +53,9 @@ describe("NavbarView", () => { canViewAuditLog />, ); - const templatesLink = await screen.findByText("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 () => { @@ -71,8 +73,8 @@ describe("NavbarView", () => { ); const deploymentMenu = await screen.findByText("Admin settings"); await userEvent.click(deploymentMenu); - const auditLink = await screen.findByText("Audit Logs"); - 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 () => { @@ -90,9 +92,8 @@ describe("NavbarView", () => { ); const deploymentMenu = await screen.findByText("Admin settings"); await userEvent.click(deploymentMenu); - const deploymentSettingsLink = await screen.findByText("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/proxyUtils.tsx b/site/src/modules/dashboard/Navbar/proxyUtils.tsx index 674c62ef38f1e..57afadb7fbdd9 100644 --- a/site/src/modules/dashboard/Navbar/proxyUtils.tsx +++ b/site/src/modules/dashboard/Navbar/proxyUtils.tsx @@ -4,7 +4,7 @@ export function sortProxiesByLatency( proxies: Proxies, latencies: ProxyLatencies, ) { - return [...proxies].sort((a, b) => { + 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; From 0699cb4080008465937ab37950d1388d524280ba Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 30 Dec 2024 13:29:49 +0000 Subject: [PATCH 11/13] Align breadcrumb, deployment and org settings layouts to navbar. --- site/src/components/Breadcrumb/Breadcrumb.tsx | 2 +- site/src/modules/management/DeploymentSettingsLayout.tsx | 2 +- site/src/modules/management/OrganizationSettingsLayout.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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<
    {
    -
    +
    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 = () => {
    -
    +
    From 283080179ce1cdae76f08e1481fc4127ad0b1f47 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 30 Dec 2024 17:05:35 +0000 Subject: [PATCH 12/13] Remove DropdownMenuComponent --- .../components/DropdownMenu/DropdownMenu.tsx | 20 ------------------- .../dashboard/Navbar/DeploymentDropdown.tsx | 8 ++++++-- .../modules/dashboard/Navbar/ProxyMenu.tsx | 10 +++++++--- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/site/src/components/DropdownMenu/DropdownMenu.tsx b/site/src/components/DropdownMenu/DropdownMenu.tsx index ab3866bd4e951..c924317b20f87 100644 --- a/site/src/components/DropdownMenu/DropdownMenu.tsx +++ b/site/src/components/DropdownMenu/DropdownMenu.tsx @@ -216,23 +216,3 @@ export const DropdownMenuShortcut = ({ ); }; DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; - -export interface DropdownMenuButtonProps - extends ComponentPropsWithoutRef {} - -export const DropdownMenuButton = forwardRef< - HTMLButtonElement, - DropdownMenuButtonProps ->(({ children, ...props }, ref) => { - return ( - - ); -}); -DropdownMenuButton.displayName = "DropdownMenuButton"; - -export const DropdownMenuChevronDown: FC = () => { - return ; -}; diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx index 6bbc2489a4ae5..d1a75c02cd315 100644 --- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -1,6 +1,6 @@ import { type Interpolation, type Theme, css, useTheme } from "@emotion/react"; import MenuItem from "@mui/material/MenuItem"; -import { DropdownMenuButton } from "components/DropdownMenu/DropdownMenu"; +import { Button } from "components/Button/Button"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Popover, @@ -8,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"; @@ -39,7 +40,10 @@ export const DeploymentDropdown: FC = ({ return ( - Admin settings + = ({ proxyContextValue }) => { return ( <> - setIsOpen(true)} size="lg" @@ -102,7 +104,9 @@ export const ProxyMenu: FC = ({ proxyContextValue }) => { ) : ( "Select Proxy" )} - + + + Date: Mon, 30 Dec 2024 17:12:10 +0000 Subject: [PATCH 13/13] Run make fmt --- site/src/modules/dashboard/Navbar/ProxyMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/modules/dashboard/Navbar/ProxyMenu.tsx b/site/src/modules/dashboard/Navbar/ProxyMenu.tsx index 3954d63e4eda3..5345d3db9cdae 100644 --- a/site/src/modules/dashboard/Navbar/ProxyMenu.tsx +++ b/site/src/modules/dashboard/Navbar/ProxyMenu.tsx @@ -6,15 +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 { sortProxiesByLatency } from "./proxyUtils"; -import { Button } from "components/Button/Button"; -import { ChevronDownIcon } from "lucide-react"; interface ProxyMenuProps { proxyContextValue: ProxyContextValue;