Skip to content

Commit 0e76e62

Browse files
authored
feat: show agent metadata (#92)
1 parent 4c37680 commit 0e76e62

File tree

7 files changed

+170
-65
lines changed

7 files changed

+170
-65
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@
226226
"tar-fs": "^2.1.1",
227227
"which": "^2.0.2",
228228
"ws": "^8.11.0",
229-
"yaml": "^1.10.0"
229+
"yaml": "^1.10.0",
230+
"zod": "^3.21.4"
230231
}
231232
}

src/api-helper.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
2+
import { z } from "zod"
23

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

89
return agents
910
}
11+
12+
export const AgentMetadataEventSchema = z.object({
13+
result: z.object({
14+
collected_at: z.string(),
15+
age: z.number(),
16+
value: z.string(),
17+
error: z.string(),
18+
}),
19+
description: z.object({
20+
display_name: z.string(),
21+
key: z.string(),
22+
script: z.string(),
23+
interval: z.number(),
24+
timeout: z.number(),
25+
}),
26+
})
27+
28+
export const AgentMetadataEventSchemaArray = z.array(AgentMetadataEventSchema)
29+
30+
export type AgentMetadataEvent = z.infer<typeof AgentMetadataEventSchema>

src/commands.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as vscode from "vscode"
55
import { extractAgents } from "./api-helper"
66
import { Remote } from "./remote"
77
import { Storage } from "./storage"
8-
import { WorkspaceTreeItem } from "./workspacesProvider"
8+
import { OpenableTreeItem } from "./workspacesProvider"
99

1010
export class Commands {
1111
public constructor(private readonly vscodeProposed: typeof vscode, private readonly storage: Storage) {}
@@ -118,7 +118,7 @@ export class Commands {
118118
await vscode.commands.executeCommand("vscode.open", uri)
119119
}
120120

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

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

146-
public async openFromSidebar(treeItem: WorkspaceTreeItem) {
146+
public async openFromSidebar(treeItem: OpenableTreeItem) {
147147
if (treeItem) {
148148
await openWorkspace(
149149
treeItem.workspaceOwner,

src/extension.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"use strict"
2-
32
import { getAuthenticatedUser } from "coder/site/src/api/api"
43
import * as module from "module"
54
import * as vscode from "vscode"
@@ -13,8 +12,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
1312
const storage = new Storage(output, ctx.globalState, ctx.secrets, ctx.globalStorageUri, ctx.logUri)
1413
await storage.init()
1514

16-
const myWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.Mine)
17-
const allWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.All)
15+
const myWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.Mine, storage)
16+
const allWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.All, storage)
1817

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

src/remote.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,6 @@ export class Remote {
282282
"Coder-Session-Token": await this.storage.getSessionToken(),
283283
},
284284
})
285-
eventSource.addEventListener("open", () => {
286-
// TODO: Add debug output that we began watching here!
287-
})
288-
eventSource.addEventListener("error", () => {
289-
// TODO: Add debug output that we got an error here!
290-
})
291285

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

src/workspacesProvider.ts

Lines changed: 136 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,178 @@
11
import { getWorkspaces } from "coder/site/src/api/api"
2-
import { WorkspaceAgent } from "coder/site/src/api/typesGenerated"
2+
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
3+
import EventSource from "eventsource"
34
import * as path from "path"
45
import * as vscode from "vscode"
5-
import { extractAgents } from "./api-helper"
6+
import { AgentMetadataEvent, AgentMetadataEventSchemaArray, extractAgents } from "./api-helper"
7+
import { Storage } from "./storage"
68

79
export enum WorkspaceQuery {
810
Mine = "owner:me",
911
All = "",
1012
}
1113

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

