From 7c5a7503e592a26e1e6c6fe93775bf414a7181c5 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 29 May 2024 21:11:16 +0000 Subject: [PATCH 1/5] feat(site): add deployment menu to navbar --- .../dashboard/Navbar/DeploymentDropdown.tsx | 137 ++++++++++++++++++ .../modules/dashboard/Navbar/NavbarView.tsx | 66 ++------- .../UserDropdown/UserDropdownContent.tsx | 110 +++++++------- site/src/modules/navigation.ts | 7 + site/src/pages/UsersPage/UsersLayout.tsx | 2 +- 5 files changed, 215 insertions(+), 107 deletions(-) create mode 100644 site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx create mode 100644 site/src/modules/navigation.ts diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx new file mode 100644 index 0000000000000..e734c693f39ca --- /dev/null +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -0,0 +1,137 @@ +import { css, type Interpolation, type Theme, useTheme } from "@emotion/react"; +import Button from "@mui/material/Button"; +import MenuItem from "@mui/material/MenuItem"; +import { type FC } from "react"; +import { NavLink } from "react-router-dom"; +import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; +import { + Popover, + PopoverContent, + PopoverTrigger, + usePopover, +} from "components/Popover/Popover"; + +import { USERS_LINK } from "modules/navigation"; + +interface DeploymentDropdownProps { + canViewAuditLog: boolean; + canViewDeployment: boolean; + canViewAllUsers: boolean; + canViewHealth: boolean; +} + +export const DeploymentDropdown: FC = ({ + canViewAuditLog, + canViewDeployment, + canViewAllUsers, + canViewHealth, +}) => { + const theme = useTheme(); + + if ( + !canViewAuditLog && + !canViewDeployment && + !canViewAllUsers && + !canViewHealth + ) { + return null; + } + + return ( + + + + + + + + + + ); +}; + +const DeploymentDropdownContent: FC = ({ + canViewAuditLog, + canViewDeployment, + canViewAllUsers, + canViewHealth, +}) => { + const popover = usePopover(); + + const onPopoverClose = () => popover.setIsOpen(false); + + return ( + + ); +}; + +const styles = { + link: { + textDecoration: "none", + color: "inherit", + }, + menuItem: (theme) => css` + gap: 20px; + padding: 8px 20px; + font-size: 14px; + + &:hover { + background-color: ${theme.palette.action.hover}; + transition: background-color 0.3s ease; + } + `, + menuItemIcon: (theme) => ({ + color: theme.palette.text.secondary, + width: 20, + height: 20, + }), +} satisfies Record>; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 440bdd44f249d..d11f2ee17b689 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -9,10 +9,11 @@ import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import Skeleton from "@mui/material/Skeleton"; import { visuallyHidden } from "@mui/utils"; -import { type FC, type ReactNode, useRef, useState } from "react"; +import { type FC, useRef, useState } from "react"; import { NavLink, useLocation, useNavigate } from "react-router-dom"; import type * as TypesGen from "api/typesGenerated"; import { Abbr } from "components/Abbr/Abbr"; +import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { displayError } from "components/GlobalSnackbar/utils"; import { CoderIcon } from "components/Icons/CoderIcon"; @@ -21,10 +22,7 @@ import { useAuthenticated } from "contexts/auth/RequireAuth"; import type { ProxyContextValue } from "contexts/ProxyContext"; import { BUTTON_SM_HEIGHT, navHeight } from "theme/constants"; import { UserDropdown } from "./UserDropdown/UserDropdown"; - -export const USERS_LINK = `/users?filter=${encodeURIComponent( - "status:active", -)}`; +import { DeploymentDropdown } from "./DeploymentDropdown"; export interface NavbarViewProps { logo_url?: string; @@ -44,25 +42,14 @@ export const Language = { templates: "Templates", users: "Users", audit: "Audit", - deployment: "Deployment", + deployment: "Settings", }; interface NavItemsProps { - children?: ReactNode; className?: string; - canViewAuditLog: boolean; - canViewDeployment: boolean; - canViewAllUsers: boolean; - canViewHealth: boolean; } -const NavItems: FC = ({ - className, - canViewAuditLog, - canViewDeployment, - canViewAllUsers, - canViewHealth, -}) => { +const NavItems: FC = ({ className }) => { const location = useLocation(); const theme = useTheme(); @@ -83,26 +70,6 @@ const NavItems: FC = ({ {Language.templates} - {canViewAllUsers && ( - - {Language.users} - - )} - {canViewAuditLog && ( - - {Language.audit} - - )} - {canViewDeployment && ( - - {Language.deployment} - - )} - {canViewHealth && ( - - Health - - )} ); }; @@ -157,12 +124,7 @@ export const NavbarView: FC = ({ )} - + @@ -174,18 +136,20 @@ export const NavbarView: FC = ({ )} - +
{proxyContextValue && ( )} + + + {user && ( [ - theme.typography.body2 as CSSObject, - { - padding: 20, - }, - ], - userName: { - fontWeight: 600, - }, - userEmail: (theme) => ({ - color: theme.palette.text.secondary, - width: "100%", - textOverflow: "ellipsis", - overflow: "hidden", - }), - link: { - textDecoration: "none", - color: "inherit", - }, - menuItem: (theme) => css` - gap: 20px; - padding: 8px 20px; - - &:hover { - background-color: ${theme.palette.action.hover}; - transition: background-color 0.3s ease; - } - `, - menuItemIcon: (theme) => ({ - color: theme.palette.text.secondary, - width: 20, - height: 20, - }), - menuItemText: { - fontSize: 14, - }, - footerText: (theme) => css` - font-size: 12px; - text-decoration: none; - color: ${theme.palette.text.secondary}; - display: flex; - align-items: center; - gap: 4px; - - & svg { - width: 12px; - height: 12px; - } - `, - buildInfo: (theme) => ({ - color: theme.palette.text.primary, - }), -} satisfies Record>; - export interface UserDropdownContentProps { user: TypesGen.User; organizations?: TypesGen.Organization[]; @@ -269,3 +214,58 @@ const includeBuildInfo = ( )}`, ); }; + +const styles = { + info: (theme) => [ + theme.typography.body2 as CSSObject, + { + padding: 20, + }, + ], + userName: { + fontWeight: 600, + }, + userEmail: (theme) => ({ + color: theme.palette.text.secondary, + width: "100%", + textOverflow: "ellipsis", + overflow: "hidden", + }), + link: { + textDecoration: "none", + color: "inherit", + }, + menuItem: (theme) => css` + gap: 20px; + padding: 8px 20px; + + &:hover { + background-color: ${theme.palette.action.hover}; + transition: background-color 0.3s ease; + } + `, + menuItemIcon: (theme) => ({ + color: theme.palette.text.secondary, + width: 20, + height: 20, + }), + menuItemText: { + fontSize: 14, + }, + footerText: (theme) => css` + font-size: 12px; + text-decoration: none; + color: ${theme.palette.text.secondary}; + display: flex; + align-items: center; + gap: 4px; + + & svg { + width: 12px; + height: 12px; + } + `, + buildInfo: (theme) => ({ + color: theme.palette.text.primary, + }), +} satisfies Record>; diff --git a/site/src/modules/navigation.ts b/site/src/modules/navigation.ts new file mode 100644 index 0000000000000..74217a4ceaaac --- /dev/null +++ b/site/src/modules/navigation.ts @@ -0,0 +1,7 @@ +/** + * @fileoverview TODO: centralize navigation code here! URL constants, URL formatting, all of it + */ + +export const USERS_LINK = `/users?filter=${encodeURIComponent( + "status:active", +)}`; diff --git a/site/src/pages/UsersPage/UsersLayout.tsx b/site/src/pages/UsersPage/UsersLayout.tsx index bb85cae1b03b8..891ac5fcae4b5 100644 --- a/site/src/pages/UsersPage/UsersLayout.tsx +++ b/site/src/pages/UsersPage/UsersLayout.tsx @@ -13,7 +13,7 @@ import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { TAB_PADDING_Y, TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { USERS_LINK } from "modules/dashboard/Navbar/NavbarView"; +import { USERS_LINK } from "modules/navigation"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; export const UsersLayout: FC = () => { From b081a12ad04b30a975aa8cb565a9cade41eaf456 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 3 Jun 2024 19:46:27 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=92=87=E2=80=8D=E2=99=80=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/Navbar/DeploymentDropdown.tsx | 73 +++++++++++-------- .../dashboard/Navbar/NavbarView.stories.tsx | 6 ++ .../modules/dashboard/Navbar/NavbarView.tsx | 1 - 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx index e734c693f39ca..f315de1b6f524 100644 --- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -40,12 +40,17 @@ export const DeploymentDropdown: FC = ({ return ( - @@ -83,43 +88,53 @@ const DeploymentDropdownContent: FC = ({ return ( ); }; const styles = { - link: { - textDecoration: "none", - color: "inherit", - }, menuItem: (theme) => css` + text-decoration: none; + color: inherit; gap: 20px; padding: 8px 20px; font-size: 14px; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx index 2490234bd36e1..fa34aed723331 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx @@ -3,6 +3,7 @@ import { chromaticWithTablet } from "testHelpers/chromatic"; import { MockUser, MockUser2 } from "testHelpers/entities"; import { withDashboardProvider } from "testHelpers/storybook"; import { NavbarView } from "./NavbarView"; +import { action } from "@storybook/addon-actions"; const meta: Meta = { title: "modules/dashboard/NavbarView", @@ -10,6 +11,10 @@ const meta: Meta = { component: NavbarView, args: { user: MockUser, + canViewAuditLog: true, + canViewDeployment: true, + canViewAllUsers: true, + canViewHealth: true, }, decorators: [withDashboardProvider], }; @@ -25,6 +30,7 @@ export const ForMember: Story = { canViewAuditLog: false, canViewDeployment: false, canViewAllUsers: false, + canViewHealth: false, }, }; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index d11f2ee17b689..8c1d080a1687e 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -224,7 +224,6 @@ const ProxyMenu: FC = ({ proxyContextValue }) => { size="small" endIcon={} css={{ - borderRadius: "999px", "& .MuiSvgIcon-root": { fontSize: 14 }, }} > From 9104fcef17610bd26ee16a5d2113fbaeecae3f48 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 3 Jun 2024 19:54:18 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx | 3 +-- site/src/modules/dashboard/Navbar/NavbarView.stories.tsx | 1 - site/src/modules/dashboard/Navbar/NavbarView.tsx | 3 +-- site/src/pages/UsersPage/UsersLayout.tsx | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx index f315de1b6f524..23f0355ad3e9a 100644 --- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -1,7 +1,7 @@ import { css, type Interpolation, type Theme, useTheme } from "@emotion/react"; import Button from "@mui/material/Button"; import MenuItem from "@mui/material/MenuItem"; -import { type FC } from "react"; +import type { FC } from "react"; import { NavLink } from "react-router-dom"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { @@ -10,7 +10,6 @@ import { PopoverTrigger, usePopover, } from "components/Popover/Popover"; - import { USERS_LINK } from "modules/navigation"; interface DeploymentDropdownProps { diff --git a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx index fa34aed723331..146a66b9372e3 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx @@ -3,7 +3,6 @@ import { chromaticWithTablet } from "testHelpers/chromatic"; import { MockUser, MockUser2 } from "testHelpers/entities"; import { withDashboardProvider } from "testHelpers/storybook"; import { NavbarView } from "./NavbarView"; -import { action } from "@storybook/addon-actions"; const meta: Meta = { title: "modules/dashboard/NavbarView", diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 8c1d080a1687e..c477e8dcdf4b4 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -13,7 +13,6 @@ import { type FC, useRef, useState } from "react"; import { NavLink, useLocation, useNavigate } from "react-router-dom"; import type * as TypesGen from "api/typesGenerated"; import { Abbr } from "components/Abbr/Abbr"; -import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { displayError } from "components/GlobalSnackbar/utils"; import { CoderIcon } from "components/Icons/CoderIcon"; @@ -21,8 +20,8 @@ import { Latency } from "components/Latency/Latency"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import type { ProxyContextValue } from "contexts/ProxyContext"; import { BUTTON_SM_HEIGHT, navHeight } from "theme/constants"; -import { UserDropdown } from "./UserDropdown/UserDropdown"; import { DeploymentDropdown } from "./DeploymentDropdown"; +import { UserDropdown } from "./UserDropdown/UserDropdown"; export interface NavbarViewProps { logo_url?: string; diff --git a/site/src/pages/UsersPage/UsersLayout.tsx b/site/src/pages/UsersPage/UsersLayout.tsx index 891ac5fcae4b5..8b3dc7858c41e 100644 --- a/site/src/pages/UsersPage/UsersLayout.tsx +++ b/site/src/pages/UsersPage/UsersLayout.tsx @@ -13,8 +13,8 @@ import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { TAB_PADDING_Y, TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { USERS_LINK } from "modules/navigation"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; +import { USERS_LINK } from "modules/navigation"; export const UsersLayout: FC = () => { const { permissions } = useAuthenticated(); From c3d8207a68af53cba5bdee6937b946d1cf3adcab Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 3 Jun 2024 20:50:08 +0000 Subject: [PATCH 4/5] fix tests --- site/src/modules/dashboard/Navbar/Navbar.test.tsx | 7 ++++++- site/src/modules/dashboard/Navbar/NavbarView.tsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/site/src/modules/dashboard/Navbar/Navbar.test.tsx b/site/src/modules/dashboard/Navbar/Navbar.test.tsx index 681af84728851..93953530c2357 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.test.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.test.tsx @@ -1,4 +1,5 @@ import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { HttpResponse, http } from "msw"; import { App } from "App"; import { @@ -21,6 +22,8 @@ describe("Navbar", () => { }), ); render(); + const deploymentMenu = await screen.findByText("Deployment"); + await userEvent.click(deploymentMenu); await waitFor( () => { const link = screen.getByText(Language.audit); @@ -34,6 +37,8 @@ describe("Navbar", () => { // by default, user is an Admin with permission to see the audit log, // but is unlicensed so not entitled to see the audit log render(); + const deploymentMenu = await screen.findByText("Deployment"); + await userEvent.click(deploymentMenu); await waitFor( () => { const link = screen.queryByText(Language.audit); @@ -59,7 +64,7 @@ describe("Navbar", () => { render(); await waitFor( () => { - const link = screen.queryByText(Language.audit); + const link = screen.queryByText("Deployment"); expect(link).toBe(null); }, { timeout: 2000 }, diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index c477e8dcdf4b4..376273c8d75ee 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -40,7 +40,7 @@ export const Language = { workspaces: "Workspaces", templates: "Templates", users: "Users", - audit: "Audit", + audit: "Auditing", deployment: "Settings", }; From b28025a1b5aba8074966605b255e1271e0a96ffd Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 3 Jun 2024 20:55:46 +0000 Subject: [PATCH 5/5] same thing but different --- .../modules/dashboard/Navbar/NavbarView.test.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx index c881fa300000d..a6541ea688486 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx @@ -1,4 +1,5 @@ import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import type { ProxyContextValue } from "contexts/ProxyContext"; import { MockPrimaryWorkspaceProxy, MockUser } from "testHelpers/entities"; import { renderWithAuth } from "testHelpers/renderHelpers"; @@ -65,6 +66,8 @@ describe("NavbarView", () => { canViewHealth />, ); + const deploymentMenu = await screen.findByText("Deployment"); + await userEvent.click(deploymentMenu); const userLink = await screen.findByText(navLanguage.users); expect((userLink as HTMLAnchorElement).href).toContain("/users"); }); @@ -81,6 +84,8 @@ describe("NavbarView", () => { canViewHealth />, ); + const deploymentMenu = await screen.findByText("Deployment"); + await userEvent.click(deploymentMenu); const auditLink = await screen.findByText(navLanguage.audit); expect((auditLink as HTMLAnchorElement).href).toContain("/audit"); }); @@ -97,8 +102,12 @@ describe("NavbarView", () => { canViewHealth />, ); - const auditLink = await screen.findByText(navLanguage.deployment); - expect((auditLink as HTMLAnchorElement).href).toContain( + const deploymentMenu = await screen.findByText("Deployment"); + await userEvent.click(deploymentMenu); + const deploymentSettingsLink = await screen.findByText( + navLanguage.deployment, + ); + expect((deploymentSettingsLink as HTMLAnchorElement).href).toContain( "/deployment/general", ); });