Skip to content

Commit 5c46dfa

Browse files
committed
fix: allow user to provide cluster url only (#23654)
Signed-off-by: Andre Dietisheim <[email protected]> Assisted by: gemini-cli Assisted by: cursor Assisted by: qwen-code
1 parent 187ed91 commit 5c46dfa

File tree

10 files changed

+707
-141
lines changed

10 files changed

+707
-141
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ tasks {
149149

150150
withType<Test> {
151151
useJUnitPlatform()
152+
outputs.upToDateWhen { false } // Force tests to always run
152153
jvmArgs("-Dnet.bytebuddy.experimental=true", "-Dmockk.agent.global=false")
153154
testLogging {
154155
events("skipped", "failed", "standardOut", "standardError")

src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigMonitor.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ class KubeConfigMonitor(
3636
*/
3737
suspend fun onClustersCollected(action: suspend (clusters: List<Cluster>) -> Unit) {
3838
logger.info("Setting up SharedFlow collection for cluster updates")
39-
clusters.collect { clusters ->
40-
logger.info("Found ${clusters.size} clusters")
41-
action(clusters)
39+
clusters.collect { collected ->
40+
logger.info("Found ${collected.size} clusters")
41+
action(collected)
4242
}
4343
}
4444

src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUtils.kt

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,12 @@ object KubeConfigUtils {
5959
val userToken = KubeConfigNamedUser.getUserTokenForCluster(clusterEntry.name, kubeConfig)
6060

6161
return Cluster(
62-
id = generateClusterId(clusterEntry.name, clusterEntry.cluster.server),
63-
name = clusterEntry.name,
6462
url = clusterEntry.cluster.server,
63+
name = clusterEntry.name,
6564
token = userToken
6665
)
6766
}
6867

69-
private fun generateClusterId(clusterName: String, apiServerUrl: String): String {
70-
return "$clusterName@${
71-
apiServerUrl
72-
.removePrefix("https://")
73-
.removePrefix("http://")
74-
}"
75-
}
76-
7768
private fun getEnvConfigs(): List<Path> {
7869
val env = System.getenv("KUBECONFIG")
7970
?: EnvironmentUtil.getValue("KUBECONFIG")

src/main/kotlin/com/redhat/devtools/gateway/openshift/Cluster.kt

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,49 @@
1111
*/
1212
package com.redhat.devtools.gateway.openshift
1313

14+
import java.net.URI
15+
import java.net.URISyntaxException
16+
1417
data class Cluster(
15-
val id: String,
1618
val name: String,
1719
val url: String,
18-
val token: String?
20+
val token: String? = null
1921
) {
22+
23+
companion object {
24+
fun fromUrl(url: String): Cluster? {
25+
return try {
26+
val name = toName(url)
27+
if (name == null) {
28+
null
29+
} else {
30+
Cluster(name, url) // Use host directly from URI
31+
}
32+
} catch(_: URISyntaxException) {
33+
null
34+
}
35+
}
36+
37+
private fun toName(url: String): String? {
38+
return try {
39+
val uri = URI(url)
40+
uri.host
41+
} catch (_: URISyntaxException) {
42+
null
43+
}
44+
}
45+
}
46+
47+
val id: String
48+
get() {
49+
return "$name@${
50+
url
51+
.removePrefix("https://")
52+
.removePrefix("http://")
53+
}"
54+
}
55+
2056
override fun toString(): String {
2157
return "$name ($url)"
2258
}
23-
}
59+
}

src/main/kotlin/com/redhat/devtools/gateway/view/steps/DevSpacesServerStepView.kt

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ import com.redhat.devtools.gateway.util.message
3333
import com.redhat.devtools.gateway.view.ui.Dialogs
3434
import com.redhat.devtools.gateway.view.ui.FilteringComboBox
3535
import com.redhat.devtools.gateway.view.ui.PasteClipboardMenu
36+
import com.redhat.devtools.gateway.view.ui.getAllElements
37+
import com.redhat.devtools.gateway.view.ui.requestInitialFocus
3638
import kotlinx.coroutines.*
39+
import java.awt.event.ItemEvent
3740
import javax.swing.JTextField
3841
import javax.swing.event.DocumentEvent
3942
import javax.swing.event.DocumentListener
@@ -57,13 +60,10 @@ class DevSpacesServerStepView(
5760
private var tfServer =
5861
FilteringComboBox.create(
5962
{ it?.toString() ?: "" },
60-
{ name, clusters -> clusters.firstOrNull { it.name == name } },
61-
Cluster::class.java
62-
) { cluster ->
63-
if (cluster != null) {
64-
tfToken.text = cluster.token ?: ""
65-
}
66-
}.apply {
63+
{ Cluster.fromUrl(it) }
64+
)
65+
.apply {
66+
addItemListener(::onClusterSelected)
6767
PasteClipboardMenu.addTo(this.editor.editorComponent as JTextField)
6868
}
6969
override val component = panel {
@@ -81,7 +81,9 @@ class DevSpacesServerStepView(
8181
}.apply {
8282
background = WelcomeScreenUIManager.getMainAssociatedComponentBackground()
8383
border = JBUI.Borders.empty(8)
84+
requestInitialFocus(tfServer) // tfServer.focused() does not work
8485
}
86+
8587
override val nextActionText = DevSpacesBundle.message("connector.wizard_step.openshift_connection.button.next")
8688
override val previousActionText =
8789
DevSpacesBundle.message("connector.wizard_step.openshift_connection.button.previous")
@@ -90,6 +92,17 @@ class DevSpacesServerStepView(
9092
startKubeconfigMonitor()
9193
}
9294

95+
private fun onClusterSelected(event: ItemEvent) {
96+
if (event.stateChange == ItemEvent.SELECTED) {
97+
(event.item as? Cluster)?.let { selectedCluster ->
98+
val allClusters = tfServer.getAllElements()
99+
if (allClusters.contains(selectedCluster)) {
100+
tfToken.text = selectedCluster.token
101+
}
102+
}
103+
}
104+
}
105+
93106
private fun onTokenChanged(): DocumentListener = object : DocumentListener {
94107
override fun insertUpdate(event: DocumentEvent) {
95108
enableNextButton?.invoke()
@@ -149,11 +162,8 @@ class DevSpacesServerStepView(
149162
}
150163

151164
override fun isNextEnabled(): Boolean {
152-
return if (tfServer.selectedItem == null) {
153-
false
154-
} else {
155-
!tfToken.text.isNullOrBlank()
156-
}
165+
return tfServer.selectedItem != null
166+
&& tfToken.text.isNotEmpty()
157167
}
158168

159169
private fun setClusters(clusters: List<Cluster>) {

src/main/kotlin/com/redhat/devtools/gateway/view/ui/FilteringComboBox.kt

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ package com.redhat.devtools.gateway.view.ui
1313

1414
import com.intellij.openapi.util.Key
1515
import java.awt.event.ActionListener
16+
import java.awt.event.FocusAdapter
17+
import java.awt.event.FocusEvent
1618
import java.awt.event.ItemEvent
1719
import java.awt.event.KeyAdapter
1820
import java.awt.event.KeyEvent
@@ -27,24 +29,22 @@ object FilteringComboBox {
2729

2830
fun <T> create(
2931
toString: (T?) -> String = { it.toString() },
30-
matchItem: ((expression: String, toMatch: List<T>) -> T?)? = null,
31-
type: Class<T>,
32-
onItemSelected: (T?) -> Unit
32+
toElement: (String) -> T?
3333
): JComboBox<T> {
3434
val comboBox = JComboBox<T>()
3535
comboBox.isEditable = true
36-
comboBox.editor = UnsettableComboBoxEditor(comboBox, toString, matchItem)
36+
val editor = UnsettableComboBoxEditor(comboBox, toString, toElement)
37+
comboBox.editor = editor
3738
popupOpened.reset(comboBox)
3839

3940
comboBox.model = FilteringComboBoxModel<T>()
4041
comboBox.setRenderer(onListItemRendered<T>(toString))
4142

4243
comboBox.addPopupMenuListener(onPopupVisible(comboBox, toString))
4344

44-
val editor = getEditor(comboBox)
45-
editor?.addKeyListener(onKeyPressed(editor, comboBox, toString))
46-
47-
comboBox.addItemListener(onItemSelected(editor, onItemSelected, toString, type))
45+
val editorComponent = editor.editorComponent
46+
editorComponent.addKeyListener(onKeyPressed(editorComponent, comboBox, toString))
47+
comboBox.addItemListener(onItemSelected(editorComponent, toString))
4848
return comboBox
4949
}
5050

@@ -54,7 +54,7 @@ object FilteringComboBox {
5454
): PopupMenuListener = object : PopupMenuListener {
5555
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
5656
val allItems = comboBox.filteringModel().getAllItems()
57-
val editorText = getEditor(comboBox)?.text ?: ""
57+
val editorText = getEditorComponent(comboBox)?.text ?: ""
5858
val visible = if (popupOpened.isProgrammatic(comboBox)) {
5959
filterItems(editorText, allItems, toString)
6060
} else {
@@ -88,26 +88,14 @@ object FilteringComboBox {
8888

8989
private fun <T> onItemSelected(
9090
editorComponent: JTextComponent?,
91-
onItemUpdated: (T?) -> Unit,
92-
toString: (T?) -> String,
93-
type: Class<T>
94-
): (ItemEvent) -> Unit {
95-
return { event ->
96-
// only process if item is actively selected (not when highlighted)
97-
if (event.stateChange == ItemEvent.SELECTED) {
98-
try {
99-
val selectedItem = type.cast(event.item)
100-
if (selectedItem != null) {
101-
onItemUpdated.invoke(selectedItem)
102-
editorComponent?.text = toString(selectedItem) // circumvent ComboboxEditor
103-
SwingUtilities.invokeLater {
104-
editorComponent?.requestFocusInWindow()
105-
editorComponent?.selectAll()
106-
}
107-
}
108-
} catch(_: ClassCastException) {
109-
// item is not of type T
110-
}
91+
toString: (T?) -> String
92+
): (ItemEvent) -> Unit = { event ->
93+
if (editorComponent != null
94+
&& event.stateChange == ItemEvent.SELECTED) {
95+
(event.item as? T)?.let { selectedItem ->
96+
editorComponent.text = toString(selectedItem)
97+
editorComponent.caret.isSelectionVisible = true // allow selectAll() with no focus
98+
editorComponent.selectAll()
11199
}
112100
}
113101
}
@@ -132,7 +120,7 @@ object FilteringComboBox {
132120
items: List<T>
133121
) {
134122
popupOpened.setProgrammatic(true, comboBox)
135-
val editor = getEditor(comboBox)
123+
val editor = getEditorComponent(comboBox)
136124
val selection = Selection(comboBox.editor.editorComponent as? JTextComponent).backup()
137125

138126
val currentTextInEditor = editor?.text ?: ""
@@ -173,7 +161,7 @@ object FilteringComboBox {
173161
private class UnsettableComboBoxEditor<T>(
174162
private val comboBox: JComboBox<T>,
175163
private val toString: (T?) -> String,
176-
private val matchItem: ((expression: String, toMatch: List<T>) -> T?)?
164+
private val toElement: (String) -> T?
177165
) : ComboBoxEditor {
178166
private val textField = JTextField()
179167

@@ -187,11 +175,20 @@ object FilteringComboBox {
187175
*/
188176
}
189177

190-
override fun getItem(): Any? {
178+
override fun getItem(): T? {
191179
val text = textField.text
192180
val allItems = comboBox.filteringModel().getAllItems()
193-
val matchingItem = allItems.find { toString(it) == text }
194-
return matchingItem ?: (matchItem?.invoke(text, allItems) ?: text)
181+
182+
/*
183+
* item =
184+
* 1. selected item that is matching text field OR
185+
* 2. item present in (combobox-) list if it's matching text field OR
186+
* 3. create an new item with content of text field OR
187+
* 4. null
188+
*/
189+
return (comboBox.selectedItem as? T)?.takeIf { toString(it) == text } // selected item = item not present in list
190+
?: allItems.find { toString(it) == text } // item present in list
191+
?: toElement(text) // no new item, not item in list -> create a new item
195192
}
196193

197194
override fun selectAll() {
@@ -207,7 +204,7 @@ object FilteringComboBox {
207204
}
208205
}
209206

210-
private fun <T> getEditor(comboBox: JComboBox<T>): JTextField? {
207+
private fun <T> getEditorComponent(comboBox: JComboBox<T>): JTextField? {
211208
return comboBox.editor.editorComponent as? JTextField
212209
}
213210

@@ -250,7 +247,7 @@ object FilteringComboBox {
250247
}
251248
}
252249

253-
private class PopupOpened() {
250+
private class PopupOpened {
254251

255252
private val key = Key.create<Boolean>("isPopupProgrammatic")
256253

src/main/kotlin/com/redhat/devtools/gateway/view/ui/SwingUtils.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@
1111
*/
1212
package com.redhat.devtools.gateway.view.ui
1313

14+
import com.intellij.openapi.application.invokeLater
15+
import com.intellij.ui.AncestorListenerAdapter
16+
import com.redhat.devtools.gateway.openshift.Cluster
1417
import java.awt.event.MouseAdapter
1518
import java.awt.event.MouseEvent
19+
import javax.swing.JComboBox
1620
import javax.swing.JList
21+
import javax.swing.JPanel
22+
import javax.swing.event.AncestorEvent
1723

1824
fun <T> JList<T>.onDoubleClick(action: (T) -> Unit) {
1925
addMouseListener(object : MouseAdapter() {
@@ -23,4 +29,20 @@ fun <T> JList<T>.onDoubleClick(action: (T) -> Unit) {
2329
}
2430
}
2531
})
26-
}
32+
}
33+
34+
fun <T> JComboBox<T>.getAllElements(): List<T?> {
35+
return (0 until model.size)
36+
.map { index -> model.getElementAt(index) }.toList()
37+
}
38+
39+
fun JPanel.requestInitialFocus(component: JComboBox<Cluster>) {
40+
addAncestorListener(object : AncestorListenerAdapter() {
41+
override fun ancestorAdded(event: AncestorEvent?) {
42+
invokeLater {
43+
component.requestFocusInWindow()
44+
}
45+
removeAncestorListener(this)
46+
}
47+
})
48+
}

src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigMonitorTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class KubeConfigMonitorTest {
5656
@Test
5757
fun `#start should initially parse and publish clusters`() = testScope.runTest {
5858
// given
59-
val cluster1 = Cluster("id1", "skywalker", "url1", null)
59+
val cluster1 = Cluster("skywalker", "url1", null)
6060
every { mockKubeConfigBuilder.getAllConfigs() } returns listOf(kubeconfigPath1)
6161
every { mockKubeConfigBuilder.getClusters(listOf(kubeconfigPath1)) } returns listOf(cluster1)
6262

@@ -73,8 +73,8 @@ class KubeConfigMonitorTest {
7373
@Test
7474
fun `#onFileChanged should reparse and publish updated clusters`() = testScope.runTest {
7575
// given
76-
val cluster1 = Cluster("id1", "skywalker", "url1", null)
77-
val cluster1Updated = Cluster("id1", "skywalker", "url1", "token1")
76+
val cluster1 = Cluster("skywalker", "url1", null)
77+
val cluster1Updated = Cluster("skywalker", "url1", "token1")
7878

7979
every { mockKubeConfigBuilder.getAllConfigs() } returns listOf(kubeconfigPath1)
8080
every { mockKubeConfigBuilder.getClusters(listOf(kubeconfigPath1)) } returns listOf(cluster1)
@@ -96,8 +96,8 @@ class KubeConfigMonitorTest {
9696
@Test
9797
fun `#updateMonitoredPaths should add and remove files based on KUBECONFIG env var`() = testScope.runTest {
9898
// given
99-
val cluster1 = Cluster("id1", "skywalker", "url1", null)
100-
val cluster2 = Cluster("id2", "obi-wan", "url2", null)
99+
val cluster1 = Cluster("skywalker", "url1")
100+
val cluster2 = Cluster("obi-wan", "url2")
101101

102102
// Initial KUBECONFIG
103103
every { mockKubeConfigBuilder.getAllConfigs() } returns listOf(kubeconfigPath1)

0 commit comments

Comments
 (0)