Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d0af2c9

Browse files
committedMar 4, 2025
impl: support for Toolbox 2.6.0.38881 (3)
- fix initial batch of compiler errors - mostly strings converted to LocalizedString instances - while some observable properties are now moved to Kotlin's StateFlow
1 parent 059f3fc commit d0af2c9

File tree

6 files changed

+186
-98
lines changed

6 files changed

+186
-98
lines changed
 

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

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@ import com.coder.toolbox.util.withPath
1010
import com.coder.toolbox.views.Action
1111
import com.coder.toolbox.views.EnvironmentView
1212
import com.jetbrains.toolbox.api.core.ServiceLocator
13-
import com.jetbrains.toolbox.api.remoteDev.AbstractRemoteProviderEnvironment
13+
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
1414
import com.jetbrains.toolbox.api.remoteDev.EnvironmentVisibilityState
15+
import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment
1516
import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView
16-
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateConsumer
17+
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription
18+
import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState
1719
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
1820
import com.jetbrains.toolbox.api.ui.ToolboxUi
21+
import com.jetbrains.toolbox.api.ui.actions.ActionDescription
1922
import kotlinx.coroutines.CoroutineScope
2023
import kotlinx.coroutines.delay
24+
import kotlinx.coroutines.flow.MutableStateFlow
25+
import kotlinx.coroutines.flow.StateFlow
2126
import kotlinx.coroutines.isActive
2227
import kotlinx.coroutines.launch
2328
import kotlinx.coroutines.withTimeout
@@ -35,63 +40,58 @@ class CoderRemoteEnvironment(
3540
private var workspace: Workspace,
3641
private var agent: WorkspaceAgent,
3742
private var cs: CoroutineScope,
38-
) : AbstractRemoteProviderEnvironment("${workspace.name}.${agent.name}") {
43+
) : RemoteProviderEnvironment("${workspace.name}.${agent.name}") {
3944
private var status = WorkspaceAndAgentStatus.from(workspace, agent)
4045

4146
private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java)
47+
private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java)
4248

4349
override var name: String = "${workspace.name}.${agent.name}"
50+
override val state: StateFlow<RemoteEnvironmentState>
51+
get() = TODO("Not yet implemented")
52+
override val description: StateFlow<EnvironmentDescription>
53+
get() = TODO("Not yet implemented")
4454

