diff --git a/src/main/kotlin/com/coder/gateway/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/gateway/CoderRemoteEnvironment.kt index 8f73152bf..b4d7c43ed 100644 --- a/src/main/kotlin/com/coder/gateway/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/gateway/CoderRemoteEnvironment.kt @@ -4,12 +4,15 @@ import com.coder.gateway.models.WorkspaceAndAgentStatus import com.coder.gateway.sdk.CoderRestClient import com.coder.gateway.sdk.v2.models.Workspace import com.coder.gateway.sdk.v2.models.WorkspaceAgent +import com.coder.gateway.util.withPath +import com.coder.gateway.views.Action import com.coder.gateway.views.EnvironmentView import com.jetbrains.toolbox.gateway.AbstractRemoteProviderEnvironment import com.jetbrains.toolbox.gateway.EnvironmentVisibilityState import com.jetbrains.toolbox.gateway.environments.EnvironmentContentsView import com.jetbrains.toolbox.gateway.states.EnvironmentStateConsumer import com.jetbrains.toolbox.gateway.ui.ObservablePropertiesFactory +import com.jetbrains.toolbox.gateway.ui.ToolboxUi import java.util.concurrent.CompletableFuture /** @@ -19,18 +22,60 @@ import java.util.concurrent.CompletableFuture */ class CoderRemoteEnvironment( private val client: CoderRestClient, - private val workspace: Workspace, - private val agent: WorkspaceAgent, + private var workspace: Workspace, + private var agent: WorkspaceAgent, + private val ui: ToolboxUi, observablePropertiesFactory: ObservablePropertiesFactory, ) : AbstractRemoteProviderEnvironment(observablePropertiesFactory) { override fun getId(): String = "${workspace.name}.${agent.name}" override fun getName(): String = "${workspace.name}.${agent.name}" private var status = WorkspaceAndAgentStatus.from(workspace, agent) + init { + actionsList.add( + Action("Open web terminal") { + ui.openUrl(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) + }, + ) + actionsList.add( + Action("Open in dashboard") { + ui.openUrl(client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()) + }, + ) + actionsList.add( + Action("View template") { + ui.openUrl(client.url.withPath("/templates/${workspace.templateName}").toString()) + }, + ) + actionsList.add( + Action("Start", enabled = { status.canStart() }) { + val build = client.startWorkspace(workspace) + workspace = workspace.copy(latestBuild = build) + update(workspace, agent) + }, + ) + actionsList.add( + Action("Stop", enabled = { status.ready() || status.pending() }) { + val build = client.stopWorkspace(workspace) + workspace = workspace.copy(latestBuild = build) + update(workspace, agent) + }, + ) + actionsList.add( + Action("Update", enabled = { workspace.outdated }) { + val build = client.updateWorkspace(workspace) + workspace = workspace.copy(latestBuild = build) + update(workspace, agent) + }, + ) + } + /** * Update the workspace/agent status to the listeners, if it has changed. */ fun update(workspace: Workspace, agent: WorkspaceAgent) { + this.workspace = workspace + this.agent = agent val newStatus = WorkspaceAndAgentStatus.from(workspace, agent) if (newStatus != status) { status = newStatus @@ -58,6 +103,11 @@ class CoderRemoteEnvironment( * Immediately send the state to the listener and store for updates. */ override fun addStateListener(consumer: EnvironmentStateConsumer): Boolean { + // TODO@JB: It would be ideal if we could have the workspace state and + // the connected state listed separately, since right now the + // connected state can mask the workspace state. + // TODO@JB: You can still press connect if the environment is + // unreachable. Is that expected? consumer.consume(status.toRemoteEnvironmentState()) return super.addStateListener(consumer) } diff --git a/src/main/kotlin/com/coder/gateway/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/gateway/CoderRemoteProvider.kt index 5d1299626..777b1f812 100644 --- a/src/main/kotlin/com/coder/gateway/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/gateway/CoderRemoteProvider.kt @@ -97,7 +97,7 @@ class CoderRemoteProvider( it.name }?.map { agent -> // If we have an environment already, update that. - val env = CoderRemoteEnvironment(client, ws, agent, observablePropertiesFactory) + val env = CoderRemoteEnvironment(client, ws, agent, ui, observablePropertiesFactory) lastEnvironments?.firstOrNull { it == env }?.let { it.update(ws, agent) it diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt index bc20e4dae..5425e94ca 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt @@ -52,11 +52,8 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { * Return the environment state for Toolbox, which tells it the label, color * and whether the environment is reachable. * - * We mark all ready and pending states as reachable since if the workspace - * is pending the cli will wait for it anyway. - * - * Additionally, terminal states like stopped are also marked as reachable, - * since the cli will start them. + * Note that a reachable environment will always display "connected" or + * "disconnected" regardless of the label we give that status. */ fun toRemoteEnvironmentState(): CustomRemoteEnvironmentState { // Use comments; no named arguments for non-Kotlin functions. @@ -67,7 +64,7 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { Color(104, 112, 128, 255), // lightThemeColor Color(224, 224, 240, 26), // darkThemeBackgroundColor Color(224, 224, 245, 250), // lightThemeBackgroundColor - ready() || pending() || canStart(), // reachable + ready(), // reachable // TODO@JB: How does this work? Would like a spinner for pending states. null, // iconId ) diff --git a/src/main/kotlin/com/coder/gateway/sdk/v2/models/Workspace.kt b/src/main/kotlin/com/coder/gateway/sdk/v2/models/Workspace.kt index 94c129af1..fad62c92b 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/v2/models/Workspace.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/v2/models/Workspace.kt @@ -18,4 +18,5 @@ data class Workspace( @Json(name = "latest_build") val latestBuild: WorkspaceBuild, @Json(name = "outdated") val outdated: Boolean, @Json(name = "name") val name: String, + @Json(name = "owner_name") val ownerName: String, ) diff --git a/src/main/kotlin/com/coder/gateway/views/CoderPage.kt b/src/main/kotlin/com/coder/gateway/views/CoderPage.kt index 4e9ce4352..1ee77849d 100644 --- a/src/main/kotlin/com/coder/gateway/views/CoderPage.kt +++ b/src/main/kotlin/com/coder/gateway/views/CoderPage.kt @@ -116,11 +116,13 @@ abstract class CoderPage( */ class Action( private val label: String, - private val closesPage: Boolean, + private val closesPage: Boolean = false, + private val enabled: () -> Boolean = { true }, private val cb: () -> Unit, ) : RunnableActionDescription { override fun getLabel(): String = label override fun getShouldClosePage(): Boolean = closesPage + override fun isEnabled(): Boolean = enabled() override fun run() { cb() } diff --git a/src/test/kotlin/com/coder/gateway/sdk/DataGen.kt b/src/test/kotlin/com/coder/gateway/sdk/DataGen.kt index 8e37e64ad..fda2d4181 100644 --- a/src/test/kotlin/com/coder/gateway/sdk/DataGen.kt +++ b/src/test/kotlin/com/coder/gateway/sdk/DataGen.kt @@ -53,6 +53,7 @@ class DataGen { ), outdated = false, name = name, + ownerName = "owner", ) }