Skip to content

Commit 78b8264

Browse files
authored
feat(site): add deployment menu to navbar (coder#13401)
1 parent c7233ec commit 78b8264

File tree

8 files changed

+251
-112
lines changed

8 files changed

+251
-112
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
2+
import Button from "@mui/material/Button";
3+
import MenuItem from "@mui/material/MenuItem";
4+
import type { FC } from "react";
5+
import { NavLink } from "react-router-dom";
6+
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
7+
import {
8+
Popover,
9+
PopoverContent,
10+
PopoverTrigger,
11+
usePopover,
12+
} from "components/Popover/Popover";
13+
import { USERS_LINK } from "modules/navigation";
14+
15+
interface DeploymentDropdownProps {
16+
canViewAuditLog: boolean;
17+
canViewDeployment: boolean;
18+
canViewAllUsers: boolean;
19+
canViewHealth: boolean;
20+
}
21+
22+
export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
23+
canViewAuditLog,
24+
canViewDeployment,
25+
canViewAllUsers,
26+
canViewHealth,
27+
}) => {
28+
const theme = useTheme();
29+
30+
if (
31+
!canViewAuditLog &&
32+
!canViewDeployment &&
33+
!canViewAllUsers &&
34+
!canViewHealth
35+
) {
36+
return null;
37+
}
38+
39+
return (
40+
<Popover>
41+
<PopoverTrigger>
42+
<Button
43+
size="small"
44+
endIcon={
45+
<DropdownArrow
46+
color={theme.experimental.l2.fill.solid}
47+
close={false}
48+
margin={false}
49+
/>
50+
}
51+
>
52+
Deployment
53+
</Button>
54+
</PopoverTrigger>
55+
56+
<PopoverContent
57+
horizontal="right"
58+
css={{
59+
".MuiPaper-root": {
60+
minWidth: "auto",
61+
width: 180,
62+
boxShadow: theme.shadows[6],
63+
},
64+
}}
65+
>
66+
<DeploymentDropdownContent
67+
canViewAuditLog={canViewAuditLog}
68+
canViewDeployment={canViewDeployment}
69+
canViewAllUsers={canViewAllUsers}
70+
canViewHealth={canViewHealth}
71+
/>
72+
</PopoverContent>
73+
</Popover>
74+
);
75+
};
76+
77+
const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
78+
canViewAuditLog,
79+
canViewDeployment,
80+
canViewAllUsers,
81+
canViewHealth,
82+
}) => {
83+
const popover = usePopover();
84+
85+
const onPopoverClose = () => popover.setIsOpen(false);
86+
87+
return (
88+
<nav>
89+
{canViewDeployment && (
90+
<MenuItem
91+
component={NavLink}
92+
to="/deployment/general"
93+
css={styles.menuItem}
94+
onClick={onPopoverClose}
95+
>
96+
Settings
97+
</MenuItem>
98+
)}
99+
{canViewAllUsers && (
100+
<MenuItem
101+
component={NavLink}
102+
to={USERS_LINK}
103+
css={styles.menuItem}
104+
onClick={onPopoverClose}
105+
>
106+
Users
107+
</MenuItem>
108+
)}
109+
{canViewAuditLog && (
110+
<MenuItem
111+
component={NavLink}
112+
to="/audit"
113+
css={styles.menuItem}
114+
onClick={onPopoverClose}
115+
>
116+
Auditing
117+
</MenuItem>
118+
)}
119+
{canViewHealth && (
120+
<MenuItem
121+
component={NavLink}
122+
to="/health"
123+
css={styles.menuItem}
124+
onClick={onPopoverClose}
125+
>
126+
Healthcheck
127+
</MenuItem>
128+
)}
129+
</nav>
130+
);
131+
};
132+
133+
const styles = {
134+
menuItem: (theme) => css`
135+
text-decoration: none;
136+
color: inherit;
137+
gap: 20px;
138+
padding: 8px 20px;
139+
font-size: 14px;
140+
141+
&:hover {
142+
background-color: ${theme.palette.action.hover};
143+
transition: background-color 0.3s ease;
144+
}
145+
`,
146+
menuItemIcon: (theme) => ({
147+
color: theme.palette.text.secondary,
148+
width: 20,
149+
height: 20,
150+
}),
151+
} satisfies Record<string, Interpolation<Theme>>;

