Skip to content

Commit a11ae83

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 761a67b commit a11ae83

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...")
@@ -464,16 +472,16 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
464472
)
465473
try {
466474
this.indicator.text = "Authenticating client..."
467-
authenticate(deploymentURL, token)
475+
authenticate(deploymentURL, token.first)
468476
// Remember these in order to default to them for future attempts.
469477
appPropertiesService.setValue(CODER_URL_KEY, deploymentURL.toString())
470-
appPropertiesService.setValue(SESSION_TOKEN, token)
478+
appPropertiesService.setValue(SESSION_TOKEN, token.first)
471479

472480
this.indicator.text = "Downloading Coder CLI..."
473481
cliManager.downloadCLI()
474482

475483
this.indicator.text = "Authenticating Coder CLI..."
476-
cliManager.login(token)
484+
cliManager.login(token.first)
477485

478486
this.indicator.text = "Retrieving workspaces..."
479487
loadWorkspaces()
@@ -520,22 +528,29 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
520528
}
521529

522530
/**
523-
* Open a dialog for providing the token. Show the existing token so the
524-
* user can validate it if a previous connection failed. Open a browser to
525-
* the auth page if openBrowser is true and useExisting is false. If
526-
* useExisting is true then populate the dialog with the token on disk if
527-
* there is one and it matches the url (this will overwrite the provided
528-
* token). Return the token submitted by the user.
531+
* Open a dialog for providing the token. Show any existing token so the
532+
* user can validate it if a previous connection failed. If we are not
533+
* retrying and the user has not checked the existing token box then open a
534+
* browser to the auth page. If the user has checked the existing token box
535+
* then populate the dialog with the token on disk (this will overwrite any
536+
* other existing token) unless this is a retry to avoid clobbering the
537+
* token that just failed. Return the token submitted by the user.
529538
*/
530-
private fun askToken(url: URL, token: String, openBrowser: Boolean, useExisting: Boolean): String? {
531-
var existingToken = token
539+
private fun askToken(
540+
url: URL,
541+
token: Pair<String, TokenSource>?,
542+
isRetry: Boolean,
543+
useExisting: Boolean,
544+
): Pair<String, TokenSource>? {
545+
var (existingToken, tokenSource) = token ?: Pair("", TokenSource.USER)
532546
val getTokenUrl = url.withPath("/login?redirect=%2Fcli-auth")
533-
if (openBrowser && !useExisting) {
547+
if (!isRetry && !useExisting) {
534548
BrowserUtil.browse(getTokenUrl)
535-
} else if (useExisting) {
549+
} else if (!isRetry && useExisting) {
536550
val (u, t) = CoderCLIManager.readConfig()
537-
if (url == u?.toURL() && !t.isNullOrBlank()) {
538-
logger.info("Injecting valid token from CLI config")
551+
if (url == u?.toURL() && !t.isNullOrBlank() && t != existingToken) {
552+
logger.info("Injecting token from CLI config")
553+
tokenSource = TokenSource.CONFIG
539554
existingToken = t
540555
}
541556
}
@@ -548,11 +563,32 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
548563
CoderGatewayBundle.message("gateway.connector.view.login.token.label"),
549564
getTokenUrl.toString()
550565
)
551-
sessionTokenTextField = textField().applyToComponent {
552-
text = existingToken
553-
minimumSize = Dimension(520, -1)
554-
}.component
555-
}
566+
sessionTokenTextField = textField()
567+
.applyToComponent {
568+
text = existingToken
569+
minimumSize = Dimension(520, -1)
570+
}.component
571+
}.layout(RowLayout.PARENT_GRID)
572+
row {
573+
cell() // To align with the text box.
574+
cell(
575+
ComponentPanelBuilder.createCommentComponent(
576+
CoderGatewayBundle.message(
577+
if (isRetry) "gateway.connector.view.workspaces.token.rejected"
578+
else if (tokenSource == TokenSource.CONFIG) "gateway.connector.view.workspaces.token.injected"
579+
else if (existingToken.isNotBlank()) "gateway.connector.view.workspaces.token.comment"
580+
else "gateway.connector.view.workspaces.token.none"
581+
),
582+
false,
583+
-1,
584+
true
585+
).applyIf(isRetry) {
586+
apply {
587+
foreground = UIUtil.getErrorForeground()
588+
}
589+
}
590+
)
591+
}.layout(RowLayout.PARENT_GRID)
556592
}
557593
AppIcon.getInstance().requestAttention(null, true)
558594
if (!dialog(
@@ -565,7 +601,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
565601
}
566602
tokenFromUser = sessionTokenTextField.text
567603
}, ModalityState.any())
568-
return tokenFromUser
604+
if (tokenFromUser.isNullOrBlank()) {
605+
return null
606+
}
607+
if (tokenFromUser != existingToken) {
608+
tokenSource = TokenSource.USER
609+
}
610+
return Pair(tokenFromUser!!, tokenSource)
569611
}
570612

571613
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)