Skip to content

Commit 436e607

Browse files
committed
Implement view for listing Coder Workspaces
- basic view that renders the Workspaces with name, logo and status - glued the service logic that retrieve the workspaces - refactored the connector view to support a wizard like view with multiple steps and different behaviors for the back and next buttons depending on which step is current. - refactored the bottom bar to include a next button - don't display the next button if last step is current
1 parent 6003e47 commit 436e607

15 files changed

+387
-97
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
### Added
88

99
* welcome screen
10-
* basic connector view triggered by the Coder's welcome view. It asks the user a Coder hostname, port, email and password.
10+
* basic Coder Workspaces wizard triggered by the Coder's welcome view. It asks the user a Coder hostname, port, email and password,
11+
authenticates with the Coder server and lists a collection of Workspaces created by the user.
1112
* back button to return to the main welcome view
1213
* basic Coder http client which authenticates, retrieves a session token and uses it to retrieve the Workspaces created by the
1314
user that is logged.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.coder.gateway.models
2+
3+
data class CoderWorkspacesWizardModel(var loginModel: LoginModel, var workspaces: List<Workspace>)
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
package com.coder.gateway.models
22

3-
internal data class LoginModel(var uriScheme: UriScheme = UriScheme.HTTP, var host: String = "localhost", var port: Int = 7080, var email: String = "example@email.com", var password: String? = "")
3+
data class LoginModel(var uriScheme: UriScheme = UriScheme.HTTP, var host: String = "localhost", var port: Int = 7080, var email: String = "example@email.com", var password: String? = "")
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,129 @@
11
package com.coder.gateway.views
22

3-
import com.coder.gateway.CoderGatewayBundle
3+
import com.coder.gateway.models.CoderWorkspacesWizardModel
44
import com.coder.gateway.models.LoginModel
5-
import com.coder.gateway.models.UriScheme
6-
import com.coder.gateway.sdk.CoderClientService
7-
import com.intellij.credentialStore.CredentialAttributes
8-
import com.intellij.credentialStore.askPassword
9-
import com.intellij.ide.IdeBundle
5+
import com.coder.gateway.views.steps.CoderAuthStepView
6+
import com.coder.gateway.views.steps.CoderWorkspacesStepView
7+
import com.coder.gateway.views.steps.CoderWorkspacesWizardStep
108
import com.intellij.openapi.Disposable
11-
import com.intellij.openapi.application.ApplicationManager
12-
import com.intellij.openapi.diagnostic.Logger
13-
import com.intellij.openapi.ui.DialogPanel
14-
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
159
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
16-
import com.intellij.ui.IconManager
17-
import com.intellij.ui.SeparatorComponent
18-
import com.intellij.ui.components.panels.VerticalLayout
19-
import com.intellij.ui.dsl.builder.BottomGap
2010
import com.intellij.ui.dsl.builder.RightGap
21-
import com.intellij.ui.dsl.builder.TopGap
22-
import com.intellij.ui.dsl.builder.bindIntText
23-
import com.intellij.ui.dsl.builder.bindItem
24-
import com.intellij.ui.dsl.builder.bindText
2511
import com.intellij.ui.dsl.builder.panel
26-
import com.intellij.ui.dsl.builder.toNullableProperty
2712
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
28-
import com.intellij.util.ui.JBFont
29-
import com.intellij.util.ui.JBUI
3013
import com.intellij.util.ui.components.BorderLayoutPanel
3114
import com.jetbrains.gateway.api.GatewayUI
3215
import kotlinx.coroutines.CoroutineScope
3316
import kotlinx.coroutines.Dispatchers
34-
import kotlinx.coroutines.cancel
3517
import kotlinx.coroutines.launch
3618
import kotlinx.coroutines.withContext
3719
import java.awt.Component
3820
import javax.swing.JButton
39-
import javax.swing.JPanel
4021

