Skip to content

Commit 1c2ae9f

Browse files
committed
fix: skip installed EAP, RC, NIGHTLY and PREVIEW ides from showing if they are superseded
The IDE and Project view will no longer show IDEs that are installed and which are not yet released, but they have a released version available for download.
1 parent a29031a commit 1c2ae9f

File tree

3 files changed

+124
-46
lines changed

3 files changed

+124
-46
lines changed

CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66

77
### Changed
88

9-
Retrieve workspace directly in link handler when using wildcardSSH feature
9+
- Retrieve workspace directly in link handler when using wildcardSSH feature
10+
11+
### Fixed
12+
13+
- installed EAP, RC, NIGHTLY and PREVIEW IDEs are no longer displayed if there is a higher released version available for download.
1014

1115
## 2.19.0 - 2025-02-21
1216

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

+54-17
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import com.jetbrains.gateway.ssh.IdeStatus
66
import com.jetbrains.gateway.ssh.IdeWithStatus
77
import com.jetbrains.gateway.ssh.InstalledIdeUIEx
88
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
9+
import com.jetbrains.gateway.ssh.ReleaseType
910
import com.jetbrains.gateway.ssh.deploy.ShellArgument
1011
import java.net.URL
1112
import java.nio.file.Path
1213
import kotlin.io.path.name
1314

15+
private val NON_STABLE_RELEASE_TYPES = setOf("EAP", "RC", "NIGHTLY", "PREVIEW")
16+
1417
/**
1518
* Validated parameters for downloading and opening a project using an IDE on a
1619
* workspace.
@@ -101,7 +104,8 @@ class WorkspaceProjectIDE(
101104
name = name,
102105
hostname = hostname,
103106
projectPath = projectPath,
104-
ideProduct = IntelliJPlatformProduct.fromProductCode(ideProductCode) ?: throw Exception("invalid product code"),
107+
ideProduct = IntelliJPlatformProduct.fromProductCode(ideProductCode)
108+
?: throw Exception("invalid product code"),
105109
ideBuildNumber = ideBuildNumber,
106110
idePathOnHost = idePathOnHost,
107111
downloadSource = downloadSource,
@@ -126,13 +130,13 @@ fun RecentWorkspaceConnection.toWorkspaceProjectIDE(): WorkspaceProjectIDE {
126130
// connections page, so it could be missing. Try to get it from the
127131
// host name.
128132
name =
129-
if (name.isNullOrBlank() && !hostname.isNullOrBlank()) {
130-
hostname
131-
.removePrefix("coder-jetbrains--")
132-
.removeSuffix("--${hostname.split("--").last()}")
133-
} else {
134-
name
135-
},
133+
if (name.isNullOrBlank() && !hostname.isNullOrBlank()) {
134+
hostname
135+
.removePrefix("coder-jetbrains--")
136+
.removeSuffix("--${hostname.split("--").last()}")
137+
} else {
138+
name
139+
},
136140
hostname = hostname,
137141
projectPath = projectPath,
138142
ideProductCode = ideProductCode,
@@ -146,17 +150,17 @@ fun RecentWorkspaceConnection.toWorkspaceProjectIDE(): WorkspaceProjectIDE {
146150
// the config directory). For backwards compatibility with existing
147151
// entries, extract the URL from the config directory or host name.
148152
deploymentURL =
149-
if (deploymentURL.isNullOrBlank()) {
150-
if (!dir.isNullOrBlank()) {
151-
"https://${Path.of(dir).parent.name}"
152-
} else if (!hostname.isNullOrBlank()) {
153-
"https://${hostname.split("--").last()}"
153+
if (deploymentURL.isNullOrBlank()) {
154+
if (!dir.isNullOrBlank()) {
155+
"https://${Path.of(dir).parent.name}"
156+
} else if (!hostname.isNullOrBlank()) {
157+
"https://${hostname.split("--").last()}"
158+
} else {
159+
deploymentURL
160+
}
154161
} else {
155162
deploymentURL
156-
}
157-
} else {
158-
deploymentURL
159-
},
163+
},
160164
lastOpened = lastOpened,
161165
)
162166
}
@@ -195,6 +199,39 @@ fun AvailableIde.toIdeWithStatus(): IdeWithStatus = IdeWithStatus(
195199
remoteDevType = remoteDevType,
196200
)
197201

202+
/**
203+
* Returns a list of installed IDEs that don't have a RELEASED version available for download.
204+
* Typically, installed EAP, RC, nightly or preview builds should be superseded by released versions.
205+
*/
206+
fun List<InstalledIdeUIEx>.filterOutAvailableReleasedIdes(availableIde: List<AvailableIde>): List<InstalledIdeUIEx> {
207+
val availableReleasedByProductCode = availableIde
208+
.filter { it.releaseType == ReleaseType.RELEASE }
209+
.groupBy { it.product.productCode }
210+
val result = mutableListOf<InstalledIdeUIEx>()
211+
212+
this.forEach { installedIde ->
213+
// installed IDEs have the release type embedded in the presentable version
214+
// which is a string in the form: 2024.2.4 NIGHTLY
215+
if (NON_STABLE_RELEASE_TYPES.any { it in installedIde.presentableVersion }) {
216+
// we can show the installed IDe if there isn't a higher released version available for download
217+
if (installedIde.isSNotSupersededBy(availableReleasedByProductCode[installedIde.product.productCode])) {
218+
result.add(installedIde)
219+
}
220+
} else {
221+
result.add(installedIde)
222+
}
223+
}
224+
225+
return result
226+
}
227+
228+
private fun InstalledIdeUIEx.isSNotSupersededBy(availableIdes: List<AvailableIde>?): Boolean {
229+
if (availableIdes.isNullOrEmpty()) {
230+
return true
231+
}
232+
return !availableIdes.any { it.buildNumber >= this.buildNumber }
233+
}
234+
198235
/**
199236
* Convert an installed IDE to an IDE with status.
200237
*/

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

