Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7a5eace

Browse files
committed
feat: hook up port dropdown to workspace page
1 parent c2a1851 commit 7a5eace

File tree

4 files changed

+197
-11
lines changed

4 files changed

+197
-11
lines changed

site/src/components/Resources/Resources.tsx

+35-9
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

+8-1
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

+21-1
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
)
+133
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)