Skip to content

Commit 21f4c16

Browse files
committed
replaced custom progress dialog with platform means (#23547)
Signed-off-by: Andre Dietisheim <[email protected]>
1 parent f9b8250 commit 21f4c16

File tree

2 files changed

+104
-194
lines changed

2 files changed

+104
-194
lines changed

src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnectionProvider.kt

Lines changed: 102 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,12 @@
1212
package com.redhat.devtools.gateway
1313

1414
import com.intellij.openapi.application.ApplicationManager
15-
import com.intellij.openapi.application.EDT
16-
import com.intellij.openapi.application.ModalityState
1715
import com.intellij.openapi.diagnostic.thisLogger
18-
import com.intellij.openapi.progress.ProcessCanceledException
1916
import com.intellij.openapi.progress.ProgressIndicator
20-
import com.intellij.openapi.ui.DialogWrapper
21-
import com.intellij.openapi.ui.Messages
17+
import com.intellij.openapi.progress.ProgressManager
18+
import com.intellij.openapi.progress.Task
2219
import com.intellij.ui.dsl.builder.Align.Companion.CENTER
2320
import com.intellij.ui.dsl.builder.panel
24-
import com.intellij.util.ui.JBUI
2521
import com.jetbrains.gateway.api.ConnectionRequestor
2622
import com.jetbrains.gateway.api.GatewayConnectionHandle
2723
import com.jetbrains.gateway.api.GatewayConnectionProvider
@@ -34,18 +30,11 @@ import com.redhat.devtools.gateway.util.messageWithoutPrefix
3430
import com.redhat.devtools.gateway.view.ui.Dialogs
3531
import io.kubernetes.client.openapi.ApiException
3632
import kotlinx.coroutines.CompletableDeferred
37-
import kotlinx.coroutines.Dispatchers
38-
import kotlinx.coroutines.withContext
39-
import java.awt.BorderLayout
40-
import java.awt.Dimension
41-
import javax.swing.Action
42-
import javax.swing.Box
43-
import javax.swing.BoxLayout
33+
import kotlinx.coroutines.ExperimentalCoroutinesApi
34+
import kotlinx.coroutines.suspendCancellableCoroutine
4435
import javax.swing.JComponent
45-
import javax.swing.JLabel
46-
import javax.swing.JPanel
47-
import javax.swing.JProgressBar
4836
import javax.swing.Timer
37+
import kotlin.coroutines.resume
4938

5039
private const val DW_NAMESPACE = "dwNamespace"
5140
private const val DW_NAME = "dwName"
@@ -57,107 +46,118 @@ private const val DW_NAME = "dwName"
5746
*/
5847
class DevSpacesConnectionProvider : GatewayConnectionProvider {
5948

49+
@OptIn(ExperimentalCoroutinesApi::class)
6050
@Suppress("UnstableApiUsage")
6151
override suspend fun connect(
6252
parameters: Map<String, String>,
6353
requestor: ConnectionRequestor
6454
): GatewayConnectionHandle? {
65-
val indicator = StartupProgressIndicator("Connecting to Remote IDE...")
66-
ApplicationManager.getApplication().invokeAndWait { indicator.show() }
67-
68-
return withContext(Dispatchers.IO) {
69-
try {
70-
indicator.setText("Connecting to DevSpace...")
71-
indicator.setIndeterminate(true)
72-
73-
val handle = doConnect(parameters, indicator)
74-
75-
val thinClient = handle.clientHandle
76-
?: throw RuntimeException("Failed to obtain ThinClientHandle")
77-
78-
indicator.setText("Waiting for remote IDE to start...")
79-
indicator.setText2("Launching environment and initializing IDE window…")
80-
81-
// Observe signals on thinClient to detect ready/error
82-
val readyDeferred = CompletableDeferred<GatewayConnectionHandle?>()
83-
84-
thinClient.onClientPresenceChanged.advise(thinClient.lifetime,
85-
onClientPresenceChanged(readyDeferred, indicator, handle))
86-
thinClient.clientFailedToOpenProject.advise(thinClient.lifetime,
87-
onClientFailedToOpenProject(readyDeferred, indicator))
88-
thinClient.clientClosed.advise(thinClient.lifetime,
89-
onClientClosed(readyDeferred, indicator))
55+
return suspendCancellableCoroutine { cont ->
56+
ProgressManager.getInstance().runProcessWithProgressSynchronously(
57+
{
58+
val indicator = ProgressManager.getInstance().progressIndicator
59+
try {
60+
indicator.isIndeterminate = true
61+
indicator.text = "Connecting to DevSpace..."
62+
63+
val handle = doConnect(parameters, indicator)
64+
val thinClient =
65+
handle.clientHandle ?: throw RuntimeException("Failed to obtain ThinClientHandle")
66+
67+
indicator.text = "Waiting for remote IDE to start..."
68+
69+
val ready = CompletableDeferred<GatewayConnectionHandle?>()
70+
71+
thinClient.onClientPresenceChanged.advise(
72+
thinClient.lifetime,
73+
onClientPresenceChanged(ready, indicator, handle)
74+
)
75+
thinClient.clientFailedToOpenProject.advise(
76+
thinClient.lifetime,
77+
onClientFailedToOpenProject(ready, indicator)
78+
)
79+
thinClient.clientClosed.advise(
80+
thinClient.lifetime,
81+
onClientClosed(ready, indicator)
82+
)
83+
ready.invokeOnCompletion { error ->
84+
if (error == null) {
85+
cont.resume(ready.getCompleted())
86+
} else {
87+
cont.resumeWith(Result.failure(error))
88+
}
89+
}
90+
} catch (e: ApiException) {
91+
indicator.text = "Connection failed"
92+
runDelayed(2000, { indicator.stop() })
93+
if (!(handleUnauthorizedError(e) || handleNotFoundError(e))) {
94+
// Dialogs.error is suspend — use a blocking non-suspending dialog instead
95+
Dialogs.error(
96+
e.messageWithoutPrefix() ?: "Could not connect to workspace.",
97+
"Connection Error"
98+
)
99+
}
90100

91-
readyDeferred.await()
92-
} catch (e: ApiException) {
93-
indicator.setText("Connection failed")
94-
delayedClose(indicator)
95-
if (!(handleUnauthorizedError(e) || handleNotFoundError(e))) {
96-
Dialogs.error(e.messageWithoutPrefix() ?: "Could not connect to workspace.", "Connection Error")
97-
}
98-
null
99-
} catch (e: Exception) {
100-
indicator.setText("Unexpected error: ${e.message}")
101-
delayedClose(indicator)
102-
Dialogs.error(e.messageWithoutPrefix() ?: "Could not connect to workspace.", "Connection Error")
101+
if (cont.isActive) cont.resume(null)
102+
} catch (e: Exception) {
103+
runDelayed(2000) { indicator.stop() }
104+
Dialogs.error(
105+
e.message ?: "Could not connect to workspace.",
106+
"Connection Error"
107+
)
108+
cont.resume(null)
109+
}
110+
},
111+
"Connecting to Remote IDE...",
112+
true,
103113
null
104-
} finally {
105-
withContext(Dispatchers.EDT) {
106-
if (indicator.isShowing) indicator.close(DialogWrapper.OK_EXIT_CODE)
107-
}
108-
}
114+
)
109115
}
110116
}
111117

112-
private fun delayedClose(indicator: StartupProgressIndicator) {
113-
Timer(2000) {
114-
indicator.close(DialogWrapper.CANCEL_EXIT_CODE)
115-
}.start()
116-
}
117-
118118
private fun onClientPresenceChanged(
119-
readyDeferred: CompletableDeferred<GatewayConnectionHandle?>,
120-
indicator: StartupProgressIndicator,
119+
ready: CompletableDeferred<GatewayConnectionHandle?>,
120+
indicator: ProgressIndicator,
121121
handle: GatewayConnectionHandle
122122
): (Unit) -> Unit = {
123123
ApplicationManager.getApplication().invokeLater {
124-
if (!readyDeferred.isCompleted) {
125-
indicator.setText("Remote IDE has started successfully")
126-
indicator.setText2("Opening project window…")
127-
Timer(3000) {
128-
indicator.close(DialogWrapper.OK_EXIT_CODE)
129-
readyDeferred.complete(handle)
130-
}.start()
124+
if (!ready.isCompleted) {
125+
indicator.text = "Remote IDE has started successfully"
126+
indicator.text2 = "Opening project window…"
127+
runDelayed(3000) {
128+
indicator.stop()
129+
ready.complete(handle)
130+
}
131131
}
132132
}
133133
}
134134

135135
private fun onClientFailedToOpenProject(
136-
readyDeferred: CompletableDeferred<GatewayConnectionHandle?>,
137-
indicator: StartupProgressIndicator
136+
ready: CompletableDeferred<GatewayConnectionHandle?>,
137+
indicator: ProgressIndicator
138138
): (Int) -> Unit = { errorCode ->
139139
ApplicationManager.getApplication().invokeLater {
140-
if (!readyDeferred.isCompleted) {
141-
indicator.setText("Failed to open remote project (code: $errorCode)")
142-
Timer(2000) {
143-
indicator.close(DialogWrapper.CANCEL_EXIT_CODE)
144-
readyDeferred.complete(null)
145-
}.start()
140+
if (!ready.isCompleted) {
141+
indicator.text = "Failed to open remote project (code: $errorCode)"
142+
runDelayed(2000) {
143+
indicator.stop()
144+
ready.complete(null)
145+
}
146146
}
147147
}
148148
}
149149

150150
private fun onClientClosed(
151-
readyDeferred: CompletableDeferred<GatewayConnectionHandle?>,
152-
indicator: StartupProgressIndicator
151+
ready: CompletableDeferred<GatewayConnectionHandle?>,
152+
indicator: ProgressIndicator
153153
): (Unit) -> Unit = {
154154
ApplicationManager.getApplication().invokeLater {
155-
if (!readyDeferred.isCompleted) {
156-
indicator.setText("Remote IDE closed unexpectedly.")
157-
Timer(2000) {
158-
indicator.close(DialogWrapper.CANCEL_EXIT_CODE)
159-
readyDeferred.complete(null)
160-
}.start()
155+
if (!ready.isCompleted) {
156+
indicator.text = "Remote IDE closed unexpectedly."
157+
runDelayed(2000) {
158+
indicator.stop()
159+
ready.complete(null)
160+
}
161161
}
162162
}
163163
}
@@ -166,11 +166,11 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
166166
@Throws(IllegalArgumentException::class)
167167
private fun doConnect(
168168
parameters: Map<String, String>,
169-
indicator: ProgressIndicator? = null
169+
indicator: ProgressIndicator
170170
): GatewayConnectionHandle {
171171
thisLogger().debug("Launched Dev Spaces connection provider", parameters)
172172

173-
indicator?.text2 = "Preparing connection environment…"
173+
indicator.text2 = "Preparing connection environment…"
174174

175175
val dwNamespace = parameters[DW_NAMESPACE]
176176
if (dwNamespace.isNullOrBlank()) {
@@ -184,17 +184,17 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
184184
throw IllegalArgumentException("Query parameter \"$DW_NAME\" is missing")
185185
}
186186

187-
indicator?.text2 = "Initializing Kubernetes connection…"
187+
indicator.text2 = "Initializing Kubernetes connection…"
188188
val ctx = DevSpacesContext()
189189
ctx.client = OpenShiftClientFactory().create()
190190

191-
indicator?.text2 = "Fetching DevWorkspace “$dwName” from namespace “$dwNamespace”…"
191+
indicator.text2 = "Fetching DevWorkspace “$dwName” from namespace “$dwNamespace”…"
192192
ctx.devWorkspace = DevWorkspaces(ctx.client).get(dwNamespace, dwName)
193193

194-
indicator?.text2 = "Establishing remote IDE connection…"
194+
indicator.text2 = "Establishing remote IDE connection…"
195195
val thinClient = DevSpacesConnection(ctx).connect({}, {}, {})
196196

197-
indicator?.text2 = "Connection established successfully."
197+
indicator.text2 = "Connection established successfully."
198198
return DevSpacesConnectionHandle(thinClient.lifetime, thinClient, { createComponent(dwName) }, dwName)
199199
}
200200

@@ -220,7 +220,7 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
220220
}
221221
}
222222

223-
private suspend fun handleUnauthorizedError(err: ApiException): Boolean {
223+
private fun handleUnauthorizedError(err: ApiException): Boolean {
224224
if (!err.isUnauthorized()) return false
225225

226226
val tokenNote = if (KubeConfigBuilder.isTokenAuthUsed())
@@ -234,7 +234,7 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
234234
return true
235235
}
236236

237-
private suspend fun handleNotFoundError(err: ApiException): Boolean {
237+
private fun handleNotFoundError(err: ApiException): Boolean {
238238
if (!err.isNotFound()) return false
239239

240240
val message = """
@@ -248,97 +248,10 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
248248
return true
249249
}
250250

251-
// Common progress dialog to monitor the connection and the renote IDE readiness
252-
private class StartupProgressIndicator (
253-
initialTitle: String = "Progress Indicator"
254-
) : DialogWrapper(false), ProgressIndicator {
255-
private val progressBar = JProgressBar().apply { isIndeterminate = true }
256-
private val mainTextLabel = JLabel("Initializing...")
257-
private val subTextLabel = JLabel("")
258-
259-
@Volatile
260-
private var canceled = false
261-
262-
init {
263-
title = initialTitle
264-
isModal = false
265-
isResizable = true
266-
init()
267-
}
268-
269-
fun setIndeterminateValue(indeterminate: Boolean) = ApplicationManager.getApplication().invokeLater {
270-
progressBar.isIndeterminate = indeterminate
271-
}
272-
273-
fun setFractionValue(fraction: Double) = ApplicationManager.getApplication().invokeLater {
274-
progressBar.isIndeterminate = false
275-
progressBar.value = (fraction * 100).toInt()
276-
}
277-
278-
override fun createCenterPanel(): JComponent = JPanel(BorderLayout(5, 5)).apply {
279-
border = JBUI.Borders.empty(10)
280-
layout = BoxLayout(this, BoxLayout.Y_AXIS)
281-
add(mainTextLabel)
282-
add(Box.createVerticalStrut(4))
283-
add(subTextLabel)
284-
add(Box.createVerticalStrut(8))
285-
add(progressBar)
286-
}
287-
288-
override fun createActions(): Array<Action> = emptyArray()
289-
override fun getPreferredFocusedComponent() = mainTextLabel
290-
override fun getPreferredSize(): Dimension = Dimension(400, 120)
291-
override fun getText(): String = mainTextLabel.text
292-
override fun getText2(): String = subTextLabel.text
293-
294-
override fun setText(text: String?) = ApplicationManager.getApplication().invokeLater {
295-
mainTextLabel.text = shortenMessage(text ?: "")
296-
subTextLabel.text = ""
297-
}
298-
299-
override fun setText2(text: String?) = ApplicationManager.getApplication().invokeLater {
300-
subTextLabel.text = shortenMessage(text ?: "")
301-
}
302-
303-
override fun setIndeterminate(indeterminate: Boolean) = setIndeterminateValue(indeterminate)
304-
override fun isIndeterminate(): Boolean = progressBar.isIndeterminate
305-
306-
override fun setFraction(fraction: Double) = setFractionValue(fraction)
307-
override fun getFraction(): Double = progressBar.value / 100.0
308-
309-
override fun cancel() {
310-
canceled = true
311-
close(CANCEL_EXIT_CODE)
312-
}
313-
314-
override fun isCanceled(): Boolean = canceled
315-
316-
override fun start() {}
317-
override fun stop() {}
318-
override fun isRunning(): Boolean = isShowing
319-
override fun pushState() {}
320-
override fun popState() {}
321-
322-
override fun getModalityState(): ModalityState {
323-
return if (isShowing) ModalityState.current() else ModalityState.nonModal()
324-
}
325-
326-
override fun setModalityProgress(progressIndicator: ProgressIndicator?) {}
327-
328-
override fun checkCanceled() {
329-
if (isCanceled) throw ProcessCanceledException()
330-
}
331-
332-
override fun isPopupWasShown(): Boolean {
333-
return isShowing
334-
}
335-
336-
private fun shortenMessage(message: String, maxLength: Int = 100): String {
337-
if (message.length <= maxLength) return message
338-
339-
val head = message.take(maxLength / 2 - 3)
340-
val tail = message.takeLast(maxLength / 2 - 3)
341-
return "$head$tail"
342-
}
251+
private fun runDelayed(delay: Int, runnable: () -> Unit) {
252+
Timer(delay) {
253+
runnable.invoke()
254+
}.start()
343255
}
256+
344257
}

0 commit comments

Comments
 (0)