Skip to content

Commit 1051381

Browse files
committed
impl: remote path validation
- every time text changes on the text field input a job to validate path on the remote host is queued - if path is invalid then an error hint is displayed - IDE combo box renderer is reset each time the IDE&Project panel is displayed - resolves #155
1 parent 48d20d7 commit 1051381

File tree

1 file changed

+56
-12
lines changed

1 file changed

+56
-12
lines changed

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

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ 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
2326
import com.intellij.ui.AnimatedIcon
2427
import com.intellij.ui.ColoredListCellRenderer
28+
import com.intellij.ui.DocumentAdapter
2529
import com.intellij.ui.components.JBTextField
2630
import com.intellij.ui.dsl.builder.BottomGap
2731
import com.intellij.ui.dsl.builder.RowLayout
@@ -30,6 +34,8 @@ import com.intellij.ui.dsl.builder.panel
3034
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
3135
import com.intellij.util.ui.JBFont
3236
import com.intellij.util.ui.UIUtil
37+
import com.intellij.util.ui.update.MergingUpdateQueue
38+
import com.intellij.util.ui.update.Update
3339
import com.jetbrains.gateway.api.GatewayUI
3440
import com.jetbrains.gateway.ssh.CachingProductsJsonWrapper
3541
import com.jetbrains.gateway.ssh.DeployTargetOS
@@ -47,6 +53,7 @@ import kotlinx.coroutines.async
4753
import kotlinx.coroutines.cancel
4854
import kotlinx.coroutines.cancelAndJoin
4955
import kotlinx.coroutines.launch
56+
import kotlinx.coroutines.runBlocking
5057
import kotlinx.coroutines.withContext
5158
import java.awt.Component
5259
import java.awt.FlowLayout
@@ -58,6 +65,7 @@ import javax.swing.JList
5865
import javax.swing.JPanel
5966
import javax.swing.ListCellRenderer
6067
import javax.swing.SwingConstants
68+
import javax.swing.event.DocumentEvent
6169

6270
class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit) : CoderWorkspacesWizardStep, Disposable {
6371
private val cs = CoroutineScope(Dispatchers.Main)
@@ -68,10 +76,10 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
6876
private lateinit var titleLabel: JLabel
6977
private lateinit var wizard: CoderWorkspacesWizardModel
7078
private lateinit var cbIDE: IDEComboBox
71-
private lateinit var tfProject: JBTextField
79+
private var tfProject = JBTextField()
7280
private lateinit var terminalLink: LazyBrowserLink
73-
7481
private lateinit var ideResolvingJob: Job
82+
private val pathValidationJobs = MergingUpdateQueue("remote-path-validation", 1000, true, tfProject)
7583

7684
override val component = panel {
7785
indent {
@@ -92,9 +100,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
92100

93101
row {
94102
label("Project directory:")
95-
tfProject = textField()
96-
.resizableColumn()
97-
.horizontalAlign(HorizontalAlign.FILL).component
103+
cell(tfProject).resizableColumn().horizontalAlign(HorizontalAlign.FILL).component
98104
cell()
99105
}.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).layout(RowLayout.PARENT_GRID)
100106
row {
@@ -113,6 +119,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
113119
override val nextActionText = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.next.text")
114120

115121
override fun onInit(wizardModel: CoderWorkspacesWizardModel) {
122+
cbIDE.renderer = IDECellRenderer()
116123
ideComboBoxModel.removeAllElements()
117124
wizard = wizardModel
118125
val selectedWorkspace = wizardModel.selectedWorkspace
@@ -127,7 +134,11 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
127134

128135
ideResolvingJob = cs.launch {
129136
try {
130-
retrieveIDES(selectedWorkspace)
137+
val executor = withContext(Dispatchers.IO) { createRemoteExecutor() }
138+
retrieveIDES(executor, selectedWorkspace)
139+
if (ComponentValidator.getInstance(tfProject).isEmpty) {
140+
installRemotePathValidator(executor)
141+
}
131142
} catch (e: Exception) {
132143
when (e) {
133144
is InterruptedException -> Unit
@@ -150,23 +161,56 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
150161
}
151162
}
152163

153-
private suspend fun retrieveIDES(selectedWorkspace: WorkspaceAgentModel) {
154-
logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...")
155-
val hostAccessor = HighLevelHostAccessor.create(
164+
private fun installRemotePathValidator(executor: HighLevelHostAccessor) {
165+
var disposable = Disposer.newDisposable(ApplicationManager.getApplication(), CoderLocateRemoteProjectStepView.javaClass.name)
166+
ComponentValidator(disposable).installOn(tfProject)
167+
168+
tfProject.document.addDocumentListener(object : DocumentAdapter() {
169+
override fun textChanged(event: DocumentEvent) {
170+
pathValidationJobs.queue(Update.create("validate-remote-path") {
171+
runBlocking {
172+
try {
173+
val isPathPresent = executor.isPathPresentOnRemote(tfProject.text)
174+
if (!isPathPresent) {
175+
ComponentValidator.getInstance(tfProject).ifPresent {
176+
it.updateInfo(ValidationInfo("Can't find directory: ${tfProject.text}", tfProject))
177+
}
178+
} else {
179+
ComponentValidator.getInstance(tfProject).ifPresent {
180+
it.updateInfo(null)
181+
}
182+
}
183+
} catch (e: Exception) {
184+
ComponentValidator.getInstance(tfProject).ifPresent {
185+
it.updateInfo(ValidationInfo("Can't validate directory: ${tfProject.text}", tfProject))
186+
}
187+
}
188+
}
189+
})
190+
}
191+
})
192+
}
193+
194+
private suspend fun createRemoteExecutor(): HighLevelHostAccessor {
195+
return HighLevelHostAccessor.create(
156196
RemoteCredentialsHolder().apply {
157-
setHost("coder.${selectedWorkspace.name}")
197+
setHost("coder.${wizard.selectedWorkspace?.name}")
158198
userName = "coder"
159199
authType = AuthType.OPEN_SSH
160200
},
161201
true
162202
)
203+
}
204+
205+
private suspend fun retrieveIDES(executor: HighLevelHostAccessor, selectedWorkspace: WorkspaceAgentModel) {
206+
logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...")
163207
val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null) toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) else withContext(Dispatchers.IO) {
164-
hostAccessor.guessOs()
208+
executor.guessOs()
165209
}
166210

167211
logger.info("Resolved OS and Arch for ${selectedWorkspace.name} is: $workspaceOS")
168212
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) }
213+
executor.getInstalledIDEs().map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.ALREADY_INSTALLED, null, ide.pathToIde, ide.presentableVersion, ide.remoteDevType) }
170214
}
171215
val idesWithStatusJob = cs.async(Dispatchers.IO) {
172216
IntelliJPlatformProduct.values()

0 commit comments

Comments
 (0)