Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fa7d3d1

Browse files
authoredMay 8, 2023
Add status and start/stop buttons to recents view (#243)
* Split client out of client service This way we can create multiple clients on the recent workspaces page without having to do the whole init thing for each one. * Store config directory in recent connection We need this information so we can query the status of recent connections as they could belong to multiple deployments. This could end up desyncing if the user manually edits their config file and changes the global config path in ProxyCommand. The alternative would be to parse the SSH config to make sure we have the right config directory but that would mean parsing ProxyCommand to extract the value of --global-config. As a fallback for connections that already exist and are not yet stored with the config directory we could split the host name itself on `--` since it has the domain in it and join with the default directory but this could be inaccurate if the default has been changed or if in the future we change the host name format. * Store name in recent connection So we can match against the API response. We could split the hostname on `--` but there are cases where that will fail (when the name or domain itself contains -- in specific configurations). We have to add the config path anyway so this is the best opportunity to add more information. * Standardize some casing * Ignore null hostnames I guess this could happen if you manually edit the recents? In any case if there is no host name I am not sure there is value in trying to show the connection and it is making it difficult to check if the workspace is up. * Break out toAgentModels We will need this in the recent connections view as well. * Split agent status icon and label So we can show just the icon in the recent connections view. * Add status and start/stop buttons to recent connections This relies on some new data being stored with the recent connections so old connections will be in an unknown state. * Simplify recent workspace connection constructor
1 parent b3033b7 commit fa7d3d1

17 files changed

+427
-208
lines changed
 

‎src/main/kotlin/com/coder/gateway/WorkspaceParams.kt

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ private const val IDE_PRODUCT_CODE = "ide_product_code"
2323
private const val IDE_BUILD_NUMBER = "ide_build_number"
2424
private const val IDE_PATH_ON_HOST = "ide_path_on_host"
2525
private const val WEB_TERMINAL_LINK = "web_terminal_link"
26+
private const val CONFIG_DIRECTORY = "config_directory"
27+
private const val NAME = "name"
2628

2729
private val localTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm")
2830

@@ -33,7 +35,9 @@ fun RecentWorkspaceConnection.toWorkspaceParams(): Map<String, String> {
3335
PROJECT_PATH to this.projectPath!!,
3436
IDE_PRODUCT_CODE to IntelliJPlatformProduct.fromProductCode(this.ideProductCode!!)!!.productCode,
3537
IDE_BUILD_NUMBER to "${this.ideBuildNumber}",
36-
WEB_TERMINAL_LINK to "${this.webTerminalLink}"
38+
WEB_TERMINAL_LINK to "${this.webTerminalLink}",
39+
CONFIG_DIRECTORY to "${this.configDirectory}",
40+
NAME to "${this.name}"
3741
)
3842

3943
if (!this.downloadSource.isNullOrBlank()) {
@@ -80,6 +84,19 @@ fun Map<String, String>.withWebTerminalLink(webTerminalLink: String): Map<String
8084
return map
8185
}
8286

87+
fun Map<String, String>.withConfigDirectory(dir: String): Map<String, String> {
88+
val map = this.toMutableMap()
89+
map[CONFIG_DIRECTORY] = dir
90+
return map
91+
}
92+
93+
fun Map<String, String>.withName(name: String): Map<String, String> {
94+
val map = this.toMutableMap()
95+
map[NAME] = name
96+
return map
97+
}
98+
99+
83100
fun Map<String, String>.areCoderType(): Boolean {
84101
return this[TYPE] == VALUE_FOR_TYPE && !this[CODER_WORKSPACE_HOSTNAME].isNullOrBlank() && !this[PROJECT_PATH].isNullOrBlank()
85102
}
@@ -140,7 +157,9 @@ fun Map<String, String>.toRecentWorkspaceConnection(): RecentWorkspaceConnection
140157
this[IDE_BUILD_NUMBER]!!,
141158
this[IDE_DOWNLOAD_LINK]!!,
142159
null,
143-
this[WEB_TERMINAL_LINK]!!
160+
this[WEB_TERMINAL_LINK]!!,
161+
this[CONFIG_DIRECTORY]!!,
162+
this[NAME]!!,
144163
) else RecentWorkspaceConnection(
145164
this.workspaceHostname(),
146165
this.projectPath(),
@@ -149,6 +168,8 @@ fun Map<String, String>.toRecentWorkspaceConnection(): RecentWorkspaceConnection
149168
this[IDE_BUILD_NUMBER]!!,
150169
null,
151170
this[IDE_PATH_ON_HOST],
152-
this[WEB_TERMINAL_LINK]!!
171+
this[WEB_TERMINAL_LINK]!!,
172+
this[CONFIG_DIRECTORY]!!,
173+
this[NAME]!!,
153174
)
154-
}
175+
}

‎src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ object CoderIcons {
88

99
val OPEN_TERMINAL = IconLoader.getIcon("open_terminal.svg", javaClass)
1010

11+
val PENDING = IconLoader.getIcon("pending.svg", javaClass)
12+
val RUNNING = IconLoader.getIcon("running.svg", javaClass)
13+
val OFF = IconLoader.getIcon("off.svg", javaClass)
14+
1115
val HOME = IconLoader.getIcon("homeFolder.svg", javaClass)
1216
val CREATE = IconLoader.getIcon("create.svg", javaClass)
1317
val RUN = IconLoader.getIcon("run.svg", javaClass)
@@ -55,4 +59,4 @@ object CoderIcons {
5559
val Y = IconLoader.getIcon("y.svg", javaClass)
5660
val Z = IconLoader.getIcon("z.svg", javaClass)
5761

58-
}
62+
}

