diff --git a/CHANGELOG.md b/CHANGELOG.md index 638706ce..c53cb9f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,16 @@ ## Unreleased +### Fixed +- the help text under the IDE dropdown now takes into account whether the IDE is + already installed +- various minor alignment issues + ## 2.2.1 - 2023-03-23 ### Fixed - reading an existing config would sometimes use the wrong directory on Linux -- two separate SSH sessions would spawn when connecting to a workspace through +- two separate SSH sessions would spawn when connecting to a workspace through the main flow ## 2.2.0 - 2023-03-08 diff --git a/src/main/kotlin/com/coder/gateway/views/CoderGatewayConnectorWizardView.kt b/src/main/kotlin/com/coder/gateway/views/CoderGatewayConnectorWizardView.kt index 49673158..6e7c14f2 100644 --- a/src/main/kotlin/com/coder/gateway/views/CoderGatewayConnectorWizardView.kt +++ b/src/main/kotlin/com/coder/gateway/views/CoderGatewayConnectorWizardView.kt @@ -9,6 +9,7 @@ import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.RightGap import com.intellij.ui.dsl.builder.panel +import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel import com.jetbrains.gateway.api.GatewayUI import java.awt.Component @@ -30,11 +31,9 @@ class CoderGatewayConnectorWizardView : BorderLayoutPanel(), Disposable { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() registerStep(CoderWorkspacesStepView { nextButton.isEnabled = it }) - registerStep(CoderLocateRemoteProjectStepView { - nextButton.isVisible = false - }) + registerStep(CoderLocateRemoteProjectStepView { nextButton.isEnabled = it }) - addToBottom(createBackComponent()) + addToBottom(createButtons()) steps[0].apply { onInit(model) @@ -42,6 +41,7 @@ class CoderGatewayConnectorWizardView : BorderLayoutPanel(), Disposable { updateUI() nextButton.text = nextActionText previousButton.text = previousActionText + nextButton.isEnabled = false } } @@ -73,6 +73,7 @@ class CoderGatewayConnectorWizardView : BorderLayoutPanel(), Disposable { private fun showNavigationButtons() { nextButton.isVisible = true previousButton.isVisible = true + nextButton.isEnabled = false } private fun next() { @@ -101,25 +102,23 @@ class CoderGatewayConnectorWizardView : BorderLayoutPanel(), Disposable { } } - private fun createBackComponent(): Component { + private fun createButtons(): Component { previousButton = JButton() nextButton = JButton() return panel { separator(background = WelcomeScreenUIManager.getSeparatorColor()) - indent { - row { - - label("").resizableColumn().align(AlignX.FILL).gap(RightGap.SMALL) - previousButton = button("") { previous() }.align(AlignX.RIGHT).gap(RightGap.SMALL).applyToComponent { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() }.component - nextButton = button("") { next() }.align(AlignX.RIGHT).gap(RightGap.SMALL).applyToComponent { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() }.component - cell() - } - }.apply { - background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() + row { + label("").resizableColumn().align(AlignX.FILL).gap(RightGap.SMALL) + previousButton = button("") { previous() } + .align(AlignX.RIGHT).gap(RightGap.SMALL) + .applyToComponent { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() }.component + nextButton = button("") { next() } + .align(AlignX.RIGHT) + .applyToComponent { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() }.component } - }.apply { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() + border = JBUI.Borders.empty(0, 16, 0, 16) } } @@ -127,4 +126,3 @@ class CoderGatewayConnectorWizardView : BorderLayoutPanel(), Disposable { steps.clear() } } - diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index 558e6dd5..2b8d44de 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -20,6 +20,7 @@ import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.ComponentValidator import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.ui.panel.ComponentPanelBuilder import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager import com.intellij.remote.AuthType @@ -29,12 +30,9 @@ import com.intellij.ui.AnimatedIcon import com.intellij.ui.ColoredListCellRenderer import com.intellij.ui.DocumentAdapter import com.intellij.ui.components.JBTextField -import com.intellij.ui.dsl.builder.AlignX -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.RowLayout -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.* import com.intellij.util.ui.JBFont +import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import com.intellij.util.ui.update.MergingUpdateQueue import com.intellij.util.ui.update.Update @@ -73,7 +71,7 @@ import javax.swing.ListCellRenderer import javax.swing.SwingConstants import javax.swing.event.DocumentEvent -class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit) : CoderWorkspacesWizardStep, Disposable { +class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolean) -> Unit) : CoderWorkspacesWizardStep, Disposable { private val cs = CoroutineScope(Dispatchers.Main) private val coderClient: CoderRestClientService = ApplicationManager.getApplication().getService(CoderRestClientService::class.java) @@ -82,44 +80,71 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit private lateinit var titleLabel: JLabel private lateinit var wizard: CoderWorkspacesWizardModel private lateinit var cbIDE: IDEComboBox + private lateinit var cbIDEComment: JLabel private var tfProject = JBTextField() private lateinit var terminalLink: LazyBrowserLink private lateinit var ideResolvingJob: Job private val pathValidationJobs = MergingUpdateQueue("remote-path-validation", 1000, true, tfProject) override val component = panel { - indent { - row { - titleLabel = label("").applyToComponent { - font = JBFont.h3().asBold() - icon = CoderIcons.LOGO_16 - }.component - }.bottomGap(BottomGap.MEDIUM) + row { + titleLabel = label("").applyToComponent { + font = JBFont.h3().asBold() + icon = CoderIcons.LOGO_16 + }.component + }.topGap(TopGap.SMALL) + row { + label("IDE:") + cbIDE = cell(IDEComboBox(ideComboBoxModel).apply { + renderer = IDECellRenderer() + addActionListener { + setNextButtonEnabled(this.selectedItem != null) + ApplicationManager.getApplication().invokeLater { + logger.info("Selected IDE: ${this.selectedItem}") + when (this.selectedItem?.status) { + IdeStatus.ALREADY_INSTALLED -> + cbIDEComment.text = + CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.installed.comment") - row { - label("IDE:") - cbIDE = cell(IDEComboBox(ideComboBoxModel).apply { - renderer = IDECellRenderer() - }).resizableColumn().align(AlignX.FILL).comment("The IDE will be downloaded from jetbrains.com").component - cell() - }.topGap(TopGap.NONE).layout(RowLayout.PARENT_GRID) + IdeStatus.DOWNLOAD -> + cbIDEComment.text = + CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.download.comment") - row { - label("Project directory:") - cell(tfProject).resizableColumn().align(AlignX.FILL).component - cell() - }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).layout(RowLayout.PARENT_GRID) - row { - cell() - terminalLink = cell( - LazyBrowserLink( - CoderIcons.OPEN_TERMINAL, - "Open Terminal" - ) - ).component - }.topGap(TopGap.NONE).layout(RowLayout.PARENT_GRID) - } - }.apply { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() } + else -> + cbIDEComment.text = + CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.none.comment") + } + } + } + }).resizableColumn().align(AlignX.FILL).component + }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).layout(RowLayout.PARENT_GRID) + row { + cell() // Empty cell for alignment. + cbIDEComment = cell( + ComponentPanelBuilder.createCommentComponent( + CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.none.comment"), + false, -1, true + ) + ).component + }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).layout(RowLayout.PARENT_GRID) + row { + label("Project directory:") + cell(tfProject).resizableColumn().align(AlignX.FILL).component + }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).layout(RowLayout.PARENT_GRID) + row { + cell() // Empty cell for alignment. + terminalLink = cell( + LazyBrowserLink( + CoderIcons.OPEN_TERMINAL, + "Open Terminal" + ) + ).component + }.topGap(TopGap.NONE).layout(RowLayout.PARENT_GRID) + gap(RightGap.SMALL) + }.apply { + background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() + border = JBUI.Borders.empty(0, 16, 0, 16) + } override val previousActionText = IdeBundle.message("button.back") override val nextActionText = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.next.text") @@ -153,7 +178,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit is SshException -> { logger.error("Can't connect to workspace ${selectedWorkspace.name}. Reason: $e") withContext(Dispatchers.Main) { - disableNextAction() + setNextButtonEnabled(false) cbIDE.renderer = object : ColoredListCellRenderer() { override fun customizeCellRenderer(list: JList, value: IdeWithStatus?, index: Int, isSelected: Boolean, cellHasFocus: Boolean) { background = UIUtil.getListBackground(isSelected, cellHasFocus) @@ -167,7 +192,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit else -> { logger.error("Could not resolve any IDE for workspace ${selectedWorkspace.name}. Reason: $e") withContext(Dispatchers.Main) { - disableNextAction() + setNextButtonEnabled(false) cbIDE.renderer = object : ColoredListCellRenderer() { override fun customizeCellRenderer(list: JList, value: IdeWithStatus?, index: Int, isSelected: Boolean, cellHasFocus: Boolean) { background = UIUtil.getListBackground(isSelected, cellHasFocus) 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 efbc0b3b..69b6e1cf 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -89,7 +89,7 @@ private const val SESSION_TOKEN = "session-token" private const val MOUSE_OVER_TEMPLATE_NAME_COLUMN_ON_ROW = "MOUSE_OVER_TEMPLATE_NAME_COLUMN_ON_ROW" -class CoderWorkspacesStepView(val enableNextButtonCallback: (Boolean) -> Unit) : CoderWorkspacesWizardStep, Disposable { +class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : CoderWorkspacesWizardStep, Disposable { private val cs = CoroutineScope(Dispatchers.Main) private var localWizardModel = CoderWorkspacesWizardModel() private val coderClient: CoderRestClientService = service() @@ -122,7 +122,7 @@ class CoderWorkspacesStepView(val enableNextButtonCallback: (Boolean) -> Unit) : rowHeight = 48 setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.addListSelectionListener { - enableNextButtonCallback(selectedObject != null && selectedObject?.agentStatus == RUNNING && selectedObject?.agentOS == OS.LINUX) + setNextButtonEnabled(selectedObject != null && selectedObject?.agentStatus == RUNNING && selectedObject?.agentOS == OS.LINUX) if (selectedObject?.agentStatus == RUNNING && selectedObject?.agentOS != OS.LINUX) { notificationBanner.apply { component.isVisible = true @@ -194,55 +194,74 @@ class CoderWorkspacesStepView(val enableNextButtonCallback: (Boolean) -> Unit) : private var poller: Job? = null override val component = panel { - indent { - row { - label(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.header.text")).applyToComponent { - font = JBFont.h3().asBold() - icon = CoderIcons.LOGO_16 - } - }.topGap(TopGap.SMALL) - row { - cell(ComponentPanelBuilder.createCommentComponent(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.comment"), false, -1, true)) - } - row { - browserLink(CoderGatewayBundle.message("gateway.connector.view.login.documentation.action"), "https://coder.com/docs/coder-oss/latest/workspaces") - } - row(CoderGatewayBundle.message("gateway.connector.view.login.url.label")) { - tfUrl = textField().resizableColumn().align(AlignX.FILL).gap(RightGap.SMALL).bindText(localWizardModel::coderURL).applyToComponent { - addActionListener { - poller?.cancel() - listTableModelOfWorkspaces.items = emptyList() - askTokenAndOpenSession(true) - } - }.component - button(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text")) { + row { + label(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.header.text")).applyToComponent { + font = JBFont.h3().asBold() + icon = CoderIcons.LOGO_16 + } + }.topGap(TopGap.SMALL) + row { + cell( + ComponentPanelBuilder.createCommentComponent( + CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.comment"), + false, + -1, + true + ) + ) + } + row { + browserLink( + CoderGatewayBundle.message("gateway.connector.view.login.documentation.action"), + "https://coder.com/docs/coder-oss/latest/workspaces" + ) + } + row(CoderGatewayBundle.message("gateway.connector.view.login.url.label")) { + tfUrl = textField().resizableColumn().align(AlignX.FILL).gap(RightGap.SMALL) + .bindText(localWizardModel::coderURL).applyToComponent { + addActionListener { poller?.cancel() listTableModelOfWorkspaces.items = emptyList() askTokenAndOpenSession(true) - }.applyToComponent { - background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() } - cell() - } - row { - cbExistingToken = checkBox(CoderGatewayBundle.message("gateway.connector.view.login.existing-token.label")) - .bindSelected(localWizardModel::useExistingToken) - .component - } - row { - cell(ComponentPanelBuilder.createCommentComponent( - CoderGatewayBundle.message("gateway.connector.view.login.existing-token.tooltip", - CoderGatewayBundle.message("gateway.connector.view.login.existing-token.label")), - false, -1, true)) - } - row { - scrollCell(toolbar.createPanel().apply { - add(notificationBanner.component.apply { isVisible = false }, "South") - }).resizableColumn().align(AlignX.FILL).align(AlignY.FILL) - cell() - }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).resizableRow() - } - }.apply { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() } + }.component + button(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text")) { + poller?.cancel() + listTableModelOfWorkspaces.items = emptyList() + askTokenAndOpenSession(true) + }.applyToComponent { + background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() + } + }.layout(RowLayout.PARENT_GRID) + row { + cell() // Empty cell for alignment. + cbExistingToken = checkBox(CoderGatewayBundle.message("gateway.connector.view.login.existing-token.label")) + .bindSelected(localWizardModel::useExistingToken) + .component + }.layout(RowLayout.PARENT_GRID) + row { + cell() // Empty cell for alignment. + cell( + ComponentPanelBuilder.createCommentComponent( + CoderGatewayBundle.message( + "gateway.connector.view.login.existing-token.tooltip", + CoderGatewayBundle.message("gateway.connector.view.login.existing-token.label"), + CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text") + ), + false, -1, true + ) + ) + }.layout(RowLayout.PARENT_GRID) + row { + scrollCell(toolbar.createPanel().apply { + add(notificationBanner.component.apply { isVisible = false }, "South") + }).resizableColumn().align(AlignX.FILL).align(AlignY.FILL) + }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).resizableRow() + + }.apply { + background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() + border = JBUI.Borders.empty(0, 16, 0, 16) + } override val previousActionText = IdeBundle.message("button.back") override val nextActionText = CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.next.text") diff --git a/src/main/resources/messages/CoderGatewayBundle.properties b/src/main/resources/messages/CoderGatewayBundle.properties index 95b34a1b..14f92f73 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. 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 appear as the default and can be used as-is or replaced. 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,9 @@ gateway.connector.view.coder.remoteproject.ide.error.text=Could not retrieve any gateway.connector.view.coder.remoteproject.ssh.error.text=Can't connect to the workspace. Please make sure Coder Agent is running! gateway.connector.view.coder.remoteproject.next.text=Start IDE and connect gateway.connector.view.coder.remoteproject.choose.text=Choose IDE and project for workspace {0} +gateway.connector.view.coder.remoteproject.ide.download.comment=This IDE will be downloaded from jetbrains.com and installed to the default path on the remote host. +gateway.connector.view.coder.remoteproject.ide.installed.comment=This IDE is already installed and will be used as-is. +gateway.connector.view.coder.remoteproject.ide.none.comment=No IDE selected. gateway.connector.recentconnections.title=Recent Coder Workspaces gateway.connector.recentconnections.new.wizard.button.tooltip=Open a new Coder Workspace gateway.connector.recentconnections.remove.button.tooltip=Remove from Recent Connections