diff --git a/CHANGELOG.md b/CHANGELOG.md index e1fe9ef6..6ba36dec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Added - Add a setting for a command to run to get headers that will be set on all requests to the Coder deployment. +- Support for Gateway 2023.3. ## 2.6.0 - 2023-09-06 diff --git a/gradle.properties b/gradle.properties index 855e7178..92b80e02 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ pluginGroup=com.coder.gateway pluginName=coder-gateway # SemVer format -> https://semver.org -pluginVersion=2.7.0 +pluginVersion=2.8.0 # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # for insight into build numbers and IntelliJ Platform versions. pluginSinceBuild=223.7571.70 diff --git a/src/main/kotlin/com/coder/gateway/CoderRemoteConnectionHandle.kt b/src/main/kotlin/com/coder/gateway/CoderRemoteConnectionHandle.kt index 3ebee47d..c9a19bb4 100644 --- a/src/main/kotlin/com/coder/gateway/CoderRemoteConnectionHandle.kt +++ b/src/main/kotlin/com/coder/gateway/CoderRemoteConnectionHandle.kt @@ -31,6 +31,7 @@ import com.jetbrains.gateway.ssh.SshDeployFlowUtil import com.jetbrains.gateway.ssh.SshMultistagePanelContext import com.jetbrains.gateway.ssh.deploy.DeployException import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import net.schmizz.sshj.common.SSHException import net.schmizz.sshj.connection.ConnectionException @@ -49,7 +50,7 @@ class CoderRemoteConnectionHandle { suspend fun connect(getParameters: (indicator: ProgressIndicator) -> Map) { val clientLifetime = LifetimeDefinition() - clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title"), canBeCancelled = true, isIndeterminate = true, project = null) { + clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title")) { try { val parameters = getParameters(indicator) logger.debug("Creating connection handle", parameters) @@ -78,7 +79,7 @@ class CoderRemoteConnectionHandle { indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting.failed.retry", humanizeDuration(remainingMs)) }, ) - launch { + GlobalScope.launch { logger.info("Deploying and starting IDE with $context") // At this point JetBrains takes over with their own UI. @Suppress("UnstableApiUsage") SshDeployFlowUtil.fullDeployCycle( @@ -178,13 +179,16 @@ class CoderRemoteConnectionHandle { } /** - * Open a dialog for providing the token. Show any existing token so the - * user can validate it if a previous connection failed. If we are not - * retrying and the user has not checked the existing token box then open a - * browser to the auth page. If the user has checked the existing token box - * then populate the dialog with the token on disk (this will overwrite any + * Open a dialog for providing the token. Show any existing token so + * the user can validate it if a previous connection failed. + * + * If we are not retrying and the user has not checked the existing + * token box then also open a browser to the auth page. + * + * If the user has checked the existing token box then return the token + * on disk immediately and skip the dialog (this will overwrite any * other existing token) unless this is a retry to avoid clobbering the - * token that just failed. Return the token submitted by the user. + * token that just failed. */ @JvmStatic fun askToken( diff --git a/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt b/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt index dcf54969..1c7d0d71 100644 --- a/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt +++ b/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt @@ -29,12 +29,7 @@ import com.intellij.ui.DocumentAdapter import com.intellij.ui.SearchTextField import com.intellij.ui.components.ActionLink import com.intellij.ui.components.JBScrollPane -import com.intellij.ui.dsl.builder.AlignX -import com.intellij.ui.dsl.builder.AlignY -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.* import com.intellij.util.io.readText import com.intellij.util.ui.JBFont import com.intellij.util.ui.JBUI diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt index d7c97447..1d7da3ff 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -18,6 +18,7 @@ import com.coder.gateway.sdk.TemplateIconDownloader import com.coder.gateway.sdk.ex.AuthenticationResponseException import com.coder.gateway.sdk.ex.TemplateResponseException import com.coder.gateway.sdk.ex.WorkspaceResponseException +import com.coder.gateway.sdk.isCancellation import com.coder.gateway.sdk.toURL import com.coder.gateway.sdk.v2.models.WorkspaceStatus import com.coder.gateway.sdk.v2.models.toAgentModels @@ -423,12 +424,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod tableOfWorkspaces.listTableModel.items = emptyList() // Authenticate and load in a background process with progress. - // TODO: Make this cancelable. - return LifetimeDefinition().launchUnderBackgroundProgress( - CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.cli.downloader.dialog.title"), - canBeCancelled = false, - isIndeterminate = true - ) { + return LifetimeDefinition().launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.cli.downloader.dialog.title")) { try { this.indicator.text = "Authenticating client..." authenticate(deploymentURL, token.first) @@ -456,51 +452,61 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod tableOfWorkspaces.setEmptyState(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text.connected", deploymentURL.host)) tfUrlComment?.text = CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text.connected", deploymentURL.host) } catch (e: Exception) { - val reason = e.message ?: CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.no-reason") - val msg = when (e) { - is java.nio.file.AccessDeniedException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.access-denied", e.file) - is UnknownHostException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.unknown-host", e.message ?: deploymentURL.host) - is InvalidExitValueException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.unexpected-exit", e.exitValue) - is AuthenticationResponseException -> { - CoderGatewayBundle.message( - "gateway.connector.view.workspaces.connect.unauthorized", - deploymentURL, - ) - } - is SocketTimeoutException -> { - CoderGatewayBundle.message( - "gateway.connector.view.workspaces.connect.timeout", - deploymentURL, - ) - } - is ResponseException, is ConnectException -> { - CoderGatewayBundle.message( - "gateway.connector.view.workspaces.connect.download-failed", - reason, - ) + if (isCancellation(e)) { + tfUrlComment?.text = CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text.comment", + CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text")) + tableOfWorkspaces.setEmptyState(CoderGatewayBundle.message( + "gateway.connector.view.workspaces.connect.canceled", + deploymentURL.host, + )) + logger.info("Connection canceled due to ${e.javaClass.simpleName}") + } else { + val reason = e.message ?: CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.no-reason") + val msg = when (e) { + is java.nio.file.AccessDeniedException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.access-denied", e.file) + is UnknownHostException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.unknown-host", e.message ?: deploymentURL.host) + is InvalidExitValueException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.unexpected-exit", e.exitValue) + is AuthenticationResponseException -> { + CoderGatewayBundle.message( + "gateway.connector.view.workspaces.connect.unauthorized", + deploymentURL, + ) + } + is SocketTimeoutException -> { + CoderGatewayBundle.message( + "gateway.connector.view.workspaces.connect.timeout", + deploymentURL, + ) + } + is ResponseException, is ConnectException -> { + CoderGatewayBundle.message( + "gateway.connector.view.workspaces.connect.download-failed", + reason, + ) + } + is SSLHandshakeException -> { + CoderGatewayBundle.message( + "gateway.connector.view.workspaces.connect.ssl-error", + deploymentURL.host, + reason, + ) + } + else -> reason } - is SSLHandshakeException -> { - CoderGatewayBundle.message( - "gateway.connector.view.workspaces.connect.ssl-error", - deploymentURL.host, - reason, - ) + // It would be nice to place messages directly into the table + // but it does not support wrapping or markup so place it in the + // comment field of the URL input instead. + tfUrlComment?.foreground = UIUtil.getErrorForeground() + tfUrlComment?.text = msg + tableOfWorkspaces.setEmptyState(CoderGatewayBundle.message( + "gateway.connector.view.workspaces.connect.failed", + deploymentURL.host, + )) + logger.error(msg, e) + + if (e is AuthenticationResponseException) { + cs.launch { onAuthFailure?.invoke() } } - else -> reason - } - // It would be nice to place messages directly into the table - // but it does not support wrapping or markup so place it in the - // comment field of the URL input instead. - tfUrlComment?.foreground = UIUtil.getErrorForeground() - tfUrlComment?.text = msg - tableOfWorkspaces.setEmptyState(CoderGatewayBundle.message( - "gateway.connector.view.workspaces.connect.failed", - deploymentURL.host, - )) - logger.error(msg, e) - - if (e is AuthenticationResponseException) { - cs.launch { onAuthFailure?.invoke() } } } } diff --git a/src/main/resources/messages/CoderGatewayBundle.properties b/src/main/resources/messages/CoderGatewayBundle.properties index 9d5af8de..6e93c557 100644 --- a/src/main/resources/messages/CoderGatewayBundle.properties +++ b/src/main/resources/messages/CoderGatewayBundle.properties @@ -4,7 +4,7 @@ gateway.connector.action.text=Connect to Coder gateway.connector.view.login.documentation.action=Learn more about Coder gateway.connector.view.login.url.label=URL: gateway.connector.view.login.existing-token.label=Use existing token -gateway.connector.view.login.existing-token.tooltip=Checking "{0}" will prevent the browser from being launched for generating a new token after pressing "{1}". Additionally, if a token is already configured for this URL via the CLI it will appear as the default and can be used as-is or replaced. +gateway.connector.view.login.existing-token.tooltip=Checking "{0}" will prevent the browser from being launched for generating a new token after pressing "{1}". Additionally, if a token is already configured for this URL via the CLI it will automatically be used. gateway.connector.view.login.token.dialog=Paste your token here: gateway.connector.view.login.token.label=Session Token: gateway.connector.view.coder.workspaces.header.text=Coder Workspaces @@ -26,6 +26,7 @@ gateway.connector.view.coder.workspaces.unsupported.os.info=Gateway supports onl gateway.connector.view.coder.workspaces.invalid.coder.version=Could not parse Coder version {0}. Coder Gateway plugin might not be compatible with this version. Connect to a Coder workspace manually gateway.connector.view.coder.workspaces.unsupported.coder.version=Coder version {0} might not be compatible with this plugin version. Connect to a Coder workspace manually gateway.connector.view.workspaces.connect.failed=Connection to {0} failed. See above for details. +gateway.connector.view.workspaces.connect.canceled=Connection to {0} canceled. gateway.connector.view.workspaces.connect.no-reason=No reason was provided. gateway.connector.view.workspaces.connect.access-denied=Access denied to {0}. gateway.connector.view.workspaces.connect.unknown-host=Unknown host {0}.