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. 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..86b47cfd 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 @@ -146,12 +147,97 @@ 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 { + /** + * 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 + ) { + /** + * 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 + + 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 + } + + /** + * 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() + result = 31 * result + namespaces.contentHashCode() + return result + } + } + + /** + * 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 { setText("mirrord is listing targets...") process.waitFor() @@ -163,13 +249,35 @@ 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) { + 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 (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 } } @@ -177,10 +285,12 @@ 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?): List { - val task = MirrordLsTask(cli, projectEnvVars).apply { + 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).apply { + this.namespace = namespace this.configFile = configFile this.wslDistribution = wslDistribution this.output = "json" @@ -354,11 +464,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 @@ -376,11 +487,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 } @@ -431,6 +543,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 @@ -451,11 +564,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 87890c7a..ddd7c9e1 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,337 @@ package com.metalbear.mirrord -import com.intellij.openapi.ui.DialogBuilder +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 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..." - - /** - * Label that's used to select targetless mode - */ - const val targetlessTargetName = "No Target (\"targetless\")" - +/** + * 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) { /** - * Manages the state of targets list in the dialog. Keeps all the filters in one place. + * Target and namespace selected by the user. */ - private class TargetsState(private var availableTargets: List) { + data class UserSelection( /** - * Whether to show pods. + * Path to the target, e.g `pod/my-pod`. + * null if targetless. */ - var pods = MirrordSettingsState.instance.mirrordState.showPodsInSelection ?: true - - /** - * Whether to show deployments. - */ - var deployments = MirrordSettingsState.instance.mirrordState.showDeploymentsInSelection ?: true - + val target: String?, /** - * Whether to show rollouts. + * Optional target namespace override. */ - var rollouts = MirrordSettingsState.instance.mirrordState.showRolloutsInSelection ?: false + val namespace: String? + ) + companion object { /** - * Show only targets containing this phrase. + * 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/`. */ - var searchPhrase = "" + private const val TARGETLESS_SELECTION_LABEL = "No Target (\"targeteless\")" /** - * Filtered and sorted targets, targetless option at the bottom, last chosen target at the top. + * Placeholder value for the target filter. */ - 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 const val TARGET_FILTER_PLACEHOLDER = "Filter targets..." } /** - * Shows a target selection dialog. + * Targets fetched from the cluster. + */ + private var fetched: MirrordApi.MirrordLsOutput = getTargets(null) + + /** + * Whether we are currently refreshing the widgets with new content. * - * @return a target selected from the given list, targetlessTargetName constant if user selected targetless, null if the user cancelled + * This is set in `refresh` and inspected in the custom `namespaceOptions` data model. + * Prevents infinite loops and other bugs. */ - 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 - } - } + private var refreshing: Boolean = false - 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()) - } - } - }) + /** + * List of targets available in the current namespace. + */ + private val targetOptions: JBList = JBList(emptyList()).apply { + selectionMode = ListSelectionModel.SINGLE_SELECTION + minimumSize = Dimension(250, 350) + } - // 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) { - } + /** + * Dropdown allowing for switching namespaces. + */ + private val namespaceOptions: ComboBox = ComboBox(object : DefaultComboBoxModel() { + override fun setSelectedItem(anObject: Any?) { + super.setSelectedItem(anObject) - override fun keyPressed(e: KeyEvent) { - if (e.keyCode == KeyEvent.VK_TAB) { - if (e.modifiersEx > 0) { - field.transferFocusBackward() - } else { - field.transferFocus() - } - e.consume() - } - } + 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 + } - override fun keyReleased(p0: KeyEvent) { - } - }) + val namespace = anObject as? String? ?: return + if (fetched.currentNamespace != namespace && fetched.namespaces.orEmpty().contains(namespace)) { + fetched = getTargets(namespace) + refresh() + } } - val filterHelpers = listOf( - JBCheckBox("Pods", targetsState.pods).apply { - this.addActionListener { - targetsState.pods = this.isSelected - jbTargets.setListData(targetsState.targets.toTypedArray()) + }) + + /** + * Checkbox allowing for filtering out pods from the target list. + */ + private val showPods: JBCheckBox = JBCheckBox("Pods", MirrordSettingsState.instance.mirrordState.showPodsInSelection ?: true).apply { + this.addActionListener { + refresh() + } + } + + /** + * Checkbox allowing for filtering out deployments from the target list. + */ + private val showDeployments: JBCheckBox = JBCheckBox("Deployments", MirrordSettingsState.instance.mirrordState.showDeploymentsInSelection ?: true).apply { + this.addActionListener { + refresh() + } + } + + /** + * Checkbox allowing for filtering out rollouts from the target list. + */ + private val showRollouts: JBCheckBox = JBCheckBox("Rollouts", MirrordSettingsState.instance.mirrordState.showRolloutsInSelection ?: true).apply { + this.addActionListener { + refresh() + } + } + + /** + * Text field allowing for searching targets by path. + */ + 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 } - }, - JBCheckBox("Deployments", targetsState.deployments).apply { - this.addActionListener { - targetsState.deployments = this.isSelected - jbTargets.setListData(targetsState.targets.toTypedArray()) + } + + override fun focusLost(e: FocusEvent) { + if (field.text.isEmpty()) { + field.foreground = JBColor.GRAY + field.text = TARGET_FILTER_PLACEHOLDER } - }, - 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 + /** + * Label for `targetFilter` and `targetOptions`. + */ + 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 + /** + * 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)) + + init { + title = "mirrord" + refresh() + init() + } + + /** + * Updates widgets' content based on what we fetched from the cluster (`fetched` field). + */ + private fun refresh() { + 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/")) } - ) - add(Box.createRigidArea(Dimension(0, 10))) - add( - JBBox.createHorizontalBox().apply { - filterHelpers.forEach { - this.add(it) - this.add(Box.createRigidArea(Dimension(10, 0))) + .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) { + this.removeAt(idx) + this.add(0, it) + } } - alignmentX = JBBox.LEFT_ALIGNMENT + add(TARGETLESS_SELECTION_LABEL) } - ) - add(Box.createRigidArea(Dimension(0, 10))) + .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 { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + border = JBUI.Borders.empty(10, 5) + preferredSize = Dimension(400, 400) + + if (fetched.currentNamespace != null && fetched.namespaces != null) { add( - searchField.apply { - alignmentX = JBScrollPane.LEFT_ALIGNMENT - preferredSize = Dimension(250, 30) - size = Dimension(250, 30) + JBBox.createHorizontalBox().apply { + add(selectNamespaceLabel) + add(horizontalSeparator) + add(namespaceOptions) + alignmentX = JBBox.LEFT_ALIGNMENT + maximumSize = Dimension(10000, 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 + } + ) + } + + /** + * Displays the dialog and returns the user selection. + * + * Returns null if the user cancelled. + */ + 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 7ea1343f..3c722fb9 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 /** @@ -18,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( @@ -26,26 +29,21 @@ class MirrordExecManager(private val service: MirrordProjectService) { wslDistribution: WSLDistribution?, config: String?, mirrordApi: MirrordApi - ): String { + ): MirrordExecDialog.UserSelection { 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 + var target: MirrordExecDialog.UserSelection? = null application.invokeAndWait { MirrordLogger.logger.debug("choosing target from invoke") - target = MirrordExecDialog.selectTargetDialog(pods) + target = MirrordExecDialog(service.project, getTargets).showAndGetSelection() } target } else { @@ -84,12 +82,38 @@ 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 + ) + } + }) + } + + /** + * Resolves path to the mirrord config and the session target. + * + * Returns null if mirrord is disabled. + */ private fun prepareStart( wslDistribution: WSLDistribution?, product: String, projectEnvVars: Map?, mirrordApi: MirrordApi - ): Pair? { + ): Pair? { MirrordLogger.logger.debug("MirrordExecManager.start") val mirrordActiveValue = projectEnvVars?.get("MIRRORD_ACTIVE") val explicitlyEnabled = mirrordActiveValue == "1" @@ -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\$")) { @@ -158,21 +173,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.targetlessTargetName } ?: 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) @@ -181,7 +184,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