Skip to content

WIP: feat: crud workspaces #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
/.vscode-test/
/.nyc_output/
/coverage/
*.vsix
*.vsix
yarn-error.log
81 changes: 68 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,24 @@
"views": {
"coder": [
{
"id": "coderRemote",
"name": "",
"id": "myWorkspaces",
"name": "My Workspaces",
"visibility": "visible",
"icon": "media/logo.svg",
"contextualTitle": "Coder Remote"
"icon": "media/logo.svg"
},
{
"id": "allWorkspaces",
"name": "All Workspaces",
"visibility": "visible",
"icon": "media/logo.svg"
}
]
},
"viewsWelcome": [
{
"view": "coderRemote",
"view": "myWorkspaces",
"contents": "Coder is a platform that provisions remote development environments. \n[Login](command:coder.login)",
"when": "!coder.authenticated && coder.loaded"
},
{
"view": "coderRemote",
"contents": "You're logged in! \n[Open Workspace](command:coder.open)",
"when": "coder.authenticated && coder.loaded"
}
],
"commands": [
Expand All @@ -68,18 +68,73 @@
},
{
"command": "coder.logout",
"title": "Coder: Logout"
"title": "Coder: Logout",
"when": "coder.authenticated",
"icon": "$(sign-out)"
},
{
"command": "coder.open",
"title": "Coder: Open Workspace"
"title": "Coder: Open Workspace",
"icon": "$(play)"
},
{
"command": "coder.createWorkspace",
"title": "Create Workspace",
"when": "coder.authenticated",
"icon": "$(add)"
},
{
"command": "coder.navigateToWorkspace",
"title": "Navigate to Workspace Page",
"when": "coder.authenticated",
"icon": "$(link-external)"
},
{
"command": "coder.workspace.update",
"title": "Coder: Update Workspace",
"when": "coder.workspace.updatable"
},
{
"command": "coder.refreshWorkspaces",
"title": "Coder: Refresh Workspace",
"icon": "$(refresh)",
"when": "coder.authenticated"
}
]
],
"menus": {
"view/title": [
{
"command": "coder.logout",
"when": "coder.authenticated && view == myWorkspaces"
},
{
"command": "coder.login",
"when": "!coder.authenticated && view == myWorkspaces"
},
{
"command": "coder.createWorkspace",
"when": "coder.authenticated && view == myWorkspaces",
"group": "navigation"
},
{
"command": "coder.refreshWorkspaces",
"when": "coder.authenticated && view == myWorkspaces",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "coder.open",
"when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces",
"group": "inline"
},
{
"command": "coder.navigateToWorkspace",
"when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces",
"group": "inline"
}
]
}
},
"scripts": {
"vscode:prepublish": "yarn package",
Expand Down
16 changes: 16 additions & 0 deletions src/api-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"

export function extractAgentsAndFolderPath(
workspace: Workspace,
): [agents: WorkspaceAgent[], folderPath: string | undefined] {
// TODO: multiple agent support
const agents = workspace.latest_build.resources.reduce((acc, resource) => {
return acc.concat(resource.agents || [])
}, [] as WorkspaceAgent[])

let folderPath = undefined
if (agents.length === 1) {
folderPath = agents[0].expanded_directory
}
Comment on lines +12 to +14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is more than 1 agent, does everything fail? Should we randomly choose one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good question. I am not changing the actual behavior, just extracted it to a new function. the current version also does this agents[0] choice =/ . I am not sure what is the best way to handle multiple agents...

return [agents, folderPath]
}
38 changes: 25 additions & 13 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import axios from "axios"
import { getAuthenticatedUser, getWorkspaces, updateWorkspaceVersion } from "coder/site/src/api/api"
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import { Workspace } from "coder/site/src/api/typesGenerated"
import * as vscode from "vscode"
import { extractAgentsAndFolderPath } from "./api-helper"
import { Remote } from "./remote"
import { Storage } from "./storage"
import { WorkspaceTreeItem } from "./workspacesProvider"

export class Commands {
public constructor(private readonly vscodeProposed: typeof vscode, private readonly storage: Storage) {}
Expand Down Expand Up @@ -108,7 +110,17 @@ export class Commands {
})
}

