Skip to content

List installed ides #136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- ability to open a template in the Dashboard
- ability to sort by workspace name, or by template name or by workspace status
- a new token is requested when the one persisted is expired
- support for re-using already installed IDE backends

### Changed
- renamed the plugin from `Coder Gateway` to `Gateway`
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pluginGroup=com.coder.gateway
pluginName=coder-gateway
# SemVer format -> https://semver.org
pluginVersion=2.1.3
pluginVersion=2.1.4
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild=222.3739.54
Expand Down
74 changes: 10 additions & 64 deletions src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,93 +2,39 @@

package com.coder.gateway

import com.coder.gateway.models.RecentWorkspaceConnection
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
import com.intellij.openapi.components.service
import com.intellij.openapi.rd.util.launchUnderBackgroundProgress
import com.intellij.remote.AuthType
import com.intellij.remote.RemoteCredentialsHolder
import com.intellij.ssh.config.unified.SshConfig
import com.jetbrains.gateway.api.ConnectionRequestor
import com.jetbrains.gateway.api.GatewayConnectionHandle
import com.jetbrains.gateway.api.GatewayConnectionProvider
import com.jetbrains.gateway.api.GatewayUI
import com.jetbrains.gateway.ssh.HighLevelHostAccessor
import com.jetbrains.gateway.ssh.HostDeployInputs
import com.jetbrains.gateway.ssh.IdeInfo
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
import com.jetbrains.gateway.ssh.SshDeployFlowUtil
import com.jetbrains.gateway.ssh.SshMultistagePanelContext
import com.jetbrains.gateway.ssh.deploy.DeployTargetInfo.DeployWithDownload
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
import kotlinx.coroutines.launch
import java.net.URI
import java.time.Duration
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

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

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

override suspend fun connect(parameters: Map<String, String>, requestor: ConnectionRequestor): GatewayConnectionHandle? {
val coderWorkspaceHostname = parameters["coder_workspace_hostname"]
val projectPath = parameters["project_path"]
val ideProductCode = parameters["ide_product_code"]!!
val ideBuildNumber = parameters["ide_build_number"]!!
val ideDownloadLink = parameters["ide_download_link"]!!
val webTerminalLink = parameters["web_terminal_link"]!!

if (coderWorkspaceHostname != null && projectPath != null) {
val sshConfiguration = SshConfig(true).apply {
setHost(coderWorkspaceHostname)
setUsername("coder")
port = 22
authType = AuthType.OPEN_SSH
}

val clientLifetime = LifetimeDefinition()
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title"), canBeCancelled = true, isIndeterminate = true, project = null) {
val context = SshMultistagePanelContext(
HostDeployInputs.FullySpecified(
remoteProjectPath = projectPath,
deployTarget = DeployWithDownload(
URI(ideDownloadLink),
null,
IdeInfo(
product = IntelliJPlatformProduct.fromProductCode(ideProductCode)!!,
buildNumber = ideBuildNumber
)
),
remoteInfo = HostDeployInputs.WithDeployedWorker(
HighLevelHostAccessor.create(
RemoteCredentialsHolder().apply {
setHost(coderWorkspaceHostname)
userName = "coder"
port = 22
authType = AuthType.OPEN_SSH
},
true
),
HostDeployInputs.WithHostInfo(sshConfiguration)
)
)
val clientLifetime = LifetimeDefinition()
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title"), canBeCancelled = true, isIndeterminate = true, project = null) {
val context = SshMultistagePanelContext(parameters.toHostDeployInputs())
launch {
@Suppress("UnstableApiUsage") SshDeployFlowUtil.fullDeployCycle(
clientLifetime, context, Duration.ofMinutes(10)
)
launch {
@Suppress("UnstableApiUsage") SshDeployFlowUtil.fullDeployCycle(
clientLifetime, context, Duration.ofMinutes(10)
)
}
}

recentConnectionsService.addRecentConnection(RecentWorkspaceConnection(coderWorkspaceHostname, projectPath, localTimeFormatter.format(LocalDateTime.now()), ideProductCode, ideBuildNumber, ideDownloadLink, webTerminalLink))
GatewayUI.getInstance().reset()
}

recentConnectionsService.addRecentConnection(parameters.toRecentWorkspaceConnection())
GatewayUI.getInstance().reset()
return null
}

override fun isApplicable(parameters: Map<String, String>): Boolean {
return parameters["type"] == "coder"
return parameters.areCoderType()
}
}
154 changes: 154 additions & 0 deletions src/main/kotlin/com/coder/gateway/WorkspaceParams.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.coder.gateway

import com.coder.gateway.models.RecentWorkspaceConnection
import com.intellij.remote.AuthType
import com.intellij.remote.RemoteCredentialsHolder
import com.intellij.ssh.config.unified.SshConfig
import com.jetbrains.gateway.ssh.HighLevelHostAccessor
import com.jetbrains.gateway.ssh.HostDeployInputs
import com.jetbrains.gateway.ssh.IdeInfo
import com.jetbrains.gateway.ssh.IdeWithStatus
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
import com.jetbrains.gateway.ssh.deploy.DeployTargetInfo
import java.net.URI
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

