1
- import axios from "axios "
2
- import { getAuthenticatedUser , getWorkspaces , updateWorkspaceVersion } from "coder/site/src/api/api "
3
- import { Workspace , WorkspaceAgent } from "coder/site/src/api/typesGenerated"
1
+ import { Api } from "coder/site/src/api/api "
2
+ import { getErrorMessage } from "coder/site/src/api/errors "
3
+ import { User , Workspace , WorkspaceAgent } from "coder/site/src/api/typesGenerated"
4
4
import * as vscode from "vscode"
5
5
import { extractAgents } from "./api-helper"
6
6
import { CertificateError } from "./error"
@@ -11,6 +11,7 @@ import { OpenableTreeItem } from "./workspacesProvider"
11
11
export class Commands {
12
12
public constructor (
13
13
private readonly vscodeProposed : typeof vscode ,
14
+ private readonly restClient : Api ,
14
15
private readonly storage : Storage ,
15
16
) { }
16
17
@@ -82,7 +83,9 @@ export class Commands {
82
83
if ( ! url ) {
83
84
return
84
85
}
86
+ this . restClient . setHost ( url )
85
87
88
+ let user : User | undefined
86
89
let token : string | undefined = args . length >= 2 ? args [ 1 ] : undefined
87
90
if ( ! token ) {
88
91
const opened = await vscode . env . openExternal ( vscode . Uri . parse ( `${ url } /cli-auth` ) )
@@ -97,74 +100,72 @@ export class Commands {
97
100
placeHolder : "Copy your API key from the opened browser page." ,
98
101
value : await this . storage . getSessionToken ( ) ,
99
102
ignoreFocusOut : true ,
100
- validateInput : ( value ) => {
101
- return axios
102
- . get ( "/api/v2/users/me" , {
103
- baseURL : url ,
104
- headers : {
105
- "Coder-Session-Token" : value ,
106
- } ,
107
- } )
108
- . then ( ( ) => {
109
- return undefined
110
- } )
111
- . catch ( ( err ) => {
112
- if ( err instanceof CertificateError ) {
113
- err . showNotification ( )
114
-
115
- return {
116
- message : err . x509Err || err . message ,
117
- severity : vscode . InputBoxValidationSeverity . Error ,
118
- }
119
- }
120
- // This could be something like the header command erroring or an
121
- // invalid session token.
122
- const message =
123
- err ?. response ?. data ?. detail || err ?. message || err ?. response ?. status || "no response from the server"
103
+ validateInput : async ( value ) => {
104
+ this . restClient . setSessionToken ( value )
105
+ try {
106
+ user = await this . restClient . getAuthenticatedUser ( )
107
+ if ( ! user ) {
108
+ throw new Error ( "Failed to get authenticated user" )
109
+ }
110
+ } catch ( err ) {
111
+ // For certificate errors show both a notification and add to the
112
+ // text under the input box, since users sometimes miss the
113
+ // notification.
114
+ if ( err instanceof CertificateError ) {
115
+ err . showNotification ( )
116
+
124
117
return {
125
- message : "Failed to authenticate: " + message ,
118
+ message : err . x509Err || err . message ,
126
119
severity : vscode . InputBoxValidationSeverity . Error ,
127
120
}
128
- } )
121
+ }
122
+ // This could be something like the header command erroring or an
123
+ // invalid session token.
124
+ const message = getErrorMessage ( err , "no response from the server" )
125
+ return {
126
+ message : "Failed to authenticate: " + message ,
127
+ severity : vscode . InputBoxValidationSeverity . Error ,
128
+ }
129
+ }
129
130
} ,
130
131
} )
131
132
}
132
- if ( ! token ) {
133
+ if ( ! token || ! user ) {
133
134
return
134
135
}
135
136
137
+ // Store these to be used in later sessions and in the cli.
136
138
await this . storage . setURL ( url )
137
139
await this . storage . setSessionToken ( token )
138
- try {
139
- const user = await getAuthenticatedUser ( )
140
- if ( ! user ) {
141
- throw new Error ( "Failed to get authenticated user" )
142
- }
143
- await vscode . commands . executeCommand ( "setContext" , "coder.authenticated" , true )
144
- if ( user . roles . find ( ( role ) => role . name === "owner" ) ) {
145
- await vscode . commands . executeCommand ( "setContext" , "coder.isOwner" , true )
146
- }
147
- vscode . window
148
- . showInformationMessage (
149
- `Welcome to Coder, ${ user . username } !` ,
150
- {
151
- detail : "You can now use the Coder extension to manage your Coder instance." ,
152
- } ,
153
- "Open Workspace" ,
154
- )
155
- . then ( ( action ) => {
156
- if ( action === "Open Workspace" ) {
157
- vscode . commands . executeCommand ( "coder.open" )
158
- }
159
- } )
160
- vscode . commands . executeCommand ( "coder.refreshWorkspaces" )
161
- } catch ( error ) {
162
- vscode . window . showErrorMessage ( "Failed to authenticate with Coder: " + error )
140
+
141
+ await vscode . commands . executeCommand ( "setContext" , "coder.authenticated" , true )
142
+ if ( user . roles . find ( ( role ) => role . name === "owner" ) ) {
143
+ await vscode . commands . executeCommand ( "setContext" , "coder.isOwner" , true )
163
144
}
145
+
146
+ vscode . window
147
+ . showInformationMessage (
148
+ `Welcome to Coder, ${ user . username } !` ,
149
+ {
150
+ detail : "You can now use the Coder extension to manage your Coder instance." ,
151
+ } ,
152
+ "Open Workspace" ,
153
+ )
154
+ . then ( ( action ) => {
155
+ if ( action === "Open Workspace" ) {
156
+ vscode . commands . executeCommand ( "coder.open" )
157
+ }
158
+ } )
159
+
160
+ // Fetch workspaces for the new deployment.
161
+ vscode . commands . executeCommand ( "coder.refreshWorkspaces" )
164
162
}
165
163
166
- // viewLogs opens the workspace logs.
164
+ /**
165
+ * View the logs for the currently logged-in deployment.
166
+ */
167
167
public async viewLogs ( ) : Promise < void > {
168
+ // TODO: This will need to be refactored for multi-deployment support.
168
169
if ( ! this . storage . workspaceLogPath ) {
169
170
vscode . window . showInformationMessage ( "No logs available." , this . storage . workspaceLogPath || "<unset>" )
170
171
return
@@ -174,48 +175,81 @@ export class Commands {
174
175
await vscode . window . showTextDocument ( doc )
175
176
}
176
177
178
+ /**
179
+ * Log out from the currently logged-in deployment.
180
+ */
177
181
public async logout ( ) : Promise < void > {
182
+ // Clear from the REST client. An empty url will indicate to other parts of
183
+ // the code that we are logged out.
184
+ this . restClient . setHost ( "" )
185
+ this . restClient . setSessionToken ( "" )
186
+
187
+ // Clear from memory.
178
188
await this . storage . setURL ( undefined )
179
189
await this . storage . setSessionToken ( undefined )
190
+
180
191
await vscode . commands . executeCommand ( "setContext" , "coder.authenticated" , false )
181
192
vscode . window . showInformationMessage ( "You've been logged out of Coder!" , "Login" ) . then ( ( action ) => {
182
193
if ( action === "Login" ) {
183
194
vscode . commands . executeCommand ( "coder.login" )
184
195
}
185
196
} )
197
+
198
+ // This will result in clearing the workspace list.
186
199
vscode . commands . executeCommand ( "coder.refreshWorkspaces" )
187
200
}
188
201
202
+ /**
203
+ * Create a new workspace for the currently logged-in deployment.
204
+ *
205
+ * Must only be called if currently logged in.
206
+ */
189
207
public async createWorkspace ( ) : Promise < void > {
190
- const uri = this . storage . getURL ( ) + "/templates"
208
+ const uri = this . storage . getUrl ( ) + "/templates"
191
209
await vscode . commands . executeCommand ( "vscode.open" , uri )
192
210
}
193
211
212
+ /**
213
+ * Open a link to the workspace in the Coder dashboard.
214
+ *
215
+ * Must only be called if currently logged in.
216
+ */
194
217
public async navigateToWorkspace ( workspace : OpenableTreeItem ) {
195
218
if ( workspace ) {
196
- const uri = this . storage . getURL ( ) + `/@${ workspace . workspaceOwner } /${ workspace . workspaceName } `
219
+ const uri = this . storage . getUrl ( ) + `/@${ workspace . workspaceOwner } /${ workspace . workspaceName } `
197
220
await vscode . commands . executeCommand ( "vscode.open" , uri )
198
221
} else if ( this . storage . workspace ) {
199
- const uri = this . storage . getURL ( ) + `/@${ this . storage . workspace . owner_name } /${ this . storage . workspace . name } `
222
+ const uri = this . storage . getUrl ( ) + `/@${ this . storage . workspace . owner_name } /${ this . storage . workspace . name } `
200
223
await vscode . commands . executeCommand ( "vscode.open" , uri )
201
224
} else {
202
225
vscode . window . showInformationMessage ( "No workspace found." )
203
226
}
204
227
}
205
228
229
+ /**
230
+ * Open a link to the workspace settings in the Coder dashboard.
231
+ *
232
+ * Must only be called if currently logged in.
233
+ */
206
234
public async navigateToWorkspaceSettings ( workspace : OpenableTreeItem ) {
207
235
if ( workspace ) {
208
- const uri = this . storage . getURL ( ) + `/@${ workspace . workspaceOwner } /${ workspace . workspaceName } /settings`
236
+ const uri = this . storage . getUrl ( ) + `/@${ workspace . workspaceOwner } /${ workspace . workspaceName } /settings`
209
237
await vscode . commands . executeCommand ( "vscode.open" , uri )
210
238
} else if ( this . storage . workspace ) {
211
239
const uri =
212
- this . storage . getURL ( ) + `/@${ this . storage . workspace . owner_name } /${ this . storage . workspace . name } /settings`
240
+ this . storage . getUrl ( ) + `/@${ this . storage . workspace . owner_name } /${ this . storage . workspace . name } /settings`
213
241
await vscode . commands . executeCommand ( "vscode.open" , uri )
214
242
} else {
215
243
vscode . window . showInformationMessage ( "No workspace found." )
216
244
}
217
245
}
218
246
247
+ /**
248
+ * Open a workspace or agent that is showing in the sidebar.
249
+ *
250
+ * This essentially just builds the host name and passes it to the VS Code
251
+ * Remote SSH extension.
252
+ */
219
253
public async openFromSidebar ( treeItem : OpenableTreeItem ) {
220
254
if ( treeItem ) {
221
255
await openWorkspace (
@@ -228,6 +262,11 @@ export class Commands {
228
262
}
229
263
}
230
264
265
+ /**
266
+ * Open a workspace from the currently logged-in deployment.
267
+ *
268
+ * This must only be called if the REST client is logged in.
269
+ */
231
270
public async open ( ...args : unknown [ ] ) : Promise < void > {
232
271
let workspaceOwner : string
233
272
let workspaceName : string
@@ -243,9 +282,10 @@ export class Commands {
243
282
let lastWorkspaces : readonly Workspace [ ]
244
283
quickPick . onDidChangeValue ( ( value ) => {
245
284
quickPick . busy = true
246
- getWorkspaces ( {
247
- q : value ,
248
- } )
285
+ this . restClient
286
+ . getWorkspaces ( {
287
+ q : value ,
288
+ } )
249
289
. then ( ( workspaces ) => {
250
290
lastWorkspaces = workspaces . workspaces
251
291
const items : vscode . QuickPickItem [ ] = workspaces . workspaces . map ( ( workspace ) => {
@@ -348,8 +388,12 @@ export class Commands {
348
388
await openWorkspace ( workspaceOwner , workspaceName , workspaceAgent , folderPath , openRecent )
349
389
}
350
390
391
+ /**
392
+ * Update the current workspace. If there is no active workspace connection,
393
+ * this is a no-op.
394
+ */
351
395
public async updateWorkspace ( ) : Promise < void > {
352
- if ( ! this . storage . workspace ) {
396
+ if ( ! this . storage . workspace || ! this . storage . restClient ) {
353
397
return
354
398
}
355
399
const action = await this . vscodeProposed . window . showInformationMessage (
@@ -362,11 +406,15 @@ export class Commands {
362
406
"Update" ,
363
407
)
364
408
if ( action === "Update" ) {
365
- await updateWorkspaceVersion ( this . storage . workspace )
409
+ await this . storage . restClient . updateWorkspaceVersion ( this . storage . workspace )
366
410
}
367
411
}
368
412
}
369
413
414
+ /**
415
+ * Given a workspace, build the host name, find a directory to open, and pass
416
+ * both to the Remote SSH plugin.
417
+ */
370
418
async function openWorkspace (
371
419
workspaceOwner : string ,
372
420
workspaceName : string ,
0 commit comments