Skip to content

Commit 91a194d

Browse files
committed
feat: hook up port dropdown to workspace page
1 parent 4729069 commit 91a194d

File tree

4 files changed

+197
-11
lines changed

4 files changed

+197
-11
lines changed

site/src/components/Resources/Resources.tsx

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1+
import Button from "@material-ui/core/Button"
12
import { makeStyles, Theme } from "@material-ui/core/styles"
23
import Table from "@material-ui/core/Table"
34
import TableBody from "@material-ui/core/TableBody"
45
import TableCell from "@material-ui/core/TableCell"
56
import TableHead from "@material-ui/core/TableHead"
67
import TableRow from "@material-ui/core/TableRow"
8+
import CompareArrowsIcon from "@material-ui/icons/CompareArrows"
79
import useTheme from "@material-ui/styles/useTheme"
810
import React from "react"
9-
import { Workspace, WorkspaceResource } from "../../api/typesGenerated"
11+
import { Workspace, WorkspaceAgent, WorkspaceResource } from "../../api/typesGenerated"
1012
import { getDisplayAgentStatus } from "../../util/workspace"
1113
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
1214
import { TerminalLink } from "../TerminalLink/TerminalLink"
1315
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
1416

1517
const Language = {
18+
portForwardLabel: "Port forward",
1619
resources: "Resources",
1720
resourceLabel: "Resource",
1821
agentsLabel: "Agents",
@@ -22,12 +25,18 @@ const Language = {
2225
}
2326

2427
interface ResourcesProps {
28+
handleOpenPortForward: (agent: WorkspaceAgent, anchorEl: HTMLElement) => void
2529
resources?: WorkspaceResource[]
2630
getResourcesError?: Error
2731
workspace: Workspace
2832
}
2933

30-
export const Resources: React.FC<ResourcesProps> = ({ resources, getResourcesError, workspace }) => {
34+
export const Resources: React.FC<ResourcesProps> = ({
35+
handleOpenPortForward,
36+
resources,
37+
getResourcesError,
38+
workspace,
39+
}) => {
3140
const styles = useStyles()
3241
const theme: Theme = useTheme()
3342

@@ -89,12 +98,22 @@ export const Resources: React.FC<ResourcesProps> = ({ resources, getResourcesErr
8998
</TableCell>
9099
<TableCell>
91100
{agent.status === "connected" && (
92-
<TerminalLink
93-
className={styles.accessLink}
94-
workspaceName={workspace.name}
95-
agentName={agent.name}
96-
userName={workspace.owner_name}
97-
/>
101+
<>
102+
<TerminalLink
103+
className={styles.accessLink}
104+
workspaceName={workspace.name}
105+
agentName={agent.name}
106+
userName={workspace.owner_name}
107+
/>
108+
<Button
109+
variant="text"
110+
className={styles.accessLink}
111+
onClick={(event) => handleOpenPortForward(agent, event.currentTarget)}
112+
>
113+
<CompareArrowsIcon />
114+
{Language.portForwardLabel}
115+
</Button>
116+
</>
98117
)}
99118
</TableCell>
100119
</TableRow>
@@ -134,9 +153,16 @@ const useStyles = makeStyles((theme) => ({
134153
},
135154

136155
accessLink: {
156+
alignItems: "center",
137157
color: theme.palette.text.secondary,
138158
display: "flex",
139-
alignItems: "center",
159+
border: 0,
160+
padding: 0,
161+
162+
"&:hover": {
163+
backgroundColor: "unset",
164+
textDecoration: "underline",
165+
},
140166

141167
"& svg": {
142168
width: 16,

site/src/components/Workspace/Workspace.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface WorkspaceProps {
1717
handleStop: () => void
1818
handleUpdate: () => void
1919
handleCancel: () => void
20+
handleOpenPortForward: (agent: TypesGen.WorkspaceAgent, anchorEl: HTMLElement) => void
2021
workspace: TypesGen.Workspace
2122
resources?: TypesGen.WorkspaceResource[]
2223
getResourcesError?: Error
@@ -31,6 +32,7 @@ export const Workspace: React.FC<WorkspaceProps> = ({
3132
handleStop,
3233
handleUpdate,
3334
handleCancel,
35+
handleOpenPortForward,
3436
workspace,
3537
resources,
3638
getResourcesError,
@@ -68,7 +70,12 @@ export const Workspace: React.FC<WorkspaceProps> = ({
6870

6971
<WorkspaceStats workspace={workspace} />
7072

71-
<Resources resources={resources} getResourcesError={getResourcesError} workspace={workspace} />
73+
<Resources
74+
handleOpenPortForward={handleOpenPortForward}
75+
resources={resources}
76+
getResourcesError={getResourcesError}
77+
workspace={workspace}
78+
/>
7279

7380
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
7481
<BuildsTable builds={builds} className={styles.timelineTable} />

site/src/pages/WorkspacePage/WorkspacePage.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { useMachine } from "@xstate/react"
2-
import React, { useEffect } from "react"
2+
import React, { useEffect, useState } from "react"
33
import { useParams } from "react-router-dom"
44
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
55
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
66
import { Margins } from "../../components/Margins/Margins"
7+
import { PortForwardDropdown } from "../../components/PortForwardDropdown/PortForwardDropdown"
78
import { Stack } from "../../components/Stack/Stack"
89
import { Workspace } from "../../components/Workspace/Workspace"
910
import { firstOrItem } from "../../util/array"
11+
import { agentMachine } from "../../xServices/agent/agentXService"
1012
import { workspaceMachine } from "../../xServices/workspace/workspaceXService"
1113

1214
export const WorkspacePage: React.FC = () => {
@@ -16,6 +18,10 @@ export const WorkspacePage: React.FC = () => {
1618
const [workspaceState, workspaceSend] = useMachine(workspaceMachine)
1719
const { workspace, resources, getWorkspaceError, getResourcesError, builds } = workspaceState.context
1820

21+
const [agentState, agentSend] = useMachine(agentMachine)
22+
const { netstat } = agentState.context
23+
const [portForwardAnchorEl, setPortForwardAnchorEl] = useState<HTMLElement>()
24+
1925
/**
2026
* Get workspace, template, and organization on mount and whenever workspaceId changes.
2127
* workspaceSend should not change.
@@ -38,10 +44,24 @@ export const WorkspacePage: React.FC = () => {
3844
handleStop={() => workspaceSend("STOP")}
3945
handleUpdate={() => workspaceSend("UPDATE")}
4046
handleCancel={() => workspaceSend("CANCEL")}
47+
handleOpenPortForward={(agent, anchorEl) => {
48+
agentSend("CONNECT", { agentId: agent.id })
49+
setPortForwardAnchorEl(anchorEl)
50+
}}
4151
resources={resources}
4252
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
4353
builds={builds}
4454
/>
55+
<PortForwardDropdown
56+
open={!!portForwardAnchorEl}
57+
anchorEl={portForwardAnchorEl}
58+
netstat={netstat}
59+
onClose={() => {
60+
agentSend("DISCONNECT")
61+
setPortForwardAnchorEl(undefined)
62+
}}
63+
urlFormatter={(port) => `${location.protocol}//${port}--${workspace.owner_name}--${location.host}`}
64+
/>
4565
</Stack>
4666
</Margins>
4767
)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { assign, createMachine } from "xstate"
2+
import * as Types from "../../api/types"
3+
import { errorString } from "../../util/error"
4+
5+
export interface AgentContext {
6+
agentId?: string
7+
netstat?: Types.NetstatResponse
8+
websocket?: WebSocket
9+
}
10+
11+
export type AgentEvent =
12+
| { type: "CONNECT"; agentId: string }
13+
| { type: "STAT"; data: Types.NetstatResponse }
14+
| { type: "DISCONNECT" }
15+
16+
export const agentMachine = createMachine(
17+
{
18+
tsTypes: {} as import("./agentXService.typegen").Typegen0,
19+
schema: {
20+
context: {} as AgentContext,
21+
events: {} as AgentEvent,
22+
services: {} as {
23+
connect: {
24+
data: WebSocket
25+
}
26+
},
27+
},
28+
id: "agentState",
29+
initial: "disconnected",
30+
states: {
31+
connecting: {
32+
invoke: {
33+
src: "connect",
34+
id: "connect",
35+
onDone: [
36+
{
37+
actions: ["assignWebsocket", "clearNetstat"],
38+
target: "connected",
39+
},
40+
],
41+
onError: [
42+
{
43+
actions: "assignWebsocketError",
44+
target: "disconnected",
45+
},
46+
],
47+
},
48+
},
49+
connected: {
50+
on: {
51+
STAT: {
52+
actions: "assignNetstat",
53+
},
54+
DISCONNECT: {
55+
actions: ["disconnect", "clearNetstat"],
56+
target: "disconnected",
57+
},
58+
},
59+
},
60+
disconnected: {
61+
on: {
62+
CONNECT: {
63+
actions: "assignConnection",
64+
target: "connecting",
65+
},
66+
},
67+
},
68+
},
69+
},
70+
{
71+
services: {
72+
connect: (context) => (send) => {
73+
return new Promise<WebSocket>((resolve, reject) => {
74+
if (!context.agentId) {
75+
return reject("agent ID is not set")
76+
}
77+
const proto = location.protocol === "https:" ? "wss:" : "ws:"
78+
const socket = new WebSocket(`${proto}//${location.host}/api/v2/workspaceagents/${context.agentId}/netstat`)
79+
socket.binaryType = "arraybuffer"
80+
socket.addEventListener("open", () => {
81+
resolve(socket)
82+
})
83+
socket.addEventListener("error", (error) => {
84+
reject(error)
85+
})
86+
socket.addEventListener("close", () => {
87+
send({
88+
type: "DISCONNECT",
89+
})
90+
})
91+
socket.addEventListener("message", (event) => {
92+
try {
93+
send({
94+
type: "STAT",
95+
data: JSON.parse(new TextDecoder().decode(event.data)),
96+
})
97+
} catch (error) {
98+
send({
99+
type: "STAT",
100+
data: {
101+
error: errorString(error),
102+
},
103+
})
104+
}
105+
})
106+
})
107+
},
108+
},
109+
actions: {
110+
assignConnection: assign((context, event) => ({
111+
...context,
112+
agentId: event.agentId,
113+
})),
114+
assignWebsocket: assign({
115+
websocket: (_, event) => event.data,
116+
}),
117+
assignWebsocketError: assign({
118+
netstat: (_, event) => ({ error: errorString(event.data) }),
119+
}),
120+
clearNetstat: assign((context: AgentContext) => ({
121+
...context,
122+
netstat: undefined,
123+
})),
124+
assignNetstat: assign({
125+
netstat: (_, event) => event.data,
126+
}),
127+
disconnect: (context: AgentContext) => {
128+
// Code 1000 is a successful exit!
129+
context.websocket?.close(1000)
130+
},
131+
},
132+
},
133+
)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy