diff --git a/gradle.properties b/gradle.properties index 9f95f099..0098d697 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ pluginUntilBuild=241.* # verifier should be used after bumping versions to ensure compatibility in the # range. platformType=GW -platformVersion=233.6745-EAP-CANDIDATE-SNAPSHOT +platformVersion=233.14808-EAP-CANDIDATE-SNAPSHOT instrumentationCompiler=241.10840-EAP-CANDIDATE-SNAPSHOT platformDownloadSources=true verifyVersions=2023.3,2024.1 diff --git a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt index 6e3eea12..36500848 100644 --- a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt +++ b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt @@ -6,7 +6,6 @@ import com.coder.gateway.cli.CoderCLIManager import com.coder.gateway.cli.ensureCLI import com.coder.gateway.models.TokenSource import com.coder.gateway.models.WorkspaceAndAgentStatus -import com.coder.gateway.sdk.BaseCoderRestClient import com.coder.gateway.sdk.CoderRestClient import com.coder.gateway.sdk.ex.AuthenticationResponseException import com.coder.gateway.sdk.v2.models.Workspace @@ -15,12 +14,17 @@ import com.coder.gateway.sdk.v2.models.WorkspaceStatus import com.coder.gateway.services.CoderSettingsService import com.coder.gateway.util.toURL import com.coder.gateway.util.withPath +import com.coder.gateway.views.steps.CoderWorkspaceStepView +import com.coder.gateway.views.steps.CoderWorkspacesStepSelection +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.ui.DialogWrapper import com.jetbrains.gateway.api.ConnectionRequestor import com.jetbrains.gateway.api.GatewayConnectionHandle import com.jetbrains.gateway.api.GatewayConnectionProvider import java.net.URL +import javax.swing.JComponent // In addition to `type`, these are the keys that we support in our Gateway // links. @@ -34,6 +38,18 @@ private const val IDE_DOWNLOAD_LINK = "ide_download_link" private const val IDE_PRODUCT_CODE = "ide_product_code" private const val IDE_BUILD_NUMBER = "ide_build_number" private const val IDE_PATH_ON_HOST = "ide_path_on_host" +private const val PROJECT_PATH = "project_path" + +class SelectWorkspaceIDEDialog(private val comp: JComponent) : DialogWrapper(true) { + init { + init() + title = "Select workspace IDE" + } + + override fun createCenterPanel(): JComponent? { + return comp + } +} // CoderGatewayConnectionProvider handles connecting via a Gateway link such as // jetbrains-gateway://connect#type=coder. @@ -93,31 +109,38 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { cli.login(client.token) indicator.text = "Configuring Coder CLI..." - cli.configSsh(client.agentNames(workspaces)) + cli.configSsh(client.agentNames(workspaces).toSet()) - // TODO: Ask for these if missing. Maybe we can reuse the second - // step of the wizard? Could also be nice if we automatically used - // the last IDE. - if (parameters[IDE_PRODUCT_CODE].isNullOrBlank()) { - throw IllegalArgumentException("Query parameter \"$IDE_PRODUCT_CODE\" is missing") - } - if (parameters[IDE_BUILD_NUMBER].isNullOrBlank()) { - throw IllegalArgumentException("Query parameter \"$IDE_BUILD_NUMBER\" is missing") - } - if (parameters[IDE_PATH_ON_HOST].isNullOrBlank() && parameters[IDE_DOWNLOAD_LINK].isNullOrBlank()) { - throw IllegalArgumentException("One of \"$IDE_PATH_ON_HOST\" or \"$IDE_DOWNLOAD_LINK\" is required") - } + + val openDialog = parameters[IDE_PRODUCT_CODE].isNullOrBlank() || + parameters[IDE_BUILD_NUMBER].isNullOrBlank() || + (parameters[IDE_PATH_ON_HOST].isNullOrBlank() && parameters[IDE_DOWNLOAD_LINK].isNullOrBlank()) || + parameters[FOLDER].isNullOrBlank() + + val params = if (openDialog) { + val view = CoderWorkspaceStepView{} + ApplicationManager.getApplication().invokeAndWait { + view.init( + CoderWorkspacesStepSelection(agent, workspace, cli, client, workspaces) + ) + val dialog = SelectWorkspaceIDEDialog(view.component) + dialog.show() + } + val p = parameters.toMutableMap() + + + listOf(IDE_PRODUCT_CODE, IDE_BUILD_NUMBER, PROJECT_PATH, IDE_PATH_ON_HOST, IDE_DOWNLOAD_LINK).forEach { prop -> + view.data()[prop]?.let { value -> p[prop] = value } + } + p + } else + parameters.withProjectPath(parameters[FOLDER]!!) // Check that both the domain and the redirected domain are // allowlisted. If not, check with the user whether to proceed. verifyDownloadLink(parameters) - // TODO: Ask for the project path if missing and validate the path. - val folder = parameters[FOLDER] ?: throw IllegalArgumentException("Query parameter \"$FOLDER\" is missing") - - parameters - .withWorkspaceHostname(CoderCLIManager.getHostName(deploymentURL.toURL(), agent.name)) - .withProjectPath(folder) + params.withWorkspaceHostname(CoderCLIManager.getHostName(deploymentURL.toURL(), "${workspace.name}.${agent.name}")) .withWebTerminalLink(client.url.withPath("/@$username/$workspace.name/terminal").toString()) .withConfigDirectory(cli.coderConfigPath.toString()) .withName(workspaceName) @@ -129,7 +152,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { * Return an authenticated Coder CLI and the user's name, asking for the * token as long as it continues to result in an authentication failure. */ - private fun authenticate(deploymentURL: URL, queryToken: String?, lastToken: Pair? = null): Pair { + private fun authenticate(deploymentURL: URL, queryToken: String?, lastToken: Pair? = null): Pair { // Use the token from the query, unless we already tried that. val isRetry = lastToken != null val token = if (!queryToken.isNullOrBlank() && !isRetry) diff --git a/src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt b/src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt index 89b409aa..8f6c42e3 100644 --- a/src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt +++ b/src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt @@ -209,7 +209,7 @@ class CoderCLIManager( * * This can take supported features for testing purposes only. */ - fun configSsh(workspaceNames: List, feats: Features = features) { + fun configSsh(workspaceNames: Set, feats: Features = features) { writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaceNames, feats)) } @@ -232,7 +232,7 @@ class CoderCLIManager( * If features are not provided, calculate them based on the binary * version. */ - private fun modifySSHConfig(contents: String?, workspaceNames: List, feats: Features): String? { + private fun modifySSHConfig(contents: String?, workspaceNames: Set, feats: Features): String? { val host = deploymentURL.safeHost() val startBlock = "# --- START CODER JETBRAINS $host" val endBlock = "# --- END CODER JETBRAINS $host" diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceStepView.kt index a6911124..0abcc267 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceStepView.kt @@ -89,7 +89,7 @@ class CoderWorkspaceStepView( // Called with a boolean indicating whether IDE selection is complete. private val nextButtonEnabled: (Boolean) -> Unit, ) : CoderWizardStep { - private val cs = CoroutineScope(Dispatchers.Main) + private val cs = CoroutineScope(Dispatchers.IO) private var ideComboBoxModel = DefaultComboBoxModel() private var state: CoderWorkspacesStepSelection? = null @@ -189,7 +189,7 @@ class CoderWorkspaceStepView( logger.info("Configuring Coder CLI...") cbIDE.renderer = IDECellRenderer("Configuring Coder CLI...") withContext(Dispatchers.IO) { - data.cliManager.configSsh(data.client.agentNames(data.workspaces)) + data.cliManager.configSsh(data.client.agentNames(data.workspaces).toSet()) } val ides = suspendingRetryWithExponentialBackOff( @@ -228,7 +228,7 @@ class CoderWorkspaceStepView( cbIDE.renderer = IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.retrieve-ides.failed.retry", humanizeDuration(remainingMs))) }, ) - withContext(Dispatchers.Main) { + withContext(Dispatchers.IO) { ideComboBoxModel.addAll(ides) cbIDE.selectedIndex = 0 } diff --git a/src/test/kotlin/com/coder/gateway/cli/CoderCLIManagerTest.kt b/src/test/kotlin/com/coder/gateway/cli/CoderCLIManagerTest.kt index 4852d1a2..9cac256e 100644 --- a/src/test/kotlin/com/coder/gateway/cli/CoderCLIManagerTest.kt +++ b/src/test/kotlin/com/coder/gateway/cli/CoderCLIManagerTest.kt @@ -295,12 +295,12 @@ internal class CoderCLIManagerTest { .replace("/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64", escape(ccm.localBinaryPath.toString())) // Add workspaces. - ccm.configSsh(it.workspaces, it.features ?: Features()) + ccm.configSsh(it.workspaces.toSet(), it.features ?: Features()) assertEquals(expectedConf, settings.sshConfigPath.toFile().readText()) // Remove configuration. - ccm.configSsh(emptyList(), it.features ?: Features()) + ccm.configSsh(emptySet(), it.features ?: Features()) // Remove is the configuration we expect after removing. assertEquals( @@ -330,7 +330,7 @@ internal class CoderCLIManagerTest { assertFailsWith( exceptionClass = SSHConfigFormatException::class, - block = { ccm.configSsh(emptyList()) }) + block = { ccm.configSsh(emptySet()) }) } } @@ -346,7 +346,7 @@ internal class CoderCLIManagerTest { assertFailsWith( exceptionClass = Exception::class, - block = { ccm.configSsh(listOf("foo", "bar")) }) + block = { ccm.configSsh(setOf("foo", "bar")) }) } }