diff --git a/.gitignore b/.gitignore index d535c22b..75f80c5f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /.vscode-test/ /.nyc_output/ /coverage/ -*.vsix \ No newline at end of file +*.vsix +yarn-error.log diff --git a/package.json b/package.json index 1553bdff..0c114daf 100644 --- a/package.json +++ b/package.json @@ -41,24 +41,25 @@ "views": { "coder": [ { - "id": "coderRemote", - "name": "", + "id": "myWorkspaces", + "name": "My Workspaces", + "visibility": "visible", + "icon": "media/logo.svg" + }, + { + "id": "allWorkspaces", + "name": "All Workspaces", "visibility": "visible", "icon": "media/logo.svg", - "contextualTitle": "Coder Remote" + "when": "coder.authenticated && coder.isOwner" } ] }, "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": [ @@ -68,18 +69,84 @@ }, { "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.navigateToWorkspaceSettings", + "title": "Edit Workspace Settings", + "when": "coder.authenticated", + "icon": "$(settings-gear)" }, { "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" + }, + { + "command": "coder.navigateToWorkspaceSettings", + "when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces", + "group": "inline" + } + ] + } }, "scripts": { "vscode:prepublish": "yarn package", diff --git a/src/api-helper.ts b/src/api-helper.ts new file mode 100644 index 00000000..75c0af83 --- /dev/null +++ b/src/api-helper.ts @@ -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 + } + return [agents, folderPath] +} diff --git a/src/commands.ts b/src/commands.ts index ccc2c653..bd4fc1b5 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -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) {} @@ -79,6 +81,9 @@ export class Commands { throw new Error("Failed to get authenticated user") } await vscode.commands.executeCommand("setContext", "coder.authenticated", true) + if (user.roles.find((role) => role.name === "owner")) { + await vscode.commands.executeCommand("setContext", "coder.isOwner", true) + } vscode.window .showInformationMessage( `Welcome to Coder, ${user.username}!`, @@ -108,7 +113,37 @@ export class Commands { }) } - public async open(...args: string[]): Promise { + public async createWorkspace(): Promise { + const uri = this.storage.getURL() + "/templates" + await vscode.commands.executeCommand("vscode.open", uri) + } + + public async navigateToWorkspace(workspace: WorkspaceTreeItem) { + if (workspace) { + const uri = this.storage.getURL() + `/@${workspace.workspaceOwner}/${workspace.workspaceName}` + await vscode.commands.executeCommand("vscode.open", uri) + } else if (this.storage.workspace) { + const uri = this.storage.getURL() + `/@${this.storage.workspace.owner_name}/${this.storage.workspace.name}` + await vscode.commands.executeCommand("vscode.open", uri) + } else { + vscode.window.showInformationMessage("No workspace found.") + } + } + + public async navigateToWorkspaceSettings(workspace: WorkspaceTreeItem) { + if (workspace) { + const uri = this.storage.getURL() + `/@${workspace.workspaceOwner}/${workspace.workspaceName}/settings` + await vscode.commands.executeCommand("vscode.open", uri) + } else if (this.storage.workspace) { + const uri = + this.storage.getURL() + `/@${this.storage.workspace.owner_name}/${this.storage.workspace.name}/settings` + await vscode.commands.executeCommand("vscode.open", uri) + } else { + vscode.window.showInformationMessage("No workspace found.") + } + } + + public async open(...args: unknown[]): Promise { let workspaceOwner: string let workspaceName: string let folderPath: string | undefined @@ -165,19 +200,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 diff --git a/src/extension.ts b/src/extension.ts index e5e73cd7..7131dd95 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,15 +6,27 @@ 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 { 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) + .then(async (user) => { + if (user) { + vscode.commands.executeCommand("setContext", "coder.authenticated", true) + if (user.roles.find((role) => role.name === "owner")) { + await vscode.commands.executeCommand("setContext", "coder.isOwner", true) + } + } }) .catch(() => { // Not authenticated! @@ -76,6 +88,16 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { 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.navigateToWorkspaceSettings", + commands.navigateToWorkspaceSettings.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 diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts new file mode 100644 index 00000000..f09b29e4 --- /dev/null +++ b/src/workspacesProvider.ts @@ -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 { + constructor(private readonly getWorkspacesQuery: WorkspaceQuery) {} + + private _onDidChangeTreeData: vscode.EventEmitter = + new vscode.EventEmitter() + readonly onDidChangeTreeData: vscode.Event = + this._onDidChangeTreeData.event + + refresh(): void { + this._onDidChangeTreeData.fire() + } + + getTreeItem(element: WorkspaceTreeItem): vscode.TreeItem { + return element + } + + getChildren(): Thenable { + 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"), + } +} 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