|
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