15-
private _onDidChangeTreeData: vscode.EventEmitter<WorkspaceTreeItem | undefined | null | void> =
16-
new vscode.EventEmitter<WorkspaceTreeItem | undefined | null | void>()
17-
readonly onDidChangeTreeData: vscode.Event<WorkspaceTreeItem | undefined | null | void> =
18+
constructor(private readonly getWorkspacesQuery: WorkspaceQuery, private readonly storage: Storage) {
19+
getWorkspaces({ q: this.getWorkspacesQuery })
20+
.then((workspaces) => {
21+
const workspacesTreeItem: WorkspaceTreeItem[] = []
22+
workspaces.workspaces.forEach((workspace) => {
23+
const showMetadata = this.getWorkspacesQuery === WorkspaceQuery.Mine
24+
if (showMetadata) {
25+
const agents = extractAgents(workspace)
26+
agents.forEach((agent) => this.monitorMetadata(agent.id)) // monitor metadata for all agents
27+
}
28+
const treeItem = new WorkspaceTreeItem(
29+
workspace,
30+
this.getWorkspacesQuery === WorkspaceQuery.All,
31+
showMetadata,
32+
)
33+
workspacesTreeItem.push(treeItem)
34+
})
35+
return workspacesTreeItem
36+
})
37+
.then((workspaces) => {
38+
this.workspaces = workspaces
39+
this.refresh()
40+
})
41+
}
42+
43+
private _onDidChangeTreeData: vscode.EventEmitter<vscode.TreeItem | undefined | null | void> =
44+
new vscode.EventEmitter<vscode.TreeItem | undefined | null | void>()
45+
readonly onDidChangeTreeData: vscode.Event<vscode.TreeItem | undefined | null | void> =
1846
this._onDidChangeTreeData.event
1947