‎src/main/kotlin/com/coder/gateway/models/CoderWorkspacesWizardModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ data class CoderWorkspacesWizardModel(
1111
var token: Pair<String, TokenSource>? = null,
1212
var selectedWorkspace: WorkspaceAgentModel? = null,
1313
var useExistingToken: Boolean = false,
14+
var configDirectory: String = "",
1415
)

‎src/main/kotlin/com/coder/gateway/models/RecentWorkspaceConnection.kt

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,28 @@ package com.coder.gateway.models
33
import com.intellij.openapi.components.BaseState
44
import com.intellij.util.xmlb.annotations.Attribute
55

6-
class RecentWorkspaceConnection() : BaseState(), Comparable<RecentWorkspaceConnection> {
7-
constructor(hostname: String, prjPath: String, openedAt: String, productCode: String, buildNumber: String, source: String?, idePath: String?, terminalLink: String) : this() {
8-
coderWorkspaceHostname = hostname
9-
projectPath = prjPath
10-
lastOpened = openedAt
11-
ideProductCode = productCode
12-
ideBuildNumber = buildNumber
13-
downloadSource = source
14-
idePathOnHost = idePath
15-
webTerminalLink = terminalLink
16-
}
17-
6+
class RecentWorkspaceConnection(
187
@get:Attribute
19-
var coderWorkspaceHostname by string()
20-
8+
var coderWorkspaceHostname: String? = null,
219
@get:Attribute
22-
var projectPath by string()
23-
10+
var projectPath: String? = null,
2411
@get:Attribute
25-
var lastOpened by string()
26-
12+
var lastOpened: String? = null,
2713
@get:Attribute
28-
var ideProductCode by string()
29-
14+
var ideProductCode: String? = null,
3015
@get:Attribute
31-
var ideBuildNumber by string()
32-
16+
var ideBuildNumber: String? = null,
3317
@get:Attribute
34-
var downloadSource by string()
35-
36-
18+
var downloadSource: String? = null,
3719
@get:Attribute
38-
var idePathOnHost by string()
39-
20+
var idePathOnHost: String? = null,
4021
@get:Attribute
41-
var webTerminalLink by string()
42-
22+
var webTerminalLink: String? = null,
23+
@get:Attribute
24+
var configDirectory: String? = null,
25+
@get:Attribute
26+
var name: String? = null,
27+
) : BaseState(), Comparable<RecentWorkspaceConnection> {
4328
override fun equals(other: Any?): Boolean {
4429
if (this === other) return true
4530
if (javaClass != other?.javaClass) return false
@@ -88,11 +73,11 @@ class RecentWorkspaceConnection() : BaseState(), Comparable<RecentWorkspaceConne
8873
if (m != null && m != 0) return m
8974

9075
val n = other.idePathOnHost?.let { idePathOnHost?.compareTo(it) }
91-
if (n != null && m != 0) return n
76+
if (n != null && n != 0) return n
9277

9378
val o = other.webTerminalLink?.let { webTerminalLink?.compareTo(it) }
94-
if (o != null && n != 0) return o
79+
if (o != null && o != 0) return o
9580

9681
return 0
9782
}
98-
}
83+
}

‎src/main/kotlin/com/coder/gateway/models/RecentWorkspaceConnectionState.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ class RecentWorkspaceConnectionState : BaseState() {
88
var recentConnections by treeSet<RecentWorkspaceConnection>()
99

1010
fun add(connection: RecentWorkspaceConnection): Boolean {
11-
// if the item is already there but with a different last update timestamp, remove it
11+
// If the item is already there but with a different last updated
12+
// timestamp or config directory, remove it.
1213
recentConnections.remove(connection)
13-
// and add it again with the new timestamp
1414
val result = recentConnections.add(connection)
1515
if (result) incrementModificationCount()
1616
return result
@@ -21,4 +21,4 @@ class RecentWorkspaceConnectionState : BaseState() {
2121
if (result) incrementModificationCount()
2222
return result
2323
}
24-
}
24+
}

‎src/main/kotlin/com/coder/gateway/models/WorkspaceAndAgentStatus.kt

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,46 @@
11
package com.coder.gateway.models
22

3+
import com.coder.gateway.icons.CoderIcons
34
import com.coder.gateway.sdk.v2.models.Workspace
45
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
56
import com.coder.gateway.sdk.v2.models.WorkspaceAgentLifecycleState
67
import com.coder.gateway.sdk.v2.models.WorkspaceAgentStatus
78
import com.coder.gateway.sdk.v2.models.WorkspaceStatus
89
import com.intellij.ui.JBColor
10+
import javax.swing.Icon
911

