Skip to content

Commit c56250a

Browse files
authored
impl: support uri handling (#35)
- reacts to URIs in the form of : `jetbrains://gateway/com.coder.toolbox?url=https%3A%2F%2Fdev.coder.com&token=....&workspace=bobiverse-bill` - query parameters like `url`, `token` and `workspace` are mandatory. A fallback implementation is now provided where we ask for these parameters in case they were missing - support for handling workspace from a Coder deployment that is not yet configured in the Toolbox. - resolves #37
1 parent 2b18fe4 commit c56250a

13 files changed

+233
-244
lines changed

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

+24-15
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ import com.coder.toolbox.services.CoderSecretsService
77
import com.coder.toolbox.services.CoderSettingsService
88
import com.coder.toolbox.settings.CoderSettings
99
import com.coder.toolbox.settings.Source
10+
import com.coder.toolbox.util.CoderProtocolHandler
1011
import com.coder.toolbox.util.DialogUi
11-
import com.coder.toolbox.util.LinkHandler
12-
import com.coder.toolbox.util.toQueryParameters
1312
import com.coder.toolbox.views.Action
1413
import com.coder.toolbox.views.CoderSettingsPage
1514
import com.coder.toolbox.views.ConnectPage
@@ -53,7 +52,6 @@ class CoderRemoteProvider(
5352
private val secrets: CoderSecretsService = CoderSecretsService(context.secretsStore)
5453
private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, settingsService)
5554
private val dialogUi = DialogUi(context, settings)
56-
private val linkHandler = LinkHandler(context, settings, httpClient, dialogUi)
5755

5856
// The REST client, if we are signed in
5957
private var client: CoderRestClient? = null
@@ -65,7 +63,9 @@ class CoderRemoteProvider(
6563

6664
// On the first load, automatically log in if we can.
6765
private var firstRun = true
68-
66+
private val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
67+
private var coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(getDeploymentURL()?.first ?: ""))
68+
private val linkHandler = CoderProtocolHandler(context, settings, httpClient, dialogUi, isInitialized)
6969
override val environments: MutableStateFlow<LoadableState<List<RemoteProviderEnvironment>>> = MutableStateFlow(
7070
LoadableState.Value(emptyList())
7171
)
@@ -122,6 +122,12 @@ class CoderRemoteProvider(
122122
environments.update {
123123
LoadableState.Value(resolvedEnvironments.toList())
124124
}
125+
if (isInitialized.value == false) {
126+
context.logger.info("Environments for ${client.url} are now initialized")
127+
isInitialized.update {
128+
true
129+
}
130+
}
125131

126132
lastEnvironments = resolvedEnvironments
127133
} catch (_: CancellationException) {
@@ -171,14 +177,14 @@ class CoderRemoteProvider(
171177
/**
172178
* Cancel polling and clear the client and environments.
173179
*
174-
* Called as part of our own logout but it is unclear where it is called by
175-
* Toolbox. Maybe on uninstall?
180+
* Also called as part of our own logout.
176181
*/
177182
override fun close() {
178183
pollJob?.cancel()
179-
client = null
184+
client?.close()
180185
lastEnvironments = null
181186
environments.value = LoadableState.Value(emptyList())
187+
isInitialized.update { false }
182188
}
183189

184190
override val svgIcon: SvgIcon =
@@ -213,8 +219,7 @@ class CoderRemoteProvider(
213219
* Just displays the deployment URL at the moment, but we could use this as
214220
* a form for creating new environments.
215221
*/
216-
override fun getNewEnvironmentUiPage(): UiPage =
217-
NewEnvironmentPage(context, context.i18n.pnotr(getDeploymentURL()?.first ?: ""))
222+
override fun getNewEnvironmentUiPage(): UiPage = coderHeaderPage
218223

219224
/**
220225
* We always show a list of environments.
@@ -233,11 +238,13 @@ class CoderRemoteProvider(
233238
* Handle incoming links (like from the dashboard).
234239
*/
235240
override suspend fun handleUri(uri: URI) {
236-
val params = uri.toQueryParameters()
237-
context.cs.launch {
238-
val name = linkHandler.handle(params)
239-
// TODO@JB: Now what? How do we actually connect this workspace?
240-
context.logger.debug("External request for $name: $uri")
241+
linkHandler.handle(uri, shouldDoAutoLogin()) { restClient, cli ->
242+
// stop polling and de-initialize resources
243+
close()
244+
// start initialization with the new settings
245+
this@CoderRemoteProvider.client = restClient
246+
coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(restClient.url.toString()))
247+
pollJob = poll(restClient, cli)
241248
}
242249
}
243250

@@ -263,7 +270,7 @@ class CoderRemoteProvider(
263270
// Show sign in page if we have not configured the client yet.
264271
if (client == null) {
265272
// When coming back to the application, authenticate immediately.
266-
val autologin = firstRun && secrets.rememberMe == "true"
273+
val autologin = shouldDoAutoLogin()
267274
var autologinEx: Exception? = null
268275
secrets.lastToken.let { lastToken ->
269276
secrets.lastDeploymentURL.let { lastDeploymentURL ->
@@ -302,6 +309,8 @@ class CoderRemoteProvider(
302309
return null
303310
}
304311

312+
private fun shouldDoAutoLogin(): Boolean = firstRun && secrets.rememberMe == "true"
313+
305314
/**
306315
* Create a connect page that starts polling and resets the UI on success.
307316
*/

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

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.jetbrains.toolbox.api.core.PluginSecretStore
44
import com.jetbrains.toolbox.api.core.PluginSettingsStore
55
import com.jetbrains.toolbox.api.core.diagnostics.Logger
66
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
7+
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
78
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
89
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
910
import com.jetbrains.toolbox.api.ui.ToolboxUi
@@ -13,6 +14,7 @@ data class CoderToolboxContext(
1314
val ui: ToolboxUi,
1415
val envPageManager: EnvironmentUiPageManager,
1516
val envStateColorPalette: EnvironmentStateColorPalette,
17+
val ideOrchestrator: ClientHelper,
1618
val cs: CoroutineScope,
1719
val logger: Logger,
1820
val i18n: LocalizableStringFactory,

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

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.jetbrains.toolbox.api.core.diagnostics.Logger
77
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
88
import com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension
99
import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
10+
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
1011
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
1112
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
1213
import com.jetbrains.toolbox.api.ui.ToolboxUi
@@ -24,6 +25,7 @@ class CoderToolboxExtension : RemoteDevExtension {
2425
serviceLocator.getService(ToolboxUi::class.java),
2526
serviceLocator.getService(EnvironmentUiPageManager::class.java),
2627
serviceLocator.getService(EnvironmentStateColorPalette::class.java),
28+
serviceLocator.getService(ClientHelper::class.java),
2729
serviceLocator.getService(CoroutineScope::class.java),
2830
serviceLocator.getService(Logger::class.java),
2931
serviceLocator.getService(LocalizableStringFactory::class.java),

src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt

+2-3
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ fun ensureCLI(
6060
deploymentURL: URL,
6161
buildVersion: String,
6262
settings: CoderSettings,
63-
indicator: ((t: String) -> Unit)? = null,
6463
): CoderCLIManager {
6564
val cli = CoderCLIManager(deploymentURL, context.logger, settings)
6665

@@ -76,7 +75,7 @@ fun ensureCLI(
7675

7776
// If downloads are enabled download the new version.
7877
if (settings.enableDownloads) {
79-
indicator?.invoke("Downloading Coder CLI...")
78+
context.logger.info("Downloading Coder CLI...")
8079
try {
8180
cli.download()
8281
return cli
@@ -98,7 +97,7 @@ fun ensureCLI(
9897
}
9998

10099
if (settings.enableDownloads) {
101-
indicator?.invoke("Downloading Coder CLI...")
100+
context.logger.info("Downloading Coder CLI...")
102101
dataCLI.download()
103102
return dataCLI
104103
}

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

+21
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,19 @@ open class CoderRestClient(
169169
return workspacesResponse.body()!!.workspaces
170170
}
171171

172+
/**
173+
* Retrieves a workspace with the provided id.
174+
* @throws [APIResponseException].
175+
*/
176+
fun workspace(workspaceID: UUID): Workspace {
177+
val workspacesResponse = retroRestClient.workspace(workspaceID).execute()
178+
if (!workspacesResponse.isSuccessful) {
179+
throw APIResponseException("retrieve workspace", url, workspacesResponse)
180+
}
181+
182+
return workspacesResponse.body()!!
183+
}
184+
172185
/**
173186
* Retrieves all the agent names for all workspaces, including those that
174187
* are off. Meant to be used when configuring SSH.
@@ -272,4 +285,12 @@ open class CoderRestClient(
272285
}
273286
return buildResponse.body()!!
274287
}
288+
289+
fun close() {
290+
httpClient.apply {
291+
dispatcher.executorService.shutdown()
292+
connectionPool.evictAll()
293+
cache?.close()
294+
}
295+
}
275296
}

src/main/kotlin/com/coder/toolbox/sdk/v2/CoderV2RestFacade.kt

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.coder.toolbox.sdk.v2.models.BuildInfo
44
import com.coder.toolbox.sdk.v2.models.CreateWorkspaceBuildRequest
55
import com.coder.toolbox.sdk.v2.models.Template
66
import com.coder.toolbox.sdk.v2.models.User
7+
import com.coder.toolbox.sdk.v2.models.Workspace
78
import com.coder.toolbox.sdk.v2.models.WorkspaceBuild
89
import com.coder.toolbox.sdk.v2.models.WorkspaceResource
910
import com.coder.toolbox.sdk.v2.models.WorkspacesResponse
@@ -30,6 +31,14 @@ interface CoderV2RestFacade {
3031
@Query("q") searchParams: String,
3132
): Call<WorkspacesResponse>
3233

34+
/**
35+
* Retrieves a workspace with the provided id.
36+
*/
37+
@GET("api/v2/workspaces/{workspaceID}")
38+
fun workspace(
39+
@Path("workspaceID") workspaceID: UUID
40+
): Call<Workspace>
41+
3342
@GET("api/v2/buildinfo")
3443
fun buildInfo(): Call<BuildInfo>
3544

0 commit comments

Comments
 (0)