Skip to content

Commit 296ff94

Browse files
committed
Make steps usable standalone
The previous refactor was close but we needed a bit more refactoring to make a single step usable on its own, as it will be used in the link handler. The main problem was the wrapper that had both steps in it, we would have to pass in the steps instead which brings us back to that whole generalization problem where a step has types that lead into the next step's init. The solution is to remove the wrapper and wire up the steps wherever they are needed, whether that is one step or multiple, and the main change to make that possible is to move the buttons into each step rather than having them in a wrapper component. Now you can create any single step and use its output. This is used in the link flow and the buttons in the step now replace the default buttons in the dialog wrapper.
1 parent 76d298b commit 296ff94

8 files changed

+196
-179
lines changed

src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt

+68-33
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.coder.gateway.cli.CoderCLIManager
66
import com.coder.gateway.cli.ensureCLI
77
import com.coder.gateway.models.TokenSource
88
import com.coder.gateway.models.WorkspaceAndAgentStatus
9+
import com.coder.gateway.sdk.BaseCoderRestClient
910
import com.coder.gateway.sdk.CoderRestClient
1011
import com.coder.gateway.sdk.ex.AuthenticationResponseException
1112
import com.coder.gateway.sdk.v2.models.Workspace
@@ -20,11 +21,18 @@ import com.intellij.openapi.application.ApplicationManager
2021
import com.intellij.openapi.components.service
2122
import com.intellij.openapi.diagnostic.Logger
2223
import com.intellij.openapi.ui.DialogWrapper
24+
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
25+
import com.intellij.ui.dsl.builder.panel
26+
import com.intellij.util.ui.JBUI
2327
import com.jetbrains.gateway.api.ConnectionRequestor
2428
import com.jetbrains.gateway.api.GatewayConnectionHandle
2529
import com.jetbrains.gateway.api.GatewayConnectionProvider
2630
import java.net.URL
31+
import javax.swing.Action
32+
import javax.swing.JButton
2733
import javax.swing.JComponent
34+
import javax.swing.JPanel
35+
import javax.swing.border.Border
2836

2937
// In addition to `type`, these are the keys that we support in our Gateway
3038
// links.
@@ -38,16 +46,50 @@ private const val IDE_DOWNLOAD_LINK = "ide_download_link"
3846
private const val IDE_PRODUCT_CODE = "ide_product_code"
3947
private const val IDE_BUILD_NUMBER = "ide_build_number"
4048
private const val IDE_PATH_ON_HOST = "ide_path_on_host"
41-
private const val PROJECT_PATH = "project_path"
4249