1012
/**
1113
* WorkspaceAndAgentStatus represents the combined status of a single agent and
1214
* its workspace (or just the workspace if there are no agents).
1315
*/
14-
enum class WorkspaceAndAgentStatus(val label: String, val description: String) {
16+
enum class WorkspaceAndAgentStatus(val icon: Icon, val label: String, val description: String) {
1517
// Workspace states.
16-
QUEUED("Queued", "The workspace is queueing to start."),
17-
STARTING("⦿ Starting", "The workspace is starting."),
18-
FAILED("Failed", "The workspace has failed to start."),
19-
DELETING("Deleting", "The workspace is being deleted."),
20-
DELETED("Deleted", "The workspace has been deleted."),
21-
STOPPING("Stopping", "The workspace is stopping."),
22-
STOPPED("Stopped", "The workspace has stopped."),
23-
CANCELING("Canceling action", "The workspace is being canceled."),
24-
CANCELED("Canceled action", "The workspace has been canceled."),
25-
RUNNING("⦿ Running", "The workspace is running, waiting for agents."),
18+
QUEUED(CoderIcons.PENDING, "Queued", "The workspace is queueing to start."),
19+
STARTING(CoderIcons.PENDING, "Starting", "The workspace is starting."),
20+
FAILED(CoderIcons.OFF, "Failed", "The workspace has failed to start."),
21+
DELETING(CoderIcons.PENDING, "Deleting", "The workspace is being deleted."),
22+
DELETED(CoderIcons.OFF, "Deleted", "The workspace has been deleted."),
23+
STOPPING(CoderIcons.PENDING, "Stopping", "The workspace is stopping."),
24+
STOPPED(CoderIcons.OFF, "Stopped", "The workspace has stopped."),
25+
CANCELING(CoderIcons.PENDING, "Canceling action", "The workspace is being canceled."),
26+
CANCELED(CoderIcons.OFF, "Canceled action", "The workspace has been canceled."),
27+
RUNNING(CoderIcons.RUN, "Running", "The workspace is running, waiting for agents."),
2628

2729
// Agent states.
28-
CONNECTING("⦿ Connecting", "The agent is connecting."),
29-
DISCONNECTED("Disconnected", "The agent has disconnected."),
30-
TIMEOUT("Timeout", "The agent is taking longer than expected to connect."),
31-
AGENT_STARTING("⦿ Starting", "The startup script is running."),
32-
AGENT_STARTING_READY("⦿ Starting", "The startup script is still running but the agent is ready to accept connections."),
33-
CREATED("⦿ Created", "The agent has been created."),
34-
START_ERROR("Started with error", "The agent is ready but the startup script errored."),
35-
START_TIMEOUT("Starting", "The startup script is taking longer than expected."),
36-
START_TIMEOUT_READY("Starting", "The startup script is taking longer than expected but the agent is ready to accept connections."),
37-
SHUTTING_DOWN("Shutting down", "The agent is shutting down."),
38-
SHUTDOWN_ERROR("Shutdown with error", "The agent shut down but the shutdown script errored."),
39-
SHUTDOWN_TIMEOUT("Shutting down", "The shutdown script is taking longer than expected."),
40-
OFF("Off", "The agent has shut down."),
41-
READY("⦿ Ready", "The agent is ready to accept connections.");
30+
CONNECTING(CoderIcons.PENDING, "Connecting", "The agent is connecting."),
31+
DISCONNECTED(CoderIcons.OFF, "Disconnected", "The agent has disconnected."),
32+
TIMEOUT(CoderIcons.PENDING, "Timeout", "The agent is taking longer than expected to connect."),
33+
AGENT_STARTING(CoderIcons.PENDING, "Starting", "The startup script is running."),
34+
AGENT_STARTING_READY(CoderIcons.RUNNING, "Starting", "The startup script is still running but the agent is ready to accept connections."),
35+
CREATED(CoderIcons.PENDING, "Created", "The agent has been created."),
36+
START_ERROR(CoderIcons.RUNNING, "Started with error", "The agent is ready but the startup script errored."),
37+
START_TIMEOUT(CoderIcons.PENDING, "Starting", "The startup script is taking longer than expected."),
38+
START_TIMEOUT_READY(CoderIcons.RUNNING, "Starting", "The startup script is taking longer than expected but the agent is ready to accept connections."),
39+
SHUTTING_DOWN(CoderIcons.PENDING, "Shutting down", "The agent is shutting down."),
40+
SHUTDOWN_ERROR(CoderIcons.OFF, "Shutdown with error", "The agent shut down but the shutdown script errored."),
41+
SHUTDOWN_TIMEOUT(CoderIcons.OFF, "Shutting down", "The shutdown script is taking longer than expected."),
42+
OFF(CoderIcons.OFF, "Off", "The agent has shut down."),
43+
READY(CoderIcons.RUNNING, "Ready", "The agent is ready to accept connections.");
4244

4345
fun statusColor(): JBColor = when (this) {
4446
READY, AGENT_STARTING_READY, START_TIMEOUT_READY -> JBColor.GREEN
@@ -100,7 +102,5 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) {
100102
WorkspaceStatus.DELETING -> DELETING
101103
WorkspaceStatus.DELETED -> DELETED
102104
}
103-
104-
fun from(str: String) = WorkspaceAndAgentStatus.values().first { it.label.contains(str, true) }
105105
}
106106
}

