Skip to content

fix: skip installed EAP, RC, NIGHTLY and PREVIEW ides from showing if they are superseded #548

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
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

### Changed

Retrieve workspace directly in link handler when using wildcardSSH feature
- Retrieve workspace directly in link handler when using wildcardSSH feature

### Fixed

- installed EAP, RC, NIGHTLY and PREVIEW IDEs are no longer displayed if there is a higher released version available for download.

## 2.19.0 - 2025-02-21

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pluginUntilBuild=251.*
# that exists, ideally the most recent one, for example
# 233.15325-EAP-CANDIDATE-SNAPSHOT).
platformType=GW
platformVersion=233.15619-EAP-CANDIDATE-SNAPSHOT
platformVersion=241.19416-EAP-CANDIDATE-SNAPSHOT
instrumentationCompiler=243.15521-EAP-CANDIDATE-SNAPSHOT
# Gateway does not have open sources.
platformDownloadSources=true
Expand Down
71 changes: 54 additions & 17 deletions src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import com.jetbrains.gateway.ssh.IdeStatus
import com.jetbrains.gateway.ssh.IdeWithStatus
import com.jetbrains.gateway.ssh.InstalledIdeUIEx
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
import com.jetbrains.gateway.ssh.ReleaseType
import com.jetbrains.gateway.ssh.deploy.ShellArgument
import java.net.URL
import java.nio.file.Path
import kotlin.io.path.name

private val NON_STABLE_RELEASE_TYPES = setOf("EAP", "RC", "NIGHTLY", "PREVIEW")

/**
* Validated parameters for downloading and opening a project using an IDE on a
* workspace.
Expand Down Expand Up @@ -101,7 +104,8 @@ class WorkspaceProjectIDE(
name = name,
hostname = hostname,
projectPath = projectPath,
ideProduct = IntelliJPlatformProduct.fromProductCode(ideProductCode) ?: throw Exception("invalid product code"),
ideProduct = IntelliJPlatformProduct.fromProductCode(ideProductCode)
?: throw Exception("invalid product code"),
ideBuildNumber = ideBuildNumber,
idePathOnHost = idePathOnHost,
downloadSource = downloadSource,
Expand All @@ -126,13 +130,13 @@ fun RecentWorkspaceConnection.toWorkspaceProjectIDE(): WorkspaceProjectIDE {
// connections page, so it could be missing. Try to get it from the
// host name.
name =
if (name.isNullOrBlank() && !hostname.isNullOrBlank()) {
hostname
.removePrefix("coder-jetbrains--")
.removeSuffix("--${hostname.split("--").last()}")
} else {
name
},
if (name.isNullOrBlank() && !hostname.isNullOrBlank()) {
hostname
.removePrefix("coder-jetbrains--")
.removeSuffix("--${hostname.split("--").last()}")
} else {
name
},
hostname = hostname,
projectPath = projectPath,
ideProductCode = ideProductCode,
Expand All @@ -146,17 +150,17 @@ fun RecentWorkspaceConnection.toWorkspaceProjectIDE(): WorkspaceProjectIDE {
// the config directory). For backwards compatibility with existing
// entries, extract the URL from the config directory or host name.
deploymentURL =
if (deploymentURL.isNullOrBlank()) {
if (!dir.isNullOrBlank()) {
"https://${Path.of(dir).parent.name}"
} else if (!hostname.isNullOrBlank()) {
"https://${hostname.split("--").last()}"
if (deploymentURL.isNullOrBlank()) {
if (!dir.isNullOrBlank()) {
"https://${Path.of(dir).parent.name}"
} else if (!hostname.isNullOrBlank()) {
"https://${hostname.split("--").last()}"
} else {
deploymentURL
}
} else {
deploymentURL
}
} else {
deploymentURL
},
},
lastOpened = lastOpened,
)
}
Expand Down Expand Up @@ -195,6 +199,39 @@ fun AvailableIde.toIdeWithStatus(): IdeWithStatus = IdeWithStatus(
remoteDevType = remoteDevType,
)

/**
* Returns a list of installed IDEs that don't have a RELEASED version available for download.
* Typically, installed EAP, RC, nightly or preview builds should be superseded by released versions.
*/
fun List<InstalledIdeUIEx>.filterOutAvailableReleasedIdes(availableIde: List<AvailableIde>): List<InstalledIdeUIEx> {
val availableReleasedByProductCode = availableIde
.filter { it.releaseType == ReleaseType.RELEASE }
.groupBy { it.product.productCode }
val result = mutableListOf<InstalledIdeUIEx>()

this.forEach { installedIde ->
// installed IDEs have the release type embedded in the presentable version
// which is a string in the form: 2024.2.4 NIGHTLY
if (NON_STABLE_RELEASE_TYPES.any { it in installedIde.presentableVersion }) {
// we can show the installed IDe if there isn't a higher released version available for download
if (installedIde.isSNotSupersededBy(availableReleasedByProductCode[installedIde.product.productCode])) {
result.add(installedIde)
}
} else {
result.add(installedIde)
}
}

return result
}

