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

Lines changed: 0 additions & 28 deletions
This file was deleted.

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

Lines changed: 27 additions & 10 deletions
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

Lines changed: 17 additions & 15 deletions
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
/**
Lines changed: 19 additions & 0 deletions
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+
}
Lines changed: 5 additions & 0 deletions
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)
Lines changed: 66 additions & 0 deletions
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

Lines changed: 16 additions & 15 deletions
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
*/
Lines changed: 12 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy