From a5f8300c76c1a741c24f761d43e3c31a02b33ba9 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 5 Oct 2023 18:06:26 -0300 Subject: [PATCH] refactor(site): do not block the screen when disconnected (#10057) Close https://github.com/coder/coder/issues/9858 Demo: https://github.com/coder/coder/assets/3165839/d6d4d2d7-487a-4739-86c9-042242d9820f --- .../src/pages/TerminalPage/TerminalAlerts.tsx | 136 ++++++++++++++++++ site/src/pages/TerminalPage/TerminalPage.tsx | 128 +++-------------- .../TerminalPageAlert.stories.tsx | 37 ----- .../pages/TerminalPage/TerminalPageAlert.tsx | 131 ----------------- 4 files changed, 154 insertions(+), 278 deletions(-) create mode 100644 site/src/pages/TerminalPage/TerminalAlerts.tsx delete mode 100644 site/src/pages/TerminalPage/TerminalPageAlert.stories.tsx delete mode 100644 site/src/pages/TerminalPage/TerminalPageAlert.tsx diff --git a/site/src/pages/TerminalPage/TerminalAlerts.tsx b/site/src/pages/TerminalPage/TerminalAlerts.tsx new file mode 100644 index 0000000000..3a5d7ebd66 --- /dev/null +++ b/site/src/pages/TerminalPage/TerminalAlerts.tsx @@ -0,0 +1,136 @@ +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import { Alert, AlertProps } from "components/Alert/Alert"; +import { useState } from "react"; +import { docs } from "utils/docs"; + +export const ErrorScriptAlert = () => { + return ( + } + > + The workspace{" "} + + startup script has exited with an error + + , we recommend reloading this session and{" "} + + debugging the startup script + {" "} + because{" "} + + your workspace may be incomplete. + {" "} + + ); +}; + +export const LoadingScriptsAlert = () => { + return ( + } + > + Startup scripts are still running. You can continue using this terminal, + but{" "} + + {" "} + your workspace may be incomplete. + + + ); +}; + +export const LoadedScriptsAlert = () => { + return ( + } + > + Startup scripts have completed successfully. The workspace is ready but + this{" "} + + session was started before the startup script finished. + {" "} + To ensure your shell environment is up-to-date, we recommend reloading + this session. + + ); +}; + +const TerminalAlert = (props: AlertProps) => { + return ( + theme.palette.divider, + backgroundColor: (theme) => theme.palette.background.paperLight, + borderLeft: (theme) => + `3px solid ${theme.palette[props.severity!].light}`, + marginBottom: 1, + }} + /> + ); +}; + +export const DisconnectedAlert = (props: AlertProps) => { + return ( + } + > + Disconnected + + ); +}; + +const RefreshSessionButton = () => { + const [isRefreshing, setIsRefreshing] = useState(false); + + return ( + + ); +}; diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index da022b7c3c..9292c5c657 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -1,6 +1,5 @@ import { makeStyles, useTheme } from "@mui/styles"; import { useMachine } from "@xstate/react"; -import { Stack } from "components/Stack/Stack"; import { FC, useCallback, useEffect, useRef, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; @@ -18,12 +17,17 @@ import { terminalMachine } from "xServices/terminal/terminalXService"; import { useProxy } from "contexts/ProxyContext"; import Box from "@mui/material/Box"; import { useDashboard } from "components/Dashboard/DashboardProvider"; -import { Region, WorkspaceAgent } from "api/typesGenerated"; +import { Region } from "api/typesGenerated"; import { getLatencyColor } from "utils/latency"; import Popover from "@mui/material/Popover"; import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency"; -import TerminalPageAlert, { TerminalPageAlertType } from "./TerminalPageAlert"; import { portForwardURL } from "utils/portForward"; +import { + DisconnectedAlert, + ErrorScriptAlert, + LoadedScriptsAlert, + LoadingScriptsAlert, +} from "./TerminalAlerts"; export const Language = { workspaceErrorMessagePrefix: "Unable to fetch workspace: ", @@ -31,32 +35,6 @@ export const Language = { websocketErrorMessagePrefix: "WebSocket failed: ", }; -const useTerminalWarning = ({ agent }: { agent?: WorkspaceAgent }) => { - const lifecycleState = agent?.lifecycle_state; - const [startupWarning, setStartupWarning] = useState< - TerminalPageAlertType | undefined - >(undefined); - - useEffect(() => { - if (lifecycleState === "start_error") { - setStartupWarning("error"); - } else if (lifecycleState === "starting") { - setStartupWarning("starting"); - } else { - setStartupWarning((prev) => { - if (prev === "starting") { - return "success"; - } - return undefined; - }); - } - }, [lifecycleState]); - - return { - startupWarning, - }; -}; - type TerminalPageProps = React.PropsWithChildren<{ renderer: "webgl" | "dom"; }>; @@ -110,16 +88,18 @@ const TerminalPage: FC = ({ renderer }) => { workspaceAgent, websocketError, } = terminalState.context; - const reloading = useReloading(isDisconnected); const dashboard = useDashboard(); const proxyContext = useProxy(); const selectedProxy = proxyContext.proxy.proxy; const latency = selectedProxy ? proxyContext.proxyLatencies[selectedProxy.id] : undefined; - const { startupWarning } = useTerminalWarning({ - agent: workspaceAgent, - }); + + const lifecycleState = workspaceAgent?.lifecycle_state; + const prevLifecycleState = useRef(lifecycleState); + useEffect(() => { + prevLifecycleState.current = lifecycleState; + }, [lifecycleState]); // handleWebLink handles opening of URLs in the terminal! const handleWebLink = useCallback( @@ -326,29 +306,12 @@ const TerminalPage: FC = ({ renderer }) => { : ""} - {/* This overlay makes it more obvious that the terminal is disconnected. */} - {/* It's nice for situations where Coder restarts, and they are temporarily disconnected. */} -
- {reloading.status === "reloading" ? ( - Reloading... - ) : ( - - Disconnected - - Press any key to retry - - - )} -
- {startupWarning && ( - { - fitAddon?.fit(); - }} - /> - )} + {lifecycleState === "start_error" && } + {lifecycleState === "starting" && } + {lifecycleState === "ready" && + prevLifecycleState.current === "starting" && } + {isDisconnected && }
{ ); }; -const useReloading = (isDisconnected: boolean) => { - const [status, setStatus] = useState<"reloading" | "notReloading">( - "notReloading", - ); - - // Retry connection on key press when it is disconnected - useEffect(() => { - if (!isDisconnected || status === "reloading") { - return; - } - - // Modifier keys should not trigger a reload. - const ignoredKeys = [ - "Alt", - "AltGraph", - "CapsLock", - "Control", - "Fn", - "FnLock", - "Meta", - "NumLock", - "ScrollLock", - "Shift", - "Symbol", - "SymbolLock", - ]; - - const keyDownHandler = (event: KeyboardEvent) => { - // In addition to ignored keys, avoid reloading while modifiers are held - // to cover cases where the terminal unexpectedly tries to reconnect like - // when pressing ctrl+w, ctrl+r, and so on. - if ( - !ignoredKeys.includes(event.key) && - !event.altKey && - !event.ctrlKey && - !event.metaKey && - !event.shiftKey - ) { - setStatus("reloading"); - window.location.reload(); - } - }; - - document.addEventListener("keydown", keyDownHandler, true); - - return () => { - document.removeEventListener("keydown", keyDownHandler, true); - }; - }, [status, isDisconnected]); - - return { - status, - }; -}; - const useStyles = makeStyles((theme) => ({ overlay: { position: "absolute", diff --git a/site/src/pages/TerminalPage/TerminalPageAlert.stories.tsx b/site/src/pages/TerminalPage/TerminalPageAlert.stories.tsx deleted file mode 100644 index 265769c5ce..0000000000 --- a/site/src/pages/TerminalPage/TerminalPageAlert.stories.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; - -import TerminalPageAlert from "./TerminalPageAlert"; - -const meta: Meta = { - component: TerminalPageAlert, - title: "components/TerminalPageAlert", - argTypes: { - alertType: { - control: { - type: "radio", - }, - options: ["error", "starting", "success"], - }, - }, -}; -type Story = StoryObj; - -export const Error: Story = { - args: { - alertType: "error", - }, -}; - -export const Starting: Story = { - args: { - alertType: "starting", - }, -}; - -export const Success: Story = { - args: { - alertType: "success", - }, -}; - -export default meta; diff --git a/site/src/pages/TerminalPage/TerminalPageAlert.tsx b/site/src/pages/TerminalPage/TerminalPageAlert.tsx deleted file mode 100644 index 6c5e3862fb..0000000000 --- a/site/src/pages/TerminalPage/TerminalPageAlert.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { AlertColor } from "@mui/material/Alert/Alert"; -import Button from "@mui/material/Button"; -import Link from "@mui/material/Link"; -import { Alert } from "components/Alert/Alert"; -import { ReactNode } from "react"; -import { docs } from "utils/docs"; - -export type TerminalPageAlertType = "error" | "starting" | "success"; - -type MapAlertTypeToComponent = { - [key in TerminalPageAlertType]: { - severity: AlertColor; - children: ReactNode | undefined; - }; -}; - -const mapAlertTypeToText: MapAlertTypeToComponent = { - error: { - severity: "warning", - children: ( - <> - The workspace{" "} - - startup script has exited with an error - - , we recommend reloading this session and{" "} - - debugging the startup script - {" "} - because{" "} - - your workspace may be incomplete. - {" "} - - ), - }, - starting: { - severity: "info", - children: ( - <> - Startup scripts are still running. You can continue using this terminal, - but{" "} - - {" "} - your workspace may be incomplete. - - - ), - }, - success: { - severity: "success", - children: ( - <> - Startup scripts have completed successfully. The workspace is ready but - this{" "} - - session was started before the startup script finished. - {" "} - To ensure your shell environment is up-to-date, we recommend reloading - this session. - - ), - }, -}; - -export default ({ - alertType, - onDismiss, -}: { - alertType: TerminalPageAlertType; - onDismiss: () => void; -}) => { - const severity = mapAlertTypeToText[alertType].severity; - return ( - theme.palette.divider, - backgroundColor: (theme) => theme.palette.background.paperLight, - borderLeft: (theme) => `3px solid ${theme.palette[severity].light}`, - marginBottom: 1, - }} - onDismiss={onDismiss} - dismissible - actions={[ - , - ]} - > - {mapAlertTypeToText[alertType].children} - - ); -};