diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index c3e5f64..ad9d82f 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 @@ -13,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. @@ -71,7 +78,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 +135,46 @@ 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) { + 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) + } + } + } } /** diff --git a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt index 63bf37f..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,26 +60,34 @@ 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) + else if (this == DELETING) colorPalette.getColor(StandardRemoteEnvironmentState.Deleting) + else if (this == DELETED) colorPalette.getColor(StandardRemoteEnvironmentState.Deleted) 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. */ @@ -107,6 +116,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..371c818 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,17 @@ open class CoderRestClient( return buildResponse.body()!! } + /** + * @throws [APIResponseException] if issues are encountered during deletion + */ + fun removeWorkspace(workspace: Workspace) { + 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) + } + } + /** * Start the workspace with the latest template version. Best practice is * to STOP a workspace before doing an update if it is started. 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..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 @@ -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 @@ -19,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 }