private const val CODER_WORKSPACE_HOSTNAME = "coder_workspace_hostname"
private const val TYPE = "type"
private const val VALUE_FOR_TYPE = "coder"
private const val PROJECT_PATH = "project_path"
private const val IDE_DOWNLOAD_LINK = "ide_download_link"
private const val IDE_PRODUCT_CODE = "ide_product_code"
private const val IDE_BUILD_NUMBER = "ide_build_number"
private const val IDE_PATH_ON_HOST = "ide_path_on_host"
private const val WEB_TERMINAL_LINK = "web_terminal_link"

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

fun RecentWorkspaceConnection.toWorkspaceParams(): Map<String, String> {
val map = mutableMapOf(
TYPE to VALUE_FOR_TYPE,
CODER_WORKSPACE_HOSTNAME to "${this.coderWorkspaceHostname}",
PROJECT_PATH to this.projectPath!!,
IDE_PRODUCT_CODE to IntelliJPlatformProduct.fromProductCode(this.ideProductCode!!)!!.productCode,
IDE_BUILD_NUMBER to "${this.ideBuildNumber}",
WEB_TERMINAL_LINK to "${this.webTerminalLink}"
)

if (!this.downloadSource.isNullOrBlank()) {
map[IDE_DOWNLOAD_LINK] = this.downloadSource!!
} else {
map[IDE_PATH_ON_HOST] = this.idePathOnHost!!
}
return map
}

fun IdeWithStatus.toWorkspaceParams(): Map<String, String> {
val workspaceParams = mutableMapOf(
TYPE to VALUE_FOR_TYPE,
IDE_PRODUCT_CODE to this.product.productCode,
IDE_BUILD_NUMBER to this.buildNumber
)

if (this.download != null) {
workspaceParams[IDE_DOWNLOAD_LINK] = this.download!!.link
}

if (!this.pathOnHost.isNullOrBlank()) {
workspaceParams[IDE_PATH_ON_HOST] = this.pathOnHost!!
}

return workspaceParams
}

fun Map<String, String>.withWorkspaceHostname(hostname: String): Map<String, String> {
val map = this.toMutableMap()
map[CODER_WORKSPACE_HOSTNAME] = hostname
return map
}

fun Map<String, String>.withProjectPath(projectPath: String): Map<String, String> {
val map = this.toMutableMap()
map[PROJECT_PATH] = projectPath
return map
}

fun Map<String, String>.withWebTerminalLink(webTerminalLink: String): Map<String, String> {
val map = this.toMutableMap()
map[WEB_TERMINAL_LINK] = webTerminalLink
return map
}

fun Map<String, String>.areCoderType(): Boolean {
return this[TYPE] == VALUE_FOR_TYPE && !this[CODER_WORKSPACE_HOSTNAME].isNullOrBlank() && !this[PROJECT_PATH].isNullOrBlank()
}

fun Map<String, String>.toSshConfig(): SshConfig {
return SshConfig(true).apply {
setHost([email protected]())
setUsername("coder")
port = 22
authType = AuthType.OPEN_SSH
}
}

suspend fun Map<String, String>.toHostDeployInputs(): HostDeployInputs {
return HostDeployInputs.FullySpecified(
remoteProjectPath = this[PROJECT_PATH]!!,
deployTarget = this.toDeployTargetInfo(),
remoteInfo = HostDeployInputs.WithDeployedWorker(
HighLevelHostAccessor.create(
RemoteCredentialsHolder().apply {
setHost([email protected]())
userName = "coder"
port = 22
authType = AuthType.OPEN_SSH
},
true
),
HostDeployInputs.WithHostInfo(this.toSshConfig())
)
)
}

private fun Map<String, String>.toIdeInfo(): IdeInfo {
return IdeInfo(
product = IntelliJPlatformProduct.fromProductCode(this[IDE_PRODUCT_CODE]!!)!!,
buildNumber = this[IDE_BUILD_NUMBER]!!
)
}

private fun Map<String, String>.toDeployTargetInfo(): DeployTargetInfo {
return if (!this[IDE_DOWNLOAD_LINK].isNullOrBlank()) DeployTargetInfo.DeployWithDownload(
URI(this[IDE_DOWNLOAD_LINK]),
null,
this.toIdeInfo()
)
else DeployTargetInfo.NoDeploy(this[IDE_PATH_ON_HOST]!!, this.toIdeInfo())
}

private fun Map<String, String>.workspaceHostname() = this[CODER_WORKSPACE_HOSTNAME]!!
private fun Map<String, String>.projectPath() = this[PROJECT_PATH]!!