+65-28
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.coder.gateway.CoderGatewayBundle
44
import com.coder.gateway.cli.CoderCLIManager
55
import com.coder.gateway.icons.CoderIcons
66
import com.coder.gateway.models.WorkspaceProjectIDE
7+
import com.coder.gateway.models.filterOutAvailableReleasedIdes
78
import com.coder.gateway.models.toIdeWithStatus
89
import com.coder.gateway.models.withWorkspaceProject
910
import com.coder.gateway.sdk.v2.models.Workspace
@@ -82,9 +83,12 @@ import javax.swing.SwingConstants
8283
import javax.swing.event.DocumentEvent
8384

8485
// Just extracting the way we display the IDE info into a helper function.
85-
private fun displayIdeWithStatus(ideWithStatus: IdeWithStatus): String = "${ideWithStatus.product.productCode} ${ideWithStatus.presentableVersion} ${ideWithStatus.buildNumber} | ${ideWithStatus.status.name.lowercase(
86-
Locale.getDefault(),
87-
)}"
86+
private fun displayIdeWithStatus(ideWithStatus: IdeWithStatus): String =
87+
"${ideWithStatus.product.productCode} ${ideWithStatus.presentableVersion} ${ideWithStatus.buildNumber} | ${
88+
ideWithStatus.status.name.lowercase(
89+
Locale.getDefault(),
90+
)
91+
}"
8892