private fun InstalledIdeUIEx.isSNotSupersededBy(availableIdes: List<AvailableIde>?): Boolean {
if (availableIdes.isNullOrEmpty()) {
return true
}
return !availableIdes.any { it.buildNumber >= this.buildNumber }
}

/**
* Convert an installed IDE to an IDE with status.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.coder.gateway.cli.CoderCLIManager
import com.coder.gateway.icons.CoderIcons
import com.coder.gateway.models.WorkspaceProjectIDE
import com.coder.gateway.models.filterOutAvailableReleasedIdes
import com.coder.gateway.models.toIdeWithStatus
import com.coder.gateway.models.withWorkspaceProject
import com.coder.gateway.sdk.v2.models.Workspace
Expand Down Expand Up @@ -82,9 +83,12 @@
import javax.swing.event.DocumentEvent

// Just extracting the way we display the IDE info into a helper function.
private fun displayIdeWithStatus(ideWithStatus: IdeWithStatus): String = "${ideWithStatus.product.productCode} ${ideWithStatus.presentableVersion} ${ideWithStatus.buildNumber} | ${ideWithStatus.status.name.lowercase(
Locale.getDefault(),
)}"
private fun displayIdeWithStatus(ideWithStatus: IdeWithStatus): String =
"${ideWithStatus.product.productCode} ${ideWithStatus.presentableVersion} ${ideWithStatus.buildNumber} | ${
ideWithStatus.status.name.lowercase(
Locale.getDefault(),
)
}"

/**
* View for a single workspace. In particular, show available IDEs and a button
Expand Down Expand Up @@ -222,12 +226,21 @@
cbIDE.renderer =
if (attempt > 1) {
IDECellRenderer(
CoderGatewayBundle.message("gateway.connector.view.coder.connect-ssh.retry", attempt),
CoderGatewayBundle.message(
"gateway.connector.view.coder.connect-ssh.retry",
attempt
),
)
} else {
IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.connect-ssh"))
}
val executor = createRemoteExecutor(CoderCLIManager(data.client.url).getBackgroundHostName(data.workspace, data.client.me, data.agent))
val executor = createRemoteExecutor(
CoderCLIManager(data.client.url).getBackgroundHostName(
data.workspace,
data.client.me,
data.agent
)
)

if (ComponentValidator.getInstance(tfProject).isEmpty) {
logger.info("Installing remote path validator...")
Expand All @@ -238,7 +251,10 @@
cbIDE.renderer =
if (attempt > 1) {
IDECellRenderer(
CoderGatewayBundle.message("gateway.connector.view.coder.retrieve-ides.retry", attempt),
CoderGatewayBundle.message(
"gateway.connector.view.coder.retrieve-ides.retry",
attempt
),
)
} else {
IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.retrieve-ides"))
Expand All @@ -247,9 +263,9 @@
},
retryIf = {
it is ConnectionException ||
it is TimeoutException ||
it is SSHException ||
it is DeployException
it is TimeoutException ||
it is SSHException ||
it is DeployException
},
onException = { attempt, nextMs, e ->
logger.error("Failed to retrieve IDEs (attempt $attempt; will retry in $nextMs ms)")
Expand All @@ -273,7 +289,7 @@
)

// Check the provided setting to see if there's a default IDE to set.
val defaultIde = ides.find { it ->

Check notice on line 292 in src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Redundant lambda arrow

Redundant lambda arrow
// Using contains on the displayable version of the ide means they can be as specific or as vague as they want
// CL 2023.3.6 233.15619.8 -> a specific Clion build
// CL 2023.3.6 -> a specific Clion version
Expand Down Expand Up @@ -311,7 +327,10 @@
* Validate the remote path whenever it changes.
*/
private fun installRemotePathValidator(executor: HighLevelHostAccessor) {
val disposable = Disposer.newDisposable(ApplicationManager.getApplication(), CoderWorkspaceProjectIDEStepView::class.java.name)
val disposable = Disposer.newDisposable(
ApplicationManager.getApplication(),
CoderWorkspaceProjectIDEStepView::class.java.name
)
ComponentValidator(disposable).installOn(tfProject)

tfProject.document.addDocumentListener(
Expand All @@ -324,7 +343,12 @@
val isPathPresent = validateRemotePath(tfProject.text, executor)
if (isPathPresent.pathOrNull == null) {
ComponentValidator.getInstance(tfProject).ifPresent {
it.updateInfo(ValidationInfo("Can't find directory: ${tfProject.text}", tfProject))
it.updateInfo(
ValidationInfo(
"Can't find directory: ${tfProject.text}",
tfProject
)
)
}
} else {
ComponentValidator.getInstance(tfProject).ifPresent {
Expand All @@ -333,7 +357,12 @@
}
} catch (e: Exception) {
ComponentValidator.getInstance(tfProject).ifPresent {
it.updateInfo(ValidationInfo("Can't validate directory: ${tfProject.text}", tfProject))
it.updateInfo(
ValidationInfo(
"Can't validate directory: ${tfProject.text}",
tfProject
)
)
}
}
}
Expand Down Expand Up @@ -377,27 +406,34 @@
}

