Skip to content

feat: show workspace updates in the ui #55

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 2 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@
"activationEvents": [
"onResolveRemoteAuthority:ssh-remote",
"onCommand:coder.connect",
"onCommand:coder.open",
"onCommand:coder.login",
"onView:coderRemote",
"onUri"
],
"extensionDependencies": [
Expand Down Expand Up @@ -77,6 +74,11 @@
{
"command": "coder.open",
"title": "Coder: Open Workspace"
},
{
"command": "coder.workspace.update",
"title": "Coder: Update Workspace",
"when": "coder.workspace.updatable"
}
]
},
Expand Down
22 changes: 20 additions & 2 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import axios from "axios"
import { getUser, getWorkspaces } from "coder/site/src/api/api"
import { getUser, getWorkspaces, updateWorkspaceVersion } from "coder/site/src/api/api"
import { Workspace } from "coder/site/src/api/typesGenerated"
import * as vscode from "vscode"
import { Remote } from "./remote"
import { Storage } from "./storage"

export class Commands {
public constructor(private readonly storage: Storage) {}
public constructor(private readonly vscodeProposed: typeof vscode, private readonly storage: Storage) {}

public async login(...args: string[]): Promise<void> {
let url: string | undefined = args.length >= 1 ? args[0] : undefined
Expand Down Expand Up @@ -215,4 +215,22 @@ export class Commands {
reuseWindow: !newWindow,
})
}

public async updateWorkspace(): Promise<void> {
if (!this.storage.workspace) {
return
}
const action = await this.vscodeProposed.window.showInformationMessage(
"Update Workspace",
{
useCustom: true,
modal: true,
detail: `${this.storage.workspace.owner_name}/${this.storage.workspace.name} will be updated then this window will reload to watch the build logs and reconnect.`,
},
"Update",
)
if (action === "Update") {
await updateWorkspaceVersion(this.storage.workspace)
}
}
}
13 changes: 7 additions & 6 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
},
})

const commands = new Commands(storage)

vscode.commands.registerCommand("coder.login", commands.login.bind(commands))
vscode.commands.registerCommand("coder.logout", commands.logout.bind(commands))
vscode.commands.registerCommand("coder.open", commands.open.bind(commands))

// The Remote SSH extension's proposed APIs are used to override
// the SSH host name in VS Code itself. It's visually unappealing
// having a lengthy name!
Expand All @@ -75,6 +69,13 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
false,
)

const commands = new Commands(vscodeProposed, storage)

vscode.commands.registerCommand("coder.login", commands.login.bind(commands))
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))

// Since the "onResolveRemoteAuthority:ssh-remote" activation event exists
// in package.json we're able to perform actions before the authority is
// resolved by the remote SSH extension.
Expand Down
76 changes: 61 additions & 15 deletions src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,8 @@ export class Remote {
}

// Find the workspace from the URI scheme provided!
let workspace: Workspace
try {
workspace = await getWorkspaceByOwnerAndName(parts[0], parts[1])
this.storage.workspace = await getWorkspaceByOwnerAndName(parts[0], parts[1])
} catch (error) {
if (!axios.isAxiosError(error)) {
throw error
Expand Down Expand Up @@ -120,10 +119,10 @@ export class Remote {

const disposables: vscode.Disposable[] = []
// Register before connection so the label still displays!
disposables.push(this.registerLabelFormatter(`${workspace.owner_name}/${workspace.name}`))
disposables.push(this.registerLabelFormatter(`${this.storage.workspace.owner_name}/${this.storage.workspace.name}`))

let buildComplete: undefined | (() => void)
if (workspace.latest_build.status === "stopped") {
if (this.storage.workspace.latest_build.status === "stopped") {
this.vscodeProposed.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
Expand All @@ -135,15 +134,18 @@ export class Remote {
buildComplete = r
}),
)
workspace = {
...workspace,
latest_build: await startWorkspace(workspace.id),
this.storage.workspace = {
...this.storage.workspace,
latest_build: await startWorkspace(this.storage.workspace.id),
}
}

// If a build is running we should stream the logs to the user so they can
// watch what's going on!
if (workspace.latest_build.status === "pending" || workspace.latest_build.status === "starting") {
if (
this.storage.workspace.latest_build.status === "pending" ||
this.storage.workspace.latest_build.status === "starting"
) {
const writeEmitter = new vscode.EventEmitter<string>()
// We use a terminal instead of an output channel because it feels more
// familiar to a user!
Expand All @@ -160,11 +162,11 @@ export class Remote {
} as Partial<vscode.Pseudoterminal> as any,
})
// This fetches the initial bunch of logs.
const logs = await getWorkspaceBuildLogs(workspace.latest_build.id, new Date())
const logs = await getWorkspaceBuildLogs(this.storage.workspace.latest_build.id, new Date())
logs.forEach((log) => writeEmitter.fire(log.output + "\r\n"))
terminal.show(true)
// This follows the logs for new activity!
let path = `/api/v2/workspacebuilds/${workspace.latest_build.id}/logs?follow=true`
let path = `/api/v2/workspacebuilds/${this.storage.workspace.latest_build.id}/logs?follow=true`
if (logs.length) {
path += `&after=${logs[logs.length - 1].id}`
}
Expand Down Expand Up @@ -198,15 +200,15 @@ export class Remote {
})
})
writeEmitter.fire("Build complete")
workspace = await getWorkspace(workspace.id)
terminal.hide()
this.storage.workspace = await getWorkspace(this.storage.workspace.id)
terminal.dispose()

