Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ class CoderRemoteProvider(
}
throw ex
}
coderHeaderPage.reportFilterError(null)
coderHeaderPage.resetError()
return workspaces.flatMap { ws ->
// Agents are not included in workspaces that are off
// so fetch them separately.
Expand Down
42 changes: 22 additions & 20 deletions src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import com.coder.toolbox.util.toURL
import com.coder.toolbox.util.validateStrictWebUrl
import com.coder.toolbox.views.state.CoderOAuthSessionContext
import com.coder.toolbox.views.state.WizardModel
import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.ui.components.CheckboxField
import com.jetbrains.toolbox.api.ui.components.FieldModifier
import com.jetbrains.toolbox.api.ui.components.LabelField
import com.jetbrains.toolbox.api.ui.components.LabelStyleType
import com.jetbrains.toolbox.api.ui.components.RowGroup
import com.jetbrains.toolbox.api.ui.components.TextField
import com.jetbrains.toolbox.api.ui.components.TextType
import com.jetbrains.toolbox.api.ui.components.ValidationErrorField
import kotlinx.coroutines.flow.update
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.net.MalformedURLException
Expand All @@ -31,7 +32,7 @@ private const val OAUTH2_SCOPE: String =

/**
* A page with a field for providing the Coder deployment URL.
*\
*
* Populates with the provided URL, at which point the user can accept or
* enter their own.
*/
Expand All @@ -48,28 +49,20 @@ class DeploymentUrlStep(
context.i18n.ptrl("Verify binary signature using releases.coder.com when CLI signatures are not available from the deployment")
)

private val errorField = ValidationErrorField(context.i18n.pnotr(""))

override val panel: RowGroup
get() {
if (!context.settingsStore.disableSignatureVerification) {
return RowGroup(
RowGroup.RowField(urlField),
RowGroup.RowField(emptyLine),
RowGroup.RowField(signatureFallbackStrategyField),
RowGroup.RowField(errorField)
RowGroup.RowField(signatureFallbackStrategyField)
)
}
return RowGroup(
RowGroup.RowField(urlField),
RowGroup.RowField(errorField)
)
return RowGroup(RowGroup.RowField(urlField))
}

override fun onVisible() {
errorField.textState.update {
context.i18n.pnotr("")
}
resetError()
urlField.contentState.update {
context.deploymentUrl.toString()
}
Expand All @@ -83,14 +76,14 @@ class DeploymentUrlStep(
context.settingsStore.updateSignatureFallbackStrategy(signatureFallbackStrategyField.checkedState.value)
val rawUrl = urlField.contentState.value
if (rawUrl.isBlank()) {
errorField.textState.update { context.i18n.ptrl("URL is required") }
reportError(context.i18n.ptrl("URL is required"))
return false
}

try {
model.url = validateRawUrl(rawUrl)
} catch (e: MalformedURLException) {
context.logAndShowError("Error encountered while setting up Coder", "URL is invalid", e)
reportError(context.i18n.pnotr("URL is invalid: ${e.message}"))
return false
}

Expand All @@ -104,11 +97,7 @@ class DeploymentUrlStep(
model.oauthSession = handleOAuth2(rawUrl)
return false
} catch (e: Exception) {
context.logAndShowError(
"Error encountered while setting up Coder",
"Failed to authenticate with OAuth2: ${e.message}",
e
)
reportError(context.i18n.pnotr("Failed to authenticate with OAuth2: ${e.message}"))
return false
}
}
Expand Down Expand Up @@ -190,4 +179,17 @@ class DeploymentUrlStep(
override fun onBack() {
// it's the first step. Can't go anywhere back from here
}

private fun reportError(message: LocalizableString?) {
if (message != null) {
context.logger.info(message.toString())
urlField.modifiers.value = listOf(FieldModifier.LocalizableError(message))
} else {
resetError()
}
}

private fun resetError() {
urlField.modifiers.value = emptyList()
}
}
22 changes: 13 additions & 9 deletions src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import com.coder.toolbox.util.presetNameForQuery
import com.coder.toolbox.util.withFilterTerm
import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.ui.components.ComboBoxField
import com.jetbrains.toolbox.api.ui.components.FieldModifier
import com.jetbrains.toolbox.api.ui.components.RowGroup
import com.jetbrains.toolbox.api.ui.components.TextField
import com.jetbrains.toolbox.api.ui.components.TextType
import com.jetbrains.toolbox.api.ui.components.UiField
import com.jetbrains.toolbox.api.ui.components.ValidationErrorField
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
Expand All @@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds

Expand Down Expand Up @@ -57,9 +56,6 @@ class NewEnvironmentPage(
placeholder = context.i18n.pnotr("")
)

// Shows the server's validation message under the search bar; blank when there is no error.
private val errorField = ValidationErrorField(context.i18n.pnotr(""))

// Dropdown selections. An empty string means "no filter for this key" (the leading "All ..." option).
private val filtersSelection = MutableStateFlow(DEFAULT_WORKSPACE_FILTER_QUERY.presetNameForQuery())
private val templateSelection = MutableStateFlow("")
Expand Down Expand Up @@ -113,7 +109,7 @@ class NewEnvironmentPage(
val workspaceSearchQuery: StateFlow<String?> = mutableWorkspaceSearchQuery

override val fields: StateFlow<List<UiField>> =
MutableStateFlow(listOf(workspaceSearchField, filterControlsGroup, errorField))
MutableStateFlow(listOf(workspaceSearchField, filterControlsGroup))

override fun beforeShow() {
syncJob?.cancel()
Expand Down Expand Up @@ -174,7 +170,7 @@ class NewEnvironmentPage(
fun resetFilter() {
workspaceSearchField.contentState.value = DEFAULT_WORKSPACE_FILTER_QUERY
mutableWorkspaceSearchQuery.value = DEFAULT_WORKSPACE_FILTER_QUERY
reportFilterError(null)
resetError()
}

/**
Expand All @@ -185,15 +181,23 @@ class NewEnvironmentPage(
fun setFilter(query: String) {
workspaceSearchField.contentState.value = query
mutableWorkspaceSearchQuery.value = query
reportFilterError(null)
resetError()
}

/**
* Shows the server's validation [message] under the search bar, or clears it when [message] is
* null. Called by the provider when a workspace query is rejected or succeeds.
*/
fun reportFilterError(message: String?) {
errorField.textState.update { context.i18n.pnotr(message ?: "") }
if (message != null) {
workspaceSearchField.modifiers.value = listOf(FieldModifier.LocalizableError(context.i18n.pnotr(message)))
} else {
resetError()
}
}

fun resetError() {
workspaceSearchField.modifiers.value = emptyList()
}

private fun setQuery(query: String) {
Expand Down
28 changes: 18 additions & 10 deletions src/main/kotlin/com/coder/toolbox/views/TokenStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package com.coder.toolbox.views
import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.util.withPath
import com.coder.toolbox.views.state.WizardModel
import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.ui.components.FieldModifier
import com.jetbrains.toolbox.api.ui.components.LinkField
import com.jetbrains.toolbox.api.ui.components.RowGroup
import com.jetbrains.toolbox.api.ui.components.TextField
import com.jetbrains.toolbox.api.ui.components.TextType
import com.jetbrains.toolbox.api.ui.components.ValidationErrorField
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update

Expand All @@ -23,18 +24,14 @@ class TokenStep(
) : WizardStep {
private val tokenField = TextField(context.i18n.ptrl("Token"), "", TextType.Password)
private val linkField = LinkField(context.i18n.ptrl("Get a token"), "")
private val errorField = ValidationErrorField(context.i18n.pnotr(""))

override val panel: RowGroup = RowGroup(
RowGroup.RowField(tokenField),
RowGroup.RowField(linkField),
RowGroup.RowField(errorField)
)

override fun onVisible() {
errorField.textState.update {
context.i18n.pnotr("")
}
resetError()
model.url?.let { url ->
tokenField.contentState.update {
context.secrets.apiTokenFor(url) ?: ""
Expand All @@ -43,9 +40,7 @@ class TokenStep(
url.withPath("/login?redirect=%2Fcli-auth").toString()
}
} ?: run {
errorField.textState.update {
context.i18n.pnotr("URL not configure in the previous step. Please go back and provide a proper URL.")
}
reportError(context.i18n.pnotr("URL not configured in the previous step. Please go back and provide a proper URL."))
(linkField.urlState as MutableStateFlow).update {
""
}
Expand All @@ -56,7 +51,7 @@ class TokenStep(
override suspend fun onNext(): Boolean {
val token = tokenField.contentState.value
if (token.isBlank()) {
errorField.textState.update { context.i18n.ptrl("Token is required") }
reportError(context.i18n.ptrl("Token is required"))
return false
}

Expand All @@ -68,4 +63,17 @@ class TokenStep(
override fun onBack() {
model.goToPrevious()
}

private fun reportError(message: LocalizableString?) {
if (message != null) {
context.logger.info(message.toString())
tokenField.modifiers.value = listOf(FieldModifier.LocalizableError(message))
} else {
resetError()
}
}

private fun resetError() {
tokenField.modifiers.value = emptyList()
}
}
Loading