Skip to content

Commit d75d0c7

Browse files
committed
refactor: centralize all toolbox services in a context
- service dependencies were resolved all over the place making refactoring harder - it promoted implicit, hidden dependencies - and also introduced tighter coupling between components. - in some cases we had to provide some i18n strings from upstream because the localization service was not available in the constructor. With this patch we resolve all the needed services during plugin load, wrap them in a context and inject the context via the constructor. It is now easier to refactor and the number of constructor parameters has been reduced.
1 parent cce41b0 commit d75d0c7

13 files changed

+196
-218
lines changed

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

+32-42
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,12 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
99
import com.coder.toolbox.util.withPath
1010
import com.coder.toolbox.views.Action
1111
import com.coder.toolbox.views.EnvironmentView
12-
import com.jetbrains.toolbox.api.core.ServiceLocator
13-
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
1412
import com.jetbrains.toolbox.api.remoteDev.EnvironmentVisibilityState
1513
import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment
1614
import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView
1715
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription
1816
import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState
19-
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
20-
import com.jetbrains.toolbox.api.ui.ToolboxUi
2117
import com.jetbrains.toolbox.api.ui.actions.ActionDescription
22-
import kotlinx.coroutines.CoroutineScope
2318
import kotlinx.coroutines.delay
2419
import kotlinx.coroutines.flow.MutableStateFlow
2520
import kotlinx.coroutines.flow.StateFlow
@@ -36,58 +31,54 @@ import kotlin.time.Duration.Companion.seconds
3631
* Used in the environment list view.
3732
*/
3833
class CoderRemoteEnvironment(
39-
private val serviceLocator: ServiceLocator,
34+
private val context: CoderToolboxContext,
4035
private val client: CoderRestClient,
4136
private var workspace: Workspace,
4237
private var agent: WorkspaceAgent,
43-
private var cs: CoroutineScope,
4438
) : RemoteProviderEnvironment("${workspace.name}.${agent.name}") {
4539
private var wsRawStatus = WorkspaceAndAgentStatus.from(workspace, agent)
4640

47-
private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java)
48-
private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java)
49-
5041
override var name: String = "${workspace.name}.${agent.name}"
5142
override val state: MutableStateFlow<RemoteEnvironmentState> =
52-
MutableStateFlow(wsRawStatus.toRemoteEnvironmentState(serviceLocator))
43+
MutableStateFlow(wsRawStatus.toRemoteEnvironmentState(context))
5344
override val description: MutableStateFlow<EnvironmentDescription> =
54-
MutableStateFlow(EnvironmentDescription.General(i18n.pnotr(workspace.templateName)))
45+
MutableStateFlow(EnvironmentDescription.General(context.i18n.pnotr(workspace.templateName)))
5546

