Skip to content

Commit 9cda2e1

Browse files
committedApr 18, 2024··
Replaced untyped connection parameters map
Provides more type safety, and gives us a place to do some additional deploy steps in a future commit. Additionally, make connections with same workspace, project, and IDE match, since if there are two for the same IDE but one has a download link and one has a path we should combine them, for example.
1 parent 0baac21 commit 9cda2e1

15 files changed

+322
-275
lines changed
 

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

Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,25 @@ package com.coder.gateway
44

55
import com.coder.gateway.cli.CoderCLIManager
66
import com.coder.gateway.cli.ensureCLI
7+
import com.coder.gateway.models.AGENT_ID
8+
import com.coder.gateway.models.AGENT_NAME
9+
import com.coder.gateway.models.TOKEN
710
import com.coder.gateway.models.TokenSource
11+
import com.coder.gateway.models.URL
12+
import com.coder.gateway.models.WORKSPACE
813
import com.coder.gateway.models.WorkspaceAndAgentStatus
14+
import com.coder.gateway.models.WorkspaceProjectIDE
15+
import com.coder.gateway.models.agentID
16+
import com.coder.gateway.models.agentName
17+
import com.coder.gateway.models.folder
18+
import com.coder.gateway.models.ideBuildNumber
19+
import com.coder.gateway.models.ideDownloadLink
20+
import com.coder.gateway.models.idePathOnHost
21+
import com.coder.gateway.models.ideProductCode
22+
import com.coder.gateway.models.isCoder
23+
import com.coder.gateway.models.token
24+
import com.coder.gateway.models.url
25+
import com.coder.gateway.models.workspace
926
import com.coder.gateway.sdk.CoderRestClient
1027
import com.coder.gateway.sdk.ex.AuthenticationResponseException
1128
import com.coder.gateway.sdk.v2.models.Workspace
@@ -15,7 +32,7 @@ import com.coder.gateway.services.CoderRestClientService
1532
import com.coder.gateway.services.CoderSettingsService
1633
import com.coder.gateway.util.toURL
1734
import com.coder.gateway.util.withPath
18-
import com.coder.gateway.views.steps.CoderWorkspaceStepView
35+
import com.coder.gateway.views.steps.CoderWorkspaceProjectIDEStepView
1936
import com.coder.gateway.views.steps.CoderWorkspacesStepSelection
2037
import com.intellij.openapi.application.ApplicationManager
2138
import com.intellij.openapi.components.service
@@ -26,31 +43,17 @@ import com.intellij.util.ui.JBUI
2643
import com.jetbrains.gateway.api.ConnectionRequestor
2744
import com.jetbrains.gateway.api.GatewayConnectionHandle
2845
import com.jetbrains.gateway.api.GatewayConnectionProvider
29-
import java.net.URL
3046
import javax.swing.JComponent
3147
import javax.swing.border.Border
3248

