@@ -5,18 +5,35 @@ import com.coder.gateway.icons.CoderIcons
55import com.coder.gateway.models.CoderWorkspacesWizardModel
66import com.coder.gateway.models.WorkspaceAgentModel
77import com.coder.gateway.sdk.Arch
8+ import com.coder.gateway.sdk.CoderCLIManager
89import com.coder.gateway.sdk.CoderRestClientService
910import com.coder.gateway.sdk.OS
11+ import com.coder.gateway.sdk.ex.AuthenticationResponseException
12+ import com.coder.gateway.sdk.getOS
13+ import com.coder.gateway.sdk.toURL
1014import com.coder.gateway.sdk.v2.models.ProvisionerJobStatus
1115import com.coder.gateway.sdk.v2.models.Workspace
1216import com.coder.gateway.sdk.v2.models.WorkspaceBuildTransition
17+ import com.coder.gateway.sdk.withPath
18+ import com.intellij.ide.BrowserUtil
1319import com.intellij.ide.IdeBundle
1420import com.intellij.openapi.Disposable
1521import com.intellij.openapi.application.ApplicationManager
22+ import com.intellij.openapi.application.ModalityState
23+ import com.intellij.openapi.application.invokeAndWaitIfNeeded
1624import com.intellij.openapi.diagnostic.Logger
25+ import com.intellij.openapi.progress.ProgressIndicator
26+ import com.intellij.openapi.progress.ProgressManager
27+ import com.intellij.openapi.progress.Task
28+ import com.intellij.openapi.ui.panel.ComponentPanelBuilder
1729import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
30+ import com.intellij.ui.AppIcon
31+ import com.intellij.ui.components.JBTextField
32+ import com.intellij.ui.components.dialog
1833import com.intellij.ui.dsl.builder.BottomGap
34+ import com.intellij.ui.dsl.builder.RightGap
1935import com.intellij.ui.dsl.builder.TopGap
36+ import com.intellij.ui.dsl.builder.bindText
2037import com.intellij.ui.dsl.builder.panel
2138import com.intellij.ui.dsl.gridLayout.HorizontalAlign
2239import com.intellij.ui.dsl.gridLayout.VerticalAlign
@@ -31,6 +48,7 @@ import kotlinx.coroutines.Dispatchers
3148import kotlinx.coroutines.cancel
3249import kotlinx.coroutines.launch
3350import kotlinx.coroutines.withContext
51+ import org.zeroturnaround.exec.ProcessExecutor
3452import java.awt.Color
3553import java.awt.Component
3654import java.awt.Dimension
@@ -43,10 +61,9 @@ import javax.swing.table.TableCellRenderer
4361
4462class CoderWorkspacesStepView : CoderWorkspacesWizardStep , Disposable {
4563 private val cs = CoroutineScope (Dispatchers .Main )
46-
64+ private var wizardModel = CoderWorkspacesWizardModel ()
4765 private val coderClient: CoderRestClientService = ApplicationManager .getApplication().getService(CoderRestClientService ::class .java)
4866
49-
5067 private var listTableModelOfWorkspaces = ListTableModel <WorkspaceAgentModel >(WorkspaceIconColumnInfo (" " ), WorkspaceNameColumnInfo (" Name" ), WorkspaceTemplateNameColumnInfo (" Template" ), WorkspaceStatusColumnInfo (" Status" ))
5168 private var tableOfWorkspaces = TableView (listTableModelOfWorkspaces).apply {
5269 rowSelectionAllowed = true
@@ -62,16 +79,31 @@ class CoderWorkspacesStepView : CoderWorkspacesWizardStep, Disposable {
6279 setSelectionMode(ListSelectionModel .SINGLE_SELECTION )
6380 }
6481
65- private lateinit var wizard: CoderWorkspacesWizardModel
66-
6782 override val component = panel {
6883 indent {
6984 row {
70- label(CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.choose .text" )).applyToComponent {
85+ label(CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.header .text" )).applyToComponent {
7186 font = JBFont .h3().asBold()
7287 icon = CoderIcons .LOGO_16
7388 }
89+ }.topGap(TopGap .SMALL ).bottomGap(BottomGap .MEDIUM )
90+ row {
91+ cell(ComponentPanelBuilder .createCommentComponent(CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.comment" ), false , - 1 , true ))
92+ }
93+ row {
94+ browserLink(CoderGatewayBundle .message(" gateway.connector.view.login.documentation.action" ), " https://coder.com/docs/coder-oss/latest/workspaces" )
7495 }.bottomGap(BottomGap .MEDIUM )
96+ row(CoderGatewayBundle .message(" gateway.connector.view.login.url.label" )) {
97+ textField().resizableColumn().horizontalAlign(HorizontalAlign .FILL ).gap(RightGap .SMALL ).bindText(wizardModel::coderURL).applyToComponent {
98+ addActionListener {
99+ loginAndLoadWorkspace()
100+ }
101+ }
102+ button(CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.connect.text" )) {
103+ loginAndLoadWorkspace()
104+ }
105+ cell()
106+ }
75107 row {
76108 scrollCell(tableOfWorkspaces).resizableColumn().horizontalAlign(HorizontalAlign .FILL ).verticalAlign(VerticalAlign .FILL )
77109 cell()
@@ -84,7 +116,89 @@ class CoderWorkspacesStepView : CoderWorkspacesWizardStep, Disposable {
84116 override val nextActionText = CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.next.text" )
85117
86118 override fun onInit (wizardModel : CoderWorkspacesWizardModel ) {
87- wizard = wizardModel
119+ }
120+
121+ private fun loginAndLoadWorkspace () {
122+ // force bindings to be filled
123+ component.apply ()
124+
125+ BrowserUtil .browse(wizardModel.coderURL.toURL().withPath(" /login?redirect=%2Fcli-auth" ))
126+ val pastedToken = askToken()
127+
128+ if (pastedToken.isNullOrBlank()) {
129+ return
130+ }
131+ try {
132+ coderClient.initClientSession(wizardModel.coderURL.toURL(), pastedToken)
133+ } catch (e: AuthenticationResponseException ) {
134+ CoderAuthStepView .logger.error(" Could not authenticate on ${wizardModel.coderURL} . Reason $e " )
135+ return
136+ }
137+ wizardModel.apply {
138+ token = pastedToken
139+ buildVersion = coderClient.buildVersion
140+ }
141+
142+ val authTask = object : Task .Modal (null , CoderGatewayBundle .message(" gateway.connector.view.coder.workspaces.cli.downloader.dialog.title" ), false ) {
143+ override fun run (pi : ProgressIndicator ) {
144+
145+ pi.apply {
146+ isIndeterminate = false
147+ text = " Downloading coder cli..."
148+ fraction = 0.1
149+ }
150+
151+ val cliManager = CoderCLIManager (wizardModel.coderURL.toURL(), wizardModel.buildVersion)
152+ val cli = cliManager.download() ? : throw IllegalStateException (" Could not download coder binary" )
153+ if (getOS() != OS .WINDOWS ) {
154+ pi.fraction = 0.4
155+ val chmodOutput = ProcessExecutor ().command(" chmod" , " +x" , cli.toAbsolutePath().toString()).readOutput(true ).execute().outputUTF8()
156+ CoderAuthStepView .logger.info(" chmod +x ${cli.toAbsolutePath()} $chmodOutput " )
157+ }
158+ pi.apply {
159+ text = " Configuring coder cli..."
160+ fraction = 0.5
161+ }
162+
163+ val loginOutput = ProcessExecutor ().command(cli.toAbsolutePath().toString(), " login" , wizardModel.coderURL, " --token" , wizardModel.token).readOutput(true ).execute().outputUTF8()
164+ CoderAuthStepView .logger.info(" coder-cli login output: $loginOutput " )
165+ pi.fraction = 0.8
166+ val sshConfigOutput = ProcessExecutor ().command(cli.toAbsolutePath().toString(), " config-ssh" , " --yes" , " --use-previous-options" ).readOutput(true ).execute().outputUTF8()
167+ CoderAuthStepView .logger.info(" Result of `${cli.toAbsolutePath()} config-ssh --yes --use-previous-options`: $sshConfigOutput " )
168+ pi.fraction = 1.0
169+ }
170+ }
171+
172+ wizardModel.apply {
173+ coderURL = wizardModel.coderURL
174+ token = wizardModel.token
175+ }
176+ ProgressManager .getInstance().run (authTask)
177+ loadWorkspaces()
178+ }
179+
180+ private fun askToken (): String? {
181+ return invokeAndWaitIfNeeded(ModalityState .any()) {
182+ lateinit var sessionTokenTextField: JBTextField
183+
184+ val panel = panel {
185+ row {
186+ label(CoderGatewayBundle .message(" gateway.connector.view.login.token.label" ))
187+ sessionTokenTextField = textField().applyToComponent {
188+ minimumSize = Dimension (320 , - 1 )
189+ }.component
190+ }
191+ }
192+
193+ AppIcon .getInstance().requestAttention(null , true )
194+ if (! dialog(CoderGatewayBundle .message(" gateway.connector.view.login.token.dialog" ), panel = panel, focusedComponent = sessionTokenTextField).showAndGet()) {
195+ return @invokeAndWaitIfNeeded null
196+ }
197+ return @invokeAndWaitIfNeeded sessionTokenTextField.text
198+ }
199+ }
200+
201+ private fun loadWorkspaces () {
88202 cs.launch {
89203 val workspaceList = withContext(Dispatchers .IO ) {
90204 try {
0 commit comments