@@ -3,6 +3,7 @@ package com.coder.gateway.views.steps
33import com.coder.gateway.CoderGatewayBundle
44import com.coder.gateway.icons.CoderIcons
55import com.coder.gateway.models.CoderWorkspacesWizardModel
6+ import com.coder.gateway.models.TokenSource
67import com.coder.gateway.models.WorkspaceAgentModel
78import com.coder.gateway.models.WorkspaceAgentStatus
89import com.coder.gateway.models.WorkspaceAgentStatus.FAILED
@@ -38,6 +39,7 @@ import com.intellij.openapi.components.service
3839import com.intellij.openapi.diagnostic.Logger
3940import com.intellij.openapi.rd.util.launchUnderBackgroundProgress
4041import com.intellij.openapi.ui.panel.ComponentPanelBuilder
42+ import com.intellij.openapi.ui.setEmptyState
4143import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
4244import com.intellij.ui.AnActionButton
4345import com.intellij.ui.AppIcon
@@ -55,10 +57,12 @@ import com.intellij.ui.dsl.builder.bindSelected
5557import com.intellij.ui.dsl.builder.bindText
5658import com.intellij.ui.dsl.builder.panel
5759import com.intellij.ui.table.TableView
60+ import com.intellij.util.applyIf
5861import com.intellij.util.ui.ColumnInfo
5962import com.intellij.util.ui.JBFont
6063import com.intellij.util.ui.JBUI
6164import com.intellij.util.ui.ListTableModel
65+ import com.intellij.util.ui.UIUtil
6266import com.intellij.util.ui.table.IconTableCellRenderer
6367import com.jetbrains.rd.util.lifetime.LifetimeDefinition
6468import kotlinx.coroutines.CoroutineScope
@@ -76,6 +80,7 @@ import java.awt.event.MouseListener
7680import java.awt.event.MouseMotionListener
7781import java.awt.font.TextAttribute
7882import java.awt.font.TextAttribute.UNDERLINE_ON
83+ import java.net.ConnectException
7984import java.net.SocketTimeoutException
8085import java.net.URL
8186import java.nio.file.Path
@@ -126,6 +131,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
126131 minWidth = JBUI .scale(52 )
127132 }
128133 rowHeight = 48
134+ setEmptyState(" Disconnected" )
129135 setSelectionMode(ListSelectionModel .SINGLE_SELECTION )
130136 selectionModel.addListSelectionListener {
131137 setNextButtonEnabled(selectedObject != null && selectedObject?.agentStatus == RUNNING && selectedObject?.agentOS == OS .LINUX )
@@ -345,7 +351,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
345351
346352 override fun onInit (wizardModel : CoderWorkspacesWizardModel ) {
347353 listTableModelOfWorkspaces.items = emptyList()
348- if (localWizardModel.coderURL.isNotBlank() && localWizardModel.token.isNotBlank() ) {
354+ if (localWizardModel.coderURL.isNotBlank() && localWizardModel.token != null ) {
349355 triggerWorkspacePolling(true )
350356 } else {
351357 val (url, token) = readStorageOrConfig()
@@ -354,15 +360,10 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
354360 tfUrl?.text = url
355361 }
356362 if (! token.isNullOrBlank()) {
357- localWizardModel.token = token
363+ localWizardModel.token = Pair ( token, TokenSource . CONFIG )
358364 }
359365 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 ))
366367 }
367368 }
368369 updateWorkspaceActions()
@@ -415,20 +416,26 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
415416 /* *
416417 * Ask for a new token (regardless of whether we already have a token),
417418 * 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.
418422 */
419- private fun askTokenAndConnect (openBrowser : Boolean = true) {
423+ private fun askTokenAndConnect (isRetry : Boolean = false) {
424+ val oldURL = localWizardModel.coderURL.toURL()
420425 component.apply () // Force bindings to be filled.
426+ val newURL = localWizardModel.coderURL.toURL()
421427 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,
425433 localWizardModel.useExistingToken,
426- )
427- if (pastedToken.isNullOrBlank()) {
428- return // User aborted.
429- }
434+ ) ? : return // User aborted.
430435 localWizardModel.token = pastedToken
431- connect()
436+ connect(newURL, pastedToken) {
437+ askTokenAndConnect(true )
438+ }
432439 }
433440
434441 /* *
@@ -439,80 +446,112 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
439446 * Existing workspaces will be immediately cleared before attempting to
440447 * connect to the new deployment.
441448 *
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.
444450 */
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 {
446456 // Clear out old deployment details.
447457 poller?.cancel()
458+ tableOfWorkspaces.setEmptyState(" Connecting to $deploymentURL ..." )
448459 listTableModelOfWorkspaces.items = emptyList()
449460
450- val deploymentURL = localWizardModel.coderURL.toURL()
451- val token = localWizardModel.token
452-
453461 // Authenticate and load in a background process with progress.
454462 // TODO: Make this cancelable.
455- LifetimeDefinition ().launchUnderBackgroundProgress(
463+ return LifetimeDefinition ().launchUnderBackgroundProgress(
456464 CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.cli.downloader.dialog.title" ),
457465 canBeCancelled = false ,
458466 isIndeterminate = true
459467 ) {
468+ val cliManager = CoderCLIManager (
469+ deploymentURL,
470+ if (settings.binaryDestination.isNotBlank()) Path .of(settings.binaryDestination)
471+ else CoderCLIManager .getDataDir(),
472+ settings.binarySource,
473+ )
460474 try {
461475 this .indicator.text = " Authenticating client..."
462- authenticate(deploymentURL, token)
476+ authenticate(deploymentURL, token.first )
463477 // Remember these in order to default to them for future attempts.
464478 appPropertiesService.setValue(CODER_URL_KEY , deploymentURL.toString())
465- appPropertiesService.setValue(SESSION_TOKEN , token)
479+ appPropertiesService.setValue(SESSION_TOKEN , token.first )
466480
467481 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- )
474482 cliManager.downloadCLI()
475483
476484 this .indicator.text = " Authenticating Coder CLI..."
477- cliManager.login(token)
485+ cliManager.login(token.first )
478486
479487 this .indicator.text = " Retrieving workspaces..."
480488 loadWorkspaces()
481489
482490 updateWorkspaceActions()
483491 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 " )
493494 } 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)
495527 }
496528 }
497529 }
498530
499531 /* *
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.
506539 */
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 )
509547 val getTokenUrl = url.withPath(" /login?redirect=%2Fcli-auth" )
510- if (openBrowser && ! useExisting) {
548+ if (! isRetry && ! useExisting) {
511549 BrowserUtil .browse(getTokenUrl)
512- } else if (useExisting) {
550+ } else if (! isRetry && useExisting) {
513551 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
516555 existingToken = t
517556 }
518557 }
@@ -525,11 +564,32 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
525564 CoderGatewayBundle .message(" gateway.connector.view.login.token.label" ),
526565 getTokenUrl.toString()
527566 )
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 )
533593 }
534594 AppIcon .getInstance().requestAttention(null , true )
535595 if (! dialog(
@@ -542,7 +602,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
542602 }
543603 tokenFromUser = sessionTokenTextField.text
544604 }, 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)
546612 }
547613
548614 private fun triggerWorkspacePolling (fetchNow : Boolean ) {
0 commit comments