8993
/**
9094
* View for a single workspace. In particular, show available IDEs and a button
@@ -222,12 +226,21 @@ class CoderWorkspaceProjectIDEStepView(
222226
cbIDE.renderer =
223227
if (attempt > 1) {
224228
IDECellRenderer(
225-
CoderGatewayBundle.message("gateway.connector.view.coder.connect-ssh.retry", attempt),
229+
CoderGatewayBundle.message(
230+
"gateway.connector.view.coder.connect-ssh.retry",
231+
attempt
232+
),
226233
)
227234
} else {
228235
IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.connect-ssh"))
229236
}
230-
val executor = createRemoteExecutor(CoderCLIManager(data.client.url).getBackgroundHostName(data.workspace, data.client.me, data.agent))
237+
val executor = createRemoteExecutor(
238+
CoderCLIManager(data.client.url).getBackgroundHostName(
239+
data.workspace,
240+
data.client.me,
241+
data.agent
242+
)
243+
)
231244

232245
if (ComponentValidator.getInstance(tfProject).isEmpty) {
233246
logger.info("Installing remote path validator...")
@@ -238,7 +251,10 @@ class CoderWorkspaceProjectIDEStepView(
238251
cbIDE.renderer =
239252
if (attempt > 1) {
240253
IDECellRenderer(
241-
CoderGatewayBundle.message("gateway.connector.view.coder.retrieve-ides.retry", attempt),
254+
CoderGatewayBundle.message(
255+
"gateway.connector.view.coder.retrieve-ides.retry",
256+
attempt
257+
),
242258
)
243259
} else {
244260
IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.retrieve-ides"))
@@ -247,9 +263,9 @@ class CoderWorkspaceProjectIDEStepView(
247263
},
248264
retryIf = {
249265
it is ConnectionException ||
250-
it is TimeoutException ||
251-
it is SSHException ||
252-
it is DeployException
266+
it is TimeoutException ||
267+
it is SSHException ||
268+
it is DeployException
253269
},
254270
onException = { attempt, nextMs, e ->
255271
logger.error("Failed to retrieve IDEs (attempt $attempt; will retry in $nextMs ms)")
@@ -311,7 +327,10 @@ class CoderWorkspaceProjectIDEStepView(
311327
* Validate the remote path whenever it changes.
312328
*/
313329
private fun installRemotePathValidator(executor: HighLevelHostAccessor) {
314-
val disposable = Disposer.newDisposable(ApplicationManager.getApplication(), CoderWorkspaceProjectIDEStepView::class.java.name)
330+
val disposable = Disposer.newDisposable(
331+
ApplicationManager.getApplication(),
332+
CoderWorkspaceProjectIDEStepView::class.java.name
333+
)
315334
ComponentValidator(disposable).installOn(tfProject)
316335

317336
tfProject.document.addDocumentListener(
@@ -324,7 +343,12 @@ class CoderWorkspaceProjectIDEStepView(
324343
val isPathPresent = validateRemotePath(tfProject.text, executor)
325344
if (isPathPresent.pathOrNull == null) {
326345
ComponentValidator.getInstance(tfProject).ifPresent {
327-
it.updateInfo(ValidationInfo("Can't find directory: ${tfProject.text}", tfProject))
346+
it.updateInfo(
347+
ValidationInfo(
348+
"Can't find directory: ${tfProject.text}",
349+
tfProject
350+
)
351+
)
328352
}
329353
} else {
330354
ComponentValidator.getInstance(tfProject).ifPresent {
@@ -333,7 +357,12 @@ class CoderWorkspaceProjectIDEStepView(
333357
}
334358
} catch (e: Exception) {
335359
ComponentValidator.getInstance(tfProject).ifPresent {
336-
it.updateInfo(ValidationInfo("Can't validate directory: ${tfProject.text}", tfProject))
360+
it.updateInfo(
361+
ValidationInfo(
362+
"Can't validate directory: ${tfProject.text}",
363+
tfProject
364+
)
365+
)
337366
}
338367
}
339368
}
@@ -377,27 +406,34 @@ class CoderWorkspaceProjectIDEStepView(
377406
}
378407

379408
logger.info("Resolved OS and Arch for $name is: $workspaceOS")
380-
val installedIdesJob =
381-
cs.async(Dispatchers.IO) {
382-
executor.getInstalledIDEs().map { it.toIdeWithStatus() }
383-
}
384-
val idesWithStatusJob =
385-
cs.async(Dispatchers.IO) {
386-
IntelliJPlatformProduct.entries
387-
.filter { it.showInGateway }
388-
.flatMap { CachingProductsJsonWrapper.getInstance().getAvailableIdes(it, workspaceOS) }
389-
.map { it.toIdeWithStatus() }
390-
}
409+
val installedIdesJob = cs.async(Dispatchers.IO) {
410+
executor.getInstalledIDEs()
411+
}
412+
val availableToDownloadIdesJob = cs.async(Dispatchers.IO) {
413+
IntelliJPlatformProduct.entries
414+
.filter { it.showInGateway }
415+
.flatMap { CachingProductsJsonWrapper.getInstance().getAvailableIdes(it, workspaceOS) }
416+
}
417+
418+
val installedIdes = installedIdesJob.await()
419+
val availableIdes = availableToDownloadIdesJob.await()
391420

392-
val installedIdes = installedIdesJob.await().sorted()
393-
val idesWithStatus = idesWithStatusJob.await().sorted()
394421
if (installedIdes.isEmpty()) {
395422
logger.info("No IDE is installed in $name")
396423
}
397-
if (idesWithStatus.isEmpty()) {
424+
if (availableIdes.isEmpty()) {
398425
logger.warn("Could not resolve any IDE for $name, probably $workspaceOS is not supported by Gateway")
399426
}
400-
return installedIdes + idesWithStatus
427+
428+
val remainingInstalledIdes = installedIdes.filterOutAvailableReleasedIdes(availableIdes)
429+
if (remainingInstalledIdes.size < installedIdes.size) {
430+
logger.info(
431+
"Skipping the following list of installed IDEs because there is already a released version " +
432+
"available for download: ${(installedIdes - remainingInstalledIdes).joinToString { "${it.product.productCode} ${it.presentableVersion}" }}"
433+
)
434+
}
435+
return remainingInstalledIdes.map { it.toIdeWithStatus() }.sorted() + availableIdes.map { it.toIdeWithStatus() }
436+
.sorted()
401437
}
402438

403439
private fun toDeployedOS(
@@ -455,7 +491,8 @@ class CoderWorkspaceProjectIDEStepView(
455491
override fun getSelectedItem(): IdeWithStatus? = super.getSelectedItem() as IdeWithStatus?
456492
}
457493

458-
private class IDECellRenderer(message: String, cellIcon: Icon = AnimatedIcon.Default.INSTANCE) : ListCellRenderer<IdeWithStatus> {
494+
private class IDECellRenderer(message: String, cellIcon: Icon = AnimatedIcon.Default.INSTANCE) :
495+
ListCellRenderer<IdeWithStatus> {
459496
private val loadingComponentRenderer: ListCellRenderer<IdeWithStatus> =
460497
object : ColoredListCellRenderer<IdeWithStatus>() {
461498
override fun customizeCellRenderer(

0 commit comments

Comments
 (0)