From ed37ab0aed7de41955f42a05367a0f14e46e566c Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 25 Apr 2023 12:11:34 -0800 Subject: [PATCH 01/10] Simplify workspace status check Looks like there is already a property that is the composite of the two fields we were checking. --- .../gateway/models/WorkspaceAgentStatus.kt | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentStatus.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentStatus.kt index 44a71614..4f64882a 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentStatus.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentStatus.kt @@ -1,8 +1,7 @@ package com.coder.gateway.models -import com.coder.gateway.sdk.v2.models.ProvisionerJobStatus import com.coder.gateway.sdk.v2.models.Workspace -import com.coder.gateway.sdk.v2.models.WorkspaceTransition +import com.coder.gateway.sdk.v2.models.WorkspaceStatus import com.intellij.ui.JBColor enum class WorkspaceAgentStatus(val label: String) { @@ -16,26 +15,22 @@ enum class WorkspaceAgentStatus(val label: String) { else -> if (JBColor.isBright()) JBColor.LIGHT_GRAY else JBColor.DARK_GRAY } + // Note that latest_build.status is derived from latest_build.job.status and + // latest_build.job.transition so there is no need to check those. companion object { - fun from(workspace: Workspace) = when (workspace.latestBuild.job.status) { - ProvisionerJobStatus.PENDING -> QUEUED - ProvisionerJobStatus.RUNNING -> when (workspace.latestBuild.transition) { - WorkspaceTransition.START -> STARTING - WorkspaceTransition.STOP -> STOPPING - WorkspaceTransition.DELETE -> DELETING - } - - ProvisionerJobStatus.SUCCEEDED -> when (workspace.latestBuild.transition) { - WorkspaceTransition.START -> RUNNING - WorkspaceTransition.STOP -> STOPPED - WorkspaceTransition.DELETE -> DELETED - } - - ProvisionerJobStatus.CANCELING -> CANCELING - ProvisionerJobStatus.CANCELED -> CANCELED - ProvisionerJobStatus.FAILED -> FAILED + fun from(workspace: Workspace, agent: WorkspaceAgentModel) = when (workspace.latestBuild.status) { + WorkspaceStatus.PENDING -> QUEUED + WorkspaceStatus.STARTING -> STARTING + WorkspaceStatus.RUNNING -> RUNNING + WorkspaceStatus.STOPPING -> STOPPING + WorkspaceStatus.STOPPED -> STOPPED + WorkspaceStatus.FAILED -> FAILED + WorkspaceStatus.CANCELING -> CANCELING + WorkspaceStatus.CANCELED -> CANCELED + WorkspaceStatus.DELETING -> DELETING + WorkspaceStatus.DELETED -> DELETED } fun from(str: String) = WorkspaceAgentStatus.values().first { it.label.contains(str, true) } } -} \ No newline at end of file +} From 3b06d9fd6e1aa8c6800f2e69fd7120e3d288e0af Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 25 Apr 2023 12:54:47 -0800 Subject: [PATCH 02/10] Simplify agent model list We can just check if the list is empty then add the workspace-only "agent" model rather than duplicate the block. --- .../views/steps/CoderWorkspacesStepView.kt | 121 +++++++----------- 1 file changed, 46 insertions(+), 75 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt index 4d6d1cf4..2b685f77 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -687,86 +687,57 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod } private fun Workspace.toAgentModels(): Set { - return when (this.latestBuild.resources.size) { - 0 -> { - val wm = WorkspaceAgentModel( - this.id, - this.name, - this.name, - this.templateID, - this.templateName, - this.templateIcon, - null, - WorkspaceVersionStatus.from(this), - WorkspaceAgentStatus.from(this), - this.latestBuild.transition, - null, - null, - null - ) - cs.launch(Dispatchers.IO) { - wm.templateIcon = iconDownloader.load(wm.templateIconPath, wm.name) - withContext(Dispatchers.Main) { - tableOfWorkspaces.updateUI() - } + val wam = this.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }.map { agent -> + val workspaceWithAgentName = "${this.name}.${agent.name}" + val wm = WorkspaceAgentModel( + this.id, + this.name, + workspaceWithAgentName, + this.templateID, + this.templateName, + this.templateIcon, + null, + WorkspaceVersionStatus.from(this), + WorkspaceAgentStatus.from(this), + this.latestBuild.transition, + OS.from(agent.operatingSystem), + Arch.from(agent.architecture), + agent.expandedDirectory ?: agent.directory, + ) + cs.launch(Dispatchers.IO) { + wm.templateIcon = iconDownloader.load(wm.templateIconPath, wm.name) + withContext(Dispatchers.Main) { + tableOfWorkspaces.updateUI() } - setOf(wm) } - - else -> { - val wam = this.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }.map { agent -> - val workspaceWithAgentName = "${this.name}.${agent.name}" - val wm = WorkspaceAgentModel( - this.id, - this.name, - workspaceWithAgentName, - this.templateID, - this.templateName, - this.templateIcon, - null, - WorkspaceVersionStatus.from(this), - WorkspaceAgentStatus.from(this), - this.latestBuild.transition, - OS.from(agent.operatingSystem), - Arch.from(agent.architecture), - agent.expandedDirectory ?: agent.directory, - ) - cs.launch(Dispatchers.IO) { - wm.templateIcon = iconDownloader.load(wm.templateIconPath, wm.name) - withContext(Dispatchers.Main) { - tableOfWorkspaces.updateUI() - } - } - wm - }.toSet() - - if (wam.isNullOrEmpty()) { - val wm = WorkspaceAgentModel( - this.id, - this.name, - this.name, - this.templateID, - this.templateName, - this.templateIcon, - null, - WorkspaceVersionStatus.from(this), - WorkspaceAgentStatus.from(this), - this.latestBuild.transition, - null, - null, - null - ) - cs.launch(Dispatchers.IO) { - wm.templateIcon = iconDownloader.load(wm.templateIconPath, wm.name) - withContext(Dispatchers.Main) { - tableOfWorkspaces.updateUI() - } - } - return setOf(wm) + wm + }.toSet() + + if (wam.isNullOrEmpty()) { + val wm = WorkspaceAgentModel( + this.id, + this.name, + this.name, + this.templateID, + this.templateName, + this.templateIcon, + null, + WorkspaceVersionStatus.from(this), + WorkspaceAgentStatus.from(this), + this.latestBuild.transition, + null, + null, + null + ) + cs.launch(Dispatchers.IO) { + wm.templateIcon = iconDownloader.load(wm.templateIconPath, wm.name) + withContext(Dispatchers.Main) { + tableOfWorkspaces.updateUI() } - return wam } + return setOf(wm) } + return wam } override fun onPrevious() { From 9b787c707f9eaee2cb6eb0710ef154a3f55662f3 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 25 Apr 2023 12:28:53 -0800 Subject: [PATCH 03/10] Add lifecycle_state and login_before_ready to agent response --- .../gateway/sdk/v2/models/WorkspaceAgent.kt | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceAgent.kt b/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceAgent.kt index 8f05e4bd..69eb1f52 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceAgent.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceAgent.kt @@ -26,23 +26,30 @@ data class WorkspaceAgent( @SerializedName("latency") val derpLatency: Map?, @SerializedName("connection_timeout_seconds") val connectionTimeoutSeconds: Int, @SerializedName("troubleshooting_url") val troubleshootingURL: String, + @SerializedName("lifecycle_state") val lifecycleState: WorkspaceAgentLifecycleState, + @SerializedName("login_before_ready") val loginBeforeReady: Boolean?, ) enum class WorkspaceAgentStatus { - @SerializedName("connecting") - CONNECTING, - - @SerializedName("connected") - CONNECTED, - - @SerializedName("disconnected") - DISCONNECTED, + @SerializedName("connecting") CONNECTING, + @SerializedName("connected") CONNECTED, + @SerializedName("disconnected") DISCONNECTED, + @SerializedName("timeout") TIMEOUT +} - @SerializedName("timeout") - TIMEOUT +enum class WorkspaceAgentLifecycleState { + @SerializedName("created") CREATED, + @SerializedName("starting") STARTING, + @SerializedName("start_timeout") START_TIMEOUT, + @SerializedName("start_error") START_ERROR, + @SerializedName("ready") READY, + @SerializedName("shutting_down") SHUTTING_DOWN, + @SerializedName("shutdown_timeout") SHUTDOWN_TIMEOUT, + @SerializedName("shutdown_error") SHUTDOWN_ERROR, + @SerializedName("off") OFF, } data class DERPRegion( @SerializedName("preferred") val preferred: Boolean, - @SerializedName("latency_ms") val latencyMillis: Double + @SerializedName("latency_ms") val latencyMillis: Double, ) From 9ab8769d564479ac66b14234c63012c49b156c3a Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 25 Apr 2023 12:49:31 -0800 Subject: [PATCH 04/10] Check agent status Previously we only checked the workspace, now we also check the agent. Should help ensure we only connect when the connection will succeed. --- .../gateway/models/WorkspaceAgentModel.kt | 6 +- .../gateway/models/WorkspaceAgentStatus.kt | 36 -------- .../gateway/models/WorkspaceAndAgentStatus.kt | 83 +++++++++++++++++++ .../views/steps/CoderWorkspacesStepView.kt | 28 ++++--- src/test/groovy/CoderCLIManagerTest.groovy | 6 +- 5 files changed, 107 insertions(+), 52 deletions(-) delete mode 100644 src/main/kotlin/com/coder/gateway/models/WorkspaceAgentStatus.kt create mode 100644 src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt index 5ecd186c..6cfc103b 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt @@ -2,6 +2,7 @@ package com.coder.gateway.models import com.coder.gateway.sdk.Arch import com.coder.gateway.sdk.OS +import com.coder.gateway.sdk.v2.models.WorkspaceStatus import com.coder.gateway.sdk.v2.models.WorkspaceTransition import java.util.UUID import javax.swing.Icon @@ -15,11 +16,12 @@ data class WorkspaceAgentModel( val templateIconPath: String, var templateIcon: Icon?, val status: WorkspaceVersionStatus, - val agentStatus: WorkspaceAgentStatus, + val workspaceStatus: WorkspaceStatus, + val agentStatus: WorkspaceAndAgentStatus, val lastBuildTransition: WorkspaceTransition, val agentOS: OS?, val agentArch: Arch?, - val homeDirectory: String? + val homeDirectory: String?, ) { override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentStatus.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentStatus.kt deleted file mode 100644 index 4f64882a..00000000 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentStatus.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.coder.gateway.models - -import com.coder.gateway.sdk.v2.models.Workspace -import com.coder.gateway.sdk.v2.models.WorkspaceStatus -import com.intellij.ui.JBColor - -enum class WorkspaceAgentStatus(val label: String) { - QUEUED("◍ Queued"), STARTING("⦿ Starting"), STOPPING("◍ Stopping"), DELETING("⦸ Deleting"), - RUNNING("⦿ Running"), STOPPED("◍ Stopped"), DELETED("⦸ Deleted"), - CANCELING("◍ Canceling action"), CANCELED("◍ Canceled action"), FAILED("ⓧ Failed"); - - fun statusColor() = when (this) { - RUNNING -> JBColor.GREEN - FAILED -> JBColor.RED - else -> if (JBColor.isBright()) JBColor.LIGHT_GRAY else JBColor.DARK_GRAY - } - - // Note that latest_build.status is derived from latest_build.job.status and - // latest_build.job.transition so there is no need to check those. - companion object { - fun from(workspace: Workspace, agent: WorkspaceAgentModel) = when (workspace.latestBuild.status) { - WorkspaceStatus.PENDING -> QUEUED - WorkspaceStatus.STARTING -> STARTING - WorkspaceStatus.RUNNING -> RUNNING - WorkspaceStatus.STOPPING -> STOPPING - WorkspaceStatus.STOPPED -> STOPPED - WorkspaceStatus.FAILED -> FAILED - WorkspaceStatus.CANCELING -> CANCELING - WorkspaceStatus.CANCELED -> CANCELED - WorkspaceStatus.DELETING -> DELETING - WorkspaceStatus.DELETED -> DELETED - } - - fun from(str: String) = WorkspaceAgentStatus.values().first { it.label.contains(str, true) } - } -} diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt new file mode 100644 index 00000000..23082482 --- /dev/null +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt @@ -0,0 +1,83 @@ +package com.coder.gateway.models + +import com.coder.gateway.sdk.v2.models.Workspace +import com.coder.gateway.sdk.v2.models.WorkspaceAgent +import com.coder.gateway.sdk.v2.models.WorkspaceAgentLifecycleState +import com.coder.gateway.sdk.v2.models.WorkspaceAgentStatus +import com.coder.gateway.sdk.v2.models.WorkspaceStatus +import com.intellij.ui.JBColor + +/** + * WorkspaceAndAgentStatus represents the combined status of a single agent and + * its workspace (or just the workspace if there are no agents). + */ +enum class WorkspaceAndAgentStatus(val label: String) { + // Workspace states. + QUEUED("◍ Queued"), STARTING("⦿ Starting"), FAILED("ⓧ Failed"), + DELETING("⦸ Deleting"), DELETED("⦸ Deleted"), + STOPPING("◍ Stopping"), STOPPED("◍ Stopped"), + CANCELING("◍ Canceling action"), CANCELED("◍ Canceled action"), + RUNNING("⦿ Running"), + + // Agent states. + OFF("⦸ Off"), CONNECTING("⦿ Connecting"), DISCONNECTED("⦸ Disconnected"), TIMEOUT("ⓧ Timeout"), + AGENT_STARTING("⦿ Starting"), AGENT_STARTING_READY("⦿ Starting"), + CREATED("⦿ Created"), START_ERROR("◍ Started with error"), START_TIMEOUT("◍ Started with timeout"), + SHUTTING_DOWN("◍ Shutting down"), SHUTDOWN_ERROR("⦸ Shutdown with error"), SHUTDOWN_TIMEOUT("⦸ Shutdown with timeout"), + READY("⦿ Ready"); + + fun statusColor(): JBColor = when (this) { + READY, AGENT_STARTING_READY -> JBColor.GREEN + START_ERROR, START_TIMEOUT -> JBColor.YELLOW + FAILED, DISCONNECTED, TIMEOUT, SHUTTING_DOWN, SHUTDOWN_ERROR, SHUTDOWN_TIMEOUT -> JBColor.RED + else -> if (JBColor.isBright()) JBColor.LIGHT_GRAY else JBColor.DARK_GRAY + } + + // 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. + // + // We can also choose to allow `start_timeout` and `start_error` for the + // agent state; this means the startup script did not successfully complete + // but the agent will accept SSH connections. + // + // Lastly we can also allow connections when the agent lifecycle state is + // `starting` if `login_before_ready` is true on the workspace response. + // + // Note that latest_build.status is derived from latest_build.job.status and + // latest_build.job.transition so there is no need to check those. + companion object { + fun from(workspace: Workspace, agent: WorkspaceAgent? = null) = when (workspace.latestBuild.status) { + WorkspaceStatus.PENDING -> QUEUED + WorkspaceStatus.STARTING -> STARTING + WorkspaceStatus.RUNNING -> when (agent?.status) { + WorkspaceAgentStatus.CONNECTED -> when (agent.lifecycleState) { + WorkspaceAgentLifecycleState.CREATED -> CREATED + WorkspaceAgentLifecycleState.STARTING -> if (agent.loginBeforeReady == true) AGENT_STARTING_READY else AGENT_STARTING + WorkspaceAgentLifecycleState.START_TIMEOUT -> START_TIMEOUT + WorkspaceAgentLifecycleState.START_ERROR -> START_ERROR + WorkspaceAgentLifecycleState.READY -> READY + WorkspaceAgentLifecycleState.SHUTTING_DOWN -> SHUTTING_DOWN + WorkspaceAgentLifecycleState.SHUTDOWN_TIMEOUT -> SHUTDOWN_TIMEOUT + WorkspaceAgentLifecycleState.SHUTDOWN_ERROR -> SHUTDOWN_ERROR + WorkspaceAgentLifecycleState.OFF -> OFF + } + + WorkspaceAgentStatus.DISCONNECTED -> DISCONNECTED + WorkspaceAgentStatus.TIMEOUT -> TIMEOUT + WorkspaceAgentStatus.CONNECTING -> CONNECTING + else -> RUNNING + } + + WorkspaceStatus.STOPPING -> STOPPING + WorkspaceStatus.STOPPED -> STOPPED + WorkspaceStatus.FAILED -> FAILED + WorkspaceStatus.CANCELING -> CANCELING + WorkspaceStatus.CANCELED -> CANCELED + WorkspaceStatus.DELETING -> DELETING + WorkspaceStatus.DELETED -> DELETED + } + + fun from(str: String) = WorkspaceAndAgentStatus.values().first { it.label.contains(str, true) } + } +} diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt index 2b685f77..ca9f6281 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -5,10 +5,7 @@ import com.coder.gateway.icons.CoderIcons import com.coder.gateway.models.CoderWorkspacesWizardModel import com.coder.gateway.models.TokenSource import com.coder.gateway.models.WorkspaceAgentModel -import com.coder.gateway.models.WorkspaceAgentStatus -import com.coder.gateway.models.WorkspaceAgentStatus.FAILED -import com.coder.gateway.models.WorkspaceAgentStatus.RUNNING -import com.coder.gateway.models.WorkspaceAgentStatus.STOPPED +import com.coder.gateway.models.WorkspaceAndAgentStatus import com.coder.gateway.models.WorkspaceVersionStatus import com.coder.gateway.sdk.Arch import com.coder.gateway.sdk.CoderCLIManager @@ -24,6 +21,7 @@ import com.coder.gateway.sdk.ex.TemplateResponseException import com.coder.gateway.sdk.ex.WorkspaceResponseException import com.coder.gateway.sdk.toURL import com.coder.gateway.sdk.v2.models.Workspace +import com.coder.gateway.sdk.v2.models.WorkspaceStatus import com.coder.gateway.sdk.withPath import com.coder.gateway.services.CoderSettingsState import com.intellij.ide.ActivityTracker @@ -134,8 +132,12 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod setEmptyState("Disconnected") setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.addListSelectionListener { - setNextButtonEnabled(selectedObject != null && selectedObject?.agentStatus == RUNNING && selectedObject?.agentOS == OS.LINUX) - if (selectedObject?.agentStatus == RUNNING && selectedObject?.agentOS != OS.LINUX) { + val ready = listOf( + WorkspaceAndAgentStatus.READY, WorkspaceAndAgentStatus.START_ERROR, + WorkspaceAndAgentStatus.START_TIMEOUT, WorkspaceAndAgentStatus.AGENT_STARTING_READY + ).contains(selectedObject?.agentStatus) + setNextButtonEnabled(ready && selectedObject?.agentOS == OS.LINUX) + if (ready && selectedObject?.agentOS != OS.LINUX) { notificationBanner.apply { component.isVisible = true showInfo(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.unsupported.os.info")) @@ -384,8 +386,8 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod private fun updateWorkspaceActions() { goToDashboardAction.isEnabled = coderClient.isReady createWorkspaceAction.isEnabled = coderClient.isReady - when (tableOfWorkspaces.selectedObject?.agentStatus) { - RUNNING -> { + when (tableOfWorkspaces.selectedObject?.workspaceStatus) { + WorkspaceStatus.RUNNING -> { startWorkspaceAction.isEnabled = false stopWorkspaceAction.isEnabled = true when (tableOfWorkspaces.selectedObject?.status) { @@ -395,7 +397,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod } - STOPPED, FAILED -> { + WorkspaceStatus.STOPPED, WorkspaceStatus.FAILED -> { startWorkspaceAction.isEnabled = true stopWorkspaceAction.isEnabled = false when (tableOfWorkspaces.selectedObject?.status) { @@ -698,7 +700,8 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod this.templateIcon, null, WorkspaceVersionStatus.from(this), - WorkspaceAgentStatus.from(this), + this.latestBuild.status, + WorkspaceAndAgentStatus.from(this, agent), this.latestBuild.transition, OS.from(agent.operatingSystem), Arch.from(agent.architecture), @@ -723,7 +726,8 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod this.templateIcon, null, WorkspaceVersionStatus.from(this), - WorkspaceAgentStatus.from(this), + this.latestBuild.status, + WorkspaceAndAgentStatus.from(this), this.latestBuild.transition, null, null, @@ -918,7 +922,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column) if (value is String) { text = value - foreground = WorkspaceAgentStatus.from(value).statusColor() + foreground = WorkspaceAndAgentStatus.from(value).statusColor() } font = this@CoderWorkspacesStepView.tableOfWorkspaces.tableHeader.font border = JBUI.Borders.empty(0, 8) diff --git a/src/test/groovy/CoderCLIManagerTest.groovy b/src/test/groovy/CoderCLIManagerTest.groovy index d06690ab..8f09d4c7 100644 --- a/src/test/groovy/CoderCLIManagerTest.groovy +++ b/src/test/groovy/CoderCLIManagerTest.groovy @@ -1,8 +1,9 @@ package com.coder.gateway.sdk import com.coder.gateway.models.WorkspaceAgentModel -import com.coder.gateway.models.WorkspaceAgentStatus +import com.coder.gateway.models.WorkspaceAndAgentStatus import com.coder.gateway.models.WorkspaceVersionStatus +import com.coder.gateway.sdk.v2.models.WorkspaceStatus import com.coder.gateway.sdk.v2.models.WorkspaceTransition import com.sun.net.httpserver.HttpExchange import com.sun.net.httpserver.HttpHandler @@ -373,7 +374,8 @@ class CoderCLIManagerTest extends Specification { "template-icon-path", null, WorkspaceVersionStatus.UPDATED, - WorkspaceAgentStatus.RUNNING, + WorkspaceStatus.RUNNING, + WorkspaceAndAgentStatus.READY, WorkspaceTransition.START, null, null, From 5f31f552687886462e6316e1ac2b49a566f54cbb Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 25 Apr 2023 12:55:29 -0800 Subject: [PATCH 05/10] Remove unused expressions --- .../gateway/views/steps/CoderWorkspacesStepView.kt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt index ca9f6281..e888ad85 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -814,10 +814,6 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod override fun getComparator(): Comparator { return Comparator { a, b -> - if (a === b) 0 - if (a == null) -1 - if (b == null) 1 - a.name.compareTo(b.name, ignoreCase = true) } } @@ -845,10 +841,6 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod override fun getComparator(): java.util.Comparator { return Comparator { a, b -> - if (a === b) 0 - if (a == null) -1 - if (b == null) 1 - a.templateName.compareTo(b.templateName, ignoreCase = true) } } @@ -908,10 +900,6 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod override fun getComparator(): java.util.Comparator { return Comparator { a, b -> - if (a === b) 0 - if (a == null) -1 - if (b == null) 1 - a.agentStatus.label.compareTo(b.agentStatus.label, ignoreCase = true) } } From 176d868c3df4397744ca893bc1092d34d06348a7 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 25 Apr 2023 13:39:39 -0800 Subject: [PATCH 06/10] Add tooltips to describe the status --- .../gateway/models/WorkspaceAndAgentStatus.kt | 35 +++++++++++++------ .../views/steps/CoderWorkspacesStepView.kt | 10 +++++- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt index 23082482..820dbe09 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt @@ -11,20 +11,33 @@ import com.intellij.ui.JBColor * WorkspaceAndAgentStatus represents the combined status of a single agent and * its workspace (or just the workspace if there are no agents). */ -enum class WorkspaceAndAgentStatus(val label: String) { +enum class WorkspaceAndAgentStatus(val label: String, val description: String) { // Workspace states. - QUEUED("◍ Queued"), STARTING("⦿ Starting"), FAILED("ⓧ Failed"), - DELETING("⦸ Deleting"), DELETED("⦸ Deleted"), - STOPPING("◍ Stopping"), STOPPED("◍ Stopped"), - CANCELING("◍ Canceling action"), CANCELED("◍ Canceled action"), - RUNNING("⦿ Running"), + QUEUED("◍ Queued", "The workspace is queueing to start."), + STARTING("⦿ Starting", "The workspace is starting."), + FAILED("ⓧ Failed", "The workspace has failed to start."), + DELETING("⦸ Deleting", "The workspace is being deleted."), + DELETED("⦸ Deleted", "The workspace has been deleted."), + STOPPING("◍ Stopping", "The workspace is stopping."), + STOPPED("◍ Stopped", "The workspace has stopped."), + CANCELING("◍ Canceling action", "The workspace is being canceled."), + CANCELED("◍ Canceled action", "The workspace has been canceled."), + RUNNING("⦿ Running", "The workspace is running, waiting for agents."), // Agent states. - OFF("⦸ Off"), CONNECTING("⦿ Connecting"), DISCONNECTED("⦸ Disconnected"), TIMEOUT("ⓧ Timeout"), - AGENT_STARTING("⦿ Starting"), AGENT_STARTING_READY("⦿ Starting"), - CREATED("⦿ Created"), START_ERROR("◍ Started with error"), START_TIMEOUT("◍ Started with timeout"), - SHUTTING_DOWN("◍ Shutting down"), SHUTDOWN_ERROR("⦸ Shutdown with error"), SHUTDOWN_TIMEOUT("⦸ Shutdown with timeout"), - READY("⦿ Ready"); + CONNECTING("⦿ Connecting", "The agent is connecting."), + DISCONNECTED("⦸ Disconnected", "The agent has disconnected."), + TIMEOUT("ⓧ Timeout", "The agent has timed out."), + AGENT_STARTING("⦿ Starting", "The agent is running the startup script."), + AGENT_STARTING_READY("⦿ Starting", "The agent is running the startup script but is ready to accept connections."), + CREATED("⦿ Created", "The agent has been created."), + START_ERROR("◍ Started with error", "The agent is ready but the startup script errored."), + START_TIMEOUT("◍ Started with timeout", "The agent is ready but the startup script timed out"), + SHUTTING_DOWN("◍ Shutting down", "The agent is shutting down."), + SHUTDOWN_ERROR("⦸ Shutdown with error", "The agent shut down but the shutdown script errored."), + SHUTDOWN_TIMEOUT("⦸ Shutdown with timeout", "The agent shut down but the shutdown script timed out."), + OFF("⦸ Off", "The agent has shut down."), + READY("⦿ Ready", "The agent is ready to accept connections."); fun statusColor(): JBColor = when (this) { READY, AGENT_STARTING_READY -> JBColor.GREEN diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt index e888ad85..7ea696a8 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -906,11 +906,19 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod override fun getRenderer(item: WorkspaceAgentModel?): TableCellRenderer { return object : DefaultTableCellRenderer() { - override fun getTableCellRendererComponent(table: JTable, value: Any, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int): Component { + override fun getTableCellRendererComponent( + table: JTable, + value: Any, + isSelected: Boolean, + hasFocus: Boolean, + row: Int, + column: Int, + ): Component { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column) if (value is String) { text = value foreground = WorkspaceAndAgentStatus.from(value).statusColor() + toolTipText = WorkspaceAndAgentStatus.from(value).description } font = this@CoderWorkspacesStepView.tableOfWorkspaces.tableHeader.font border = JBUI.Borders.empty(0, 8) From d5abf2b71fc511c53aecf41ad6251a1a8a568cc1 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 25 Apr 2023 14:06:24 -0800 Subject: [PATCH 07/10] Simplify some borders --- .../com/coder/gateway/views/steps/CoderWorkspacesStepView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt index 7ea696a8..f9272379 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -272,7 +272,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod }.apply { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() - border = JBUI.Borders.empty(0, 16, 0, 16) + border = JBUI.Borders.empty(0, 16) } override val previousActionText = IdeBundle.message("button.back") @@ -799,7 +799,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod override fun getTableCellRendererComponent(table: JTable?, value: Any?, selected: Boolean, focus: Boolean, row: Int, column: Int): Component { super.getTableCellRendererComponent(table, value, selected, focus, row, column).apply { - border = JBUI.Borders.empty(8, 8) + border = JBUI.Borders.empty(8) } return this } From 596934c5ddd0a2bed1ec56c60557533996cc33c2 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 26 Apr 2023 08:55:57 -0800 Subject: [PATCH 08/10] Fix start/shutdown timeout status These mean the script is still running and is taking longer than expected. Also we can connect with login_before_ready when in the start_timeout state. --- .../gateway/models/WorkspaceAndAgentStatus.kt | 26 ++++++++++--------- .../views/steps/CoderWorkspacesStepView.kt | 6 +++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt index 820dbe09..18b2af59 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt @@ -28,21 +28,22 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { CONNECTING("⦿ Connecting", "The agent is connecting."), DISCONNECTED("⦸ Disconnected", "The agent has disconnected."), TIMEOUT("ⓧ Timeout", "The agent has timed out."), - AGENT_STARTING("⦿ Starting", "The agent is running the startup script."), - AGENT_STARTING_READY("⦿ Starting", "The agent is running the startup script but is ready to accept connections."), + AGENT_STARTING("⦿ Starting", "The startup script is running."), + AGENT_STARTING_READY("⦿ Starting", "The startup script is still running but the agent is ready to accept connections."), CREATED("⦿ Created", "The agent has been created."), START_ERROR("◍ Started with error", "The agent is ready but the startup script errored."), - START_TIMEOUT("◍ Started with timeout", "The agent is ready but the startup script timed out"), + START_TIMEOUT("◍ Starting", "The startup script is taking longer than expected."), + START_TIMEOUT_READY("◍ Starting", "The startup script is taking longer than expected but the agent is ready to accept connections."), SHUTTING_DOWN("◍ Shutting down", "The agent is shutting down."), SHUTDOWN_ERROR("⦸ Shutdown with error", "The agent shut down but the shutdown script errored."), - SHUTDOWN_TIMEOUT("⦸ Shutdown with timeout", "The agent shut down but the shutdown script timed out."), + SHUTDOWN_TIMEOUT("⦸ Shutting down", "The shutdown script is taking longer than expected."), OFF("⦸ Off", "The agent has shut down."), READY("⦿ Ready", "The agent is ready to accept connections."); fun statusColor(): JBColor = when (this) { - READY, AGENT_STARTING_READY -> JBColor.GREEN - START_ERROR, START_TIMEOUT -> JBColor.YELLOW - FAILED, DISCONNECTED, TIMEOUT, SHUTTING_DOWN, SHUTDOWN_ERROR, SHUTDOWN_TIMEOUT -> JBColor.RED + READY, AGENT_STARTING_READY, START_TIMEOUT_READY -> JBColor.GREEN + START_ERROR, START_TIMEOUT, SHUTDOWN_TIMEOUT -> JBColor.YELLOW + FAILED, DISCONNECTED, TIMEOUT, SHUTDOWN_ERROR -> JBColor.RED else -> if (JBColor.isBright()) JBColor.LIGHT_GRAY else JBColor.DARK_GRAY } @@ -50,12 +51,13 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { // `connected`, and the agent lifecycle state is `ready` to ensure the best // possible scenario for attempting a connection. // - // We can also choose to allow `start_timeout` and `start_error` for the - // agent state; this means the startup script did not successfully complete - // but the agent will accept SSH connections. + // We can also choose to allow `start_error` for the agent lifecycle state; + // this means the startup script did not successfully complete but the agent + // will still accept SSH connections. // // Lastly we can also allow connections when the agent lifecycle state is - // `starting` if `login_before_ready` is true on the workspace response. + // `starting` or `start_timeout` if `login_before_ready` is true on the + // workspace response since this bypasses the need to wait for the script. // // Note that latest_build.status is derived from latest_build.job.status and // latest_build.job.transition so there is no need to check those. @@ -67,7 +69,7 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { WorkspaceAgentStatus.CONNECTED -> when (agent.lifecycleState) { WorkspaceAgentLifecycleState.CREATED -> CREATED WorkspaceAgentLifecycleState.STARTING -> if (agent.loginBeforeReady == true) AGENT_STARTING_READY else AGENT_STARTING - WorkspaceAgentLifecycleState.START_TIMEOUT -> START_TIMEOUT + WorkspaceAgentLifecycleState.START_TIMEOUT -> if (agent.loginBeforeReady == true) START_TIMEOUT_READY else START_TIMEOUT WorkspaceAgentLifecycleState.START_ERROR -> START_ERROR WorkspaceAgentLifecycleState.READY -> READY WorkspaceAgentLifecycleState.SHUTTING_DOWN -> SHUTTING_DOWN diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt index f9272379..4b625f43 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -133,8 +133,10 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.addListSelectionListener { val ready = listOf( - WorkspaceAndAgentStatus.READY, WorkspaceAndAgentStatus.START_ERROR, - WorkspaceAndAgentStatus.START_TIMEOUT, WorkspaceAndAgentStatus.AGENT_STARTING_READY + WorkspaceAndAgentStatus.READY, + WorkspaceAndAgentStatus.START_ERROR, + WorkspaceAndAgentStatus.AGENT_STARTING_READY, + WorkspaceAndAgentStatus.START_TIMEOUT_READY, ).contains(selectedObject?.agentStatus) setNextButtonEnabled(ready && selectedObject?.agentOS == OS.LINUX) if (ready && selectedObject?.agentOS != OS.LINUX) { From a70a32bf0a4f299186c7c78190094ffd488b13d6 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 26 Apr 2023 09:14:47 -0800 Subject: [PATCH 09/10] Extract ready logic to WorkspaceAndAgentStatus --- .../coder/gateway/models/WorkspaceAndAgentStatus.kt | 8 ++++++++ .../gateway/views/steps/CoderWorkspacesStepView.kt | 10 ++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt index 18b2af59..ef022e5f 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt @@ -47,6 +47,14 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { else -> if (JBColor.isBright()) JBColor.LIGHT_GRAY else JBColor.DARK_GRAY } + /** + * Return true if the agent is in a connectable state. + */ + fun ready(): Boolean { + return listOf(READY, START_ERROR, AGENT_STARTING_READY, START_TIMEOUT_READY) + .contains(this) + } + // 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/gateway/views/steps/CoderWorkspacesStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt index 4b625f43..a0fa03df 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -132,14 +132,8 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod setEmptyState("Disconnected") setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.addListSelectionListener { - val ready = listOf( - WorkspaceAndAgentStatus.READY, - WorkspaceAndAgentStatus.START_ERROR, - WorkspaceAndAgentStatus.AGENT_STARTING_READY, - WorkspaceAndAgentStatus.START_TIMEOUT_READY, - ).contains(selectedObject?.agentStatus) - setNextButtonEnabled(ready && selectedObject?.agentOS == OS.LINUX) - if (ready && selectedObject?.agentOS != OS.LINUX) { + setNextButtonEnabled(selectedObject?.agentStatus?.ready() == true && selectedObject?.agentOS == OS.LINUX) + if (selectedObject?.agentStatus?.ready() == true && selectedObject?.agentOS != OS.LINUX) { notificationBanner.apply { component.isVisible = true showInfo(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.unsupported.os.info")) From 55e4a6191bd6716da48ee48fe780a40b833835cf Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 26 Apr 2023 09:55:49 -0800 Subject: [PATCH 10/10] Change agent timeout description I think it still has a chance to connect so this wording makes that clearer. --- .../kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt index ef022e5f..2ffaa43b 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt @@ -27,7 +27,7 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { // Agent states. CONNECTING("⦿ Connecting", "The agent is connecting."), DISCONNECTED("⦸ Disconnected", "The agent has disconnected."), - TIMEOUT("ⓧ Timeout", "The agent has timed out."), + TIMEOUT("ⓧ Timeout", "The agent is taking longer than expected to connect."), AGENT_STARTING("⦿ Starting", "The startup script is running."), AGENT_STARTING_READY("⦿ Starting", "The startup script is still running but the agent is ready to accept connections."), CREATED("⦿ Created", "The agent has been created."),