Skip to content

Commit e3bf092

Browse files
committed
Surface token source and error
Now it will say whether the token was from the config or was the last known token and if it fails there will be an error message. You could always check the error in the bottom right but this way it is more obvious why the token dialog has reappeared. Also if the URL has changed there is no point trying to use the token we had stored for the previous URL.
1 parent 670954e commit e3bf092

File tree

3 files changed

+88
-36
lines changed

3 files changed

+88
-36
lines changed
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package com.coder.gateway.models
22

3+
enum class TokenSource {
4+
CONFIG, // Pulled from the Coder CLI config.
5+
USER, // Input by the user.
6+
LAST_USED, // Last used token, either from storage or current run.
7+
}
8+
39
data class CoderWorkspacesWizardModel(
410
var coderURL: String = "https://coder.example.com",
5-
var token: String = "",
11+
var token: Pair<String, TokenSource>? = null,
612
var selectedWorkspace: WorkspaceAgentModel? = null,
713
var useExistingToken: Boolean = false,
814
)

src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.coder.gateway.views.steps
33
import com.coder.gateway.CoderGatewayBundle
44
import com.coder.gateway.icons.CoderIcons
55
import com.coder.gateway.models.CoderWorkspacesWizardModel
6+
import com.coder.gateway.models.TokenSource
67
import com.coder.gateway.models.WorkspaceAgentModel
78
import com.coder.gateway.models.WorkspaceAgentStatus
89
import com.coder.gateway.models.WorkspaceAgentStatus.FAILED
@@ -56,10 +57,12 @@ import com.intellij.ui.dsl.builder.bindSelected
5657
import com.intellij.ui.dsl.builder.bindText
5758
import com.intellij.ui.dsl.builder.panel
5859
import com.intellij.ui.table.TableView
60+
import com.intellij.util.applyIf
5961
import com.intellij.util.ui.ColumnInfo
6062
import com.intellij.util.ui.JBFont
6163
import com.intellij.util.ui.JBUI
6264
import com.intellij.util.ui.ListTableModel
65+
import com.intellij.util.ui.UIUtil
6366
import com.intellij.util.ui.table.IconTableCellRenderer
6467
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
6568
import kotlinx.coroutines.CoroutineScope
@@ -348,7 +351,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
348351