20-
refresh(): void {
21-
this._onDidChangeTreeData.fire()
48+
refresh(item: vscode.TreeItem | undefined | null | void): void {
49+
this._onDidChangeTreeData.fire(item)
2250
}
2351

24-
getTreeItem(element: WorkspaceTreeItem): vscode.TreeItem {
52+
async getTreeItem(element: vscode.TreeItem): Promise<vscode.TreeItem> {
2553
return element
2654
}
2755

28-
getChildren(element?: WorkspaceTreeItem): Thenable<WorkspaceTreeItem[]> {
56+
getChildren(element?: vscode.TreeItem): Thenable<vscode.TreeItem[]> {
2957
if (element) {
30-
if (element.agents.length > 0) {
31-
return Promise.resolve(
32-
element.agents.map((agent) => {
33-
const label = agent.name
34-
const detail = `Status: ${agent.status}`
35-
return new WorkspaceTreeItem(label, detail, "", "", agent.name, agent.expanded_directory, [], "coderAgent")
36-
}),
37-
)
58+
if (element instanceof WorkspaceTreeItem) {
59+
const agents = extractAgents(element.workspace)
60+
const agentTreeItems = agents.map((agent) => new AgentTreeItem(agent, element.watchMetadata))
61+
return Promise.resolve(agentTreeItems)
62+
} else if (element instanceof AgentTreeItem) {
63+
const savedMetadata = this.agentMetadata[element.agent.id] || []
64+
return Promise.resolve(savedMetadata.map((metadata) => new AgentMetadataTreeItem(metadata)))
3865
}
66+
3967
return Promise.resolve([])
4068
}
41-
return getWorkspaces({ q: this.getWorkspacesQuery }).then((workspaces) => {
42-
return workspaces.workspaces.map((workspace) => {
43-
const status =
44-
workspace.latest_build.status.substring(0, 1).toUpperCase() + workspace.latest_build.status.substring(1)
45-
46-
const label =
47-
this.getWorkspacesQuery === WorkspaceQuery.All
48-
? `${workspace.owner_name} / ${workspace.name}`
49-
: workspace.name
50-
const detail = `Template: ${workspace.template_display_name || workspace.template_name} • Status: ${status}`
51-
const agents = extractAgents(workspace)
52-
return new WorkspaceTreeItem(
53-
label,
54-
detail,
55-
workspace.owner_name,
56-
workspace.name,
57-
undefined,
58-
agents[0]?.expanded_directory,
59-
agents,
60-
agents.length > 1 ? "coderWorkspaceMultipleAgents" : "coderWorkspaceSingleAgent",
61-
)
62-
})
69+
return Promise.resolve(this.workspaces)
70+
}
71+
72+
async monitorMetadata(agentId: WorkspaceAgent["id"]): Promise<void> {
73+
const agentMetadataURL = new URL(`${this.storage.getURL()}/api/v2/workspaceagents/${agentId}/watch-metadata`)
74+
const agentMetadataEventSource = new EventSource(agentMetadataURL.toString(), {
75+
headers: {
76+
"Coder-Session-Token": await this.storage.getSessionToken(),
77+
},
78+
})
79+
80+
agentMetadataEventSource.addEventListener("data", (event) => {
81+
try {
82+
const dataEvent = JSON.parse(event.data)
83+
const agentMetadata = AgentMetadataEventSchemaArray.parse(dataEvent)
84+
85+
if (agentMetadata.length === 0) {
86+
agentMetadataEventSource.close()
87+
}
88+
89+
const savedMetadata = this.agentMetadata[agentId]
90+
if (JSON.stringify(savedMetadata) !== JSON.stringify(agentMetadata)) {
91+
this.agentMetadata[agentId] = agentMetadata // overwrite existing metadata
92+
this.refresh()
93+
}
94+
} catch (error) {
95+
agentMetadataEventSource.close()
96+
}
6397
})
6498
}
6599
}
66100

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

69-
export class WorkspaceTreeItem extends vscode.TreeItem {
103+
class AgentMetadataTreeItem extends vscode.TreeItem {
104+
constructor(metadataEvent: AgentMetadataEvent) {
105+
const label =
106+
metadataEvent.description.display_name.trim() + ": " + metadataEvent.result.value.replace(/\n/g, "").trim()
107+
108+
super(label, vscode.TreeItemCollapsibleState.None)
109+
this.tooltip = "Collected at " + metadataEvent.result.collected_at
110+
this.contextValue = "coderAgentMetadata"
111+
}
112+
}
113+
114+
export class OpenableTreeItem extends vscode.TreeItem {
70115
constructor(
71-
public readonly label: string,
72-
public readonly tooltip: string,
116+
label: string,
117+
tooltip: string,
118+
collapsibleState: vscode.TreeItemCollapsibleState,
119+
73120
public readonly workspaceOwner: string,
74121
public readonly workspaceName: string,
75122
public readonly workspaceAgent: string | undefined,
76123
public readonly workspaceFolderPath: string | undefined,
77-
public readonly agents: WorkspaceAgent[],
124+
78125
contextValue: CoderTreeItemType,
79126
) {
80-
super(
81-
label,
82-
contextValue === "coderWorkspaceMultipleAgents"
83-
? vscode.TreeItemCollapsibleState.Collapsed
84-
: vscode.TreeItemCollapsibleState.None,
85-
)
127+
super(label, collapsibleState)
86128
this.contextValue = contextValue
129+
this.tooltip = tooltip
87130
}
88131

89132
iconPath = {
90133
light: path.join(__filename, "..", "..", "media", "logo.svg"),
91134
dark: path.join(__filename, "..", "..", "media", "logo.svg"),
92135
}
93136
}
137+
138+
class AgentTreeItem extends OpenableTreeItem {
139+
constructor(public readonly agent: WorkspaceAgent, watchMetadata = false) {
140+
const label = agent.name
141+
const detail = `Status: ${agent.status}`
142+
super(
143+
label,
144+
detail,
145+
watchMetadata ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None,
146+
"",
147+
"",
148+
agent.name,
149+
agent.expanded_directory,
150+
"coderAgent",
151+
)
152+
}
153+
}
154+
155+
export class WorkspaceTreeItem extends OpenableTreeItem {
156+
constructor(
157+
public readonly workspace: Workspace,
158+
public readonly showOwner: boolean,
159+
public readonly watchMetadata = false,
160+
) {
161+
const status =
162+
workspace.latest_build.status.substring(0, 1).toUpperCase() + workspace.latest_build.status.substring(1)
163+
164+
const label = showOwner ? `${workspace.owner_name} / ${workspace.name}` : workspace.name
165+
const detail = `Template: ${workspace.template_display_name || workspace.template_name} • Status: ${status}`
166+
const agents = extractAgents(workspace)
167+
super(
168+
label,
169+
detail,
170+
showOwner ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.Expanded,
171+
workspace.owner_name,
172+
workspace.name,
173+
undefined,
174+
agents[0]?.expanded_directory,
175+
"coderWorkspaceMultipleAgents",
176+
)
177+
}
178+
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5761,3 +5761,8 @@ yocto-queue@^1.0.0:
57615761
version "1.0.0"
57625762
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
57635763
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
5764+
5765+
zod@^3.21.4:
5766+
version "3.21.4"
5767+
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
5768+
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==

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