diff --git a/CHANGELOG.md b/CHANGELOG.md index 038ecb5..368f967 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- visual text progress during Coder CLI downloading + ### Changed - the plugin will now remember the SSH connection state for each workspace, and it will try to automatically diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index d72b130..101cf71 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -9,10 +9,10 @@ import com.coder.toolbox.util.CoderProtocolHandler import com.coder.toolbox.util.DialogUi import com.coder.toolbox.util.withPath import com.coder.toolbox.views.Action -import com.coder.toolbox.views.AuthWizardPage +import com.coder.toolbox.views.CoderCliSetupWizardPage import com.coder.toolbox.views.CoderSettingsPage import com.coder.toolbox.views.NewEnvironmentPage -import com.coder.toolbox.views.state.AuthWizardState +import com.coder.toolbox.views.state.CoderCliSetupWizardState import com.coder.toolbox.views.state.WizardStep import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType @@ -242,7 +242,7 @@ class CoderRemoteProvider( environments.value = LoadableState.Value(emptyList()) isInitialized.update { false } client = null - AuthWizardState.resetSteps() + CoderCliSetupWizardState.resetSteps() } override val svgIcon: SvgIcon = @@ -301,7 +301,7 @@ class CoderRemoteProvider( */ override suspend fun handleUri(uri: URI) { linkHandler.handle( - uri, shouldDoAutoLogin(), + uri, shouldDoAutoSetup(), { coderHeaderPage.isBusyCreatingNewEnvironment.update { true @@ -343,17 +343,17 @@ class CoderRemoteProvider( * list. */ override fun getOverrideUiPage(): UiPage? { - // Show sign in page if we have not configured the client yet. + // Show the setup page if we have not configured the client yet. if (client == null) { val errorBuffer = mutableListOf() - // When coming back to the application, authenticate immediately. - val autologin = shouldDoAutoLogin() + // When coming back to the application, initializeSession immediately. + val autoSetup = shouldDoAutoSetup() context.secrets.lastToken.let { lastToken -> context.secrets.lastDeploymentURL.let { lastDeploymentURL -> - if (autologin && lastDeploymentURL.isNotBlank() && (lastToken.isNotBlank() || !settings.requireTokenAuth)) { + if (autoSetup && lastDeploymentURL.isNotBlank() && (lastToken.isNotBlank() || !settings.requireTokenAuth)) { try { - AuthWizardState.goToStep(WizardStep.LOGIN) - return AuthWizardPage(context, settingsPage, visibilityState, true, ::onConnect) + CoderCliSetupWizardState.goToStep(WizardStep.CONNECT) + return CoderCliSetupWizardPage(context, settingsPage, visibilityState, true, ::onConnect) } catch (ex: Exception) { errorBuffer.add(ex) } @@ -363,18 +363,19 @@ class CoderRemoteProvider( firstRun = false // Login flow. - val authWizard = AuthWizardPage(context, settingsPage, visibilityState, onConnect = ::onConnect) + val setupWizardPage = + CoderCliSetupWizardPage(context, settingsPage, visibilityState, onConnect = ::onConnect) // We might have navigated here due to a polling error. errorBuffer.forEach { - authWizard.notify("Error encountered", it) + setupWizardPage.notify("Error encountered", it) } // and now reset the errors, otherwise we show it every time on the screen - return authWizard + return setupWizardPage } return null } - private fun shouldDoAutoLogin(): Boolean = firstRun && context.secrets.rememberMe == true + private fun shouldDoAutoSetup(): Boolean = firstRun && context.secrets.rememberMe == true private suspend fun onConnect(client: CoderRestClient, cli: CoderCLIManager) { // Store the URL and token for use next time. diff --git a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt index 2898179..e4ef501 100644 --- a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt +++ b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt @@ -32,7 +32,7 @@ import java.net.HttpURLConnection import java.net.URL import java.nio.file.Files import java.nio.file.Path -import java.nio.file.StandardCopyOption +import java.nio.file.StandardOpenOption import java.util.zip.GZIPInputStream import javax.net.ssl.HttpsURLConnection @@ -44,6 +44,8 @@ internal data class Version( @Json(name = "version") val version: String, ) +private const val DOWNLOADING_CODER_CLI = "Downloading Coder CLI..." + /** * Do as much as possible to get a valid, up-to-date CLI. * @@ -60,6 +62,7 @@ fun ensureCLI( context: CoderToolboxContext, deploymentURL: URL, buildVersion: String, + showTextProgress: (String) -> Unit ): CoderCLIManager { val settings = context.settingsStore.readOnly() val cli = CoderCLIManager(deploymentURL, context.logger, settings) @@ -76,9 +79,10 @@ fun ensureCLI( // If downloads are enabled download the new version. if (settings.enableDownloads) { - context.logger.info("Downloading Coder CLI...") + context.logger.info(DOWNLOADING_CODER_CLI) + showTextProgress(DOWNLOADING_CODER_CLI) try { - cli.download() + cli.download(buildVersion, showTextProgress) return cli } catch (e: java.nio.file.AccessDeniedException) { // Might be able to fall back to the data directory. @@ -98,8 +102,9 @@ fun ensureCLI( } if (settings.enableDownloads) { - context.logger.info("Downloading Coder CLI...") - dataCLI.download() + context.logger.info(DOWNLOADING_CODER_CLI) + showTextProgress(DOWNLOADING_CODER_CLI) + dataCLI.download(buildVersion, showTextProgress) return dataCLI } @@ -137,7 +142,7 @@ class CoderCLIManager( /** * Download the CLI from the deployment if necessary. */ - fun download(): Boolean { + fun download(buildVersion: String, showTextProgress: (String) -> Unit): Boolean { val eTag = getBinaryETag() val conn = remoteBinaryURL.openConnection() as HttpURLConnection if (!settings.headerCommand.isNullOrBlank()) { @@ -162,13 +167,27 @@ class CoderCLIManager( when (conn.responseCode) { HttpURLConnection.HTTP_OK -> { logger.info("Downloading binary to $localBinaryPath") + Files.deleteIfExists(localBinaryPath) Files.createDirectories(localBinaryPath.parent) - conn.inputStream.use { - Files.copy( - if (conn.contentEncoding == "gzip") GZIPInputStream(it) else it, - localBinaryPath, - StandardCopyOption.REPLACE_EXISTING, - ) + val outputStream = Files.newOutputStream( + localBinaryPath, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + ) + val sourceStream = if (conn.isGzip()) GZIPInputStream(conn.inputStream) else conn.inputStream + + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var bytesRead: Int + var totalRead = 0L + + sourceStream.use { source -> + outputStream.use { sink -> + while (source.read(buffer).also { bytesRead = it } != -1) { + sink.write(buffer, 0, bytesRead) + totalRead += bytesRead + showTextProgress("${settings.defaultCliBinaryNameByOsAndArch} $buildVersion - ${totalRead.toHumanReadableSize()} downloaded") + } + } } if (getOS() != OS.WINDOWS) { localBinaryPath.toFile().setExecutable(true) @@ -178,6 +197,7 @@ class CoderCLIManager( HttpURLConnection.HTTP_NOT_MODIFIED -> { logger.info("Using cached binary at $localBinaryPath") + showTextProgress("Using cached binary") return false } } @@ -190,6 +210,21 @@ class CoderCLIManager( throw ResponseException("Unexpected response from $remoteBinaryURL", conn.responseCode) } + private fun HttpURLConnection.isGzip(): Boolean = this.contentEncoding.equals("gzip", ignoreCase = true) + + fun Long.toHumanReadableSize(): String { + if (this < 1024) return "$this B" + + val kb = this / 1024.0 + if (kb < 1024) return String.format("%.1f KB", kb) + + val mb = kb / 1024.0 + if (mb < 1024) return String.format("%.1f MB", mb) + + val gb = mb / 1024.0 + return String.format("%.1f GB", gb) + } + /** * Return the entity tag for the binary on disk, if any. */ @@ -203,7 +238,7 @@ class CoderCLIManager( } /** - * Use the provided token to authenticate the CLI. + * Use the provided token to initializeSession the CLI. */ fun login(token: String): String { logger.info("Storing CLI credentials in $coderConfigPath") diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index 9f619bc..365e1ed 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -131,12 +131,11 @@ open class CoderRestClient( } /** - * Authenticate and load information about the current user and the build - * version. + * Load information about the current user and the build version. * * @throws [APIResponseException]. */ - suspend fun authenticate(): User { + suspend fun initializeSession(): User { me = me() buildVersion = buildInfo().version return me @@ -149,7 +148,12 @@ open class CoderRestClient( suspend fun me(): User { val userResponse = retroRestClient.me() if (!userResponse.isSuccessful) { - throw APIResponseException("authenticate", url, userResponse.code(), userResponse.parseErrorBody(moshi)) + throw APIResponseException( + "initializeSession", + url, + userResponse.code(), + userResponse.parseErrorBody(moshi) + ) } return userResponse.body()!! diff --git a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt index c605dec..7d24029 100644 --- a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt +++ b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt @@ -24,6 +24,7 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.toJavaDuration private const val CAN_T_HANDLE_URI_TITLE = "Can't handle URI" +private val noOpTextProgress: (String) -> Unit = { _ -> } @Suppress("UnstableApiUsage") open class CoderProtocolHandler( @@ -143,7 +144,7 @@ open class CoderProtocolHandler( if (settings.requireTokenAuth) token else null, PluginManager.pluginInfo.version ) - client.authenticate() + client.initializeSession() return client } @@ -304,7 +305,8 @@ open class CoderProtocolHandler( val cli = ensureCLI( context, deploymentURL.toURL(), - restClient.buildInfo().version + restClient.buildInfo().version, + noOpTextProgress ) // We only need to log in if we are using token-based auth. diff --git a/src/main/kotlin/com/coder/toolbox/views/AuthWizardPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt similarity index 80% rename from src/main/kotlin/com/coder/toolbox/views/AuthWizardPage.kt rename to src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt index affa96e..c6193da 100644 --- a/src/main/kotlin/com/coder/toolbox/views/AuthWizardPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt @@ -5,8 +5,8 @@ import com.coder.toolbox.cli.CoderCLIManager import com.coder.toolbox.sdk.CoderRestClient import com.coder.toolbox.sdk.ex.APIResponseException import com.coder.toolbox.util.toURL -import com.coder.toolbox.views.state.AuthContext -import com.coder.toolbox.views.state.AuthWizardState +import com.coder.toolbox.views.state.CoderCliSetupContext +import com.coder.toolbox.views.state.CoderCliSetupWizardState import com.coder.toolbox.views.state.WizardStep import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription @@ -16,26 +16,26 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.UUID -class AuthWizardPage( +class CoderCliSetupWizardPage( private val context: CoderToolboxContext, private val settingsPage: CoderSettingsPage, private val visibilityState: MutableStateFlow, - initialAutoLogin: Boolean = false, + initialAutoSetup: Boolean = false, onConnect: suspend ( client: CoderRestClient, cli: CoderCLIManager, ) -> Unit, -) : CoderPage(context.i18n.ptrl("Authenticate to Coder"), false) { - private val shouldAutoLogin = MutableStateFlow(initialAutoLogin) +) : CoderPage(context.i18n.ptrl("Setting up Coder"), false) { + private val shouldAutoSetup = MutableStateFlow(initialAutoSetup) private val settingsAction = Action(context.i18n.ptrl("Settings"), actionBlock = { context.ui.showUiPage(settingsPage) }) - private val signInStep = SignInStep(context, this::notify) + private val deploymentUrlStep = DeploymentUrlStep(context, this::notify) private val tokenStep = TokenStep(context) private val connectStep = ConnectStep( context, - shouldAutoLogin, + shouldAutoSetup, this::notify, this::displaySteps, onConnect @@ -50,9 +50,9 @@ class AuthWizardPage( private val errorBuffer = mutableListOf() init { - if (shouldAutoLogin.value) { - AuthContext.url = context.secrets.lastDeploymentURL.toURL() - AuthContext.token = context.secrets.lastToken + if (shouldAutoSetup.value) { + CoderCliSetupContext.url = context.secrets.lastDeploymentURL.toURL() + CoderCliSetupContext.token = context.secrets.lastToken } } @@ -67,22 +67,22 @@ class AuthWizardPage( } private fun displaySteps() { - when (AuthWizardState.currentStep()) { + when (CoderCliSetupWizardState.currentStep()) { WizardStep.URL_REQUEST -> { fields.update { - listOf(signInStep.panel) + listOf(deploymentUrlStep.panel) } actionButtons.update { listOf( - Action(context.i18n.ptrl("Sign In"), closesPage = false, actionBlock = { - if (signInStep.onNext()) { + Action(context.i18n.ptrl("Next"), closesPage = false, actionBlock = { + if (deploymentUrlStep.onNext()) { displaySteps() } }), settingsAction ) } - signInStep.onVisible() + deploymentUrlStep.onVisible() } WizardStep.TOKEN_REQUEST -> { @@ -106,7 +106,7 @@ class AuthWizardPage( tokenStep.onVisible() } - WizardStep.LOGIN -> { + WizardStep.CONNECT -> { fields.update { listOf(connectStep.panel) } @@ -115,7 +115,7 @@ class AuthWizardPage( settingsAction, Action(context.i18n.ptrl("Back"), closesPage = false, actionBlock = { connectStep.onBack() - shouldAutoLogin.update { + shouldAutoSetup.update { false } displaySteps() @@ -150,7 +150,7 @@ class AuthWizardPage( context.cs.launch { context.ui.showSnackbar( UUID.randomUUID().toString(), - context.i18n.ptrl("Error encountered during authentication"), + context.i18n.ptrl("Error encountered while setting up Coder"), context.i18n.pnotr(textError ?: ""), context.i18n.ptrl("Dismiss") ) diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt index 58e154e..9964d0c 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt @@ -5,9 +5,8 @@ import com.coder.toolbox.cli.CoderCLIManager import com.coder.toolbox.cli.ensureCLI import com.coder.toolbox.plugin.PluginManager import com.coder.toolbox.sdk.CoderRestClient -import com.coder.toolbox.views.state.AuthContext -import com.coder.toolbox.views.state.AuthWizardState -import com.jetbrains.toolbox.api.localization.LocalizableString +import com.coder.toolbox.views.state.CoderCliSetupContext +import com.coder.toolbox.views.state.CoderCliSetupWizardState import com.jetbrains.toolbox.api.ui.components.LabelField import com.jetbrains.toolbox.api.ui.components.RowGroup import com.jetbrains.toolbox.api.ui.components.ValidationErrorField @@ -43,21 +42,19 @@ class ConnectStep( RowGroup.RowField(errorField) ) - override val nextButtonTitle: LocalizableString? = null - override fun onVisible() { errorField.textState.update { context.i18n.pnotr("") } - if (AuthContext.isNotReadyForAuth()) { + if (CoderCliSetupContext.isNotReadyForAuth()) { errorField.textState.update { context.i18n.pnotr("URL and token were not properly configured. Please go back and provide a proper URL and token!") } return } - statusField.textState.update { context.i18n.pnotr("Connecting to ${AuthContext.url!!.host}...") } + statusField.textState.update { context.i18n.pnotr("Connecting to ${CoderCliSetupContext.url!!.host}...") } connect() } @@ -65,51 +62,55 @@ class ConnectStep( * Try connecting to Coder with the provided URL and token. */ private fun connect() { - if (!AuthContext.hasUrl()) { + if (!CoderCliSetupContext.hasUrl()) { errorField.textState.update { context.i18n.ptrl("URL is required") } return } - if (!AuthContext.hasToken()) { + if (!CoderCliSetupContext.hasToken()) { errorField.textState.update { context.i18n.ptrl("Token is required") } return } signInJob?.cancel() signInJob = context.cs.launch { try { - statusField.textState.update { (context.i18n.ptrl("Authenticating to ${AuthContext.url!!.host}...")) } val client = CoderRestClient( context, - AuthContext.url!!, - AuthContext.token!!, + CoderCliSetupContext.url!!, + CoderCliSetupContext.token!!, PluginManager.pluginInfo.version, ) // allows interleaving with the back/cancel action yield() - client.authenticate() - statusField.textState.update { (context.i18n.ptrl("Checking Coder binary...")) } - val cli = ensureCLI(context, client.url, client.buildVersion) + client.initializeSession() + statusField.textState.update { (context.i18n.ptrl("Checking Coder CLI...")) } + val cli = ensureCLI( + context, client.url, + client.buildVersion + ) { progress -> + statusField.textState.update { (context.i18n.pnotr(progress)) } + } // We only need to log in if we are using token-based auth. if (client.token != null) { - statusField.textState.update { (context.i18n.ptrl("Configuring CLI...")) } + statusField.textState.update { (context.i18n.ptrl("Configuring Coder CLI...")) } // allows interleaving with the back/cancel action yield() cli.login(client.token) } - statusField.textState.update { (context.i18n.ptrl("Successfully configured ${AuthContext.url!!.host}...")) } + statusField.textState.update { (context.i18n.ptrl("Successfully configured ${CoderCliSetupContext.url!!.host}...")) } // allows interleaving with the back/cancel action yield() - AuthContext.reset() - AuthWizardState.resetSteps() + CoderCliSetupContext.reset() + CoderCliSetupWizardState.resetSteps() onConnect(client, cli) } catch (ex: CancellationException) { if (ex.message != USER_HIT_THE_BACK_BUTTON) { - notify("Connection to ${AuthContext.url!!.host} was configured", ex) + notify("Connection to ${CoderCliSetupContext.url!!.host} was configured", ex) onBack() refreshWizard() } } catch (ex: Exception) { - notify("Failed to configure ${AuthContext.url!!.host}", ex) + notify("Failed to configure ${CoderCliSetupContext.url!!.host}", ex) onBack() refreshWizard() } @@ -125,11 +126,11 @@ class ConnectStep( signInJob?.cancel(CancellationException(USER_HIT_THE_BACK_BUTTON)) } finally { if (shouldAutoLogin.value) { - AuthContext.reset() - AuthWizardState.resetSteps() + CoderCliSetupContext.reset() + CoderCliSetupWizardState.resetSteps() context.secrets.rememberMe = false } else { - AuthWizardState.goToPreviousStep() + CoderCliSetupWizardState.goToPreviousStep() } } } diff --git a/src/main/kotlin/com/coder/toolbox/views/SignInStep.kt b/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt similarity index 85% rename from src/main/kotlin/com/coder/toolbox/views/SignInStep.kt rename to src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt index 34cdf2d..aa87b57 100644 --- a/src/main/kotlin/com/coder/toolbox/views/SignInStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt @@ -2,9 +2,8 @@ package com.coder.toolbox.views import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.util.toURL -import com.coder.toolbox.views.state.AuthContext -import com.coder.toolbox.views.state.AuthWizardState -import com.jetbrains.toolbox.api.localization.LocalizableString +import com.coder.toolbox.views.state.CoderCliSetupContext +import com.coder.toolbox.views.state.CoderCliSetupWizardState import com.jetbrains.toolbox.api.ui.components.RowGroup import com.jetbrains.toolbox.api.ui.components.TextField import com.jetbrains.toolbox.api.ui.components.TextType @@ -19,7 +18,7 @@ import java.net.URL * Populates with the provided URL, at which point the user can accept or * enter their own. */ -class SignInStep( +class DeploymentUrlStep( private val context: CoderToolboxContext, private val notify: (String, Throwable) -> Unit ) : @@ -32,8 +31,6 @@ class SignInStep( RowGroup.RowField(errorField) ) - override val nextButtonTitle: LocalizableString? = context.i18n.ptrl("Sign In") - override fun onVisible() { errorField.textState.update { context.i18n.pnotr("") @@ -55,12 +52,12 @@ class SignInStep( url } try { - AuthContext.url = validateRawUrl(url) + CoderCliSetupContext.url = validateRawUrl(url) } catch (e: MalformedURLException) { notify("URL is invalid", e) return false } - AuthWizardState.goToNextStep() + CoderCliSetupWizardState.goToNextStep() return true } diff --git a/src/main/kotlin/com/coder/toolbox/views/TokenStep.kt b/src/main/kotlin/com/coder/toolbox/views/TokenStep.kt index b02e9ed..b449f40 100644 --- a/src/main/kotlin/com/coder/toolbox/views/TokenStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/TokenStep.kt @@ -2,9 +2,8 @@ package com.coder.toolbox.views import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.util.withPath -import com.coder.toolbox.views.state.AuthContext -import com.coder.toolbox.views.state.AuthWizardState -import com.jetbrains.toolbox.api.localization.LocalizableString +import com.coder.toolbox.views.state.CoderCliSetupContext +import com.coder.toolbox.views.state.CoderCliSetupWizardState import com.jetbrains.toolbox.api.ui.components.LinkField import com.jetbrains.toolbox.api.ui.components.RowGroup import com.jetbrains.toolbox.api.ui.components.TextField @@ -31,15 +30,14 @@ class TokenStep( RowGroup.RowField(linkField), RowGroup.RowField(errorField) ) - override val nextButtonTitle: LocalizableString? = context.i18n.ptrl("Connect") override fun onVisible() { errorField.textState.update { context.i18n.pnotr("") } - if (AuthContext.hasUrl()) { + if (CoderCliSetupContext.hasUrl()) { tokenField.textState.update { - context.secrets.tokenFor(AuthContext.url!!) ?: "" + context.secrets.tokenFor(CoderCliSetupContext.url!!) ?: "" } } else { errorField.textState.update { @@ -48,7 +46,7 @@ class TokenStep( } } (linkField.urlState as MutableStateFlow).update { - AuthContext.url!!.withPath("/login?redirect=%2Fcli-auth")?.toString() ?: "" + CoderCliSetupContext.url!!.withPath("/login?redirect=%2Fcli-auth")?.toString() ?: "" } } @@ -59,12 +57,12 @@ class TokenStep( return false } - AuthContext.token = token - AuthWizardState.goToNextStep() + CoderCliSetupContext.token = token + CoderCliSetupWizardState.goToNextStep() return true } override fun onBack() { - AuthWizardState.goToPreviousStep() + CoderCliSetupWizardState.goToPreviousStep() } } diff --git a/src/main/kotlin/com/coder/toolbox/views/WizardStep.kt b/src/main/kotlin/com/coder/toolbox/views/WizardStep.kt index 6ba3d52..bb19281 100644 --- a/src/main/kotlin/com/coder/toolbox/views/WizardStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/WizardStep.kt @@ -1,11 +1,9 @@ package com.coder.toolbox.views -import com.jetbrains.toolbox.api.localization.LocalizableString import com.jetbrains.toolbox.api.ui.components.RowGroup interface WizardStep { val panel: RowGroup - val nextButtonTitle: LocalizableString? /** * Callback when step is visible diff --git a/src/main/kotlin/com/coder/toolbox/views/state/AuthContext.kt b/src/main/kotlin/com/coder/toolbox/views/state/CoderCliSetupContext.kt similarity index 89% rename from src/main/kotlin/com/coder/toolbox/views/state/AuthContext.kt rename to src/main/kotlin/com/coder/toolbox/views/state/CoderCliSetupContext.kt index 320bd63..8d503b9 100644 --- a/src/main/kotlin/com/coder/toolbox/views/state/AuthContext.kt +++ b/src/main/kotlin/com/coder/toolbox/views/state/CoderCliSetupContext.kt @@ -3,13 +3,13 @@ package com.coder.toolbox.views.state import java.net.URL /** - * Singleton that holds authentication context (URL and token) across multiple + * Singleton that holds Coder CLI setup context (URL and token) across multiple * Toolbox window lifecycle events. * * This ensures that user input (URL and token) is not lost when the Toolbox * window is temporarily closed or recreated. */ -object AuthContext { +object CoderCliSetupContext { /** * The currently entered URL. */ diff --git a/src/main/kotlin/com/coder/toolbox/views/state/AuthWizardState.kt b/src/main/kotlin/com/coder/toolbox/views/state/CoderCliSetupWizardState.kt similarity index 82% rename from src/main/kotlin/com/coder/toolbox/views/state/AuthWizardState.kt rename to src/main/kotlin/com/coder/toolbox/views/state/CoderCliSetupWizardState.kt index c29fbc9..f1efca4 100644 --- a/src/main/kotlin/com/coder/toolbox/views/state/AuthWizardState.kt +++ b/src/main/kotlin/com/coder/toolbox/views/state/CoderCliSetupWizardState.kt @@ -2,13 +2,13 @@ package com.coder.toolbox.views.state /** - * A singleton that maintains the state of the authorization wizard across Toolbox window lifecycle events. + * A singleton that maintains the state of the coder setup wizard across Toolbox window lifecycle events. * * This is used to persist the wizard's progress (i.e., current step) between visibility changes * of the Toolbox window. Without this object, closing and reopening the window would reset the wizard * to its initial state by creating a new instance. */ -object AuthWizardState { +object CoderCliSetupWizardState { private var currentStep = WizardStep.URL_REQUEST fun currentStep(): WizardStep = currentStep @@ -31,5 +31,5 @@ object AuthWizardState { } enum class WizardStep { - URL_REQUEST, TOKEN_REQUEST, LOGIN; + URL_REQUEST, TOKEN_REQUEST, CONNECT; } \ No newline at end of file diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index 1b04695..fe1f90c 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -106,7 +106,7 @@ msgstr "" msgid "Configuring CLI..." msgstr "" -msgid "Sign In" +msgid "Next" msgstr "" msgid "Token" @@ -142,5 +142,8 @@ msgstr "" msgid "Error encountered while handling Coder URI" msgstr "" -msgid "Error encountered during authentication" +msgid "Error encountered while setting up Coder" +msgstr "" + +msgid "Setting up Coder" msgstr "" \ No newline at end of file diff --git a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt index 4603fda..5c37c9e 100644 --- a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt +++ b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt @@ -62,6 +62,9 @@ import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertTrue +private const val VERSION_FOR_PROGRESS_REPORTING = "v2.23.1-devel+de07351b8" +private val noOpTextProgress: (String) -> Unit = { _ -> } + internal class CoderCLIManagerTest { private val context = CoderToolboxContext( mockk(), @@ -145,7 +148,7 @@ internal class CoderCLIManagerTest { val ex = assertFailsWith( exceptionClass = ResponseException::class, - block = { ccm.download() }, + block = { ccm.download(VERSION_FOR_PROGRESS_REPORTING, noOpTextProgress) }, ) assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, ex.code) @@ -200,7 +203,7 @@ internal class CoderCLIManagerTest { assertFailsWith( exceptionClass = AccessDeniedException::class, - block = { ccm.download() }, + block = { ccm.download(VERSION_FOR_PROGRESS_REPORTING, noOpTextProgress) }, ) srv.stop(0) @@ -229,11 +232,11 @@ internal class CoderCLIManagerTest { ).readOnly(), ) - assertTrue(ccm.download()) + assertTrue(ccm.download(VERSION_FOR_PROGRESS_REPORTING, noOpTextProgress)) assertDoesNotThrow { ccm.version() } // It should skip the second attempt. - assertFalse(ccm.download()) + assertFalse(ccm.download(VERSION_FOR_PROGRESS_REPORTING, noOpTextProgress)) // Make sure login failures propagate. assertFailsWith( @@ -258,11 +261,11 @@ internal class CoderCLIManagerTest { ).readOnly(), ) - assertEquals(true, ccm.download()) + assertEquals(true, ccm.download(VERSION_FOR_PROGRESS_REPORTING, noOpTextProgress)) assertEquals(SemVer(url.port.toLong(), 0, 0), ccm.version()) // It should skip the second attempt. - assertEquals(false, ccm.download()) + assertEquals(false, ccm.download(VERSION_FOR_PROGRESS_REPORTING, noOpTextProgress)) // Should use the source override. ccm = CoderCLIManager( @@ -278,7 +281,7 @@ internal class CoderCLIManagerTest { ).readOnly(), ) - assertEquals(true, ccm.download()) + assertEquals(true, ccm.download(VERSION_FOR_PROGRESS_REPORTING, noOpTextProgress)) assertContains(ccm.localBinaryPath.toFile().readText(), "0.0.0") srv.stop(0) @@ -326,7 +329,7 @@ internal class CoderCLIManagerTest { assertEquals("cli", ccm.localBinaryPath.toFile().readText()) assertEquals(0, ccm.localBinaryPath.toFile().lastModified()) - assertTrue(ccm.download()) + assertTrue(ccm.download(VERSION_FOR_PROGRESS_REPORTING, noOpTextProgress)) assertNotEquals("cli", ccm.localBinaryPath.toFile().readText()) assertNotEquals(0, ccm.localBinaryPath.toFile().lastModified()) @@ -351,8 +354,8 @@ internal class CoderCLIManagerTest { val ccm1 = CoderCLIManager(url1, context.logger, settings) val ccm2 = CoderCLIManager(url2, context.logger, settings) - assertTrue(ccm1.download()) - assertTrue(ccm2.download()) + assertTrue(ccm1.download(VERSION_FOR_PROGRESS_REPORTING, noOpTextProgress)) + assertTrue(ccm2.download(VERSION_FOR_PROGRESS_REPORTING, noOpTextProgress)) srv1.stop(0) srv2.stop(0) @@ -883,12 +886,12 @@ internal class CoderCLIManagerTest { Result.ERROR -> { assertFailsWith( exceptionClass = AccessDeniedException::class, - block = { ensureCLI(localContext, url, it.buildVersion) }, + block = { ensureCLI(localContext, url, it.buildVersion, noOpTextProgress) }, ) } Result.NONE -> { - val ccm = ensureCLI(localContext, url, it.buildVersion) + val ccm = ensureCLI(localContext, url, it.buildVersion, noOpTextProgress) assertEquals(settings.binPath(url), ccm.localBinaryPath) assertFailsWith( exceptionClass = ProcessInitException::class, @@ -897,25 +900,25 @@ internal class CoderCLIManagerTest { } Result.DL_BIN -> { - val ccm = ensureCLI(localContext, url, it.buildVersion) + val ccm = ensureCLI(localContext, url, it.buildVersion, noOpTextProgress) assertEquals(settings.binPath(url), ccm.localBinaryPath) assertEquals(SemVer(url.port.toLong(), 0, 0), ccm.version()) } Result.DL_DATA -> { - val ccm = ensureCLI(localContext, url, it.buildVersion) + val ccm = ensureCLI(localContext, url, it.buildVersion, noOpTextProgress) assertEquals(settings.binPath(url, true), ccm.localBinaryPath) assertEquals(SemVer(url.port.toLong(), 0, 0), ccm.version()) } Result.USE_BIN -> { - val ccm = ensureCLI(localContext, url, it.buildVersion) + val ccm = ensureCLI(localContext, url, it.buildVersion, noOpTextProgress) assertEquals(settings.binPath(url), ccm.localBinaryPath) assertEquals(SemVer.parse(it.version ?: ""), ccm.version()) } Result.USE_DATA -> { - val ccm = ensureCLI(localContext, url, it.buildVersion) + val ccm = ensureCLI(localContext, url, it.buildVersion, noOpTextProgress) assertEquals(settings.binPath(url, true), ccm.localBinaryPath) assertEquals(SemVer.parse(it.fallbackVersion ?: ""), ccm.version()) } @@ -955,7 +958,7 @@ internal class CoderCLIManagerTest { context.logger, ).readOnly(), ) - assertEquals(true, ccm.download()) + assertEquals(true, ccm.download(VERSION_FOR_PROGRESS_REPORTING, noOpTextProgress)) assertEquals(it.second, ccm.features, "version: ${it.first}") srv.stop(0)