Skip to content

Commit 2d1328a

Browse files
authored
Merge pull request #136 from coder/list-installed-ides
List installed ides
2 parents a4432f0 + f523d51 commit 2d1328a

File tree

7 files changed

+215
-102
lines changed

7 files changed

+215
-102
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- ability to open a template in the Dashboard
99
- ability to sort by workspace name, or by template name or by workspace status
1010
- a new token is requested when the one persisted is expired
11+
- support for re-using already installed IDE backends
1112

1213
### Changed
1314
- renamed the plugin from `Coder Gateway` to `Gateway`

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
pluginGroup=com.coder.gateway
44
pluginName=coder-gateway
55
# SemVer format -> https://semver.org
6-
pluginVersion=2.1.3
6+
pluginVersion=2.1.4
77
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
88
# for insight into build numbers and IntelliJ Platform versions.
99
pluginSinceBuild=222.3739.54

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

Lines changed: 10 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,93 +2,39 @@
22

33
package com.coder.gateway
44

5-
import com.coder.gateway.models.RecentWorkspaceConnection
65
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
76
import com.intellij.openapi.components.service
87
import com.intellij.openapi.rd.util.launchUnderBackgroundProgress
9-
import com.intellij.remote.AuthType
10-
import com.intellij.remote.RemoteCredentialsHolder
11-
import com.intellij.ssh.config.unified.SshConfig
128
import com.jetbrains.gateway.api.ConnectionRequestor
139
import com.jetbrains.gateway.api.GatewayConnectionHandle
1410
import com.jetbrains.gateway.api.GatewayConnectionProvider
1511
import com.jetbrains.gateway.api.GatewayUI
16-
import com.jetbrains.gateway.ssh.HighLevelHostAccessor
17-
import com.jetbrains.gateway.ssh.HostDeployInputs
18-
import com.jetbrains.gateway.ssh.IdeInfo
19-
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
2012
import com.jetbrains.gateway.ssh.SshDeployFlowUtil
2113
import com.jetbrains.gateway.ssh.SshMultistagePanelContext
22-
import com.jetbrains.gateway.ssh.deploy.DeployTargetInfo.DeployWithDownload
2314
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
2415
import kotlinx.coroutines.launch
25-
import java.net.URI
2616
import java.time.Duration
27-
import java.time.LocalDateTime
28-
import java.time.format.DateTimeFormatter
2917