349352
override fun onInit(wizardModel: CoderWorkspacesWizardModel) {
350353
listTableModelOfWorkspaces.items = emptyList()
351-
if (localWizardModel.coderURL.isNotBlank() && localWizardModel.token.isNotBlank()) {
354+
if (localWizardModel.coderURL.isNotBlank() && localWizardModel.token != null) {
352355
triggerWorkspacePolling(true)
353356
} else {
354357
val (url, token) = readStorageOrConfig()
@@ -357,10 +360,10 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
357360
tfUrl?.text = url
358361
}
359362
if (!token.isNullOrBlank()) {
360-
localWizardModel.token = token
363+
localWizardModel.token = Pair(token, TokenSource.CONFIG)
361364
}
362365
if (!url.isNullOrBlank() && !token.isNullOrBlank()) {
363-
connect(url.toURL(), token)
366+
connect(url.toURL(), Pair(token, TokenSource.CONFIG))
364367
}
365368
}
366369
updateWorkspaceActions()
@@ -417,20 +420,21 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
417420
* If the token is invalid abort and start over from askTokenAndConnect()
418421
* unless retry is false.
419422
*/
420-
private fun askTokenAndConnect(openBrowser: Boolean = true) {
423+
private fun askTokenAndConnect(isRetry: Boolean = false) {
424+
val oldURL = localWizardModel.coderURL.toURL()
421425
component.apply() // Force bindings to be filled.
426+
val newURL = localWizardModel.coderURL.toURL()
422427
val pastedToken = askToken(
423-
localWizardModel.coderURL.toURL(),
424-
localWizardModel.token,
425-
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,
426433
localWizardModel.useExistingToken,
427-
)
428-
if (pastedToken.isNullOrBlank()) {
429-
return // User aborted.
430-
}
434+
) ?: return // User aborted.
431435
localWizardModel.token = pastedToken
432-
connect(localWizardModel.coderURL.toURL(), localWizardModel.token) {
433-
askTokenAndConnect(false)
436+
connect(newURL, pastedToken) {
437+
askTokenAndConnect(true)
434438
}
435439
}
436440

@@ -444,7 +448,11 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
444448
*
445449
* If the token is invalid invoke onAuthFailure.
446450
*/
447-
private fun connect(deploymentURL: URL, token: String, onAuthFailure: (() -> Unit)? = null): Job {
451+
private fun connect(
452+
deploymentURL: URL,
453+
token: Pair<String, TokenSource>,
454+
onAuthFailure: (() -> Unit)? = null,
455+
): Job {
448456
// Clear out old deployment details.
449457
poller?.cancel()
450458
tableOfWorkspaces.setEmptyState("Connecting to $deploymentURL...")
@@ -465,16 +473,16 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
465473
)
466474
try {
467475
this.indicator.text = "Authenticating client..."
468-
authenticate(deploymentURL, token)
476+
authenticate(deploymentURL, token.first)
469477
// Remember these in order to default to them for future attempts.
470478
appPropertiesService.setValue(CODER_URL_KEY, deploymentURL.toString())
471-
appPropertiesService.setValue(SESSION_TOKEN, token)
479+
appPropertiesService.setValue(SESSION_TOKEN, token.first)
472480

473481
this.indicator.text = "Downloading Coder CLI..."
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()
@@ -521,22 +529,29 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
521529
}
522530

523531
/**
524-
* Open a dialog for providing the token. Show the existing token so the
525-
* user can validate it if a previous connection failed. Open a browser to
526-
* the auth page if openBrowser is true and useExisting is false. If
527-
* useExisting is true then populate the dialog with the token on disk if
528-
* there is one and it matches the url (this will overwrite the provided
529-
* 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.
530539
*/
531-
private fun askToken(url: URL, token: String, openBrowser: Boolean, useExisting: Boolean): String? {
532-
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)
533547
val getTokenUrl = url.withPath("/login?redirect=%2Fcli-auth")
534-
if (openBrowser && !useExisting) {
548+
if (!isRetry && !useExisting) {
535549
BrowserUtil.browse(getTokenUrl)
536-
} else if (useExisting) {
550+
} else if (!isRetry && useExisting) {
537551
val (u, t) = CoderCLIManager.readConfig()
538-
if (url == u?.toURL() && !t.isNullOrBlank()) {
539-
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
540555
existingToken = t
541556
}
542557
}
@@ -549,11 +564,32 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
549564
CoderGatewayBundle.message("gateway.connector.view.login.token.label"),
550565
getTokenUrl.toString()
551566
)
552-
sessionTokenTextField = textField().applyToComponent {
553-
text = existingToken
554-
minimumSize = Dimension(520, -1)
555-
}.component
556-
}
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)
557593
}
558594
AppIcon.getInstance().requestAttention(null, true)
559595
if (!dialog(
@@ -566,7 +602,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
566602
}
567603
tokenFromUser = sessionTokenTextField.text
568604
}, ModalityState.any())
569-
return tokenFromUser
605+
if (tokenFromUser.isNullOrBlank()) {
606+
return null
607+
}
608+
if (tokenFromUser != existingToken) {
609+
tokenSource = TokenSource.USER
610+
}
611+
return Pair(tokenFromUser!!, tokenSource)
570612
}
571613

572614
private fun triggerWorkspacePolling(fetchNow: Boolean) {

src/main/resources/messages/CoderGatewayBundle.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ gateway.connector.view.workspaces.connect.unauthorized=Token was rejected by {0}
2424
gateway.connector.view.workspaces.connect.timeout=Unable to connect to {0}; is it up?
2525
gateway.connector.view.workspaces.connect.download-failed=Failed to download Coder CLI from {0}: {1}
2626
gateway.connector.view.workspaces.connect.failed=Failed to configure connection to {0}: {1}
27+
gateway.connector.view.workspaces.token.comment=The last used token is shown above.
28+
gateway.connector.view.workspaces.token.rejected=This token was rejected.
29+
gateway.connector.view.workspaces.token.injected=This token was pulled from your CLI config.
30+
gateway.connector.view.workspaces.token.none=No existing token found.
2731
gateway.connector.view.coder.remoteproject.loading.text=Retrieving products...
2832
gateway.connector.view.coder.remoteproject.ide.error.text=Could not retrieve any IDE because an error was encountered. Please check the logs for more details!
2933
gateway.connector.view.coder.remoteproject.ssh.error.text=Can't connect to the workspace. Please make sure Coder Agent is running!

0 commit comments

Comments
 (0)