Skip to content

Commit 7f9e1b3

Browse files
authored
Fix Coder connect workflows (#2)
- fixes login screen and glitches related jumping to the workspaces page once the connection to Coder deployment is estabilished. - support for opening URLs in browser - fixes user agent reporting - fixes connection status rendering
1 parent 04f1bd8 commit 7f9e1b3

39 files changed

+325
-170
lines changed

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

-28
This file was deleted.

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

+27-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package com.coder.toolbox
22

3+
import com.coder.toolbox.browser.BrowserUtil
34
import com.coder.toolbox.models.WorkspaceAndAgentStatus
45
import com.coder.toolbox.sdk.CoderRestClient
56
import com.coder.toolbox.sdk.v2.models.Workspace
67
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
8+
import com.coder.toolbox.util.withPath
79
import com.coder.toolbox.views.Action
810
import com.coder.toolbox.views.EnvironmentView
11+
import com.jetbrains.toolbox.api.core.ServiceLocator
912
import com.jetbrains.toolbox.api.remoteDev.AbstractRemoteProviderEnvironment
1013
import com.jetbrains.toolbox.api.remoteDev.EnvironmentVisibilityState
1114
import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView
1215
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateConsumer
1316
import com.jetbrains.toolbox.api.ui.ToolboxUi
17+
import kotlinx.coroutines.CoroutineScope
18+
import kotlinx.coroutines.launch
1419
import java.util.concurrent.CompletableFuture
1520

1621
/**
@@ -19,32 +24,44 @@ import java.util.concurrent.CompletableFuture
1924
* Used in the environment list view.
2025
*/
2126
class CoderRemoteEnvironment(
27+
private val serviceLocator: ServiceLocator,
2228
private val client: CoderRestClient,
2329
private var workspace: Workspace,
2430
private var agent: WorkspaceAgent,
25-
private val ui: ToolboxUi,
31+
private var cs: CoroutineScope,
2632
) : AbstractRemoteProviderEnvironment() {
33+
private var status = WorkspaceAndAgentStatus.from(workspace, agent)
34+
35+
private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java)
2736
override fun getId(): String = "${workspace.name}.${agent.name}"
2837
override fun getName(): String = "${workspace.name}.${agent.name}"
29-
private var status = WorkspaceAndAgentStatus.from(workspace, agent)
3038

3139
init {
3240
actionsList.add(
3341
Action("Open web terminal") {
34-
// TODO - check this later
35-
// ui.openUrl(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString())
42+
cs.launch {
43+
BrowserUtil.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) {
44+
ui.showErrorInfoPopup(it)
45+
}
46+
}
3647
},
3748
)
3849
actionsList.add(
3950
Action("Open in dashboard") {
40-
// TODO - check this later
41-
// ui.openUrl(client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString())
51+
cs.launch {
52+
BrowserUtil.browse(client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()) {
53+
ui.showErrorInfoPopup(it)
54+
}
55+
}
4256
},
4357
)
4458
actionsList.add(
4559
Action("View template") {
46-
// TODO - check this later
47-
// ui.openUrl(client.url.withPath("/templates/${workspace.templateName}").toString())
60+
cs.launch {
61+
BrowserUtil.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) {
62+
ui.showErrorInfoPopup(it)
63+
}
64+
}
4865
},
4966
)
5067
actionsList.add(
@@ -79,7 +96,7 @@ class CoderRemoteEnvironment(
7996
val newStatus = WorkspaceAndAgentStatus.from(workspace, agent)
8097
if (newStatus != status) {
8198
status = newStatus
82-
val state = status.toRemoteEnvironmentState()
99+
val state = status.toRemoteEnvironmentState(serviceLocator)
83100
listenerSet.forEach { it.consume(state) }
84101
}
85102
}
@@ -108,7 +125,7 @@ class CoderRemoteEnvironment(
108125
// connected state can mask the workspace state.
109126
// TODO@JB: You can still press connect if the environment is
110127
// unreachable. Is that expected?
111-
consumer.consume(status.toRemoteEnvironmentState())
128+
consumer.consume(status.toRemoteEnvironmentState(serviceLocator))
112129
return super.addStateListener(consumer)
113130
}
114131

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

+17-15
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ import com.coder.toolbox.views.SignInPage
1818
import com.coder.toolbox.views.TokenPage
1919
import com.jetbrains.toolbox.api.core.PluginSecretStore
2020
import com.jetbrains.toolbox.api.core.PluginSettingsStore
21+
import com.jetbrains.toolbox.api.core.ServiceLocator
2122
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
2223
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
2324
import com.jetbrains.toolbox.api.remoteDev.RemoteEnvironmentConsumer
2425
import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
26+
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
2527
import com.jetbrains.toolbox.api.ui.ToolboxUi
2628
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
2729
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField
@@ -39,15 +41,17 @@ import kotlin.coroutines.cancellation.CancellationException
3941
import kotlin.time.Duration.Companion.seconds
4042

4143
class CoderRemoteProvider(
44+
private val serviceLocator: ServiceLocator,
4245
private val httpClient: OkHttpClient,
43-
private val consumer: RemoteEnvironmentConsumer,
44-
private val coroutineScope: CoroutineScope,
45-
private val ui: ToolboxUi,
46-
settingsStore: PluginSettingsStore,
47-
secretsStore: PluginSecretStore,
4846
) : RemoteProvider {
4947
private val logger = LoggerFactory.getLogger(javaClass)
5048

49+
private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java)
50+
private val consumer: RemoteEnvironmentConsumer = serviceLocator.getService(RemoteEnvironmentConsumer::class.java)
51+
private val coroutineScope: CoroutineScope = serviceLocator.getService(CoroutineScope::class.java)
52+
private val settingsStore: PluginSettingsStore = serviceLocator.getService(PluginSettingsStore::class.java)
53+
private val secretsStore: PluginSecretStore = serviceLocator.getService(PluginSecretStore::class.java)
54+
5155
// Current polling job.
5256
private var pollJob: Job? = null
5357
private var lastEnvironments: Set<CoderRemoteEnvironment>? = null
@@ -60,7 +64,7 @@ class CoderRemoteProvider(
6064
private val dialogUi = DialogUi(settings, ui)
6165
private val linkHandler = LinkHandler(settings, httpClient, dialogUi)
6266

63-
// The REST client, if we are signed in.
67+
// The REST client, if we are signed in
6468
private var client: CoderRestClient? = null
6569

6670
// If we have an error in the polling we store it here before going back to
@@ -96,7 +100,7 @@ class CoderRemoteProvider(
96100
it.name
97101
}?.map { agent ->
98102
// If we have an environment already, update that.
99-
val env = CoderRemoteEnvironment(client, ws, agent, ui)
103+
val env = CoderRemoteEnvironment(serviceLocator, client, ws, agent, coroutineScope)
100104
lastEnvironments?.firstOrNull { it == env }?.let {
101105
it.update(ws, agent)
102106
it
@@ -146,7 +150,6 @@ class CoderRemoteProvider(
146150
// rememberMe to false so we do not try to automatically log in.
147151
secrets.rememberMe = "false"
148152
close()
149-
reset()
150153
}
151154

152155
/**
@@ -182,7 +185,7 @@ class CoderRemoteProvider(
182185
consumer.consumeEnvironments(emptyList(), true)
183186
}
184187

185-
override fun getName(): String = "Coder Gateway"
188+
override fun getName(): String = "Coder"
186189
override fun getSvgIcon(): SvgIcon =
187190
SvgIcon(this::class.java.getResourceAsStream("/icon.svg")?.readAllBytes() ?: byteArrayOf())
188191

@@ -208,7 +211,7 @@ class CoderRemoteProvider(
208211
* Just displays the deployment URL at the moment, but we could use this as
209212
* a form for creating new environments.
210213
*/
211-
override fun getNewEnvironmentUiPage(): UiPage = NewEnvironmentPage(client?.url?.toString())
214+
override fun getNewEnvironmentUiPage(): UiPage = NewEnvironmentPage(getDeploymentURL()?.first)
212215

213216
/**
214217
* We always show a list of environments.
@@ -251,9 +254,8 @@ class CoderRemoteProvider(
251254
* ui.hideUiPage() which stacks and has built-in back navigation, rather
252255
* than using multiple root pages.
253256
*/
254-
private fun reset() {
255-
// TODO - check this later
256-
// ui.showPluginEnvironmentsPage()
257+
private fun goToEnvironmentsPage() {
258+
serviceLocator.getService(EnvironmentUiPageManager::class.java).showPluginEnvironmentsPage()
257259
}
258260

259261
/**
@@ -309,7 +311,7 @@ class CoderRemoteProvider(
309311
settings,
310312
httpClient,
311313
coroutineScope,
312-
{ reset() },
314+
::goToEnvironmentsPage,
313315
) { client, cli ->
314316
// Store the URL and token for use next time.
315317
secrets.lastDeploymentURL = client.url.toString()
@@ -320,7 +322,7 @@ class CoderRemoteProvider(
320322
pollError = null
321323
pollJob?.cancel()
322324
pollJob = poll(client, cli)
323-
reset()
325+
goToEnvironmentsPage()
324326
}
325327

326328
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.coder.toolbox
2+
3+
import com.jetbrains.toolbox.api.core.ServiceLocator
4+
import com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension
5+
import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
6+
import okhttp3.OkHttpClient
7+
8+
/**
9+
* Entry point into the extension.
10+
*/
11+
class CoderToolboxExtension : RemoteDevExtension {
12+
// All services must be passed in here and threaded as necessary.
13+
override fun createRemoteProviderPluginInstance(serviceLocator: ServiceLocator): RemoteProvider {
14+
return CoderRemoteProvider(
15+
serviceLocator,
16+
OkHttpClient(),
17+
)
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.coder.toolbox.browser
2+
3+
import java.io.IOException
4+
5+
class BrowserException(msg: String, error: Throwable? = null) : IOException(msg, error)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.coder.toolbox.browser
2+
3+
import com.coder.toolbox.util.OS
4+
import com.coder.toolbox.util.getOS
5+
import org.zeroturnaround.exec.ProcessExecutor
6+
7+
class BrowserUtil {
8+
companion object {
9+
fun browse(url: String, errorHandler: (BrowserException) -> Unit) {
10+
val os = getOS()
11+
if (os == null) {
12+
errorHandler(BrowserException("Failed to open the URL because we can't detect the OS"))
13+
return
14+
}
15+
when (os) {
16+
OS.LINUX -> linuxBrowse(url, errorHandler)
17+
OS.MAC -> macBrowse(url, errorHandler)
18+
OS.WINDOWS -> windowsBrowse(url, errorHandler)
19+
}
20+
}
21+
22+
private fun linuxBrowse(url: String, errorHandler: (BrowserException) -> Unit) {
23+
try {
24+
if (OS.LINUX.getDesktopEnvironment()?.uppercase()?.contains("GNOME") == true) {
25+
exec("gnome-open", url)
26+
} else {
27+
exec("xdg-open", url)
28+
}
29+
} catch (e: Exception) {
30+
errorHandler(
31+
BrowserException(
32+
"Failed to open URL because an error was encountered. Please make sure xdg-open from package xdg-utils is available!",
33+
e
34+
)
35+
)
36+
}
37+
}
38+
39+
private fun macBrowse(url: String, errorHandler: (BrowserException) -> Unit) {
40+
try {
41+
exec("open", url)
42+
} catch (e: Exception) {
43+
errorHandler(BrowserException("Failed to open URL because an error was encountered.", e))
44+
}
45+
}
46+
47+
private fun windowsBrowse(url: String, errorHandler: (BrowserException) -> Unit) {
48+
try {
49+
exec("cmd", "start \"$url\"")
50+
} catch (e: Exception) {
51+
errorHandler(BrowserException("Failed to open URL because an error was encountered.", e))
52+
}
53+
}
54+
55+
private fun exec(vararg args: String): String {
56+
val stdout =
57+
ProcessExecutor()
58+
.command(*args)
59+
.exitValues(0)
60+
.readOutput(true)
61+
.execute()
62+
.outputUTF8()
63+
return stdout
64+
}
65+
}
66+
}

src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt

+16-15
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
55
import com.coder.toolbox.sdk.v2.models.WorkspaceAgentLifecycleState
66
import com.coder.toolbox.sdk.v2.models.WorkspaceAgentStatus
77
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
8-
import com.jetbrains.toolbox.api.core.ui.color.Color
8+
import com.jetbrains.toolbox.api.core.ServiceLocator
99
import com.jetbrains.toolbox.api.core.ui.color.StateColor
10-
import com.jetbrains.toolbox.api.core.ui.color.ThemeColor
1110
import com.jetbrains.toolbox.api.remoteDev.states.CustomRemoteEnvironmentState
11+
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
12+
import com.jetbrains.toolbox.api.remoteDev.states.StandardRemoteEnvironmentState
1213

1314
/**
1415
* WorkspaceAndAgentStatus represents the combined status of a single agent and
@@ -57,27 +58,27 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) {
5758
* Note that a reachable environment will always display "connected" or
5859
* "disconnected" regardless of the label we give that status.
5960
*/
60-
fun toRemoteEnvironmentState(): CustomRemoteEnvironmentState {
61-
// Use comments; no named arguments for non-Kotlin functions.
62-
// TODO@JB: Is there a set of default colors we could use?
61+
fun toRemoteEnvironmentState(serviceLocator: ServiceLocator): CustomRemoteEnvironmentState {
62+
val stateColor = getStateColor(serviceLocator)
6363
return CustomRemoteEnvironmentState(
6464
label,
65-
StateColor(
66-
ThemeColor(
67-
Color(0.407f, 0.439f, 0.502f, 1.0f), // lightThemeColor
68-
Color(0.784f, 0.784f, 0.784f, 0.784f), // darkThemeColor
69-
),
70-
ThemeColor(
71-
Color(0.878f, 0.878f, 0.941f, 0.102f), // darkThemeBackgroundColor
72-
Color(0.878f, 0.878f, 0.961f, 0.980f), // lightThemeBackgroundColor
73-
)
74-
),
65+
stateColor,
7566
ready(), // reachable
7667
// TODO@JB: How does this work? Would like a spinner for pending states.
7768
null, // iconId
7869
)
7970
}
8071

72+
private fun getStateColor(serviceLocator: ServiceLocator): StateColor {
73+
val colorPalette = serviceLocator.getService(EnvironmentStateColorPalette::class.java)
74+
75+
76+
return if (ready()) colorPalette.getColor(StandardRemoteEnvironmentState.Active)
77+
else if (canStart()) colorPalette.getColor(StandardRemoteEnvironmentState.Failed)
78+
else if (pending()) colorPalette.getColor(StandardRemoteEnvironmentState.Activating)
79+
else colorPalette.getColor(StandardRemoteEnvironmentState.Unreachable)
80+
}
81+
8182
/**
8283
* Return true if the agent is in a connectable state.
8384
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.coder.toolbox.plugin
2+
3+
import com.squareup.moshi.Json
4+
import com.squareup.moshi.JsonClass
5+
6+
/**
7+
* Small subset representation of extension.json
8+
*/
9+
@JsonClass(generateAdapter = true)
10+
data class PluginInfo(
11+
@Json(name = "id") val id: String,
12+
@Json(name = "version") val version: String)

0 commit comments

Comments
 (0)