5647
override val actionsList: StateFlow<List<ActionDescription>> = MutableStateFlow(
5748
listOf(
58-
Action(i18n.ptrl("Open web terminal")) {
59-
cs.launch {
49+
Action(context.i18n.ptrl("Open web terminal")) {
50+
context.cs.launch {
6051
BrowserUtil.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) {
61-
ui.showErrorInfoPopup(it)
52+
context.ui.showErrorInfoPopup(it)
6253
}
6354
}
6455
},
65-
Action(i18n.ptrl("Open in dashboard")) {
66-
cs.launch {
56+
Action(context.i18n.ptrl("Open in dashboard")) {
57+
context.cs.launch {
6758
BrowserUtil.browse(client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()) {
68-
ui.showErrorInfoPopup(it)
59+
context.ui.showErrorInfoPopup(it)
6960
}
7061
}
7162
},
7263

73-
Action(i18n.ptrl("View template")) {
74-
cs.launch {
64+
Action(context.i18n.ptrl("View template")) {
65+
context.cs.launch {
7566
BrowserUtil.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) {
76-
ui.showErrorInfoPopup(it)
67+
context.ui.showErrorInfoPopup(it)
7768
}
7869
}
7970
},
80-
Action(i18n.ptrl("Start"), enabled = { wsRawStatus.canStart() }) {
71+
Action(context.i18n.ptrl("Start"), enabled = { wsRawStatus.canStart() }) {
8172
val build = client.startWorkspace(workspace)
8273
workspace = workspace.copy(latestBuild = build)
8374
update(workspace, agent)
8475
},
85-
Action(i18n.ptrl("Stop"), enabled = { wsRawStatus.canStop() }) {
76+
Action(context.i18n.ptrl("Stop"), enabled = { wsRawStatus.canStop() }) {
8677
val build = client.stopWorkspace(workspace)
8778
workspace = workspace.copy(latestBuild = build)
8879
update(workspace, agent)
8980
},
90-
Action(i18n.ptrl("Update"), enabled = { workspace.outdated }) {
81+
Action(context.i18n.ptrl("Update"), enabled = { workspace.outdated }) {
9182
val build = client.updateWorkspace(workspace)
9283
workspace = workspace.copy(latestBuild = build)
9384
update(workspace, agent)
@@ -101,9 +92,9 @@ class CoderRemoteEnvironment(
10192
this.workspace = workspace
10293
this.agent = agent
10394
wsRawStatus = WorkspaceAndAgentStatus.from(workspace, agent)
104-
cs.launch {
95+
context.cs.launch {
10596
state.update {
106-
wsRawStatus.toRemoteEnvironmentState(serviceLocator)
97+
wsRawStatus.toRemoteEnvironmentState(context)
10798
}
10899
}
109100
}
@@ -137,43 +128,42 @@ class CoderRemoteEnvironment(
137128
// }
138129

139130
override fun onDelete() {
140-
cs.launch {
131+
context.cs.launch {
141132
// TODO info and cancel pop-ups only appear on the main page where all environments are listed.
142133
// However, #showSnackbar works on other pages. Until JetBrains fixes this issue we are going to use the snackbar
143134
val shouldDelete = if (wsRawStatus.canStop()) {
144-
ui.showOkCancelPopup(
145-
i18n.ptrl("Delete running workspace?"),
146-
i18n.ptrl("Workspace will be closed and all the information in this workspace will be lost, including all files, unsaved changes and historical."),
147-
i18n.ptrl("Delete"),
148-
i18n.ptrl("Cancel")
135+
context.ui.showOkCancelPopup(
136+
context.i18n.ptrl("Delete running workspace?"),
137+
context.i18n.ptrl("Workspace will be closed and all the information in this workspace will be lost, including all files, unsaved changes and historical."),
138+
context.i18n.ptrl("Delete"),
139+
context.i18n.ptrl("Cancel")
149140
)
150141
} else {
151-
ui.showOkCancelPopup(
152-
i18n.ptrl("Delete workspace?"),
153-
i18n.ptrl("All the information in this workspace will be lost, including all files, unsaved changes and historical."),
154-
i18n.ptrl("Delete"),
155-
i18n.ptrl("Cancel")
142+
context.ui.showOkCancelPopup(
143+
context.i18n.ptrl("Delete workspace?"),
144+
context.i18n.ptrl("All the information in this workspace will be lost, including all files, unsaved changes and historical."),
145+
context.i18n.ptrl("Delete"),
146+
context.i18n.ptrl("Cancel")
156147
)
157148
}
158149
if (shouldDelete) {
159150
try {
160151
client.removeWorkspace(workspace)
161-
cs.launch {
152+
context.cs.launch {
162153
withTimeout(5.minutes) {
163154
var workspaceStillExists = true
164-
while (cs.isActive && workspaceStillExists) {
155+
while (context.cs.isActive && workspaceStillExists) {
165156
if (wsRawStatus == WorkspaceAndAgentStatus.DELETING || wsRawStatus == WorkspaceAndAgentStatus.DELETED) {
166157
workspaceStillExists = false
167-
serviceLocator.getService(EnvironmentUiPageManager::class.java)
168-
.showPluginEnvironmentsPage()
158+
context.envPageManager.showPluginEnvironmentsPage()
169159
} else {
170160
delay(1.seconds)
171161
}
172162
}
173163
}
174164
}
175165
} catch (e: APIResponseException) {
176-
ui.showErrorInfoPopup(e)
166+
context.ui.showErrorInfoPopup(e)
177167
}
178168
}
179169
}

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

+31-50
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.coder.toolbox
22

33
import com.coder.toolbox.cli.CoderCLIManager
4-
import com.coder.toolbox.logger.CoderLoggerFactory
54
import com.coder.toolbox.sdk.CoderRestClient
65
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
76
import com.coder.toolbox.services.CoderSecretsService
@@ -17,20 +16,13 @@ import com.coder.toolbox.views.ConnectPage
1716
import com.coder.toolbox.views.NewEnvironmentPage
1817
import com.coder.toolbox.views.SignInPage
1918
import com.coder.toolbox.views.TokenPage
20-
import com.jetbrains.toolbox.api.core.PluginSecretStore
21-
import com.jetbrains.toolbox.api.core.PluginSettingsStore
22-
import com.jetbrains.toolbox.api.core.ServiceLocator
2319
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
2420
import com.jetbrains.toolbox.api.core.util.LoadableState
25-
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
2621
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
2722
import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
2823
import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment
29-
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
30-
import com.jetbrains.toolbox.api.ui.ToolboxUi
3124
import com.jetbrains.toolbox.api.ui.actions.ActionDescription
3225
import com.jetbrains.toolbox.api.ui.components.UiPage
33-
import kotlinx.coroutines.CoroutineScope
3426
import kotlinx.coroutines.Job
3527
import kotlinx.coroutines.delay
3628
import kotlinx.coroutines.flow.MutableStateFlow
@@ -47,29 +39,20 @@ import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as DropDownM
4739
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as dropDownFactory
4840

4941
class CoderRemoteProvider(
50-
private val serviceLocator: ServiceLocator,
42+
private val context: CoderToolboxContext,
5143
private val httpClient: OkHttpClient,
5244
) : RemoteProvider("Coder") {
53-
private val logger = CoderLoggerFactory.getLogger(javaClass)
54-
55-
private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java)
56-
private val coroutineScope: CoroutineScope = serviceLocator.getService(CoroutineScope::class.java)
57-
private val settingsStore: PluginSettingsStore = serviceLocator.getService(PluginSettingsStore::class.java)
58-
private val secretsStore: PluginSecretStore = serviceLocator.getService(PluginSecretStore::class.java)
59-
private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java)
60-
6145
// Current polling job.
6246
private var pollJob: Job? = null
6347
private var lastEnvironments: Set<CoderRemoteEnvironment>? = null
6448

6549
// Create our services from the Toolbox ones.
66-
private val settingsService = CoderSettingsService(settingsStore)
50+
private val settingsService = CoderSettingsService(context.settingsStore)
6751
private val settings: CoderSettings = CoderSettings(settingsService)
68-
private val secrets: CoderSecretsService = CoderSecretsService(secretsStore)
69-
private val settingsPage: CoderSettingsPage =
70-
CoderSettingsPage(serviceLocator, settingsService, i18n.ptrl("Coder Settings"))
71-
private val dialogUi = DialogUi(serviceLocator, settings)
72-
private val linkHandler = LinkHandler(serviceLocator, settings, httpClient, dialogUi)
52+
private val secrets: CoderSecretsService = CoderSecretsService(context.secretsStore)
53+
private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, settingsService)
54+
private val dialogUi = DialogUi(context, settings)
55+
private val linkHandler = LinkHandler(context, settings, httpClient, dialogUi)
7356

7457
// The REST client, if we are signed in
7558
private var client: CoderRestClient? = null
@@ -91,10 +74,10 @@ class CoderRemoteProvider(
9174
* workspace is added, reconfigure SSH using the provided cli (including the
9275
* first time).
9376
*/
94-
private fun poll(client: CoderRestClient, cli: CoderCLIManager): Job = coroutineScope.launch {
77+
private fun poll(client: CoderRestClient, cli: CoderCLIManager): Job = context.cs.launch {
9578
while (isActive) {
9679
try {
97-
logger.debug("Fetching workspace agents from {}", client.url)
80+
context.logger.debug("Fetching workspace agents from ${client.url}")
9881
val resolvedEnvironments = client.workspaces().flatMap { ws ->
9982
// Agents are not included in workspaces that are off
10083
// so fetch them separately.
@@ -111,7 +94,7 @@ class CoderRemoteProvider(
11194
it.name
11295
}?.map { agent ->
11396
// If we have an environment already, update that.
114-
val env = CoderRemoteEnvironment(serviceLocator, client, ws, agent, coroutineScope)
97+
val env = CoderRemoteEnvironment(context, client, ws, agent)
11598
lastEnvironments?.firstOrNull { it == env }?.let {
11699
it.update(ws, agent)
117100
it
@@ -131,7 +114,7 @@ class CoderRemoteProvider(
131114
?.let { resolvedEnvironments.subtract(it) }
132115
?: resolvedEnvironments
133116
if (newEnvironments.isNotEmpty()) {
134-
logger.info("Found new environment(s), reconfiguring CLI: {}", newEnvironments)
117+
context.logger.info("Found new environment(s), reconfiguring CLI: $newEnvironments")
135118
cli.configSsh(newEnvironments.map { it.name }.toSet())
136119
}
137120

@@ -141,10 +124,10 @@ class CoderRemoteProvider(
141124

142125
lastEnvironments = resolvedEnvironments
143126
} catch (_: CancellationException) {
144-
logger.debug("{} polling loop canceled", client.url)
127+
context.logger.debug("${client.url} polling loop canceled")
145128
break
146129
} catch (ex: Exception) {
147-
logger.info("setting exception $ex")
130+
context.logger.info(ex, "workspace polling error encountered")
148131
pollError = ex
149132
logout()
150133
break
@@ -171,15 +154,15 @@ class CoderRemoteProvider(
171154
override fun getAccountDropDown(): DropDownMenu? {
172155
val username = client?.me?.username
173156
if (username != null) {
174-
return dropDownFactory(i18n.pnotr(username), { logout() })
157+
return dropDownFactory(context.i18n.pnotr(username), { logout() })
175158
}
176159
return null
177160
}
178161

179162
override val additionalPluginActions: StateFlow<List<ActionDescription>> = MutableStateFlow(
180163
listOf(
181-
Action(i18n.ptrl("Settings")) {
182-
ui.showUiPage(settingsPage)
164+
Action(context.i18n.ptrl("Settings")) {
165+
context.ui.showUiPage(settingsPage)
183166
},
184167
)
185168
)
@@ -224,7 +207,7 @@ class CoderRemoteProvider(
224207
* a form for creating new environments.
225208
*/
226209
override fun getNewEnvironmentUiPage(): UiPage =
227-
NewEnvironmentPage(serviceLocator, i18n.pnotr(getDeploymentURL()?.first ?: ""))
210+
NewEnvironmentPage(context, context.i18n.pnotr(getDeploymentURL()?.first ?: ""))
228211

229212
/**
230213
* We always show a list of environments.
@@ -244,10 +227,10 @@ class CoderRemoteProvider(
244227
*/
245228
override suspend fun handleUri(uri: URI) {
246229
val params = uri.toQueryParameters()
247-
coroutineScope.launch {
230+
context.cs.launch {
248231
val name = linkHandler.handle(params)
249232
// TODO@JB: Now what? How do we actually connect this workspace?
250-
logger.debug("External request for {}: {}", name, uri)
233+
context.logger.debug("External request for $name: $uri")
251234
}
252235
}
253236

@@ -260,7 +243,7 @@ class CoderRemoteProvider(
260243
* than using multiple root pages.
261244
*/
262245
private fun goToEnvironmentsPage() {
263-
serviceLocator.getService(EnvironmentUiPageManager::class.java).showPluginEnvironmentsPage()
246+
context.envPageManager.showPluginEnvironmentsPage()
264247
}
265248

266249
/**
@@ -290,18 +273,17 @@ class CoderRemoteProvider(
290273

291274
// Login flow.
292275
val signInPage =
293-
SignInPage(serviceLocator, i18n.ptrl("Sign In to Coder"), getDeploymentURL()) { deploymentURL ->
294-
ui.showUiPage(
295-
TokenPage(
296-
serviceLocator,
297-
i18n.ptrl("Enter your token"),
298-
deploymentURL,
299-
getToken(deploymentURL)
300-
) { selectedToken ->
301-
ui.showUiPage(createConnectPage(deploymentURL, selectedToken))
302-
},
303-
)
304-
}
276+
SignInPage(context, getDeploymentURL()) { deploymentURL ->
277+
context.ui.showUiPage(
278+
TokenPage(
279+
context,
280+
deploymentURL,
281+
getToken(deploymentURL)
282+
) { selectedToken ->
283+
context.ui.showUiPage(createConnectPage(deploymentURL, selectedToken))
284+
},
285+
)
286+
}
305287

306288
// We might have tried and failed to automatically log in.
307289
autologinEx?.let { signInPage.notify("Error logging in", it) }
@@ -317,12 +299,11 @@ class CoderRemoteProvider(
317299
* Create a connect page that starts polling and resets the UI on success.
318300
*/
319301
private fun createConnectPage(deploymentURL: URL, token: String?): ConnectPage = ConnectPage(
320-
serviceLocator,
302+
context,
321303
deploymentURL,
322304
token,
323305
settings,
324306
httpClient,
325-
i18n.ptrl("Connecting to Coder"),
326307
::goToEnvironmentsPage,
327308
) { client, cli ->
328309
// Store the URL and token for use next time.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.coder.toolbox
2+
3+
import com.jetbrains.toolbox.api.core.PluginSecretStore
4+
import com.jetbrains.toolbox.api.core.PluginSettingsStore
5+
import com.jetbrains.toolbox.api.core.diagnostics.Logger
6+
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
7+
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
8+
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
9+
import com.jetbrains.toolbox.api.ui.ToolboxUi
10+
import kotlinx.coroutines.CoroutineScope
11+
12+
data class CoderToolboxContext(
13+
val ui: ToolboxUi,
14+
val envPageManager: EnvironmentUiPageManager,
15+
val envStateColorPalette: EnvironmentStateColorPalette,
16+
val cs: CoroutineScope,
17+
val logger: Logger,
18+
val i18n: LocalizableStringFactory,
19+
val settingsStore: PluginSettingsStore,
20+
val secretsStore: PluginSecretStore
21+
)

0 commit comments

Comments
 (0)