Skip to content

Commit 05dac38

Browse files
committed
Impl: resolve compatible jetbrains products
- new step in the wizard showing the jetbrains products and an input for the project location - this step is necessary after a user selected a workspace because - we need to be able to resolve the OS and Arch for the workspace - we need to be able to construct a link the workspace terminal - retrieves a list of gateway compatible products (i.e IDE's) and filters out those that don't match the workspace OS and architecture - connector is initialized with selected platform
1 parent 902bf51 commit 05dac38

9 files changed

+235
-61
lines changed

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

+16-16
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,21 @@ import javax.swing.JComponent
2323
class CoderGatewayConnectionProvider : GatewayConnectionProvider {
2424
private val connections = mutableSetOf<CoderConnectionMetadata>()
2525
override suspend fun connect(parameters: Map<String, String>, requestor: ConnectionRequestor): GatewayConnectionHandle? {
26-
val coderUrl = parameters["coder_url"]
27-
val workspaceName = parameters["workspace_name"]
28-
val user = parameters["username"]
26+
val coderWorkspaceHostname = parameters["coder_workspace_hostname"]
2927
val projectPath = parameters["project_path"]
28+
val ideProductCode = parameters["ide_product_code"]!!
29+
val ideBuildNumber = parameters["ide_build_number"]!!
30+
val ideDownloadLink = parameters["ide_download_link"]
3031

31-
if (coderUrl != null && workspaceName != null) {
32-
val connection = CoderConnectionMetadata(coderUrl, workspaceName)
32+
if (coderWorkspaceHostname != null) {
33+
val connection = CoderConnectionMetadata(coderWorkspaceHostname)
3334
if (connection in connections) {
34-
logger.warning("There is already a connection started on ${connection.url} using the workspace ${connection.workspaceId}")
35+
logger.warning("There is already a connection started on ${connection.workspaceHostname}")
3536
return null
3637
}
3738
val clientLifetime = LifetimeDefinition()
38-
val credentials = RemoteCredentialsHolder()
39-
credentials.apply {
40-
setHost("coder.${workspaceName}")
39+
val credentials = RemoteCredentialsHolder().apply {
40+
setHost(coderWorkspaceHostname)
4141
userName = "coder"
4242
authType = AuthType.OPEN_SSH
4343
}
@@ -46,17 +46,17 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
4646
val context = SshMultistagePanelContext().apply {
4747
deploy = true
4848
sshConfig = SshConfig(true).apply {
49-
setHost("coder.${workspaceName}")
50-
setUsername(user)
49+
setHost(coderWorkspaceHostname)
50+
setUsername("coder")
5151
authType = AuthType.OPEN_SSH
5252
}
5353
remoteProjectPath = projectPath
5454
remoteCommandsExecutor = SshCommandsExecutor.Companion.create(credentials)
5555
downloadMethod = SshDownloadMethod.CustomizedLink
56-
customDownloadLink = "https://download.jetbrains.com/idea/ideaIU-2021.3.3.tar.gz"
56+
customDownloadLink = ideDownloadLink
5757
ide = IdeInfo(
58-
IntelliJPlatformProduct.IDEA,
59-
buildNumber = "213.7172.25"
58+
product = IntelliJPlatformProduct.fromProductCode(ideProductCode)!!,
59+
buildNumber = ideBuildNumber
6060
)
6161
}
6262
val deployPair = async {
@@ -72,7 +72,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
7272

7373
return object : GatewayConnectionHandle(clientLifetime) {
7474
override fun createComponent(): JComponent {
75-
return CoderGatewayConnectionComponent(clientLifetime, coderUrl, workspaceName)
75+
return CoderGatewayConnectionComponent(clientLifetime, coderWorkspaceHostname)
7676
}
7777

7878
override fun getTitle(): String {
@@ -96,4 +96,4 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
9696
}
9797
}
9898

99-
internal data class CoderConnectionMetadata(val url: String, val workspaceId: String)
99+
internal data class CoderConnectionMetadata(val workspaceHostname: String)
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package com.coder.gateway.models
22

3-
import com.coder.gateway.sdk.v1.Workspace
3+
import com.coder.gateway.sdk.v2.models.Workspace
44

5-
data class CoderWorkspacesWizardModel(var coderURL: String = "https://localhost", var token: String = "", var workspaces: List<Workspace> = mutableListOf())
5+
data class CoderWorkspacesWizardModel(
6+
var coderURL: String = "https://localhost",
7+
var token: String = "",
8+
var workspaces: List<Workspace> = mutableListOf(),
9+
var selectedWorkspace: Workspace? = null
10+
)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.intellij.util.ui.components.BorderLayoutPanel
77
import com.jetbrains.rd.util.lifetime.Lifetime
88
import com.jetbrains.rd.util.lifetime.onTermination
99

10-
class CoderGatewayConnectionComponent(val lifetime: Lifetime, val url: String, val workspaceId: String) : BorderLayoutPanel() {
10+
class CoderGatewayConnectionComponent(val lifetime: Lifetime, val url: String) : BorderLayoutPanel() {
1111
private val disposable = Disposer.newDisposable()
1212
private val mainPanel = BorderLayoutPanel().apply {
1313
add(JBLabel(IconManager.getInstance().getIcon("coder_logo_52.svg", CoderGatewayConnectionComponent::class.java)), "Center")

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

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.coder.gateway.views
22

33
import com.coder.gateway.models.CoderWorkspacesWizardModel
44
import com.coder.gateway.views.steps.CoderAuthStepView
5+
import com.coder.gateway.views.steps.CoderLocateRemoteProjectStepView
56
import com.coder.gateway.views.steps.CoderWorkspacesStepView
67
import com.coder.gateway.views.steps.CoderWorkspacesWizardStep
78
import com.intellij.openapi.Disposable
@@ -31,6 +32,7 @@ class CoderGatewayConnectorWizardView : BorderLayoutPanel(), Disposable {
3132

3233
registerStep(CoderAuthStepView())
3334
registerStep(CoderWorkspacesStepView())
35+
registerStep(CoderLocateRemoteProjectStepView())
3436

3537
addToBottom(createBackComponent())
3638

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package com.coder.gateway.views.steps
2+
3+
import com.coder.gateway.CoderGatewayBundle
4+
import com.coder.gateway.models.CoderWorkspacesWizardModel
5+
import com.coder.gateway.sdk.CoderRestClientService
6+
import com.coder.gateway.views.LazyBrowserLink
7+
import com.intellij.ide.IdeBundle
8+
import com.intellij.openapi.Disposable
9+
import com.intellij.openapi.application.ApplicationManager
10+
import com.intellij.openapi.ui.ComboBox
11+
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
12+
import com.intellij.remote.AuthType
13+
import com.intellij.remote.RemoteCredentialsHolder
14+
import com.intellij.ui.AnimatedIcon
15+
import com.intellij.ui.IconManager
16+
import com.intellij.ui.components.JBTextField
17+
import com.intellij.ui.dsl.builder.BottomGap
18+
import com.intellij.ui.dsl.builder.RowLayout
19+
import com.intellij.ui.dsl.builder.TopGap
20+
import com.intellij.ui.dsl.builder.panel
21+
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
22+
import com.intellij.util.ui.JBFont
23+
import com.intellij.util.ui.JBUI
24+
import com.intellij.util.ui.UIUtil
25+
import com.jetbrains.gateway.api.GatewayUI
26+
import com.jetbrains.gateway.ssh.CachingProductsJsonWrapper
27+
import com.jetbrains.gateway.ssh.IdeStatus
28+
import com.jetbrains.gateway.ssh.IdeWithStatus
29+
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
30+
import com.jetbrains.gateway.ssh.guessOs
31+
import kotlinx.coroutines.CoroutineScope
32+
import kotlinx.coroutines.Dispatchers
33+
import kotlinx.coroutines.launch
34+
import kotlinx.coroutines.withContext
35+
import java.awt.Component
36+
import java.awt.FlowLayout
37+
import java.util.logging.Logger
38+
import javax.swing.ComboBoxModel
39+
import javax.swing.DefaultComboBoxModel
40+
import javax.swing.JLabel
41+
import javax.swing.JList
42+
import javax.swing.JPanel
43+
import javax.swing.ListCellRenderer
44+
import javax.swing.SwingConstants
45+
46+
class CoderLocateRemoteProjectStepView : CoderWorkspacesWizardStep, Disposable {
47+
private val cs = CoroutineScope(Dispatchers.Main)
48+
private val coderClient: CoderRestClientService = ApplicationManager.getApplication().getService(CoderRestClientService::class.java)
49+
50+
private val spinner = JLabel("", AnimatedIcon.Default(), SwingConstants.LEFT)
51+
private var ideComboBoxModel = DefaultComboBoxModel<IdeWithStatus>()
52+
53+
private lateinit var titleLabel: JLabel
54+
private lateinit var wizard: CoderWorkspacesWizardModel
55+
private lateinit var cbIDE: IDEComboBox
56+
private lateinit var tfProject: JBTextField
57+
private lateinit var terminalLink: LazyBrowserLink
58+
59+
override val component = panel {
60+
indent {
61+
row {
62+
titleLabel = label("").applyToComponent {
63+
font = JBFont.h3().asBold()
64+
icon = IconManager.getInstance().getIcon("coder_logo_16.svg", this@CoderLocateRemoteProjectStepView::class.java)
65+
}.component
66+
}.bottomGap(BottomGap.MEDIUM)
67+
68+
row {
69+
label("IDE:")
70+
cbIDE = cell(IDEComboBox(ideComboBoxModel).apply {
71+
renderer = IDECellRenderer()
72+
}).resizableColumn().horizontalAlign(HorizontalAlign.FILL).comment("The IDE will be downloaded from jetbrains.com").component
73+
cell()
74+
}.topGap(TopGap.NONE).layout(RowLayout.PARENT_GRID)
75+
76+
row {
77+
label("Project directory:")
78+
tfProject = textField()
79+
.resizableColumn()
80+
.horizontalAlign(HorizontalAlign.FILL)
81+
.applyToComponent {
82+
this.text = "/home/coder/workspace/"
83+
}.component
84+
cell()
85+
}.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).layout(RowLayout.PARENT_GRID)
86+
row {
87+
cell()
88+
terminalLink = cell(
89+
LazyBrowserLink(
90+
IconManager.getInstance().getIcon("open_terminal.svg", this::class.java),
91+
"Open Terminal"
92+
)
93+
).component
94+
}.topGap(TopGap.NONE).layout(RowLayout.PARENT_GRID)
95+
}
96+
}.apply { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() }
97+
98+
override val previousActionText = IdeBundle.message("button.back")
99+
override val nextActionText = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.next.text")
100+
101+
override fun onInit(wizardModel: CoderWorkspacesWizardModel) {
102+
wizard = wizardModel
103+
val selectedWorkspace = wizardModel.selectedWorkspace
104+
if (selectedWorkspace == null) {
105+
logger.warning("No workspace was selected. Please go back to the previous step and select a Coder Workspace")
106+
return
107+
}
108+
109+
titleLabel.text = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", selectedWorkspace.name)
110+
terminalLink.url = "${coderClient.coderURL}/@${coderClient.me.username}/${selectedWorkspace.name}.coder/terminal"
111+
112+
cs.launch {
113+
logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...")
114+
val workspaceOS = withContext(Dispatchers.IO) {
115+
RemoteCredentialsHolder().apply {
116+
setHost("coder.${selectedWorkspace.name}")
117+
userName = "coder"
118+
authType = AuthType.OPEN_SSH
119+
}.guessOs
120+
}
121+
logger.info("Resolved OS and Arch for ${selectedWorkspace.name} is: $workspaceOS")
122+
val idesWithStatus = IntelliJPlatformProduct.values()
123+
.filter { it.showInGateway }
124+
.flatMap { CachingProductsJsonWrapper.getAvailableIdes(it, workspaceOS) }
125+
.map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.DOWNLOAD, ide.downloadLink, ide.presentableVersion) }
126+
127+
if (idesWithStatus.isNullOrEmpty()) {
128+
logger.warning("Could not resolve any IDE for workspace ${selectedWorkspace.name}, probably $workspaceOS is not supported by Gateway")
129+
} else {
130+
cbIDE.remove(spinner)
131+
ideComboBoxModel.addAll(idesWithStatus)
132+
cbIDE.selectedIndex = 0
133+
}
134+
}
135+
}
136+
137+
override fun onNext(wizardModel: CoderWorkspacesWizardModel): Boolean {
138+
val selectedIDE = cbIDE.selectedItem ?: return false
139+
140+
cs.launch {
141+
142+
GatewayUI.getInstance().connect(
143+
mapOf(
144+
"type" to "coder",
145+
"coder_workspace_hostname" to "coder.${wizardModel.selectedWorkspace?.name}",
146+
"project_path" to tfProject.text,
147+
"ide_product_code" to "${selectedIDE.product.productCode}",
148+
"ide_build_number" to "${selectedIDE.buildNumber}",
149+
"ide_download_link" to "${selectedIDE.source}"
150+
)
151+
)
152+
}
153+
return true
154+
}
155+
156+
override fun dispose() {
157+
}
158+
159+
companion object {
160+
val logger = Logger.getLogger(CoderLocateRemoteProjectStepView::class.java.simpleName)
161+
}
162+
163+
private class IDEComboBox(model: ComboBoxModel<IdeWithStatus>) : ComboBox<IdeWithStatus>(model) {
164+
override fun getSelectedItem(): IdeWithStatus? {
165+
return super.getSelectedItem() as IdeWithStatus?
166+
}
167+
}
168+
169+
private class IDECellRenderer : ListCellRenderer<IdeWithStatus> {
170+
override fun getListCellRendererComponent(list: JList<out IdeWithStatus>?, ideWithStatus: IdeWithStatus?, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component {
171+
return if (ideWithStatus == null && index == -1) {
172+
JPanel().apply {
173+
layout = FlowLayout(FlowLayout.LEFT)
174+
add(JLabel("Retrieving products...", AnimatedIcon.Default(), SwingConstants.LEFT))
175+
}
176+
} else if (ideWithStatus != null) {
177+
JPanel().apply {
178+
layout = FlowLayout(FlowLayout.LEFT)
179+
add(JLabel(ideWithStatus.product.ideName, ideWithStatus.product.icon, SwingConstants.LEFT))
180+
add(JLabel("${ideWithStatus.product.productCode} ${ideWithStatus.presentableVersion} ${ideWithStatus.buildNumber} | ${ideWithStatus.status.name.toLowerCase()}").apply {
181+
foreground = UIUtil.getLabelDisabledForeground()
182+
})
183+
background = JBUI.CurrentTheme.List.background(isSelected, cellHasFocus)
184+
}
185+
} else {
186+
JPanel()
187+
}
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)