site/src/modules/dashboard/Navbar/Navbar.test.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { render, screen, waitFor } from "@testing-library/react";
2+
import userEvent from "@testing-library/user-event";
23
import { HttpResponse, http } from "msw";
34
import { App } from "App";
45
import {
@@ -21,6 +22,8 @@ describe("Navbar", () => {
2122
}),
2223
);
2324
render(<App />);
25+
const deploymentMenu = await screen.findByText("Deployment");
26+
await userEvent.click(deploymentMenu);
2427
await waitFor(
2528
() => {
2629
const link = screen.getByText(Language.audit);
@@ -34,6 +37,8 @@ describe("Navbar", () => {
3437
// by default, user is an Admin with permission to see the audit log,
3538
// but is unlicensed so not entitled to see the audit log
3639
render(<App />);
40+
const deploymentMenu = await screen.findByText("Deployment");
41+
await userEvent.click(deploymentMenu);
3742
await waitFor(
3843
() => {
3944
const link = screen.queryByText(Language.audit);
@@ -59,7 +64,7 @@ describe("Navbar", () => {
5964
render(<App />);
6065
await waitFor(
6166
() => {
62-
const link = screen.queryByText(Language.audit);
67+
const link = screen.queryByText("Deployment");
6368
expect(link).toBe(null);
6469
},
6570
{ timeout: 2000 },

site/src/modules/dashboard/Navbar/NavbarView.stories.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ const meta: Meta<typeof NavbarView> = {
1010
component: NavbarView,
1111
args: {
1212
user: MockUser,
13+
canViewAuditLog: true,
14+
canViewDeployment: true,
15+
canViewAllUsers: true,
16+
canViewHealth: true,
1317
},
1418
decorators: [withDashboardProvider],
1519
};
@@ -25,6 +29,7 @@ export const ForMember: Story = {
2529
canViewAuditLog: false,
2630
canViewDeployment: false,
2731
canViewAllUsers: false,
32+
canViewHealth: false,
2833
},
2934
};
3035

site/src/modules/dashboard/Navbar/NavbarView.test.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { screen } from "@testing-library/react";
2+
import userEvent from "@testing-library/user-event";
23
import type { ProxyContextValue } from "contexts/ProxyContext";
34
import { MockPrimaryWorkspaceProxy, MockUser } from "testHelpers/entities";
45
import { renderWithAuth } from "testHelpers/renderHelpers";
@@ -65,6 +66,8 @@ describe("NavbarView", () => {
6566
canViewHealth
6667
/>,
6768
);
69+
const deploymentMenu = await screen.findByText("Deployment");
70+
await userEvent.click(deploymentMenu);
6871
const userLink = await screen.findByText(navLanguage.users);
6972
expect((userLink as HTMLAnchorElement).href).toContain("/users");
7073
});
@@ -81,6 +84,8 @@ describe("NavbarView", () => {
8184
canViewHealth
8285
/>,
8386
);
87+
const deploymentMenu = await screen.findByText("Deployment");
88+
await userEvent.click(deploymentMenu);
8489
const auditLink = await screen.findByText(navLanguage.audit);
8590
expect((auditLink as HTMLAnchorElement).href).toContain("/audit");
8691
});
@@ -97,8 +102,12 @@ describe("NavbarView", () => {
97102
canViewHealth
98103
/>,
99104
);
100-
const auditLink = await screen.findByText(navLanguage.deployment);
101-
expect((auditLink as HTMLAnchorElement).href).toContain(
105+
const deploymentMenu = await screen.findByText("Deployment");
106+
await userEvent.click(deploymentMenu);
107+
const deploymentSettingsLink = await screen.findByText(
108+
navLanguage.deployment,
109+
);
110+
expect((deploymentSettingsLink as HTMLAnchorElement).href).toContain(
102111
"/deployment/general",
103112
);
104113
});

site/src/modules/dashboard/Navbar/NavbarView.tsx

Lines changed: 15 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Menu from "@mui/material/Menu";
99
import MenuItem from "@mui/material/MenuItem";
1010
import Skeleton from "@mui/material/Skeleton";
1111
import { visuallyHidden } from "@mui/utils";
12-
import { type FC, type ReactNode, useRef, useState } from "react";
12+
import { type FC, useRef, useState } from "react";
1313
import { NavLink, useLocation, useNavigate } from "react-router-dom";
1414
import type * as TypesGen from "api/typesGenerated";
1515
import { Abbr } from "components/Abbr/Abbr";
@@ -20,12 +20,9 @@ import { Latency } from "components/Latency/Latency";
2020
import { useAuthenticated } from "contexts/auth/RequireAuth";
2121
import type { ProxyContextValue } from "contexts/ProxyContext";
2222
import { BUTTON_SM_HEIGHT, navHeight } from "theme/constants";
23+
import { DeploymentDropdown } from "./DeploymentDropdown";
2324
import { UserDropdown } from "./UserDropdown/UserDropdown";
2425

25-
export const USERS_LINK = `/users?filter=${encodeURIComponent(
26-
"status:active",
27-
)}`;
28-
2926
export interface NavbarViewProps {
3027
logo_url?: string;
3128
user?: TypesGen.User;
@@ -43,26 +40,15 @@ export const Language = {
4340
workspaces: "Workspaces",
4441
templates: "Templates",
4542
users: "Users",
46-
audit: "Audit",
47-
deployment: "Deployment",
43+
audit: "Auditing",
44+
deployment: "Settings",
4845
};
4946

5047
interface NavItemsProps {
51-
children?: ReactNode;
5248
className?: string;
53-
canViewAuditLog: boolean;
54-
canViewDeployment: boolean;
55-
canViewAllUsers: boolean;
56-
canViewHealth: boolean;
5749
}
5850

59-
const NavItems: FC<NavItemsProps> = ({
60-
className,
61-
canViewAuditLog,
62-
canViewDeployment,
63-
canViewAllUsers,
64-
canViewHealth,
65-
}) => {
51+
const NavItems: FC<NavItemsProps> = ({ className }) => {
6652
const location = useLocation();
6753
const theme = useTheme();
6854

@@ -83,26 +69,6 @@ const NavItems: FC<NavItemsProps> = ({
8369
<NavLink css={styles.link} to="/templates">
8470
{Language.templates}
8571
</NavLink>
86-
{canViewAllUsers && (
87-
<NavLink css={styles.link} to={USERS_LINK}>
88-
{Language.users}
89-
</NavLink>
90-
)}
91-
{canViewAuditLog && (
92-
<NavLink css={styles.link} to="/audit">
93-
{Language.audit}
94-
</NavLink>
95-
)}
96-
{canViewDeployment && (
97-
<NavLink css={styles.link} to="/deployment/general">
98-
{Language.deployment}
99-
</NavLink>
100-
)}
101-
{canViewHealth && (
102-
<NavLink css={styles.link} to="/health">
103-
Health
104-
</NavLink>
105-
)}
10672
</nav>
10773
);
10874
};
@@ -157,12 +123,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
157123
)}
158124
</div>
159125
</div>
160-
<NavItems
161-
canViewAuditLog={canViewAuditLog}
162-
canViewDeployment={canViewDeployment}
163-
canViewAllUsers={canViewAllUsers}
164-
canViewHealth={canViewHealth}
165-
/>
126+
<NavItems />
166127
</div>
167128
</Drawer>
168129

@@ -174,18 +135,20 @@ export const NavbarView: FC<NavbarViewProps> = ({
174135
)}
175136
</NavLink>
176137

177-
<NavItems
178-
css={styles.desktopNavItems}
179-
canViewAuditLog={canViewAuditLog}
180-
canViewDeployment={canViewDeployment}
181-
canViewAllUsers={canViewAllUsers}
182-
canViewHealth={canViewHealth}
183-
/>
138+
<NavItems css={styles.desktopNavItems} />
184139

185140
<div css={styles.navMenus}>
186141
{proxyContextValue && (
187142
<ProxyMenu proxyContextValue={proxyContextValue} />
188143
)}
144+
145+
<DeploymentDropdown
146+
canViewAuditLog={canViewAuditLog}
147+
canViewDeployment={canViewDeployment}
148+
canViewAllUsers={canViewAllUsers}
149+
canViewHealth={canViewHealth}
150+
/>
151+
189152
{user && (
190153
<UserDropdown
191154
user={user}
@@ -260,7 +223,6 @@ const ProxyMenu: FC<ProxyMenuProps> = ({ proxyContextValue }) => {
260223
size="small"
261224
endIcon={<KeyboardArrowDownOutlined />}
262225
css={{
263-
borderRadius: "999px",
264226
"& .MuiSvgIcon-root": { fontSize: 14 },
265227
}}
266228
>

0 commit comments

Comments
 (0)