Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0852d88

Browse files
authored
impl: report progress while handling URI (#180)
Up until now there was no progress while downloading CLI, setting up the cli and the ssh config while handling URIs. This PR reworks the uri handler and the connection screen to be able to reuse the later part in the URI handler. This should improve the experience because the user is no longer left in the dark for a good couple of seconds.
1 parent acd0578 commit 0852d88

File tree

8 files changed

+91
-79
lines changed

8 files changed

+91
-79
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- progress reporting while handling URIs
8+
59
### Changed
610

711
- workspaces status is now refresh every time Coder Toolbox becomes visible

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import com.coder.toolbox.sdk.ex.APIResponseException
77
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
88
import com.coder.toolbox.util.CoderProtocolHandler
99
import com.coder.toolbox.util.DialogUi
10+
import com.coder.toolbox.util.toURL
1011
import com.coder.toolbox.util.waitForTrue
1112
import com.coder.toolbox.util.withPath
1213
import com.coder.toolbox.views.Action
1314
import com.coder.toolbox.views.CoderCliSetupWizardPage
1415
import com.coder.toolbox.views.CoderSettingsPage
1516
import com.coder.toolbox.views.NewEnvironmentPage
17+
import com.coder.toolbox.views.state.CoderCliSetupContext
1618
import com.coder.toolbox.views.state.CoderCliSetupWizardState
1719
import com.coder.toolbox.views.state.WizardStep
1820
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
@@ -35,7 +37,6 @@ import kotlinx.coroutines.launch
3537
import kotlinx.coroutines.selects.onTimeout
3638
import kotlinx.coroutines.selects.select
3739
import java.net.URI
38-
import java.util.UUID
3940
import kotlin.coroutines.cancellation.CancellationException
4041
import kotlin.time.Duration.Companion.seconds
4142
import kotlin.time.TimeSource
@@ -66,19 +67,18 @@ class CoderRemoteProvider(
6667
private var firstRun = true
6768
private val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
6869
private val coderHeaderPage = NewEnvironmentPage(context.i18n.pnotr(context.deploymentUrl.toString()))
69-
private val linkHandler = CoderProtocolHandler(context, dialogUi, isInitialized)
70-
71-
override val loadingEnvironmentsDescription: LocalizableString = context.i18n.ptrl("Loading workspaces...")
72-
override val environments: MutableStateFlow<LoadableState<List<CoderRemoteEnvironment>>> = MutableStateFlow(
73-
LoadableState.Loading
74-
)
75-
7670
private val visibilityState = MutableStateFlow(
7771
ProviderVisibilityState(
7872
applicationVisible = false,
7973
providerVisible = false
8074
)
8175
)
76+
private val linkHandler = CoderProtocolHandler(context, dialogUi, settingsPage, visibilityState, isInitialized)
77+
78+
override val loadingEnvironmentsDescription: LocalizableString = context.i18n.ptrl("Loading workspaces...")
79+
override val environments: MutableStateFlow<LoadableState<List<CoderRemoteEnvironment>>> = MutableStateFlow(
80+
LoadableState.Loading
81+
)
8282

8383
private val errorBuffer = mutableListOf<Throwable>()
8484

@@ -311,17 +311,8 @@ class CoderRemoteProvider(
311311
override suspend fun handleUri(uri: URI) {
312312
try {
313313
linkHandler.handle(
314-
uri, shouldDoAutoSetup(),
315-
{
316-
coderHeaderPage.isBusyCreatingNewEnvironment.update {
317-
true
318-
}
319-
},
320-
{
321-
coderHeaderPage.isBusyCreatingNewEnvironment.update {
322-
false
323-
}
324-
}
314+
uri,
315+
shouldDoAutoSetup()
325316
) { restClient, cli ->
326317
// stop polling and de-initialize resources
327318
close()
@@ -337,23 +328,16 @@ class CoderRemoteProvider(
337328
isInitialized.waitForTrue()
338329
}
339330
} catch (ex: Exception) {
340-
context.logger.error(ex, "")
341331
val textError = if (ex is APIResponseException) {
342332
if (!ex.reason.isNullOrBlank()) {
343333
ex.reason
344334
} else ex.message
345335
} else ex.message
346-
347-
context.ui.showSnackbar(
348-
UUID.randomUUID().toString(),
349-
context.i18n.ptrl("Error encountered while handling Coder URI"),
350-
context.i18n.pnotr(textError ?: ""),
351-
context.i18n.ptrl("Dismiss")
336+
context.logAndShowError(
337+
"Error encountered while handling Coder URI",
338+
textError ?: ""
352339
)
353-
} finally {
354-
coderHeaderPage.isBusyCreatingNewEnvironment.update {
355-
false
356-
}
340+
context.envPageManager.showPluginEnvironmentsPage()
357341
}
358342
}
359343

@@ -369,8 +353,17 @@ class CoderRemoteProvider(
369353
// When coming back to the application, initializeSession immediately.
370354
if (shouldDoAutoSetup()) {
371355
try {
356+
CoderCliSetupContext.apply {
357+
url = context.secrets.lastDeploymentURL.toURL()
358+
token = context.secrets.lastToken
359+
}
372360
CoderCliSetupWizardState.goToStep(WizardStep.CONNECT)
373-
return CoderCliSetupWizardPage(context, settingsPage, visibilityState, true, ::onConnect)
361+
return CoderCliSetupWizardPage(
362+
context, settingsPage, visibilityState,
363+
initialAutoSetup = true,
364+
jumpToMainPageOnError = false,
365+
onConnect = ::onConnect
366+
)
374367
} catch (ex: Exception) {
375368
errorBuffer.add(ex)
376369
} finally {

src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,9 @@ data class CoderToolboxContext(
8888
i18n.ptrl("OK")
8989
)
9090
}
91+
92+
fun popupPluginMainPage() {
93+
this.ui.showWindow()
94+
this.envPageManager.showPluginEnvironmentsPage(true)
95+
}
9196
}

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@ import com.coder.toolbox.sdk.v2.models.Workspace
1010
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
1111
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
1212
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
13+
import com.coder.toolbox.views.CoderCliSetupWizardPage
14+
import com.coder.toolbox.views.CoderSettingsPage
15+
import com.coder.toolbox.views.state.CoderCliSetupContext
16+
import com.coder.toolbox.views.state.CoderCliSetupWizardState
17+
import com.coder.toolbox.views.state.WizardStep
18+
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
1319
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
1420
import kotlinx.coroutines.Job
1521
import kotlinx.coroutines.TimeoutCancellationException
1622
import kotlinx.coroutines.delay
23+
import kotlinx.coroutines.flow.MutableStateFlow
1724
import kotlinx.coroutines.flow.StateFlow
1825
import kotlinx.coroutines.launch
1926
import kotlinx.coroutines.time.withTimeout
@@ -25,12 +32,13 @@ import kotlin.time.Duration.Companion.seconds
2532
import kotlin.time.toJavaDuration
2633

2734
private const val CAN_T_HANDLE_URI_TITLE = "Can't handle URI"
28-
private val noOpTextProgress: (String) -> Unit = { _ -> }
2935

3036
@Suppress("UnstableApiUsage")
3137
open class CoderProtocolHandler(
3238
private val context: CoderToolboxContext,
3339
private val dialogUi: DialogUi,
40+
private val settingsPage: CoderSettingsPage,
41+
private val visibilityState: MutableStateFlow<ProviderVisibilityState>,
3442
private val isInitialized: StateFlow<Boolean>,
3543
) {
3644
private val settings = context.settingsStore.readOnly()
@@ -45,8 +53,6 @@ open class CoderProtocolHandler(
4553
suspend fun handle(
4654
uri: URI,
4755
shouldWaitForAutoLogin: Boolean,
48-
markAsBusy: () -> Unit,
49-
unmarkAsBusy: () -> Unit,
5056
reInitialize: suspend (CoderRestClient, CoderCLIManager) -> Unit
5157
) {
5258
val params = uri.toQueryParameters()
@@ -58,7 +64,6 @@ open class CoderProtocolHandler(
5864
// this switches to the main plugin screen, even
5965
// if last opened provider was not Coder
6066
context.envPageManager.showPluginEnvironmentsPage()
61-
markAsBusy()
6267
if (shouldWaitForAutoLogin) {
6368
isInitialized.waitForTrue()
6469
}
@@ -67,39 +72,53 @@ open class CoderProtocolHandler(
6772
val deploymentURL = resolveDeploymentUrl(params) ?: return
6873
val token = if (!context.settingsStore.requireTokenAuth) null else resolveToken(params) ?: return
6974
val workspaceName = resolveWorkspaceName(params) ?: return
70-
val restClient = buildRestClient(deploymentURL, token) ?: return
71-
val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL) ?: return
7275

73-
val cli = configureCli(deploymentURL, restClient)
74-
75-
var agent: WorkspaceAgent
76-
try {
76+
suspend fun onConnect(
77+
restClient: CoderRestClient,
78+
cli: CoderCLIManager
79+
) {
80+
val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL)
81+
if (workspace == null) {
82+
context.envPageManager.showPluginEnvironmentsPage()
83+
return
84+
}
7785
reInitialize(restClient, cli)
7886
context.envPageManager.showPluginEnvironmentsPage()
7987
if (!prepareWorkspace(workspace, restClient, workspaceName, deploymentURL)) return
8088
// we resolve the agent after the workspace is started otherwise we can get misleading
8189
// errors like: no agent available while workspace is starting or stopping
8290
// we also need to retrieve the workspace again to have the latest resources (ex: agent)
8391
// attached to the workspace.
84-
agent = resolveAgent(
92+
val agent: WorkspaceAgent = resolveAgent(
8593
params,
8694
restClient.workspace(workspace.id)
8795
) ?: return
8896
if (!ensureAgentIsReady(workspace, agent)) return
89-
} finally {
90-
unmarkAsBusy()
91-
}
92-
delay(2.seconds)
93-
val environmentId = "${workspace.name}.${agent.name}"
94-
context.showEnvironmentPage(environmentId)
97+
delay(2.seconds)
98+
val environmentId = "${workspace.name}.${agent.name}"
99+
context.showEnvironmentPage(environmentId)
95100

96-
val productCode = params.ideProductCode()
97-
val buildNumber = params.ideBuildNumber()
98-
val projectFolder = params.projectFolder()
101+
val productCode = params.ideProductCode()
102+
val buildNumber = params.ideBuildNumber()
103+
val projectFolder = params.projectFolder()
104+
105+
if (!productCode.isNullOrBlank() && !buildNumber.isNullOrBlank()) {
106+
launchIde(environmentId, productCode, buildNumber, projectFolder)
107+
}
108+
}
99109

100-
if (!productCode.isNullOrBlank() && !buildNumber.isNullOrBlank()) {
101-
launchIde(environmentId, productCode, buildNumber, projectFolder)
110+
CoderCliSetupContext.apply {
111+
url = deploymentURL.toURL()
112+
CoderCliSetupContext.token = token
102113
}
114+
CoderCliSetupWizardState.goToStep(WizardStep.CONNECT)
115+
context.ui.showUiPage(
116+
CoderCliSetupWizardPage(
117+
context, settingsPage, visibilityState, true,
118+
jumpToMainPageOnError = true,
119+
onConnect = ::onConnect
120+
)
121+
)
103122
}
104123

105124
private suspend fun resolveDeploymentUrl(params: Map<String, String>): String? {
@@ -308,13 +327,14 @@ open class CoderProtocolHandler(
308327

309328
private suspend fun configureCli(
310329
deploymentURL: String,
311-
restClient: CoderRestClient
330+
restClient: CoderRestClient,
331+
progressReporter: (String) -> Unit
312332
): CoderCLIManager {
313333
val cli = ensureCLI(
314334
context,
315335
deploymentURL.toURL(),
316336
restClient.buildInfo().version,
317-
noOpTextProgress
337+
progressReporter
318338
)
319339

320340
// We only need to log in if we are using token-based auth.
@@ -455,12 +475,6 @@ open class CoderProtocolHandler(
455475
}
456476
}
457477

458-
459-
private fun CoderToolboxContext.popupPluginMainPage() {
460-
this.ui.showWindow()
461-
this.envPageManager.showPluginEnvironmentsPage(true)
462-
}
463-
464478
private suspend fun CoderToolboxContext.showEnvironmentPage(envId: String) {
465479
this.ui.showWindow()
466480
this.envPageManager.showEnvironmentPage(envId, false)

src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import com.coder.toolbox.CoderToolboxContext
44
import com.coder.toolbox.cli.CoderCLIManager
55
import com.coder.toolbox.sdk.CoderRestClient
66
import com.coder.toolbox.sdk.ex.APIResponseException
7-
import com.coder.toolbox.util.toURL
8-
import com.coder.toolbox.views.state.CoderCliSetupContext
97
import com.coder.toolbox.views.state.CoderCliSetupWizardState
108
import com.coder.toolbox.views.state.WizardStep
119
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
@@ -21,6 +19,7 @@ class CoderCliSetupWizardPage(
2119
private val settingsPage: CoderSettingsPage,
2220
private val visibilityState: MutableStateFlow<ProviderVisibilityState>,
2321
initialAutoSetup: Boolean = false,
22+
jumpToMainPageOnError: Boolean = false,
2423
onConnect: suspend (
2524
client: CoderRestClient,
2625
cli: CoderCLIManager,
@@ -35,7 +34,8 @@ class CoderCliSetupWizardPage(
3534
private val tokenStep = TokenStep(context)
3635
private val connectStep = ConnectStep(
3736
context,
38-
shouldAutoSetup,
37+
shouldAutoLogin = shouldAutoSetup,
38+
jumpToMainPageOnError,
3939
this::notify,
4040
this::displaySteps,
4141
onConnect
@@ -49,13 +49,6 @@ class CoderCliSetupWizardPage(
4949

5050
private val errorBuffer = mutableListOf<Throwable>()
5151

52-
init {
53-
if (shouldAutoSetup.value) {
54-
CoderCliSetupContext.url = context.secrets.lastDeploymentURL.toURL()
55-
CoderCliSetupContext.token = context.secrets.lastToken
56-
}
57-
}
58-
5952
override fun beforeShow() {
6053
displaySteps()
6154
if (errorBuffer.isNotEmpty() && visibilityState.value.applicationVisible) {

src/main/kotlin/com/coder/toolbox/views/CoderPage.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.coder.toolbox.views
22

3-
import com.coder.toolbox.CoderToolboxContext
43
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
54
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType
65
import com.jetbrains.toolbox.api.localization.LocalizableString
@@ -43,12 +42,6 @@ abstract class CoderPage(
4342
} else {
4443
SvgIcon(byteArrayOf(), type = IconType.Masked)
4544
}
46-
47-
override val isBusyCreatingNewEnvironment: MutableStateFlow<Boolean> = MutableStateFlow(false)
48-
49-
companion object {
50-
fun emptyPage(ctx: CoderToolboxContext): UiPage = UiPage(ctx.i18n.pnotr(""))
51-
}
5245
}
5346

5447
/**

src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ private const val USER_HIT_THE_BACK_BUTTON = "User hit the back button"
2525
class ConnectStep(
2626
private val context: CoderToolboxContext,
2727
private val shouldAutoLogin: StateFlow<Boolean>,
28+
private val jumpToMainPageOnError: Boolean,
2829
private val notify: (String, Throwable) -> Unit,
2930
private val refreshWizard: () -> Unit,
3031
private val onConnect: suspend (
@@ -127,7 +128,11 @@ class ConnectStep(
127128
} finally {
128129
if (shouldAutoLogin.value) {
129130
CoderCliSetupContext.reset()
130-
CoderCliSetupWizardState.goToFirstStep()
131+
if (jumpToMainPageOnError) {
132+
context.popupPluginMainPage()
133+
} else {
134+
CoderCliSetupWizardState.goToFirstStep()
135+
}
131136
} else {
132137
if (context.settingsStore.requireTokenAuth) {
133138
CoderCliSetupWizardState.goToPreviousStep()

src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import com.coder.toolbox.sdk.DataGen
55
import com.coder.toolbox.settings.Environment
66
import com.coder.toolbox.store.CoderSecretsStore
77
import com.coder.toolbox.store.CoderSettingsStore
8+
import com.coder.toolbox.views.CoderSettingsPage
89
import com.jetbrains.toolbox.api.core.diagnostics.Logger
910
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
1011
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
12+
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
1113
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
1214
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
1315
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
@@ -16,6 +18,7 @@ import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
1618
import com.jetbrains.toolbox.api.ui.ToolboxUi
1719
import io.mockk.mockk
1820
import kotlinx.coroutines.CoroutineScope
21+
import kotlinx.coroutines.channels.Channel
1922
import kotlinx.coroutines.flow.MutableStateFlow
2023
import kotlinx.coroutines.runBlocking
2124
import org.junit.jupiter.api.DisplayName
@@ -43,6 +46,8 @@ internal class CoderProtocolHandlerTest {
4346
private val protocolHandler = CoderProtocolHandler(
4447
context,
4548
DialogUi(context),
49+
CoderSettingsPage(context, Channel(Channel.CONFLATED)),
50+
MutableStateFlow(ProviderVisibilityState(applicationVisible = true, providerVisible = true)),
4651
MutableStateFlow(false)
4752
)
4853

0 commit comments

Comments
 (0)