diff --git a/CHANGELOG.md b/CHANGELOG.md index e87ca97..08ac799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- progress reporting while handling URIs + ### Changed - workspaces status is now refresh every time Coder Toolbox becomes visible diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 596255e..eb9997e 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -7,12 +7,14 @@ import com.coder.toolbox.sdk.ex.APIResponseException import com.coder.toolbox.sdk.v2.models.WorkspaceStatus import com.coder.toolbox.util.CoderProtocolHandler import com.coder.toolbox.util.DialogUi +import com.coder.toolbox.util.toURL import com.coder.toolbox.util.waitForTrue import com.coder.toolbox.util.withPath import com.coder.toolbox.views.Action import com.coder.toolbox.views.CoderCliSetupWizardPage import com.coder.toolbox.views.CoderSettingsPage import com.coder.toolbox.views.NewEnvironmentPage +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.core.ui.icons.SvgIcon @@ -35,7 +37,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.selects.onTimeout import kotlinx.coroutines.selects.select import java.net.URI -import java.util.UUID import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration.Companion.seconds import kotlin.time.TimeSource @@ -66,19 +67,18 @@ class CoderRemoteProvider( private var firstRun = true private val isInitialized: MutableStateFlow = MutableStateFlow(false) private val coderHeaderPage = NewEnvironmentPage(context.i18n.pnotr(context.deploymentUrl.toString())) - private val linkHandler = CoderProtocolHandler(context, dialogUi, isInitialized) - - override val loadingEnvironmentsDescription: LocalizableString = context.i18n.ptrl("Loading workspaces...") - override val environments: MutableStateFlow>> = MutableStateFlow( - LoadableState.Loading - ) - private val visibilityState = MutableStateFlow( ProviderVisibilityState( applicationVisible = false, providerVisible = false ) ) + private val linkHandler = CoderProtocolHandler(context, dialogUi, settingsPage, visibilityState, isInitialized) + + override val loadingEnvironmentsDescription: LocalizableString = context.i18n.ptrl("Loading workspaces...") + override val environments: MutableStateFlow>> = MutableStateFlow( + LoadableState.Loading + ) private val errorBuffer = mutableListOf() @@ -311,17 +311,8 @@ class CoderRemoteProvider( override suspend fun handleUri(uri: URI) { try { linkHandler.handle( - uri, shouldDoAutoSetup(), - { - coderHeaderPage.isBusyCreatingNewEnvironment.update { - true - } - }, - { - coderHeaderPage.isBusyCreatingNewEnvironment.update { - false - } - } + uri, + shouldDoAutoSetup() ) { restClient, cli -> // stop polling and de-initialize resources close() @@ -337,23 +328,16 @@ class CoderRemoteProvider( isInitialized.waitForTrue() } } catch (ex: Exception) { - context.logger.error(ex, "") val textError = if (ex is APIResponseException) { if (!ex.reason.isNullOrBlank()) { ex.reason } else ex.message } else ex.message - - context.ui.showSnackbar( - UUID.randomUUID().toString(), - context.i18n.ptrl("Error encountered while handling Coder URI"), - context.i18n.pnotr(textError ?: ""), - context.i18n.ptrl("Dismiss") + context.logAndShowError( + "Error encountered while handling Coder URI", + textError ?: "" ) - } finally { - coderHeaderPage.isBusyCreatingNewEnvironment.update { - false - } + context.envPageManager.showPluginEnvironmentsPage() } } @@ -369,8 +353,17 @@ class CoderRemoteProvider( // When coming back to the application, initializeSession immediately. if (shouldDoAutoSetup()) { try { + CoderCliSetupContext.apply { + url = context.secrets.lastDeploymentURL.toURL() + token = context.secrets.lastToken + } CoderCliSetupWizardState.goToStep(WizardStep.CONNECT) - return CoderCliSetupWizardPage(context, settingsPage, visibilityState, true, ::onConnect) + return CoderCliSetupWizardPage( + context, settingsPage, visibilityState, + initialAutoSetup = true, + jumpToMainPageOnError = false, + onConnect = ::onConnect + ) } catch (ex: Exception) { errorBuffer.add(ex) } finally { diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt index 4291321..baac820 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt @@ -88,4 +88,9 @@ data class CoderToolboxContext( i18n.ptrl("OK") ) } + + fun popupPluginMainPage() { + this.ui.showWindow() + this.envPageManager.showPluginEnvironmentsPage(true) + } } diff --git a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt index f0e84b9..76877cf 100644 --- a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt +++ b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt @@ -10,10 +10,17 @@ import com.coder.toolbox.sdk.v2.models.Workspace import com.coder.toolbox.sdk.v2.models.WorkspaceAgent import com.coder.toolbox.sdk.v2.models.WorkspaceStatus import com.coder.toolbox.util.WebUrlValidationResult.Invalid +import com.coder.toolbox.views.CoderCliSetupWizardPage +import com.coder.toolbox.views.CoderSettingsPage +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.remoteDev.connection.RemoteToolsHelper import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.time.withTimeout @@ -25,12 +32,13 @@ 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( private val context: CoderToolboxContext, private val dialogUi: DialogUi, + private val settingsPage: CoderSettingsPage, + private val visibilityState: MutableStateFlow, private val isInitialized: StateFlow, ) { private val settings = context.settingsStore.readOnly() @@ -45,8 +53,6 @@ open class CoderProtocolHandler( suspend fun handle( uri: URI, shouldWaitForAutoLogin: Boolean, - markAsBusy: () -> Unit, - unmarkAsBusy: () -> Unit, reInitialize: suspend (CoderRestClient, CoderCLIManager) -> Unit ) { val params = uri.toQueryParameters() @@ -58,7 +64,6 @@ open class CoderProtocolHandler( // this switches to the main plugin screen, even // if last opened provider was not Coder context.envPageManager.showPluginEnvironmentsPage() - markAsBusy() if (shouldWaitForAutoLogin) { isInitialized.waitForTrue() } @@ -67,13 +72,16 @@ open class CoderProtocolHandler( val deploymentURL = resolveDeploymentUrl(params) ?: return val token = if (!context.settingsStore.requireTokenAuth) null else resolveToken(params) ?: return val workspaceName = resolveWorkspaceName(params) ?: return - val restClient = buildRestClient(deploymentURL, token) ?: return - val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL) ?: return - val cli = configureCli(deploymentURL, restClient) - - var agent: WorkspaceAgent - try { + suspend fun onConnect( + restClient: CoderRestClient, + cli: CoderCLIManager + ) { + val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL) + if (workspace == null) { + context.envPageManager.showPluginEnvironmentsPage() + return + } reInitialize(restClient, cli) context.envPageManager.showPluginEnvironmentsPage() if (!prepareWorkspace(workspace, restClient, workspaceName, deploymentURL)) return @@ -81,25 +89,36 @@ open class CoderProtocolHandler( // errors like: no agent available while workspace is starting or stopping // we also need to retrieve the workspace again to have the latest resources (ex: agent) // attached to the workspace. - agent = resolveAgent( + val agent: WorkspaceAgent = resolveAgent( params, restClient.workspace(workspace.id) ) ?: return if (!ensureAgentIsReady(workspace, agent)) return - } finally { - unmarkAsBusy() - } - delay(2.seconds) - val environmentId = "${workspace.name}.${agent.name}" - context.showEnvironmentPage(environmentId) + delay(2.seconds) + val environmentId = "${workspace.name}.${agent.name}" + context.showEnvironmentPage(environmentId) - val productCode = params.ideProductCode() - val buildNumber = params.ideBuildNumber() - val projectFolder = params.projectFolder() + val productCode = params.ideProductCode() + val buildNumber = params.ideBuildNumber() + val projectFolder = params.projectFolder() + + if (!productCode.isNullOrBlank() && !buildNumber.isNullOrBlank()) { + launchIde(environmentId, productCode, buildNumber, projectFolder) + } + } - if (!productCode.isNullOrBlank() && !buildNumber.isNullOrBlank()) { - launchIde(environmentId, productCode, buildNumber, projectFolder) + CoderCliSetupContext.apply { + url = deploymentURL.toURL() + CoderCliSetupContext.token = token } + CoderCliSetupWizardState.goToStep(WizardStep.CONNECT) + context.ui.showUiPage( + CoderCliSetupWizardPage( + context, settingsPage, visibilityState, true, + jumpToMainPageOnError = true, + onConnect = ::onConnect + ) + ) } private suspend fun resolveDeploymentUrl(params: Map): String? { @@ -308,13 +327,14 @@ open class CoderProtocolHandler( private suspend fun configureCli( deploymentURL: String, - restClient: CoderRestClient + restClient: CoderRestClient, + progressReporter: (String) -> Unit ): CoderCLIManager { val cli = ensureCLI( context, deploymentURL.toURL(), restClient.buildInfo().version, - noOpTextProgress + progressReporter ) // We only need to log in if we are using token-based auth. @@ -455,12 +475,6 @@ open class CoderProtocolHandler( } } - -private fun CoderToolboxContext.popupPluginMainPage() { - this.ui.showWindow() - this.envPageManager.showPluginEnvironmentsPage(true) -} - private suspend fun CoderToolboxContext.showEnvironmentPage(envId: String) { this.ui.showWindow() this.envPageManager.showEnvironmentPage(envId, false) diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt index 5115204..1e6b152 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt @@ -4,8 +4,6 @@ import com.coder.toolbox.CoderToolboxContext 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.CoderCliSetupContext import com.coder.toolbox.views.state.CoderCliSetupWizardState import com.coder.toolbox.views.state.WizardStep import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState @@ -21,6 +19,7 @@ class CoderCliSetupWizardPage( private val settingsPage: CoderSettingsPage, private val visibilityState: MutableStateFlow, initialAutoSetup: Boolean = false, + jumpToMainPageOnError: Boolean = false, onConnect: suspend ( client: CoderRestClient, cli: CoderCLIManager, @@ -35,7 +34,8 @@ class CoderCliSetupWizardPage( private val tokenStep = TokenStep(context) private val connectStep = ConnectStep( context, - shouldAutoSetup, + shouldAutoLogin = shouldAutoSetup, + jumpToMainPageOnError, this::notify, this::displaySteps, onConnect @@ -49,13 +49,6 @@ class CoderCliSetupWizardPage( private val errorBuffer = mutableListOf() - init { - if (shouldAutoSetup.value) { - CoderCliSetupContext.url = context.secrets.lastDeploymentURL.toURL() - CoderCliSetupContext.token = context.secrets.lastToken - } - } - override fun beforeShow() { displaySteps() if (errorBuffer.isNotEmpty() && visibilityState.value.applicationVisible) { diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt index 363d618..eec0765 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt @@ -1,6 +1,5 @@ package com.coder.toolbox.views -import com.coder.toolbox.CoderToolboxContext import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType import com.jetbrains.toolbox.api.localization.LocalizableString @@ -43,12 +42,6 @@ abstract class CoderPage( } else { SvgIcon(byteArrayOf(), type = IconType.Masked) } - - override val isBusyCreatingNewEnvironment: MutableStateFlow = MutableStateFlow(false) - - companion object { - fun emptyPage(ctx: CoderToolboxContext): UiPage = UiPage(ctx.i18n.pnotr("")) - } } /** diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt index 7ea93e4..40db0cb 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt @@ -25,6 +25,7 @@ private const val USER_HIT_THE_BACK_BUTTON = "User hit the back button" class ConnectStep( private val context: CoderToolboxContext, private val shouldAutoLogin: StateFlow, + private val jumpToMainPageOnError: Boolean, private val notify: (String, Throwable) -> Unit, private val refreshWizard: () -> Unit, private val onConnect: suspend ( @@ -127,7 +128,11 @@ class ConnectStep( } finally { if (shouldAutoLogin.value) { CoderCliSetupContext.reset() - CoderCliSetupWizardState.goToFirstStep() + if (jumpToMainPageOnError) { + context.popupPluginMainPage() + } else { + CoderCliSetupWizardState.goToFirstStep() + } } else { if (context.settingsStore.requireTokenAuth) { CoderCliSetupWizardState.goToPreviousStep() diff --git a/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt b/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt index b26acde..56402e5 100644 --- a/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt +++ b/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt @@ -5,9 +5,11 @@ import com.coder.toolbox.sdk.DataGen import com.coder.toolbox.settings.Environment import com.coder.toolbox.store.CoderSecretsStore import com.coder.toolbox.store.CoderSettingsStore +import com.coder.toolbox.views.CoderSettingsPage import com.jetbrains.toolbox.api.core.diagnostics.Logger import com.jetbrains.toolbox.api.core.os.LocalDesktopManager import com.jetbrains.toolbox.api.localization.LocalizableStringFactory +import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings @@ -16,6 +18,7 @@ import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager import com.jetbrains.toolbox.api.ui.ToolboxUi import io.mockk.mockk import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.DisplayName @@ -43,6 +46,8 @@ internal class CoderProtocolHandlerTest { private val protocolHandler = CoderProtocolHandler( context, DialogUi(context), + CoderSettingsPage(context, Channel(Channel.CONFLATED)), + MutableStateFlow(ProviderVisibilityState(applicationVisible = true, providerVisible = true)), MutableStateFlow(false) )