Skip to content

Commit 57e48ea

Browse files
authored
Support for removing workspaces (#10)
- facade for Coder's REST endpoint to remove a workspace - pop-up dailog for user to confirm the workspace removal - add support for status icons and as a bonus Toolbox is now displaying nice animation around the header bar when workspace is starting or stopping - resolves #7 Small note: info and error dialogs created with ToolboxUi#showInfoPopup, ToolboxUi#showYesNoPopup, and ToolboxUi#showOkCancelPopup only appear on the main page where all environments are listed. JetBrains is addressing the issue but until then user can interact with the confirmation dialog only from the front page.
1 parent f2dc464 commit 57e48ea

File tree

4 files changed

+83
-11
lines changed

4 files changed

+83
-11
lines changed

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

+48-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.coder.toolbox
33
import com.coder.toolbox.browser.BrowserUtil
44
import com.coder.toolbox.models.WorkspaceAndAgentStatus
55
import com.coder.toolbox.sdk.CoderRestClient
6+
import com.coder.toolbox.sdk.ex.APIResponseException
67
import com.coder.toolbox.sdk.v2.models.Workspace
78
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
89
import com.coder.toolbox.util.withPath
@@ -13,9 +14,15 @@ import com.jetbrains.toolbox.api.remoteDev.AbstractRemoteProviderEnvironment
1314
import com.jetbrains.toolbox.api.remoteDev.EnvironmentVisibilityState
1415
import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView
1516
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateConsumer
17+
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
1618
import com.jetbrains.toolbox.api.ui.ToolboxUi
1719
import kotlinx.coroutines.CoroutineScope
20+
import kotlinx.coroutines.delay
21+
import kotlinx.coroutines.isActive
1822
import kotlinx.coroutines.launch
23+
import kotlinx.coroutines.withTimeout
24+
import kotlin.time.Duration.Companion.minutes
25+
import kotlin.time.Duration.Companion.seconds
1926

2027
/**
2128
* Represents an agent and workspace combination.
@@ -71,7 +78,7 @@ class CoderRemoteEnvironment(
7178
},
7279
)
7380
actionsList.add(
74-
Action("Stop", enabled = { status.ready() || status.pending() }) {
81+
Action("Stop", enabled = { status.canStop() }) {
7582
val build = client.stopWorkspace(workspace)
7683
workspace = workspace.copy(latestBuild = build)
7784
update(workspace, agent)
@@ -128,7 +135,46 @@ class CoderRemoteEnvironment(
128135
}
129136

130137
override fun onDelete() {
131-
throw NotImplementedError()
138+
cs.launch {
139+
// TODO info and cancel pop-ups only appear on the main page where all environments are listed.
140+
// However, #showSnackbar works on other pages. Until JetBrains fixes this issue we are going to use the snackbar
141+
val shouldDelete = if (status.canStop()) {
142+
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"
147+
)
148+
} else {
149+
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"
154+
)
155+
}
156+
if (shouldDelete) {
157+
try {
158+
client.removeWorkspace(workspace)
159+
cs.launch {
160+
withTimeout(5.minutes) {
161+
var workspaceStillExists = true
162+
while (cs.isActive && workspaceStillExists) {
163+
if (status == WorkspaceAndAgentStatus.DELETING || status == WorkspaceAndAgentStatus.DELETED) {
164+
workspaceStillExists = false
165+
serviceLocator.getService(EnvironmentUiPageManager::class.java)
166+
.showPluginEnvironmentsPage()
167+
} else {
168+
delay(1.seconds)
169+
}
170+
}
171+
}
172+
}
173+
} catch (e: APIResponseException) {
174+
ui.showErrorInfoPopup(e)
175+
}
176+
}
177+
}
132178
}
133179

134180
/**

src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt

+18-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.jetbrains.toolbox.api.core.ServiceLocator
99
import com.jetbrains.toolbox.api.core.ui.color.StateColor
1010
import com.jetbrains.toolbox.api.remoteDev.states.CustomRemoteEnvironmentState
1111
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
12+
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateIcons
1213
import com.jetbrains.toolbox.api.remoteDev.states.StandardRemoteEnvironmentState
1314

1415
/**
@@ -59,26 +60,34 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) {
5960
* "disconnected" regardless of the label we give that status.
6061
*/
6162
fun toRemoteEnvironmentState(serviceLocator: ServiceLocator): CustomRemoteEnvironmentState {
62-
val stateColor = getStateColor(serviceLocator)
6363
return CustomRemoteEnvironmentState(
6464
label,
65-
stateColor,
65+
getStateColor(serviceLocator),
6666
ready(), // reachable
6767
// TODO@JB: How does this work? Would like a spinner for pending states.
68-
null, // iconId
68+
getStateIcon()
6969
)
7070
}
7171

7272
private fun getStateColor(serviceLocator: ServiceLocator): StateColor {
7373
val colorPalette = serviceLocator.getService(EnvironmentStateColorPalette::class.java)
7474

75-
7675
return if (ready()) colorPalette.getColor(StandardRemoteEnvironmentState.Active)
7776
else if (canStart()) colorPalette.getColor(StandardRemoteEnvironmentState.Failed)
7877
else if (pending()) colorPalette.getColor(StandardRemoteEnvironmentState.Activating)
78+
else if (this == DELETING) colorPalette.getColor(StandardRemoteEnvironmentState.Deleting)
79+
else if (this == DELETED) colorPalette.getColor(StandardRemoteEnvironmentState.Deleted)
7980
else colorPalette.getColor(StandardRemoteEnvironmentState.Unreachable)
8081
}
8182

83+
private fun getStateIcon(): EnvironmentStateIcons {
84+
return if (ready()) EnvironmentStateIcons.Active
85+
else if (canStart()) EnvironmentStateIcons.Hibernated
86+
else if (pending()) EnvironmentStateIcons.Connecting
87+
else if (this == DELETING || this == DELETED) EnvironmentStateIcons.Offline
88+
else EnvironmentStateIcons.NoIcon
89+
}
90+
8291
/**
8392
* Return true if the agent is in a connectable state.
8493
*/
@@ -107,6 +116,11 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) {
107116
fun canStart(): Boolean = listOf(STOPPED, FAILED, CANCELED)
108117
.contains(this)
109118

119+
/**
120+
* Return true if the workspace can be stopped.
121+
*/
122+
fun canStop(): Boolean = ready() || pending()
123+
110124
// We want to check that the workspace is `running`, the agent is
111125
// `connected`, and the agent lifecycle state is `ready` to ensure the best
112126
// possible scenario for attempting a connection.

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

+12-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import retrofit2.converter.moshi.MoshiConverterFactory
3030
import java.net.HttpURLConnection
3131
import java.net.ProxySelector
3232
import java.net.URL
33-
import java.util.*
33+
import java.util.UUID
3434
import javax.net.ssl.X509TrustManager
3535

3636
/**
@@ -229,7 +229,6 @@ open class CoderRestClient(
229229
}
230230

231231
/**
232-
* @throws [APIResponseException].
233232
*/
234233
fun stopWorkspace(workspace: Workspace): WorkspaceBuild {
235234
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.STOP)
@@ -240,6 +239,17 @@ open class CoderRestClient(
240239
return buildResponse.body()!!
241240
}
242241

242+
/**
243+
* @throws [APIResponseException] if issues are encountered during deletion
244+
*/
245+
fun removeWorkspace(workspace: Workspace) {
246+
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.DELETE, false)
247+
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest).execute()
248+
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
249+
throw APIResponseException("delete workspace ${workspace.name}", url, buildResponse)
250+
}
251+
}
252+
243253
/**
244254
* Start the workspace with the latest template version. Best practice is
245255
* to STOP a workspace before doing an update if it is started.

src/main/kotlin/com/coder/toolbox/sdk/v2/models/CreateWorkspaceBuildRequest.kt

+5-3
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import java.util.UUID
88
data class CreateWorkspaceBuildRequest(
99
// Use to update the workspace to a new template version.
1010
@Json(name = "template_version_id") val templateVersionID: UUID?,
11-
// Use to start and stop the workspace.
11+
// Use to start, stop and delete the workspace.
1212
@Json(name = "transition") val transition: WorkspaceTransition,
13+
@Json(name = "orphan") var orphan: Boolean? = null
1314
) {
1415
override fun equals(other: Any?): Boolean {
1516
if (this === other) return true
@@ -19,12 +20,13 @@ data class CreateWorkspaceBuildRequest(
1920

2021
if (templateVersionID != other.templateVersionID) return false
2122
if (transition != other.transition) return false
22-
23+
if (orphan != other.orphan) return false
2324
return true
2425
}
2526

2627
override fun hashCode(): Int {
27-
var result = templateVersionID?.hashCode() ?: 0
28+
var result = orphan?.hashCode() ?: 0
29+
result = 31 * result + (templateVersionID?.hashCode() ?: 0)
2830
result = 31 * result + transition.hashCode()
2931
return result
3032
}

0 commit comments

Comments
 (0)