45-
init {
46-
actionsList.add(
47-
Action("Open web terminal") {
55+
override val actionsList: StateFlow<List<ActionDescription>> = MutableStateFlow(
56+
listOf(
57+
Action(i18n.ptrl("Open web terminal")) {
4858
cs.launch {
4959
BrowserUtil.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) {
5060
ui.showErrorInfoPopup(it)
5161
}
5262
}
5363
},
54-
)
55-
actionsList.add(
56-
Action("Open in dashboard") {
64+
Action(i18n.ptrl("Open in dashboard")) {
5765
cs.launch {
5866
BrowserUtil.browse(client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()) {
5967
ui.showErrorInfoPopup(it)
6068
}
6169
}
6270
},
63-
)
64-
actionsList.add(
65-
Action("View template") {
71+
72+
Action(i18n.ptrl("View template")) {
6673
cs.launch {
6774
BrowserUtil.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) {
6875
ui.showErrorInfoPopup(it)
6976
}
7077
}
7178
},
72-
)
73-
actionsList.add(
74-
Action("Start", enabled = { status.canStart() }) {
79+
Action(i18n.ptrl("Start"), enabled = { status.canStart() }) {
7580
val build = client.startWorkspace(workspace)
7681
workspace = workspace.copy(latestBuild = build)
7782
update(workspace, agent)
7883
},
79-
)
80-
actionsList.add(
81-
Action("Stop", enabled = { status.canStop() }) {
84+
Action(i18n.ptrl("Stop"), enabled = { status.canStop() }) {
8285
val build = client.stopWorkspace(workspace)
8386
workspace = workspace.copy(latestBuild = build)
8487
update(workspace, agent)
8588
},
86-
)
87-
actionsList.add(
88-
Action("Update", enabled = { workspace.outdated }) {
89+
Action(i18n.ptrl("Update"), enabled = { workspace.outdated }) {
8990
val build = client.updateWorkspace(workspace)
9091
workspace = workspace.copy(latestBuild = build)
9192
update(workspace, agent)
92-
},
93-
)
94-
}
93+
})
94+
)
9595

9696
/**
9797
* Update the workspace/agent status to the listeners, if it has changed.
@@ -103,15 +103,16 @@ class CoderRemoteEnvironment(
103103
if (newStatus != status) {
104104
status = newStatus
105105
val state = status.toRemoteEnvironmentState(serviceLocator)
106-
listenerSet.forEach { it.consume(state) }
106+
// listenerSet.forEach { it.consume(state) }
107107
}
108108
}
109109

110110
/**
111111
* The contents are provided by the SSH view provided by Toolbox, all we
112112
* have to do is provide it a host name.
113113
*/
114-
override suspend fun getContentsView(): EnvironmentContentsView = EnvironmentView(client.url, workspace, agent)
114+
override suspend
115+
fun getContentsView(): EnvironmentContentsView = EnvironmentView(client.url, workspace, agent)
115116

116117
/**
117118
* Does nothing. In theory, we could do something like start the workspace
@@ -124,33 +125,33 @@ class CoderRemoteEnvironment(
124125
/**
125126
* Immediately send the state to the listener and store for updates.
126127
*/
127-
override fun addStateListener(consumer: EnvironmentStateConsumer): Boolean {
128-
// TODO@JB: It would be ideal if we could have the workspace state and
129-
// the connected state listed separately, since right now the
130-
// connected state can mask the workspace state.
131-
// TODO@JB: You can still press connect if the environment is
132-
// unreachable. Is that expected?
133-
consumer.consume(status.toRemoteEnvironmentState(serviceLocator))
134-
return super.addStateListener(consumer)
135-
}
128+
// override fun addStateListener(consumer: EnvironmentStateConsumer): Boolean {
129+
// // TODO@JB: It would be ideal if we could have the workspace state and
130+
// // the connected state listed separately, since right now the
131+
// // connected state can mask the workspace state.
132+
// // TODO@JB: You can still press connect if the environment is
133+
// // unreachable. Is that expected?
134+
// consumer.consume(status.toRemoteEnvironmentState(serviceLocator))
135+
// return super.addStateListener(consumer)
136+
// }
136137

137138
override fun onDelete() {
138139
cs.launch {
139140
// TODO info and cancel pop-ups only appear on the main page where all environments are listed.
140141
// However, #showSnackbar works on other pages. Until JetBrains fixes this issue we are going to use the snackbar
141142
val shouldDelete = if (status.canStop()) {
142143
ui.showOkCancelPopup(
143-
"Delete running workspace?",
144-
"Workspace will be closed and all the information in this workspace will be lost, including all files, unsaved changes and historical.",
145-
"Delete",
146-
"Cancel"
144+
i18n.ptrl("Delete running workspace?"),
145+
i18n.ptrl("Workspace will be closed and all the information in this workspace will be lost, including all files, unsaved changes and historical."),
146+
i18n.ptrl("Delete"),
147+
i18n.ptrl("Cancel")
147148
)
148149
} else {
149150
ui.showOkCancelPopup(
150-
"Delete workspace?",
151-
"All the information in this workspace will be lost, including all files, unsaved changes and historical.",
152-
"Delete",
153-
"Cancel"
151+
i18n.ptrl("Delete workspace?"),
152+
i18n.ptrl("All the information in this workspace will be lost, including all files, unsaved changes and historical."),
153+
i18n.ptrl("Delete"),
154+
i18n.ptrl("Cancel")
154155
)
155156
}
156157
if (shouldDelete) {

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

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,29 @@ import com.jetbrains.toolbox.api.core.PluginSecretStore
2121
import com.jetbrains.toolbox.api.core.PluginSettingsStore
2222
import com.jetbrains.toolbox.api.core.ServiceLocator
2323
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
24+
import com.jetbrains.toolbox.api.core.util.LoadableState
25+
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
2426
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
25-
import com.jetbrains.toolbox.api.remoteDev.RemoteEnvironmentConsumer
2627
import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
28+
import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment
2729
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
2830
import com.jetbrains.toolbox.api.ui.ToolboxUi
29-
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
30-
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField
31+
import com.jetbrains.toolbox.api.ui.actions.ActionDescription
3132
import com.jetbrains.toolbox.api.ui.components.UiPage
3233
import kotlinx.coroutines.CoroutineScope
3334
import kotlinx.coroutines.Job
3435
import kotlinx.coroutines.delay
36+
import kotlinx.coroutines.flow.MutableStateFlow
37+
import kotlinx.coroutines.flow.StateFlow
3538
import kotlinx.coroutines.isActive
3639
import kotlinx.coroutines.launch
3740
import okhttp3.OkHttpClient
3841
import java.net.URI
3942
import java.net.URL
4043
import kotlin.coroutines.cancellation.CancellationException
4144
import kotlin.time.Duration.Companion.seconds
45+
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as DropDownMenu
46+
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as dropDownFactory
4247

4348
class CoderRemoteProvider(
4449
private val serviceLocator: ServiceLocator,
@@ -47,10 +52,10 @@ class CoderRemoteProvider(
4752
private val logger = CoderLoggerFactory.getLogger(javaClass)
4853

4954
private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java)
50-
private val consumer: RemoteEnvironmentConsumer = serviceLocator.getService(RemoteEnvironmentConsumer::class.java)
5155
private val coroutineScope: CoroutineScope = serviceLocator.getService(CoroutineScope::class.java)
5256
private val settingsStore: PluginSettingsStore = serviceLocator.getService(PluginSettingsStore::class.java)
5357
private val secretsStore: PluginSecretStore = serviceLocator.getService(PluginSecretStore::class.java)
58+
private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java)
5459

5560
// Current polling job.
5661
private var pollJob: Job? = null
@@ -61,8 +66,8 @@ class CoderRemoteProvider(
6166
private val settings: CoderSettings = CoderSettings(settingsService)
6267
private val secrets: CoderSecretsService = CoderSecretsService(secretsStore)
6368
private val settingsPage: CoderSettingsPage = CoderSettingsPage(settingsService)
64-
private val dialogUi = DialogUi(settings, ui)
65-
private val linkHandler = LinkHandler(settings, httpClient, dialogUi)
69+
private val dialogUi = DialogUi(serviceLocator, settings)
70+
private val linkHandler = LinkHandler(serviceLocator, settings, httpClient, dialogUi)
6671

6772
// The REST client, if we are signed in
6873
private var client: CoderRestClient? = null
@@ -75,6 +80,10 @@ class CoderRemoteProvider(
7580
// On the first load, automatically log in if we can.
7681
private var firstRun = true
7782

83+
override val environments: MutableStateFlow<LoadableState<List<RemoteProviderEnvironment>>> = MutableStateFlow(
84+
LoadableState.Loading
85+
)
86+
7887
/**
7988
* With the provided client, start polling for workspaces. Every time a new
8089
* workspace is added, reconfigure SSH using the provided cli (including the
@@ -84,7 +93,7 @@ class CoderRemoteProvider(
8493
while (isActive) {
8594
try {
8695
logger.debug("Fetching workspace agents from {}", client.url)
87-
val environments = client.workspaces().flatMap { ws ->
96+
val resolvedEnvironments = client.workspaces().flatMap { ws ->
8897
// Agents are not included in workspaces that are off
8998
// so fetch them separately.
9099
when (ws.latestBuild.status) {
@@ -117,16 +126,16 @@ class CoderRemoteProvider(
117126
// Reconfigure if a new environment is found.
118127
// TODO@JB: Should we use the add/remove listeners instead?
119128
val newEnvironments = lastEnvironments
120-
?.let { environments.subtract(it) }
121-
?: environments
129+
?.let { resolvedEnvironments.subtract(it) }
130+
?: resolvedEnvironments
122131
if (newEnvironments.isNotEmpty()) {
123132
logger.info("Found new environment(s), reconfiguring CLI: {}", newEnvironments)
124133
cli.configSsh(newEnvironments.map { it.name }.toSet())
125134
}
126135

127-
consumer.consumeEnvironments(environments, true)
136+
environments.value = LoadableState.Value(resolvedEnvironments.toList())
128137

129-
lastEnvironments = environments
138+
lastEnvironments = resolvedEnvironments
130139
} catch (_: CancellationException) {
131140
logger.debug("{} polling loop canceled", client.url)
132141
break
@@ -155,21 +164,20 @@ class CoderRemoteProvider(
155164
/**
156165
* A dropdown that appears at the top of the environment list to the right.
157166
*/
158-
override fun getAccountDropDown(): AccountDropdownField? {
167+
override fun getAccountDropDown(): DropDownMenu? {
159168
val username = client?.me?.username
160169
if (username != null) {
161-
return AccountDropdownField(username, Runnable { logout() })
170+
return dropDownFactory(i18n.pnotr(username), { logout() })
162171
}
163172
return null
164173
}
165174

166-
/**
167-
* List of actions that appear next to the account.
168-
*/
169-
override fun getAdditionalPluginActions(): List<RunnableActionDescription> = listOf(
170-
Action("Settings", closesPage = false) {
171-
ui.showUiPage(settingsPage)
172-
},
175+
override val additionalPluginActions: StateFlow<List<ActionDescription>> = MutableStateFlow(
176+
listOf(
177+
Action(i18n.ptrl("Settings")) {
178+
ui.showUiPage(settingsPage)
179+
},
180+
)
173181
)
174182

175183
/**
@@ -182,7 +190,7 @@ class CoderRemoteProvider(
182190
pollJob?.cancel()
183191
client = null
184192
lastEnvironments = null
185-
consumer.consumeEnvironments(emptyList(), true)
193+
environments.value = LoadableState.Value(emptyList())
186194
}
187195

188196
override val svgIcon: SvgIcon =
@@ -226,20 +234,10 @@ class CoderRemoteProvider(
226234
*/
227235
override fun setVisible(visibilityState: ProviderVisibilityState) {}
228236

229-
/**
230-
* Ignored; unsure if we should use this over the consumer we get passed in.
231-
*/
232-
override fun addEnvironmentsListener(listener: RemoteEnvironmentConsumer) {}
233-
234-
/**
235-
* Ignored; unsure if we should use this over the consumer we get passed in.
236-
*/
237-
override fun removeEnvironmentsListener(listener: RemoteEnvironmentConsumer) {}
238-
239237
/**
240238
* Handle incoming links (like from the dashboard).
241239
*/
242-
override fun handleUri(uri: URI) {
240+
override suspend fun handleUri(uri: URI) {
243241
val params = uri.toQueryParameters()
244242
coroutineScope.launch {
245243
val name = linkHandler.handle(params)

‎src/main/kotlin/com/coder/toolbox/util/Dialogs.kt

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package com.coder.toolbox.util
33
import com.coder.toolbox.browser.BrowserUtil
44
import com.coder.toolbox.settings.CoderSettings
55
import com.coder.toolbox.settings.Source
6+
import com.jetbrains.toolbox.api.core.ServiceLocator
7+
import com.jetbrains.toolbox.api.localization.LocalizableString
8+
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
69
import com.jetbrains.toolbox.api.ui.ToolboxUi
710
import com.jetbrains.toolbox.api.ui.components.TextType
811
import java.net.URL
@@ -13,23 +16,33 @@ import java.net.URL
1316
* This is meant to mimic ToolboxUi.
1417
*/
1518
class DialogUi(
19+
private val serviceLocator: ServiceLocator,
1620
private val settings: CoderSettings,
17-
private val ui: ToolboxUi,
1821
) {
19-
suspend fun confirm(title: String, description: String): Boolean {
20-
return ui.showOkCancelPopup(title, description, "Yes", "No")
22+
private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java)
23+
private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java)
24+
25+
suspend fun confirm(title: LocalizableString, description: LocalizableString): Boolean {
26+
return ui.showOkCancelPopup(title, description, i18n.ptrl("Yes"), i18n.ptrl("No"))
2127
}
2228

2329
suspend fun ask(
24-
title: String,
25-
description: String,
26-
placeholder: String? = null,
30+
title: LocalizableString,
31+
description: LocalizableString,
32+
placeholder: LocalizableString? = null,
2733
// There is no link or error support in Toolbox so for now isError and
2834
// link are unused.
2935
isError: Boolean = false,
3036
link: Pair<String, String>? = null,
3137
): String? {
32-
return ui.showTextInputPopup(title, description, placeholder, TextType.General, "OK", "Cancel")
38+
return ui.showTextInputPopup(
39+
title,
40+
description,
41+
placeholder,
42+
TextType.General,
43+
i18n.ptrl("OK"),
44+
i18n.ptrl("Cancel")
45+
)
3346
}
3447

3548
private suspend fun openUrl(url: URL) {
@@ -79,11 +92,13 @@ class DialogUi(
7992
// for the token.
8093
val tokenFromUser =
8194
ask(
82-
title = "Session Token",
83-
description = error
95+
title = i18n.ptrl("Session Token"),
96+
description = i18n.pnotr(
97+
error
8498
?: token?.second?.description("token")
85-
?: "No existing token for ${url.host} found.",
86-
placeholder = token?.first,
99+
?: "No existing token for ${url.host} found."
100+
),
101+
placeholder = token?.first?.let { i18n.pnotr(it) },
87102
link = Pair("Session Token:", getTokenUrl.toString()),
88103
isError = error != null,
89104
)
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Failed to load comments.