mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): Add proxy menu into navbar (#7715)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { App } from "app"
|
||||
import { Language } from "components/NavbarView/NavbarView"
|
||||
import { Language } from "./NavbarView"
|
||||
import { rest } from "msw"
|
||||
import {
|
||||
MockEntitlementsWithAuditLog,
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useFeatureVisibility } from "hooks/useFeatureVisibility"
|
||||
import { useMe } from "hooks/useMe"
|
||||
import { usePermissions } from "hooks/usePermissions"
|
||||
import { FC } from "react"
|
||||
import { NavbarView } from "../NavbarView/NavbarView"
|
||||
import { NavbarView } from "./NavbarView"
|
||||
import { useProxy } from "contexts/ProxyContext"
|
||||
|
||||
export const Navbar: FC = () => {
|
||||
const { appearance, buildInfo } = useDashboard()
|
||||
@@ -16,6 +17,8 @@ export const Navbar: FC = () => {
|
||||
featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog)
|
||||
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
|
||||
const onSignOut = () => authSend("SIGN_OUT")
|
||||
const proxyContextValue = useProxy()
|
||||
const dashboard = useDashboard()
|
||||
|
||||
return (
|
||||
<NavbarView
|
||||
@@ -26,6 +29,9 @@ export const Navbar: FC = () => {
|
||||
onSignOut={onSignOut}
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
canViewDeployment={canViewDeployment}
|
||||
proxyContextValue={
|
||||
dashboard.experiments.includes("moons") ? proxyContextValue : undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+28
-1
@@ -1,7 +1,26 @@
|
||||
import { screen } from "@testing-library/react"
|
||||
import { MockUser, MockUser2 } from "../../testHelpers/entities"
|
||||
import {
|
||||
MockPrimaryWorkspaceProxy,
|
||||
MockUser,
|
||||
MockUser2,
|
||||
} from "../../testHelpers/entities"
|
||||
import { render } from "../../testHelpers/renderHelpers"
|
||||
import { Language as navLanguage, NavbarView } from "./NavbarView"
|
||||
import { ProxyContextValue } from "contexts/ProxyContext"
|
||||
import { action } from "@storybook/addon-actions"
|
||||
|
||||
const proxyContextValue: ProxyContextValue = {
|
||||
proxy: {
|
||||
preferredPathAppURL: "",
|
||||
preferredWildcardHostname: "",
|
||||
proxy: MockPrimaryWorkspaceProxy,
|
||||
},
|
||||
isLoading: false,
|
||||
isFetched: true,
|
||||
setProxy: jest.fn(),
|
||||
clearProxy: action("clearProxy"),
|
||||
proxyLatencies: {},
|
||||
}
|
||||
|
||||
describe("NavbarView", () => {
|
||||
const noop = () => {
|
||||
@@ -23,6 +42,7 @@ describe("NavbarView", () => {
|
||||
it("workspaces nav link has the correct href", async () => {
|
||||
render(
|
||||
<NavbarView
|
||||
proxyContextValue={proxyContextValue}
|
||||
user={MockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
@@ -36,6 +56,7 @@ describe("NavbarView", () => {
|
||||
it("templates nav link has the correct href", async () => {
|
||||
render(
|
||||
<NavbarView
|
||||
proxyContextValue={proxyContextValue}
|
||||
user={MockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
@@ -49,6 +70,7 @@ describe("NavbarView", () => {
|
||||
it("users nav link has the correct href", async () => {
|
||||
render(
|
||||
<NavbarView
|
||||
proxyContextValue={proxyContextValue}
|
||||
user={MockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
@@ -70,6 +92,7 @@ describe("NavbarView", () => {
|
||||
// When
|
||||
render(
|
||||
<NavbarView
|
||||
proxyContextValue={proxyContextValue}
|
||||
user={mockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
@@ -86,6 +109,7 @@ describe("NavbarView", () => {
|
||||
it("audit nav link has the correct href", async () => {
|
||||
render(
|
||||
<NavbarView
|
||||
proxyContextValue={proxyContextValue}
|
||||
user={MockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
@@ -99,6 +123,7 @@ describe("NavbarView", () => {
|
||||
it("audit nav link is hidden for members", async () => {
|
||||
render(
|
||||
<NavbarView
|
||||
proxyContextValue={proxyContextValue}
|
||||
user={MockUser2}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog={false}
|
||||
@@ -112,6 +137,7 @@ describe("NavbarView", () => {
|
||||
it("deployment nav link has the correct href", async () => {
|
||||
render(
|
||||
<NavbarView
|
||||
proxyContextValue={proxyContextValue}
|
||||
user={MockUser}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog
|
||||
@@ -127,6 +153,7 @@ describe("NavbarView", () => {
|
||||
it("deployment nav link is hidden for members", async () => {
|
||||
render(
|
||||
<NavbarView
|
||||
proxyContextValue={proxyContextValue}
|
||||
user={MockUser2}
|
||||
onSignOut={noop}
|
||||
canViewAuditLog={false}
|
||||
+179
-12
@@ -2,16 +2,27 @@ import Drawer from "@mui/material/Drawer"
|
||||
import IconButton from "@mui/material/IconButton"
|
||||
import List from "@mui/material/List"
|
||||
import ListItem from "@mui/material/ListItem"
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import { makeStyles, useTheme } from "@mui/styles"
|
||||
import MenuIcon from "@mui/icons-material/Menu"
|
||||
import { CoderIcon } from "components/Icons/CoderIcon"
|
||||
import { useState } from "react"
|
||||
import { NavLink, useLocation } from "react-router-dom"
|
||||
import { FC, useRef, useState } from "react"
|
||||
import { NavLink, useLocation, useNavigate } from "react-router-dom"
|
||||
import { colors } from "theme/colors"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { navHeight } from "../../theme/constants"
|
||||
import { combineClasses } from "../../utils/combineClasses"
|
||||
import { UserDropdown } from "../UserDropdown/UsersDropdown"
|
||||
import Box from "@mui/material/Box"
|
||||
import Menu from "@mui/material/Menu"
|
||||
import Button from "@mui/material/Button"
|
||||
import MenuItem from "@mui/material/MenuItem"
|
||||
import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined"
|
||||
import { ProxyContextValue } from "contexts/ProxyContext"
|
||||
import { displayError } from "components/GlobalSnackbar/utils"
|
||||
import Divider from "@mui/material/Divider"
|
||||
import HelpOutline from "@mui/icons-material/HelpOutline"
|
||||
import Tooltip from "@mui/material/Tooltip"
|
||||
import Skeleton from "@mui/material/Skeleton"
|
||||
|
||||
export const USERS_LINK = `/users?filter=${encodeURIComponent("status:active")}`
|
||||
|
||||
@@ -23,6 +34,7 @@ export interface NavbarViewProps {
|
||||
onSignOut: () => void
|
||||
canViewAuditLog: boolean
|
||||
canViewDeployment: boolean
|
||||
proxyContextValue?: ProxyContextValue
|
||||
}
|
||||
|
||||
export const Language = {
|
||||
@@ -83,7 +95,7 @@ const NavItems: React.FC<
|
||||
</List>
|
||||
)
|
||||
}
|
||||
export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
||||
export const NavbarView: FC<NavbarViewProps> = ({
|
||||
user,
|
||||
logo_url,
|
||||
buildInfo,
|
||||
@@ -91,6 +103,7 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
||||
onSignOut,
|
||||
canViewAuditLog,
|
||||
canViewDeployment,
|
||||
proxyContextValue,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||
@@ -145,7 +158,16 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
||||
canViewDeployment={canViewDeployment}
|
||||
/>
|
||||
|
||||
<div className={styles.profileButton}>
|
||||
<Box
|
||||
display="flex"
|
||||
marginLeft={{ lg: "auto" }}
|
||||
gap={2}
|
||||
alignItems="center"
|
||||
paddingRight={2}
|
||||
>
|
||||
{proxyContextValue && (
|
||||
<ProxyMenu proxyContextValue={proxyContextValue} />
|
||||
)}
|
||||
{user && (
|
||||
<UserDropdown
|
||||
user={user}
|
||||
@@ -154,12 +176,163 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
||||
onSignOut={onSignOut}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
||||
proxyContextValue,
|
||||
}) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const selectedProxy = proxyContextValue.proxy.proxy
|
||||
const closeMenu = () => setIsOpen(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (!proxyContextValue.isFetched) {
|
||||
return (
|
||||
<Skeleton
|
||||
width="160px"
|
||||
height={30}
|
||||
sx={{ borderRadius: "4px", transform: "none" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
onClick={() => setIsOpen(true)}
|
||||
size="small"
|
||||
endIcon={<KeyboardArrowDownOutlined />}
|
||||
sx={{
|
||||
borderRadius: "4px",
|
||||
"& .MuiSvgIcon-root": { fontSize: 14 },
|
||||
}}
|
||||
>
|
||||
{selectedProxy ? (
|
||||
<Box display="flex" gap={2} alignItems="center">
|
||||
<Box width={14} height={14} lineHeight={0}>
|
||||
<Box
|
||||
component="img"
|
||||
src={selectedProxy.icon_url}
|
||||
alt=""
|
||||
sx={{ objectFit: "contain" }}
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
</Box>
|
||||
{selectedProxy.display_name}
|
||||
<ProxyStatusLatency
|
||||
proxy={selectedProxy}
|
||||
latency={
|
||||
proxyContextValue.proxyLatencies?.[selectedProxy.id]?.latencyMS
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
"Select Proxy"
|
||||
)}
|
||||
</Button>
|
||||
<Menu
|
||||
open={isOpen}
|
||||
anchorEl={buttonRef.current}
|
||||
onClick={closeMenu}
|
||||
onClose={closeMenu}
|
||||
sx={{ "& .MuiMenu-paper": { py: 1 } }}
|
||||
>
|
||||
{proxyContextValue.proxies?.map((proxy) => (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (!proxy.healthy) {
|
||||
displayError("Please select a healthy workspace proxy.")
|
||||
closeMenu()
|
||||
return
|
||||
}
|
||||
|
||||
proxyContextValue.setProxy(proxy)
|
||||
closeMenu()
|
||||
}}
|
||||
key={proxy.id}
|
||||
selected={proxy.id === selectedProxy?.id}
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
<Box display="flex" gap={3} alignItems="center" width="100%">
|
||||
<Box width={14} height={14} lineHeight={0}>
|
||||
<Box
|
||||
component="img"
|
||||
src={proxy.icon_url}
|
||||
alt=""
|
||||
sx={{ objectFit: "contain" }}
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
</Box>
|
||||
{proxy.display_name}
|
||||
<ProxyStatusLatency
|
||||
proxy={proxy}
|
||||
latency={
|
||||
proxyContextValue.proxyLatencies?.[proxy.id]?.latencyMS
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
|
||||
<MenuItem
|
||||
sx={{ fontSize: 14 }}
|
||||
onClick={() => {
|
||||
navigate("/settings/workspace-proxies")
|
||||
}}
|
||||
>
|
||||
Proxy settings
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ProxyStatusLatency: FC<{ proxy: TypesGen.Region; latency?: number }> = ({
|
||||
proxy,
|
||||
latency,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
let color = theme.palette.success.light
|
||||
|
||||
if (!latency) {
|
||||
return (
|
||||
<Tooltip title="Latency not available">
|
||||
<HelpOutline
|
||||
sx={{
|
||||
ml: "auto",
|
||||
fontSize: "14px !important",
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (latency >= 300) {
|
||||
color = theme.palette.error.light
|
||||
}
|
||||
|
||||
if (!proxy.healthy || latency >= 100) {
|
||||
color = theme.palette.warning.light
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ color, fontSize: 13, marginLeft: "auto" }}>
|
||||
{latency.toFixed(0)}ms
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
height: navHeight,
|
||||
@@ -192,12 +365,6 @@ const useStyles = makeStyles((theme) => ({
|
||||
display: "flex",
|
||||
},
|
||||
},
|
||||
profileButton: {
|
||||
paddingRight: theme.spacing(2),
|
||||
[theme.breakpoints.up("md")]: {
|
||||
marginLeft: "auto",
|
||||
},
|
||||
},
|
||||
mobileMenuButton: {
|
||||
[theme.breakpoints.up("md")]: {
|
||||
display: "none",
|
||||
@@ -4,7 +4,7 @@ import { makeStyles } from "@mui/styles"
|
||||
import GroupAdd from "@mui/icons-material/GroupAddOutlined"
|
||||
import PersonAdd from "@mui/icons-material/PersonAddOutlined"
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { USERS_LINK } from "components/NavbarView/NavbarView"
|
||||
import { USERS_LINK } from "components/Navbar/NavbarView"
|
||||
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"
|
||||
import { useFeatureVisibility } from "hooks/useFeatureVisibility"
|
||||
import { usePermissions } from "hooks/usePermissions"
|
||||
|
||||
@@ -127,8 +127,8 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<ProxyContext.Provider
|
||||
value={{
|
||||
proxyLatencies,
|
||||
userProxy: userSavedProxy,
|
||||
proxyLatencies: proxyLatencies,
|
||||
proxy: experimentEnabled
|
||||
? proxy
|
||||
: {
|
||||
|
||||
@@ -25,8 +25,10 @@ const proxyLatenciesReducer = (
|
||||
action: ProxyLatencyAction,
|
||||
): Record<string, ProxyLatencyReport> => {
|
||||
// Just overwrite any existing latency.
|
||||
state[action.proxyID] = action.report
|
||||
return state
|
||||
return {
|
||||
...state,
|
||||
[action.proxyID]: action.report,
|
||||
}
|
||||
}
|
||||
|
||||
export const useProxyLatency = (
|
||||
|
||||
@@ -9,10 +9,11 @@ import {
|
||||
HealthyBadge,
|
||||
NotHealthyBadge,
|
||||
} from "components/DeploySettingsLayout/Badges"
|
||||
import { makeStyles, useTheme } from "@mui/styles"
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import { combineClasses } from "utils/combineClasses"
|
||||
import { ProxyLatencyReport } from "contexts/useProxyLatency"
|
||||
import { getLatencyColor } from "utils/colors"
|
||||
import { alpha } from "@mui/material/styles"
|
||||
|
||||
export const ProxyRow: FC<{
|
||||
latency?: ProxyLatencyReport
|
||||
@@ -21,7 +22,6 @@ export const ProxyRow: FC<{
|
||||
preferred: boolean
|
||||
}> = ({ proxy, onSelectRegion, preferred, latency }) => {
|
||||
const styles = useStyles()
|
||||
const theme = useTheme()
|
||||
|
||||
const clickable = useClickableTableRow(() => {
|
||||
onSelectRegion(proxy)
|
||||
@@ -47,24 +47,32 @@ export const ProxyRow: FC<{
|
||||
}
|
||||
avatar={
|
||||
proxy.icon_url !== "" && (
|
||||
<Avatar src={proxy.icon_url} variant="square" fitImage />
|
||||
<Avatar
|
||||
size="sm"
|
||||
src={proxy.icon_url}
|
||||
variant="square"
|
||||
fitImage
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{proxy.path_app_url}</TableCell>
|
||||
<TableCell>
|
||||
<TableCell sx={{ fontSize: 14 }}>{proxy.path_app_url}</TableCell>
|
||||
<TableCell sx={{ fontSize: 14 }}>
|
||||
<ProxyStatus proxy={proxy} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
style={{
|
||||
color: latency ? getLatencyColor(theme, latency.latencyMS) : "",
|
||||
}}
|
||||
>
|
||||
{latency ? `${latency.latencyMS.toFixed(1)} ms` : "?"}
|
||||
</span>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
textAlign: "right",
|
||||
color: (theme) =>
|
||||
latency
|
||||
? getLatencyColor(theme, latency.latencyMS)
|
||||
: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
{latency ? `${latency.latencyMS.toFixed(0)} ms` : "Not available"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
@@ -83,9 +91,11 @@ const ProxyStatus: FC<{
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
preferredrow: {
|
||||
// TODO: What is the best way to show what proxy is currently being used?
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
outline: `3px solid ${theme.palette.secondary.light}`,
|
||||
outlineOffset: -3,
|
||||
backgroundColor: alpha(
|
||||
theme.palette.primary.main,
|
||||
theme.palette.action.hoverOpacity,
|
||||
),
|
||||
outline: `1px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: "-1px",
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -50,7 +50,9 @@ export const WorkspaceProxyView: FC<
|
||||
<TableCell width="40%">Proxy</TableCell>
|
||||
<TableCell width="30%">URL</TableCell>
|
||||
<TableCell width="10%">Status</TableCell>
|
||||
<TableCell width="20%">Latency</TableCell>
|
||||
<TableCell width="20%" sx={{ textAlign: "right" }}>
|
||||
Latency
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
||||
Reference in New Issue
Block a user