From a254da896680a887cb96c9492c1a889812d2f80b Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 27 Feb 2025 22:28:10 +0200 Subject: [PATCH 1/7] impl: initial stubs for removing a workspace - biggest issue is that popup dialogs are only visible in the main page so user can only confirm from that place. It can press the delete button even from the workspace details page, but it will have to navigate back to the main page to confirm whether he wants to proceed with the delete action. - the actual delete is not yet implemented --- .../coder/toolbox/CoderRemoteEnvironment.kt | 29 +++++++++++++++++-- .../toolbox/models/WorkspaceAndAgentStatus.kt | 5 ++++ .../com/coder/toolbox/sdk/CoderRestClient.kt | 10 +++++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index c3e5f64..bd20b23 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -71,7 +71,7 @@ class CoderRemoteEnvironment( }, ) actionsList.add( - Action("Stop", enabled = { status.ready() || status.pending() }) { + Action("Stop", enabled = { status.canStop() }) { val build = client.stopWorkspace(workspace) workspace = workspace.copy(latestBuild = build) update(workspace, agent) @@ -128,7 +128,32 @@ class CoderRemoteEnvironment( } override fun onDelete() { - throw NotImplementedError() + cs.launch { + // TODO info and cancel pop-ups only appear on the main page where all environments are listed. + // However, #showSnackbar works on other pages. Until JetBrains fixes this issue we are going to use the snackbar + val shouldDelete = if (status.canStop()) { + ui.showOkCancelPopup( + "Delete running workspace?", + "Workspace will be closed and all the information in this workspace will be lost, including all files, unsaved changes and historical.", + "Delete", + "Cancel" + ) + } else { + ui.showOkCancelPopup( + "Delete workspace?", + "All the information in this workspace will be lost, including all files, unsaved changes and historical.", + "Delete", + "Cancel" + ) + } + if (shouldDelete) { + if (status.canStop()) { + client.stopWorkspace(workspace) + } + + client.removeWorkspace(workspace) + } + } } /** diff --git a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt index 63bf37f..12ef4ca 100644 --- a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt +++ b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt @@ -107,6 +107,11 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { fun canStart(): Boolean = listOf(STOPPED, FAILED, CANCELED) .contains(this) + /** + * Return true if the workspace can be stopped. + */ + fun canStop(): Boolean = ready() || pending() + // We want to check that the workspace is `running`, the agent is // `connected`, and the agent lifecycle state is `ready` to ensure the best // possible scenario for attempting a connection. diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index 4f4f7e0..b9ccaf0 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -30,7 +30,7 @@ import retrofit2.converter.moshi.MoshiConverterFactory import java.net.HttpURLConnection import java.net.ProxySelector import java.net.URL -import java.util.* +import java.util.UUID import javax.net.ssl.X509TrustManager /** @@ -229,7 +229,6 @@ open class CoderRestClient( } /** - * @throws [APIResponseException]. */ fun stopWorkspace(workspace: Workspace): WorkspaceBuild { val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.STOP) @@ -240,6 +239,13 @@ open class CoderRestClient( return buildResponse.body()!! } + /** + * @throws [APIResponseException]. + */ + fun removeWorkspace(workspace: Workspace) { + // TODO - implement this + } + /** * Start the workspace with the latest template version. Best practice is * to STOP a workspace before doing an update if it is started. From 82cca7bbc39e68a7b3dd2bcf43af03de45b44144 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 27 Feb 2025 22:29:16 +0200 Subject: [PATCH 2/7] impl: support for removing the workspace - calls the coder REST API endpoints to remove a workspace id. --- src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt | 8 ++++++-- .../toolbox/sdk/v2/models/CreateWorkspaceBuildRequest.kt | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index b9ccaf0..371c818 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -240,10 +240,14 @@ open class CoderRestClient( } /** - * @throws [APIResponseException]. + * @throws [APIResponseException] if issues are encountered during deletion */ fun removeWorkspace(workspace: Workspace) { - // TODO - implement this + val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.DELETE, false) + val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest).execute() + if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) { + throw APIResponseException("delete workspace ${workspace.name}", url, buildResponse) + } } /** diff --git a/src/main/kotlin/com/coder/toolbox/sdk/v2/models/CreateWorkspaceBuildRequest.kt b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/CreateWorkspaceBuildRequest.kt index 65e310c..6b59529 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/v2/models/CreateWorkspaceBuildRequest.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/CreateWorkspaceBuildRequest.kt @@ -8,8 +8,9 @@ import java.util.UUID data class CreateWorkspaceBuildRequest( // Use to update the workspace to a new template version. @Json(name = "template_version_id") val templateVersionID: UUID?, - // Use to start and stop the workspace. + // Use to start, stop and delete the workspace. @Json(name = "transition") val transition: WorkspaceTransition, + @Json(name = "orphan") var orphan: Boolean? = null ) { override fun equals(other: Any?): Boolean { if (this === other) return true From d5f80fe1eb2cb2fd81c65e87d539a0f5da8cc335 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 27 Feb 2025 23:03:32 +0200 Subject: [PATCH 3/7] fix: show the error if there is an issue when removing the workspace --- .../kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index bd20b23..c744676 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -3,6 +3,7 @@ package com.coder.toolbox import com.coder.toolbox.browser.BrowserUtil import com.coder.toolbox.models.WorkspaceAndAgentStatus import com.coder.toolbox.sdk.CoderRestClient +import com.coder.toolbox.sdk.ex.APIResponseException import com.coder.toolbox.sdk.v2.models.Workspace import com.coder.toolbox.sdk.v2.models.WorkspaceAgent import com.coder.toolbox.util.withPath @@ -147,11 +148,11 @@ class CoderRemoteEnvironment( ) } if (shouldDelete) { - if (status.canStop()) { - client.stopWorkspace(workspace) + try { + client.removeWorkspace(workspace) + } catch (e: APIResponseException) { + ui.showErrorInfoPopup(e) } - - client.removeWorkspace(workspace) } } } From 9c23f9a73631a43da84b1090b4c1d19d2356c17b Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 27 Feb 2025 23:16:54 +0200 Subject: [PATCH 4/7] fix: delete status color instead of unreachable --- .../kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt index 12ef4ca..8e8e769 100644 --- a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt +++ b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt @@ -76,6 +76,8 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { return if (ready()) colorPalette.getColor(StandardRemoteEnvironmentState.Active) else if (canStart()) colorPalette.getColor(StandardRemoteEnvironmentState.Failed) else if (pending()) colorPalette.getColor(StandardRemoteEnvironmentState.Activating) + else if (this == DELETING) colorPalette.getColor(StandardRemoteEnvironmentState.Deleting) + else if (this == DELETED) colorPalette.getColor(StandardRemoteEnvironmentState.Deleted) else colorPalette.getColor(StandardRemoteEnvironmentState.Unreachable) } From 376e65fd12aa7137c354cb48b422538963098909 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 27 Feb 2025 23:46:41 +0200 Subject: [PATCH 5/7] fix: goto main page when the workspace is going to be deleted - needed otherwise we are going to show a stale workspace in the contents view. - only when workspace changes status to DELETING or DELETED. --- .../coder/toolbox/CoderRemoteEnvironment.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index c744676..ad9d82f 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -14,9 +14,15 @@ import com.jetbrains.toolbox.api.remoteDev.AbstractRemoteProviderEnvironment import com.jetbrains.toolbox.api.remoteDev.EnvironmentVisibilityState import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateConsumer +import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager import com.jetbrains.toolbox.api.ui.ToolboxUi import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds /** * Represents an agent and workspace combination. @@ -150,6 +156,20 @@ class CoderRemoteEnvironment( if (shouldDelete) { try { client.removeWorkspace(workspace) + cs.launch { + withTimeout(5.minutes) { + var workspaceStillExists = true + while (cs.isActive && workspaceStillExists) { + if (status == WorkspaceAndAgentStatus.DELETING || status == WorkspaceAndAgentStatus.DELETED) { + workspaceStillExists = false + serviceLocator.getService(EnvironmentUiPageManager::class.java) + .showPluginEnvironmentsPage() + } else { + delay(1.seconds) + } + } + } + } } catch (e: APIResponseException) { ui.showErrorInfoPopup(e) } From 27ae574ce15b55652c65d473ed39b79f0c1a1c8f Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 28 Feb 2025 00:21:14 +0200 Subject: [PATCH 6/7] impl: 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 --- .../toolbox/models/WorkspaceAndAgentStatus.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt index 8e8e769..dd5bb8b 100644 --- a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt +++ b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt @@ -9,6 +9,7 @@ import com.jetbrains.toolbox.api.core.ServiceLocator import com.jetbrains.toolbox.api.core.ui.color.StateColor import com.jetbrains.toolbox.api.remoteDev.states.CustomRemoteEnvironmentState import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette +import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateIcons import com.jetbrains.toolbox.api.remoteDev.states.StandardRemoteEnvironmentState /** @@ -59,20 +60,18 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { * "disconnected" regardless of the label we give that status. */ fun toRemoteEnvironmentState(serviceLocator: ServiceLocator): CustomRemoteEnvironmentState { - val stateColor = getStateColor(serviceLocator) return CustomRemoteEnvironmentState( label, - stateColor, + getStateColor(serviceLocator), ready(), // reachable // TODO@JB: How does this work? Would like a spinner for pending states. - null, // iconId + getStateIcon() ) } private fun getStateColor(serviceLocator: ServiceLocator): StateColor { val colorPalette = serviceLocator.getService(EnvironmentStateColorPalette::class.java) - return if (ready()) colorPalette.getColor(StandardRemoteEnvironmentState.Active) else if (canStart()) colorPalette.getColor(StandardRemoteEnvironmentState.Failed) else if (pending()) colorPalette.getColor(StandardRemoteEnvironmentState.Activating) @@ -81,6 +80,14 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { else colorPalette.getColor(StandardRemoteEnvironmentState.Unreachable) } + private fun getStateIcon(): EnvironmentStateIcons { + return if (ready()) EnvironmentStateIcons.Active + else if (canStart()) EnvironmentStateIcons.Hibernated + else if (pending()) EnvironmentStateIcons.Connecting + else if (this == DELETING || this == DELETED) EnvironmentStateIcons.Offline + else EnvironmentStateIcons.NoIcon + } + /** * Return true if the agent is in a connectable state. */ From c1dbb1a066880a41846f669d241651d3e957572b Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 28 Feb 2025 00:28:09 +0200 Subject: [PATCH 7/7] fix: orphan field was not part of equals and hash code --- .../toolbox/sdk/v2/models/CreateWorkspaceBuildRequest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/sdk/v2/models/CreateWorkspaceBuildRequest.kt b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/CreateWorkspaceBuildRequest.kt index 6b59529..a2f1ca2 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/v2/models/CreateWorkspaceBuildRequest.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/CreateWorkspaceBuildRequest.kt @@ -20,12 +20,13 @@ data class CreateWorkspaceBuildRequest( if (templateVersionID != other.templateVersionID) return false if (transition != other.transition) return false - + if (orphan != other.orphan) return false return true } override fun hashCode(): Int { - var result = templateVersionID?.hashCode() ?: 0 + var result = orphan?.hashCode() ?: 0 + result = 31 * result + (templateVersionID?.hashCode() ?: 0) result = 31 * result + transition.hashCode() return result }