Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit ee340c6

Browse files
authored
Merge pull request #158 from coder/impl-assist-project-path
Impl: remote project path validation
2 parents 48d20d7 + 85e90da commit ee340c6

File tree

2 files changed

+78
-14
lines changed

2 files changed

+78
-14
lines changed

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

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@ import com.intellij.openapi.Disposable
1717
import com.intellij.openapi.application.ApplicationManager
1818
import com.intellij.openapi.diagnostic.Logger
1919
import com.intellij.openapi.ui.ComboBox
20+
import com.intellij.openapi.ui.ComponentValidator
21+
import com.intellij.openapi.ui.ValidationInfo
22+
import com.intellij.openapi.util.Disposer
2023
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
2124
import com.intellij.remote.AuthType
2225
import com.intellij.remote.RemoteCredentialsHolder
26+
import com.intellij.ssh.SshException
2327
import com.intellij.ui.AnimatedIcon
2428
import com.intellij.ui.ColoredListCellRenderer
29+
import com.intellij.ui.DocumentAdapter
2530
import com.intellij.ui.components.JBTextField
2631
import com.intellij.ui.dsl.builder.BottomGap
2732
import com.intellij.ui.dsl.builder.RowLayout
@@ -30,6 +35,8 @@ import com.intellij.ui.dsl.builder.panel
3035
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
3136
import com.intellij.util.ui.JBFont
3237
import com.intellij.util.ui.UIUtil
38+
import com.intellij.util.ui.update.MergingUpdateQueue
39+
import com.intellij.util.ui.update.Update
3340
import com.jetbrains.gateway.api.GatewayUI
3441
import com.jetbrains.gateway.ssh.CachingProductsJsonWrapper
3542
import com.jetbrains.gateway.ssh.DeployTargetOS
@@ -43,13 +50,17 @@ import kotlinx.coroutines.CancellationException
4350
import kotlinx.coroutines.CoroutineScope
4451
import kotlinx.coroutines.Dispatchers
4552
import kotlinx.coroutines.Job
53+
import kotlinx.coroutines.TimeoutCancellationException
4654
import kotlinx.coroutines.async
4755
import kotlinx.coroutines.cancel
4856
import kotlinx.coroutines.cancelAndJoin
4957
import kotlinx.coroutines.launch
58+
import kotlinx.coroutines.runBlocking
59+
import kotlinx.coroutines.time.withTimeout
5060
import kotlinx.coroutines.withContext
5161
import java.awt.Component
5262
import java.awt.FlowLayout
63+
import java.time.Duration
5364
import java.util.Locale
5465
import javax.swing.ComboBoxModel
5566
import javax.swing.DefaultComboBoxModel
@@ -58,6 +69,7 @@ import javax.swing.JList
5869
import javax.swing.JPanel
5970
import javax.swing.ListCellRenderer
6071
import javax.swing.SwingConstants
72+
import javax.swing.event.DocumentEvent
6173

