diff --git a/site/src/components/Dashboard/DashboardLayout.test.tsx b/site/src/components/Dashboard/DashboardLayout.test.tsx
index 49ca5e30576c0..8897aaca8a6ea 100644
--- a/site/src/components/Dashboard/DashboardLayout.test.tsx
+++ b/site/src/components/Dashboard/DashboardLayout.test.tsx
@@ -1,4 +1,3 @@
-import { Route, Routes } from "react-router-dom"
import { renderWithAuth } from "testHelpers/renderHelpers"
import { DashboardLayout } from "./DashboardLayout"
import * as API from "api/api"
@@ -10,12 +9,8 @@ test("Show the new Coder version notification", async () => {
version: "v0.12.9",
url: "https://github.com/coder/coder/releases/tag/v0.12.9",
})
- renderWithAuth(
-
- }>
- Test page} />
-
- ,
- )
+ renderWithAuth(, {
+ children: [{ element:
Test page
}],
+ })
await screen.findByTestId("update-check-snackbar")
})
diff --git a/site/src/components/Navbar/NavbarView.tsx b/site/src/components/Navbar/NavbarView.tsx
index f5e7bdbefb0c2..ca682ab1adae4 100644
--- a/site/src/components/Navbar/NavbarView.tsx
+++ b/site/src/components/Navbar/NavbarView.tsx
@@ -2,7 +2,7 @@ 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, useTheme } from "@mui/styles"
+import { makeStyles } from "@mui/styles"
import MenuIcon from "@mui/icons-material/Menu"
import { CoderIcon } from "components/Icons/CoderIcon"
import { FC, useRef, useState } from "react"
@@ -20,10 +20,9 @@ import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutl
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"
import { BUTTON_SM_HEIGHT } from "theme/theme"
+import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency"
export const USERS_LINK = `/users?filter=${encodeURIComponent("status:active")}`
@@ -232,7 +231,6 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
{selectedProxy.display_name}
@@ -277,10 +275,7 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
/>
{proxy.display_name}
-
+
))}
@@ -301,42 +296,6 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
)
}
-const ProxyStatusLatency: FC<{ proxy: TypesGen.Region; latency?: number }> = ({
- proxy,
- latency,
-}) => {
- const theme = useTheme()
- let color = theme.palette.success.light
-
- if (!latency) {
- return (
-
- theme.palette.text.secondary,
- }}
- />
-
- )
- }
-
- if (latency >= 300) {
- color = theme.palette.error.light
- }
-
- if (!proxy.healthy || latency >= 100) {
- color = theme.palette.warning.light
- }
-
- return (
-
- {latency.toFixed(0)}ms
-
- )
-}
-
const useStyles = makeStyles((theme) => ({
root: {
height: navHeight,
diff --git a/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx
new file mode 100644
index 0000000000000..6988050e5594e
--- /dev/null
+++ b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx
@@ -0,0 +1,31 @@
+import { useTheme } from "@mui/material/styles"
+import HelpOutline from "@mui/icons-material/HelpOutline"
+import Box from "@mui/material/Box"
+import Tooltip from "@mui/material/Tooltip"
+import { FC } from "react"
+import { getLatencyColor } from "utils/latency"
+
+export const ProxyStatusLatency: FC<{ latency?: number }> = ({ latency }) => {
+ const theme = useTheme()
+ const color = getLatencyColor(theme, latency)
+
+ if (!latency) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+ {latency.toFixed(0)}ms
+
+ )
+}
diff --git a/site/src/components/Resources/AgentLatency.tsx b/site/src/components/Resources/AgentLatency.tsx
index e604ab0787a82..04f790980c977 100644
--- a/site/src/components/Resources/AgentLatency.tsx
+++ b/site/src/components/Resources/AgentLatency.tsx
@@ -8,7 +8,7 @@ import {
} from "components/Tooltips/HelpTooltip"
import { Stack } from "components/Stack/Stack"
import { WorkspaceAgent, DERPRegion } from "api/typesGenerated"
-import { getLatencyColor } from "utils/colors"
+import { getLatencyColor } from "utils/latency"
const getDisplayLatency = (theme: Theme, agent: WorkspaceAgent) => {
// Find the right latency to display
diff --git a/site/src/pages/TerminalPage/TerminalPage.test.tsx b/site/src/pages/TerminalPage/TerminalPage.test.tsx
index fb117b19b91bd..8268832fef1b1 100644
--- a/site/src/pages/TerminalPage/TerminalPage.test.tsx
+++ b/site/src/pages/TerminalPage/TerminalPage.test.tsx
@@ -3,19 +3,18 @@ import "jest-canvas-mock"
import WS from "jest-websocket-mock"
import { rest } from "msw"
import {
- MockPrimaryWorkspaceProxy,
- MockProxyLatencies,
+ MockUser,
MockWorkspace,
MockWorkspaceAgent,
- MockWorkspaceProxies,
} from "testHelpers/entities"
import { TextDecoder, TextEncoder } from "util"
import { ReconnectingPTYRequest } from "../../api/types"
-import { history, render } from "../../testHelpers/renderHelpers"
+import {
+ renderWithAuth,
+ waitForLoaderToBeRemoved,
+} from "../../testHelpers/renderHelpers"
import { server } from "../../testHelpers/server"
import TerminalPage, { Language } from "./TerminalPage"
-import { Route, Routes } from "react-router-dom"
-import { ProxyContext } from "contexts/ProxyContext"
Object.defineProperty(window, "matchMedia", {
writable: true,
@@ -35,56 +34,35 @@ Object.defineProperty(window, "TextEncoder", {
value: TextEncoder,
})
-const renderTerminal = () => {
- // @emyrk using renderWithAuth would be best here, but I was unable to get it to work.
- return render(
-
-
-
-
- }
- />
- ,
- )
+const renderTerminal = async (
+ route = `/${MockUser.username}/${MockWorkspace.name}/terminal`,
+) => {
+ const utils = renderWithAuth(, {
+ route,
+ path: "/:username/:workspace/terminal",
+ })
+ await waitForLoaderToBeRemoved()
+ return utils
}
const expectTerminalText = (container: HTMLElement, text: string) => {
- return waitFor(() => {
- const elements = container.getElementsByClassName("xterm-rows")
- if (elements.length === 0) {
- throw new Error("no xterm-rows")
- }
- const row = elements[0] as HTMLDivElement
- if (!row.textContent) {
- throw new Error("no text content")
- }
- expect(row.textContent).toContain(text)
- })
+ return waitFor(
+ () => {
+ const elements = container.getElementsByClassName("xterm-rows")
+ if (elements.length === 0) {
+ throw new Error("no xterm-rows")
+ }
+ const row = elements[0] as HTMLDivElement
+ if (!row.textContent) {
+ throw new Error("no text content")
+ }
+ expect(row.textContent).toContain(text)
+ },
+ { timeout: 3_000 },
+ )
}
describe("TerminalPage", () => {
- beforeEach(() => {
- history.push(`/some-user/${MockWorkspace.name}/terminal`)
- })
-
it("shows an error if fetching workspace fails", async () => {
// Given
server.use(
@@ -97,7 +75,7 @@ describe("TerminalPage", () => {
)
// When
- const { container } = renderTerminal()
+ const { container } = await renderTerminal()
// Then
await expectTerminalText(container, Language.workspaceErrorMessagePrefix)
@@ -112,7 +90,7 @@ describe("TerminalPage", () => {
)
// When
- const { container } = renderTerminal()
+ const { container } = await renderTerminal()
// Then
await expectTerminalText(container, Language.websocketErrorMessagePrefix)
@@ -120,59 +98,58 @@ describe("TerminalPage", () => {
it("renders data from the backend", async () => {
// Given
- const server = new WS(
- "ws://localhost/api/v2/workspaceagents/" + MockWorkspaceAgent.id + "/pty",
+ const ws = new WS(
+ `ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`,
)
const text = "something to render"
// When
- const { container } = renderTerminal()
+ const { container } = await renderTerminal()
// Then
- await server.connected
- server.send(text)
+ await ws.connected
+ ws.send(text)
await expectTerminalText(container, text)
- server.close()
+ ws.close()
})
it("resizes on connect", async () => {
// Given
- const server = new WS(
- "ws://localhost/api/v2/workspaceagents/" + MockWorkspaceAgent.id + "/pty",
+ const ws = new WS(
+ `ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`,
)
// When
- renderTerminal()
+ await renderTerminal()
// Then
- await server.connected
- const msg = await server.nextMessage
+ await ws.connected
+ const msg = await ws.nextMessage
const req: ReconnectingPTYRequest = JSON.parse(
new TextDecoder().decode(msg as Uint8Array),
)
expect(req.height).toBeGreaterThan(0)
expect(req.width).toBeGreaterThan(0)
- server.close()
+ ws.close()
})
it("supports workspace.agent syntax", async () => {
// Given
- const server = new WS(
- "ws://localhost/api/v2/workspaceagents/" + MockWorkspaceAgent.id + "/pty",
+ const ws = new WS(
+ `ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`,
)
const text = "something to render"
// When
- history.push(
+ const { container } = await renderTerminal(
`/some-user/${MockWorkspace.name}.${MockWorkspaceAgent.name}/terminal`,
)
- const { container } = renderTerminal()
// Then
- await server.connected
- server.send(text)
+ await ws.connected
+ ws.send(text)
await expectTerminalText(container, text)
- server.close()
+ ws.close()
})
})
diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx
index 811b73e8a8916..7c3ccafb018d1 100644
--- a/site/src/pages/TerminalPage/TerminalPage.tsx
+++ b/site/src/pages/TerminalPage/TerminalPage.tsx
@@ -1,5 +1,5 @@
import Button from "@mui/material/Button"
-import { makeStyles } from "@mui/styles"
+import { makeStyles, useTheme } from "@mui/styles"
import WarningIcon from "@mui/icons-material/ErrorOutlineRounded"
import RefreshOutlined from "@mui/icons-material/RefreshOutlined"
import { useMachine } from "@xstate/react"
@@ -20,6 +20,11 @@ import { terminalMachine } from "../../xServices/terminal/terminalXService"
import { useProxy } from "contexts/ProxyContext"
import { combineClasses } from "utils/combineClasses"
import Box from "@mui/material/Box"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
+import { Region } from "api/typesGenerated"
+import { getLatencyColor } from "utils/latency"
+import Popover from "@mui/material/Popover"
+import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency"
export const Language = {
workspaceErrorMessagePrefix: "Unable to fetch workspace: ",
@@ -81,6 +86,12 @@ const TerminalPage: FC = () => {
const shouldDisplayStartupError = workspaceAgent
? workspaceAgent.lifecycle_state === "start_error"
: false
+ const dashboard = useDashboard()
+ const proxyContext = useProxy()
+ const selectedProxy = proxyContext.proxy.proxy
+ const latency = selectedProxy
+ ? proxyContext.proxyLatencies[selectedProxy.id]
+ : undefined
// handleWebLink handles opening of URLs in the terminal!
const handleWebLink = useCallback(
@@ -342,11 +353,114 @@ const TerminalPage: FC = () => {
ref={xtermRef}
data-testid="terminal"
/>
+ {dashboard.experiments.includes("moons") &&
+ selectedProxy &&
+ latency && (
+
+ )}
>
)
}
+const BottomBar = ({ proxy, latency }: { proxy: Region; latency?: number }) => {
+ const theme = useTheme()
+ const color = getLatencyColor(theme, latency)
+ const anchorRef = useRef(null)
+ const [isOpen, setIsOpen] = useState(false)
+
+ return (
+ theme.spacing(2),
+ background: (theme) => theme.palette.background.paper,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "flex-end",
+ fontSize: 12,
+ }}
+ >
+ setIsOpen(true)}
+ onMouseLeave={() => setIsOpen(false)}
+ sx={{
+ background: "none",
+ cursor: "pointer",
+ display: "flex",
+ alignItems: "center",
+ gap: 1,
+ border: 0,
+ }}
+ >
+
+
+
+ setIsOpen(false)}
+ sx={{
+ pointerEvents: "none",
+ "& .MuiPaper-root": {
+ padding: (theme) => theme.spacing(1, 2),
+ marginTop: -1,
+ },
+ }}
+ anchorOrigin={{
+ vertical: "top",
+ horizontal: "right",
+ }}
+ transformOrigin={{
+ vertical: "bottom",
+ horizontal: "right",
+ }}
+ >
+ theme.palette.text.secondary,
+ fontWeight: 500,
+ }}
+ >
+ Selected proxy
+
+
+
+
+
+
+ {proxy.display_name}
+
+
+
+
+
+ )
+}
+
const useReloading = (isDisconnected: boolean) => {
const [status, setStatus] = useState<"reloading" | "notReloading">(
"notReloading",
diff --git a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx
index 8db1e9493af39..406746e527a5f 100644
--- a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx
+++ b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx
@@ -12,7 +12,7 @@ import {
import { makeStyles } from "@mui/styles"
import { combineClasses } from "utils/combineClasses"
import { ProxyLatencyReport } from "contexts/useProxyLatency"
-import { getLatencyColor } from "utils/colors"
+import { getLatencyColor } from "utils/latency"
import { alpha } from "@mui/material/styles"
export const ProxyRow: FC<{
diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx
index d7e1cc728468a..0110d207df079 100644
--- a/site/src/testHelpers/renderHelpers.tsx
+++ b/site/src/testHelpers/renderHelpers.tsx
@@ -47,6 +47,8 @@ type RenderWithAuthOptions = {
extraRoutes?: RouteObject[]
// The same as extraRoutes but for routes that don't require authentication
nonAuthenticatedRoutes?: RouteObject[]
+ // In case you want to render a layout inside of it
+ children?: RouteObject["children"]
}
export function renderWithAuth(
@@ -56,17 +58,13 @@ export function renderWithAuth(
route = "/",
extraRoutes = [],
nonAuthenticatedRoutes = [],
+ children,
}: RenderWithAuthOptions = {},
) {
const routes: RouteObject[] = [
{
element: ,
- children: [
- {
- element: ,
- children: [{ path, element }, ...extraRoutes],
- },
- ],
+ children: [{ path, element, children }, ...extraRoutes],
},
...nonAuthenticatedRoutes,
]
diff --git a/site/src/utils/colors.ts b/site/src/utils/colors.ts
index f164702719d6f..054e2cb6e98ef 100644
--- a/site/src/utils/colors.ts
+++ b/site/src/utils/colors.ts
@@ -1,5 +1,3 @@
-import { Theme } from "@mui/material/styles"
-
// Used to convert our theme colors to Hex since monaco theme only support hex colors
// From https://www.jameslmilner.com/posts/converting-rgb-hex-hsl-colors/
export function hslToHex(hsl: string): string {
@@ -23,15 +21,3 @@ export function hslToHex(hsl: string): string {
}
return `#${f(0)}${f(8)}${f(4)}`
}
-
-// getLatencyColor is the text color to use for a given latency
-// in milliseconds.
-export const getLatencyColor = (theme: Theme, latency: number) => {
- let color = theme.palette.success.light
- if (latency >= 150 && latency < 300) {
- color = theme.palette.warning.light
- } else if (latency >= 300) {
- color = theme.palette.error.light
- }
- return color
-}
diff --git a/site/src/utils/latency.ts b/site/src/utils/latency.ts
new file mode 100644
index 0000000000000..1767742d4defc
--- /dev/null
+++ b/site/src/utils/latency.ts
@@ -0,0 +1,16 @@
+import { Theme } from "@mui/material/styles"
+
+export const getLatencyColor = (theme: Theme, latency?: number) => {
+ if (!latency) {
+ return theme.palette.text.secondary
+ }
+
+ let color = theme.palette.success.light
+
+ if (latency >= 150 && latency < 300) {
+ color = theme.palette.warning.light
+ } else if (latency >= 300) {
+ color = theme.palette.error.light
+ }
+ return color
+}