feat: add terminal link component (#1538)

* Fix not being able to specify agent when connecting to terminal

The `workspace.agent` syntax was only used when fetching the agent and
not the workspace so it would try to fetch a workspace called
`workspace.agent` instead of just `workspace`.

* Add terminal link component

Currently it does not show anywhere but we can drop it into the
resources card later.
This commit is contained in:
Asher
2022-05-18 10:53:59 -05:00
committed by GitHub
parent 5f21a145d1
commit e4e7e10690
6 changed files with 85 additions and 13 deletions
@@ -0,0 +1,16 @@
import { Story } from "@storybook/react"
import React from "react"
import { MockWorkspace } from "../../testHelpers/renderHelpers"
import { TerminalLink, TerminalLinkProps } from "./TerminalLink"
export default {
title: "components/TerminalLink",
component: TerminalLink,
}
const Template: Story<TerminalLinkProps> = (args) => <TerminalLink {...args} />
export const Example = Template.bind({})
Example.args = {
workspaceName: MockWorkspace.name,
}
@@ -0,0 +1,28 @@
import Link from "@material-ui/core/Link"
import React from "react"
import * as TypesGen from "../../api/typesGenerated"
export const Language = {
linkText: "Open in terminal",
}
export interface TerminalLinkProps {
agentName?: TypesGen.WorkspaceAgent["name"]
userName?: TypesGen.User["username"]
workspaceName: TypesGen.Workspace["name"]
}
/**
* Generate a link to a terminal connected to the provided workspace agent. If
* no agent is provided connect to the first agent.
*
* If no user name is provided "me" is used however it makes the link not
* shareable.
*/
export const TerminalLink: React.FC<TerminalLinkProps> = ({ agentName, userName = "me", workspaceName }) => {
return (
<Link href={`/${userName}/${workspaceName}${agentName ? `.${agentName}` : ""}/terminal`} target="_blank">
{Language.linkText}
</Link>
)
}
@@ -6,7 +6,7 @@ import React from "react"
import { Route, Routes } from "react-router-dom"
import { TextDecoder, TextEncoder } from "util"
import { ReconnectingPTYRequest } from "../../api/types"
import { history, MockWorkspaceAgent, render } from "../../testHelpers/renderHelpers"
import { history, MockWorkspace, MockWorkspaceAgent, render } from "../../testHelpers/renderHelpers"
import { server } from "../../testHelpers/server"
import TerminalPage, { Language } from "./TerminalPage"
@@ -52,7 +52,7 @@ const expectTerminalText = (container: HTMLElement, text: string) => {
describe("TerminalPage", () => {
beforeEach(() => {
history.push("/some-user/my-workspace/terminal")
history.push(`/some-user/${MockWorkspace.name}/terminal`)
})
it("shows an error if fetching organizations fails", async () => {
@@ -146,4 +146,20 @@ describe("TerminalPage", () => {
expect(req.width).toBeGreaterThan(0)
server.close()
})
it("supports workspace.agent syntax", async () => {
// Given
const server = new WS("ws://localhost/api/v2/workspaceagents/" + MockWorkspaceAgent.id + "/pty")
const text = "something to render"
// When
history.push(`/some-user/${MockWorkspace.name}.${MockWorkspaceAgent.name}/terminal`)
const { container } = renderTerminal()
// Then
await server.connected
server.send(text)
await expectTerminalText(container, text)
server.close()
})
})
+5 -1
View File
@@ -34,10 +34,14 @@ const TerminalPage: React.FC<{
const search = new URLSearchParams(location.search)
return search.get("reconnect") ?? uuidv4()
})
// The workspace name is in the format:
// <workspace name>[.<agent name>]
const workspaceNameParts = workspace?.split(".")
const [terminalState, sendEvent] = useMachine(terminalMachine, {
context: {
agentName: workspaceNameParts?.[1],
reconnection: reconnectionToken,
workspaceName: workspace,
workspaceName: workspaceNameParts?.[0],
username: username,
},
actions: {
+10 -1
View File
@@ -80,7 +80,16 @@ export const handlers = [
// workspaces
rest.get("/api/v2/organizations/:organizationId/workspaces/:userName/:workspaceName", (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
if (req.params.workspaceName !== M.MockWorkspace.name) {
return res(
ctx.status(404),
ctx.json({
message: "workspace not found",
}),
)
} else {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
}
}),
rest.get("/api/v2/workspaces/:workspaceId", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
@@ -14,13 +14,16 @@ export interface TerminalContext {
websocketError?: Error | unknown
// Assigned by connecting!
// The workspace agent is entirely optional. If the agent is omitted the
// first agent will be used.
agentName?: string
username?: string
workspaceName?: string
reconnection?: string
}
export type TerminalEvent =
| { type: "CONNECT"; reconnection?: string; workspaceName?: string; username?: string }
| { type: "CONNECT"; agentName?: string; reconnection?: string; workspaceName?: string; username?: string }
| { type: "WRITE"; request: Types.ReconnectingPTYRequest }
| { type: "READ"; data: ArrayBuffer }
| { type: "DISCONNECT" }
@@ -153,7 +156,7 @@ export const terminalMachine =
getOrganizations: API.getOrganizations,
getWorkspace: async (context) => {
if (!context.organizations || !context.workspaceName) {
throw new Error("organizations or workspace not set")
throw new Error("organizations or workspace name not set")
}
return API.getWorkspaceByOwnerAndName(context.organizations[0].id, context.username, context.workspaceName)
},
@@ -161,11 +164,6 @@ export const terminalMachine =
if (!context.workspace || !context.workspaceName) {
throw new Error("workspace or workspace name is not set")
}
// The workspace name is in the format:
// <workspace name>[.<agent name>]
// The workspace agent is entirely optional.
const workspaceNameParts = context.workspaceName.split(".")
const agentName = workspaceNameParts[1]
const resources = await API.getWorkspaceResources(context.workspace.latest_build.id)
@@ -174,10 +172,10 @@ export const terminalMachine =
if (!resource.agents || resource.agents.length < 1) {
return
}
if (!agentName) {
if (!context.agentName) {
return resource.agents[0]
}
return resource.agents.find((agent) => agent.name === agentName)
return resource.agents.find((agent) => agent.name === context.agentName)
})
.filter((a) => a)[0]
if (!agent) {
@@ -218,6 +216,7 @@ export const terminalMachine =
actions: {
assignConnection: assign((context, event) => ({
...context,
agentName: event.agentName ?? context.agentName,
reconnection: event.reconnection ?? context.reconnection,
workspaceName: event.workspaceName ?? context.workspaceName,
})),