4122
class CoderGatewayLoginView : BorderLayoutPanel(), Disposable {
42-
private val logger = Logger.getInstance(CoderClientService::class.java)
4323
private val cs = CoroutineScope(Dispatchers.Main)
44-
private val model = LoginModel()
45-
private val coderClient: CoderClientService = ApplicationManager.getApplication().getService(CoderClientService::class.java)
24+
private var steps = arrayListOf<CoderWorkspacesWizardStep>()
25+
private var currentStep = 0
26+
private val model = CoderWorkspacesWizardModel(LoginModel(), emptyList())
4627

47-
private lateinit var loginPanel: DialogPanel
28+
private lateinit var previousButton: JButton
29+
private lateinit var nextButton: JButton
4830

4931
init {
50-
initView()
32+
setupWizard()
5133
}
5234

53-
private fun initView() {
35+
private fun setupWizard() {
5436
background = WelcomeScreenUIManager.getMainAssociatedComponentBackground()
55-
addToCenter(createLoginComponent())
37+
38+
registerStep(CoderAuthStepView())
39+
registerStep(CoderWorkspacesStepView())
40+
5641
addToBottom(createBackComponent())
42+
43+
steps[0].apply {
44+
onInit(model)
45+
addToCenter(component)
46+
updateUI()
47+
nextButton.text = nextActionText
48+
previousButton.text = previousActionText
49+
}
50+
5751
}
5852

59-
private fun createLoginComponent(): DialogPanel {
60-
loginPanel = panel {
61-
indent {
62-
row {
63-
label(CoderGatewayBundle.message("gateway.connector.view.login.header.text")).applyToComponent {
64-
font = JBFont.h3().asBold()
65-
icon = IconManager.getInstance().getIcon("coder_logo_16.svg", CoderGatewayLoginView::class.java)
66-
}
67-
}.topGap(TopGap.SMALL).bottomGap(BottomGap.MEDIUM)
68-
row {
69-
cell(ComponentPanelBuilder.createCommentComponent(CoderGatewayBundle.message("gateway.connector.view.login.comment.text"), false, -1, true))
70-
}
71-
row {
72-
browserLink(CoderGatewayBundle.message("gateway.connector.view.login.documentation.action"), "https://coder.com/docs/coder/latest/workspaces")
73-
}.bottomGap(BottomGap.MEDIUM)
74-
row {
75-
label(CoderGatewayBundle.message("gateway.connector.view.login.scheme.label"))
76-
comboBox(UriScheme.values().toList()).bindItem(model::uriScheme.toNullableProperty())
77-
label(CoderGatewayBundle.message("gateway.connector.view.login.host.label"))
78-
textField().resizableColumn().horizontalAlign(HorizontalAlign.FILL).gap(RightGap.SMALL).bindText(model::host)
79-
label(CoderGatewayBundle.message("gateway.connector.view.login.port.label"))
80-
intTextField(0..65536).bindIntText(model::port)
81-
button(CoderGatewayBundle.message("gateway.connector.view.login.connect.action")) {
82-
loginPanel.apply()
83-
model.password = askPassword(
84-
null,
85-
CoderGatewayBundle.message("gateway.connector.view.login.credentials.dialog.title"),
86-
CoderGatewayBundle.message("gateway.connector.view.login.password.label"),
87-
CredentialAttributes("Coder"),
88-
false
89-
)
90-
cs.launch {
91-
val workspaces = withContext(Dispatchers.IO) {
92-
coderClient.initClientSession(model.uriScheme, model.host, model.port, model.email, model.password!!)
93-
coderClient.workspaces()
94-
}
95-
}
96-
97-
}.applyToComponent {
98-
background = WelcomeScreenUIManager.getMainAssociatedComponentBackground()
99-
border = JBUI.Borders.empty(3, 3, 3, 3)
100-
}
101-
cell()
102-
}
53+
private fun registerStep(step: CoderWorkspacesWizardStep) {
54+
steps.add(step)
55+
}
56+
57+
private fun previous() {
58+
nextButton.isVisible = true
59+
if (currentStep == 0) {
60+
GatewayUI.Companion.getInstance().reset()
61+
} else {
62+
remove(steps[currentStep].component)
63+
updateUI()
64+
65+
currentStep--
66+
steps[currentStep].apply {
67+
onInit(model)
68+
addToCenter(component)
69+
nextButton.text = nextActionText
70+
previousButton.text = previousActionText
71+
}
72+
}
73+
}
10374

104-
row(CoderGatewayBundle.message("gateway.connector.view.login.email.label")) {
105-
textField().resizableColumn().bindText(model::email)
106-
cell()
75+
private fun next() {
76+
cs.launch {
77+
if (currentStep + 1 < steps.size) {
78+
withContext(Dispatchers.Main) { doNextCallback() }
79+
remove(steps[currentStep].component)
80+
updateUI()
81+
currentStep++
82+
steps[currentStep].apply {
83+
addToCenter(component)
84+
onInit(model)
85+
updateUI()
86+
87+
nextButton.text = nextActionText
88+
previousButton.text = previousActionText
10789
}
90+
91+
nextButton.isVisible = currentStep != steps.size - 1
10892
}
109-
}.apply {
110-
background = WelcomeScreenUIManager.getMainAssociatedComponentBackground()
11193
}
94+
}
95+
11296

113-
return loginPanel
97+
private suspend fun doNextCallback() {
98+
steps[currentStep].apply {
99+
component.apply()
100+
onNext(model)
101+
}
114102
}
115103

116104
private fun createBackComponent(): Component {
117-
return JPanel(VerticalLayout(0)).apply {
118-
add(SeparatorComponent(0, 0, WelcomeScreenUIManager.getSeparatorColor(), null))
119-
add(BorderLayoutPanel().apply {
120-
border = JBUI.Borders.empty(6, 24, 6, 0)
121-
addToLeft(JButton(IdeBundle.message("button.back")).apply {
122-
border = JBUI.Borders.empty(3, 3, 3, 3)
123-
background = WelcomeScreenUIManager.getMainAssociatedComponentBackground()
124-
addActionListener {
125-
GatewayUI.Companion.getInstance().reset()
126-
}
127-
})
105+
previousButton = JButton()
106+
nextButton = JButton()
107+
return panel {
108+
separator(background = WelcomeScreenUIManager.getSeparatorColor())
109+
indent {
110+
row {
111+
112+
label("").resizableColumn().horizontalAlign(HorizontalAlign.FILL).gap(RightGap.SMALL)
113+
previousButton = button("") { previous() }.horizontalAlign(HorizontalAlign.RIGHT).gap(RightGap.SMALL).applyToComponent { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() }.component
114+
nextButton = button("") { next() }.horizontalAlign(HorizontalAlign.RIGHT).gap(RightGap.SMALL).applyToComponent { background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() }.component
115+
}
116+
}.apply {
128117
background = WelcomeScreenUIManager.getMainAssociatedComponentBackground()
129-
})
118+
}
119+
120+
}.apply {
130121
background = WelcomeScreenUIManager.getMainAssociatedComponentBackground()
131122
}
132123
}
133124

134125
override fun dispose() {
135-
cs.cancel()
126+
steps.clear()
136127
}
137128
}
138129

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.coder.gateway.views.steps
2+
3+
import com.coder.gateway.CoderGatewayBundle
4+
import com.coder.gateway.models.CoderWorkspacesWizardModel
5+
import com.coder.gateway.models.LoginModel
6+
import com.coder.gateway.models.UriScheme
7+
import com.coder.gateway.sdk.CoderClientService
8+
import com.intellij.credentialStore.CredentialAttributes
9+
import com.intellij.credentialStore.askPassword
10+
import com.intellij.ide.IdeBundle
11+
import com.intellij.openapi.Disposable
12+
import com.intellij.openapi.application.ApplicationManager
13+
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
14+
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
15+
import com.intellij.ui.IconManager
16+
import com.intellij.ui.dsl.builder.BottomGap
17+
import com.intellij.ui.dsl.builder.RightGap
18+
import com.intellij.ui.dsl.builder.TopGap
19+
import com.intellij.ui.dsl.builder.bindIntText
20+
import com.intellij.ui.dsl.builder.bindItem
21+
import com.intellij.ui.dsl.builder.bindText
22+
import com.intellij.ui.dsl.builder.panel
23+
import com.intellij.ui.dsl.builder.toNullableProperty
24+
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
25+
import com.intellij.util.ui.JBFont
26+
import kotlinx.coroutines.CoroutineScope
27+
import kotlinx.coroutines.Dispatchers
28+
import kotlinx.coroutines.cancel
29+
import kotlinx.coroutines.withContext
30+
31+
class CoderAuthStepView : CoderWorkspacesWizardStep, Disposable {
32+
private val cs = CoroutineScope(Dispatchers.Main)
33+
private var model = LoginModel()
34+
private val coderClient: CoderClientService = ApplicationManager.getApplication().getService(CoderClientService::class.java)
35+
36+
override val component = panel {
37+
indent {
38+
row {
39+
label(CoderGatewayBundle.message("gateway.connector.view.login.header.text")).applyToComponent {
40+
font = JBFont.h3().asBold()
41+
icon = IconManager.getInstance().getIcon("coder_logo_16.svg", this@CoderAuthStepView::class.java)
42+
}
43+
}.topGap(TopGap.SMALL).bottomGap(BottomGap.MEDIUM)
44+
row {
45+
cell(ComponentPanelBuilder.createCommentComponent(CoderGatewayBundle.message("gateway.connector.view.login.comment.text"), false, -1, true))
46+
}
47+
row {
48+
browserLink(CoderGatewayBundle.message("gateway.connector.view.login.documentation.action"), "https://coder.com/docs/coder/latest/workspaces")
49+
}.bottomGap(BottomGap.MEDIUM)
50+
row {
51+
label(CoderGatewayBundle.message("gateway.connector.view.login.scheme.label"))
52+
comboBox(UriScheme.values().toList()).bindItem(model::uriScheme.toNullableProperty())
53+
label(CoderGatewayBundle.message("gateway.connector.view.login.host.label"))
54+
textField().resizableColumn().horizontalAlign(HorizontalAlign.FILL).gap(RightGap.SMALL).bindText(model::host)
55+
label(CoderGatewayBundle.message("gateway.connector.view.login.port.label"))
56+
intTextField(0..65536).bindIntText(model::port)
57+
cell()
58+
}
59+
60+
row(CoderGatewayBundle.message("gateway.connector.view.login.email.label")) {
61+
textField().resizableColumn().bindText(model::email)
62+
cell()
63+
}
64+
}
65+
}.apply {
66+
background = WelcomeScreenUIManager.getMainAssociatedComponentBackground()
67+
}
68+
69+
override val nextActionText = CoderGatewayBundle.message("gateway.connector.view.coder.auth.next.text")
70+
override val previousActionText = IdeBundle.message("button.back")
71+
72+
override fun onInit(wizardModel: CoderWorkspacesWizardModel) {
73+
model.apply {
74+
uriScheme = wizardModel.loginModel.uriScheme
75+
host = wizardModel.loginModel.host
76+
port = wizardModel.loginModel.port
77+
email = wizardModel.loginModel.email
78+
password = wizardModel.loginModel.password
79+
}
80+
component.apply()
81+
}
82+
83+
override suspend fun onNext(wizardModel: CoderWorkspacesWizardModel) {
84+
model.password = askPassword(
85+
null,
86+
CoderGatewayBundle.message("gateway.connector.view.login.credentials.dialog.title"),
87+
CoderGatewayBundle.message("gateway.connector.view.login.password.label"),
88+
CredentialAttributes("Coder"),
89+
true
90+
)
91+
withContext(Dispatchers.IO) {
92+
coderClient.initClientSession(model.uriScheme, model.host, model.port, model.email, model.password!!)
93+
}
94+
wizardModel.apply {
95+
loginModel = model.copy()
96+
}
97+
98+
}
99+
100+
101+
override fun dispose() {
102+
cs.cancel()
103+
}
104+
}

0 commit comments

Comments
 (0)