Skip to content

Commit abe0326

Browse files
committed
Refactor link handler for Toolbox
In Toolbox, you get a ToolboxUi object that lets you show dialogs so we now have a dialog class for that which can be refactored to use ToolboxUi more easily. Also passed in the http client since we will need that in Toolbox to construct the client.
1 parent 9d040a8 commit abe0326

File tree

6 files changed

+345
-357
lines changed

6 files changed

+345
-357
lines changed

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

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
package com.coder.gateway
44

55
import com.coder.gateway.services.CoderSettingsService
6-
import com.coder.gateway.util.handleLink
6+
import com.coder.gateway.util.DialogUi
7+
import com.coder.gateway.util.LinkHandler
78
import com.coder.gateway.util.isCoder
89
import com.intellij.openapi.components.service
910
import com.intellij.openapi.diagnostic.Logger
@@ -13,16 +14,16 @@ import com.jetbrains.gateway.api.GatewayConnectionProvider
1314

1415
// CoderGatewayConnectionProvider handles connecting via a Gateway link such as
1516
// jetbrains-gateway://connect#type=coder.
16-
class CoderGatewayConnectionProvider : GatewayConnectionProvider {
17-
private val settings: CoderSettingsService = service<CoderSettingsService>()
18-
17+
class CoderGatewayConnectionProvider :
18+
LinkHandler(service<CoderSettingsService>(), null, DialogUi(service<CoderSettingsService>())),
19+
GatewayConnectionProvider {
1920
override suspend fun connect(
2021
parameters: Map<String, String>,
2122
requestor: ConnectionRequestor,
2223
): GatewayConnectionHandle? {
2324
CoderRemoteConnectionHandle().connect { indicator ->
2425
logger.debug("Launched Coder link handler", parameters)
25-
handleLink(parameters, settings) {
26+
handle(parameters) {
2627
indicator.text = it
2728
}
2829
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import com.coder.gateway.models.toRawString
99
import com.coder.gateway.models.withWorkspaceProject
1010
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
1111
import com.coder.gateway.services.CoderSettingsService
12+
import com.coder.gateway.util.DialogUi
1213
import com.coder.gateway.util.SemVer
13-
import com.coder.gateway.util.confirm
1414
import com.coder.gateway.util.humanizeDuration
1515
import com.coder.gateway.util.isCancellation
1616
import com.coder.gateway.util.isWorkerTimeout
@@ -63,6 +63,7 @@ class CoderRemoteConnectionHandle {
6363
private val settings = service<CoderSettingsService>()
6464

6565
private val localTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm")
66+
private val dialogUi = DialogUi(settings)
6667

6768
fun connect(getParameters: (indicator: ProgressIndicator) -> WorkspaceProjectIDE) {
6869
val clientLifetime = LifetimeDefinition()
@@ -198,7 +199,7 @@ class CoderRemoteConnectionHandle {
198199
.minOfOrNull { it.toIdeWithStatus() }
199200
if (latest != null && SemVer.parse(latest.buildNumber) > SemVer.parse(workspace.ideBuildNumber)) {
200201
logger.info("Got newer version: ${latest.buildNumber} versus current ${workspace.ideBuildNumber}")
201-
if (confirm("Update IDE", "There is a new version of this IDE: ${latest.buildNumber}", "Would you like to update?")) {
202+
if (dialogUi.confirm("Update IDE", "There is a new version of this IDE: ${latest.buildNumber}. Would you like to update?")) {
202203
return latest
203204
}
204205
}

src/main/kotlin/com/coder/gateway/util/Dialogs.kt

+148-157
Original file line numberDiff line numberDiff line change
@@ -70,180 +70,171 @@ private class CoderWorkspaceStepDialog(
7070
}
7171
}
7272

73-
/**
74-
* Generic function to ask for consent.
75-
*/
76-
fun confirm(
77-
title: String,
78-
comment: String,
79-
details: String,
80-
): Boolean {
81-
var inputFromUser = false
82-
ApplicationManager.getApplication().invokeAndWait({
83-
val panel =
84-
panel {
85-
row {
86-
label(comment)
87-
}
88-
row {
89-
label(details)
90-
}
91-
}
92-
AppIcon.getInstance().requestAttention(null, true)
93-
if (!dialog(
94-
title = title,
95-
panel = panel,
96-
).showAndGet()
97-
) {
98-
return@invokeAndWait
99-
}
100-
inputFromUser = true
101-
}, ModalityState.defaultModalityState())
102-
return inputFromUser
73+
fun askIDE(
74+
name: String,
75+
agent: WorkspaceAgent,
76+
workspace: Workspace,
77+
cli: CoderCLIManager,
78+
client: CoderRestClient,
79+
workspaces: List<Workspace>,
80+
): WorkspaceProjectIDE? {
81+
var data: WorkspaceProjectIDE? = null
82+
ApplicationManager.getApplication().invokeAndWait {
83+
val dialog =
84+
CoderWorkspaceStepDialog(
85+
name,
86+
CoderWorkspacesStepSelection(agent, workspace, cli, client, workspaces),
87+
)
88+
data = dialog.showAndGetData()
89+
}
90+
return data
10391
}
10492

10593
/**
106-
* Generic function to ask for input.
94+
* Dialog implementation for standalone Gateway.
95+
*
96+
* This is meant to mimic ToolboxUi.
10797
*/
108-
fun ask(
109-
comment: String,
110-
isError: Boolean = false,
111-
link: Pair<String, String>? = null,
112-
default: String? = null,
113-
): String? {
114-
var inputFromUser: String? = null
115-
ApplicationManager.getApplication().invokeAndWait({
116-
lateinit var inputTextField: JBTextField
117-
val panel =
118-
panel {
119-
row {
120-
if (link != null) browserLink(link.first, link.second)
121-
inputTextField =
122-
textField()
123-
.applyToComponent {
124-
text = default ?: ""
125-
minimumSize = Dimension(520, -1)
126-
}.component
127-
}.layout(RowLayout.PARENT_GRID)
128-
row {
129-
cell() // To align with the text box.
130-
cell(
131-
ComponentPanelBuilder.createCommentComponent(comment, false, -1, true)
132-
.applyIf(isError) {
133-
apply {
134-
foreground = UIUtil.getErrorForeground()
135-
}
136-
},
137-
)
138-
}.layout(RowLayout.PARENT_GRID)
98+
class DialogUi(
99+
private val settings: CoderSettings,
100+
) {
101+
fun confirm(title: String, description: String): Boolean {
102+
var inputFromUser = false
103+
ApplicationManager.getApplication().invokeAndWait({
104+
AppIcon.getInstance().requestAttention(null, true)
105+
if (!dialog(
106+
title = title,
107+
panel = panel {
108+
row {
109+
label(description)
110+
}
111+
},
112+
).showAndGet()
113+
) {
114+
return@invokeAndWait
139115
}
140-
AppIcon.getInstance().requestAttention(null, true)
141-
if (!dialog(
142-
comment,
143-
panel = panel,
144-
focusedComponent = inputTextField,
145-
).showAndGet()
146-
) {
147-
return@invokeAndWait
148-
}
149-
inputFromUser = inputTextField.text
150-
}, ModalityState.any())
151-
return inputFromUser
152-
}
116+
inputFromUser = true
117+
}, ModalityState.defaultModalityState())
118+
return inputFromUser
119+
}
153120

154-
/**
155-
* Open a dialog for providing the token. Show any existing token so
156-
* the user can validate it if a previous connection failed.
157-
*
158-
* If we are not retrying and the user has not checked the existing
159-
* token box then also open a browser to the auth page.
160-
*
161-
* If the user has checked the existing token box then return the token
162-
* on disk immediately and skip the dialog (this will overwrite any
163-
* other existing token) unless this is a retry to avoid clobbering the
164-
* token that just failed.
165-
*/
166-
fun askToken(
167-
url: URL,
168-
token: Pair<String, Source>?,
169-
isRetry: Boolean,
170-
useExisting: Boolean,
171-
settings: CoderSettings,
172-
): Pair<String, Source>? {
173-
var (existingToken, tokenSource) = token ?: Pair("", Source.USER)
174-
val getTokenUrl = url.withPath("/login?redirect=%2Fcli-auth")
121+
fun ask(
122+
title: String,
123+
description: String,
124+
placeholder: String? = null,
125+
isError: Boolean = false,
126+
link: Pair<String, String>? = null,
127+
): String? {
128+
var inputFromUser: String? = null
129+
ApplicationManager.getApplication().invokeAndWait({
130+
lateinit var inputTextField: JBTextField
131+
AppIcon.getInstance().requestAttention(null, true)
132+
if (!dialog(
133+
title = title,
134+
panel = panel {
135+
row {
136+
if (link != null) browserLink(link.first, link.second)
137+
inputTextField =
138+
textField()
139+
.applyToComponent {
140+
this.text = placeholder
141+
minimumSize = Dimension(520, -1)
142+
}.component
143+
}.layout(RowLayout.PARENT_GRID)
144+
row {
145+
cell() // To align with the text box.
146+
cell(
147+
ComponentPanelBuilder.createCommentComponent(description, false, -1, true)
148+
.applyIf(isError) {
149+
apply {
150+
foreground = UIUtil.getErrorForeground()
151+
}
152+
},
153+
)
154+
}.layout(RowLayout.PARENT_GRID)
155+
},
156+
focusedComponent = inputTextField,
157+
).showAndGet()
158+
) {
159+
return@invokeAndWait
160+
}
161+
inputFromUser = inputTextField.text
162+
}, ModalityState.any())
163+
return inputFromUser
164+
}
165+
166+
private fun openUrl(url: URL) {
167+
BrowserUtil.browse(url)
168+
}
169+
170+
/**
171+
* Open a dialog for providing the token. Show any existing token so
172+
* the user can validate it if a previous connection failed.
173+
*
174+
* If we are not retrying and the user has not checked the existing
175+
* token box then also open a browser to the auth page.
176+
*
177+
* If the user has checked the existing token box then return the token
178+
* on disk immediately and skip the dialog (this will overwrite any
179+
* other existing token) unless this is a retry to avoid clobbering the
180+
* token that just failed.
181+
*/
182+
fun askToken(
183+
url: URL,
184+
token: Pair<String, Source>?,
185+
isRetry: Boolean,
186+
useExisting: Boolean,
187+
): Pair<String, Source>? {
188+
var (existingToken, tokenSource) = token ?: Pair("", Source.USER)
189+
val getTokenUrl = url.withPath("/login?redirect=%2Fcli-auth")
175190

176-
// On the first run either open a browser to generate a new token
177-
// or, if using an existing token, use the token on disk if it
178-
// exists otherwise assume the user already copied an existing
179-
// token and they will paste in.
180-
if (!isRetry) {
181-
if (!useExisting) {
182-
BrowserUtil.browse(getTokenUrl)
183-
} else {
184-
// Look on disk in case we already have a token, either in
185-
// the deployment's config or the global config.
186-
val tryToken = settings.token(url)
187-
if (tryToken != null && tryToken.first != existingToken) {
188-
return tryToken
191+
// On the first run either open a browser to generate a new token
192+
// or, if using an existing token, use the token on disk if it
193+
// exists otherwise assume the user already copied an existing
194+
// token and they will paste in.
195+
if (!isRetry) {
196+
if (!useExisting) {
197+
openUrl(getTokenUrl)
198+
} else {
199+
// Look on disk in case we already have a token, either in
200+
// the deployment's config or the global config.
201+
val tryToken = settings.token(url)
202+
if (tryToken != null && tryToken.first != existingToken) {
203+
return tryToken
204+
}
189205
}
190206
}
191-
}
192207

193-
// On subsequent tries or if not using an existing token, ask the user
194-
// for the token.
195-
val tokenFromUser =
196-
ask(
197-
CoderGatewayBundle.message(
198-
if (isRetry) {
199-
"gateway.connector.view.workspaces.token.rejected"
208+
// On subsequent tries or if not using an existing token, ask the user
209+
// for the token.
210+
val tokenFromUser =
211+
ask(
212+
title = "Session Token",
213+
description = if (isRetry) {
214+
"This token was rejected by ${url.host}."
200215
} else if (tokenSource == Source.CONFIG) {
201-
"gateway.connector.view.workspaces.token.injected-global"
216+
"This token was pulled from your global CLI config."
202217
} else if (tokenSource == Source.DEPLOYMENT_CONFIG) {
203-
"gateway.connector.view.workspaces.token.injected"
218+
"This token was pulled from your CLI config for ${url.host}."
204219
} else if (tokenSource == Source.LAST_USED) {
205-
"gateway.connector.view.workspaces.token.last-used"
220+
"This token was the last used token for ${url.host}."
206221
} else if (tokenSource == Source.QUERY) {
207-
"gateway.connector.view.workspaces.token.query"
222+
"This token was pulled from the Gateway link from ${url.host}."
208223
} else if (existingToken.isNotBlank()) {
209-
"gateway.connector.view.workspaces.token.comment"
224+
"The last used token for ${url.host} is shown above."
210225
} else {
211-
"gateway.connector.view.workspaces.token.none"
226+
"No existing token for ${url.host} found."
212227
},
213-
url.host,
214-
),
215-
isRetry,
216-
Pair(
217-
CoderGatewayBundle.message("gateway.connector.view.login.token.label"),
218-
getTokenUrl.toString(),
219-
),
220-
existingToken,
221-
)
222-
if (tokenFromUser.isNullOrBlank()) {
223-
return null
224-
}
225-
if (tokenFromUser != existingToken) {
226-
tokenSource = Source.USER
227-
}
228-
return Pair(tokenFromUser, tokenSource)
229-
}
230-
231-
fun askIDE(
232-
name: String,
233-
agent: WorkspaceAgent,
234-
workspace: Workspace,
235-
cli: CoderCLIManager,
236-
client: CoderRestClient,
237-
workspaces: List<Workspace>,
238-
): WorkspaceProjectIDE? {
239-
var data: WorkspaceProjectIDE? = null
240-
ApplicationManager.getApplication().invokeAndWait {
241-
val dialog =
242-
CoderWorkspaceStepDialog(
243-
name,
244-
CoderWorkspacesStepSelection(agent, workspace, cli, client, workspaces),
228+
placeholder = existingToken,
229+
link = Pair("Session Token:", getTokenUrl.toString()),
230+
isError = isRetry,
245231
)
246-
data = dialog.showAndGetData()
232+
if (tokenFromUser.isNullOrBlank()) {
233+
return null
234+
}
235+
if (tokenFromUser != existingToken) {
236+
tokenSource = Source.USER
237+
}
238+
return Pair(tokenFromUser, tokenSource)
247239
}
248-
return data
249240
}

0 commit comments

Comments
 (0)