‎src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class CoderCLIManager @JvmOverloads constructor(
3131
) {
3232
var remoteBinaryURL: URL
3333
var localBinaryPath: Path
34-
private var coderConfigPath: Path
34+
var coderConfigPath: Path
3535

3636
init {
3737
val binaryName = getCoderCLIForOS(getOS(), getArch())

‎src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,30 @@ import java.util.UUID
3131
class CoderRestClientService {
3232
var isReady: Boolean = false
3333
private set
34-
private lateinit var httpClient: OkHttpClient
35-
private lateinit var retroRestClient: CoderV2RestFacade
36-
private lateinit var sessionToken: String
37-
lateinit var coderURL: URL
3834
lateinit var me: User
3935
lateinit var buildVersion: String
36+
lateinit var client: CoderRestClient
4037

4138
/**
42-
* This must be called before anything else. It will authenticate with coder and retrieve a session token
39+
* This must be called before anything else. It will authenticate and load
40+
* information about the current user and the build version.
41+
*
4342
* @throws [AuthenticationResponseException] if authentication failed.
4443
*/
4544
fun initClientSession(url: URL, token: String): User {
45+
client = CoderRestClient(url, token)
46+
me = client.me()
47+
buildVersion = client.buildInfo().version
48+
isReady = true
49+
return me
50+
}
51+
}
52+
53+
class CoderRestClient(var url: URL, private var token: String) {
54+
private var httpClient: OkHttpClient
55+
private var retroRestClient: CoderV2RestFacade
56+
57+
init {
4658
val gson: Gson = GsonBuilder().registerTypeAdapter(Instant::class.java, InstantConverter()).setPrettyPrinting().create()
4759
val pluginVersion = PluginManagerCore.getPlugin(PluginId.getId("com.coder.gateway"))!! // this is the id from the plugin.xml
4860

@@ -54,18 +66,19 @@ class CoderRestClientService {
5466
.build()
5567

5668
retroRestClient = Retrofit.Builder().baseUrl(url.toString()).client(httpClient).addConverterFactory(GsonConverterFactory.create(gson)).build().create(CoderV2RestFacade::class.java)
69+
}
5770

71+
/**
72+
* Retrieve the current user.
73+
* @throws [AuthenticationResponseException] if authentication failed.
74+
*/
75+
fun me(): User {
5876
val userResponse = retroRestClient.me().execute()
5977
if (!userResponse.isSuccessful) {
60-
throw AuthenticationResponseException("Could not retrieve information about logged user:${userResponse.code()}, reason: ${userResponse.message()}")
78+
throw AuthenticationResponseException("Could not retrieve information about logged user:${userResponse.code()}, reason: ${userResponse.message().ifBlank { "no reason provided" }}")
6179
}
6280

63-
coderURL = url
64-
sessionToken = token
65-
me = userResponse.body()!!
66-
buildVersion = buildInfo().version
67-
isReady = true
68-
return me
81+
return userResponse.body()!!
6982
}
7083

7184
/**
@@ -75,24 +88,24 @@ class CoderRestClientService {
7588
fun workspaces(): List<Workspace> {
7689
val workspacesResponse = retroRestClient.workspaces("owner:me").execute()
7790
if (!workspacesResponse.isSuccessful) {
78-
throw WorkspaceResponseException("Could not retrieve Coder Workspaces:${workspacesResponse.code()}, reason: ${workspacesResponse.message()}")
91+
throw WorkspaceResponseException("Could not retrieve Coder Workspaces:${workspacesResponse.code()}, reason: ${workspacesResponse.message().ifBlank { "no reason provided" }}")
7992
}
8093

8194
return workspacesResponse.body()!!.workspaces
8295
}
8396

84-
private fun buildInfo(): BuildInfo {
97+
fun buildInfo(): BuildInfo {
8598
val buildInfoResponse = retroRestClient.buildInfo().execute()
8699
if (!buildInfoResponse.isSuccessful) {
87-
throw java.lang.IllegalStateException("Could not retrieve build information for Coder instance $coderURL, reason:${buildInfoResponse.message()}")
100+
throw java.lang.IllegalStateException("Could not retrieve build information for Coder instance $url, reason:${buildInfoResponse.message().ifBlank { "no reason provided" }}")
88101
}
89102
return buildInfoResponse.body()!!
90103
}
91104

92-
private fun template(templateID: UUID): Template {
105+
fun template(templateID: UUID): Template {
93106
val templateResponse = retroRestClient.template(templateID).execute()
94107
if (!templateResponse.isSuccessful) {
95-
throw TemplateResponseException("Failed to retrieve template with id: $templateID, reason: ${templateResponse.message()}")
108+
throw TemplateResponseException("Failed to retrieve template with id: $templateID, reason: ${templateResponse.message().ifBlank { "no reason provided" }}")
96109
}
97110
return templateResponse.body()!!
98111
}
@@ -101,7 +114,7 @@ class CoderRestClientService {
101114
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.START, null, null, null, null)
102115
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
103116
if (buildResponse.code() != HTTP_CREATED) {
104-
throw WorkspaceResponseException("Failed to build workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
117+
throw WorkspaceResponseException("Failed to build workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message().ifBlank { "no reason provided" }}")
105118
}
106119

107120
return buildResponse.body()!!
@@ -111,7 +124,7 @@ class CoderRestClientService {
111124
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.STOP, null, null, null, null)
112125
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
113126
if (buildResponse.code() != HTTP_CREATED) {
114-
throw WorkspaceResponseException("Failed to stop workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
127+
throw WorkspaceResponseException("Failed to stop workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message().ifBlank { "no reason provided" }}")
115128
}
116129

117130
return buildResponse.body()!!
@@ -123,9 +136,9 @@ class CoderRestClientService {
123136
val buildRequest = CreateWorkspaceBuildRequest(template.activeVersionID, lastWorkspaceTransition, null, null, null, null)
124137
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
125138
if (buildResponse.code() != HTTP_CREATED) {
126-
throw WorkspaceResponseException("Failed to update workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
139+
throw WorkspaceResponseException("Failed to update workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message().ifBlank { "no reason provided" }}")
127140
}
128141

129142
return buildResponse.body()!!
130143
}
131-
}
144+
}

‎src/main/kotlin/com/coder/gateway/sdk/TemplateIconDownloader.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ import javax.swing.Icon
1818

1919
@Service(Service.Level.APP)
2020
class TemplateIconDownloader {
21-
private val coderClient: CoderRestClientService = service()
21+
private val clientService: CoderRestClientService = service()
2222
private val cache = mutableMapOf<Pair<String, String>, Icon>()
2323

2424
fun load(path: String, workspaceName: String): Icon {
2525
var url: URL? = null
2626
if (path.startsWith("http")) {
2727
url = path.toURL()
2828
} else if (!path.contains(":") && !path.contains("//")) {
29-
url = coderClient.coderURL.withPath(path)
29+
url = clientService.client.url.withPath(path)
3030
}
3131

3232
if (url != null) {
@@ -115,4 +115,4 @@ class TemplateIconDownloader {
115115
else -> CoderIcons.UNKNOWN
116116
}
117117

118-
}
118+
}

‎src/main/kotlin/com/coder/gateway/sdk/v2/models/Workspace.kt

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package com.coder.gateway.sdk.v2.models
22

3+
import com.coder.gateway.models.WorkspaceAgentModel
4+
import com.coder.gateway.models.WorkspaceAndAgentStatus
5+
import com.coder.gateway.models.WorkspaceVersionStatus
6+
import com.coder.gateway.sdk.Arch
7+
import com.coder.gateway.sdk.OS
38
import com.google.gson.annotations.SerializedName
49
import java.time.Instant
510
import java.util.UUID
@@ -24,4 +29,48 @@ data class Workspace(
2429
@SerializedName("autostart_schedule") val autostartSchedule: String?,
2530
@SerializedName("ttl_ms") val ttlMillis: Long?,
2631
@SerializedName("last_used_at") val lastUsedAt: Instant,
27-
)
32+
)
33+
34+
fun Workspace.toAgentModels(): Set<WorkspaceAgentModel> {
35+
val wam = this.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }.map { agent ->
36+
val workspaceWithAgentName = "${this.name}.${agent.name}"
37+
val wm = WorkspaceAgentModel(
38+
this.id,
39+
this.name,
40+
workspaceWithAgentName,
41+
this.templateID,
42+
this.templateName,
43+
this.templateIcon,
44+
null,
45+
WorkspaceVersionStatus.from(this),
46+
this.latestBuild.status,
47+
WorkspaceAndAgentStatus.from(this, agent),
48+
this.latestBuild.transition,
49+
OS.from(agent.operatingSystem),
50+
Arch.from(agent.architecture),
51+
agent.expandedDirectory ?: agent.directory,
52+
)
53+
54+
wm
55+
}.toSet()
56+
if (wam.isNullOrEmpty()) {
57+
val wm = WorkspaceAgentModel(
58+
this.id,
59+
this.name,
60+
this.name,
61+
this.templateID,
62+
this.templateName,
63+
this.templateIcon,
64+
null,
65+
WorkspaceVersionStatus.from(this),
66+
this.latestBuild.status,
67+
WorkspaceAndAgentStatus.from(this),
68+
this.latestBuild.transition,
69+
null,
70+
null,
71+
null
72+
)
73+
return setOf(wm)
74+
}
75+
return wam
76+
}

‎src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt

Lines changed: 189 additions & 24 deletions
Large diffs are not rendered by default.

‎src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import com.coder.gateway.sdk.toURL
1616
import com.coder.gateway.sdk.withPath
1717
import com.coder.gateway.toWorkspaceParams
1818
import com.coder.gateway.views.LazyBrowserLink
19+
import com.coder.gateway.withConfigDirectory
20+
import com.coder.gateway.withName
1921
import com.coder.gateway.withProjectPath
2022
import com.coder.gateway.withWebTerminalLink
2123
import com.coder.gateway.withWorkspaceHostname
@@ -84,7 +86,7 @@ import javax.swing.event.DocumentEvent
8486

8587
class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolean) -> Unit) : CoderWorkspacesWizardStep, Disposable {
8688
private val cs = CoroutineScope(Dispatchers.Main)
87-
private val coderClient: CoderRestClientService = ApplicationManager.getApplication().getService(CoderRestClientService::class.java)
89+
private val clientService: CoderRestClientService = ApplicationManager.getApplication().getService(CoderRestClientService::class.java)
8890

8991
private var ideComboBoxModel = DefaultComboBoxModel<IdeWithStatus>()
9092

@@ -177,13 +179,13 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
177179

178180
tfProject.text = if (selectedWorkspace.homeDirectory.isNullOrBlank()) "/home" else selectedWorkspace.homeDirectory
179181
titleLabel.text = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", selectedWorkspace.name)
180-
terminalLink.url = coderClient.coderURL.withPath("/@${coderClient.me.username}/${selectedWorkspace.name}/terminal").toString()
182+
terminalLink.url = clientService.client.url.withPath("/@${clientService.me.username}/${selectedWorkspace.name}/terminal").toString()
181183

182184
ideResolvingJob = cs.launch {
183185
try {
184186
val ides = suspendingRetryWithExponentialBackOff(
185187
action = { attempt ->
186-
logger.info("Retrieving IDEs...(attempt $attempt)")
188+
logger.info("Retrieving IDEs... (attempt $attempt)")
187189
if (attempt > 1) {
188190
cbIDE.renderer = IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.retrieve.ides.retry", attempt))
189191
}
@@ -334,6 +336,8 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
334336
.withWorkspaceHostname(CoderCLIManager.getHostName(deploymentURL, selectedWorkspace))
335337
.withProjectPath(tfProject.text)
336338
.withWebTerminalLink("${terminalLink.url}")
339+
.withConfigDirectory(wizardModel.configDirectory)
340+
.withName(selectedWorkspace.name)
337341
)
338342
}
339343
return true

‎src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

Lines changed: 35 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import com.coder.gateway.icons.CoderIcons
55
import com.coder.gateway.models.CoderWorkspacesWizardModel
66
import com.coder.gateway.models.TokenSource
77
import com.coder.gateway.models.WorkspaceAgentModel
8-
import com.coder.gateway.models.WorkspaceAndAgentStatus
98
import com.coder.gateway.models.WorkspaceVersionStatus
10-
import com.coder.gateway.sdk.Arch
119
import com.coder.gateway.sdk.CoderCLIManager
1210
import com.coder.gateway.sdk.CoderRestClientService
1311
import com.coder.gateway.sdk.CoderSemVer
@@ -20,8 +18,8 @@ import com.coder.gateway.sdk.ex.AuthenticationResponseException
2018
import com.coder.gateway.sdk.ex.TemplateResponseException
2119
import com.coder.gateway.sdk.ex.WorkspaceResponseException
2220
import com.coder.gateway.sdk.toURL
23-
import com.coder.gateway.sdk.v2.models.Workspace
2421
import com.coder.gateway.sdk.v2.models.WorkspaceStatus
22+
import com.coder.gateway.sdk.v2.models.toAgentModels
2523
import com.coder.gateway.sdk.withPath
2624
import com.coder.gateway.services.CoderSettingsState
2725
import com.intellij.ide.ActivityTracker
@@ -100,7 +98,7 @@ private const val MOUSE_OVER_TEMPLATE_NAME_COLUMN_ON_ROW = "MOUSE_OVER_TEMPLATE_
10098
class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : CoderWorkspacesWizardStep, Disposable {
10199
private val cs = CoroutineScope(Dispatchers.Main)
102100
private var localWizardModel = CoderWorkspacesWizardModel()
103-
private val coderClient: CoderRestClientService = service()
101+
private val clientService: CoderRestClientService = service()
104102
private val iconDownloader: TemplateIconDownloader = service()
105103
private val settings: CoderSettingsState = service()
106104

@@ -145,7 +143,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
145143
val workspace = tblView.selectedObject
146144

147145
if (col == 2 && workspace != null) {
148-
BrowserUtil.browse(coderClient.coderURL.toURI().resolve("/templates/${workspace.templateName}"))
146+
BrowserUtil.browse(clientService.client.url.toURI().resolve("/templates/${workspace.templateName}"))
149147
}
150148
}
151149
}
@@ -270,7 +268,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
270268
private inner class GoToDashboardAction :
271269
AnActionButton(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.dashboard.text"), CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.dashboard.text"), CoderIcons.HOME) {
272270
override fun actionPerformed(p0: AnActionEvent) {
273-
BrowserUtil.browse(coderClient.coderURL)
271+
BrowserUtil.browse(clientService.client.url)
274272
}
275273
}
276274

@@ -282,7 +280,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
282280
cs.launch {
283281
withContext(Dispatchers.IO) {
284282
try {
285-
coderClient.startWorkspace(workspace.workspaceID, workspace.workspaceName)
283+
clientService.client.startWorkspace(workspace.workspaceID, workspace.workspaceName)
286284
loadWorkspaces()
287285
} catch (e: WorkspaceResponseException) {
288286
logger.warn("Could not build workspace ${workspace.name}, reason: $e")
@@ -301,7 +299,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
301299
cs.launch {
302300
withContext(Dispatchers.IO) {
303301
try {
304-
coderClient.updateWorkspace(workspace.workspaceID, workspace.workspaceName, workspace.lastBuildTransition, workspace.templateID)
302+
clientService.client.updateWorkspace(workspace.workspaceID, workspace.workspaceName, workspace.lastBuildTransition, workspace.templateID)
305303
loadWorkspaces()
306304
} catch (e: WorkspaceResponseException) {
307305
logger.warn("Could not update workspace ${workspace.name}, reason: $e")
@@ -322,7 +320,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
322320
cs.launch {
323321
withContext(Dispatchers.IO) {
324322
try {
325-
coderClient.stopWorkspace(workspace.workspaceID, workspace.workspaceName)
323+
clientService.client.stopWorkspace(workspace.workspaceID, workspace.workspaceName)
326324
loadWorkspaces()
327325
} catch (e: WorkspaceResponseException) {
328326
logger.warn("Could not stop workspace ${workspace.name}, reason: $e")
@@ -336,7 +334,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
336334
private inner class CreateWorkspaceAction :
337335
AnActionButton(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.create.text"), CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.create.text"), CoderIcons.CREATE) {
338336
override fun actionPerformed(p0: AnActionEvent) {
339-
BrowserUtil.browse(coderClient.coderURL.toURI().resolve("/templates"))
337+
BrowserUtil.browse(clientService.client.url.toURI().resolve("/templates"))
340338
}
341339
}
342340

@@ -373,8 +371,8 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
373371
}
374372

375373
private fun updateWorkspaceActions() {
376-
goToDashboardAction.isEnabled = coderClient.isReady
377-
createWorkspaceAction.isEnabled = coderClient.isReady
374+
goToDashboardAction.isEnabled = clientService.isReady
375+
createWorkspaceAction.isEnabled = clientService.isReady
378376
when (tableOfWorkspaces.selectedObject?.workspaceStatus) {
379377
WorkspaceStatus.RUNNING -> {
380378
startWorkspaceAction.isEnabled = false
@@ -623,28 +621,28 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
623621
*/
624622
private fun authenticate(url: URL, token: String) {
625623
logger.info("Authenticating to $url...")
626-
coderClient.initClientSession(url, token)
624+
clientService.initClientSession(url, token)
627625

628626
try {
629-
logger.info("Checking compatibility with Coder version ${coderClient.buildVersion}...")
630-
CoderSemVer.checkVersionCompatibility(coderClient.buildVersion)
631-
logger.info("${coderClient.buildVersion} is compatible")
627+
logger.info("Checking compatibility with Coder version ${clientService.buildVersion}...")
628+
CoderSemVer.checkVersionCompatibility(clientService.buildVersion)
629+
logger.info("${clientService.buildVersion} is compatible")
632630
} catch (e: InvalidVersionException) {
633631
logger.warn(e)
634632
notificationBanner.apply {
635633
component.isVisible = true
636634
showWarning(
637635
CoderGatewayBundle.message(
638636
"gateway.connector.view.coder.workspaces.invalid.coder.version",
639-
coderClient.buildVersion
637+
clientService.buildVersion
640638
)
641639
)
642640
}
643641
} catch (e: IncompatibleVersionException) {
644642
logger.warn(e)
645643
notificationBanner.apply {
646644
component.isVisible = true
647-
showWarning(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.unsupported.coder.version", coderClient.buildVersion))
645+
showWarning(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.unsupported.coder.version", clientService.buildVersion))
648646
}
649647
}
650648

@@ -658,13 +656,21 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
658656
val ws = withContext(Dispatchers.IO) {
659657
val timeBeforeRequestingWorkspaces = System.currentTimeMillis()
660658
try {
661-
val ws = coderClient.workspaces()
662-
val ams = ws.flatMap { it.toAgentModels() }.toSet()
659+
val ws = clientService.client.workspaces()
660+
val ams = ws.flatMap { it.toAgentModels() }
661+
ams.forEach {
662+
cs.launch(Dispatchers.IO) {
663+
it.templateIcon = iconDownloader.load(it.templateIconPath, it.name)
664+
withContext(Dispatchers.Main) {
665+
tableOfWorkspaces.updateUI()
666+
}
667+
}
668+
}
663669
val timeAfterRequestingWorkspaces = System.currentTimeMillis()
664670
logger.info("Retrieving the workspaces took: ${timeAfterRequestingWorkspaces - timeBeforeRequestingWorkspaces} millis")
665671
return@withContext ams
666672
} catch (e: Exception) {
667-
logger.error("Could not retrieve workspaces for ${coderClient.me.username} on ${coderClient.coderURL}. Reason: $e")
673+
logger.error("Could not retrieve workspaces for ${clientService.me.username} on ${clientService.client.url}. Reason: $e")
668674
emptySet()
669675
}
670676
}
@@ -675,62 +681,6 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
675681
}
676682
}
677683

678-
private fun Workspace.toAgentModels(): Set<WorkspaceAgentModel> {
679-
val wam = this.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }.map { agent ->
680-
val workspaceWithAgentName = "${this.name}.${agent.name}"
681-
val wm = WorkspaceAgentModel(
682-
this.id,
683-
this.name,
684-
workspaceWithAgentName,
685-
this.templateID,
686-
this.templateName,
687-
this.templateIcon,
688-
null,
689-
WorkspaceVersionStatus.from(this),
690-
this.latestBuild.status,
691-
WorkspaceAndAgentStatus.from(this, agent),
692-
this.latestBuild.transition,
693-
OS.from(agent.operatingSystem),
694-
Arch.from(agent.architecture),
695-
agent.expandedDirectory ?: agent.directory,
696-
)
697-
cs.launch(Dispatchers.IO) {
698-
wm.templateIcon = iconDownloader.load(wm.templateIconPath, wm.name)
699-
withContext(Dispatchers.Main) {
700-
tableOfWorkspaces.updateUI()
701-
}
702-
}
703-
wm
704-
}.toSet()
705-
706-
if (wam.isNullOrEmpty()) {
707-
val wm = WorkspaceAgentModel(
708-
this.id,
709-
this.name,
710-
this.name,
711-
this.templateID,
712-
this.templateName,
713-
this.templateIcon,
714-
null,
715-
WorkspaceVersionStatus.from(this),
716-
this.latestBuild.status,
717-
WorkspaceAndAgentStatus.from(this),
718-
this.latestBuild.transition,
719-
null,
720-
null,
721-
null
722-
)
723-
cs.launch(Dispatchers.IO) {
724-
wm.templateIcon = iconDownloader.load(wm.templateIconPath, wm.name)
725-
withContext(Dispatchers.Main) {
726-
tableOfWorkspaces.updateUI()
727-
}
728-
}
729-
return setOf(wm)
730-
}
731-
return wam
732-
}
733-
734684
override fun onPrevious() {
735685
super.onPrevious()
736686
logger.info("Going back to the main view")
@@ -757,6 +707,11 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
757707
)
758708
cliManager.configSsh(tableOfWorkspaces.items)
759709

710+
// The config directory can be used to pull the URL and token in
711+
// order to query this workspace's status in other flows, for
712+
// example from the recent connections screen.
713+
wizardModel.configDirectory = cliManager.coderConfigPath.toString()
714+
760715
logger.info("Opening IDE and Project Location window for ${workspace.name}")
761716
return true
762717
}
@@ -904,12 +859,13 @@ class WorkspacesTableModel : ListTableModel<WorkspaceAgentModel>(
904859

905860
override fun getRenderer(item: WorkspaceAgentModel?): TableCellRenderer {
906861
return object : DefaultTableCellRenderer() {
862+
private val workspace = item
907863
override fun getTableCellRendererComponent(table: JTable, value: Any, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int): Component {
908864
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
909865
if (value is String) {
910866
text = value
911-
foreground = WorkspaceAndAgentStatus.from(value).statusColor()
912-
toolTipText = WorkspaceAndAgentStatus.from(value).description
867+
foreground = workspace?.agentStatus?.statusColor()
868+
toolTipText = workspace?.agentStatus?.description
913869
}
914870
font = table.tableHeader.font
915871
border = JBUI.Borders.empty(0, 8)

‎src/main/resources/messages/CoderGatewayBundle.properties

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ gateway.connector.view.coder.workspaces.header.text=Coder Workspaces
1111
gateway.connector.view.coder.workspaces.comment=Self-hosted developer workspaces in the cloud or on-premises. Coder empowers developers with secure, consistent, and fast developer workspaces.
1212
gateway.connector.view.coder.workspaces.connect.text=Connect
1313
gateway.connector.view.coder.workspaces.cli.downloader.dialog.title=Authenticate and setup Coder
14-
gateway.connector.view.coder.workspaces.next.text=Select IDE and Project
15-
gateway.connector.view.coder.workspaces.dashboard.text=Open Dashboard
16-
gateway.connector.view.coder.workspaces.start.text=Start Workspace
17-
gateway.connector.view.coder.workspaces.stop.text=Stop Workspace
18-
gateway.connector.view.coder.workspaces.update.text=Update Workspace Template
14+
gateway.connector.view.coder.workspaces.next.text=Select IDE and project
15+
gateway.connector.view.coder.workspaces.dashboard.text=Open dashboard
16+
gateway.connector.view.coder.workspaces.start.text=Start workspace
17+
gateway.connector.view.coder.workspaces.stop.text=Stop workspace
18+
gateway.connector.view.coder.workspaces.update.text=Update workspace template
1919
gateway.connector.view.coder.workspaces.create.text=Create workspace
2020
gateway.connector.view.coder.workspaces.unsupported.os.info=Gateway supports only Linux machines. Support for macOS and Windows is planned.
2121
gateway.connector.view.coder.workspaces.invalid.coder.version=Could not parse Coder version {0}. Coder Gateway plugin might not be compatible with this version. <a href='https://coder.com/docs/coder-oss/latest/ides/gateway#creating-a-new-jetbrains-gateway-connection'>Connect to a Coder workspace manually</a>
@@ -37,10 +37,12 @@ gateway.connector.view.coder.remoteproject.choose.text=Choose IDE and project fo
3737
gateway.connector.view.coder.remoteproject.ide.download.comment=This IDE will be downloaded from jetbrains.com and installed to the default path on the remote host.
3838
gateway.connector.view.coder.remoteproject.ide.installed.comment=This IDE is already installed and will be used as-is.
3939
gateway.connector.view.coder.remoteproject.ide.none.comment=No IDE selected.
40-
gateway.connector.recentconnections.title=Recent Coder Workspaces
41-
gateway.connector.recentconnections.new.wizard.button.tooltip=Open a new Coder Workspace
42-
gateway.connector.recentconnections.remove.button.tooltip=Remove from Recent Connections
43-
gateway.connector.recentconnections.terminal.button.tooltip=Open SSH Web Terminal
40+
gateway.connector.recent-connections.title=Recent Coder workspaces
41+
gateway.connector.recent-connections.new.wizard.button.tooltip=Open a new Coder workspace
42+
gateway.connector.recent-connections.remove.button.tooltip=Remove from recent connections
43+
gateway.connector.recent-connections.terminal.button.tooltip=Open SSH web terminal
44+
gateway.connector.recent-connections.start.button.tooltip=Start workspace
45+
gateway.connector.recent-connections.stop.button.tooltip=Stop workspace
4446
gateway.connector.coder.connection.provider.title=Connecting to Coder workspace...
4547
gateway.connector.coder.connecting=Connecting...
4648
gateway.connector.coder.connecting.retry=Connecting (attempt {0})...

‎src/main/resources/off.svg

Lines changed: 6 additions & 0 deletions
Loading

‎src/main/resources/pending.svg

Lines changed: 7 additions & 0 deletions
Loading

‎src/main/resources/running.svg

Lines changed: 6 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)
Please sign in to comment.