Skip to content

Commit 93d6de6

Browse files
authored
feat: crud workspaces (#72)
1 parent 3e9cfec commit 93d6de6

File tree

6 files changed

+229
-28
lines changed

6 files changed

+229
-28
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
/.vscode-test/
55
/.nyc_output/
66
/coverage/
7-
*.vsix
7+
*.vsix
8+
yarn-error.log

package.json

+79-12
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,25 @@
5757
"views": {
5858
"coder": [
5959
{
60-
"id": "coderRemote",
61-
"name": "",
60+
"id": "myWorkspaces",
61+
"name": "My Workspaces",
62+
"visibility": "visible",
63+
"icon": "media/logo.svg"
64+
},
65+
{
66+
"id": "allWorkspaces",
67+
"name": "All Workspaces",
6268
"visibility": "visible",
6369
"icon": "media/logo.svg",
64-
"contextualTitle": "Coder Remote"
70+
"when": "coder.authenticated && coder.isOwner"
6571
}
6672
]
6773
},
6874
"viewsWelcome": [
6975
{
70-
"view": "coderRemote",
76+
"view": "myWorkspaces",
7177
"contents": "Coder is a platform that provisions remote development environments. \n[Login](command:coder.login)",
7278
"when": "!coder.authenticated && coder.loaded"
73-
},
74-
{
75-
"view": "coderRemote",
76-
"contents": "You're logged in! \n[Open Workspace](command:coder.open)",
77-
"when": "coder.authenticated && coder.loaded"
7879
}
7980
],
8081
"commands": [
@@ -84,18 +85,84 @@
8485
},
8586
{
8687
"command": "coder.logout",
87-
"title": "Coder: Logout"
88+
"title": "Coder: Logout",
89+
"when": "coder.authenticated",
90+
"icon": "$(sign-out)"
8891
},
8992
{
9093
"command": "coder.open",
91-
"title": "Coder: Open Workspace"
94+
"title": "Coder: Open Workspace",
95+
"icon": "$(play)"
96+
},
97+
{
98+
"command": "coder.createWorkspace",
99+
"title": "Create Workspace",
100+
"when": "coder.authenticated",
101+
"icon": "$(add)"
102+
},
103+
{
104+
"command": "coder.navigateToWorkspace",
105+
"title": "Navigate to Workspace Page",
106+
"when": "coder.authenticated",
107+
"icon": "$(link-external)"
108+
},
109+
{
110+
"command": "coder.navigateToWorkspaceSettings",
111+
"title": "Edit Workspace Settings",
112+
"when": "coder.authenticated",
113+
"icon": "$(settings-gear)"
92114
},
93115
{
94116
"command": "coder.workspace.update",
95117
"title": "Coder: Update Workspace",
96118
"when": "coder.workspace.updatable"
119+
},
120+
{
121+
"command": "coder.refreshWorkspaces",
122+
"title": "Coder: Refresh Workspace",
123+
"icon": "$(refresh)",
124+
"when": "coder.authenticated"
97125
}
98-
]
126+
],
127+
"menus": {
128+
"view/title": [
129+
{
130+
"command": "coder.logout",
131+
"when": "coder.authenticated && view == myWorkspaces"
132+
},
133+
{
134+
"command": "coder.login",
135+
"when": "!coder.authenticated && view == myWorkspaces"
136+
},
137+
{
138+
"command": "coder.createWorkspace",
139+
"when": "coder.authenticated && view == myWorkspaces",
140+
"group": "navigation"
141+
},
142+
{
143+
"command": "coder.refreshWorkspaces",
144+
"when": "coder.authenticated && view == myWorkspaces",
145+
"group": "navigation"
146+
}
147+
],
148+
"view/item/context": [
149+
{
150+
"command": "coder.open",
151+
"when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces",
152+
"group": "inline"
153+
},
154+
{
155+
"command": "coder.navigateToWorkspace",
156+
"when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces",
157+
"group": "inline"
158+
},
159+
{
160+
"command": "coder.navigateToWorkspaceSettings",
161+
"when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces",
162+
"group": "inline"
163+
}
164+
]
165+
}
99166
},
100167
"scripts": {
101168
"vscode:prepublish": "yarn package",

src/api-helper.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
2+
3+
export function extractAgentsAndFolderPath(
4+
workspace: Workspace,
5+
): [agents: WorkspaceAgent[], folderPath: string | undefined] {
6+
// TODO: multiple agent support
7+
const agents = workspace.latest_build.resources.reduce((acc, resource) => {
8+
return acc.concat(resource.agents || [])
9+
}, [] as WorkspaceAgent[])
10+
11+
let folderPath = undefined
12+
if (agents.length === 1) {
13+
folderPath = agents[0].expanded_directory
14+
}
15+
return [agents, folderPath]
16+
}

src/commands.ts

+48-13
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import axios from "axios"
22
import { getAuthenticatedUser, getWorkspaces, updateWorkspaceVersion } from "coder/site/src/api/api"
3-
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
3+
import { Workspace } from "coder/site/src/api/typesGenerated"
44
import * as vscode from "vscode"
5+
import { extractAgentsAndFolderPath } from "./api-helper"
56
import { Remote } from "./remote"
67
import { Storage } from "./storage"
8+
import { WorkspaceTreeItem } from "./workspacesProvider"
79

810
export class Commands {
911
public constructor(private readonly vscodeProposed: typeof vscode, private readonly storage: Storage) {}
@@ -79,6 +81,9 @@ export class Commands {
7981
throw new Error("Failed to get authenticated user")
8082
}
8183
await vscode.commands.executeCommand("setContext", "coder.authenticated", true)
84+
if (user.roles.find((role) => role.name === "owner")) {
85+
await vscode.commands.executeCommand("setContext", "coder.isOwner", true)
86+
}
8287
vscode.window
8388
.showInformationMessage(
8489
`Welcome to Coder, ${user.username}!`,
@@ -108,7 +113,37 @@ export class Commands {
108113
})
109114
}
110115

111-
public async open(...args: string[]): Promise<void> {
116+
public async createWorkspace(): Promise<void> {
117+
const uri = this.storage.getURL() + "/templates"
118+
await vscode.commands.executeCommand("vscode.open", uri)
119+
}
120+
121+
public async navigateToWorkspace(workspace: WorkspaceTreeItem) {
122+
if (workspace) {
123+
const uri = this.storage.getURL() + `/@${workspace.workspaceOwner}/${workspace.workspaceName}`
124+
await vscode.commands.executeCommand("vscode.open", uri)
125+
} else if (this.storage.workspace) {
126+
const uri = this.storage.getURL() + `/@${this.storage.workspace.owner_name}/${this.storage.workspace.name}`
127+
await vscode.commands.executeCommand("vscode.open", uri)
128+
} else {
129+
vscode.window.showInformationMessage("No workspace found.")
130+
}
131+
}
132+
133+
public async navigateToWorkspaceSettings(workspace: WorkspaceTreeItem) {
134+
if (workspace) {
135+
const uri = this.storage.getURL() + `/@${workspace.workspaceOwner}/${workspace.workspaceName}/settings`
136+
await vscode.commands.executeCommand("vscode.open", uri)
137+
} else if (this.storage.workspace) {
138+
const uri =
139+
this.storage.getURL() + `/@${this.storage.workspace.owner_name}/${this.storage.workspace.name}/settings`
140+
await vscode.commands.executeCommand("vscode.open", uri)
141+
} else {
142+
vscode.window.showInformationMessage("No workspace found.")
143+
}
144+
}
145+
146+
public async open(...args: unknown[]): Promise<void> {
112147
let workspaceOwner: string
113148
let workspaceName: string
114149
let folderPath: string | undefined
@@ -165,19 +200,19 @@ export class Commands {
165200
workspaceOwner = workspace.owner_name
166201
workspaceName = workspace.name
167202

168-
// TODO: multiple agent support
169-
const agents = workspace.latest_build.resources.reduce((acc, resource) => {
170-
return acc.concat(resource.agents || [])
171-
}, [] as WorkspaceAgent[])
172-
173-
if (agents.length === 1) {
174-
folderPath = agents[0].expanded_directory
175-
}
203+
const [, folderPathExtracted] = extractAgentsAndFolderPath(workspace)
204+
folderPath = folderPathExtracted
205+
} else if (args.length === 2) {
206+
// opening a workspace from the sidebar
207+
const workspaceTreeItem = args[0] as WorkspaceTreeItem
208+
workspaceOwner = workspaceTreeItem.workspaceOwner
209+
workspaceName = workspaceTreeItem.workspaceName
210+
folderPath = workspaceTreeItem.workspaceFolderPath
176211
} else {
177-
workspaceOwner = args[0]
178-
workspaceName = args[1]
212+
workspaceOwner = args[0] as string
213+
workspaceName = args[1] as string
179214
// workspaceAgent is reserved for args[2], but multiple agents aren't supported yet.
180-
folderPath = args[3]
215+
folderPath = args[3] as string | undefined
181216
}
182217

183218
// A workspace can have multiple agents, but that's handled

src/extension.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,27 @@ import * as vscode from "vscode"
66
import { Commands } from "./commands"
77
import { Remote } from "./remote"
88
import { Storage } from "./storage"
9+
import { WorkspaceQuery, WorkspaceProvider } from "./workspacesProvider"
910

1011
export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
1112
const output = vscode.window.createOutputChannel("Coder")
1213
const storage = new Storage(output, ctx.globalState, ctx.secrets, ctx.globalStorageUri, ctx.logUri)
1314
await storage.init()
1415

16+
const myWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.Mine)
17+
const allWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.All)
18+
19+
vscode.window.registerTreeDataProvider("myWorkspaces", myWorkspacesProvider)
20+
vscode.window.registerTreeDataProvider("allWorkspaces", allWorkspacesProvider)
21+
1522
getAuthenticatedUser()
16-
.then(() => {
17-
vscode.commands.executeCommand("setContext", "coder.authenticated", true)
23+
.then(async (user) => {
24+
if (user) {
25+
vscode.commands.executeCommand("setContext", "coder.authenticated", true)
26+
if (user.roles.find((role) => role.name === "owner")) {
27+
await vscode.commands.executeCommand("setContext", "coder.isOwner", true)
28+
}
29+
}
1830
})
1931
.catch(() => {
2032
// Not authenticated!
@@ -76,6 +88,16 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
7688
vscode.commands.registerCommand("coder.logout", commands.logout.bind(commands))
7789
vscode.commands.registerCommand("coder.open", commands.open.bind(commands))
7890
vscode.commands.registerCommand("coder.workspace.update", commands.updateWorkspace.bind(commands))
91+
vscode.commands.registerCommand("coder.createWorkspace", commands.createWorkspace.bind(commands))
92+
vscode.commands.registerCommand("coder.navigateToWorkspace", commands.navigateToWorkspace.bind(commands))
93+
vscode.commands.registerCommand(
94+
"coder.navigateToWorkspaceSettings",
95+
commands.navigateToWorkspaceSettings.bind(commands),
96+
)
97+
vscode.commands.registerCommand("coder.refreshWorkspaces", () => {
98+
myWorkspacesProvider.refresh()
99+
allWorkspacesProvider.refresh()
100+
})
79101

80102
// Since the "onResolveRemoteAuthority:ssh-remote" activation event exists
81103
// in package.json we're able to perform actions before the authority is

src/workspacesProvider.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { getWorkspaces } from "coder/site/src/api/api"
2+
import * as path from "path"
3+
import * as vscode from "vscode"
4+
import { extractAgentsAndFolderPath } from "./api-helper"
5+
6+
export enum WorkspaceQuery {
7+
Mine = "owner:me",
8+
All = "",
9+
}
10+
11+
export class WorkspaceProvider implements vscode.TreeDataProvider<WorkspaceTreeItem> {
12+
constructor(private readonly getWorkspacesQuery: WorkspaceQuery) {}
13+
14+
private _onDidChangeTreeData: vscode.EventEmitter<WorkspaceTreeItem | undefined | null | void> =
15+
new vscode.EventEmitter<WorkspaceTreeItem | undefined | null | void>()
16+
readonly onDidChangeTreeData: vscode.Event<WorkspaceTreeItem | undefined | null | void> =
17+
this._onDidChangeTreeData.event
18+
19+
refresh(): void {
20+
this._onDidChangeTreeData.fire()
21+
}
22+
23+
getTreeItem(element: WorkspaceTreeItem): vscode.TreeItem {
24+
return element
25+
}
26+
27+
getChildren(): Thenable<WorkspaceTreeItem[]> {
28+
return getWorkspaces({ q: this.getWorkspacesQuery }).then((workspaces) => {
29+
return workspaces.workspaces.map((workspace) => {
30+
const status =
31+
workspace.latest_build.status.substring(0, 1).toUpperCase() + workspace.latest_build.status.substring(1)
32+
33+
const label =
34+
this.getWorkspacesQuery === WorkspaceQuery.All
35+
? `${workspace.owner_name} / ${workspace.name}`
36+
: workspace.name
37+
const detail = `Template: ${workspace.template_display_name || workspace.template_name} • Status: ${status}`
38+
const [, folderPath] = extractAgentsAndFolderPath(workspace)
39+
return new WorkspaceTreeItem(label, detail, workspace.owner_name, workspace.name, folderPath)
40+
})
41+
})
42+
}
43+
}
44+
45+
export class WorkspaceTreeItem extends vscode.TreeItem {
46+
constructor(
47+
public readonly label: string,
48+
public readonly tooltip: string,
49+
public readonly workspaceOwner: string,
50+
public readonly workspaceName: string,
51+
public readonly workspaceFolderPath: string | undefined,
52+
) {
53+
super(label, vscode.TreeItemCollapsibleState.None)
54+
}
55+
56+
iconPath = {
57+
light: path.join(__filename, "..", "..", "media", "logo.svg"),
58+
dark: path.join(__filename, "..", "..", "media", "logo.svg"),
59+
}
60+
}

0 commit comments

Comments
 (0)