8000 feat: use wildcard url for local links in the web terminal (#6070) · coder/coder@bed37b4 · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit bed37b4

Browse files
authored
feat: use wildcard url for local links in the web terminal (#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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 54 additions & 3 deletions
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

Lines changed: 67 additions & 16 deletions
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)
0