Skip to content

Commit bed37b4

Browse files
authored
feat: use wildcard url for local links in the web terminal (coder#6070)
1 parent 135a4d8 commit bed37b4

File tree

4 files changed

+123
-21
lines changed

4 files changed

+123
-21
lines changed

site/src/components/PortForwardButton/PortForwardButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export interface PortForwardButtonProps {
2626
agentId: string
2727
}
2828

29-
const portForwardURL = (
29+
export const portForwardURL = (
3030
host: string,
3131
port: number,
3232
agentName: string,

site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
293293
</div>
294294

295295
<div className={styles.editorPane}>
296-
<div className={styles.editor}>
296+
<div className={styles.editor} data-chromatic="ignore">
297297
{activeFile ? (
298298
<MonacoEditor
299299
value={activeFile?.content}

site/src/pages/TerminalPage/TerminalPage.tsx

+54-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { makeStyles } from "@material-ui/core/styles"
22
import { useMachine } from "@xstate/react"
3+
import { portForwardURL } from "components/PortForwardButton/PortForwardButton"
34
import { Stack } from "components/Stack/Stack"
4-
import { FC, useEffect, useRef, useState } from "react"
5+
import { FC, useCallback, useEffect, useRef, useState } from "react"
56
import { Helmet } from "react-helmet-async"
67
import { useNavigate, useParams, useSearchParams } from "react-router-dom"
78
import { colors } from "theme/colors"
@@ -95,9 +96,55 @@ const TerminalPage: FC<
9596
workspaceAgentError,
9697
workspaceAgent,
9798
websocketError,
99+
applicationsHost,
98100
} = terminalState.context
99101
const reloading = useReloading(isDisconnected)
100102

103+
// handleWebLink handles opening of URLs in the terminal!
104+
const handleWebLink = useCallback(
105+
(uri: string) => {
106+
if (!workspaceAgent || !workspace || !username || !applicationsHost) {
107+
return
108+
}
109+
110+
const open = (uri: string) => {
111+
// Copied from: https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-web-links/src/WebLinksAddon.ts#L23
112+
const newWindow = window.open()
113+
if (newWindow) {
114+
try {
115+
newWindow.opener = null
116+
} catch {
117+
// no-op, Electron can throw
118+
}
119+
newWindow.location.href = uri
120+
} else {
121+
console.warn("Opening link blocked as opener could not be cleared")
122+
}
123+
}
124+
125+
try {
126+
const url = new URL(uri)
127+
const localHosts = ["0.0.0.0", "127.0.0.1", "localhost"]
128+
if (!localHosts.includes(url.hostname)) {
129+
open(uri)
130+
return
131+
}
132+
open(
133+
portForwardURL(
134+
applicationsHost,
135+
parseInt(url.port),
136+
workspaceAgent.name,
137+
workspace,
138+
username,
139+
),
140+
)
141+
} catch (ex) {
142+
open(uri)
143+
}
144+
},
145+
[workspaceAgent, workspace, username, applicationsHost],
146+
)
147+
101148
// Create the terminal!
102149
useEffect(() => {
103150
if (!xtermRef.current) {
@@ -116,7 +163,11 @@ const TerminalPage: FC<
116163
const fitAddon = new FitAddon()
117164
setFitAddon(fitAddon)
118165
terminal.loadAddon(fitAddon)
119-
terminal.loadAddon(new WebLinksAddon())
166+
terminal.loadAddon(
167+
new WebLinksAddon((_, uri) => {
168+
handleWebLink(uri)
169+
}),
170+
)
120171
terminal.onData((data) => {
121172
sendEvent({
122173
type: "WRITE",
@@ -145,7 +196,7 @@ const TerminalPage: FC<
145196
window.removeEventListener("resize", listener)
146197
terminal.dispose()
147198
}
148-
}, [renderer, sendEvent, xtermRef])
199+
}, [renderer, sendEvent, xtermRef, handleWebLink])
149200

150201
// Triggers the initial terminal connection using
151202
// the reconnection token and workspace name found

site/src/xServices/terminal/terminalXService.ts

+67-16
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface TerminalContext {
1010
workspaceAgentError?: Error | unknown
1111
websocket?: WebSocket
1212
websocketError?: Error | unknown
13+
applicationsHost?: string
1314

1415
// Assigned by connecting!
1516
// The workspace agent is entirely optional. If the agent is omitted the
@@ -47,6 +48,9 @@ export const terminalMachine =
4748
getWorkspace: {
4849
data: TypesGen.Workspace
4950
}
51+
getApplicationsHost: {
52+
data: TypesGen.AppHostResponse
53+
}
5054
getWorkspaceAgent: {
5155
data: TypesGen.WorkspaceAgent
5256
}
@@ -55,24 +59,61 @@ export const terminalMachine =
5559
}
5660
},
5761
},
58-
initial: "gettingWorkspace",
62+
initial: "setup",
5963
states: {
60-
gettingWorkspace: {
61-
invoke: {
62-
src: "getWorkspace",
63-
id: "getWorkspace",
64-
onDone: [
65-
{
66-
actions: ["assignWorkspace", "clearWorkspaceError"],
67-
target: "gettingWorkspaceAgent",
64+
setup: {
65+
type: "parallel",
66+
states: {
67+
getApplicationsHost: {
68+
initial: "gettingApplicationsHost",
69+
states: {
70+
gettingApplicationsHost: {
71+
invoke: {
72+
src: "getApplicationsHost",
73+
id: "getApplicationsHost",
74+
onDone: {
75+
actions: [
76+
"assignApplicationsHost",
77+
"clearApplicationsHostError",
78+
],
79+
target: "success",
80+
},
81+
},
82+
},
83+
success: {
84+
type: "final",
85+
},
6886
},
69-
],
70-
onError: [
71-
{
72-
actions: "assignWorkspaceError",
73-
target: "disconnected",
87+
},
88+
getWorkspace: {
89+
initial: "gettingWorkspace",
90+
states: {
91+
gettingWorkspace: {
92+
invoke: {
93+
src: "getWorkspace",
94+
id: "getWorkspace",
95+
onDone: [
96+
{
97+
actions: ["assignWorkspace", "clearWorkspaceError"],
98+
target: "success",
99+
},
100+
],
101+
onError: [
102+
{
103+
actions: "assignWorkspaceError",
104+
target: "success",
105+
},
106+
],
107+
},
108+
},
109+
success: {
110+
type: "final",
111+
},
74112
},
75-
],
113+
},
114+
},
115+
onDone: {
116+
target: "gettingWorkspaceAgent",
76117
},
77118
},
78119
gettingWorkspaceAgent: {
@@ -129,7 +170,7 @@ export const terminalMachine =
129170
on: {
130171
CONNECT: {
131172
actions: "assignConnection",
132-
target: "gettingWorkspace",
173+
target: "gettingWorkspaceAgent",
133174
},
134175
},
135176
},
@@ -146,6 +187,9 @@ export const terminalMachine =
146187
context.workspaceName,
147188
)
148189
},
190+
getApplicationsHost: async () => {
191+
return API.getApplicationsHost()
192+
},
149193
getWorkspaceAgent: async (context) => {
150194
if (!context.workspace || !context.workspaceName) {
151195
throw new Error("workspace or workspace name is not set")
@@ -218,6 +262,13 @@ export const terminalMachine =
218262
...context,
219263
workspaceError: undefined,
220264
})),
265+
assignApplicationsHost: assign({
266+
applicationsHost: (_, { data }) => data.host,
267+
}),
268+
clearApplicationsHostError: assign((context) => ({
269+
...context,
270+
applicationsHostError: undefined,
271+
})),
221272
assignWorkspaceAgent: assign({
222273
workspaceAgent: (_, event) => event.data,
223274
}),

0 commit comments

Comments
 (0)