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 a0d5c160..b1d1e59b 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -77,8 +77,10 @@ import java.net.ConnectException import java.net.SocketTimeoutException import java.net.URL import java.net.UnknownHostException +import javax.net.ssl.SSLHandshakeException import javax.swing.Icon import javax.swing.JCheckBox +import javax.swing.JLabel import javax.swing.JTable import javax.swing.JTextField import javax.swing.ListSelectionModel @@ -101,6 +103,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod private val appPropertiesService: PropertiesComponent = service() private var tfUrl: JTextField? = null + private var tfUrlComment: JLabel? = null private var cbExistingToken: JCheckBox? = null private val notificationBanner = NotificationBanner() @@ -116,7 +119,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod minWidth = JBUI.scale(52) } rowHeight = 48 - setEmptyState("Disconnected") + setEmptyState(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text.disconnected")) setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.addListSelectionListener { setNextButtonEnabled(selectedObject?.agentStatus?.ready() == true && selectedObject?.agentOS == OS.LINUX) @@ -186,6 +189,16 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() } }.layout(RowLayout.PARENT_GRID) + row { + cell() // Empty cells for alignment. + tfUrlComment = cell( + ComponentPanelBuilder.createCommentComponent( + CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text.comment", + CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text")), + false, -1, true + ) + ).resizableColumn().align(AlignX.FILL).component + }.layout(RowLayout.PARENT_GRID) row { cell() // Empty cell for alignment. cbExistingToken = checkBox(CoderGatewayBundle.message("gateway.connector.view.login.existing-token.label")) @@ -410,7 +423,9 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod // Clear out old deployment details. cliManager = null poller?.cancel() - tableOfWorkspaces.setEmptyState("Connecting to $deploymentURL...") + tfUrlComment?.foreground = UIUtil.getContextHelpForeground() + tfUrlComment?.text = CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text.connecting", deploymentURL.host) + tableOfWorkspaces.setEmptyState(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text.connecting", deploymentURL.host)) tableOfWorkspaces.listTableModel.items = emptyList() // Authenticate and load in a background process with progress. @@ -444,44 +459,55 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod triggerWorkspacePolling(false) cliManager = cli - tableOfWorkspaces.setEmptyState("Connected to $deploymentURL") + 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 errorSummary = when (e) { - is java.nio.file.AccessDeniedException -> "Access denied to ${e.file}" - is UnknownHostException -> "Unknown host ${e.message}" - is InvalidExitValueException -> "CLI exited unexpectedly with ${e.exitValue}" - else -> e.message ?: "No reason was provided" - } - var msg = CoderGatewayBundle.message( - "gateway.connector.view.workspaces.connect.failed", - deploymentURL, - errorSummary, - ) - when (e) { + 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 -> { - msg = CoderGatewayBundle.message( + CoderGatewayBundle.message( "gateway.connector.view.workspaces.connect.unauthorized", deploymentURL, ) - cs.launch { onAuthFailure?.invoke() } } - is SocketTimeoutException -> { - msg = CoderGatewayBundle.message( + CoderGatewayBundle.message( "gateway.connector.view.workspaces.connect.timeout", deploymentURL, ) } - is ResponseException, is ConnectException -> { - msg = CoderGatewayBundle.message( + CoderGatewayBundle.message( "gateway.connector.view.workspaces.connect.download-failed", - errorSummary, + reason, + ) + } + is SSLHandshakeException -> { + CoderGatewayBundle.message( + "gateway.connector.view.workspaces.connect.ssl-error", + deploymentURL.host, + reason, ) } + else -> reason } - tableOfWorkspaces.setEmptyState(msg) + // 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 443af9c1..b2ac2026 100644 --- a/src/main/resources/messages/CoderGatewayBundle.properties +++ b/src/main/resources/messages/CoderGatewayBundle.properties @@ -10,6 +10,10 @@ gateway.connector.view.login.token.label=Session Token: gateway.connector.view.coder.workspaces.header.text=Coder Workspaces gateway.connector.view.coder.workspaces.comment=Self-hosted developer workspaces in the cloud or on-premises. Coder empowers developers with secure, consistent, and fast developer workspaces. gateway.connector.view.coder.workspaces.connect.text=Connect +gateway.connector.view.coder.workspaces.connect.text.comment=Please enter your deployment URL and press "{0}". +gateway.connector.view.coder.workspaces.connect.text.disconnected=Disconnected +gateway.connector.view.coder.workspaces.connect.text.connected=Connected to {0} +gateway.connector.view.coder.workspaces.connect.text.connecting=Connecting to {0}... gateway.connector.view.coder.workspaces.cli.downloader.dialog.title=Authenticate and setup Coder gateway.connector.view.coder.workspaces.next.text=Select IDE and project gateway.connector.view.coder.workspaces.dashboard.text=Open dashboard @@ -19,12 +23,19 @@ gateway.connector.view.coder.workspaces.stop.text=Stop workspace gateway.connector.view.coder.workspaces.update.text=Update workspace template gateway.connector.view.coder.workspaces.create.text=Create workspace gateway.connector.view.coder.workspaces.unsupported.os.info=Gateway supports only Linux machines. Support for macOS and Windows is planned. -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.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.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}. +gateway.connector.view.workspaces.connect.unexpected-exit=CLI exited unexpectedly with {0}. gateway.connector.view.workspaces.connect.unauthorized=Token was rejected by {0}; has your token expired? gateway.connector.view.workspaces.connect.timeout=Unable to connect to {0}; is it up? gateway.connector.view.workspaces.connect.download-failed=Failed to download Coder CLI: {0} -gateway.connector.view.workspaces.connect.failed=Failed to connect to {0}: {1} +gateway.connector.view.workspaces.connect.ssl-error=Connection to {0} failed: {1}. See the \ + documentation for TLS certificates \ + for information on how to make your system trust certificates coming from your deployment. gateway.connector.view.workspaces.token.comment=The last used token is shown above. gateway.connector.view.workspaces.token.rejected=This token was rejected. gateway.connector.view.workspaces.token.injected=This token was pulled from your CLI config.