Skip to content

Commit 925cb45

Browse files
BrunoQuaresmaammario
authored andcommitted
feat(site): Add proxy menu into navbar (coder#7715)
1 parent 70658d4 commit 925cb45

File tree

10 files changed

+251
-37
lines changed

10 files changed

+251
-37
lines changed

site/src/components/Navbar/Navbar.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { render, screen, waitFor } from "@testing-library/react"
22
import { App } from "app"
3-
import { Language } from "components/NavbarView/NavbarView"
3+
import { Language } from "./NavbarView"
44
import { rest } from "msw"
55
import {
66
MockEntitlementsWithAuditLog,

site/src/components/Navbar/Navbar.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { useFeatureVisibility } from "hooks/useFeatureVisibility"
44
import { useMe } from "hooks/useMe"
55
import { usePermissions } from "hooks/usePermissions"
66
import { FC } from "react"
7-
import { NavbarView } from "../NavbarView/NavbarView"
7+
import { NavbarView } from "./NavbarView"
8+
import { useProxy } from "contexts/ProxyContext"
89

910
export const Navbar: FC = () => {
1011
const { appearance, buildInfo } = useDashboard()
@@ -16,6 +17,8 @@ export const Navbar: FC = () => {
1617
featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog)
1718
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
1819
const onSignOut = () => authSend("SIGN_OUT")
20+
const proxyContextValue = useProxy()
21+
const dashboard = useDashboard()
1922

2023
return (
2124
<NavbarView
@@ -26,6 +29,9 @@ export const Navbar: FC = () => {
2629
onSignOut={onSignOut}
2730
canViewAuditLog={canViewAuditLog}
2831
canViewDeployment={canViewDeployment}
32+
proxyContextValue={
33+
dashboard.experiments.includes("moons") ? proxyContextValue : undefined
34+
}
2935
/>
3036
)
3137
}

site/src/components/NavbarView/NavbarView.test.tsx renamed to site/src/components/Navbar/NavbarView.test.tsx

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
11
import { screen } from "@testing-library/react"
2-
import { MockUser, MockUser2 } from "../../testHelpers/entities"
2+
import {
3+
MockPrimaryWorkspaceProxy,
4+
MockUser,
5+
MockUser2,
6+
} from "../../testHelpers/entities"
37
import { render } from "../../testHelpers/renderHelpers"
48
import { Language as navLanguage, NavbarView } from "./NavbarView"
9+
import { ProxyContextValue } from "contexts/ProxyContext"
10+
import { action } from "@storybook/addon-actions"
11+
12+
const proxyContextValue: ProxyContextValue = {
13+
proxy: {
14+
preferredPathAppURL: "",
15+
preferredWildcardHostname: "",
16+
proxy: MockPrimaryWorkspaceProxy,
17+
},
18+
isLoading: false,
19+
isFetched: true,
20+
setProxy: jest.fn(),
21+
clearProxy: action("clearProxy"),
22+
proxyLatencies: {},
23+
}
524

625
describe("NavbarView", () => {
726
const noop = () => {
@@ -23,6 +42,7 @@ describe("NavbarView", () => {
2342
it("workspaces nav link has the correct href", async () => {
2443
render(
2544
<NavbarView
45+
proxyContextValue={proxyContextValue}
2646
user={MockUser}
2747
onSignOut={noop}
2848
canViewAuditLog
@@ -36,6 +56,7 @@ describe("NavbarView", () => {
3656
it("templates nav link has the correct href", async () => {
3757
render(
3858
<NavbarView
59+
proxyContextValue={proxyContextValue}
3960
user={MockUser}
4061
onSignOut={noop}
4162
canViewAuditLog
@@ -49,6 +70,7 @@ describe("NavbarView", () => {
4970
it("users nav link has the correct href", async () => {
5071
render(
5172
<NavbarView
73+
proxyContextValue={proxyContextValue}
5274
user={MockUser}
5375
onSignOut={noop}
5476
canViewAuditLog
@@ -70,6 +92,7 @@ describe("NavbarView", () => {
7092
// When
7193
render(
7294
<NavbarView
95+
proxyContextValue={proxyContextValue}
7396
user={mockUser}
7497
onSignOut={noop}
7598
canViewAuditLog
@@ -86,6 +109,7 @@ describe("NavbarView", () => {
86109
it("audit nav link has the correct href", async () => {
87110
render(
88111
<NavbarView
112+
proxyContextValue={proxyContextValue}
89113
user={MockUser}
90114
onSignOut={noop}
91115
canViewAuditLog
@@ -99,6 +123,7 @@ describe("NavbarView", () => {
99123
it("audit nav link is hidden for members", async () => {
100124
render(
101125
<NavbarView
126+
proxyContextValue={proxyContextValue}
102127
user={MockUser2}
103128
onSignOut={noop}
104129
canViewAuditLog={false}
@@ -112,6 +137,7 @@ describe("NavbarView", () => {
112137
it("deployment nav link has the correct href", async () => {
113138
render(
114139
<NavbarView
140+
proxyContextValue={proxyContextValue}
115141
user={MockUser}
116142
onSignOut={noop}
117143
canViewAuditLog
@@ -127,6 +153,7 @@ describe("NavbarView", () => {
127153
it("deployment nav link is hidden for members", async () => {
128154
render(
129155
<NavbarView
156+
proxyContextValue={proxyContextValue}
130157
user={MockUser2}
131158
onSignOut={noop}
132159
canViewAuditLog={false}

site/src/components/NavbarView/NavbarView.tsx renamed to site/src/components/Navbar/NavbarView.tsx

+179-12
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,27 @@ import Drawer from "@mui/material/Drawer"
22
import IconButton from "@mui/material/IconButton"
33
import List from "@mui/material/List"
44
import ListItem from "@mui/material/ListItem"
5-
import { makeStyles } from "@mui/styles"
5+
import { makeStyles, useTheme } from "@mui/styles"
66
import MenuIcon from "@mui/icons-material/Menu"
77
import { CoderIcon } from "components/Icons/CoderIcon"
8-
import { useState } from "react"
9-
import { NavLink, useLocation } from "react-router-dom"
8+
import { FC, useRef, useState } from "react"
9+
import { NavLink, useLocation, useNavigate } from "react-router-dom"
1010
import { colors } from "theme/colors"
1111
import * as TypesGen from "../../api/typesGenerated"
1212
import { navHeight } from "../../theme/constants"
1313
import { combineClasses } from "../../utils/combineClasses"
1414
import { UserDropdown } from "../UserDropdown/UsersDropdown"
15+
import Box from "@mui/material/Box"
16+
import Menu from "@mui/material/Menu"
17+
import Button from "@mui/material/Button"
18+
import MenuItem from "@mui/material/MenuItem"
19+
import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined"
20+
import { ProxyContextValue } from "contexts/ProxyContext"
21+
import { displayError } from "components/GlobalSnackbar/utils"
22+
import Divider from "@mui/material/Divider"
23+
import HelpOutline from "@mui/icons-material/HelpOutline"
24+
import Tooltip from "@mui/material/Tooltip"
25+
import Skeleton from "@mui/material/Skeleton"
1526

1627
export const USERS_LINK = `/users?filter=${encodeURIComponent("status:active")}`
1728

@@ -23,6 +34,7 @@ export interface NavbarViewProps {
2334
onSignOut: () => void
2435
canViewAuditLog: boolean
2536
canViewDeployment: boolean
37+
proxyContextValue?: ProxyContextValue
2638
}
2739

2840
export const Language = {
@@ -83,14 +95,15 @@ const NavItems: React.FC<
8395
</List>
8496
)
8597
}
86-
export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
98+
export const NavbarView: FC<NavbarViewProps> = ({
8799
user,
88100
logo_url,
89101
buildInfo,
90102
supportLinks,
91103
onSignOut,
92104
canViewAuditLog,
93105
canViewDeployment,
106+
proxyContextValue,
94107
}) => {
95108
const styles = useStyles()
96109
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
@@ -145,7 +158,16 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
145158
canViewDeployment={canViewDeployment}
146159
/>
147160

148-
<div className={styles.profileButton}>
161+
<Box
162+
display="flex"
163+
marginLeft={{ lg: "auto" }}
164+
gap={2}
165+
alignItems="center"
166+
paddingRight={2}
167+
>
168+
{proxyContextValue && (
169+
<ProxyMenu proxyContextValue={proxyContextValue} />
170+
)}
149171
{user && (
150172
<UserDropdown
151173
user={user}
@@ -154,12 +176,163 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
154176
onSignOut={onSignOut}
155177
/>
156178
)}
157-
</div>
179+
</Box>
158180
</div>
159181
</nav>
160182
)
161183
}
162184

185+
const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
186+
proxyContextValue,
187+
}) => {
188+
const buttonRef = useRef<HTMLButtonElement>(null)
189+
const [isOpen, setIsOpen] = useState(false)
190+
const selectedProxy = proxyContextValue.proxy.proxy
191+
const closeMenu = () => setIsOpen(false)
192+
const navigate = useNavigate()
193+
194+
if (!proxyContextValue.isFetched) {
195+
return (
196+
<Skeleton
197+
width="160px"
198+
height={30}
199+
sx={{ borderRadius: "4px", transform: "none" }}
200+
/>
201+
)
202+
}
203+
204+
return (
205+
<>
206+
<Button
207+
ref={buttonRef}
208+
onClick={() => setIsOpen(true)}
209+
size="small"
210+
endIcon={<KeyboardArrowDownOutlined />}
211+
sx={{
212+
borderRadius: "4px",
213+
"& .MuiSvgIcon-root": { fontSize: 14 },
214+
}}
215+
>
216+
{selectedProxy ? (
217+
<Box display="flex" gap={2} alignItems="center">
218+
<Box width={14} height={14} lineHeight={0}>
219+
<Box
220+
component="img"
221+
src={selectedProxy.icon_url}
222+
alt=""
223+
sx={{ objectFit: "contain" }}
224+
width="100%"
225+
height="100%"
226+
/>
227+
</Box>
228+
{selectedProxy.display_name}
229+
<ProxyStatusLatency
230+
proxy={selectedProxy}
231+
latency={
232+
proxyContextValue.proxyLatencies?.[selectedProxy.id]?.latencyMS
233+
}
234+
/>
235+
</Box>
236+
) : (
237+
"Select Proxy"
238+
)}
239+
</Button>
240+
<Menu
241+
open={isOpen}
242+
anchorEl={buttonRef.current}
243+
onClick={closeMenu}
244+
onClose={closeMenu}
245+
sx={{ "& .MuiMenu-paper": { py: 1 } }}
246+
>
247+
{proxyContextValue.proxies?.map((proxy) => (
248+
<MenuItem
249+
onClick={() => {
250+
if (!proxy.healthy) {
251+
displayError("Please select a healthy workspace proxy.")
252+
closeMenu()
253+
return
254+
}
255+
256+
proxyContextValue.setProxy(proxy)
257+
closeMenu()
258+
}}
259+
key={proxy.id}
260+
selected={proxy.id === selectedProxy?.id}
261+
sx={{
262+
fontSize: 14,
263+
}}
264+
>
265+
<Box display="flex" gap={3} alignItems="center" width="100%">
266+
<Box width={14} height={14} lineHeight={0}>
267+
<Box
268+
component="img"
269+
src={proxy.icon_url}
270+
alt=""
271+
sx={{ objectFit: "contain" }}
272+
width="100%"
273+
height="100%"
274+
/>
275+
</Box>
276+
{proxy.display_name}
277+
<ProxyStatusLatency
278+
proxy={proxy}
279+
latency={
280+
proxyContextValue.proxyLatencies?.[proxy.id]?.latencyMS
281+
}
282+
/>
283+
</Box>
284+
</MenuItem>
285+
))}
286+
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
287+
<MenuItem
288+
sx={{ fontSize: 14 }}
289+
onClick={() => {
290+
navigate("/settings/workspace-proxies")
291+
}}
292+
>
293+
Proxy settings
294+
</MenuItem>
295+
</Menu>
296+
</>
297+
)
298+
}
299+
300+
const ProxyStatusLatency: FC<{ proxy: TypesGen.Region; latency?: number }> = ({
301+
proxy,
302+
latency,
303+
}) => {
304+
const theme = useTheme()
305+
let color = theme.palette.success.light
306+
307+
if (!latency) {
308+
return (
309+
<Tooltip title="Latency not available">
310+
<HelpOutline
311+
sx={{
312+
ml: "auto",
313+
fontSize: "14px !important",
314+
color: (theme) => theme.palette.text.secondary,
315+
}}
316+
/>
317+
</Tooltip>
318+
)
319+
}
320+
321+
if (latency >= 300) {
322+
color = theme.palette.error.light
323+
}
324+
325+
if (!proxy.healthy || latency >= 100) {
326+
color = theme.palette.warning.light
327+
}
328+
329+
return (
330+
<Box sx={{ color, fontSize: 13, marginLeft: "auto" }}>
331+
{latency.toFixed(0)}ms
332+
</Box>
333+
)
334+
}
335+
163336
const useStyles = makeStyles((theme) => ({
164337
root: {
165338
height: navHeight,
@@ -192,12 +365,6 @@ const useStyles = makeStyles((theme) => ({
192365
display: "flex",
193366
},
194367
},
195-
profileButton: {
196-
paddingRight: theme.spacing(2),
197-
[theme.breakpoints.up("md")]: {
198-
marginLeft: "auto",
199-
},
200-
},
201368
mobileMenuButton: {
202369
[theme.breakpoints.up("md")]: {
203370
display: "none",

site/src/components/UsersLayout/UsersLayout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { makeStyles } from "@mui/styles"
44
import GroupAdd from "@mui/icons-material/GroupAddOutlined"
55
import PersonAdd from "@mui/icons-material/PersonAddOutlined"
66
import { useMachine } from "@xstate/react"
7-
import { USERS_LINK } from "components/NavbarView/NavbarView"
7+
import { USERS_LINK } from "components/Navbar/NavbarView"
88
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"
99
import { useFeatureVisibility } from "hooks/useFeatureVisibility"
1010
import { usePermissions } from "hooks/usePermissions"

site/src/contexts/ProxyContext.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
127127
return (
128128
<ProxyContext.Provider
129129
value={{
130+
proxyLatencies,
130131
userProxy: userSavedProxy,
131-
proxyLatencies: proxyLatencies,
132132
proxy: experimentEnabled
133133
? proxy
134134
: {

0 commit comments

Comments
 (0)