|
1 | 1 | 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" |
3 | 4 | import * as path from "path" |
4 | 5 | import * as vscode from "vscode" |
5 | | -import { extractAgents } from "./api-helper" |
| 6 | +import { AgentMetadataEvent, AgentMetadataEventSchemaArray, extractAgents } from "./api-helper" |
| 7 | +import { Storage } from "./storage" |
6 | 8 |
|
7 | 9 | export enum WorkspaceQuery { |
8 | 10 | Mine = "owner:me", |
9 | 11 | All = "", |
10 | 12 | } |
11 | 13 |
|
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[]> = {} |
14 | 17 |
|
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> = |
18 | 46 | this._onDidChangeTreeData.event |
19 | 47 |
|
20 | | - refresh(): void { |
21 | | - this._onDidChangeTreeData.fire() |
| 48 | + refresh(item: vscode.TreeItem | undefined | null | void): void { |
| 49 | + this._onDidChangeTreeData.fire(item) |
22 | 50 | } |
23 | 51 |
|
24 | | - getTreeItem(element: WorkspaceTreeItem): vscode.TreeItem { |
| 52 | + async getTreeItem(element: vscode.TreeItem): Promise<vscode.TreeItem> { |
25 | 53 | return element |
26 | 54 | } |
27 | 55 |
|
28 | | - getChildren(element?: WorkspaceTreeItem): Thenable<WorkspaceTreeItem[]> { |
| 56 | + getChildren(element?: vscode.TreeItem): Thenable<vscode.TreeItem[]> { |
29 | 57 | 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))) |
38 | 65 | } |
| 66 | + |
39 | 67 | return Promise.resolve([]) |
40 | 68 | } |
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 | + } |
63 | 97 | }) |
64 | 98 | } |
65 | 99 | } |
66 | 100 |
|
67 | 101 | type CoderTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent" |
68 | 102 |
|
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 { |
70 | 115 | constructor( |
71 | | - public readonly label: string, |
72 | | - public readonly tooltip: string, |
| 116 | + label: string, |
| 117 | + tooltip: string, |
| 118 | + collapsibleState: vscode.TreeItemCollapsibleState, |
| 119 | + |
73 | 120 | public readonly workspaceOwner: string, |
74 | 121 | public readonly workspaceName: string, |
75 | 122 | public readonly workspaceAgent: string | undefined, |
76 | 123 | public readonly workspaceFolderPath: string | undefined, |
77 | | - public readonly agents: WorkspaceAgent[], |
| 124 | + |
78 | 125 | contextValue: CoderTreeItemType, |
79 | 126 | ) { |
80 | | - super( |
81 | | - label, |
82 | | - contextValue === "coderWorkspaceMultipleAgents" |
83 | | - ? vscode.TreeItemCollapsibleState.Collapsed |
84 | | - : vscode.TreeItemCollapsibleState.None, |
85 | | - ) |
| 127 | + super(label, collapsibleState) |
86 | 128 | this.contextValue = contextValue |
| 129 | + this.tooltip = tooltip |
87 | 130 | } |
88 | 131 |
|
89 | 132 | iconPath = { |
90 | 133 | light: path.join(__filename, "..", "..", "media", "logo.svg"), |
91 | 134 | dark: path.join(__filename, "..", "..", "media", "logo.svg"), |
92 | 135 | } |
93 | 136 | } |
| 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 | +} |
0 commit comments