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 +}