fun Map<String, String>.toRecentWorkspaceConnection(): RecentWorkspaceConnection {
return if (!this[IDE_DOWNLOAD_LINK].isNullOrBlank()) RecentWorkspaceConnection(
this.workspaceHostname(),
this.projectPath(),
localTimeFormatter.format(LocalDateTime.now()),
this[IDE_PRODUCT_CODE]!!,
this[IDE_BUILD_NUMBER]!!,
this[IDE_DOWNLOAD_LINK]!!,
null,
this[WEB_TERMINAL_LINK]!!
) else RecentWorkspaceConnection(
this.workspaceHostname(),
this.projectPath(),
localTimeFormatter.format(LocalDateTime.now()),
this[IDE_PRODUCT_CODE]!!,
this[IDE_BUILD_NUMBER]!!,
null,
this[IDE_PATH_ON_HOST],
this[WEB_TERMINAL_LINK]!!
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import com.intellij.openapi.components.BaseState
import com.intellij.util.xmlb.annotations.Attribute

class RecentWorkspaceConnection() : BaseState(), Comparable<RecentWorkspaceConnection> {
constructor(hostname: String, prjPath: String, openedAt: String, productCode: String, buildNumber: String, source: String, terminalLink: String) : this() {
constructor(hostname: String, prjPath: String, openedAt: String, productCode: String, buildNumber: String, source: String?, idePath: String?, terminalLink: String) : this() {
coderWorkspaceHostname = hostname
projectPath = prjPath
lastOpened = openedAt
ideProductCode = productCode
ideBuildNumber = buildNumber
downloadSource = source
idePathOnHost = idePath
webTerminalLink = terminalLink
}

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


@get:Attribute
var idePathOnHost by string()

@get:Attribute
var webTerminalLink by string()

Expand All @@ -47,6 +52,7 @@ class RecentWorkspaceConnection() : BaseState(), Comparable<RecentWorkspaceConne
if (ideProductCode != other.ideProductCode) return false
if (ideBuildNumber != other.ideBuildNumber) return false
if (downloadSource != other.downloadSource) return false
if (idePathOnHost != other.idePathOnHost) return false
if (webTerminalLink != other.webTerminalLink) return false

return true
Expand All @@ -59,6 +65,7 @@ class RecentWorkspaceConnection() : BaseState(), Comparable<RecentWorkspaceConne
result = 31 * result + (ideProductCode?.hashCode() ?: 0)
result = 31 * result + (ideBuildNumber?.hashCode() ?: 0)
result = 31 * result + (downloadSource?.hashCode() ?: 0)
result = 31 * result + (idePathOnHost?.hashCode() ?: 0)
result = 31 * result + (webTerminalLink?.hashCode() ?: 0)

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

val n = other.webTerminalLink?.let { webTerminalLink?.compareTo(it) }
if (n != null && n != 0) return n
val n = other.idePathOnHost?.let { idePathOnHost?.compareTo(it) }
if (n != null && m != 0) return n

val o = other.webTerminalLink?.let { webTerminalLink?.compareTo(it) }
if (o != null && n != 0) return o

return 0
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.coder.gateway.CoderGatewayConstants
import com.coder.gateway.icons.CoderIcons
import com.coder.gateway.models.RecentWorkspaceConnection
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
import com.coder.gateway.toWorkspaceParams
import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.Disposable
Expand Down Expand Up @@ -37,6 +38,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.awt.Component
import java.awt.Dimension
import java.util.Locale
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.event.DocumentEvent
Expand Down Expand Up @@ -70,7 +72,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
val toSearchFor = [email protected]
val filteredConnections = recentConnectionsService.getAllRecentConnections().filter { it.coderWorkspaceHostname?.toLowerCase()?.contains(toSearchFor) ?: false || it.projectPath?.toLowerCase()?.contains(toSearchFor) ?: false }
val filteredConnections = recentConnectionsService.getAllRecentConnections().filter { it.coderWorkspaceHostname?.lowercase(Locale.getDefault())?.contains(toSearchFor) ?: false || it.projectPath?.lowercase(Locale.getDefault())?.contains(toSearchFor) ?: false }
updateContentView(filteredConnections.groupBy { it.coderWorkspaceHostname })
}
})
Expand Down Expand Up @@ -127,17 +129,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
icon(product.icon)
cell(ActionLink(connectionDetails.projectPath!!) {
cs.launch {
GatewayUI.getInstance().connect(
mapOf(
"type" to "coder",
"coder_workspace_hostname" to "${connectionDetails.coderWorkspaceHostname}",
"project_path" to connectionDetails.projectPath!!,
"ide_product_code" to product.productCode,
"ide_build_number" to "${connectionDetails.ideBuildNumber}",
"ide_download_link" to "${connectionDetails.downloadSource}",
"web_terminal_link" to "${connectionDetails.webTerminalLink}"
)
)
GatewayUI.getInstance().connect(connectionDetails.toWorkspaceParams())
}
})
label("").resizableColumn().horizontalAlign(HorizontalAlign.FILL)
Expand Down
Loading