Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Added

- progress reporting while handling URIs

### Changed

- workspaces status is now refresh every time Coder Toolbox becomes visible
Expand Down
48 changes: 18 additions & 30 deletions src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -66,19 +67,18 @@ class CoderRemoteProvider(
private var firstRun = true
private val isInitialized: MutableStateFlow<Boolean> = 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<LoadableState<List<CoderRemoteEnvironment>>> = 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<LoadableState<List<CoderRemoteEnvironment>>> = MutableStateFlow(
LoadableState.Loading
)

private val errorBuffer = mutableListOf<Throwable>()

Expand Down Expand Up @@ -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()
Expand All @@ -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()
}
}

Expand All @@ -369,6 +353,10 @@ 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)
} catch (ex: Exception) {
Expand Down
62 changes: 38 additions & 24 deletions src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ProviderVisibilityState>,
private val isInitialized: StateFlow<Boolean>,
) {
private val settings = context.settingsStore.readOnly()
Expand All @@ -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()
Expand All @@ -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()
}
Expand All @@ -67,39 +72,47 @@ 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
// we resolve the agent after the workspace is started otherwise we can get misleading
// 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, ::onConnect))
}

private suspend fun resolveDeploymentUrl(params: Map<String, String>): String? {
Expand Down Expand Up @@ -308,13 +321,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,13 +47,6 @@ class CoderCliSetupWizardPage(

private val errorBuffer = mutableListOf<Throwable>()

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) {
Expand Down
7 changes: 0 additions & 7 deletions src/main/kotlin/com/coder/toolbox/views/CoderPage.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -43,12 +42,6 @@ abstract class CoderPage(
} else {
SvgIcon(byteArrayOf(), type = IconType.Masked)
}

override val isBusyCreatingNewEnvironment: MutableStateFlow<Boolean> = MutableStateFlow(false)

companion object {
fun emptyPage(ctx: CoderToolboxContext): UiPage = UiPage(ctx.i18n.pnotr(""))
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ internal class CoderProtocolHandlerTest {
private val protocolHandler = CoderProtocolHandler(
context,
DialogUi(context),
MutableStateFlow(false)
MutableStateFlow(false),
visibilityState,
isInitialized
)

private val agents =
Expand Down
Loading