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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 79 additions & 12 deletions
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

Lines changed: 16 additions & 0 deletions
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

Lines changed: 48 additions & 13 deletions
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

Lines changed: 24 additions & 2 deletions
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

Lines changed: 60 additions & 0 deletions
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)
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