@@ -3,6 +3,7 @@ package com.coder.gateway.views.steps
3
3
import com.coder.gateway.CoderGatewayBundle
4
4
import com.coder.gateway.icons.CoderIcons
5
5
import com.coder.gateway.models.CoderWorkspacesWizardModel
6
+ import com.coder.gateway.models.TokenSource
6
7
import com.coder.gateway.models.WorkspaceAgentModel
7
8
import com.coder.gateway.models.WorkspaceAgentStatus
8
9
import com.coder.gateway.models.WorkspaceAgentStatus.FAILED
@@ -38,6 +39,7 @@ import com.intellij.openapi.components.service
38
39
import com.intellij.openapi.diagnostic.Logger
39
40
import com.intellij.openapi.rd.util.launchUnderBackgroundProgress
40
41
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
42
+ import com.intellij.openapi.ui.setEmptyState
41
43
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
42
44
import com.intellij.ui.AnActionButton
43
45
import com.intellij.ui.AppIcon
@@ -55,10 +57,12 @@ import com.intellij.ui.dsl.builder.bindSelected
55
57
import com.intellij.ui.dsl.builder.bindText
56
58
import com.intellij.ui.dsl.builder.panel
57
59
import com.intellij.ui.table.TableView
60
+ import com.intellij.util.applyIf
58
61
import com.intellij.util.ui.ColumnInfo
59
62
import com.intellij.util.ui.JBFont
60
63
import com.intellij.util.ui.JBUI
61
64
import com.intellij.util.ui.ListTableModel
65
+ import com.intellij.util.ui.UIUtil
62
66
import com.intellij.util.ui.table.IconTableCellRenderer
63
67
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
64
68
import kotlinx.coroutines.CoroutineScope
@@ -76,6 +80,7 @@ import java.awt.event.MouseListener
76
80
import java.awt.event.MouseMotionListener
77
81
import java.awt.font.TextAttribute
78
82
import java.awt.font.TextAttribute.UNDERLINE_ON
83
+ import java.net.ConnectException
79
84
import java.net.SocketTimeoutException
80
85
import java.net.URL
81
86
import java.nio.file.Path
@@ -126,6 +131,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
126
131
minWidth = JBUI .scale(52 )
127
132
}
128
133
rowHeight = 48
134
+ setEmptyState(" Disconnected" )
129
135
setSelectionMode(ListSelectionModel .SINGLE_SELECTION )
130
136
selectionModel.addListSelectionListener {
131
137
setNextButtonEnabled(selectedObject != null && selectedObject?.agentStatus == RUNNING && selectedObject?.agentOS == OS .LINUX )
@@ -345,7 +351,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
345
351
346
352
override fun onInit (wizardModel : CoderWorkspacesWizardModel ) {
347
353
listTableModelOfWorkspaces.items = emptyList()
348
- if (localWizardModel.coderURL.isNotBlank() && localWizardModel.token.isNotBlank() ) {
354
+ if (localWizardModel.coderURL.isNotBlank() && localWizardModel.token != null ) {
349
355
triggerWorkspacePolling(true )
350
356
} else {
351
357
val (url, token) = readStorageOrConfig()
@@ -354,15 +360,10 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
354
360
tfUrl?.text = url
355
361
}
356
362
if (! token.isNullOrBlank()) {
357
- localWizardModel.token = token
363
+ localWizardModel.token = Pair ( token, TokenSource . CONFIG )
358
364
}
359
365
if (! url.isNullOrBlank() && ! token.isNullOrBlank()) {
360
- // It could be jarring to suddenly ask for a token when you are
361
- // just trying to launch the Coder plugin so in this case where
362
- // we are trying to automatically connect to the last deployment
363
- // (or the deployment in the CLI config) do not ask for the
364
- // token again until they explicitly press connect.
365
- connect(false )
366
+ connect(url.toURL(), Pair (token, TokenSource .CONFIG ))
366
367
}
367
368
}
368
369
updateWorkspaceActions()
@@ -415,20 +416,26 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
415
416
/* *
416
417
* Ask for a new token (regardless of whether we already have a token),
417
418
* place it in the local model, then connect.
419
+ *
420
+ * If the token is invalid abort and start over from askTokenAndConnect()
421
+ * unless retry is false.
418
422
*/
419
- private fun askTokenAndConnect (openBrowser : Boolean = true) {
423
+ private fun askTokenAndConnect (isRetry : Boolean = false) {
424
+ val oldURL = localWizardModel.coderURL.toURL()
420
425
component.apply () // Force bindings to be filled.
426
+ val newURL = localWizardModel.coderURL.toURL()
421
427
val pastedToken = askToken(
422
- localWizardModel.coderURL.toURL(),
423
- localWizardModel.token,
424
- openBrowser,
428
+ newURL,
429
+ // If this is a new URL there is no point in trying to use the same
430
+ // token.
431
+ if (oldURL == newURL) localWizardModel.token else null ,
432
+ isRetry,
425
433
localWizardModel.useExistingToken,
426
- )
427
- if (pastedToken.isNullOrBlank()) {
428
- return // User aborted.
429
- }
434
+ ) ? : return // User aborted.
430
435
localWizardModel.token = pastedToken
431
- connect()
436
+ connect(newURL, pastedToken) {
437
+ askTokenAndConnect(true )
438
+ }
432
439
}
433
440
434
441
/* *
@@ -439,80 +446,112 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
439
446
* Existing workspaces will be immediately cleared before attempting to
440
447
* connect to the new deployment.
441
448
*
442
- * If the token is invalid abort and start over from askTokenAndConnect()
443
- * unless retry is false.
449
+ * If the token is invalid invoke onAuthFailure.
444
450
*/
445
- private fun connect (retry : Boolean = true) {
451
+ private fun connect (
452
+ deploymentURL : URL ,
453
+ token : Pair <String , TokenSource >,
454
+ onAuthFailure : (() -> Unit )? = null,
455
+ ): Job {
446
456
// Clear out old deployment details.
447
457
poller?.cancel()
458
+ tableOfWorkspaces.setEmptyState(" Connecting to $deploymentURL ..." )
448
459
listTableModelOfWorkspaces.items = emptyList()
449
460
450
- val deploymentURL = localWizardModel.coderURL.toURL()
451
- val token = localWizardModel.token
452
-
453
461
// Authenticate and load in a background process with progress.
454
462
// TODO: Make this cancelable.
455
- LifetimeDefinition ().launchUnderBackgroundProgress(
463
+ return LifetimeDefinition ().launchUnderBackgroundProgress(
456
464
CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.cli.downloader.dialog.title" ),
457
465
canBeCancelled = false ,
458
466
isIndeterminate = true
459
467
) {
468
+ val cliManager = CoderCLIManager (
469
+ deploymentURL,
470
+ if (settings.binaryDestination.isNotBlank()) Path .of(settings.binaryDestination)
471
+ else CoderCLIManager .getDataDir(),
472
+ settings.binarySource,
473
+ )
460
474
try {
461
475
this .indicator.text = " Authenticating client..."
462
- authenticate(deploymentURL, token)
476
+ authenticate(deploymentURL, token.first )
463
477
// Remember these in order to default to them for future attempts.
464
478
appPropertiesService.setValue(CODER_URL_KEY , deploymentURL.toString())
465
- appPropertiesService.setValue(SESSION_TOKEN , token)
479
+ appPropertiesService.setValue(SESSION_TOKEN , token.first )
466
480
467
481
this .indicator.text = " Downloading Coder CLI..."
468
- val cliManager = CoderCLIManager (
469
- deploymentURL,
470
- if (settings.binaryDestination.isNotBlank()) Path .of(settings.binaryDestination)
471
- else CoderCLIManager .getDataDir(),
472
- settings.binarySource,
473
- )
474
482
cliManager.downloadCLI()
475
483
476
484
this .indicator.text = " Authenticating Coder CLI..."
477
- cliManager.login(token)
485
+ cliManager.login(token.first )
478
486
479
487
this .indicator.text = " Retrieving workspaces..."
480
488
loadWorkspaces()
481
489
482
490
updateWorkspaceActions()
483
491
triggerWorkspacePolling(false )
484
- } catch (e: AuthenticationResponseException ) {
485
- logger.error(" Token was rejected by $deploymentURL ; has your token expired?" , e)
486
- if (retry) {
487
- askTokenAndConnect(false ) // Try again but no more opening browser windows.
488
- }
489
- } catch (e: SocketTimeoutException ) {
490
- logger.error(" Unable to connect to $deploymentURL ; is it up?" , e)
491
- } catch (e: ResponseException ) {
492
- logger.error(" Failed to download Coder CLI" , e)
492
+
493
+ tableOfWorkspaces.setEmptyState(" Connected to $deploymentURL " )
493
494
} catch (e: Exception ) {
494
- logger.error(" Failed to configure connection to $deploymentURL " , e)
495
+ val errorSummary = e.message ? : " No reason was provided"
496
+ var msg = CoderGatewayBundle .message(
497
+ " gateway.connector.view.workspaces.connect.failed" ,
498
+ deploymentURL,
499
+ errorSummary,
500
+ )
501
+ when (e) {
502
+ is AuthenticationResponseException -> {
503
+ msg = CoderGatewayBundle .message(
504
+ " gateway.connector.view.workspaces.connect.unauthorized" ,
505
+ deploymentURL,
506
+ )
507
+ cs.launch { onAuthFailure?.invoke() }
508
+ }
509
+
510
+ is SocketTimeoutException -> {
511
+ msg = CoderGatewayBundle .message(
512
+ " gateway.connector.view.workspaces.connect.timeout" ,
513
+ deploymentURL,
514
+ )
515
+ }
516
+
517
+ is ResponseException , is ConnectException -> {
518
+ msg = CoderGatewayBundle .message(
519
+ " gateway.connector.view.workspaces.connect.download-failed" ,
520
+ cliManager.remoteBinaryURL,
521
+ errorSummary,
522
+ )
523
+ }
524
+ }
525
+ tableOfWorkspaces.setEmptyState(msg)
526
+ logger.error(msg, e)
495
527
}
496
528
}
497
529
}
498
530
499
531
/* *
500
- * Open a dialog for providing the token. Show the existing token so the
501
- * user can validate it if a previous connection failed. Open a browser to
502
- * the auth page if openBrowser is true and useExisting is false. If
503
- * useExisting is true then populate the dialog with the token on disk if
504
- * there is one and it matches the url (this will overwrite the provided
505
- * token). Return the token submitted by the user.
532
+ * Open a dialog for providing the token. Show any existing token so the
533
+ * user can validate it if a previous connection failed. If we are not
534
+ * retrying and the user has not checked the existing token box then open a
535
+ * browser to the auth page. If the user has checked the existing token box
536
+ * then populate the dialog with the token on disk (this will overwrite any
537
+ * other existing token) unless this is a retry to avoid clobbering the
538
+ * token that just failed. Return the token submitted by the user.
506
539
*/
507
- private fun askToken (url : URL , token : String , openBrowser : Boolean , useExisting : Boolean ): String? {
508
- var existingToken = token
540
+ private fun askToken (
541
+ url : URL ,
542
+ token : Pair <String , TokenSource >? ,
543
+ isRetry : Boolean ,
544
+ useExisting : Boolean ,
545
+ ): Pair <String , TokenSource >? {
546
+ var (existingToken, tokenSource) = token ? : Pair (" " , TokenSource .USER )
509
547
val getTokenUrl = url.withPath(" /login?redirect=%2Fcli-auth" )
510
- if (openBrowser && ! useExisting) {
548
+ if (! isRetry && ! useExisting) {
511
549
BrowserUtil .browse(getTokenUrl)
512
- } else if (useExisting) {
550
+ } else if (! isRetry && useExisting) {
513
551
val (u, t) = CoderCLIManager .readConfig()
514
- if (url == u?.toURL() && ! t.isNullOrBlank()) {
515
- logger.info(" Injecting valid token from CLI config" )
552
+ if (url == u?.toURL() && ! t.isNullOrBlank() && t != existingToken) {
553
+ logger.info(" Injecting token from CLI config" )
554
+ tokenSource = TokenSource .CONFIG
516
555
existingToken = t
517
556
}
518
557
}
@@ -525,11 +564,32 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
525
564
CoderGatewayBundle .message(" gateway.connector.view.login.token.label" ),
526
565
getTokenUrl.toString()
527
566
)
528
- sessionTokenTextField = textField().applyToComponent {
529
- text = existingToken
530
- minimumSize = Dimension (520 , - 1 )
531
- }.component
532
- }
567
+ sessionTokenTextField = textField()
568
+ .applyToComponent {
569
+ text = existingToken
570
+ minimumSize = Dimension (520 , - 1 )
571
+ }.component
572
+ }.layout(RowLayout .PARENT_GRID )
573
+ row {
574
+ cell() // To align with the text box.
575
+ cell(
576
+ ComponentPanelBuilder .createCommentComponent(
577
+ CoderGatewayBundle .message(
578
+ if (isRetry) " gateway.connector.view.workspaces.token.rejected"
579
+ else if (tokenSource == TokenSource .CONFIG ) " gateway.connector.view.workspaces.token.injected"
580
+ else if (existingToken.isNotBlank()) " gateway.connector.view.workspaces.token.comment"
581
+ else " gateway.connector.view.workspaces.token.none"
582
+ ),
583
+ false ,
584
+ - 1 ,
585
+ true
586
+ ).applyIf(isRetry) {
587
+ apply {
588
+ foreground = UIUtil .getErrorForeground()
589
+ }
590
+ }
591
+ )
592
+ }.layout(RowLayout .PARENT_GRID )
533
593
}
534
594
AppIcon .getInstance().requestAttention(null , true )
535
595
if (! dialog(
@@ -542,7 +602,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
542
602
}
543
603
tokenFromUser = sessionTokenTextField.text
544
604
}, ModalityState .any())
545
- return tokenFromUser
605
+ if (tokenFromUser.isNullOrBlank()) {
606
+ return null
607
+ }
608
+ if (tokenFromUser != existingToken) {
609
+ tokenSource = TokenSource .USER
610
+ }
611
+ return Pair (tokenFromUser!! , tokenSource)
546
612
}
547
613
548
614
private fun triggerWorkspacePolling (fetchNow : Boolean ) {
0 commit comments