if (buildComplete) {
buildComplete()
}
}

const agents = workspace.latest_build.resources.reduce((acc, resource) => {
const agents = this.storage.workspace.latest_build.resources.reduce((acc, resource) => {
return acc.concat(resource.agents || [])
}, [] as WorkspaceAgent[])

Expand Down Expand Up @@ -250,7 +252,7 @@ export class Remote {
await fs.writeFile(this.storage.getUserSettingsPath(), jsonc.applyEdits(settingsContent, edits))

const workspaceUpdate = new vscode.EventEmitter<Workspace>()
const watchURL = new URL(`${this.storage.getURL()}/api/v2/workspaces/${workspace.id}/watch`)
const watchURL = new URL(`${this.storage.getURL()}/api/v2/workspaces/${this.storage.workspace.id}/watch`)
const eventSource = new EventSource(watchURL.toString(), {
headers: {
"Coder-Session-Token": await this.storage.getSessionToken(),
Expand All @@ -262,11 +264,48 @@ export class Remote {
eventSource.addEventListener("error", () => {
// TODO: Add debug output that we got an error here!
})

const workspaceUpdatedStatus = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 999)
disposables.push(workspaceUpdatedStatus)

let hasShownOutdatedNotification = false
const refreshWorkspaceUpdatedStatus = (newWorkspace: Workspace) => {
// If the newly gotten workspace was updated, then we show a notification
// to the user that they should update.
if (newWorkspace.outdated) {
if (!this.storage.workspace?.outdated || !hasShownOutdatedNotification) {
hasShownOutdatedNotification = true
vscode.window
.showInformationMessage("A new version of your workspace is available.", "Update")
.then((action) => {
if (action === "Update") {
vscode.commands.executeCommand("coder.workspace.update", newWorkspace)
}
})
}
}
if (!newWorkspace.outdated) {
vscode.commands.executeCommand("setContext", "coder.workspace.updatable", false)
workspaceUpdatedStatus.hide()
return
}
workspaceUpdatedStatus.name = "Coder Workspace Update"
workspaceUpdatedStatus.text = "$(fold-up) Update Workspace"
workspaceUpdatedStatus.command = "coder.workspace.update"
// Important for hiding the "Update Workspace" command.
vscode.commands.executeCommand("setContext", "coder.workspace.updatable", true)
workspaceUpdatedStatus.show()
}
// Show an initial status!
refreshWorkspaceUpdatedStatus(this.storage.workspace)

eventSource.addEventListener("data", (event: MessageEvent<string>) => {
const workspace = JSON.parse(event.data) as Workspace
if (!workspace) {
return
}
refreshWorkspaceUpdatedStatus(workspace)
this.storage.workspace = workspace
workspaceUpdate.fire(workspace)
if (workspace.latest_build.status === "stopping" || workspace.latest_build.status === "stopped") {
const action = this.vscodeProposed.window.showInformationMessage(
Expand All @@ -283,6 +322,13 @@ export class Remote {
}
this.reloadWindow()
}
// If a new build is initialized for a workspace, we automatically
// reload the window. Then the build log will appear, and startup
// will continue as expected.
if (workspace.latest_build.status === "starting") {
this.reloadWindow()
return
}
})

if (agent.status === "connecting") {
Expand Down Expand Up @@ -352,7 +398,7 @@ export class Remote {
})

// Register the label formatter again because SSH overrides it!
let label = `${workspace.owner_name}/${workspace.name}`
let label = `${this.storage.workspace.owner_name}/${this.storage.workspace.name}`
if (agents.length > 1) {
label += `/${agent.name}`
}
Expand Down
5 changes: 4 additions & 1 deletion src/storage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import axios from "axios"
import { execFile } from "child_process"
import { getBuildInfo } from "coder/site/src/api/api"
import { Workspace } from "coder/site/src/api/typesGenerated"
import * as crypto from "crypto"
import { createWriteStream, createReadStream } from "fs"
import { createReadStream, createWriteStream } from "fs"
import fs from "fs/promises"
import { ensureDir } from "fs-extra"
import { IncomingMessage } from "http"
Expand All @@ -12,6 +13,8 @@ import prettyBytes from "pretty-bytes"
import * as vscode from "vscode"

export class Storage {
public workspace?: Workspace

constructor(
private readonly output: vscode.OutputChannel,
private readonly memento: vscode.Memento,
Expand Down