@@ -44,7 +44,15 @@ import com.intellij.ui.RelativeFont
44
44
import com.intellij.ui.ToolbarDecorator
45
45
import com.intellij.ui.components.JBTextField
46
46
import com.intellij.ui.components.dialog
47
- import com.intellij.ui.dsl.builder.*
47
+ import com.intellij.ui.dsl.builder.AlignX
48
+ import com.intellij.ui.dsl.builder.AlignY
49
+ import com.intellij.ui.dsl.builder.BottomGap
50
+ import com.intellij.ui.dsl.builder.RightGap
51
+ import com.intellij.ui.dsl.builder.RowLayout
52
+ import com.intellij.ui.dsl.builder.TopGap
53
+ import com.intellij.ui.dsl.builder.bindSelected
54
+ import com.intellij.ui.dsl.builder.bindText
55
+ import com.intellij.ui.dsl.builder.panel
48
56
import com.intellij.ui.table.TableView
49
57
import com.intellij.util.ui.ColumnInfo
50
58
import com.intellij.util.ui.JBFont
@@ -68,6 +76,7 @@ import java.awt.event.MouseMotionListener
68
76
import java.awt.font.TextAttribute
69
77
import java.awt.font.TextAttribute.UNDERLINE_ON
70
78
import java.net.SocketTimeoutException
79
+ import java.net.URL
71
80
import javax.swing.Icon
72
81
import javax.swing.JCheckBox
73
82
import javax.swing.JTable
@@ -214,15 +223,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
214
223
tfUrl = textField().resizableColumn().align(AlignX .FILL ).gap(RightGap .SMALL )
215
224
.bindText(localWizardModel::coderURL).applyToComponent {
216
225
addActionListener {
217
- poller?.cancel()
218
- listTableModelOfWorkspaces.items = emptyList()
219
- askTokenAndOpenSession(true )
226
+ // Reconnect when the enter key is pressed.
227
+ askTokenAndConnect()
220
228
}
221
229
}.component
222
230
button(CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.connect.text" )) {
223
- poller?.cancel()
224
- listTableModelOfWorkspaces.items = emptyList()
225
- askTokenAndOpenSession(true )
231
+ // Reconnect when the connect button is pressed.
232
+ askTokenAndConnect()
226
233
}.applyToComponent {
227
234
background = WelcomeScreenUIManager .getMainAssociatedComponentBackground()
228
235
}
@@ -347,7 +354,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
347
354
localWizardModel.token = token
348
355
}
349
356
if (! url.isNullOrBlank() && ! token.isNullOrBlank()) {
350
- loginAndLoadWorkspaces(token, true )
357
+ connect( )
351
358
}
352
359
}
353
360
updateWorkspaceActions()
@@ -397,105 +404,126 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
397
404
ActivityTracker .getInstance().inc()
398
405
}
399
406
400
- private fun askTokenAndOpenSession (openBrowser : Boolean ) {
401
- // force bindings to be filled
402
- component.apply ()
403
-
404
- val pastedToken = askToken(openBrowser)
407
+ /* *
408
+ * Ask for a new token (regardless of whether we already have a token),
409
+ * place it in the local model, then connect.
410
+ */
411
+ private fun askTokenAndConnect (openBrowser : Boolean = true) {
412
+ component.apply () // Force bindings to be filled.
413
+ val pastedToken = askToken(
414
+ localWizardModel.coderURL.toURL(),
415
+ localWizardModel.token,
416
+ openBrowser,
417
+ localWizardModel.useExistingToken,
418
+ )
405
419
if (pastedToken.isNullOrBlank()) {
406
- return
420
+ return // User aborted.
407
421
}
408
- // False so that subsequent authentication failures do not keep opening
409
- // the browser as it was already opened earlier.
410
- loginAndLoadWorkspaces(pastedToken, false )
422
+ localWizardModel.token = pastedToken
423
+ // If the token ends up being invalid we will ask for it again; pass
424
+ // false so we do not keep endlessly opening the browser.
425
+ connect(false )
411
426
}
412
427
413
- private fun loginAndLoadWorkspaces (token : String , openBrowser : Boolean ) {
414
- LifetimeDefinition ().launchUnderBackgroundProgress(CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.cli.downloader.dialog.title" ), canBeCancelled = false , isIndeterminate = true ) {
415
- this .indicator.apply {
416
- text = " Authenticating..."
417
- }
428
+ /* *
429
+ * Connect to the deployment in the local model and if successful store the
430
+ * URL and token for use as the default in subsequent launches then load
431
+ * workspaces into the table and keep it updated with a poll.
432
+ *
433
+ * Existing workspaces will be immediately cleared before attempting to
434
+ * connect to the new deployment.
435
+ *
436
+ * If the token is invalid abort and start over from askTokenAndConnect().
437
+ */
438
+ private fun connect (openBrowser : Boolean = true) {
439
+ // Clear out old deployment details.
440
+ poller?.cancel()
441
+ listTableModelOfWorkspaces.items = emptyList()
418
442
443
+ // Authenticate and load in a background process with progress.
444
+ // TODO: Make this cancelable.
445
+ LifetimeDefinition ().launchUnderBackgroundProgress(
446
+ CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.cli.downloader.dialog.title" ),
447
+ canBeCancelled = false ,
448
+ isIndeterminate = true
449
+ ) {
419
450
try {
420
- authenticate(token)
421
- } catch (e: AuthenticationResponseException ) {
422
- logger.error(" Unable to authenticate to ${localWizardModel.coderURL} ; has your token expired?" , e)
423
- askTokenAndOpenSession(openBrowser)
424
- return @launchUnderBackgroundProgress
425
- } catch (e: SocketTimeoutException ) {
426
- logger.error(" Unable to connect to ${localWizardModel.coderURL} ; is it up?" , e)
427
- return @launchUnderBackgroundProgress
428
- }
451
+ this .indicator.text = " Authenticating client..."
452
+ authenticate(localWizardModel.coderURL.toURL(), localWizardModel.token)
453
+ // Remember these in order to default to them for future attempts.
454
+ appPropertiesService.setValue(CODER_URL_KEY , localWizardModel.coderURL)
455
+ appPropertiesService.setValue(SESSION_TOKEN , localWizardModel.token)
429
456
430
- val cliManager = CoderCLIManager (localWizardModel.coderURL.toURL())
431
- localWizardModel.token = token
457
+ this .indicator.text = " Retrieving workspaces... "
458
+ loadWorkspaces()
432
459
433
- this .indicator.apply {
434
- isIndeterminate = false
435
- text = " Retrieving Workspaces..."
436
- fraction = 0.1
437
- }
460
+ this .indicator.text = " Downloading Coder CLI..."
461
+ val cliManager = CoderCLIManager (localWizardModel.coderURL.toURL())
462
+ cliManager.downloadCLI()
438
463
439
- loadWorkspaces()
464
+ this .indicator.text = " Authenticating Coder CLI..."
465
+ cliManager.login(localWizardModel.token)
440
466
441
- this .indicator.apply {
442
- isIndeterminate = false
443
- text = " Downloading Coder CLI..."
444
- fraction = 0.3
445
- }
446
- try {
447
- cliManager.downloadCLI()
467
+ this .indicator.text = " Configuring SSH..."
468
+ cliManager.configSsh()
469
+
470
+ updateWorkspaceActions()
471
+ triggerWorkspacePolling(false )
472
+ } catch (e: AuthenticationResponseException ) {
473
+ logger.error(" Token was rejected by ${localWizardModel.coderURL} ; has your token expired?" , e)
474
+ askTokenAndConnect(openBrowser) // Try again but no more opening browser windows.
475
+ } catch (e: SocketTimeoutException ) {
476
+ logger.error(" Unable to connect to ${localWizardModel.coderURL} ; is it up?" , e)
448
477
} catch (e: ResponseException ) {
449
- logger.error(" Download failed with response code ${e.code} " , e)
450
- return @launchUnderBackgroundProgress
451
- } catch (e: Exception ) {
452
478
logger.error(" Failed to download Coder CLI" , e)
453
- return @launchUnderBackgroundProgress
454
- }
455
- this .indicator.apply {
456
- text = " Logging in..."
457
- fraction = 0.5
458
- }
459
- cliManager.login(localWizardModel.coderURL, localWizardModel.token)
460
-
461
- this .indicator.apply {
462
- text = " Configuring SSH..."
463
- fraction = 0.7
479
+ } catch (e: Exception ) {
480
+ logger.error(" Failed to configure connection to ${localWizardModel.coderURL} " , e)
464
481
}
465
- cliManager.configSsh()
466
-
467
- this .indicator.fraction = 1.0
468
- updateWorkspaceActions()
469
- triggerWorkspacePolling(false )
470
482
}
471
483
}
472
484
473
- private fun askToken (openBrowser : Boolean ): String? {
474
- val getTokenUrl = localWizardModel.coderURL.toURL().withPath(" /login?redirect=%2Fcli-auth" )
475
- if (openBrowser && ! localWizardModel.useExistingToken) {
485
+ /* *
486
+ * Open a dialog for providing the token. Show the existing token so the
487
+ * user can validate it if a previous connection failed. Open a browser to
488
+ * the auth page if openBrowser is true and useExisting is false. If
489
+ * useExisting is true then populate the dialog with the token on disk if
490
+ * there is one and it matches the url (this will overwrite the provided
491
+ * token). Return the token submitted by the user.
492
+ */
493
+ private fun askToken (url : URL , token : String , openBrowser : Boolean , useExisting : Boolean ): String? {
494
+ var existingToken = token
495
+ val getTokenUrl = url.withPath(" /login?redirect=%2Fcli-auth" )
496
+ if (openBrowser && ! useExisting) {
476
497
BrowserUtil .browse(getTokenUrl)
477
- } else if (localWizardModel.useExistingToken ) {
478
- val (url, token ) = CoderCLIManager .readConfig()
479
- if (url == localWizardModel.coderURL && ! token .isNullOrBlank()) {
498
+ } else if (useExisting ) {
499
+ val (u, t ) = CoderCLIManager .readConfig()
500
+ if (url == u?.toURL() && ! t .isNullOrBlank()) {
480
501
logger.info(" Injecting valid token from CLI config" )
481
- localWizardModel.token = token
502
+ existingToken = t
482
503
}
483
504
}
484
505
var tokenFromUser: String? = null
485
506
ApplicationManager .getApplication().invokeAndWait({
486
507
lateinit var sessionTokenTextField: JBTextField
487
-
488
508
val panel = panel {
489
509
row {
490
- browserLink(CoderGatewayBundle .message(" gateway.connector.view.login.token.label" ), getTokenUrl.toString())
491
- sessionTokenTextField = textField().bindText(localWizardModel::token).applyToComponent {
510
+ browserLink(
511
+ CoderGatewayBundle .message(" gateway.connector.view.login.token.label" ),
512
+ getTokenUrl.toString()
513
+ )
514
+ sessionTokenTextField = textField().applyToComponent {
515
+ text = existingToken
492
516
minimumSize = Dimension (320 , - 1 )
493
517
}.component
494
518
}
495
519
}
496
-
497
520
AppIcon .getInstance().requestAttention(null , true )
498
- if (! dialog(CoderGatewayBundle .message(" gateway.connector.view.login.token.dialog" ), panel = panel, focusedComponent = sessionTokenTextField).showAndGet()) {
521
+ if (! dialog(
522
+ CoderGatewayBundle .message(" gateway.connector.view.login.token.dialog" ),
523
+ panel = panel,
524
+ focusedComponent = sessionTokenTextField
525
+ ).showAndGet()
526
+ ) {
499
527
return @invokeAndWait
500
528
}
501
529
tokenFromUser = sessionTokenTextField.text
@@ -518,13 +546,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
518
546
}
519
547
520
548
/* *
521
- * Check that the token is valid for the URL in the wizard and throw if not.
522
- * On success store the URL and token and display warning banners if
523
- * versions do not match.
549
+ * Authenticate the Coder client with the provided token and URL. On
550
+ * failure throw an error. On success display warning banners if versions
551
+ * do not match.
524
552
*/
525
- private fun authenticate (token : String ) {
526
- logger.info(" Authenticating to ${localWizardModel.coderURL} ..." )
527
- coderClient.initClientSession(localWizardModel.coderURL.toURL() , token)
553
+ private fun authenticate (url : URL , token : String ) {
554
+ logger.info(" Authenticating to $url ..." )
555
+ coderClient.initClientSession(url , token)
528
556
529
557
try {
530
558
logger.info(" Checking compatibility with Coder version ${coderClient.buildVersion} ..." )
@@ -534,7 +562,12 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
534
562
logger.warn(e)
535
563
notificationBanner.apply {
536
564
component.isVisible = true
537
- showWarning(CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.invalid.coder.version" , coderClient.buildVersion))
565
+ showWarning(
566
+ CoderGatewayBundle .message(
567
+ " gateway.connector.view.coder.workspaces.invalid.coder.version" ,
568
+ coderClient.buildVersion
569
+ )
570
+ )
538
571
}
539
572
} catch (e: IncompatibleVersionException ) {
540
573
logger.warn(e)
@@ -545,12 +578,11 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
545
578
}
546
579
547
580
logger.info(" Authenticated successfully" )
548
-
549
- // Remember these in order to default to them for future attempts.
550
- appPropertiesService.setValue(CODER_URL_KEY , localWizardModel.coderURL)
551
- appPropertiesService.setValue(SESSION_TOKEN , token)
552
581
}
553
582
583
+ /* *
584
+ * Request workspaces then update the table.
585
+ */
554
586
private suspend fun loadWorkspaces () {
555
587
val ws = withContext(Dispatchers .IO ) {
556
588
val timeBeforeRequestingWorkspaces = System .currentTimeMillis()
0 commit comments