Skip to content

Commit 15d843e

Browse files
committed
Add xstate service
1 parent 31f27bc commit 15d843e

File tree

3 files changed

+258
-3
lines changed

3 files changed

+258
-3
lines changed

site/src/api/index.ts

+15
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@ export const getUsers = async (): Promise<Types.PagedUsers> => {
8585
})
8686
}
8787

88+
export const getOrganizations = async (): Promise<Types.Organization[]> => {
89+
const response = await axios.get<Types.Organization[]>("/api/v2/users/me/organizations")
90+
return response.data
91+
}
92+
93+
export const getWorkspace = async (organizationID: string, workspaceName: string): Promise<Types.Workspace> => {
94+
const response = await axios.get<Types.Workspace>(`/api/v2/organizations/${organizationID}/workspaces/me/${workspaceName}`)
95+
return response.data
96+
}
97+
98+
export const getWorkspaceResources = async (workspaceBuildID: string): Promise<Types.WorkspaceResource[]> => {
99+
const response = await axios.get<Types.WorkspaceResource[]>(`/api/v2/workspacebuilds/${workspaceBuildID}/resources`)
100+
return response.data
101+
}
102+
88103
export const getBuildInfo = async (): Promise<Types.BuildInfoResponse> => {
89104
const response = await axios.get("/api/v2/buildinfo")
90105
return response.data

site/src/api/types.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ export interface CreateWorkspaceRequest {
5555
template_id: string
5656
}
5757

58-
/**
59-
* @remarks Keep in sync with codersdk/workspaces.go
60-
*/
58+
export interface WorkspaceBuild {
59+
id: string
60+
}
61+
6162
export interface Workspace {
6263
id: string
6364
created_at: string
@@ -67,6 +68,17 @@ export interface Workspace {
6768
name: string
6869
autostart_schedule: string
6970
autostop_schedule: string
71+
latest_build: WorkspaceBuild
72+
}
73+
74+
export interface WorkspaceResource {
75+
id: string
76+
agents: WorkspaceAgent[]
77+
}
78+
79+
export interface WorkspaceAgent {
80+
id: string
81+
name: string
7082
}
7183

7284
export interface APIKeyResponse {
@@ -102,3 +114,9 @@ export interface UpdateProfileRequest {
102114
readonly email: string
103115
readonly name: string
104116
}
117+
118+
export interface ReconnectingPTYRequest {
119+
readonly data: string
120+
readonly height: number
121+
readonly width: number
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { assign, createMachine } from "xstate"
2+
import * as API from "../../api"
3+
import * as Types from "../../api/types"
4+
5+
// TypeScript doesn't have the randomUUID type on Crypto yet. See:
6+
// https://github.com/denoland/deno/issues/12754#issuecomment-970386235
7+
declare global {
8+
interface Crypto {
9+
randomUUID: () => string
10+
}
11+
}
12+
13+
export interface TerminalContext {
14+
organizationsError?: Error | unknown
15+
organizations?: Types.Organization[]
16+
workspaceError?: Error | unknown
17+
workspace?: Types.Workspace
18+
workspaceAgent?: Types.WorkspaceAgent
19+
workspaceAgentError?: Error | unknown
20+
21+
reconnection: string
22+
websocket?: WebSocket
23+
}
24+
25+
export type TerminalEvent =
26+
| { type: "CONNECT" }
27+
| { type: "WRITE"; data: Types.ReconnectingPTYRequest }
28+
| { type: "READ"; data: string }
29+
30+
export const terminalMachine =
31+
/** @xstate-layout N4IgpgJg5mDOIC5QBcwCcC2BLAdgQwBsBlZPVAOhmWVygHk0o8csAvMrAex1gGIJuYcrgBunANZCqDJi3Y1u8JCAAOnWFgU5EoAB6IALAYAM5AwE5LANgAcAJgDsNqwGYrxqwBoQAT0QBWAEZTBysw8wc3W2NAg38AX3jvVExcQhIyKTBqWhlmNg5FXnQ0TjRyFQIyADMyjEpsvLlCnh1VdU0ubWV9BCNTC2t7J1d3L19EaPJIlzdzY39h20Tk9Gx8YlIKKhocKAB1MvFYFTwAYzB+QWEcMUkG5EO0Y9OLtrUNLTbe4ONzckClnMLjsBjcdg83j8CDc-jMszccRcCzsVgSSRAKXW6S2WRyeyeL3OlxKZQqVWQtUwD0JJ2J7w6Xx6iF+-0BlhBYKsEPG0P8BkC5BsCIMDgcBhsxkcdhWmLWaU2mQeuwORzpFwAgjAcMgrjghKIJHjaa8wFqwDqGZ8ut8WVZAnYhQ6LKKDFZrFDEHZkeR-AigsibGK7OYZRisQqMttsiqTcTzTrimhSuVKjU6jS1aaE8grZ1uLaEIF7Y6bM7zK73eZeYZjAMgUH7UHUeZZRGNlGhGduPqziq9QbbkbyN2cL3c8oPvnunovQ7HeYHe4bHFFjZ-J6i27yHXd-b5qN-OjVqkO7iRz2wH3aEmU+T09TR+O80zZwg7PPyIvUcYV0ebOum6ooK7h1oE4qBEEgLgW28pnkqT5XqgEC8PsABKACSAAqACiL42sy74uEY5AltybrulKDibvMX5AuY66-vYooOLBp44kqpJoLwADCdAAHL8ThPFYfhBaER+DHkHYjagi4K7ETYNE2Duu6-kxFj+BEiQYjgnAQHAbTthx0b4vQjD5PIXRKKAU6viAvQGHYm5WE55DybMcTCmEgQweGcEmXisZZvSk6MgRb4+TuoLVsCdigos1ETAgAY7mEti+X6aKWGx2KKqZwXPOqZrahOtnheJb4Og6Tqgg4gIOKiwoGJuLhHtMIqhC45j+E4uWRueiHXnsYkzg5LLrlY0zcv4riQQ6DjGC4QEgkKCJtX8s0uIEbj9fBFBDcho2Ft6bhCjNYrcqGqIbslgT2EKanVg48z2DYe2BeQEBYLAh2QMdhGxAY0keKi7oSrNELOclvUuO5wpRAxApBqx-nsflQhcQDb6nVNzh2L1oQhvFaKbgKU3dUCTiSo1LioyeeWdtj41Fkpd0OHRQJjEGgLOKjiRAA */
32+
createMachine(
33+
{
34+
tsTypes: {} as import("./terminalXService.typegen").Typegen0,
35+
schema: {
36+
context: {
37+
reconnection: crypto.randomUUID(),
38+
} as TerminalContext,
39+
events: {} as TerminalEvent,
40+
services: {} as {
41+
getOrganizations: {
42+
data: Types.Organization[]
43+
}
44+
getWorkspace: {
45+
data: Types.Workspace
46+
}
47+
getWorkspaceAgent: {
48+
data: Types.WorkspaceAgent
49+
}
50+
connect: {
51+
data: WebSocket
52+
}
53+
},
54+
},
55+
id: "terminalState",
56+
initial: "gettingOrganizations",
57+
states: {
58+
gettingOrganizations: {
59+
invoke: {
60+
src: "getOrganizations",
61+
id: "getOrganizations",
62+
onDone: [
63+
{
64+
actions: ["assignOrganizations", "clearOrganizationsError"],
65+
target: "gettingWorkspace",
66+
},
67+
],
68+
onError: [
69+
{
70+
actions: "assignOrganizationsError",
71+
target: "error",
72+
},
73+
],
74+
},
75+
tags: "loading",
76+
},
77+
gettingWorkspace: {
78+
invoke: {
79+
src: "getWorkspace",
80+
id: "getWorkspace",
81+
onDone: [
82+
{
83+
actions: ["assignWorkspace", "clearWorkspaceError"],
84+
target: "gettingWorkspaceAgent",
85+
},
86+
],
87+
onError: [
88+
{
89+
actions: "assignWorkspaceError",
90+
target: "error",
91+
},
92+
],
93+
},
94+
},
95+
gettingWorkspaceAgent: {
96+
invoke: {
97+
src: "getWorkspaceAgent",
98+
id: "getWorkspaceAgent",
99+
onDone: [
100+
{
101+
actions: ["assignWorkspaceAgent", "clearWorkspaceAgentError"],
102+
target: "connecting",
103+
},
104+
],
105+
onError: [
106+
{
107+
actions: "assignWorkspaceAgentError",
108+
target: "error",
109+
},
110+
],
111+
},
112+
},
113+
connecting: {
114+
invoke: {
115+
src: "connect",
116+
id: "connect",
117+
onDone: [
118+
{
119+
actions: ["assignWebsocket", "clearWebsocketError"],
120+
target: "connected",
121+
},
122+
],
123+
onError: [
124+
{
125+
actions: "assignWebsocketError",
126+
target: "error",
127+
},
128+
],
129+
},
130+
},
131+
connected: {
132+
on: {
133+
WRITE: {
134+
actions: "sendMessage",
135+
},
136+
},
137+
},
138+
disconnected: {},
139+
error: {
140+
on: {
141+
CONNECT: {
142+
target: "gettingOrganizations",
143+
},
144+
},
145+
},
146+
},
147+
},
148+
{
149+
services: {
150+
getOrganizations: API.getOrganizations,
151+
getWorkspace: (context: TerminalContext) => {
152+
return API.getWorkspace(context.organizations![0].id, "")
153+
},
154+
getWorkspaceAgent: async (context: TerminalContext) => {
155+
const resources = await API.getWorkspaceResources(context.workspace!.latest_build.id)
156+
for (let i = 0; i < resources.length; i++) {
157+
const resource = resources[i]
158+
if (resource.agents.length <= 0) {
159+
continue
160+
}
161+
return resource.agents[0]
162+
}
163+
throw new Error("no agent found with id")
164+
},
165+
connect: (context: TerminalContext) => (send) => {
166+
return new Promise<WebSocket>((resolve, reject) => {
167+
const socket = new WebSocket(`/api/v2/workspaceagents/${context.workspaceAgent!.id}/pty`)
168+
socket.addEventListener("open", () => {
169+
resolve(socket)
170+
})
171+
socket.addEventListener("error", (event) => {
172+
reject("socket errored")
173+
})
174+
socket.addEventListener("close", (event) => {
175+
reject(event.reason)
176+
})
177+
socket.addEventListener("message", (event) => {
178+
send({
179+
type: "READ",
180+
data: event.data,
181+
})
182+
})
183+
})
184+
},
185+
},
186+
actions: {
187+
assignOrganizations: assign({
188+
organizations: (_, event) => event.data,
189+
}),
190+
assignOrganizationsError: assign({
191+
organizationsError: (_, event) => event.data,
192+
}),
193+
clearOrganizationsError: assign((context: TerminalContext) => ({
194+
...context,
195+
organizationsError: undefined,
196+
})),
197+
assignWorkspace: assign({
198+
workspace: (_, event) => event.data,
199+
}),
200+
assignWorkspaceError: assign({
201+
workspaceError: (_, event) => event.data,
202+
}),
203+
clearWorkspaceError: assign((context: TerminalContext) => ({
204+
...context,
205+
workspaceError: undefined,
206+
})),
207+
assignWorkspaceAgent: assign({
208+
workspaceAgent: (_, event) => event.data,
209+
}),
210+
assignWorkspaceAgentError: assign({
211+
workspaceAgentError: (_, event) => event.data,
212+
}),
213+
clearWorkspaceAgentError: assign((context: TerminalContext) => ({
214+
...context,
215+
workspaceAgentError: undefined,
216+
})),
217+
sendMessage: (context, event) => {
218+
context.websocket!.send(JSON.stringify(event.data))
219+
},
220+
},
221+
},
222+
)

0 commit comments

Comments
 (0)