From 76066cb8e5932424345412386bcd36e6af0f368a Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Wed, 22 Jan 2025 14:55:57 +0100 Subject: [PATCH 01/12] Moved plugin version check to a background thread. --- .../metalbear/mirrord/MirrordExecManager.kt | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt index 7ea1343f..3493846b 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt @@ -7,6 +7,9 @@ import com.intellij.openapi.application.WriteAction import com.intellij.openapi.components.service import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task import com.intellij.openapi.util.SystemInfo /** @@ -84,6 +87,27 @@ class MirrordExecManager(private val service: MirrordProjectService) { return wslDistribution?.getWslPath(path) ?: path } + /** + * Starts a plugin version check in a background thread. + */ + private fun dispatchPluginVersionCheck() { + MirrordLogger.logger.debug("Plugin version check triggered") + + ProgressManager.getInstance().run(object : Task.Backgroundable(service.project, "mirrord plugin version check", true) { + override fun run(indicator: ProgressIndicator) { + service.versionCheck.checkVersion() + } + + override fun onThrowable(error: Throwable) { + MirrordLogger.logger.debug("Failed to check plugin updates", error) + service.notifier.notifySimple( + "Failed to check for plugin update", + NotificationType.WARNING + ) + } + }) + } + private fun prepareStart( wslDistribution: WSLDistribution?, product: String, @@ -103,16 +127,7 @@ class MirrordExecManager(private val service: MirrordProjectService) { throw MirrordError("can't use on Windows without WSL") } - MirrordLogger.logger.debug("version check trigger") - try { - service.versionCheck.checkVersion() // TODO makes an HTTP request, move to background - } catch (e: Throwable) { - MirrordLogger.logger.debug("Failed checking plugin updates", e) - service.notifier.notifySimple( - "Couldn't check for plugin update", - NotificationType.WARNING - ) - } + dispatchPluginVersionCheck() val mirrordConfigPath = projectEnvVars?.get(CONFIG_ENV_NAME)?.let { if (it.contains("\$ProjectPath\$")) { @@ -181,7 +196,7 @@ class MirrordExecManager(private val service: MirrordProjectService) { /** * Starts mirrord, shows dialog for selecting pod if target is not set and returns env to set. * - * @param envVars Contains both system env vars, and (active) launch settings, see `Wrapper`. + * @param projectEnvVars Contains both system env vars, and (active) launch settings, see `Wrapper`. * @return extra environment variables to set for the executed process and path to the patched executable. * null if mirrord service is disabled * @throws ProcessCanceledException if the user cancelled From 3bde50e74475b81795259ae9dffbf9be4c565e1c Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Wed, 22 Jan 2025 15:50:03 +0100 Subject: [PATCH 02/12] Handle rich output in MirrordApi --- .../com/metalbear/mirrord/MirrordApi.kt | 70 ++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt index 3e441f4b..cabc5d48 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt @@ -146,12 +146,47 @@ private const val MIRRORD_FOR_TEAMS_INVITE_AFTER = 100 */ private const val MIRRORD_FOR_TEAMS_INVITE_EVERY = 30 +/** + * Name of the environment variable used to trigger rich output of `mirrord ls`. + */ +private const val MIRRORD_LS_RICH_OUTPUT_ENV = "MIRRORD_LS_RICH_OUTPUT" + /** * Interact with mirrord CLI using this API. */ class MirrordApi(private val service: MirrordProjectService, private val projectEnvVars: Map?) { - private class MirrordLsTask(cli: String, projectEnvVars: Map?) : MirrordCliTask>(cli, "ls", null, projectEnvVars) { - override fun compute(project: Project, process: Process, setText: (String) -> Unit): List { + data class FoundTarget(val path: String, val available: Boolean) + private data class RichOutput( + val targets: Array, + @SerializedName("current_namespace") val currentNamespace: String, + val namespaces: Array, + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RichOutput + + if (!targets.contentEquals(other.targets)) return false + if (currentNamespace != other.currentNamespace) return false + if (!namespaces.contentEquals(other.namespaces)) return false + + return true + } + + override fun hashCode(): Int { + var result = targets.contentHashCode() + result = 31 * result + currentNamespace.hashCode() + result = 31 * result + namespaces.contentHashCode() + return result + } + } + + class MirrordLsOutput(val targets: List, val currentNamespace: String?, val namespaces: List?) + + + private class MirrordLsTask(cli: String, projectEnvVars: Map?, namespace: String?) : MirrordCliTask(cli, "ls", namespace?.let { listOf("-n", it) }, projectEnvVars) { + override fun compute(project: Project, process: Process, setText: (String) -> Unit): MirrordLsOutput { setText("mirrord is listing targets...") process.waitFor() @@ -163,13 +198,31 @@ class MirrordApi(private val service: MirrordProjectService, private val project val data = process.inputStream.bufferedReader().readText() MirrordLogger.logger.debug("parsing mirrord ls output: $data") - val pods = SafeParser().parse(data, Array::class.java).toMutableList() + val output = try { + val richOutput = SafeParser().parse(data, RichOutput::class.java) + MirrordLsOutput(richOutput.targets.toList(), richOutput.currentNamespace, richOutput.namespaces.toList()) + } catch (error: Throwable) { + val simpleOutput = SafeParser().parse(data, Array::class.java) + MirrordLsOutput( + simpleOutput.map { FoundTarget(it, true) }, + null, + null, + ) + } - if (pods.isEmpty()) { - project.service().notifier.notifySimple("No mirrord target available in the configured namespace. You can run targetless, or set a different target namespace or kubeconfig in the mirrord configuration file.", NotificationType.INFORMATION) + if (output.targets.isEmpty()) { + project + .service() + .notifier + .notifySimple( + "No mirrord target available in the configured namespace. " + + "You can run targetless, or set a different target namespace " + + "or kubeconfig in the mirrord configuration file.", + NotificationType.INFORMATION + ) } - return pods + return output } } @@ -179,8 +232,9 @@ class MirrordApi(private val service: MirrordProjectService, private val project * * @return list of pods */ - fun listPods(cli: String, configFile: String?, wslDistribution: WSLDistribution?): List { - val task = MirrordLsTask(cli, projectEnvVars).apply { + fun listPods(cli: String, configFile: String?, wslDistribution: WSLDistribution?, namespace: String?): MirrordLsOutput { + val envVars = projectEnvVars.orEmpty() + (MIRRORD_LS_RICH_OUTPUT_ENV to "true") + val task = MirrordLsTask(cli, envVars, namespace).apply { this.configFile = configFile this.wslDistribution = wslDistribution this.output = "json" From b0fd827be20ee4150b9789126e5898255c9cbc7c Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Tue, 28 Jan 2025 23:45:10 +0100 Subject: [PATCH 03/12] Kind of works --- .../com/metalbear/mirrord/MirrordApi.kt | 4 +- .../metalbear/mirrord/MirrordExecDialog.kt | 377 +++++++++--------- .../metalbear/mirrord/MirrordExecManager.kt | 13 +- 3 files changed, 195 insertions(+), 199 deletions(-) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt index cabc5d48..f40b6cdc 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt @@ -230,9 +230,9 @@ class MirrordApi(private val service: MirrordProjectService, private val project * Runs `mirrord ls` to get the list of available targets. * Displays a modal progress dialog. * - * @return list of pods + * @return available targets */ - fun listPods(cli: String, configFile: String?, wslDistribution: WSLDistribution?, namespace: String?): MirrordLsOutput { + fun listTargets(cli: String, configFile: String?, wslDistribution: WSLDistribution?, namespace: String?): MirrordLsOutput { val envVars = projectEnvVars.orEmpty() + (MIRRORD_LS_RICH_OUTPUT_ENV to "true") val task = MirrordLsTask(cli, envVars, namespace).apply { this.configFile = configFile diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt index 87890c7a..59945264 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt @@ -1,234 +1,235 @@ package com.metalbear.mirrord -import com.intellij.openapi.ui.DialogBuilder +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogWrapper import com.intellij.ui.JBColor import com.intellij.ui.components.JBBox import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBList import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.JBFont import com.intellij.util.ui.JBUI +import java.awt.Component import java.awt.Dimension +import java.awt.Font import java.awt.event.* import javax.swing.* import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener -object MirrordExecDialog { - private const val dialogHeading: String = "mirrord" - private const val targetLabel = "Select Target" - private const val searchPlaceHolder = "Filter targets..." +class MirrordExecDialog(project: Project, private val getTargets: (String?) -> MirrordApi.MirrordLsOutput) : DialogWrapper(project, true) { + companion object { + const val TARGETLESS_SELECTION_VALUE = "No Target (\"targeteless\")" + private const val TARGET_FILTER_PLACEHOLDER = "Filter targets..." + } + + private var fetched: MirrordApi.MirrordLsOutput = getTargets(null) + + private val targetOptions: JBList = JBList(emptyList()).apply { + selectionMode = ListSelectionModel.SINGLE_SELECTION + minimumSize = Dimension(250, 350) + } + + private val namespaceOptions: ComboBox = ComboBox() /** - * Label that's used to select targetless mode + * Whether to show pods. */ - const val targetlessTargetName = "No Target (\"targetless\")" + private val showPods: JBCheckBox = JBCheckBox("Pods", MirrordSettingsState.instance.mirrordState.showPodsInSelection ?: true).apply { + this.addActionListener { + refresh() + } + } /** - * Manages the state of targets list in the dialog. Keeps all the filters in one place. + * Whether to show deployments. */ - private class TargetsState(private var availableTargets: List) { - /** - * Whether to show pods. - */ - var pods = MirrordSettingsState.instance.mirrordState.showPodsInSelection ?: true - - /** - * Whether to show deployments. - */ - var deployments = MirrordSettingsState.instance.mirrordState.showDeploymentsInSelection ?: true - - /** - * Whether to show rollouts. - */ - var rollouts = MirrordSettingsState.instance.mirrordState.showRolloutsInSelection ?: false - - /** - * Show only targets containing this phrase. - */ - var searchPhrase = "" - - /** - * Filtered and sorted targets, targetless option at the bottom, last chosen target at the top. - */ - val targets: List - get() { - return this.availableTargets - .filter { - (this.pods && it.startsWith("pod/")) || - (this.deployments && it.startsWith("deployment/")) || - (this.rollouts && it.startsWith("rollout/")) - } - .filter { it.contains(this.searchPhrase) } - .toMutableList() - .apply { - sort() - MirrordSettingsState.instance.mirrordState.lastChosenTarget?.let { - val idx = this.indexOf(it) - if (idx != -1) { - this.removeAt(idx) - this.add(0, it) - } - } - add(targetlessTargetName) - } - .toList() - } + private val showDeployments: JBCheckBox = JBCheckBox("Deployments", MirrordSettingsState.instance.mirrordState.showDeploymentsInSelection ?: true).apply { + this.addActionListener { + refresh() + } } /** - * Shows a target selection dialog. - * - * @return a target selected from the given list, targetlessTargetName constant if user selected targetless, null if the user cancelled + * Whether to show rollouts. */ - fun selectTargetDialog(availableTargets: List): String? { - val targetsState = TargetsState(availableTargets) - - val jbTargets = (targetsState.targets).asJBList() - val searchField = JTextField().apply { - val field = this - - // Add an informative placeholder. - val previousForeground = foreground - text = searchPlaceHolder - foreground = JBColor.GRAY - addFocusListener(object : FocusListener { - override fun focusGained(e: FocusEvent) { - if (field.text.equals(searchPlaceHolder)) { - field.text = "" - field.foreground = previousForeground - } - } - - override fun focusLost(e: FocusEvent) { - if (field.text.isEmpty()) { - field.foreground = JBColor.GRAY - field.text = searchPlaceHolder - } - } - }) - - // Add filtering logic on search field update. - document.addDocumentListener(object : DocumentListener { - override fun insertUpdate(e: DocumentEvent) = updateList() - override fun removeUpdate(e: DocumentEvent) = updateList() - override fun changedUpdate(e: DocumentEvent) = updateList() - - private fun updateList() { - val searchTerm = field.text - if (!searchTerm.equals(searchPlaceHolder)) { - targetsState.searchPhrase = searchTerm - jbTargets.setListData(targetsState.targets.toTypedArray()) - } - } - }) - - // Add focus logic so that the user can change back and forth from search field - // to target selection using tab/shift+tab. - addKeyListener(object : KeyListener { - override fun keyTyped(p0: KeyEvent) { - } + private val showRollouts: JBCheckBox = JBCheckBox("Rollouts", MirrordSettingsState.instance.mirrordState.showRolloutsInSelection ?: true).apply { + this.addActionListener { + refresh() + } + } - override fun keyPressed(e: KeyEvent) { - if (e.keyCode == KeyEvent.VK_TAB) { - if (e.modifiersEx > 0) { - field.transferFocusBackward() - } else { - field.transferFocus() - } - e.consume() - } + private val targetFilter = JTextField().apply { + val field = this + + // Add an informative placeholder. + val previousForeground = foreground + text = TARGET_FILTER_PLACEHOLDER + foreground = JBColor.GRAY + addFocusListener(object : FocusListener { + override fun focusGained(e: FocusEvent) { + if (field.text.equals(TARGET_FILTER_PLACEHOLDER)) { + field.text = "" + field.foreground = previousForeground } + } - override fun keyReleased(p0: KeyEvent) { + override fun focusLost(e: FocusEvent) { + if (field.text.isEmpty()) { + field.foreground = JBColor.GRAY + field.text = TARGET_FILTER_PLACEHOLDER } - }) - } - val filterHelpers = listOf( - JBCheckBox("Pods", targetsState.pods).apply { - this.addActionListener { - targetsState.pods = this.isSelected - jbTargets.setListData(targetsState.targets.toTypedArray()) - } - }, - JBCheckBox("Deployments", targetsState.deployments).apply { - this.addActionListener { - targetsState.deployments = this.isSelected - jbTargets.setListData(targetsState.targets.toTypedArray()) - } - }, - JBCheckBox("Rollouts", targetsState.rollouts).apply { - this.addActionListener { - targetsState.rollouts = this.isSelected - jbTargets.setListData(targetsState.targets.toTypedArray()) + } + }) + + // Add filtering logic on search field update. + document.addDocumentListener(object : DocumentListener { + override fun insertUpdate(e: DocumentEvent) = updateList() + override fun removeUpdate(e: DocumentEvent) = updateList() + override fun changedUpdate(e: DocumentEvent) = updateList() + + private fun updateList() { + val searchTerm = field.text + if (!searchTerm.equals(TARGET_FILTER_PLACEHOLDER)) { + refresh() } } - ) - val result = DialogBuilder().apply { - setCenterPanel(createSelectionDialog(jbTargets, searchField, filterHelpers)) - setTitle(dialogHeading) - setPreferredFocusComponent(searchField) - }.show() - - if (result == DialogWrapper.OK_EXIT_CODE) { - MirrordSettingsState.instance.mirrordState.showPodsInSelection = targetsState.pods - MirrordSettingsState.instance.mirrordState.showDeploymentsInSelection = targetsState.deployments - MirrordSettingsState.instance.mirrordState.showRolloutsInSelection = targetsState.rollouts - - if (jbTargets.isSelectionEmpty) { - // The user did not select any target, and clicked ok. - return targetlessTargetName + }) + + // Add focus logic so that the user can change back and forth from search field + // to target selection using tab/shift+tab. + addKeyListener(object : KeyListener { + override fun keyTyped(p0: KeyEvent) {} + + override fun keyPressed(e: KeyEvent) { + if (e.keyCode == KeyEvent.VK_TAB) { + if (e.modifiersEx > 0) { + field.transferFocusBackward() + } else { + field.transferFocus() + } + e.consume() + } } - val selectedValue = jbTargets.selectedValue - MirrordSettingsState.instance.mirrordState.lastChosenTarget = selectedValue - return selectedValue - } + override fun keyReleased(p0: KeyEvent) {} + }) - // The user clicked cancel, or closed the dialog. - return null + alignmentX = JBScrollPane.LEFT_ALIGNMENT + maximumSize = Dimension(10000, 30) } - private fun List.asJBList() = JBList(this).apply { - selectionMode = ListSelectionModel.SINGLE_SELECTION + private val selectTargetLabel = JLabel("Select Target").apply { + alignmentX = JLabel.LEFT_ALIGNMENT + font = JBFont.create(font.deriveFont(Font.BOLD), false) } - private fun createSelectionDialog(items: JBList, searchField: JTextField, filterHelpers: List): JPanel = - JPanel().apply { - layout = BoxLayout(this, BoxLayout.Y_AXIS) - border = JBUI.Borders.empty(10, 5) - add( - JLabel(targetLabel).apply { - alignmentX = JLabel.LEFT_ALIGNMENT + private val selectNamespaceLabel = JLabel("Select Namespace").apply { + alignmentX = JLabel.LEFT_ALIGNMENT + font = JBFont.create(font.deriveFont(Font.BOLD), false) + } + + private val verticalSeparator: Component + get() = Box.createRigidArea(Dimension(0, 10)) + + private val horizontalSeparator: Component + get() = Box.createRigidArea(Dimension(10, 0)) + + init { + title = "mirrord" + refresh() + init() + } + + private fun refresh() { + val selectableTargets = fetched + .targets + .asSequence() + .filter { it.available } + .map { it.path } + .filter { + (showPods.isSelected && it.startsWith("pod/")) || + (showDeployments.isSelected && it.startsWith("deployment/")) || + (showRollouts.isSelected && it.startsWith("rollout/")) + } + .filter { targetFilter.text == TARGET_FILTER_PLACEHOLDER || it.contains(targetFilter.text) } + .toMutableList() + .apply { + MirrordSettingsState.instance.mirrordState.lastChosenTarget?.let { + val idx = this.indexOf(it) + if (idx != -1) { + this.removeAt(idx) + this.add(0, it) + } } - ) - add(Box.createRigidArea(Dimension(0, 10))) + add(TARGETLESS_SELECTION_VALUE) + } + .toTypedArray() + targetOptions.setListData(selectableTargets) + + namespaceOptions.removeAllItems() + fetched.namespaces?.forEach { namespaceOptions.addItem(it) } + fetched.currentNamespace?.let { namespaceOptions.selectedItem = it } + } + + override fun createCenterPanel(): JComponent = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + border = JBUI.Borders.empty(10, 5) + preferredSize = Dimension(400, 400) + + if (fetched.currentNamespace != null && fetched.namespaces != null) { add( JBBox.createHorizontalBox().apply { - filterHelpers.forEach { - this.add(it) - this.add(Box.createRigidArea(Dimension(10, 0))) - } + add(selectNamespaceLabel) + add(horizontalSeparator) + add(JComboBox().apply { + selectedItem = fetched.currentNamespace + fetched.namespaces?.forEach( this::addItem ) + }) alignmentX = JBBox.LEFT_ALIGNMENT + maximumSize = Dimension(10000, 30) } ) - add(Box.createRigidArea(Dimension(0, 10))) - add( - searchField.apply { - alignmentX = JBScrollPane.LEFT_ALIGNMENT - preferredSize = Dimension(250, 30) - size = Dimension(250, 30) - } - ) - add(Box.createRigidArea(Dimension(0, 10))) - add( - JBScrollPane( - items.apply { - minimumSize = Dimension(250, 350) - } - ).apply { - alignmentX = JBScrollPane.LEFT_ALIGNMENT - } - ) + add(verticalSeparator) + } + + add(selectTargetLabel) + add(verticalSeparator) + add( + JBBox.createHorizontalBox().apply { + add(showPods) + add(horizontalSeparator) + add(showDeployments) + add(horizontalSeparator) + add(showRollouts) + alignmentX = JBBox.LEFT_ALIGNMENT + } + ) + add(verticalSeparator) + add(targetFilter) + add(verticalSeparator) + add( + JBScrollPane(targetOptions).apply { + alignmentX = JBScrollPane.LEFT_ALIGNMENT + } + ) + } + + fun showAndGetSelection(): String? { + return if (showAndGet()) { + MirrordSettingsState.instance.mirrordState.showPodsInSelection = showPods.isSelected + MirrordSettingsState.instance.mirrordState.showDeploymentsInSelection = showDeployments.isSelected + MirrordSettingsState.instance.mirrordState.showRolloutsInSelection = showRollouts.isSelected + + if (targetOptions.isSelectionEmpty) { + TARGETLESS_SELECTION_VALUE + } else { + MirrordSettingsState.instance.mirrordState.lastChosenTarget = targetOptions.selectedValue + targetOptions.selectedValue + } + } else { + null } + } } diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt index 3493846b..d2e68b51 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt @@ -32,23 +32,18 @@ class MirrordExecManager(private val service: MirrordProjectService) { ): String { MirrordLogger.logger.debug("choose target called") - val pods = mirrordApi.listPods( - cli, - config, - wslDistribution - ) - + val getTargets = { namespace: String? -> mirrordApi.listTargets(cli, config, wslDistribution, namespace) } val application = ApplicationManager.getApplication() val selected = if (application.isDispatchThread) { MirrordLogger.logger.debug("dispatch thread detected, choosing target on current thread") - MirrordExecDialog.selectTargetDialog(pods) + MirrordExecDialog(service.project, getTargets).showAndGetSelection() } else if (!application.isReadAccessAllowed) { MirrordLogger.logger.debug("no read lock detected, choosing target on dispatch thread") var target: String? = null application.invokeAndWait { MirrordLogger.logger.debug("choosing target from invoke") - target = MirrordExecDialog.selectTargetDialog(pods) + target = MirrordExecDialog(service.project, getTargets).showAndGetSelection() } target } else { @@ -175,7 +170,7 @@ class MirrordExecManager(private val service: MirrordProjectService) { MirrordLogger.logger.debug("target not selected, showing dialog") chooseTarget(cli, wslDistribution, configPath, mirrordApi) - .takeUnless { it == MirrordExecDialog.targetlessTargetName } ?: run { + .takeUnless { it == MirrordExecDialog.TARGETLESS_SELECTION_VALUE } ?: run { MirrordLogger.logger.info("No target specified - running targetless") service.notifier.notification( "No target specified, mirrord running targetless.", From f2e4b7033a3aa88b4734520f04cb3210bb93f51f Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Wed, 29 Jan 2025 12:40:39 +0100 Subject: [PATCH 04/12] Selection works --- .../metalbear/mirrord/MirrordExecDialog.kt | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt index 59945264..ab0dc847 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt @@ -31,7 +31,25 @@ class MirrordExecDialog(project: Project, private val getTargets: (String?) -> M minimumSize = Dimension(250, 350) } - private val namespaceOptions: ComboBox = ComboBox() + private val namespaceOptions: ComboBox = ComboBox(object : DefaultComboBoxModel() { + private var refreshing: Boolean = false + + override fun setSelectedItem(anObject: Any?) { + super.setSelectedItem(anObject) + + if (refreshing) { + return + } + + val namespace = anObject as? String? ?: return + if (fetched.currentNamespace != namespace && fetched.namespaces.orEmpty().contains(namespace)) { + fetched = getTargets(namespace) + refreshing = true + refresh() + refreshing = false + } + } + }) /** * Whether to show pods. @@ -183,10 +201,7 @@ class MirrordExecDialog(project: Project, private val getTargets: (String?) -> M JBBox.createHorizontalBox().apply { add(selectNamespaceLabel) add(horizontalSeparator) - add(JComboBox().apply { - selectedItem = fetched.currentNamespace - fetched.namespaces?.forEach( this::addItem ) - }) + add(namespaceOptions) alignmentX = JBBox.LEFT_ALIGNMENT maximumSize = Dimension(10000, 30) } From 92fa455afa7b10fe765daa9c1654d3e9ad9e0e5f Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Wed, 29 Jan 2025 12:59:34 +0100 Subject: [PATCH 05/12] Namespace passed to execution --- .../com/metalbear/mirrord/MirrordApi.kt | 16 +++++-- .../metalbear/mirrord/MirrordExecDialog.kt | 46 ++++++++++++------- .../metalbear/mirrord/MirrordExecManager.kt | 20 ++------ 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt index f40b6cdc..960dec91 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt @@ -408,11 +408,12 @@ class MirrordApi(private val service: MirrordProjectService, private val project * * @return environment for the user's application */ - fun exec(cli: String, target: String?, configFile: String?, executable: String?, wslDistribution: WSLDistribution?): MirrordExecution { + fun exec(cli: String, target: MirrordExecDialog.UserSelection, configFile: String?, executable: String?, wslDistribution: WSLDistribution?): MirrordExecution { bumpRunCounter() val task = MirrordExtTask(cli, projectEnvVars).apply { - this.target = target + this.target = target.target + this.namespace = target.namespace this.configFile = configFile this.executable = executable this.wslDistribution = wslDistribution @@ -430,11 +431,12 @@ class MirrordApi(private val service: MirrordProjectService, private val project return result } - fun containerExec(cli: String, target: String?, configFile: String?, wslDistribution: WSLDistribution?): MirrordContainerExecution { + fun containerExec(cli: String, target: MirrordExecDialog.UserSelection, configFile: String?, wslDistribution: WSLDistribution?): MirrordContainerExecution { bumpRunCounter() val task = MirrordContainerExtTask(cli, projectEnvVars).apply { - this.target = target + this.target = target.target + this.namespace = target.namespace this.configFile = configFile this.wslDistribution = wslDistribution } @@ -485,6 +487,7 @@ class MirrordApi(private val service: MirrordProjectService, private val project */ private abstract class MirrordCliTask(private val cli: String, private val command: String, private val args: List?, private val projectEnvVars: Map?) { var target: String? = null + var namespace: String? = null var configFile: String? = null var executable: String? = null var wslDistribution: WSLDistribution? = null @@ -505,11 +508,16 @@ private abstract class MirrordCliTask(private val cli: String, private val co addParameter(it) } + namespace?.let { + environment.put("MIRRORD_TARGET_NAMESPACE", it) + } + configFile?.let { val formattedPath = wslDistribution?.getWslPath(it) ?: it addParameter("-f") addParameter(formattedPath) } + executable?.let { addParameter("-e") addParameter(it) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt index ab0dc847..57ee32c7 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt @@ -1,5 +1,7 @@ package com.metalbear.mirrord +import com.intellij.notification.NotificationType +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogWrapper @@ -18,9 +20,11 @@ import javax.swing.* import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener -class MirrordExecDialog(project: Project, private val getTargets: (String?) -> MirrordApi.MirrordLsOutput) : DialogWrapper(project, true) { +class MirrordExecDialog(private val project: Project, private val getTargets: (String?) -> MirrordApi.MirrordLsOutput) : DialogWrapper(project, true) { + data class UserSelection(val target: String?, val namespace: String?) + companion object { - const val TARGETLESS_SELECTION_VALUE = "No Target (\"targeteless\")" + private const val TARGETLESS_SELECTION_LABEL = "No Target (\"targeteless\")" private const val TARGET_FILTER_PLACEHOLDER = "Filter targets..." } @@ -181,7 +185,7 @@ class MirrordExecDialog(project: Project, private val getTargets: (String?) -> M this.add(0, it) } } - add(TARGETLESS_SELECTION_VALUE) + add(TARGETLESS_SELECTION_LABEL) } .toTypedArray() targetOptions.setListData(selectableTargets) @@ -231,20 +235,30 @@ class MirrordExecDialog(project: Project, private val getTargets: (String?) -> M ) } - fun showAndGetSelection(): String? { - return if (showAndGet()) { - MirrordSettingsState.instance.mirrordState.showPodsInSelection = showPods.isSelected - MirrordSettingsState.instance.mirrordState.showDeploymentsInSelection = showDeployments.isSelected - MirrordSettingsState.instance.mirrordState.showRolloutsInSelection = showRollouts.isSelected - - if (targetOptions.isSelectionEmpty) { - TARGETLESS_SELECTION_VALUE - } else { - MirrordSettingsState.instance.mirrordState.lastChosenTarget = targetOptions.selectedValue - targetOptions.selectedValue - } - } else { + fun showAndGetSelection(): UserSelection? { + if (!showAndGet()) { + return null + } + + MirrordSettingsState.instance.mirrordState.showPodsInSelection = showPods.isSelected + MirrordSettingsState.instance.mirrordState.showDeploymentsInSelection = showDeployments.isSelected + MirrordSettingsState.instance.mirrordState.showRolloutsInSelection = showRollouts.isSelected + + val target = if (targetOptions.isSelectionEmpty) { + MirrordLogger.logger.info("No target specified - running targetless") + project.service().notifier.notification( + "No target specified, mirrord running targetless.", + NotificationType.INFORMATION + ) + .withDontShowAgain(MirrordSettingsState.NotificationId.RUNNING_TARGETLESS) + .fire() + null + } else { + MirrordSettingsState.instance.mirrordState.lastChosenTarget = targetOptions.selectedValue + targetOptions.selectedValue.takeUnless { it == TARGETLESS_SELECTION_LABEL } } + + return UserSelection(target, fetched.currentNamespace) } } diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt index d2e68b51..d1f6d464 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt @@ -29,7 +29,7 @@ class MirrordExecManager(private val service: MirrordProjectService) { wslDistribution: WSLDistribution?, config: String?, mirrordApi: MirrordApi - ): String { + ): MirrordExecDialog.UserSelection { MirrordLogger.logger.debug("choose target called") val getTargets = { namespace: String? -> mirrordApi.listTargets(cli, config, wslDistribution, namespace) } @@ -40,7 +40,7 @@ class MirrordExecManager(private val service: MirrordProjectService) { MirrordExecDialog(service.project, getTargets).showAndGetSelection() } else if (!application.isReadAccessAllowed) { MirrordLogger.logger.debug("no read lock detected, choosing target on dispatch thread") - var target: String? = null + var target: MirrordExecDialog.UserSelection? = null application.invokeAndWait { MirrordLogger.logger.debug("choosing target from invoke") target = MirrordExecDialog(service.project, getTargets).showAndGetSelection() @@ -108,7 +108,7 @@ class MirrordExecManager(private val service: MirrordProjectService) { product: String, projectEnvVars: Map?, mirrordApi: MirrordApi - ): Pair? { + ): Pair? { MirrordLogger.logger.debug("MirrordExecManager.start") val mirrordActiveValue = projectEnvVars?.get("MIRRORD_ACTIVE") val explicitlyEnabled = mirrordActiveValue == "1" @@ -168,21 +168,9 @@ class MirrordExecManager(private val service: MirrordProjectService) { val target = if (!targetSet) { // There is no config file or the config does not specify a target, so show dialog. MirrordLogger.logger.debug("target not selected, showing dialog") - chooseTarget(cli, wslDistribution, configPath, mirrordApi) - .takeUnless { it == MirrordExecDialog.TARGETLESS_SELECTION_VALUE } ?: run { - MirrordLogger.logger.info("No target specified - running targetless") - service.notifier.notification( - "No target specified, mirrord running targetless.", - NotificationType.INFORMATION - ) - .withDontShowAgain(MirrordSettingsState.NotificationId.RUNNING_TARGETLESS) - .fire() - - null - } } else { - null + MirrordExecDialog.UserSelection(null, null) } return Pair(configPath, target) From 96f3e4ac377b85fb46090bb17f952fd23fc30ca7 Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Wed, 29 Jan 2025 13:20:48 +0100 Subject: [PATCH 06/12] Fixed refresh loop --- .../com/metalbear/mirrord/MirrordApi.kt | 5 +- .../metalbear/mirrord/MirrordExecDialog.kt | 63 ++++++++++--------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt index 960dec91..42454175 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt @@ -185,7 +185,7 @@ class MirrordApi(private val service: MirrordProjectService, private val project class MirrordLsOutput(val targets: List, val currentNamespace: String?, val namespaces: List?) - private class MirrordLsTask(cli: String, projectEnvVars: Map?, namespace: String?) : MirrordCliTask(cli, "ls", namespace?.let { listOf("-n", it) }, projectEnvVars) { + private class MirrordLsTask(cli: String, projectEnvVars: Map?) : MirrordCliTask(cli, "ls", null, projectEnvVars) { override fun compute(project: Project, process: Process, setText: (String) -> Unit): MirrordLsOutput { setText("mirrord is listing targets...") @@ -234,7 +234,8 @@ class MirrordApi(private val service: MirrordProjectService, private val project */ fun listTargets(cli: String, configFile: String?, wslDistribution: WSLDistribution?, namespace: String?): MirrordLsOutput { val envVars = projectEnvVars.orEmpty() + (MIRRORD_LS_RICH_OUTPUT_ENV to "true") - val task = MirrordLsTask(cli, envVars, namespace).apply { + val task = MirrordLsTask(cli, envVars).apply { + this.namespace = namespace this.configFile = configFile this.wslDistribution = wslDistribution this.output = "json" diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt index 57ee32c7..688e3a0b 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt @@ -30,14 +30,14 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S private var fetched: MirrordApi.MirrordLsOutput = getTargets(null) + private var refreshing: Boolean = false + private val targetOptions: JBList = JBList(emptyList()).apply { selectionMode = ListSelectionModel.SINGLE_SELECTION minimumSize = Dimension(250, 350) } private val namespaceOptions: ComboBox = ComboBox(object : DefaultComboBoxModel() { - private var refreshing: Boolean = false - override fun setSelectedItem(anObject: Any?) { super.setSelectedItem(anObject) @@ -48,9 +48,7 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S val namespace = anObject as? String? ?: return if (fetched.currentNamespace != namespace && fetched.namespaces.orEmpty().contains(namespace)) { fetched = getTargets(namespace) - refreshing = true refresh() - refreshing = false } } }) @@ -165,34 +163,39 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S } private fun refresh() { - val selectableTargets = fetched - .targets - .asSequence() - .filter { it.available } - .map { it.path } - .filter { - (showPods.isSelected && it.startsWith("pod/")) || - (showDeployments.isSelected && it.startsWith("deployment/")) || - (showRollouts.isSelected && it.startsWith("rollout/")) - } - .filter { targetFilter.text == TARGET_FILTER_PLACEHOLDER || it.contains(targetFilter.text) } - .toMutableList() - .apply { - MirrordSettingsState.instance.mirrordState.lastChosenTarget?.let { - val idx = this.indexOf(it) - if (idx != -1) { - this.removeAt(idx) - this.add(0, it) + refreshing = true + try { + val selectableTargets = fetched + .targets + .asSequence() + .filter { it.available } + .map { it.path } + .filter { + (showPods.isSelected && it.startsWith("pod/")) || + (showDeployments.isSelected && it.startsWith("deployment/")) || + (showRollouts.isSelected && it.startsWith("rollout/")) + } + .filter { targetFilter.text == TARGET_FILTER_PLACEHOLDER || it.contains(targetFilter.text) } + .toMutableList() + .apply { + MirrordSettingsState.instance.mirrordState.lastChosenTarget?.let { + val idx = this.indexOf(it) + if (idx != -1) { + this.removeAt(idx) + this.add(0, it) + } } + add(TARGETLESS_SELECTION_LABEL) } - add(TARGETLESS_SELECTION_LABEL) - } - .toTypedArray() - targetOptions.setListData(selectableTargets) - - namespaceOptions.removeAllItems() - fetched.namespaces?.forEach { namespaceOptions.addItem(it) } - fetched.currentNamespace?.let { namespaceOptions.selectedItem = it } + .toTypedArray() + targetOptions.setListData(selectableTargets) + + namespaceOptions.removeAllItems() + fetched.namespaces?.forEach { namespaceOptions.addItem(it) } + fetched.currentNamespace?.let { namespaceOptions.selectedItem = it } + } finally { + refreshing = false + } } override fun createCenterPanel(): JComponent = JPanel().apply { From 16f0b99ae75ad9cacc0b76f08df5703a85e6a9c1 Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Wed, 29 Jan 2025 13:26:14 +0100 Subject: [PATCH 07/12] More strict parse error check --- .../kotlin/com/metalbear/mirrord/MirrordApi.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt index 42454175..bda5d19c 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt @@ -4,6 +4,7 @@ package com.metalbear.mirrord import com.google.gson.Gson import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException import com.google.gson.annotations.SerializedName import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.wsl.WSLCommandLineOptions @@ -202,12 +203,16 @@ class MirrordApi(private val service: MirrordProjectService, private val project val richOutput = SafeParser().parse(data, RichOutput::class.java) MirrordLsOutput(richOutput.targets.toList(), richOutput.currentNamespace, richOutput.namespaces.toList()) } catch (error: Throwable) { - val simpleOutput = SafeParser().parse(data, Array::class.java) - MirrordLsOutput( - simpleOutput.map { FoundTarget(it, true) }, - null, - null, - ) + if (error.cause != null && error.cause is JsonSyntaxException) { + val simpleOutput = SafeParser().parse(data, Array::class.java) + MirrordLsOutput( + simpleOutput.map { FoundTarget(it, true) }, + null, + null, + ) + } else { + throw error + } } if (output.targets.isEmpty()) { From 307242fc7dba17135b143d9c89f61586b9c98fe6 Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Wed, 29 Jan 2025 13:27:37 +0100 Subject: [PATCH 08/12] Changelog --- changelog.d/+namespace-selection.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/+namespace-selection.added.md diff --git a/changelog.d/+namespace-selection.added.md b/changelog.d/+namespace-selection.added.md new file mode 100644 index 00000000..9363e176 --- /dev/null +++ b/changelog.d/+namespace-selection.added.md @@ -0,0 +1 @@ +The target selection dialog now allows for switching between available namespaces. From 73581ee04c07cf474b8e0d91dc512dae41e1117b Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Wed, 29 Jan 2025 13:33:16 +0100 Subject: [PATCH 09/12] Format --- .../src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt | 9 ++++----- .../kotlin/com/metalbear/mirrord/MirrordExecDialog.kt | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt index bda5d19c..43319294 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt @@ -160,7 +160,7 @@ class MirrordApi(private val service: MirrordProjectService, private val project private data class RichOutput( val targets: Array, @SerializedName("current_namespace") val currentNamespace: String, - val namespaces: Array, + val namespaces: Array ) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -185,7 +185,6 @@ class MirrordApi(private val service: MirrordProjectService, private val project class MirrordLsOutput(val targets: List, val currentNamespace: String?, val namespaces: List?) - private class MirrordLsTask(cli: String, projectEnvVars: Map?) : MirrordCliTask(cli, "ls", null, projectEnvVars) { override fun compute(project: Project, process: Process, setText: (String) -> Unit): MirrordLsOutput { setText("mirrord is listing targets...") @@ -208,7 +207,7 @@ class MirrordApi(private val service: MirrordProjectService, private val project MirrordLsOutput( simpleOutput.map { FoundTarget(it, true) }, null, - null, + null ) } else { throw error @@ -221,8 +220,8 @@ class MirrordApi(private val service: MirrordProjectService, private val project .notifier .notifySimple( "No mirrord target available in the configured namespace. " + - "You can run targetless, or set a different target namespace " + - "or kubeconfig in the mirrord configuration file.", + "You can run targetless, or set a different target namespace " + + "or kubeconfig in the mirrord configuration file.", NotificationType.INFORMATION ) } diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt index 688e3a0b..5482fe9a 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt @@ -172,8 +172,8 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S .map { it.path } .filter { (showPods.isSelected && it.startsWith("pod/")) || - (showDeployments.isSelected && it.startsWith("deployment/")) || - (showRollouts.isSelected && it.startsWith("rollout/")) + (showDeployments.isSelected && it.startsWith("deployment/")) || + (showRollouts.isSelected && it.startsWith("rollout/")) } .filter { targetFilter.text == TARGET_FILTER_PLACEHOLDER || it.contains(targetFilter.text) } .toMutableList() From 585915973d9ff219c0c865a841557ee35b1dfcd7 Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Wed, 29 Jan 2025 13:51:13 +0100 Subject: [PATCH 10/12] Docs --- .../com/metalbear/mirrord/MirrordApi.kt | 45 ++++++++++- .../metalbear/mirrord/MirrordExecDialog.kt | 76 ++++++++++++++++++- .../metalbear/mirrord/MirrordExecManager.kt | 7 +- 3 files changed, 121 insertions(+), 7 deletions(-) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt index 43319294..4436d02c 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt @@ -156,10 +156,35 @@ private const val MIRRORD_LS_RICH_OUTPUT_ENV = "MIRRORD_LS_RICH_OUTPUT" * Interact with mirrord CLI using this API. */ class MirrordApi(private val service: MirrordProjectService, private val projectEnvVars: Map?) { - data class FoundTarget(val path: String, val available: Boolean) + /** + * New format of found target returned from `mirrord ls`. + */ + data class FoundTarget( + /** + * Path to the target, e.g `pod/my-pod`. + */ + val path: String, + /** + * Whether this target can be selected. + */ + val available: Boolean + ) + + /** + * New format of `mirrord ls`, enabled by setting MIRRORD_LS_RICH_OUTPUT_ENV to `true`. + */ private data class RichOutput( + /** + * Targets found in the namespace. + */ val targets: Array, + /** + * Namespace where the lookup was done. + */ @SerializedName("current_namespace") val currentNamespace: String, + /** + * All namespaces available to the user. + */ val namespaces: Array ) { override fun equals(other: Any?): Boolean { @@ -183,7 +208,23 @@ class MirrordApi(private val service: MirrordProjectService, private val project } } - class MirrordLsOutput(val targets: List, val currentNamespace: String?, val namespaces: List?) + /** + * Output of `mirrord ls`. + */ + class MirrordLsOutput( + /** + * List of found targets. + */ + val targets: List, + /** + * Namespace where the lookup was done. + */ + val currentNamespace: String?, + /** + * All namespaces avaiable to the user. + */ + val namespaces: List? + ) private class MirrordLsTask(cli: String, projectEnvVars: Map?) : MirrordCliTask(cli, "ls", null, projectEnvVars) { override fun compute(project: Project, process: Process, setText: (String) -> Unit): MirrordLsOutput { diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt index 5482fe9a..67356802 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt @@ -20,28 +20,73 @@ import javax.swing.* import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener +/** + * Target and namespace selection dialog. + * @param project for getting the MirrordNotifier. + * @param getTargets function used to fetch targets from the cluster. + * Accepts the name of a namespace where the lookup should be done. + * If no name is given, the default value from the mirrord config should be user. + */ class MirrordExecDialog(private val project: Project, private val getTargets: (String?) -> MirrordApi.MirrordLsOutput) : DialogWrapper(project, true) { - data class UserSelection(val target: String?, val namespace: String?) + /** + * Target and namespace selected by the user. + */ + data class UserSelection( + /** + * Path to the target, e.g `pod/my-pod`. + * null if targetless. + */ + val target: String?, + /** + * Optional target namespace override. + */ + val namespace: String? + ) companion object { + /** + * Dummy label we use in the dialog to allow the user for explicitly selecting the targetless mode. + * There can be no clash with real target labels, because each of those starts with a target type, e.g `pod/`. + */ private const val TARGETLESS_SELECTION_LABEL = "No Target (\"targeteless\")" + + /** + * Placeholder value for the target filter. + */ private const val TARGET_FILTER_PLACEHOLDER = "Filter targets..." } + /** + * Targets fetched from the cluster. + */ private var fetched: MirrordApi.MirrordLsOutput = getTargets(null) + /** + * Whether we are currently refreshing the widgets with new content. + * + * This is set in `refresh` and inspected in the custom `namespaceOptions` data model. + * Prevents infinite loops and other bugs. + */ private var refreshing: Boolean = false + /** + * List of targets available in the current namespace. + */ private val targetOptions: JBList = JBList(emptyList()).apply { selectionMode = ListSelectionModel.SINGLE_SELECTION minimumSize = Dimension(250, 350) } + /** + * Dropdown allowing for switching namespaces. + */ private val namespaceOptions: ComboBox = ComboBox(object : DefaultComboBoxModel() { override fun setSelectedItem(anObject: Any?) { super.setSelectedItem(anObject) if (refreshing) { + // If we don't check this, we're going to have problems. + // `refresh` changes data in this data model, which triggers this function. return } @@ -54,7 +99,7 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S }) /** - * Whether to show pods. + * Checkbox allowing for filtering out pods from the target list. */ private val showPods: JBCheckBox = JBCheckBox("Pods", MirrordSettingsState.instance.mirrordState.showPodsInSelection ?: true).apply { this.addActionListener { @@ -63,7 +108,7 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S } /** - * Whether to show deployments. + * Checkbox allowing for filtering out deployments from the target list. */ private val showDeployments: JBCheckBox = JBCheckBox("Deployments", MirrordSettingsState.instance.mirrordState.showDeploymentsInSelection ?: true).apply { this.addActionListener { @@ -72,7 +117,7 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S } /** - * Whether to show rollouts. + * Checkbox allowing for filtering out rollouts from the target list. */ private val showRollouts: JBCheckBox = JBCheckBox("Rollouts", MirrordSettingsState.instance.mirrordState.showRolloutsInSelection ?: true).apply { this.addActionListener { @@ -80,6 +125,9 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S } } + /** + * Text field allowing for searching targets by path. + */ private val targetFilter = JTextField().apply { val field = this @@ -140,19 +188,31 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S maximumSize = Dimension(10000, 30) } + /** + * Label for `targetFilter` and `targetOptions`. + */ private val selectTargetLabel = JLabel("Select Target").apply { alignmentX = JLabel.LEFT_ALIGNMENT font = JBFont.create(font.deriveFont(Font.BOLD), false) } + /** + * Label for `namespaceOptions`. + */ private val selectNamespaceLabel = JLabel("Select Namespace").apply { alignmentX = JLabel.LEFT_ALIGNMENT font = JBFont.create(font.deriveFont(Font.BOLD), false) } + /** + * Small vertical gap between widgets. + */ private val verticalSeparator: Component get() = Box.createRigidArea(Dimension(0, 10)) + /** + * Small horizontal gap between widgets. + */ private val horizontalSeparator: Component get() = Box.createRigidArea(Dimension(10, 0)) @@ -162,6 +222,9 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S init() } + /** + * Updates widgets' content based on what we fetched from the cluster (`fetched` field). + */ private fun refresh() { refreshing = true try { @@ -238,6 +301,11 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S ) } + /** + * Displays the dialog and returns the user selection. + * + * Returns null if the user cancelled. + */ fun showAndGetSelection(): UserSelection? { if (!showAndGet()) { return null diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt index d1f6d464..3c722fb9 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt @@ -21,7 +21,7 @@ class MirrordExecManager(private val service: MirrordProjectService) { /** * Attempts to show the target selection dialog and allow user to select the mirrord target. * - * @return target chosen by the user (or special constant for targetless mode) + * @return target chosen by the user * @throws ProcessCanceledException if the dialog cannot be displayed */ private fun chooseTarget( @@ -103,6 +103,11 @@ class MirrordExecManager(private val service: MirrordProjectService) { }) } + /** + * Resolves path to the mirrord config and the session target. + * + * Returns null if mirrord is disabled. + */ private fun prepareStart( wslDistribution: WSLDistribution?, product: String, From 499966005b6063d54754613ef2f0622225e80d5f Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Wed, 29 Jan 2025 15:50:02 +0100 Subject: [PATCH 11/12] RichOutput docs --- .../main/kotlin/com/metalbear/mirrord/MirrordApi.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt index 4436d02c..86b47cfd 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt @@ -187,6 +187,11 @@ class MirrordApi(private val service: MirrordProjectService, private val project */ val namespaces: Array ) { + /** + * Generated by IntelliJ. + * + * If it's not overrode, we get a warning, because this class has an Array field. + */ override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -200,6 +205,11 @@ class MirrordApi(private val service: MirrordProjectService, private val project return true } + /** + * Generated by IntelliJ. + * + * If it's not overrode, we get a warning, because this class has an Array field. + */ override fun hashCode(): Int { var result = targets.contentHashCode() result = 31 * result + currentNamespace.hashCode() From 41e8ceb7e04d8246426320c3e18993b1008b4f2a Mon Sep 17 00:00:00 2001 From: Razz4780 Date: Wed, 29 Jan 2025 15:52:00 +0100 Subject: [PATCH 12/12] Added comment on inserting last chosen target. --- .../src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt index 67356802..ddd7c9e1 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecDialog.kt @@ -241,6 +241,8 @@ class MirrordExecDialog(private val project: Project, private val getTargets: (S .filter { targetFilter.text == TARGET_FILTER_PLACEHOLDER || it.contains(targetFilter.text) } .toMutableList() .apply { + // Here, for user convenience, we insert the last chosen target at the head of the list. + // Target is identified only by its path, no matter the namespace. MirrordSettingsState.instance.mirrordState.lastChosenTarget?.let { val idx = this.indexOf(it) if (idx != -1) {