Skip to content

Commit 757ee87

Browse files
authored
impl: confirmation dialog for workspace deletion (#179)
Users are now required to confirm the workspace name if they want to delete a workspace. This is in order to avoid any accidental removals. Note: right now there are two issues with Toolbox input dialogs, the dialog title is not rendered, and worse - the text field is rendered as a password input field, so it does not make sense to merge this until Toolbox fixes the issues. <img width="486" height="746" alt="image" src="https://github.com/user-attachments/assets/e8fc2409-6e24-42d0-8258-473c29f5df44" /> - resolves #178
1 parent c00704d commit 757ee87

File tree

5 files changed

+66
-38
lines changed

5 files changed

+66
-38
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Changed
6+
7+
- workspaces can no longer be removed by accident - users are now required to input the workspace name.
8+
59
### Fixed
610

711
- relaxed SNI hostname resolution

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

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
1212
import com.coder.toolbox.util.waitForFalseWithTimeout
1313
import com.coder.toolbox.util.withPath
1414
import com.coder.toolbox.views.Action
15+
import com.coder.toolbox.views.CoderDelimiter
1516
import com.coder.toolbox.views.EnvironmentView
1617
import com.jetbrains.toolbox.api.localization.LocalizableString
1718
import com.jetbrains.toolbox.api.remoteDev.AfterDisconnectHook
1819
import com.jetbrains.toolbox.api.remoteDev.BeforeConnectionHook
19-
import com.jetbrains.toolbox.api.remoteDev.DeleteEnvironmentConfirmationParams
2020
import com.jetbrains.toolbox.api.remoteDev.EnvironmentVisibilityState
2121
import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment
2222
import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView
2323
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription
2424
import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState
2525
import com.jetbrains.toolbox.api.ui.actions.ActionDescription
26+
import com.jetbrains.toolbox.api.ui.components.TextType
2627
import com.squareup.moshi.Moshi
2728
import kotlinx.coroutines.CoroutineName
2829
import kotlinx.coroutines.Job
@@ -79,7 +80,7 @@ class CoderRemoteEnvironment(
7980
fun asPairOfWorkspaceAndAgent(): Pair<Workspace, WorkspaceAgent> = Pair(workspace, agent)
8081

8182
private fun getAvailableActions(): List<ActionDescription> {
82-
val actions = mutableListOf<Action>()
83+
val actions = mutableListOf<ActionDescription>()
8384
if (wsRawStatus.canStop()) {
8485
actions.add(Action(context, "Open web terminal") {
8586
context.desktop.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) {
@@ -137,6 +138,28 @@ class CoderRemoteEnvironment(
137138
}
138139
)
139140
}
141+
actions.add(CoderDelimiter(context.i18n.pnotr("")))
142+
actions.add(Action(context, "Delete workspace", highlightInRed = true) {
143+
context.cs.launch(CoroutineName("Delete Workspace Action")) {
144+
var dialogText =
145+
if (wsRawStatus.canStop()) "This will close the workspace and remove all its information, including files, unsaved changes, history, and usage data."
146+
else "This will remove all information from the workspace, including files, unsaved changes, history, and usage data."
147+
dialogText += "\n\nType \"${workspace.name}\" below to confirm:"
148+
149+
val confirmation = context.ui.showTextInputPopup(
150+
if (wsRawStatus.canStop()) context.i18n.ptrl("Delete running workspace?") else context.i18n.ptrl("Delete workspace?"),
151+
context.i18n.pnotr(dialogText),
152+
context.i18n.ptrl("Workspace name"),
153+
TextType.General,
154+
context.i18n.ptrl("OK"),
155+
context.i18n.ptrl("Cancel")
156+
)
157+
if (confirmation != workspace.name) {
158+
return@launch
159+
}
160+
deleteWorkspace()
161+
}
162+
})
140163
return actions
141164
}
142165

@@ -266,43 +289,32 @@ class CoderRemoteEnvironment(
266289
return false
267290
}
268291

269-
override fun getDeleteEnvironmentConfirmationParams(): DeleteEnvironmentConfirmationParams? {
270-
return object : DeleteEnvironmentConfirmationParams {
271-
override val cancelButtonText: String = "Cancel"
272-
override val confirmButtonText: String = "Delete"
273-
override val message: String =
274-
if (wsRawStatus.canStop()) "Workspace will be closed and all the information will be lost, including all files, unsaved changes, historical info and usage data."
275-
else "All the information in this workspace will be lost, including all files, unsaved changes, historical info and usage data."
276-
override val title: String = if (wsRawStatus.canStop()) "Delete running workspace?" else "Delete workspace?"
277-
}
278-
}
292+
override val deleteActionFlow: StateFlow<(() -> Unit)?> = MutableStateFlow(null)
279293

280-
override val deleteActionFlow: StateFlow<(() -> Unit)?> = MutableStateFlow {
281-
context.cs.launch(CoroutineName("Delete Workspace Action")) {
282-
try {
283-
client.removeWorkspace(workspace)
284-
// mark the env as deleting otherwise we will have to
285-
// wait for the poller to update the status in the next 5 seconds
286-
state.update {
287-
WorkspaceAndAgentStatus.DELETING.toRemoteEnvironmentState(context)
288-
}
294+
suspend fun deleteWorkspace() {
295+
try {
296+
client.removeWorkspace(workspace)
297+
// mark the env as deleting otherwise we will have to
298+
// wait for the poller to update the status in the next 5 seconds
299+
state.update {
300+
WorkspaceAndAgentStatus.DELETING.toRemoteEnvironmentState(context)
301+
}
289302

290-
context.cs.launch(CoroutineName("Workspace Deletion Poller")) {
291-
withTimeout(5.minutes) {
292-
var workspaceStillExists = true
293-
while (context.cs.isActive && workspaceStillExists) {
294-
if (wsRawStatus == WorkspaceAndAgentStatus.DELETING || wsRawStatus == WorkspaceAndAgentStatus.DELETED) {
295-
workspaceStillExists = false
296-
context.envPageManager.showPluginEnvironmentsPage()
297-
} else {
298-
delay(1.seconds)
299-
}
303+
context.cs.launch(CoroutineName("Workspace Deletion Poller")) {
304+
withTimeout(5.minutes) {
305+
var workspaceStillExists = true
306+
while (context.cs.isActive && workspaceStillExists) {
307+
if (wsRawStatus == WorkspaceAndAgentStatus.DELETING || wsRawStatus == WorkspaceAndAgentStatus.DELETED) {
308+
workspaceStillExists = false
309+
context.envPageManager.showPluginEnvironmentsPage()
310+
} else {
311+
delay(1.seconds)
300312
}
301313
}
302314
}
303-
} catch (e: APIResponseException) {
304-
context.ui.showErrorInfoPopup(e)
305315
}
316+
} catch (e: APIResponseException) {
317+
context.ui.showErrorInfoPopup(e)
306318
}
307319
}
308320

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.coder.toolbox.util.waitForTrue
1212
import com.coder.toolbox.util.withPath
1313
import com.coder.toolbox.views.Action
1414
import com.coder.toolbox.views.CoderCliSetupWizardPage
15+
import com.coder.toolbox.views.CoderDelimiter
1516
import com.coder.toolbox.views.CoderSettingsPage
1617
import com.coder.toolbox.views.NewEnvironmentPage
1718
import com.coder.toolbox.views.state.CoderCliSetupContext
@@ -23,7 +24,6 @@ import com.jetbrains.toolbox.api.core.util.LoadableState
2324
import com.jetbrains.toolbox.api.localization.LocalizableString
2425
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
2526
import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
26-
import com.jetbrains.toolbox.api.ui.actions.ActionDelimiter
2727
import com.jetbrains.toolbox.api.ui.actions.ActionDescription
2828
import com.jetbrains.toolbox.api.ui.components.UiPage
2929
import kotlinx.coroutines.CoroutineName
@@ -428,6 +428,4 @@ class CoderRemoteProvider(
428428
LoadableState.Loading
429429
}
430430
}
431-
}
432-
433-
private class CoderDelimiter(override val label: LocalizableString) : ActionDelimiter
431+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.coder.toolbox.sdk.ex.APIResponseException
55
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
66
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType
77
import com.jetbrains.toolbox.api.localization.LocalizableString
8+
import com.jetbrains.toolbox.api.ui.actions.ActionDelimiter
89
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
910
import com.jetbrains.toolbox.api.ui.components.UiPage
1011
import kotlinx.coroutines.CoroutineName
@@ -55,12 +56,14 @@ class Action(
5556
private val context: CoderToolboxContext,
5657
private val description: String,
5758
closesPage: Boolean = false,
59+
highlightInRed: Boolean = false,
5860
enabled: () -> Boolean = { true },
5961
private val actionBlock: suspend () -> Unit,
6062
) : RunnableActionDescription {
6163
override val label: LocalizableString = context.i18n.ptrl(description)
6264
override val shouldClosePage: Boolean = closesPage
6365
override val isEnabled: Boolean = enabled()
66+
override val isDangerous: Boolean = highlightInRed
6467
override fun run() {
6568
context.cs.launch(CoroutineName("$description Action")) {
6669
try {
@@ -76,3 +79,5 @@ class Action(
7679
}
7780
}
7881
}
82+
83+
class CoderDelimiter(override val label: LocalizableString) : ActionDelimiter

src/main/resources/localization/defaultMessages.po

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,13 @@ msgid "Headers"
179179
msgstr ""
180180

181181
msgid "Body"
182-
msgstr ""
182+
msgstr ""
183+
184+
msgid "Delete workspace"
185+
msgstr ""
186+
187+
msgid "Delete running workspace?"
188+
msgstr ""
189+
190+
msgid "Workspace name"
191+
msgstr ""

0 commit comments

Comments
 (0)