Skip to content

Commit 48ed27d

Browse files
committed
fix: report errors while running actions
JetBrains team reported in the past a couple of errors in the log, one of them being `A workspace build is already active`. The issue can be reproduced if the user hits the `Stop` action for example quite quick. It takes maybe one or two seconds to make rest api request, then for the backend to enqueue the build and change the workspace action. If we hit the action buttons really fast then this error could be reproduced. One approach I tried was to disable the action buttons in the context menu for the duration the request is executed. But for some reason the "enabled" property is not working in context menu, only when the actions are rendered on a UI "page". Instead, I decided to refactor the existing code and (also) visually report the errors in the UI screen to make the user aware in some cases that a job is already running on the backend.
1 parent 6b00191 commit 48ed27d

File tree

6 files changed

+70
-62
lines changed

6 files changed

+70
-62
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixed
66

77
- token is no longer required when authentication is done via certificates
8+
- errors while running actions are now reported
89

910
## 0.6.4 - 2025-09-03
1011

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

Lines changed: 38 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -81,68 +81,61 @@ class CoderRemoteEnvironment(
8181
private fun getAvailableActions(): List<ActionDescription> {
8282
val actions = mutableListOf<Action>()
8383
if (wsRawStatus.canStop()) {
84-
actions.add(Action(context.i18n.ptrl("Open web terminal")) {
85-
context.cs.launch(CoroutineName("Open Web Terminal Action")) {
86-
context.desktop.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) {
87-
context.ui.showErrorInfoPopup(it)
88-
}
84+
actions.add(Action(context, "Open web terminal") {
85+
context.desktop.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) {
86+
context.ui.showErrorInfoPopup(it)
8987
}
90-
})
88+
}
89+
)
9190
}
9291
actions.add(
93-
Action(context.i18n.ptrl("Open in dashboard")) {
94-
context.cs.launch(CoroutineName("Open in Dashboard Action")) {
95-
context.desktop.browse(
96-
client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()
97-
) {
98-
context.ui.showErrorInfoPopup(it)
99-
}
100-
}
101-
})
102-
103-
actions.add(Action(context.i18n.ptrl("View template")) {
104-
context.cs.launch(CoroutineName("View Template Action")) {
105-
context.desktop.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) {
92+
Action(context, "Open in dashboard") {
93+
context.desktop.browse(
94+
client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()
95+
) {
10696
context.ui.showErrorInfoPopup(it)
10797
}
10898
}
109-
})
99+
)
100+
101+
actions.add(Action(context, "View template") {
102+
context.desktop.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) {
103+
context.ui.showErrorInfoPopup(it)
104+
}
105+
}
106+
)
110107

111108
if (wsRawStatus.canStart()) {
112109
if (workspace.outdated) {
113-
actions.add(Action(context.i18n.ptrl("Update and start")) {
114-
context.cs.launch(CoroutineName("Update and Start Action")) {
115-
val build = client.updateWorkspace(workspace)
116-
update(workspace.copy(latestBuild = build), agent)
117-
}
118-
})
110+
actions.add(Action(context, "Update and start") {
111+
val build = client.updateWorkspace(workspace)
112+
update(workspace.copy(latestBuild = build), agent)
113+
}
114+
)
119115
} else {
120-
actions.add(Action(context.i18n.ptrl("Start")) {
121-
context.cs.launch(CoroutineName("Start Action")) {
122-
val build = client.startWorkspace(workspace)
123-
update(workspace.copy(latestBuild = build), agent)
116+
actions.add(Action(context, "Start") {
117+
val build = client.startWorkspace(workspace)
118+
update(workspace.copy(latestBuild = build), agent)
124119

125-
}
126-
})
120+
}
121+
)
127122
}
128123
}
129124
if (wsRawStatus.canStop()) {
130125
if (workspace.outdated) {
131-
actions.add(Action(context.i18n.ptrl("Update and restart")) {
132-
context.cs.launch(CoroutineName("Update and Restart Action")) {
133-
val build = client.updateWorkspace(workspace)
134-
update(workspace.copy(latestBuild = build), agent)
135-
}
136-
})
137-
}
138-
actions.add(Action(context.i18n.ptrl("Stop")) {
139-
context.cs.launch(CoroutineName("Stop Action")) {
140-
tryStopSshConnection()
141-
142-
val build = client.stopWorkspace(workspace)
126+
actions.add(Action(context, "Update and restart") {
127+
val build = client.updateWorkspace(workspace)
143128
update(workspace.copy(latestBuild = build), agent)
144129
}
145-
})
130+
)
131+
}
132+
actions.add(Action(context, "Stop") {
133+
tryStopSshConnection()
134+
135+
val build = client.stopWorkspace(workspace)
136+
update(workspace.copy(latestBuild = build), agent)
137+
}
138+
)
146139
}
147140
return actions
148141
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,15 +222,13 @@ class CoderRemoteProvider(
222222

223223
override val additionalPluginActions: StateFlow<List<ActionDescription>> = MutableStateFlow(
224224
listOf(
225-
Action(context.i18n.ptrl("Create workspace")) {
226-
context.cs.launch(CoroutineName("Create Workspace Action")) {
227-
context.desktop.browse(client?.url?.withPath("/templates").toString()) {
228-
context.ui.showErrorInfoPopup(it)
229-
}
225+
Action(context, "Create workspace") {
226+
context.desktop.browse(client?.url?.withPath("/templates").toString()) {
227+
context.ui.showErrorInfoPopup(it)
230228
}
231229
},
232230
CoderDelimiter(context.i18n.pnotr("")),
233-
Action(context.i18n.ptrl("Settings")) {
231+
Action(context, "Settings") {
234232
context.ui.showUiPage(settingsPage)
235233
},
236234
)

src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ class CoderCliSetupWizardPage(
2424
) -> Unit,
2525
) : CoderPage(MutableStateFlow(context.i18n.ptrl("Setting up Coder")), false) {
2626
private val shouldAutoSetup = MutableStateFlow(initialAutoSetup)
27-
private val settingsAction = Action(context.i18n.ptrl("Settings"), actionBlock = {
27+
private val settingsAction = Action(context, "Settings") {
2828
context.ui.showUiPage(settingsPage)
29-
})
29+
}
3030

3131
private val deploymentUrlStep = DeploymentUrlStep(context, visibilityState)
3232
private val tokenStep = TokenStep(context)
@@ -60,7 +60,7 @@ class CoderCliSetupWizardPage(
6060
}
6161
actionButtons.update {
6262
listOf(
63-
Action(context.i18n.ptrl("Next"), closesPage = false, actionBlock = {
63+
Action(context, "Next", closesPage = false, actionBlock = {
6464
if (deploymentUrlStep.onNext()) {
6565
displaySteps()
6666
}
@@ -77,13 +77,13 @@ class CoderCliSetupWizardPage(
7777
}
7878
actionButtons.update {
7979
listOf(
80-
Action(context.i18n.ptrl("Connect"), closesPage = false, actionBlock = {
80+
Action(context, "Connect", closesPage = false, actionBlock = {
8181
if (tokenStep.onNext()) {
8282
displaySteps()
8383
}
8484
}),
8585
settingsAction,
86-
Action(context.i18n.ptrl("Back"), closesPage = false, actionBlock = {
86+
Action(context, "Back", closesPage = false, actionBlock = {
8787
tokenStep.onBack()
8888
displaySteps()
8989
})
@@ -99,7 +99,7 @@ class CoderCliSetupWizardPage(
9999
actionButtons.update {
100100
listOf(
101101
settingsAction,
102-
Action(context.i18n.ptrl("Back"), closesPage = false, actionBlock = {
102+
Action(context, "Back", closesPage = false, actionBlock = {
103103
connectStep.onBack()
104104
shouldAutoSetup.update {
105105
false

src/main/kotlin/com/coder/toolbox/views/CoderPage.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package com.coder.toolbox.views
22

3+
import com.coder.toolbox.CoderToolboxContext
4+
import com.coder.toolbox.sdk.ex.APIResponseException
35
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
46
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType
57
import com.jetbrains.toolbox.api.localization.LocalizableString
68
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
79
import com.jetbrains.toolbox.api.ui.components.UiPage
10+
import kotlinx.coroutines.CoroutineName
811
import kotlinx.coroutines.flow.MutableStateFlow
912
import kotlinx.coroutines.flow.update
13+
import kotlinx.coroutines.launch
1014

1115
/**
1216
* Base page that handles the icon, displaying error notifications, and
@@ -48,15 +52,27 @@ abstract class CoderPage(
4852
* An action that simply runs the provided callback.
4953
*/
5054
class Action(
51-
description: LocalizableString,
55+
private val context: CoderToolboxContext,
56+
private val description: String,
5257
closesPage: Boolean = false,
5358
enabled: () -> Boolean = { true },
54-
private val actionBlock: () -> Unit,
59+
private val actionBlock: suspend () -> Unit,
5560
) : RunnableActionDescription {
56-
override val label: LocalizableString = description
61+
override val label: LocalizableString = context.i18n.ptrl(description)
5762
override val shouldClosePage: Boolean = closesPage
5863
override val isEnabled: Boolean = enabled()
5964
override fun run() {
60-
actionBlock()
65+
context.cs.launch(CoroutineName("$description Action")) {
66+
try {
67+
actionBlock()
68+
} catch (ex: Exception) {
69+
val textError = if (ex is APIResponseException) {
70+
if (!ex.reason.isNullOrBlank()) {
71+
ex.reason
72+
} else ex.message
73+
} else ex.message
74+
context.logAndShowError("Error while running `$description`", textError ?: "", ex)
75+
}
76+
}
6177
}
6278
}

src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf
116116

117117
override val actionButtons: StateFlow<List<RunnableActionDescription>> = MutableStateFlow(
118118
listOf(
119-
Action(context.i18n.ptrl("Save"), closesPage = true) {
119+
Action(context, "Save", closesPage = true) {
120120
context.settingsStore.updateBinarySource(binarySourceField.contentState.value)
121121
context.settingsStore.updateBinaryDirectory(binaryDirectoryField.contentState.value)
122122
context.settingsStore.updateDataDirectory(dataDirectoryField.contentState.value)

0 commit comments

Comments
 (0)