43-
class SelectWorkspaceIDEDialog(private val comp: JComponent) : DialogWrapper(true) {
50+
/**
51+
* A dialog wrapper around CoderWorkspaceStepView.
52+
*/
53+
class CoderWorkspaceStepDialog(
54+
name: String,
55+
private val state: CoderWorkspacesStepSelection,
56+
) : DialogWrapper(true) {
57+
private val view = CoderWorkspaceStepView(showTitle = false)
58+
4459
init {
4560
init()
46-
title = "Select workspace IDE"
61+
title = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", name)
62+
}
63+
64+
override fun show() {
65+
view.init(state)
66+
view.onPrevious = { close(1) }
67+
view.onNext = { close(0) }
68+
super.show()
69+
view.dispose()
4770
}
4871

49-
override fun createCenterPanel(): JComponent? {
50-
return comp
72+
fun showAndGetData(): Map<String, String>? {
73+
if (showAndGet()) {
74+
return view.data()
75+
}
76+
return null
77+
}
78+
79+
override fun createContentPaneBorder(): Border {
80+
return JBUI.Borders.empty()
81+
}
82+
83+
override fun createCenterPanel(): JComponent {
84+
return view
85+
}
86+
87+
override fun createSouthPanel(): JComponent {
88+
// The plugin provides its own buttons.
89+
// TODO: Is it more idiomatic to handle buttons out here?
90+
return panel{}.apply {
91+
border = JBUI.Borders.empty()
92+
}
5193
}
5294
}
5395

@@ -109,41 +151,34 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
109151
cli.login(client.token)
110152

111153
indicator.text = "Configuring Coder CLI..."
112-
cli.configSsh(client.agentNames(workspaces).toSet())
113-
154+
cli.configSsh(client.agentNames(workspaces))
114155

156+
val name = "${workspace.name}.${agent.name}"
115157
val openDialog = parameters[IDE_PRODUCT_CODE].isNullOrBlank() ||
116158
parameters[IDE_BUILD_NUMBER].isNullOrBlank() ||
117159
(parameters[IDE_PATH_ON_HOST].isNullOrBlank() && parameters[IDE_DOWNLOAD_LINK].isNullOrBlank()) ||
118160
parameters[FOLDER].isNullOrBlank()
119161

120-
val params = if (openDialog) {
121-
val view = CoderWorkspaceStepView{}
162+
if (openDialog) {
163+
var data: Map<String, String>? = null
122164
ApplicationManager.getApplication().invokeAndWait {
123-
view.init(
124-
CoderWorkspacesStepSelection(agent, workspace, cli, client, workspaces)
125-
)
126-
val dialog = SelectWorkspaceIDEDialog(view.component)
127-
dialog.show()
165+
val dialog = CoderWorkspaceStepDialog(name,
166+
CoderWorkspacesStepSelection(agent, workspace, cli, client, workspaces))
167+
data = dialog.showAndGetData()
128168
}
129-
val p = parameters.toMutableMap()
130-
131-
132-
listOf(IDE_PRODUCT_CODE, IDE_BUILD_NUMBER, PROJECT_PATH, IDE_PATH_ON_HOST, IDE_DOWNLOAD_LINK).forEach { prop ->
133-
view.data()[prop]?.let { value -> p[prop] = value }
134-
}
135-
p
136-
} else
137-
parameters.withProjectPath(parameters[FOLDER]!!)
138-
139-
// Check that both the domain and the redirected domain are
140-
// allowlisted. If not, check with the user whether to proceed.
141-
verifyDownloadLink(parameters)
142-
143-
params.withWorkspaceHostname(CoderCLIManager.getHostName(deploymentURL.toURL(), "${workspace.name}.${agent.name}"))
144-
.withWebTerminalLink(client.url.withPath("/@$username/$workspace.name/terminal").toString())
145-
.withConfigDirectory(cli.coderConfigPath.toString())
146-
.withName(workspaceName)
169+
data ?: throw Exception("IDE selection aborted; unable to connect")
170+
} else {
171+
// Check that both the domain and the redirected domain are
172+
// allowlisted. If not, check with the user whether to proceed.
173+
verifyDownloadLink(parameters)
174+
175+
parameters
176+
.withWorkspaceHostname(CoderCLIManager.getHostName(deploymentURL.toURL(), name))
177+
.withProjectPath(parameters[FOLDER]!!)
178+
.withWebTerminalLink(client.url.withPath("/@$username/$workspace.name/terminal").toString())
179+
.withConfigDirectory(cli.coderConfigPath.toString())
180+
.withName(name)
181+
}
147182
}
148183
return null
149184
}
@@ -152,7 +187,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
152187
* Return an authenticated Coder CLI and the user's name, asking for the
153188
* token as long as it continues to result in an authentication failure.
154189
*/
155-
private fun authenticate(deploymentURL: URL, queryToken: String?, lastToken: Pair<String, TokenSource>? = null): Pair<CoderRestClient, String> {
190+
private fun authenticate(deploymentURL: URL, queryToken: String?, lastToken: Pair<String, TokenSource>? = null): Pair<BaseCoderRestClient, String> {
156191
// Use the token from the query, unless we already tried that.
157192
val isRetry = lastToken != null
158193
val token = if (!queryToken.isNullOrBlank() && !isRetry)

src/main/kotlin/com/coder/gateway/WorkspaceParams.kt

+3
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ fun Map<String, String>.withConfigDirectory(dir: String): Map<String, String> {
9090
return map
9191
}
9292

93+
/**
94+
* Set the full name in `workspace.agent` format.
95+
*/
9396
fun Map<String, String>.withName(name: String): Map<String, String> {
9497
val map = this.toMutableMap()
9598
map[NAME] = name

src/main/kotlin/com/coder/gateway/sdk/BaseCoderRestClient.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,14 @@ open class BaseCoderRestClient(
155155
* Retrieves all the agent names for all workspaces, including those that
156156
* are off. Meant to be used when configuring SSH.
157157
*/
158-
fun agentNames(workspaces: List<Workspace>): List<String> {
158+
fun agentNames(workspaces: List<Workspace>): Set<String> {
159+
// It is possible for there to be resources with duplicate names so we
160+
// need to use a set.
159161
return workspaces.flatMap { ws ->
160162
resources(ws).filter { it.agents != null }.flatMap { it.agents!! }.map {
161163
"${ws.name}.${it.name}"
162164
}
163-
}
165+
}.toSet()
164166
}
165167

166168
/**
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
11
package com.coder.gateway.views
22

33
import com.coder.gateway.CoderRemoteConnectionHandle
4-
import com.coder.gateway.views.steps.CoderWizardView
4+
import com.coder.gateway.views.steps.CoderWorkspaceStepView
5+
import com.coder.gateway.views.steps.CoderWorkspacesStepView
56
import com.intellij.ui.components.panels.Wrapper
67
import com.intellij.util.ui.JBUI
78
import com.jetbrains.gateway.api.GatewayConnectorView
9+
import com.jetbrains.gateway.api.GatewayUI
810
import javax.swing.JComponent
911

1012
class CoderGatewayConnectorWizardWrapperView : GatewayConnectorView {
1113
override val component: JComponent
1214
get() {
13-
return Wrapper(CoderWizardView { params ->
15+
val step1 = CoderWorkspacesStepView()
16+
val step2 = CoderWorkspaceStepView()
17+
val wrapper = Wrapper(step1).apply { border = JBUI.Borders.empty() }
18+
step1.init()
19+
20+
step1.onPrevious = {
21+
GatewayUI.getInstance().reset()
22+
step1.dispose()
23+
step2.dispose()
24+
}
25+
step1.onNext = {
26+
step1.stop()
27+
step2.init(it)
28+
wrapper.setContent(step2)
29+
}
30+
31+
step2.onPrevious = {
32+
step2.stop()
33+
step1.init()
34+
wrapper.setContent(step1)
35+
}
36+
step2.onNext = { params ->
37+
GatewayUI.getInstance().reset()
38+
step1.dispose()
39+
step2.dispose()
1440
CoderRemoteConnectionHandle().connect { params }
15-
}).apply { border = JBUI.Borders.empty() }
41+
}
42+
43+
return wrapper
1644
}
1745
}

src/main/kotlin/com/coder/gateway/views/steps/CoderWizardStep.kt

+56-6
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,67 @@
11
package com.coder.gateway.views.steps
22

3+
import com.intellij.ide.IdeBundle
34
import com.intellij.openapi.Disposable
4-
import com.intellij.openapi.ui.DialogPanel
5+
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
6+
import com.intellij.ui.dsl.builder.AlignX
7+
import com.intellij.ui.dsl.builder.BottomGap
8+
import com.intellij.ui.dsl.builder.RightGap
9+
import com.intellij.ui.dsl.builder.TopGap
10+
import com.intellij.ui.dsl.builder.panel
11+
import com.intellij.util.ui.JBUI
12+
import com.intellij.util.ui.components.BorderLayoutPanel
13+
import javax.swing.JButton
514

6-
sealed interface CoderWizardStep : Disposable {
7-
val component: DialogPanel
8-
var nextActionText: String
9-
var previousActionText: String
15+
sealed class CoderWizardStep<T>(
16+
nextActionText: String,
17+
) : BorderLayoutPanel(), Disposable {
18+
var onPrevious: (() -> Unit)? = null
19+
var onNext: ((data: T) -> Unit)? = null
20+
21+
private lateinit var previousButton: JButton
22+
protected lateinit var nextButton: JButton
23+
24+
private val buttons = panel {
25+
separator(background = WelcomeScreenUIManager.getSeparatorColor())
26+
row {
27+
label("").resizableColumn().align(AlignX.FILL).gap(RightGap.SMALL)
28+
previousButton = button(IdeBundle.message("button.back")) { previous() }
29+
.align(AlignX.RIGHT).gap(RightGap.SMALL)
30+
.applyToComponent { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() }.component
31+
nextButton = button(nextActionText) { next() }
32+
.align(AlignX.RIGHT)
33+
.applyToComponent { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() }.component
34+
}.bottomGap(BottomGap.SMALL)
35+
}.apply {
36+
background = WelcomeScreenUIManager.getMainAssociatedComponentBackground()
37+
border = JBUI.Borders.empty(0, 16)
38+
}
39+
40+
init {
41+
nextButton.isEnabled = false
42+
addToBottom(buttons)
43+
}
44+
45+
private fun previous() {
46+
withoutNull(onPrevious) {
47+
it()
48+
}
49+
}
50+
private fun next() {
51+
withoutNull(onNext) {
52+
it(data())
53+
}
54+
}
55+
56+
/**
57+
* Return data gathered by this step.
58+
*/
59+
abstract fun data(): T
1060

1161
/**
1262
* Stop any background processes. Data will still be available.
1363
*/
14-
fun stop()
64+
abstract fun stop()
1565
}
1666

1767
/**

0 commit comments

Comments
 (0)