6274
class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit) : CoderWorkspacesWizardStep, Disposable {
6375
private val cs = CoroutineScope(Dispatchers.Main)
@@ -68,10 +80,10 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
6880
private lateinit var titleLabel: JLabel
6981
private lateinit var wizard: CoderWorkspacesWizardModel
7082
private lateinit var cbIDE: IDEComboBox
71-
private lateinitvar tfProject:JBTextField
83+
private var tfProject=JBTextField()
7284
private lateinit var terminalLink: LazyBrowserLink
73-
7485
private lateinit var ideResolvingJob: Job
86+
private val pathValidationJobs = MergingUpdateQueue("remote-path-validation", 1000, true, tfProject)
7587

7688
override val component = panel {
7789
indent {
@@ -92,9 +104,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
92104

93105
row {
94106
label("Project directory:")
95-
tfProject = textField()
96-
.resizableColumn()
97-
.horizontalAlign(HorizontalAlign.FILL).component
107+
cell(tfProject).resizableColumn().horizontalAlign(HorizontalAlign.FILL).component
98108
cell()
99109
}.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).layout(RowLayout.PARENT_GRID)
100110
row {
@@ -113,6 +123,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
113123
override val nextActionText = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.next.text")
114124

115125
override fun onInit(wizardModel: CoderWorkspacesWizardModel) {
126+
cbIDE.renderer = IDECellRenderer()
116127
ideComboBoxModel.removeAllElements()
117128
wizard = wizardModel
118129
val selectedWorkspace = wizardModel.selectedWorkspace
@@ -127,11 +138,30 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
127138

128139
ideResolvingJob = cs.launch {
129140
try {
130-
retrieveIDES(selectedWorkspace)
141+
val executor = withTimeout(Duration.ofSeconds(60)) { createRemoteExecutor() }
142+
retrieveIDES(executor, selectedWorkspace)
143+
if (ComponentValidator.getInstance(tfProject).isEmpty) {
144+
installRemotePathValidator(executor)
145+
}
131146
} catch (e: Exception) {
132147
when (e) {
133148
is InterruptedException -> Unit
134149
is CancellationException -> Unit
150+
is TimeoutCancellationException,
151+
is SshException -> {
152+
logger.error("Can't connect to workspace ${selectedWorkspace.name}. Reason: $e")
153+
withContext(Dispatchers.Main) {
154+
disableNextAction()
155+
cbIDE.renderer = object : ColoredListCellRenderer<IdeWithStatus>() {
156+
override fun customizeCellRenderer(list: JList<out IdeWithStatus>, value: IdeWithStatus?, index: Int, isSelected: Boolean, cellHasFocus: Boolean) {
157+
background = UIUtil.getListBackground(isSelected, cellHasFocus)
158+
icon = UIUtil.getBalloonErrorIcon()
159+
append(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ssh.error.text"))
160+
}
161+
}
162+
}
163+
}
164+
135165
else -> {
136166
logger.error("Could not resolve any IDE for workspace ${selectedWorkspace.name}. Reason: $e")
137167
withContext(Dispatchers.Main) {
@@ -140,7 +170,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
140170
override fun customizeCellRenderer(list: JList<out IdeWithStatus>, value: IdeWithStatus?, index: Int, isSelected: Boolean, cellHasFocus: Boolean) {
141171
background = UIUtil.getListBackground(isSelected, cellHasFocus)
142172
icon = UIUtil.getBalloonErrorIcon()
143-
append(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.error.text", selectedWorkspace.name))
173+
append(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.error.text"))
144174
}
145175
}
146176
}
@@ -150,23 +180,56 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
150180
}
151181
}
152182

153-
private suspend fun retrieveIDES(selectedWorkspace: WorkspaceAgentModel) {
154-
logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...")
155-
val hostAccessor = HighLevelHostAccessor.create(
183+
private fun installRemotePathValidator(executor: HighLevelHostAccessor) {
184+
var disposable = Disposer.newDisposable(ApplicationManager.getApplication(), CoderLocateRemoteProjectStepView.javaClass.name)
185+
ComponentValidator(disposable).installOn(tfProject)
186+
187+
tfProject.document.addDocumentListener(object : DocumentAdapter() {
188+
override fun textChanged(event: DocumentEvent) {
189+
pathValidationJobs.queue(Update.create("validate-remote-path") {
190+
runBlocking {
191+
try {
192+
val isPathPresent = executor.isPathPresentOnRemote(tfProject.text)
193+
if (!isPathPresent) {
194+
ComponentValidator.getInstance(tfProject).ifPresent {
195+
it.updateInfo(ValidationInfo("Can't find directory: ${tfProject.text}", tfProject))
196+
}
197+
} else {
198+
ComponentValidator.getInstance(tfProject).ifPresent {
199+
it.updateInfo(null)
200+
}
201+
}
202+
} catch (e: Exception) {
203+
ComponentValidator.getInstance(tfProject).ifPresent {
204+
it.updateInfo(ValidationInfo("Can't validate directory: ${tfProject.text}", tfProject))
205+
}
206+
}
207+
}
208+
})
209+
}
210+
})
211+
}
212+
213+
private suspend fun createRemoteExecutor(): HighLevelHostAccessor {
214+
return HighLevelHostAccessor.create(
156215
RemoteCredentialsHolder().apply {
157-
setHost("coder.${selectedWorkspace.name}")
216+
setHost("coder.${wizard.selectedWorkspace?.name}")
158217
userName = "coder"
159218
authType = AuthType.OPEN_SSH
160219
},
161220
true
162221
)
222+
}
223+
224+
private suspend fun retrieveIDES(executor: HighLevelHostAccessor, selectedWorkspace: WorkspaceAgentModel) {
225+
logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...")
163226
val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null) toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) else withContext(Dispatchers.IO) {
164-
hostAccessor.guessOs()
227+
executor.guessOs()
165228
}
166229

167230
logger.info("Resolved OS and Arch for ${selectedWorkspace.name} is: $workspaceOS")
168231
val installedIdesJob = cs.async(Dispatchers.IO) {
169-
hostAccessor.getInstalledIDEs().map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.ALREADY_INSTALLED, null, ide.pathToIde, ide.presentableVersion, ide.remoteDevType) }
232+
executor.getInstalledIDEs().map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.ALREADY_INSTALLED, null, ide.pathToIde, ide.presentableVersion, ide.remoteDevType) }
170233
}
171234
val idesWithStatusJob = cs.async(Dispatchers.IO) {
172235
IntelliJPlatformProduct.values()

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ gateway.connector.view.coder.workspaces.unsupported.os.info=Gateway supports onl
2020
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>
2121
gateway.connector.view.coder.workspaces.unsupported.coder.version=Coder version {0} might not be compatible with this plugin 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>
2222
gateway.connector.view.coder.remoteproject.loading.text=Retrieving products...
23-
gateway.connector.view.coder.remoteproject.ide.error.text=Could not retrieve any IDE for workspace {0} because an error was encountered. Please check the logs for more details!
23+
gateway.connector.view.coder.remoteproject.ide.error.text=Could not retrieve any IDE because an error was encountered. Please check the logs for more details!
24+
gateway.connector.view.coder.remoteproject.ssh.error.text=Can't connect to the workspace. Please make sure Coder Agent is running!
2425
gateway.connector.view.coder.remoteproject.next.text=Start IDE and connect
2526
gateway.connector.view.coder.remoteproject.choose.text=Choose IDE and project for workspace {0}
2627
gateway.connector.recentconnections.title=Recent Coder Workspaces

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /