@@ -44,7 +44,15 @@ import com.intellij.ui.RelativeFont
4444import com.intellij.ui.ToolbarDecorator
4545import com.intellij.ui.components.JBTextField
4646import 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
4856import com.intellij.ui.table.TableView
4957import com.intellij.util.ui.ColumnInfo
5058import com.intellij.util.ui.JBFont
@@ -68,6 +76,7 @@ import java.awt.event.MouseMotionListener
6876import java.awt.font.TextAttribute
6977import java.awt.font.TextAttribute.UNDERLINE_ON
7078import java.net.SocketTimeoutException
79+ import java.net.URL
7180import javax.swing.Icon
7281import javax.swing.JCheckBox
7382import javax.swing.JTable
@@ -214,15 +223,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
214223 tfUrl = textField().resizableColumn().align(AlignX .FILL ).gap(RightGap .SMALL )
215224 .bindText(localWizardModel::coderURL).applyToComponent {
216225 addActionListener {
217- poller?.cancel()
218- listTableModelOfWorkspaces.items = emptyList()
219- askTokenAndOpenSession(true )
226+ // Reconnect when the enter key is pressed.
227+ askTokenAndConnect()
220228 }
221229 }.component
222230 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()
226233 }.applyToComponent {
227234 background = WelcomeScreenUIManager .getMainAssociatedComponentBackground()
228235 }
@@ -347,7 +354,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
347354 localWizardModel.token = token
348355 }
349356 if (! url.isNullOrBlank() && ! token.isNullOrBlank()) {
350- loginAndLoadWorkspaces(token, true )
357+ connect( )
351358 }
352359 }
353360 updateWorkspaceActions()
@@ -397,105 +404,126 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
397404 ActivityTracker .getInstance().inc()
398405 }
399406
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+ )
405419 if (pastedToken.isNullOrBlank()) {
406- return
420+ return // User aborted.
407421 }
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 )
411426 }
412427
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()
418442
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+ ) {
419450 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)
429456
430- val cliManager = CoderCLIManager (localWizardModel.coderURL.toURL())
431- localWizardModel.token = token
457+ this .indicator.text = " Retrieving workspaces... "
458+ loadWorkspaces()
432459
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()
438463
439- loadWorkspaces()
464+ this .indicator.text = " Authenticating Coder CLI..."
465+ cliManager.login(localWizardModel.token)
440466
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)
448477 } catch (e: ResponseException ) {
449- logger.error(" Download failed with response code ${e.code} " , e)
450- return @launchUnderBackgroundProgress
451- } catch (e: Exception ) {
452478 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)
464481 }
465- cliManager.configSsh()
466-
467- this .indicator.fraction = 1.0
468- updateWorkspaceActions()
469- triggerWorkspacePolling(false )
470482 }
471483 }
472484
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) {
476497 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()) {
480501 logger.info(" Injecting valid token from CLI config" )
481- localWizardModel.token = token
502+ existingToken = t
482503 }
483504 }
484505 var tokenFromUser: String? = null
485506 ApplicationManager .getApplication().invokeAndWait({
486507 lateinit var sessionTokenTextField: JBTextField
487-
488508 val panel = panel {
489509 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
492516 minimumSize = Dimension (320 , - 1 )
493517 }.component
494518 }
495519 }
496-
497520 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+ ) {
499527 return @invokeAndWait
500528 }
501529 tokenFromUser = sessionTokenTextField.text
@@ -518,13 +546,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
518546 }
519547
520548 /* *
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.
524552 */
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)
528556
529557 try {
530558 logger.info(" Checking compatibility with Coder version ${coderClient.buildVersion} ..." )
@@ -534,7 +562,12 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
534562 logger.warn(e)
535563 notificationBanner.apply {
536564 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+ )
538571 }
539572 } catch (e: IncompatibleVersionException ) {
540573 logger.warn(e)
@@ -545,12 +578,11 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
545578 }
546579
547580 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)
552581 }
553582
583+ /* *
584+ * Request workspaces then update the table.
585+ */
554586 private suspend fun loadWorkspaces () {
555587 val ws = withContext(Dispatchers .IO ) {
556588 val timeBeforeRequestingWorkspaces = System .currentTimeMillis()
0 commit comments