public async open(...args: string[]): Promise<void> {
public async createWorkspace(): Promise<void> {
const uri = this.storage.getURL() + "/templates"
await vscode.commands.executeCommand("vscode.open", uri)
}

public async navigateToWorkspace(workspace: WorkspaceTreeItem): Promise<void> {
const uri = this.storage.getURL() + `/@${workspace.workspaceOwner}/${workspace.workspaceName}`
await vscode.commands.executeCommand("vscode.open", uri)
}

public async open(...args: unknown[]): Promise<void> {
let workspaceOwner: string
let workspaceName: string
let folderPath: string | undefined
Expand Down Expand Up @@ -165,19 +177,19 @@ export class Commands {
workspaceOwner = workspace.owner_name
workspaceName = workspace.name

// TODO: multiple agent support
const agents = workspace.latest_build.resources.reduce((acc, resource) => {
return acc.concat(resource.agents || [])
}, [] as WorkspaceAgent[])

if (agents.length === 1) {
folderPath = agents[0].expanded_directory
}
const [, folderPathExtracted] = extractAgentsAndFolderPath(workspace)
folderPath = folderPathExtracted
} else if (args.length === 2) {
// opening a workspace from the sidebar
const workspaceTreeItem = args[0] as WorkspaceTreeItem
workspaceOwner = workspaceTreeItem.workspaceOwner
workspaceName = workspaceTreeItem.workspaceName
folderPath = workspaceTreeItem.workspaceFolderPath
} else {
workspaceOwner = args[0]
workspaceName = args[1]
workspaceOwner = args[0] as string
workspaceName = args[1] as string
// workspaceAgent is reserved for args[2], but multiple agents aren't supported yet.
folderPath = args[3]
folderPath = args[3] as string | undefined
}

// A workspace can have multiple agents, but that's handled
Expand Down
13 changes: 13 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ import * as vscode from "vscode"
import { Commands } from "./commands"
import { Remote } from "./remote"
import { Storage } from "./storage"
import { WorkspaceQuery, WorkspaceProvider } from "./workspacesProvider"

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

const myWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.Mine)
const allWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.All)

vscode.window.registerTreeDataProvider("myWorkspaces", myWorkspacesProvider)
vscode.window.registerTreeDataProvider("allWorkspaces", allWorkspacesProvider)

getAuthenticatedUser()
.then(() => {
vscode.commands.executeCommand("setContext", "coder.authenticated", true)
Expand Down Expand Up @@ -76,6 +83,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
vscode.commands.registerCommand("coder.logout", commands.logout.bind(commands))
vscode.commands.registerCommand("coder.open", commands.open.bind(commands))
vscode.commands.registerCommand("coder.workspace.update", commands.updateWorkspace.bind(commands))
vscode.commands.registerCommand("coder.createWorkspace", commands.createWorkspace.bind(commands))
vscode.commands.registerCommand("coder.navigateToWorkspace", commands.navigateToWorkspace.bind(commands))
vscode.commands.registerCommand("coder.refreshWorkspaces", () => {
myWorkspacesProvider.refresh()
allWorkspacesProvider.refresh()
})

// Since the "onResolveRemoteAuthority:ssh-remote" activation event exists
// in package.json we're able to perform actions before the authority is
Expand Down
60 changes: 60 additions & 0 deletions src/workspacesProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { getWorkspaces } from "coder/site/src/api/api"
import * as path from "path"
import * as vscode from "vscode"
import { extractAgentsAndFolderPath } from "./api-helper"

export enum WorkspaceQuery {
Mine = "owner:me",
All = "",
}

export class WorkspaceProvider implements vscode.TreeDataProvider<WorkspaceTreeItem> {
constructor(private readonly getWorkspacesQuery: WorkspaceQuery) {}

private _onDidChangeTreeData: vscode.EventEmitter<WorkspaceTreeItem | undefined | null | void> =
new vscode.EventEmitter<WorkspaceTreeItem | undefined | null | void>()
readonly onDidChangeTreeData: vscode.Event<WorkspaceTreeItem | undefined | null | void> =
this._onDidChangeTreeData.event

refresh(): void {
this._onDidChangeTreeData.fire()
}

getTreeItem(element: WorkspaceTreeItem): vscode.TreeItem {
return element
}

getChildren(): Thenable<WorkspaceTreeItem[]> {
return getWorkspaces({ q: this.getWorkspacesQuery }).then((workspaces) => {
return workspaces.workspaces.map((workspace) => {
const status =
workspace.latest_build.status.substring(0, 1).toUpperCase() + workspace.latest_build.status.substring(1)

const label =
this.getWorkspacesQuery === WorkspaceQuery.All
? `${workspace.owner_name} / ${workspace.name}`
: workspace.name
const detail = `Template: ${workspace.template_display_name || workspace.template_name} • Status: ${status}`
const [, folderPath] = extractAgentsAndFolderPath(workspace)
return new WorkspaceTreeItem(label, detail, workspace.owner_name, workspace.name, folderPath)
})
})
}
}

export class WorkspaceTreeItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly tooltip: string,
public readonly workspaceOwner: string,
public readonly workspaceName: string,
public readonly workspaceFolderPath: string | undefined,
) {
super(label, vscode.TreeItemCollapsibleState.None)
}

iconPath = {
light: path.join(__filename, "..", "..", "media", "logo.svg"),
dark: path.join(__filename, "..", "..", "media", "logo.svg"),
}
}