Skip to content

feat: show agent metadata #92

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 7 commits into from
May 9, 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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@
"tar-fs": "^2.1.1",
"which": "^2.0.2",
"ws": "^8.11.0",
"yaml": "^1.10.0"
"yaml": "^1.10.0",
"zod": "^3.21.4"
}
}
21 changes: 21 additions & 0 deletions src/api-helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import { z } from "zod"

export function extractAgents(workspace: Workspace): WorkspaceAgent[] {
const agents = workspace.latest_build.resources.reduce((acc, resource) => {
Expand All @@ -7,3 +8,23 @@ export function extractAgents(workspace: Workspace): WorkspaceAgent[] {

return agents
}

export const AgentMetadataEventSchema = z.object({
result: z.object({
collected_at: z.string(),
age: z.number(),
value: z.string(),
error: z.string(),
}),
description: z.object({
display_name: z.string(),
key: z.string(),
script: z.string(),
interval: z.number(),
timeout: z.number(),
}),
})

export const AgentMetadataEventSchemaArray = z.array(AgentMetadataEventSchema)

export type AgentMetadataEvent = z.infer<typeof AgentMetadataEventSchema>
8 changes: 4 additions & 4 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as vscode from "vscode"
import { extractAgents } from "./api-helper"
import { Remote } from "./remote"
import { Storage } from "./storage"
import { WorkspaceTreeItem } from "./workspacesProvider"
import { OpenableTreeItem } from "./workspacesProvider"

export class Commands {
public constructor(private readonly vscodeProposed: typeof vscode, private readonly storage: Storage) {}
Expand Down Expand Up @@ -118,7 +118,7 @@ export class Commands {
await vscode.commands.executeCommand("vscode.open", uri)
}

public async navigateToWorkspace(workspace: WorkspaceTreeItem) {
public async navigateToWorkspace(workspace: OpenableTreeItem) {
if (workspace) {
const uri = this.storage.getURL() + `/@${workspace.workspaceOwner}/${workspace.workspaceName}`
await vscode.commands.executeCommand("vscode.open", uri)
Expand All @@ -130,7 +130,7 @@ export class Commands {
}
}

public async navigateToWorkspaceSettings(workspace: WorkspaceTreeItem) {
public async navigateToWorkspaceSettings(workspace: OpenableTreeItem) {
if (workspace) {
const uri = this.storage.getURL() + `/@${workspace.workspaceOwner}/${workspace.workspaceName}/settings`
await vscode.commands.executeCommand("vscode.open", uri)
Expand All @@ -143,7 +143,7 @@ export class Commands {
}
}

public async openFromSidebar(treeItem: WorkspaceTreeItem) {
public async openFromSidebar(treeItem: OpenableTreeItem) {
if (treeItem) {
await openWorkspace(
treeItem.workspaceOwner,
Expand Down
5 changes: 2 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"use strict"

import { getAuthenticatedUser } from "coder/site/src/api/api"
import * as module from "module"
import * as vscode from "vscode"
Expand All @@ -13,8 +12,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
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)
const myWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.Mine, storage)
const allWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.All, storage)

vscode.window.registerTreeDataProvider("myWorkspaces", myWorkspacesProvider)
vscode.window.registerTreeDataProvider("allWorkspaces", allWorkspacesProvider)
Expand Down
6 changes: 0 additions & 6 deletions src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,6 @@ export class Remote {
"Coder-Session-Token": await this.storage.getSessionToken(),
},
})
eventSource.addEventListener("open", () => {
// TODO: Add debug output that we began watching here!
})
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)
Expand Down
187 changes: 136 additions & 51 deletions src/workspacesProvider.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,178 @@
import { getWorkspaces } from "coder/site/src/api/api"
import { WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import EventSource from "eventsource"
import * as path from "path"
import * as vscode from "vscode"
import { extractAgents } from "./api-helper"
import { AgentMetadataEvent, AgentMetadataEventSchemaArray, extractAgents } from "./api-helper"
import { Storage } from "./storage"

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

export class WorkspaceProvider implements vscode.TreeDataProvider<WorkspaceTreeItem> {
constructor(private readonly getWorkspacesQuery: WorkspaceQuery) {}
export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
private workspaces: WorkspaceTreeItem[] = []
private agentMetadata: Record<WorkspaceAgent["id"], AgentMetadataEvent[]> = {}

private _onDidChangeTreeData: vscode.EventEmitter<WorkspaceTreeItem | undefined | null | void> =
new vscode.EventEmitter<WorkspaceTreeItem | undefined | null | void>()
readonly onDidChangeTreeData: vscode.Event<WorkspaceTreeItem | undefined | null | void> =
constructor(private readonly getWorkspacesQuery: WorkspaceQuery, private readonly storage: Storage) {
getWorkspaces({ q: this.getWorkspacesQuery })
.then((workspaces) => {
const workspacesTreeItem: WorkspaceTreeItem[] = []
workspaces.workspaces.forEach((workspace) => {
const showMetadata = this.getWorkspacesQuery === WorkspaceQuery.Mine
if (showMetadata) {
const agents = extractAgents(workspace)
agents.forEach((agent) => this.monitorMetadata(agent.id)) // monitor metadata for all agents
}
const treeItem = new WorkspaceTreeItem(
workspace,
this.getWorkspacesQuery === WorkspaceQuery.All,
showMetadata,
)
workspacesTreeItem.push(treeItem)
})
return workspacesTreeItem
})
.then((workspaces) => {
this.workspaces = workspaces
this.refresh()
})
}

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

refresh(): void {
this._onDidChangeTreeData.fire()
refresh(item: vscode.TreeItem | undefined | null | void): void {
this._onDidChangeTreeData.fire(item)
}

getTreeItem(element: WorkspaceTreeItem): vscode.TreeItem {
async getTreeItem(element: vscode.TreeItem): Promise<vscode.TreeItem> {
return element
}

getChildren(element?: WorkspaceTreeItem): Thenable<WorkspaceTreeItem[]> {
getChildren(element?: vscode.TreeItem): Thenable<vscode.TreeItem[]> {
if (element) {
if (element.agents.length > 0) {
return Promise.resolve(
element.agents.map((agent) => {
const label = agent.name
const detail = `Status: ${agent.status}`
return new WorkspaceTreeItem(label, detail, "", "", agent.name, agent.expanded_directory, [], "coderAgent")
}),
)
if (element instanceof WorkspaceTreeItem) {
const agents = extractAgents(element.workspace)
const agentTreeItems = agents.map((agent) => new AgentTreeItem(agent, element.watchMetadata))
return Promise.resolve(agentTreeItems)
} else if (element instanceof AgentTreeItem) {
const savedMetadata = this.agentMetadata[element.agent.id] || []
return Promise.resolve(savedMetadata.map((metadata) => new AgentMetadataTreeItem(metadata)))
}

return Promise.resolve([])
}
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 agents = extractAgents(workspace)
return new WorkspaceTreeItem(
label,
detail,
workspace.owner_name,
workspace.name,
undefined,
agents[0]?.expanded_directory,
agents,
agents.length > 1 ? "coderWorkspaceMultipleAgents" : "coderWorkspaceSingleAgent",
)
})
return Promise.resolve(this.workspaces)
}

async monitorMetadata(agentId: WorkspaceAgent["id"]): Promise<void> {
const agentMetadataURL = new URL(`${this.storage.getURL()}/api/v2/workspaceagents/${agentId}/watch-metadata`)
const agentMetadataEventSource = new EventSource(agentMetadataURL.toString(), {
headers: {
"Coder-Session-Token": await this.storage.getSessionToken(),
},
})

agentMetadataEventSource.addEventListener("data", (event) => {
try {
const dataEvent = JSON.parse(event.data)
const agentMetadata = AgentMetadataEventSchemaArray.parse(dataEvent)

if (agentMetadata.length === 0) {
agentMetadataEventSource.close()
}

const savedMetadata = this.agentMetadata[agentId]
if (JSON.stringify(savedMetadata) !== JSON.stringify(agentMetadata)) {
this.agentMetadata[agentId] = agentMetadata // overwrite existing metadata
this.refresh()
}
} catch (error) {
agentMetadataEventSource.close()
}
})
}
}

type CoderTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"

export class WorkspaceTreeItem extends vscode.TreeItem {
class AgentMetadataTreeItem extends vscode.TreeItem {
constructor(metadataEvent: AgentMetadataEvent) {
const label =
metadataEvent.description.display_name.trim() + ": " + metadataEvent.result.value.replace(/\n/g, "").trim()

super(label, vscode.TreeItemCollapsibleState.None)
this.tooltip = "Collected at " + metadataEvent.result.collected_at
this.contextValue = "coderAgentMetadata"
}
}

export class OpenableTreeItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly tooltip: string,
label: string,
tooltip: string,
collapsibleState: vscode.TreeItemCollapsibleState,

public readonly workspaceOwner: string,
public readonly workspaceName: string,
public readonly workspaceAgent: string | undefined,
public readonly workspaceFolderPath: string | undefined,
public readonly agents: WorkspaceAgent[],

contextValue: CoderTreeItemType,
) {
super(
label,
contextValue === "coderWorkspaceMultipleAgents"
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None,
)
super(label, collapsibleState)
this.contextValue = contextValue
this.tooltip = tooltip
}

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

class AgentTreeItem extends OpenableTreeItem {
constructor(public readonly agent: WorkspaceAgent, watchMetadata = false) {
const label = agent.name
const detail = `Status: ${agent.status}`
super(
label,
detail,
watchMetadata ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None,
"",
"",
agent.name,
agent.expanded_directory,
"coderAgent",
)
}
}

export class WorkspaceTreeItem extends OpenableTreeItem {
constructor(
public readonly workspace: Workspace,
public readonly showOwner: boolean,
public readonly watchMetadata = false,
) {
const status =
workspace.latest_build.status.substring(0, 1).toUpperCase() + workspace.latest_build.status.substring(1)

const label = showOwner ? `${workspace.owner_name} / ${workspace.name}` : workspace.name
const detail = `Template: ${workspace.template_display_name || workspace.template_name} • Status: ${status}`
const agents = extractAgents(workspace)
super(
label,
detail,
showOwner ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.Expanded,
workspace.owner_name,
workspace.name,
undefined,
agents[0]?.expanded_directory,
"coderWorkspaceMultipleAgents",
)
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5761,3 +5761,8 @@ yocto-queue@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==

zod@^3.21.4:
version "3.21.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==