33-
// In addition to `type`, these are the keys that we support in our Gateway
34-
// links.
35-
private const val URL = "url"
36-
private const val TOKEN = "token"
37-
private const val WORKSPACE = "workspace"
38-
private const val AGENT_NAME = "agent"
39-
private const val AGENT_ID = "agent_id"
40-
private const val FOLDER = "folder"
41-
private const val IDE_DOWNLOAD_LINK = "ide_download_link"
42-
private const val IDE_PRODUCT_CODE = "ide_product_code"
43-
private const val IDE_BUILD_NUMBER = "ide_build_number"
44-
private const val IDE_PATH_ON_HOST = "ide_path_on_host"
45-
4649
/**
4750
* A dialog wrapper around CoderWorkspaceStepView.
4851
*/
4952
class CoderWorkspaceStepDialog(
5053
name: String,
5154
private val state: CoderWorkspacesStepSelection,
5255
) : DialogWrapper(true) {
53-
private val view = CoderWorkspaceStepView(showTitle = false)
56+
private val view = CoderWorkspaceProjectIDEStepView(showTitle = false)
5457

5558
init {
5659
init()
@@ -65,7 +68,7 @@ class CoderWorkspaceStepDialog(
6568
view.dispose()
6669
}
6770

68-
fun showAndGetData(): Map<String, String>? {
71+
fun showAndGetData(): WorkspaceProjectIDE? {
6972
if (showAndGet()) {
7073
return view.data()
7174
}
@@ -98,16 +101,16 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
98101
CoderRemoteConnectionHandle().connect{ indicator ->
99102
logger.debug("Launched Coder connection provider", parameters)
100103

101-
val deploymentURL = parameters[URL]
104+
val deploymentURL = parameters.url()
102105
?: CoderRemoteConnectionHandle.ask("Enter the full URL of your Coder deployment")
103106
if (deploymentURL.isNullOrBlank()) {
104107
throw IllegalArgumentException("Query parameter \"$URL\" is missing")
105108
}
106109

107-
val (client, username) = authenticate(deploymentURL.toURL(), parameters[TOKEN])
110+
val (client, username) = authenticate(deploymentURL, parameters.token())
108111

109112
// TODO: If the workspace is missing we could launch the wizard.
110-
val workspaceName = parameters[WORKSPACE] ?: throw IllegalArgumentException("Query parameter \"$WORKSPACE\" is missing")
113+
val workspaceName = parameters.workspace() ?: throw IllegalArgumentException("Query parameter \"$WORKSPACE\" is missing")
111114

112115
val workspaces = client.workspaces()
113116
val workspace = workspaces.firstOrNull{ it.name == workspaceName } ?: throw IllegalArgumentException("The workspace $workspaceName does not exist")
@@ -150,13 +153,13 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
150153
cli.configSsh(client.agentNames(workspaces))
151154

152155
val name = "${workspace.name}.${agent.name}"
153-
val openDialog = parameters[IDE_PRODUCT_CODE].isNullOrBlank() ||
154-
parameters[IDE_BUILD_NUMBER].isNullOrBlank() ||
155-
(parameters[IDE_PATH_ON_HOST].isNullOrBlank() && parameters[IDE_DOWNLOAD_LINK].isNullOrBlank()) ||
156-
parameters[FOLDER].isNullOrBlank()
156+
val openDialog = parameters.ideProductCode().isNullOrBlank() ||
157+
parameters.ideBuildNumber().isNullOrBlank() ||
158+
(parameters.idePathOnHost().isNullOrBlank() && parameters.ideDownloadLink().isNullOrBlank()) ||
159+
parameters.folder().isNullOrBlank()
157160

158161
if (openDialog) {
159-
var data: Map<String, String>? = null
162+
var data: WorkspaceProjectIDE? = null
160163
ApplicationManager.getApplication().invokeAndWait {
161164
val dialog = CoderWorkspaceStepDialog(name,
162165
CoderWorkspacesStepSelection(agent, workspace, cli, client, workspaces))
@@ -167,13 +170,18 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
167170
// Check that both the domain and the redirected domain are
168171
// allowlisted. If not, check with the user whether to proceed.
169172
verifyDownloadLink(parameters)
170-
171-
parameters
172-
.withWorkspaceHostname(CoderCLIManager.getHostName(deploymentURL.toURL(), name))
173-
.withProjectPath(parameters[FOLDER]!!)
174-
.withWebTerminalLink(client.url.withPath("/@$username/$workspace.name/terminal").toString())
175-
.withConfigDirectory(cli.coderConfigPath.toString())
176-
.withName(name)
173+
WorkspaceProjectIDE.fromInputs(
174+
name = name,
175+
hostname = CoderCLIManager.getHostName(deploymentURL.toURL(), name),
176+
projectPath = parameters.folder(),
177+
ideProductCode = parameters.ideProductCode(),
178+
ideBuildNumber = parameters.ideBuildNumber(),
179+
webTerminalLink = client.url.withPath("/@$username/$workspace.name/terminal").toString(),
180+
configDirectory = cli.coderConfigPath.toString(),
181+
idePathOnHost = parameters.idePathOnHost(),
182+
downloadSource = parameters.ideDownloadLink(),
183+
lastOpened = null, // Have not opened yet.
184+
)
177185
}
178186
}
179187
return null
@@ -183,22 +191,22 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
183191
* Return an authenticated Coder CLI and the user's name, asking for the
184192
* token as long as it continues to result in an authentication failure.
185193
*/
186-
private fun authenticate(deploymentURL: URL, queryToken: String?, lastToken: Pair<String, TokenSource>? = null): Pair<CoderRestClient, String> {
194+
private fun authenticate(deploymentURL: String, queryToken: String?, lastToken: Pair<String, TokenSource>? = null): Pair<CoderRestClient, String> {
187195
// Use the token from the query, unless we already tried that.
188196
val isRetry = lastToken != null
189197
val token = if (!queryToken.isNullOrBlank() && !isRetry)
190198
Pair(queryToken, TokenSource.QUERY)
191199
else CoderRemoteConnectionHandle.askToken(
192-
deploymentURL,
200+
deploymentURL.toURL(),
193201
lastToken,
194202
isRetry,
195203
useExisting = true,
196204
settings,
197205
)
198206
if (token == null) { // User aborted.
199-
throw IllegalArgumentException("Unable to connect to $deploymentURL, $TOKEN is missing")
207+
throw IllegalArgumentException("Unable to connect to $deploymentURL, query parameter \"$TOKEN\" is missing")
200208
}
201-
val client = CoderRestClientService(deploymentURL, token.first)
209+
val client = CoderRestClientService(deploymentURL.toURL(), token.first)
202210
return try {
203211
Pair(client, client.me().username)
204212
} catch (ex: AuthenticationResponseException) {
@@ -210,7 +218,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
210218
* Check that the link is allowlisted. If not, confirm with the user.
211219
*/
212220
private fun verifyDownloadLink(parameters: Map<String, String>) {
213-
val link = parameters[IDE_DOWNLOAD_LINK]
221+
val link = parameters.ideDownloadLink()
214222
if (link.isNullOrBlank()) {
215223
return // Nothing to verify
216224
}
@@ -244,7 +252,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
244252
}
245253

246254
override fun isApplicable(parameters: Map<String, String>): Boolean {
247-
return parameters.areCoderType()
255+
return parameters.isCoder()
248256
}
249257

250258
companion object {
@@ -267,18 +275,18 @@ fun getMatchingAgent(parameters: Map<String, String?>, workspace: Workspace): Wo
267275

268276
// If the agent is missing and the workspace has only one, use that.
269277
// Prefer the ID over the name if both are set.
270-
val agent = if (!parameters[AGENT_ID].isNullOrBlank())
271-
agents.firstOrNull { it.id.toString() == parameters[AGENT_ID] }
272-
else if (!parameters[AGENT_NAME].isNullOrBlank())
273-
agents.firstOrNull { it.name == parameters[AGENT_NAME]}
278+
val agent = if (!parameters.agentID().isNullOrBlank())
279+
agents.firstOrNull { it.id.toString() == parameters.agentID() }
280+
else if (!parameters.agentName().isNullOrBlank())
281+
agents.firstOrNull { it.name == parameters.agentName()}
274282
else if (agents.size == 1) agents.first()
275283
else null
276284

277285
if (agent == null) {
278-
if (!parameters[AGENT_ID].isNullOrBlank()) {
279-
throw IllegalArgumentException("The workspace \"${workspace.name}\" does not have an agent with ID \"${parameters[AGENT_ID]}\"")
280-
} else if (!parameters[AGENT_NAME].isNullOrBlank()){
281-
throw IllegalArgumentException("The workspace \"${workspace.name}\"does not have an agent named \"${parameters[AGENT_NAME]}\"")
286+
if (!parameters.agentID().isNullOrBlank()) {
287+
throw IllegalArgumentException("The workspace \"${workspace.name}\" does not have an agent with ID \"${parameters.agentID()}\"")
288+
} else if (!parameters.agentName().isNullOrBlank()){
289+
throw IllegalArgumentException("The workspace \"${workspace.name}\"does not have an agent named \"${parameters.agentName()}\"")
282290
} else {
283291
throw MissingArgumentException("Unable to determine which agent to connect to; one of \"$AGENT_NAME\" or \"$AGENT_ID\" must be set because the workspace \"${workspace.name}\" has more than one agent")
284292
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
package com.coder.gateway
44

55
import com.coder.gateway.models.TokenSource
6+
import com.coder.gateway.models.WorkspaceProjectIDE
7+
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
8+
import com.coder.gateway.settings.CoderSettings
69
import com.coder.gateway.util.humanizeDuration
710
import com.coder.gateway.util.isCancellation
811
import com.coder.gateway.util.isWorkerTimeout
912
import com.coder.gateway.util.suspendingRetryWithExponentialBackOff
10-
import com.coder.gateway.util.toURL
1113
import com.coder.gateway.util.withPath
12-
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
13-
import com.coder.gateway.settings.CoderSettings
1414
import com.intellij.ide.BrowserUtil
1515
import com.intellij.openapi.application.ApplicationManager
1616
import com.intellij.openapi.application.ModalityState
@@ -46,7 +46,7 @@ import javax.net.ssl.SSLHandshakeException
4646
class CoderRemoteConnectionHandle {
4747
private val recentConnectionsService = service<CoderRecentWorkspaceConnectionsService>()
4848

49-
fun connect(getParameters: (indicator: ProgressIndicator) -> Map<String, String>) {
49+
fun connect(getParameters: (indicator: ProgressIndicator) -> WorkspaceProjectIDE) {
5050
val clientLifetime = LifetimeDefinition()
5151
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title")) {
5252
try {

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

Lines changed: 0 additions & 178 deletions
This file was deleted.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ fun ensureCLI(
110110
/**
111111
* The supported features of the CLI.
112112
*/
113-
data class Features (
113+
data class Features(
114114
val disableAutostart: Boolean = false,
115115
)
116116

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.coder.gateway.models
2+
3+
// These are keys that we support in our Gateway links and must not be changed.
4+
private const val TYPE = "type"
5+
const val URL = "url"
6+
const val TOKEN = "token"
7+
const val WORKSPACE = "workspace"
8+
const val AGENT_NAME = "agent"
9+
const val AGENT_ID = "agent_id"
10+
private const val FOLDER = "folder"
11+
private const val IDE_DOWNLOAD_LINK = "ide_download_link"
12+
private const val IDE_PRODUCT_CODE = "ide_product_code"
13+
private const val IDE_BUILD_NUMBER = "ide_build_number"
14+
private const val IDE_PATH_ON_HOST = "ide_path_on_host"
15+
16+
// Helper functions for reading from the map. Prefer these to directly
17+
// interacting with the map.
18+
19+
fun Map<String, String>.isCoder(): Boolean = this[TYPE] == "coder"
20+
fun Map<String, String>.url() = this[URL]
21+
fun Map<String, String>.token() = this[TOKEN]
22+
fun Map<String, String>.workspace() = this[WORKSPACE]
23+
fun Map<String, String?>.agentName() = this[AGENT_NAME]
24+
fun Map<String, String?>.agentID() = this[AGENT_ID]
25+
fun Map<String, String>.folder() = this[FOLDER]
26+
fun Map<String, String>.ideDownloadLink() = this[IDE_DOWNLOAD_LINK]
27+
fun Map<String, String>.ideProductCode() = this[IDE_PRODUCT_CODE]
28+
fun Map<String, String>.ideBuildNumber() = this[IDE_BUILD_NUMBER]
29+
fun Map<String, String>.idePathOnHost() = this[IDE_PATH_ON_HOST]

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

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

6+
/**
7+
* A workspace, project, and IDE.
8+
*
9+
* This is read from a file so values could be missing, and names must not be
10+
* changed to maintain backwards compatibility.
11+
*/
612
class RecentWorkspaceConnection(
713
coderWorkspaceHostname: String? = null,
814
projectPath: String? = null,
@@ -60,9 +66,6 @@ class RecentWorkspaceConnection(
6066
if (projectPath != other.projectPath) return false
6167
if (ideProductCode != other.ideProductCode) return false
6268
if (ideBuildNumber != other.ideBuildNumber) return false
63-
if (downloadSource != other.downloadSource) return false
64-
if (idePathOnHost != other.idePathOnHost) return false
65-
if (webTerminalLink != other.webTerminalLink) return false
6669

6770
return true
6871
}
@@ -73,9 +76,6 @@ class RecentWorkspaceConnection(
7376
result = 31 * result + (projectPath?.hashCode() ?: 0)
7477
result = 31 * result + (ideProductCode?.hashCode() ?: 0)
7578
result = 31 * result + (ideBuildNumber?.hashCode() ?: 0)
76-
result = 31 * result + (downloadSource?.hashCode() ?: 0)
77-
result = 31 * result + (idePathOnHost?.hashCode() ?: 0)
78-
result = 31 * result + (webTerminalLink?.hashCode() ?: 0)
7979

8080
return result
8181
}
@@ -93,15 +93,6 @@ class RecentWorkspaceConnection(
9393
val l = other.ideBuildNumber?.let { ideBuildNumber?.compareTo(it) }
9494
if (l != null && l != 0) return l
9595

96-
val m = other.downloadSource?.let { downloadSource?.compareTo(it) }
97-
if (m != null && m != 0) return m
98-
99-
val n = other.idePathOnHost?.let { idePathOnHost?.compareTo(it) }
100-
if (n != null && n != 0) return n
101-
102-
val o = other.webTerminalLink?.let { webTerminalLink?.compareTo(it) }
103-
if (o != null && o != 0) return o
104-
10596
return 0
10697
}
10798
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package com.coder.gateway.models
33
import com.intellij.openapi.components.BaseState
44
import com.intellij.util.xmlb.annotations.XCollection
55

6+
/**
7+
* Store recent workspace connections.
8+
*/
69
class RecentWorkspaceConnectionState : BaseState() {
710
@get:XCollection
811
var recentConnections by treeSet<RecentWorkspaceConnection>()
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package com.coder.gateway.models
2+
3+
import com.intellij.remote.AuthType
4+
import com.intellij.remote.RemoteCredentialsHolder
5+
import com.intellij.ssh.config.unified.SshConfig
6+
import com.jetbrains.gateway.ssh.HighLevelHostAccessor
7+
import com.jetbrains.gateway.ssh.HostDeployInputs
8+
import com.jetbrains.gateway.ssh.IdeInfo
9+
import com.jetbrains.gateway.ssh.IdeWithStatus
10+
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
11+
import com.jetbrains.gateway.ssh.deploy.DeployTargetInfo
12+
import java.net.URI
13+
import java.time.LocalDateTime
14+
import java.time.format.DateTimeFormatter
15+
16+
private val localTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm")
17+
18+
/**
19+
* Validated parameters for downloading (if necessary) and opening a project
20+
* using an IDE on a workspace.
21+
*/
22+
@Suppress("UnstableApiUsage")
23+
class WorkspaceProjectIDE(
24+
val name: String?,
25+
val hostname: String,
26+
val projectPath: String,
27+
val ideProductCode: IntelliJPlatformProduct,
28+
val ideBuildNumber: String,
29+
30+
// Either a path or URL.
31+
val ideSource: String,
32+
val isDownloadSource: Boolean,
33+
34+
// These are used in the recent connections window.
35+
val webTerminalLink: String?,
36+
val configDirectory: String?,
37+
var lastOpened: String?,
38+
) {
39+
/**
40+
* Return accessor for deploying the IDE.
41+
*/
42+
suspend fun toHostDeployInputs(): HostDeployInputs {
43+
this.lastOpened = localTimeFormatter.format(LocalDateTime.now())
44+
return HostDeployInputs.FullySpecified(
45+
remoteProjectPath = projectPath,
46+
deployTarget = toDeployTargetInfo(),
47+
remoteInfo = HostDeployInputs.WithDeployedWorker(
48+
HighLevelHostAccessor.create(
49+
RemoteCredentialsHolder().apply {
50+
setHost(hostname)
51+
userName = "coder"
52+
port = 22
53+
authType = AuthType.OPEN_SSH
54+
},
55+
true
56+
),
57+
HostDeployInputs.WithHostInfo(this.toSshConfig())
58+
)
59+
)
60+
}
61+
62+
private fun toSshConfig(): SshConfig {
63+
return SshConfig(true).apply {
64+
setHost(hostname)
65+
setUsername("coder")
66+
port = 22
67+
authType = AuthType.OPEN_SSH
68+
}
69+
}
70+
71+
private fun toDeployTargetInfo(): DeployTargetInfo {
72+
return if (this.isDownloadSource) DeployTargetInfo.DeployWithDownload(
73+
URI(this.ideSource),
74+
null,
75+
this.toIdeInfo()
76+
)
77+
else DeployTargetInfo.NoDeploy(this.ideSource, this.toIdeInfo())
78+
}
79+
80+
private fun toIdeInfo(): IdeInfo {
81+
return IdeInfo(
82+
product = this.ideProductCode,
83+
buildNumber = this.ideBuildNumber,
84+
)
85+
}
86+
87+
/**
88+
* Convert parameters into a recent workspace connection (for storage).
89+
*/
90+
fun toRecentWorkspaceConnection(): RecentWorkspaceConnection {
91+
return RecentWorkspaceConnection(
92+
name = this.name,
93+
coderWorkspaceHostname = this.hostname,
94+
projectPath = this.projectPath,
95+
ideProductCode = this.ideProductCode.productCode,
96+
ideBuildNumber = this.ideBuildNumber,
97+
downloadSource = if (this.isDownloadSource) this.ideSource else "",
98+
idePathOnHost = if (this.isDownloadSource) "" else this.ideSource,
99+
lastOpened = this.lastOpened,
100+
webTerminalLink = this.webTerminalLink,
101+
configDirectory = this.configDirectory,
102+
)
103+
}
104+
105+
companion object {
106+
/**
107+
* Create from unvalidated user inputs.
108+
*/
109+
@JvmStatic
110+
fun fromInputs(
111+
name: String?,
112+
hostname: String?,
113+
projectPath: String?,
114+
lastOpened: String?,
115+
ideProductCode: String?,
116+
ideBuildNumber: String?,
117+
downloadSource: String?,
118+
idePathOnHost: String?,
119+
webTerminalLink: String?,
120+
configDirectory: String?,
121+
): WorkspaceProjectIDE {
122+
val ideSource = if (idePathOnHost.isNullOrBlank()) downloadSource else idePathOnHost
123+
if (hostname.isNullOrBlank()) {
124+
throw Error("host name is missing")
125+
} else if (projectPath.isNullOrBlank()) {
126+
throw Error("project path is missing")
127+
} else if (ideProductCode.isNullOrBlank()) {
128+
throw Error("ide product code is missing")
129+
} else if (ideBuildNumber.isNullOrBlank()) {
130+
throw Error("ide build number is missing")
131+
} else if (ideSource.isNullOrBlank()) {
132+
throw Error("one of path or download is required")
133+
}
134+
135+
return WorkspaceProjectIDE(
136+
name = name,
137+
hostname = hostname,
138+
projectPath = projectPath,
139+
ideProductCode = IntelliJPlatformProduct.fromProductCode(ideProductCode) ?: throw Error("invalid product code"),
140+
ideBuildNumber = ideBuildNumber,
141+
webTerminalLink = webTerminalLink,
142+
configDirectory = configDirectory,
143+
144+
ideSource = ideSource,
145+
isDownloadSource = idePathOnHost.isNullOrBlank(),
146+
lastOpened = lastOpened,
147+
)
148+
}
149+
}
150+
}
151+
152+
/**
153+
* Convert into parameters for making a connection to a project using an IDE
154+
* on a workspace. Throw if invalid.
155+
*/
156+
fun RecentWorkspaceConnection.toWorkspaceProjectIDE(): WorkspaceProjectIDE {
157+
return WorkspaceProjectIDE.fromInputs(
158+
name = name,
159+
hostname = coderWorkspaceHostname,
160+
projectPath = projectPath,
161+
ideProductCode = ideProductCode,
162+
ideBuildNumber = ideBuildNumber,
163+
webTerminalLink = webTerminalLink,
164+
configDirectory = configDirectory,
165+
idePathOnHost = idePathOnHost,
166+
downloadSource = downloadSource,
167+
lastOpened = lastOpened,
168+
)
169+
}
170+
171+
/**
172+
* Convert an IDE into parameters for making a connection to a project using
173+
* that IDE on a workspace. Throw if invalid.
174+
*/
175+
fun IdeWithStatus.withWorkspaceProject(
176+
name: String,
177+
hostname: String,
178+
projectPath: String,
179+
webTerminalLink: String,
180+
configDirectory: String,
181+
): WorkspaceProjectIDE {
182+
val download = this.download
183+
val pathOnHost = this.pathOnHost
184+
val ideSource = if (pathOnHost.isNullOrBlank()) download?.link else pathOnHost
185+
if (ideSource.isNullOrBlank()) {
186+
throw Error("one of path or download is required")
187+
}
188+
return WorkspaceProjectIDE(
189+
name = name,
190+
hostname = hostname,
191+
projectPath = projectPath,
192+
ideProductCode = this.product,
193+
ideBuildNumber = this.buildNumber,
194+
webTerminalLink = webTerminalLink,
195+
configDirectory = configDirectory,
196+
197+
ideSource = ideSource,
198+
isDownloadSource = pathOnHost.isNullOrBlank(),
199+
lastOpened = null,
200+
)
201+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import javax.swing.Icon
4747
/**
4848
* Holds proxy information.
4949
*/
50-
data class ProxyValues (
50+
data class ProxyValues(
5151
val username: String?,
5252
val password: String?,
5353
val useAuth: Boolean,

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import retrofit2.http.Query
1616
import java.util.UUID
1717

1818
interface CoderV2RestFacade {
19-
2019
/**
2120
* Retrieves details about the authenticated user.
2221
*/

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import com.squareup.moshi.Json
44
import com.squareup.moshi.JsonClass
55

66
@JsonClass(generateAdapter = true)
7-
data class Validation (
7+
data class Validation(
88
@Json(name = "field") val field: String,
99
@Json(name = "detail") val detail: String,
1010
)
1111

1212
@JsonClass(generateAdapter = true)
13-
data class Response (
13+
data class Response(
1414
@Json(name = "message") val message: String,
1515
@Json(name = "detail") val detail: String,
1616
@Json(name = "validations") val validations: List<Validation> = emptyList(),

‎src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import java.nio.file.Path
1515
import java.nio.file.Paths
1616

1717
const val CODER_SSH_CONFIG_OPTIONS = "CODER_SSH_CONFIG_OPTIONS";
1818

1919
open class CoderSettingsState(
2020
// Used to download the Coder CLI which is necessary to proxy SSH
@@ -66,7 +66,7 @@
6666
/**
6767
* Consolidated TLS settings.
6868
*/
69-
data class CoderTLSSettings (private val state: CoderSettingsState) {
69+
data class CoderTLSSettings(private val state: CoderSettingsState) {
7070
val certPath: String
7171
get() = state.tlsCertPath
7272
val keyPath: String

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.coder.gateway.views
22

33
import com.coder.gateway.CoderRemoteConnectionHandle
4-
import com.coder.gateway.views.steps.CoderWorkspaceStepView
4+
import com.coder.gateway.views.steps.CoderWorkspaceProjectIDEStepView
55
import com.coder.gateway.views.steps.CoderWorkspacesStepView
66
import com.intellij.ui.components.panels.Wrapper
77
import com.intellij.util.ui.JBUI
@@ -13,7 +13,7 @@ class CoderGatewayConnectorWizardWrapperView : GatewayConnectorView {
1313
override val component: JComponent
1414
get() {
1515
val step1 = CoderWorkspacesStepView()
16-
val step2 = CoderWorkspaceStepView()
16+
val step2 = CoderWorkspaceProjectIDEStepView()
1717
val wrapper = Wrapper(step1).apply { border = JBUI.Borders.empty() }
1818
step1.init()
1919

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ import com.coder.gateway.CoderRemoteConnectionHandle
88
import com.coder.gateway.icons.CoderIcons
99
import com.coder.gateway.models.RecentWorkspaceConnection
1010
import com.coder.gateway.models.WorkspaceAgentListModel
11+
import com.coder.gateway.models.toWorkspaceProjectIDE
1112
import com.coder.gateway.sdk.CoderRestClient
1213
import com.coder.gateway.sdk.v2.models.WorkspaceStatus
1314
import com.coder.gateway.sdk.v2.models.toAgentList
1415
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
1516
import com.coder.gateway.services.CoderRestClientService
16-
import com.coder.gateway.services.CoderSettingsService
17-
import com.coder.gateway.toWorkspaceParams
1817
import com.coder.gateway.util.toURL
1918
import com.intellij.icons.AllIcons
2019
import com.intellij.ide.BrowserUtil
@@ -74,7 +73,6 @@ data class DeploymentInfo(
7473
)
7574

7675
class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback: (Component) -> Unit) : GatewayRecentConnections, Disposable {
77-
private val settings: CoderSettingsService = service<CoderSettingsService>()
7876
private val recentConnectionsService = service<CoderRecentWorkspaceConnectionsService>()
7977
private val cs = CoroutineScope(Dispatchers.Main)
8078

@@ -148,7 +146,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
148146

149147
private fun updateContentView() {
150148
val connections = recentConnectionsService.getAllRecentConnections()
151-
.filter { it.coderWorkspaceHostname != null }
149+
.filter { !it.coderWorkspaceHostname.isNullOrBlank() }
152150
.filter { matchesFilter(it) }
153151
.groupBy { it.coderWorkspaceHostname!! }
154152
recentWorkspacesContentPanel.viewport.view = panel {
@@ -220,7 +218,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
220218
row {
221219
icon(product.icon)
222220
cell(ActionLink(connectionDetails.projectPath!!) {
223-
CoderRemoteConnectionHandle().connect{ connectionDetails.toWorkspaceParams() }
221+
CoderRemoteConnectionHandle().connect{ connectionDetails.toWorkspaceProjectIDE() }
224222
GatewayUI.getInstance().reset()
225223
})
226224
label("").resizableColumn().align(AlignX.FILL)

‎src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceStepView.kt renamed to ‎src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ package com.coder.gateway.views.steps
33
import com.coder.gateway.CoderGatewayBundle
44
import com.coder.gateway.cli.CoderCLIManager
55
import com.coder.gateway.icons.CoderIcons
6+
import com.coder.gateway.models.WorkspaceProjectIDE
7+
import com.coder.gateway.models.withWorkspaceProject
68
import com.coder.gateway.sdk.v2.models.Workspace
79
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
8-
import com.coder.gateway.toWorkspaceParams
910
import com.coder.gateway.util.Arch
1011
import com.coder.gateway.util.OS
1112
import com.coder.gateway.util.humanizeDuration
@@ -14,11 +15,6 @@ import com.coder.gateway.util.isWorkerTimeout
1415
import com.coder.gateway.util.suspendingRetryWithExponentialBackOff
1516
import com.coder.gateway.util.withPath
1617
import com.coder.gateway.views.LazyBrowserLink
17-
import com.coder.gateway.withConfigDirectory
18-
import com.coder.gateway.withName
19-
import com.coder.gateway.withProjectPath
20-
import com.coder.gateway.withWebTerminalLink
21-
import com.coder.gateway.withWorkspaceHostname
2218
import com.intellij.openapi.application.ApplicationManager
2319
import com.intellij.openapi.application.ModalityState
2420
import com.intellij.openapi.application.asContextElement
@@ -83,11 +79,11 @@ import javax.swing.event.DocumentEvent
8379

8480
/**
8581
* View for a single workspace. In particular, show available IDEs and a button
86-
* to connect to the workspace
82+
* to select an IDE and project to run on the workspace.
8783
*/
88-
class CoderWorkspaceStepView(
84+
class CoderWorkspaceProjectIDEStepView(
8985
private val showTitle: Boolean = true,
90-
) : CoderWizardStep<Map<String, String>>(
86+
) : CoderWizardStep<WorkspaceProjectIDE>(
9187
CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.next.text")
9288
) {
9389
private val cs = CoroutineScope(Dispatchers.IO)
@@ -252,7 +248,7 @@ class CoderWorkspaceStepView(
252248
* Validate the remote path whenever it changes.
253249
*/
254250
private fun installRemotePathValidator(executor: HighLevelHostAccessor) {
255-
val disposable = Disposer.newDisposable(ApplicationManager.getApplication(), CoderWorkspaceStepView::class.java.name)
251+
val disposable = Disposer.newDisposable(ApplicationManager.getApplication(), CoderWorkspaceProjectIDEStepView::class.java.name)
256252
ComponentValidator(disposable).installOn(tfProject)
257253

258254
tfProject.document.addDocumentListener(object : DocumentAdapter() {
@@ -355,16 +351,16 @@ class CoderWorkspaceStepView(
355351
/**
356352
* Return the selected parameters. Throw if not configured.
357353
*/
358-
override fun data(): Map<String, String> {
354+
override fun data(): WorkspaceProjectIDE {
359355
return withoutNull(cbIDE.selectedItem, state) { selectedIDE, state ->
360356
val name = "${state.workspace.name}.${state.agent.name}"
361-
selectedIDE
362-
.toWorkspaceParams()
363-
.withWorkspaceHostname(CoderCLIManager.getHostName(state.client.url, name))
364-
.withProjectPath(tfProject.text)
365-
.withWebTerminalLink("${terminalLink.url}")
366-
.withConfigDirectory(state.cliManager.coderConfigPath.toString())
367-
.withName(name)
357+
selectedIDE.withWorkspaceProject(
358+
name = name,
359+
hostname = CoderCLIManager.getHostName(state.client.url, name),
360+
projectPath = tfProject.text,
361+
webTerminalLink = terminalLink.url.toString(),
362+
configDirectory = state.cliManager.coderConfigPath.toString(),
363+
)
368364
}
369365
}
370366

@@ -416,6 +412,6 @@ class CoderWorkspaceStepView(
416412
}
417413

418414
companion object {
419-
val logger = Logger.getInstance(CoderWorkspaceStepView::class.java.simpleName)
415+
val logger = Logger.getInstance(CoderWorkspaceProjectIDEStepView::class.java.simpleName)
420416
}
421417
}

0 commit comments

Comments
 (0)
Please sign in to comment.