feat: use wildcard url for local links in the web terminal (#6070)

This commit is contained in:
Kyle Carberry
2023-02-07 07:21:06 -06:00
committed by GitHub
parent 135a4d87f1
commit bed37b4208
4 changed files with 123 additions and 21 deletions

View File

@ -26,7 +26,7 @@ export interface PortForwardButtonProps {
agentId: string
}
const portForwardURL = (
export const portForwardURL = (
host: string,
port: number,
agentName: string,

View File

@ -293,7 +293,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
</div>
<div className={styles.editorPane}>
<div className={styles.editor}>
<div className={styles.editor} data-chromatic="ignore">
{activeFile ? (
<MonacoEditor
value={activeFile?.content}

View File

@ -1,7 +1,8 @@
import { makeStyles } from "@material-ui/core/styles"
import { useMachine } from "@xstate/react"
import { portForwardURL } from "components/PortForwardButton/PortForwardButton"
import { Stack } from "components/Stack/Stack"
import { FC, useEffect, useRef, useState } from "react"
import { FC, useCallback, useEffect, useRef, useState } from "react"
import { Helmet } from "react-helmet-async"
import { useNavigate, useParams, useSearchParams } from "react-router-dom"
import { colors } from "theme/colors"
@ -95,9 +96,55 @@ const TerminalPage: FC<
workspaceAgentError,
workspaceAgent,
websocketError,
applicationsHost,
} = terminalState.context
const reloading = useReloading(isDisconnected)
// handleWebLink handles opening of URLs in the terminal!
const handleWebLink = useCallback(
(uri: string) => {
if (!workspaceAgent || !workspace || !username || !applicationsHost) {
return
}
const open = (uri: string) => {
// Copied from: https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-web-links/src/WebLinksAddon.ts#L23
const newWindow = window.open()
if (newWindow) {
try {
newWindow.opener = null
} catch {
// no-op, Electron can throw
}
newWindow.location.href = uri
} else {
console.warn("Opening link blocked as opener could not be cleared")
}
}
try {
const url = new URL(uri)
const localHosts = ["0.0.0.0", "127.0.0.1", "localhost"]
if (!localHosts.includes(url.hostname)) {
open(uri)
return
}
open(
portForwardURL(
applicationsHost,
parseInt(url.port),
workspaceAgent.name,
workspace,
username,
),
)
} catch (ex) {
open(uri)
}
},
[workspaceAgent, workspace, username, applicationsHost],
)
// Create the terminal!
useEffect(() => {
if (!xtermRef.current) {
@ -116,7 +163,11 @@ const TerminalPage: FC<
const fitAddon = new FitAddon()
setFitAddon(fitAddon)
terminal.loadAddon(fitAddon)
terminal.loadAddon(new WebLinksAddon())
terminal.loadAddon(
new WebLinksAddon((_, uri) => {
handleWebLink(uri)
}),
)
terminal.onData((data) => {
sendEvent({
type: "WRITE",
@ -145,7 +196,7 @@ const TerminalPage: FC<
window.removeEventListener("resize", listener)
terminal.dispose()
}
}, [renderer, sendEvent, xtermRef])
}, [renderer, sendEvent, xtermRef, handleWebLink])
// Triggers the initial terminal connection using
// the reconnection token and workspace name found

View File

@ -10,6 +10,7 @@ export interface TerminalContext {
workspaceAgentError?: Error | unknown
websocket?: WebSocket
websocketError?: Error | unknown
applicationsHost?: string
// Assigned by connecting!
// The workspace agent is entirely optional. If the agent is omitted the
@ -47,6 +48,9 @@ export const terminalMachine =
getWorkspace: {
data: TypesGen.Workspace
}
getApplicationsHost: {
data: TypesGen.AppHostResponse
}
getWorkspaceAgent: {
data: TypesGen.WorkspaceAgent
}
@ -55,24 +59,61 @@ export const terminalMachine =
}
},
},
initial: "gettingWorkspace",
initial: "setup",
states: {
gettingWorkspace: {
invoke: {
src: "getWorkspace",
id: "getWorkspace",
onDone: [
{
actions: ["assignWorkspace", "clearWorkspaceError"],
target: "gettingWorkspaceAgent",
setup: {
type: "parallel",
states: {
getApplicationsHost: {
initial: "gettingApplicationsHost",
states: {
gettingApplicationsHost: {
invoke: {
src: "getApplicationsHost",
id: "getApplicationsHost",
onDone: {
actions: [
"assignApplicationsHost",
"clearApplicationsHostError",
],
target: "success",
},
},
},
success: {
type: "final",
},
},
],
onError: [
{
actions: "assignWorkspaceError",
target: "disconnected",
},
getWorkspace: {
initial: "gettingWorkspace",
states: {
gettingWorkspace: {
invoke: {
src: "getWorkspace",
id: "getWorkspace",
onDone: [
{
actions: ["assignWorkspace", "clearWorkspaceError"],
target: "success",
},
],
onError: [
{
actions: "assignWorkspaceError",
target: "success",
},
],
},
},
success: {
type: "final",
},
},
],
},
},
onDone: {
target: "gettingWorkspaceAgent",
},
},
gettingWorkspaceAgent: {
@ -129,7 +170,7 @@ export const terminalMachine =
on: {
CONNECT: {
actions: "assignConnection",
target: "gettingWorkspace",
target: "gettingWorkspaceAgent",
},
},
},
@ -146,6 +187,9 @@ export const terminalMachine =
context.workspaceName,
)
},
getApplicationsHost: async () => {
return API.getApplicationsHost()
},
getWorkspaceAgent: async (context) => {
if (!context.workspace || !context.workspaceName) {
throw new Error("workspace or workspace name is not set")
@ -218,6 +262,13 @@ export const terminalMachine =
...context,
workspaceError: undefined,
})),
assignApplicationsHost: assign({
applicationsHost: (_, { data }) => data.host,
}),
clearApplicationsHostError: assign((context) => ({
...context,
applicationsHostError: undefined,
})),
assignWorkspaceAgent: assign({
workspaceAgent: (_, event) => event.data,
}),