3018
class CoderGatewayConnectionProvider : GatewayConnectionProvider {
3119
private val recentConnectionsService = service<CoderRecentWorkspaceConnectionsService>()
3220

33-
private val localTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm")
34-
3521
override suspend fun connect(parameters: Map<String, String>, requestor: ConnectionRequestor): GatewayConnectionHandle? {
36-
val coderWorkspaceHostname = parameters["coder_workspace_hostname"]
37-
val projectPath = parameters["project_path"]
38-
val ideProductCode = parameters["ide_product_code"]!!
39-
val ideBuildNumber = parameters["ide_build_number"]!!
40-
val ideDownloadLink = parameters["ide_download_link"]!!
41-
val webTerminalLink = parameters["web_terminal_link"]!!
42-
43-
if (coderWorkspaceHostname != null && projectPath != null) {
44-
val sshConfiguration = SshConfig(true).apply {
45-
setHost(coderWorkspaceHostname)
46-
setUsername("coder")
47-
port = 22
48-
authType = AuthType.OPEN_SSH
49-
}
50-
51-
val clientLifetime = LifetimeDefinition()
52-
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title"), canBeCancelled = true, isIndeterminate = true, project = null) {
53-
val context = SshMultistagePanelContext(
54-
HostDeployInputs.FullySpecified(
55-
remoteProjectPath = projectPath,
56-
deployTarget = DeployWithDownload(
57-
URI(ideDownloadLink),
58-
null,
59-
IdeInfo(
60-
product = IntelliJPlatformProduct.fromProductCode(ideProductCode)!!,
61-
buildNumber = ideBuildNumber
62-
)
63-
),
64-
remoteInfo = HostDeployInputs.WithDeployedWorker(
65-
HighLevelHostAccessor.create(
66-
RemoteCredentialsHolder().apply {
67-
setHost(coderWorkspaceHostname)
68-
userName = "coder"
69-
port = 22
70-
authType = AuthType.OPEN_SSH
71-
},
72-
true
73-
),
74-
HostDeployInputs.WithHostInfo(sshConfiguration)
75-
)
76-
)
22+
val clientLifetime = LifetimeDefinition()
23+
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title"), canBeCancelled = true, isIndeterminate = true, project = null) {
24+
val context = SshMultistagePanelContext(parameters.toHostDeployInputs())
25+
launch {
26+
@Suppress("UnstableApiUsage") SshDeployFlowUtil.fullDeployCycle(
27+
clientLifetime, context, Duration.ofMinutes(10)
7728
)
78-
launch {
79-
@Suppress("UnstableApiUsage") SshDeployFlowUtil.fullDeployCycle(
80-
clientLifetime, context, Duration.ofMinutes(10)
81-
)
82-
}
8329
}
84-
85-
recentConnectionsService.addRecentConnection(RecentWorkspaceConnection(coderWorkspaceHostname, projectPath, localTimeFormatter.format(LocalDateTime.now()), ideProductCode, ideBuildNumber, ideDownloadLink, webTerminalLink))
86-
GatewayUI.getInstance().reset()
8730
}
31+
32+
recentConnectionsService.addRecentConnection(parameters.toRecentWorkspaceConnection())
33+
GatewayUI.getInstance().reset()
8834
return null
8935
}
9036

9137
override fun isApplicable(parameters: Map<String, String>): Boolean {
92-
return parameters["type"] == "coder"
38+
return parameters.areCoderType()
9339
}
9440
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package com.coder.gateway
2+
3+
import com.coder.gateway.models.RecentWorkspaceConnection
4+
import com.intellij.remote.AuthType
5+
import com.intellij.remote.RemoteCredentialsHolder
6+
import com.intellij.ssh.config.unified.SshConfig
7+
import com.jetbrains.gateway.ssh.HighLevelHostAccessor
8+
import com.jetbrains.gateway.ssh.HostDeployInputs
9+
import com.jetbrains.gateway.ssh.IdeInfo
10+
import com.jetbrains.gateway.ssh.IdeWithStatus
11+
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
12+
import com.jetbrains.gateway.ssh.deploy.DeployTargetInfo
13+
import java.net.URI
14+
import java.time.LocalDateTime
15+
import java.time.format.DateTimeFormatter
16+
17+
private const val CODER_WORKSPACE_HOSTNAME = "coder_workspace_hostname"
18+
private const val TYPE = "type"
19+
private const val VALUE_FOR_TYPE = "coder"
20+
private const val PROJECT_PATH = "project_path"
21+
private const val IDE_DOWNLOAD_LINK = "ide_download_link"
22+
private const val IDE_PRODUCT_CODE = "ide_product_code"
23+
private const val IDE_BUILD_NUMBER = "ide_build_number"
24+
private const val IDE_PATH_ON_HOST = "ide_path_on_host"
25+
private const val WEB_TERMINAL_LINK = "web_terminal_link"
26+
27+
private val localTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm")
28+
29+
fun RecentWorkspaceConnection.toWorkspaceParams(): Map<String, String> {
30+
val map = mutableMapOf(
31+
TYPE to VALUE_FOR_TYPE,
32+
CODER_WORKSPACE_HOSTNAME to "${this.coderWorkspaceHostname}",
33+
PROJECT_PATH to this.projectPath!!,
34+
IDE_PRODUCT_CODE to IntelliJPlatformProduct.fromProductCode(this.ideProductCode!!)!!.productCode,
35+
IDE_BUILD_NUMBER to "${this.ideBuildNumber}",
36+
WEB_TERMINAL_LINK to "${this.webTerminalLink}"
37+
)
38+
39+
if (!this.downloadSource.isNullOrBlank()) {
40+
map[IDE_DOWNLOAD_LINK] = this.downloadSource!!
41+
} else {
42+
map[IDE_PATH_ON_HOST] = this.idePathOnHost!!
43+
}
44+
return map
45+
}
46+
47+
fun IdeWithStatus.toWorkspaceParams(): Map<String, String> {
48+
val workspaceParams = mutableMapOf(
49+
TYPE to VALUE_FOR_TYPE,
50+
IDE_PRODUCT_CODE to this.product.productCode,
51+
IDE_BUILD_NUMBER to this.buildNumber
52+
)
53+
54+
if (this.download != null) {
55+
workspaceParams[IDE_DOWNLOAD_LINK] = this.download!!.link
56+
}
57+
58+
if (!this.pathOnHost.isNullOrBlank()) {
59+
workspaceParams[IDE_PATH_ON_HOST] = this.pathOnHost!!
60+
}
61+
62+
return workspaceParams
63+
}
64+
65+
fun Map<String, String>.withWorkspaceHostname(hostname: String): Map<String, String> {
66+
val map = this.toMutableMap()
67+
map[CODER_WORKSPACE_HOSTNAME] = hostname
68+
return map
69+
}
70+
71+
fun Map<String, String>.withProjectPath(projectPath: String): Map<String, String> {
72+
val map = this.toMutableMap()
73+
map[PROJECT_PATH] = projectPath
74+
return map
75+
}
76+
77+
fun Map<String, String>.withWebTerminalLink(webTerminalLink: String): Map<String, String> {
78+
val map = this.toMutableMap()
79+
map[WEB_TERMINAL_LINK] = webTerminalLink
80+
return map
81+
}
82+
83+
fun Map<String, String>.areCoderType(): Boolean {
84+
return this[TYPE] == VALUE_FOR_TYPE && !this[CODER_WORKSPACE_HOSTNAME].isNullOrBlank() && !this[PROJECT_PATH].isNullOrBlank()
85+
}
86+
87+
fun Map<String, String>.toSshConfig(): SshConfig {
88+
return SshConfig(true).apply {
89+
setHost(this@toSshConfig.workspaceHostname())
90+
setUsername("coder")
91+
port = 22
92+
authType = AuthType.OPEN_SSH
93+
}
94+
}
95+
96+
suspend fun Map<String, String>.toHostDeployInputs(): HostDeployInputs {
97+
return HostDeployInputs.FullySpecified(
98+
remoteProjectPath = this[PROJECT_PATH]!!,
99+
deployTarget = this.toDeployTargetInfo(),
100+
remoteInfo = HostDeployInputs.WithDeployedWorker(
101+
HighLevelHostAccessor.create(
102+
RemoteCredentialsHolder().apply {
103+
setHost(this@toHostDeployInputs.workspaceHostname())
104+
userName = "coder"
105+
port = 22
106+
authType = AuthType.OPEN_SSH
107+
},
108+
true
109+
),
110+
HostDeployInputs.WithHostInfo(this.toSshConfig())
111+
)
112+
)
113+
}
114+
115+
private fun Map<String, String>.toIdeInfo(): IdeInfo {
116+
return IdeInfo(
117+
product = IntelliJPlatformProduct.fromProductCode(this[IDE_PRODUCT_CODE]!!)!!,
118+
buildNumber = this[IDE_BUILD_NUMBER]!!
119+
)
120+
}
121+
122+
private fun Map<String, String>.toDeployTargetInfo(): DeployTargetInfo {
123+
return if (!this[IDE_DOWNLOAD_LINK].isNullOrBlank()) DeployTargetInfo.DeployWithDownload(
124+
URI(this[IDE_DOWNLOAD_LINK]),
125+
null,
126+
this.toIdeInfo()
127+
)
128+
else DeployTargetInfo.NoDeploy(this[IDE_PATH_ON_HOST]!!, this.toIdeInfo())
129+
}
130+
131+
private fun Map<String, String>.workspaceHostname() = this[CODER_WORKSPACE_HOSTNAME]!!
132+
private fun Map<String, String>.projectPath() = this[PROJECT_PATH]!!
133+
134+
fun Map<String, String>.toRecentWorkspaceConnection(): RecentWorkspaceConnection {
135+
return if (!this[IDE_DOWNLOAD_LINK].isNullOrBlank()) RecentWorkspaceConnection(
136+
this.workspaceHostname(),
137+
this.projectPath(),
138+
localTimeFormatter.format(LocalDateTime.now()),
139+
this[IDE_PRODUCT_CODE]!!,
140+
this[IDE_BUILD_NUMBER]!!,
141+
this[IDE_DOWNLOAD_LINK]!!,
142+
null,
143+
this[WEB_TERMINAL_LINK]!!
144+
) else RecentWorkspaceConnection(
145+
this.workspaceHostname(),
146+
this.projectPath(),
147+
localTimeFormatter.format(LocalDateTime.now()),
148+
this[IDE_PRODUCT_CODE]!!,
149+
this[IDE_BUILD_NUMBER]!!,
150+
null,
151+
this[IDE_PATH_ON_HOST],
152+
this[WEB_TERMINAL_LINK]!!
153+
)
154+
}

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import com.intellij.openapi.components.BaseState
44
import com.intellij.util.xmlb.annotations.Attribute
55

66
class RecentWorkspaceConnection() : BaseState(), Comparable<RecentWorkspaceConnection> {
7-
constructor(hostname: String, prjPath: String, openedAt: String, productCode: String, buildNumber: String, source: String, terminalLink: String) : this() {
7+
constructor(hostname: String, prjPath: String, openedAt: String, productCode: String, buildNumber: String, source: String?, idePath: String?, terminalLink: String) : this() {
88
coderWorkspaceHostname = hostname
99
projectPath = prjPath
1010
lastOpened = openedAt
1111
ideProductCode = productCode
1212
ideBuildNumber = buildNumber
1313
downloadSource = source
14+
idePathOnHost = idePath
1415
webTerminalLink = terminalLink
1516
}
1617

@@ -32,6 +33,10 @@ class RecentWorkspaceConnection() : BaseState(), Comparable<RecentWorkspaceConne
3233
@get:Attribute
3334
var downloadSource by string()
3435

36+
37+
@get:Attribute
38+
var idePathOnHost by string()
39+
3540
@get:Attribute
3641
var webTerminalLink by string()
3742

@@ -47,6 +52,7 @@ class RecentWorkspaceConnection() : BaseState(), Comparable<RecentWorkspaceConne
4752
if (ideProductCode != other.ideProductCode) return false
4853
if (ideBuildNumber != other.ideBuildNumber) return false
4954
if (downloadSource != other.downloadSource) return false
55+
if (idePathOnHost != other.idePathOnHost) return false
5056
if (webTerminalLink != other.webTerminalLink) return false
5157

5258
return true
@@ -59,6 +65,7 @@ class RecentWorkspaceConnection() : BaseState(), Comparable<RecentWorkspaceConne
5965
result = 31 * result + (ideProductCode?.hashCode() ?: 0)
6066
result = 31 * result + (ideBuildNumber?.hashCode() ?: 0)
6167
result = 31 * result + (downloadSource?.hashCode() ?: 0)
68+
result = 31 * result + (idePathOnHost?.hashCode() ?: 0)
6269
result = 31 * result + (webTerminalLink?.hashCode() ?: 0)
6370

6471
return result
@@ -80,8 +87,11 @@ class RecentWorkspaceConnection() : BaseState(), Comparable<RecentWorkspaceConne
8087
val m = other.downloadSource?.let { downloadSource?.compareTo(it) }
8188
if (m != null && m != 0) return m
8289

83-
val n = other.webTerminalLink?.let { webTerminalLink?.compareTo(it) }
84-
if (n != null && n != 0) return n
90+
val n = other.idePathOnHost?.let { idePathOnHost?.compareTo(it) }
91+
if (n != null && m != 0) return n
92+
93+
val o = other.webTerminalLink?.let { webTerminalLink?.compareTo(it) }
94+
if (o != null && n != 0) return o
8595

8696
return 0
8797
}

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

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.coder.gateway.CoderGatewayConstants
77
import com.coder.gateway.icons.CoderIcons
88
import com.coder.gateway.models.RecentWorkspaceConnection
99
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
10+
import com.coder.gateway.toWorkspaceParams
1011
import com.intellij.icons.AllIcons
1112
import com.intellij.ide.BrowserUtil
1213
import com.intellij.openapi.Disposable
@@ -37,6 +38,7 @@ import kotlinx.coroutines.cancel
3738
import kotlinx.coroutines.launch
3839
import java.awt.Component
3940
import java.awt.Dimension
41+
import java.util.Locale
4042
import javax.swing.JComponent
4143
import javax.swing.JLabel
4244
import javax.swing.event.DocumentEvent
@@ -70,7 +72,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
7072
addDocumentListener(object : DocumentAdapter() {
7173
override fun textChanged(e: DocumentEvent) {
7274
val toSearchFor = this@applyToComponent.text
73-
val filteredConnections = recentConnectionsService.getAllRecentConnections().filter { it.coderWorkspaceHostname?.toLowerCase()?.contains(toSearchFor) ?: false || it.projectPath?.toLowerCase()?.contains(toSearchFor) ?: false }
75+
val filteredConnections = recentConnectionsService.getAllRecentConnections().filter { it.coderWorkspaceHostname?.lowercase(Locale.getDefault())?.contains(toSearchFor) ?: false || it.projectPath?.lowercase(Locale.getDefault())?.contains(toSearchFor) ?: false }
7476
updateContentView(filteredConnections.groupBy { it.coderWorkspaceHostname })
7577
}
7678
})
@@ -127,17 +129,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
127129
icon(product.icon)
128130
cell(ActionLink(connectionDetails.projectPath!!) {
129131
cs.launch {
130-
GatewayUI.getInstance().connect(
131-
mapOf(
132-
"type" to "coder",
133-
"coder_workspace_hostname" to "${connectionDetails.coderWorkspaceHostname}",
134-
"project_path" to connectionDetails.projectPath!!,
135-
"ide_product_code" to product.productCode,
136-
"ide_build_number" to "${connectionDetails.ideBuildNumber}",
137-
"ide_download_link" to "${connectionDetails.downloadSource}",
138-
"web_terminal_link" to "${connectionDetails.webTerminalLink}"
139-
)
140-
)
132+
GatewayUI.getInstance().connect(connectionDetails.toWorkspaceParams())
141133
}
142134
})
143135
label("").resizableColumn().horizontalAlign(HorizontalAlign.FILL)

0 commit comments

Comments
 (0)