logger.info("Resolved OS and Arch for $name is: $workspaceOS")
val installedIdesJob =
cs.async(Dispatchers.IO) {
executor.getInstalledIDEs().map { it.toIdeWithStatus() }
}
val idesWithStatusJob =
cs.async(Dispatchers.IO) {
IntelliJPlatformProduct.entries
.filter { it.showInGateway }
.flatMap { CachingProductsJsonWrapper.getInstance().getAvailableIdes(it, workspaceOS) }
.map { it.toIdeWithStatus() }
}
val installedIdesJob = cs.async(Dispatchers.IO) {
executor.getInstalledIDEs()
}
val availableToDownloadIdesJob = cs.async(Dispatchers.IO) {
IntelliJPlatformProduct.entries
.filter { it.showInGateway }
.flatMap { CachingProductsJsonWrapper.getInstance().getAvailableIdes(it, workspaceOS) }
}

val installedIdes = installedIdesJob.await()
val availableIdes = availableToDownloadIdesJob.await()

val installedIdes = installedIdesJob.await().sorted()
val idesWithStatus = idesWithStatusJob.await().sorted()
if (installedIdes.isEmpty()) {
logger.info("No IDE is installed in $name")
}
if (idesWithStatus.isEmpty()) {
if (availableIdes.isEmpty()) {
logger.warn("Could not resolve any IDE for $name, probably $workspaceOS is not supported by Gateway")
}
return installedIdes + idesWithStatus

val remainingInstalledIdes = installedIdes.filterOutAvailableReleasedIdes(availableIdes)
if (remainingInstalledIdes.size < installedIdes.size) {
logger.info(
"Skipping the following list of installed IDEs because there is already a released version " +
"available for download: ${(installedIdes - remainingInstalledIdes).joinToString { "${it.product.productCode} ${it.presentableVersion}" }}"

Check notice on line 432 in src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Argument could be converted to 'Set' to improve performance

The argument can be converted to 'Set' to improve performance
)
}
return remainingInstalledIdes.map { it.toIdeWithStatus() }.sorted() + availableIdes.map { it.toIdeWithStatus() }
.sorted()
}

private fun toDeployedOS(
Expand Down Expand Up @@ -455,7 +491,8 @@
override fun getSelectedItem(): IdeWithStatus? = super.getSelectedItem() as IdeWithStatus?
}

private class IDECellRenderer(message: String, cellIcon: Icon = AnimatedIcon.Default.INSTANCE) : ListCellRenderer<IdeWithStatus> {
private class IDECellRenderer(message: String, cellIcon: Icon = AnimatedIcon.Default.INSTANCE) :
ListCellRenderer<IdeWithStatus> {
private val loadingComponentRenderer: ListCellRenderer<IdeWithStatus> =
object : ColoredListCellRenderer<IdeWithStatus>() {
override fun customizeCellRenderer(
Expand Down
Loading
Loading