feat(site): add deployment menu to navbar (#13401)

This commit is contained in:
Kayla Washburn-Love
2024-06-03 15:05:49 -06:00
committed by GitHub
parent c7233eccec
commit 78b8264a90
8 changed files with 251 additions and 112 deletions
@@ -0,0 +1,151 @@
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<DeploymentDropdownProps> = ({
canViewAuditLog,
canViewDeployment,
canViewAllUsers,
canViewHealth,
}) => {
const theme = useTheme();
if (
!canViewAuditLog &&
!canViewDeployment &&
!canViewAllUsers &&
!canViewHealth
) {
return null;
}
return (
<Popover>
<PopoverTrigger>
<Button
size="small"
endIcon={
<DropdownArrow
color={theme.experimental.l2.fill.solid}
close={false}
margin={false}
/>
}
>
Deployment
</Button>
</PopoverTrigger>
<PopoverContent
horizontal="right"
css={{
".MuiPaper-root": {
minWidth: "auto",
width: 180,
boxShadow: theme.shadows[6],
},
}}
>
<DeploymentDropdownContent
canViewAuditLog={canViewAuditLog}
canViewDeployment={canViewDeployment}
canViewAllUsers={canViewAllUsers}
canViewHealth={canViewHealth}
/>
</PopoverContent>
</Popover>
);
};
const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
canViewAuditLog,
canViewDeployment,
canViewAllUsers,
canViewHealth,
}) => {
const popover = usePopover();
const onPopoverClose = () => popover.setIsOpen(false);
return (
<nav>
{canViewDeployment && (
<MenuItem
component={NavLink}
to="/deployment/general"
css={styles.menuItem}
onClick={onPopoverClose}
>
Settings
</MenuItem>
)}
{canViewAllUsers && (
<MenuItem
component={NavLink}
to={USERS_LINK}
css={styles.menuItem}
onClick={onPopoverClose}
>
Users
</MenuItem>
)}
{canViewAuditLog && (
<MenuItem
component={NavLink}
to="/audit"
css={styles.menuItem}
onClick={onPopoverClose}
>
Auditing
</MenuItem>
)}
{canViewHealth && (
<MenuItem
component={NavLink}
to="/health"
css={styles.menuItem}
onClick={onPopoverClose}
>
Healthcheck
</MenuItem>
)}
</nav>
);
};
const styles = {
menuItem: (theme) => css`
text-decoration: none;
color: inherit;
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<string, Interpolation<Theme>>;
@@ -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(<App />);
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(<App />);
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(<App />);
await waitFor(
() => {
const link = screen.queryByText(Language.audit);
const link = screen.queryByText("Deployment");
expect(link).toBe(null);
},
{ timeout: 2000 },
@@ -10,6 +10,10 @@ const meta: Meta<typeof NavbarView> = {
component: NavbarView,
args: {
user: MockUser,
canViewAuditLog: true,
canViewDeployment: true,
canViewAllUsers: true,
canViewHealth: true,
},
decorators: [withDashboardProvider],
};
@@ -25,6 +29,7 @@ export const ForMember: Story = {
canViewAuditLog: false,
canViewDeployment: false,
canViewAllUsers: false,
canViewHealth: false,
},
};
@@ -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",
);
});
@@ -9,7 +9,7 @@ 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";
@@ -20,12 +20,9 @@ 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 { DeploymentDropdown } from "./DeploymentDropdown";
import { UserDropdown } from "./UserDropdown/UserDropdown";
export const USERS_LINK = `/users?filter=${encodeURIComponent(
"status:active",
)}`;
export interface NavbarViewProps {
logo_url?: string;
user?: TypesGen.User;
@@ -43,26 +40,15 @@ export const Language = {
workspaces: "Workspaces",
templates: "Templates",
users: "Users",
audit: "Audit",
deployment: "Deployment",
audit: "Auditing",
deployment: "Settings",
};
interface NavItemsProps {
children?: ReactNode;
className?: string;
canViewAuditLog: boolean;
canViewDeployment: boolean;
canViewAllUsers: boolean;
canViewHealth: boolean;
}
const NavItems: FC<NavItemsProps> = ({
className,
canViewAuditLog,
canViewDeployment,
canViewAllUsers,
canViewHealth,
}) => {
const NavItems: FC<NavItemsProps> = ({ className }) => {
const location = useLocation();
const theme = useTheme();
@@ -83,26 +69,6 @@ const NavItems: FC<NavItemsProps> = ({
<NavLink css={styles.link} to="/templates">
{Language.templates}
</NavLink>
{canViewAllUsers && (
<NavLink css={styles.link} to={USERS_LINK}>
{Language.users}
</NavLink>
)}
{canViewAuditLog && (
<NavLink css={styles.link} to="/audit">
{Language.audit}
</NavLink>
)}
{canViewDeployment && (
<NavLink css={styles.link} to="/deployment/general">
{Language.deployment}
</NavLink>
)}
{canViewHealth && (
<NavLink css={styles.link} to="/health">
Health
</NavLink>
)}
</nav>
);
};
@@ -157,12 +123,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
)}
</div>
</div>
<NavItems
canViewAuditLog={canViewAuditLog}
canViewDeployment={canViewDeployment}
canViewAllUsers={canViewAllUsers}
canViewHealth={canViewHealth}
/>
<NavItems />
</div>
</Drawer>
@@ -174,18 +135,20 @@ export const NavbarView: FC<NavbarViewProps> = ({
)}
</NavLink>
<NavItems
css={styles.desktopNavItems}
canViewAuditLog={canViewAuditLog}
canViewDeployment={canViewDeployment}
canViewAllUsers={canViewAllUsers}
canViewHealth={canViewHealth}
/>
<NavItems css={styles.desktopNavItems} />
<div css={styles.navMenus}>
{proxyContextValue && (
<ProxyMenu proxyContextValue={proxyContextValue} />
)}
<DeploymentDropdown
canViewAuditLog={canViewAuditLog}
canViewDeployment={canViewDeployment}
canViewAllUsers={canViewAllUsers}
canViewHealth={canViewHealth}
/>
{user && (
<UserDropdown
user={user}
@@ -260,7 +223,6 @@ const ProxyMenu: FC<ProxyMenuProps> = ({ proxyContextValue }) => {
size="small"
endIcon={<KeyboardArrowDownOutlined />}
css={{
borderRadius: "999px",
"& .MuiSvgIcon-root": { fontSize: 14 },
}}
>
@@ -27,61 +27,6 @@ export const Language = {
copyrightText: `\u00a9 ${new Date().getFullYear()} Coder Technologies, Inc.`,
};
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<string, Interpolation<Theme>>;
export interface UserDropdownContentProps {
user: TypesGen.User;
organizations?: TypesGen.Organization[];
@@ -268,3 +213,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<string, Interpolation<Theme>>;
+7
View File
@@ -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",
)}`;
+1 -1
View File
@@ -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/dashboard/Navbar/NavbarView";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { USERS_LINK } from "modules/navigation";
export const UsersLayout: FC = () => {
const { permissions } = useAuthenticated();