Skip to content

Commit a5f8300

Browse files
refactor(site): do not block the screen when disconnected (#10057)
Close #9858 Demo: https://github.com/coder/coder/assets/3165839/d6d4d2d7-487a-4739-86c9-042242d9820f
1 parent 2d6c4fe commit a5f8300

File tree

4 files changed

+154
-278
lines changed

4 files changed

+154
-278
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import Button from "@mui/material/Button";
2+
import Link from "@mui/material/Link";
3+
import { Alert, AlertProps } from "components/Alert/Alert";
4+
import { useState } from "react";
5+
import { docs } from "utils/docs";
6+
7+
export const ErrorScriptAlert = () => {
8+
return (
9+
<TerminalAlert
10+
severity="warning"
11+
dismissible
12+
actions={<RefreshSessionButton />}
13+
>
14+
The workspace{" "}
15+
<Link
16+
title="startup script has exited with an error"
17+
href={docs("/templates#startup-script-exited-with-an-error")}
18+
target="_blank"
19+
rel="noreferrer"
20+
>
21+
startup script has exited with an error
22+
</Link>
23+
, we recommend reloading this session and{" "}
24+
<Link
25+
title=" debugging the startup script"
26+
href={docs("/templates#debugging-the-startup-script")}
27+
target="_blank"
28+
rel="noreferrer"
29+
>
30+
debugging the startup script
31+
</Link>{" "}
32+
because{" "}
33+
<Link
34+
title="your workspace may be incomplete."
35+
href={docs("/templates#your-workspace-may-be-incomplete")}
36+
target="_blank"
37+
rel="noreferrer"
38+
>
39+
your workspace may be incomplete.
40+
</Link>{" "}
41+
</TerminalAlert>
42+
);
43+
};
44+
45+
export const LoadingScriptsAlert = () => {
46+
return (
47+
<TerminalAlert
48+
dismissible
49+
severity="info"
50+
actions={<RefreshSessionButton />}
51+
>
52+
Startup scripts are still running. You can continue using this terminal,
53+
but{" "}
54+
<Link
55+
title="your workspace may be incomplete."
56+
href={docs("/templates#your-workspace-may-be-incomplete")}
57+
target="_blank"
58+
rel="noreferrer"
59+
>
60+
{" "}
61+
your workspace may be incomplete.
62+
</Link>
63+
</TerminalAlert>
64+
);
65+
};
66+
67+
export const LoadedScriptsAlert = () => {
68+
return (
69+
<TerminalAlert
70+
severity="success"
71+
dismissible
72+
actions={<RefreshSessionButton />}
73+
>
74+
Startup scripts have completed successfully. The workspace is ready but
75+
this{" "}
76+
<Link
77+
title="session was started before the startup scripts finished"
78+
href={docs("/templates#your-workspace-may-be-incomplete")}
79+
target="_blank"
80+
rel="noreferrer"
81+
>
82+
session was started before the startup script finished.
83+
</Link>{" "}
84+
To ensure your shell environment is up-to-date, we recommend reloading
85+
this session.
86+
</TerminalAlert>
87+
);
88+
};
89+
90+
const TerminalAlert = (props: AlertProps) => {
91+
return (
92+
<Alert
93+
{...props}
94+
sx={{
95+
borderRadius: 0,
96+
borderWidth: 0,
97+
borderBottomWidth: 1,
98+
borderBottomColor: (theme) => theme.palette.divider,
99+
backgroundColor: (theme) => theme.palette.background.paperLight,
100+
borderLeft: (theme) =>
101+
`3px solid ${theme.palette[props.severity!].light}`,
102+
marginBottom: 1,
103+
}}
104+
/>
105+
);
106+
};
107+
108+
export const DisconnectedAlert = (props: AlertProps) => {
109+
return (
110+
<TerminalAlert
111+
{...props}
112+
severity="warning"
113+
actions={<RefreshSessionButton />}
114+
>
115+
Disconnected
116+
</TerminalAlert>
117+
);
118+
};
119+
120+
const RefreshSessionButton = () => {
121+
const [isRefreshing, setIsRefreshing] = useState(false);
122+
123+
return (
124+
<Button
125+
disabled={isRefreshing}
126+
size="small"
127+
variant="text"
128+
onClick={() => {
129+
setIsRefreshing(true);
130+
window.location.reload();
131+
}}
132+
>
133+
{isRefreshing ? "Refreshing session..." : "Refresh session"}
134+
</Button>
135+
);
136+
};

site/src/pages/TerminalPage/TerminalPage.tsx

Lines changed: 18 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { makeStyles, useTheme } from "@mui/styles";
22
import { useMachine } from "@xstate/react";
3-
import { Stack } from "components/Stack/Stack";
43
import { FC, useCallback, useEffect, useRef, useState } from "react";
54
import { Helmet } from "react-helmet-async";
65
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
@@ -18,45 +17,24 @@ import { terminalMachine } from "xServices/terminal/terminalXService";
1817
import { useProxy } from "contexts/ProxyContext";
1918
import Box from "@mui/material/Box";
2019
import { useDashboard } from "components/Dashboard/DashboardProvider";
21-
import { Region, WorkspaceAgent } from "api/typesGenerated";
20+
import { Region } from "api/typesGenerated";
2221
import { getLatencyColor } from "utils/latency";
2322
import Popover from "@mui/material/Popover";
2423
import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency";
25-
import TerminalPageAlert, { TerminalPageAlertType } from "./TerminalPageAlert";
2624
import { portForwardURL } from "utils/portForward";
25+
import {
26+
DisconnectedAlert,
27+
ErrorScriptAlert,
28+
LoadedScriptsAlert,
29+
LoadingScriptsAlert,
30+
} from "./TerminalAlerts";
2731

2832
export const Language = {
2933
workspaceErrorMessagePrefix: "Unable to fetch workspace: ",
3034
workspaceAgentErrorMessagePrefix: "Unable to fetch workspace agent: ",
3135
websocketErrorMessagePrefix: "WebSocket failed: ",
3236
};
3337

34-
const useTerminalWarning = ({ agent }: { agent?: WorkspaceAgent }) => {
35-
const lifecycleState = agent?.lifecycle_state;
36-
const [startupWarning, setStartupWarning] = useState<
37-
TerminalPageAlertType | undefined
38-
>(undefined);
39-
40-
useEffect(() => {
41-
if (lifecycleState === "start_error") {
42-
setStartupWarning("error");
43-
} else if (lifecycleState === "starting") {
44-
setStartupWarning("starting");
45-
} else {
46-
setStartupWarning((prev) => {
47-
if (prev === "starting") {
48-
return "success";
49-
}
50-
return undefined;
51-
});
52-
}
53-
}, [lifecycleState]);
54-
55-
return {
56-
startupWarning,
57-
};
58-
};
59-
6038
type TerminalPageProps = React.PropsWithChildren<{
6139
renderer: "webgl" | "dom";
6240
}>;
@@ -110,16 +88,18 @@ const TerminalPage: FC<TerminalPageProps> = ({ renderer }) => {
11088
workspaceAgent,
11189
websocketError,
11290
} = terminalState.context;
113-
const reloading = useReloading(isDisconnected);
11491
const dashboard = useDashboard();
11592
const proxyContext = useProxy();
11693
const selectedProxy = proxyContext.proxy.proxy;
11794
const latency = selectedProxy
11895
? proxyContext.proxyLatencies[selectedProxy.id]
11996
: undefined;
120-
const { startupWarning } = useTerminalWarning({
121-
agent: workspaceAgent,
122-
});
97+
98+
const lifecycleState = workspaceAgent?.lifecycle_state;
99+
const prevLifecycleState = useRef(lifecycleState);
100+
useEffect(() => {
101+
prevLifecycleState.current = lifecycleState;
102+
}, [lifecycleState]);
123103

124104
// handleWebLink handles opening of URLs in the terminal!
125105
const handleWebLink = useCallback(
@@ -326,29 +306,12 @@ const TerminalPage: FC<TerminalPageProps> = ({ renderer }) => {
326306
: ""}
327307
</title>
328308
</Helmet>
329-
{/* This overlay makes it more obvious that the terminal is disconnected. */}
330-
{/* It's nice for situations where Coder restarts, and they are temporarily disconnected. */}
331-
<div className={`${styles.overlay} ${isDisconnected ? "" : "connected"}`}>
332-
{reloading.status === "reloading" ? (
333-
<span className={styles.overlayText}>Reloading...</span>
334-
) : (
335-
<Stack spacing={0.5} alignItems="center">
336-
<span className={styles.overlayText}>Disconnected</span>
337-
<span className={styles.overlaySubtext}>
338-
Press any key to retry
339-
</span>
340-
</Stack>
341-
)}
342-
</div>
343309
<Box display="flex" flexDirection="column" height="100vh">
344-
{startupWarning && (
345-
<TerminalPageAlert
346-
alertType={startupWarning}
347-
onDismiss={() => {
348-
fitAddon?.fit();
349-
}}
350-
/>
351-
)}
310+
{lifecycleState === "start_error" && <ErrorScriptAlert />}
311+
{lifecycleState === "starting" && <LoadingScriptsAlert />}
312+
{lifecycleState === "ready" &&
313+
prevLifecycleState.current === "starting" && <LoadedScriptsAlert />}
314+
{isDisconnected && <DisconnectedAlert />}
352315
<div
353316
className={styles.terminal}
354317
ref={xtermRef}
@@ -463,61 +426,6 @@ const BottomBar = ({ proxy, latency }: { proxy: Region; latency?: number }) => {
463426
);
464427
};
465428

466-
const useReloading = (isDisconnected: boolean) => {
467-
const [status, setStatus] = useState<"reloading" | "notReloading">(
468-
"notReloading",
469-
);
470-
471-
// Retry connection on key press when it is disconnected
472-
useEffect(() => {
473-
if (!isDisconnected || status === "reloading") {
474-
return;
475-
}
476-
477-
// Modifier keys should not trigger a reload.
478-
const ignoredKeys = [
479-
"Alt",
480-
"AltGraph",
481-
"CapsLock",
482-
"Control",
483-
"Fn",
484-
"FnLock",
485-
"Meta",
486-
"NumLock",
487-
"ScrollLock",
488-
"Shift",
489-
"Symbol",
490-
"SymbolLock",
491-
];
492-
493-
const keyDownHandler = (event: KeyboardEvent) => {
494-
// In addition to ignored keys, avoid reloading while modifiers are held
495-
// to cover cases where the terminal unexpectedly tries to reconnect like
496-
// when pressing ctrl+w, ctrl+r, and so on.
497-
if (
498-
!ignoredKeys.includes(event.key) &&
499-
!event.altKey &&
500-
!event.ctrlKey &&
501-
!event.metaKey &&
502-
!event.shiftKey
503-
) {
504-
setStatus("reloading");
505-
window.location.reload();
506-
}
507-
};
508-
509-
document.addEventListener("keydown", keyDownHandler, true);
510-
511-
return () => {
512-
document.removeEventListener("keydown", keyDownHandler, true);
513-
};
514-
}, [status, isDisconnected]);
515-
516-
return {
517-
status,
518-
};
519-
};
520-
521429
const useStyles = makeStyles((theme) => ({
522430
overlay: {
523431
position: "absolute",

site/src/pages/TerminalPage/TerminalPageAlert.stories.tsx

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)