From 605ff32b3ab3dcf0b1f3c2b9518522fe04eb9604 Mon Sep 17 00:00:00 2001 From: crschnick Date: Mon, 2 Feb 2026 16:05:43 +0000 Subject: [PATCH] Rework batch actions --- .../app/hub/action/BatchHubProvider.java | 10 +++- .../app/hub/action/BatchStoreAction.java | 26 ++++++++- .../action/impl/OpenHubMenuLeafProvider.java | 5 ++ .../impl/OpenSplitHubBatchProvider.java | 37 +++++++----- .../hub/comp/StoreEntryListBatchBarComp.java | 4 ++ .../io/xpipe/app/hub/comp/StoreViewState.java | 10 +--- .../java/io/xpipe/app/util/CommandDialog.java | 58 +++++++++++++------ .../io/xpipe/app/util/ScanDialogBase.java | 8 +-- dist/changelog/21.0.md | 2 + .../RunHubBatchScriptActionProvider.java | 7 ++- .../script/RunScriptActionProviderMenu.java | 40 +++++++++++++ .../service/ServiceRefreshHubProvider.java | 5 ++ .../base/store/StorePauseActionProvider.java | 5 ++ .../store/StoreRestartActionProvider.java | 5 ++ .../base/store/StoreStartActionProvider.java | 8 +++ .../base/store/StoreStopActionProvider.java | 5 ++ lang/strings/translations_en.properties | 2 +- 17 files changed, 185 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/io/xpipe/app/hub/action/BatchHubProvider.java b/app/src/main/java/io/xpipe/app/hub/action/BatchHubProvider.java index d9ae62187..ea0cfa21c 100644 --- a/app/src/main/java/io/xpipe/app/hub/action/BatchHubProvider.java +++ b/app/src/main/java/io/xpipe/app/hub/action/BatchHubProvider.java @@ -20,6 +20,10 @@ public interface BatchHubProvider extends ActionProvider { Class getApplicableClass(); + default boolean requiresValidStore() { + return true; + } + default boolean isApplicable(DataStoreEntryRef o) { return true; } @@ -39,7 +43,11 @@ public interface BatchHubProvider extends ActionProvider { }) .filter(action -> action != null) .toList(); - return BatchStoreAction.builder().actions(individual).build(); + return BatchStoreAction.builder().actions(individual).parallel(runParallel()).build(); + } + + default boolean runParallel() { + return false; } @SneakyThrows diff --git a/app/src/main/java/io/xpipe/app/hub/action/BatchStoreAction.java b/app/src/main/java/io/xpipe/app/hub/action/BatchStoreAction.java index d3d2e5ef0..cfc010f45 100644 --- a/app/src/main/java/io/xpipe/app/hub/action/BatchStoreAction.java +++ b/app/src/main/java/io/xpipe/app/hub/action/BatchStoreAction.java @@ -2,9 +2,11 @@ package io.xpipe.app.hub.action; import io.xpipe.app.action.*; import io.xpipe.app.ext.DataStore; +import io.xpipe.app.process.CountDown; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.JacksonMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -14,6 +16,7 @@ import lombok.experimental.SuperBuilder; import java.util.List; import java.util.Optional; +import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; @SuperBuilder @@ -21,6 +24,7 @@ import java.util.stream.Collectors; public final class BatchStoreAction extends SerializableAction implements StoreContextAction { private final List> actions; + private boolean parallel; @Override public ActionProvider getProvider() { @@ -40,10 +44,26 @@ public final class BatchStoreAction extends SerializableAct @Override public void executeImpl() { - for (AbstractAction action : actions) { - if (!action.executeSyncImpl(true)) { - break; + if (!parallel) { + for (AbstractAction action : actions) { + // Don't confirm twice + if (!action.executeSyncImpl(false)) { + break; + } } + } else { + var latch = new CountDownLatch(actions.size()); + for (AbstractAction action : actions) { + ThreadHelper.runAsync(() -> { + // Don't confirm twice + action.executeSyncImpl(false); + latch.countDown(); + }); + } + + try { + latch.await(); + } catch (InterruptedException ignored) {} } } diff --git a/app/src/main/java/io/xpipe/app/hub/action/impl/OpenHubMenuLeafProvider.java b/app/src/main/java/io/xpipe/app/hub/action/impl/OpenHubMenuLeafProvider.java index f00ec7538..7ea9065b1 100644 --- a/app/src/main/java/io/xpipe/app/hub/action/impl/OpenHubMenuLeafProvider.java +++ b/app/src/main/java/io/xpipe/app/hub/action/impl/OpenHubMenuLeafProvider.java @@ -17,6 +17,11 @@ import lombok.extern.jackson.Jacksonized; public class OpenHubMenuLeafProvider implements HubLeafProvider, BatchHubProvider { + @Override + public boolean requiresValidStore() { + return true; + } + @Override public StoreActionCategory getCategory() { return StoreActionCategory.OPEN; diff --git a/app/src/main/java/io/xpipe/app/hub/action/impl/OpenSplitHubBatchProvider.java b/app/src/main/java/io/xpipe/app/hub/action/impl/OpenSplitHubBatchProvider.java index b86b912b3..98fb8756d 100644 --- a/app/src/main/java/io/xpipe/app/hub/action/impl/OpenSplitHubBatchProvider.java +++ b/app/src/main/java/io/xpipe/app/hub/action/impl/OpenSplitHubBatchProvider.java @@ -13,14 +13,17 @@ import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.terminal.*; +import io.xpipe.app.util.ThreadHelper; import javafx.beans.value.ObservableValue; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.UUID; +import java.util.concurrent.CountDownLatch; public class OpenSplitHubBatchProvider implements BatchHubProvider { @@ -65,21 +68,29 @@ public class OpenSplitHubBatchProvider implements BatchHubProvider { throw ErrorEventFactory.expected(new IllegalStateException(AppI18n.get("noTerminalSet"))); } - var panes = new ArrayList(); - for (DataStoreEntryRef ref : getRefs()) { - var replacement = ProcessControlProvider.get().replace(ref); - ShellStore store = replacement.getStore().asNeeded(); - var control = store.standaloneControl(); - // These prepend scripts, not append - TerminalPromptManager.configurePromptScript(control); - ProcessControlProvider.get().withDefaultScripts(control); + var latch = new CountDownLatch(getRefs().size()); + var panes = new TerminalLauncher.Config[getRefs().size()]; + for (int i = 0; i < getRefs().size(); i++) { + var ii = i; + var ref = refs.get(i); + ThreadHelper.runFailableAsync(() -> { + try { + var replacement = ProcessControlProvider.get().replace(ref); + ShellStore store = replacement.getStore().asNeeded(); + var control = store.standaloneControl(); + // These prepend scripts, not append + TerminalPromptManager.configurePromptScript(control); + ProcessControlProvider.get().withDefaultScripts(control); - var title = DataStorage.get().getStoreEntryDisplayName(ref.get()); - var config = - new TerminalLauncher.Config(ref.get(), title, null, UUID.randomUUID(), true, true, control); - panes.add(config); + var title = DataStorage.get().getStoreEntryDisplayName(ref.get()); + var config = new TerminalLauncher.Config(ref.get(), title, null, UUID.randomUUID(), true, true, control); + panes[ii] = config; + } finally { + latch.countDown(); + } + }); } - TerminalLauncher.open(panes, true, type); + TerminalLauncher.open(Arrays.asList(panes), true, type); } } } diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryListBatchBarComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryListBatchBarComp.java index 3421cfe59..da0059f18 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryListBatchBarComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryListBatchBarComp.java @@ -113,6 +113,10 @@ public class StoreEntryListBatchBarComp extends SimpleRegionBuilder { return false; } + if (!w.getEntry().getValidity().isUsable() && s.requiresValidStore()) { + return false; + } + if (!s.isApplicable(w.getEntry().ref())) { return false; } diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreViewState.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreViewState.java index 3f8430db4..4285e4c7a 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreViewState.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreViewState.java @@ -70,10 +70,6 @@ public class StoreViewState { @Getter private final DerivedObservableList effectiveBatchModeSelection = batchModeSelection.filtered( storeEntryWrapper -> { - if (!storeEntryWrapper.getValidity().getValue().isUsable()) { - return false; - } - if (storeEntryWrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) { return false; } @@ -187,8 +183,7 @@ public class StoreViewState { batchModeSelection.getList().add(wrapper); } if (wrapper == null - || (wrapper.getValidity().getValue().isUsable() - && wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP)) { + || wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) { section.getShownChildren().getList().forEach(c -> selectBatchMode(c)); } } @@ -199,8 +194,7 @@ public class StoreViewState { batchModeSelection.getList().remove(wrapper); } if (wrapper == null - || (wrapper.getValidity().getValue().isUsable() - && wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP)) { + || wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) { section.getShownChildren().getList().forEach(c -> unselectBatchMode(c)); } } diff --git a/app/src/main/java/io/xpipe/app/util/CommandDialog.java b/app/src/main/java/io/xpipe/app/util/CommandDialog.java index cb6a9e2eb..f14ab8cdb 100644 --- a/app/src/main/java/io/xpipe/app/util/CommandDialog.java +++ b/app/src/main/java/io/xpipe/app/util/CommandDialog.java @@ -8,35 +8,55 @@ import io.xpipe.app.process.ProcessOutputException; import javafx.scene.control.TextArea; import javafx.scene.layout.StackPane; +import lombok.Value; import org.apache.commons.lang3.exception.ExceptionUtils; +import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.SequencedMap; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; public class CommandDialog { - public static void runMultipleAndShow(Map cmds) { - StringBuilder acc = new StringBuilder(); - for (var e : cmds.entrySet()) { - String out; - try { - out = e.getValue().readStdoutOrThrow(); - out = formatOutput(out); - } catch (ProcessOutputException ex) { - out = ex.getMessage(); - } catch (Throwable t) { - out = ExceptionUtils.getStackTrace(t); - } + @Value + public static class CommandEntry { - acc.append(e.getKey()) - .append(" (exit code ") - .append(e.getValue().getExitCode()) - .append("):\n") - .append(out) - .append("\n\n"); + String name; + CommandControl command; + } + + public static void runMultipleAndShow(List cmds) { + var parts = new String[cmds.size()]; + var latch = new CountDownLatch(parts.length); + for (int i = 0; i < cmds.size(); i++) { + var e = cmds.get(i); + var ii = i; + ThreadHelper.runAsync(() -> { + String out; + try { + out = e.getCommand().readStdoutOrThrow(); + out = formatOutput(out); + } catch (ProcessOutputException ex) { + out = ex.getMessage(); + } catch (Throwable t) { + out = ExceptionUtils.getStackTrace(t); + } + + var s = e.getName() + " (exit code " + e.getCommand().getExitCode() + "):\n" + out; + parts[ii] = s; + latch.countDown(); + }); } - show(acc.toString()); + + try { + latch.await(); + } catch (InterruptedException ignored) {} + + var joined = String.join("\n\n", parts); + show(joined); } public static void runAndShow(CommandControl cmd) { diff --git a/app/src/main/java/io/xpipe/app/util/ScanDialogBase.java b/app/src/main/java/io/xpipe/app/util/ScanDialogBase.java index 1c4ee643d..748bc2e28 100644 --- a/app/src/main/java/io/xpipe/app/util/ScanDialogBase.java +++ b/app/src/main/java/io/xpipe/app/util/ScanDialogBase.java @@ -137,10 +137,10 @@ public class ScanDialogBase { stackPane.getStyleClass().add("scan-list"); VBox.setVgrow(stackPane, ALWAYS); - var emptyLabel = new LabelComp(AppI18n.observable("noScanPossible")) - .visible(busy.not().and(Bindings.isEmpty(available))) - .build(); - stackPane.getChildren().add(emptyLabel); + if (!showButton) { + var emptyLabel = new LabelComp(AppI18n.observable("noScanPossible")).visible(busy.not().and(Bindings.isEmpty(available))).build(); + stackPane.getChildren().add(emptyLabel); + } Function nameFunc = (ScanProvider.ScanOpportunity s) -> { var n = s.getName().getValue(); diff --git a/dist/changelog/21.0.md b/dist/changelog/21.0.md index 65301987f..d57d90173 100644 --- a/dist/changelog/21.0.md +++ b/dist/changelog/21.0.md @@ -72,3 +72,5 @@ The scripting system has been completely reworked with the goal of becoming simp - Fix ordering for some child connections being random after a restart - Fix hcloud profile names containing whitespace when multiple profiles were configured - Fix WinSCP open action requiring an existing ppk key and only working with external key files, not in-place keys +- Fix batch mode selection not working for incomplete connections, like newly added VMs without credentials +- Fix batch action confirmation setting requiring a double confirmation for each individual connection diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/RunHubBatchScriptActionProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/RunHubBatchScriptActionProvider.java index 7dfdd9359..582cc2458 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/RunHubBatchScriptActionProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/RunHubBatchScriptActionProvider.java @@ -10,6 +10,7 @@ import io.xpipe.app.util.CommandDialog; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; +import java.util.ArrayList; import java.util.LinkedHashMap; public class RunHubBatchScriptActionProvider implements ActionProvider { @@ -27,14 +28,14 @@ public class RunHubBatchScriptActionProvider implements ActionProvider { @Override public void executeImpl() throws Exception { - var map = new LinkedHashMap(); + var list = new ArrayList(); for (DataStoreEntryRef ref : refs) { var sc = ref.getStore().getOrStartSession(); var script = scriptStore.getStore().assembleScriptChain(sc, false); var cmd = sc.command(script); - map.put(ref.get().getName(), cmd); + list.add(new CommandDialog.CommandEntry(ref.get().getName(), cmd)); } - CommandDialog.runMultipleAndShow(map); + CommandDialog.runMultipleAndShow(list); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/RunScriptActionProviderMenu.java b/ext/base/src/main/java/io/xpipe/ext/base/script/RunScriptActionProviderMenu.java index 713fabc43..d24c04ae2 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/RunScriptActionProviderMenu.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/RunScriptActionProviderMenu.java @@ -168,6 +168,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider ref) { return RunTerminalScriptActionProvider.Action.builder() @@ -176,6 +181,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider o) { return true; @@ -224,6 +234,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider ref) { return RunHubScriptActionProvider.Action.builder() @@ -285,6 +300,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider ref) { return RunBackgroundScriptActionProvider.Action.builder() @@ -331,6 +351,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider getChildren(List> batch) { return List.of(); @@ -402,6 +427,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider, BatchHubProvider { + @Override + public boolean requiresValidStore() { + return true; + } + @Override public void execute(DataStoreEntryRef ref) { var cat = StoreViewState.get().getAllScriptsCategory(); @@ -448,6 +478,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider, BatchHubProvider { + @Override + public boolean requiresValidStore() { + return true; + } + @Override public void execute(DataStoreEntryRef ref) { var cat = StoreViewState.get().getCategoryWrapper(DataStorage.get().getStoreCategory(ref.get())); @@ -495,6 +530,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider, BatchHubProvider { + @Override + public boolean requiresValidStore() { + return true; + } + @Override public boolean isApplicable(DataStoreEntryRef o) { return true; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceRefreshHubProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceRefreshHubProvider.java index 0ce985d93..3a67e9ee2 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceRefreshHubProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceRefreshHubProvider.java @@ -17,6 +17,11 @@ import lombok.extern.jackson.Jacksonized; public class ServiceRefreshHubProvider implements HubLeafProvider, BatchHubProvider { + @Override + public boolean requiresValidStore() { + return true; + } + @Override public StoreActionCategory getCategory() { return StoreActionCategory.CUSTOM; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseActionProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseActionProvider.java index a8ff515f5..ca6e85c3b 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseActionProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseActionProvider.java @@ -15,6 +15,11 @@ import lombok.extern.jackson.Jacksonized; public class StorePauseActionProvider implements HubLeafProvider, BatchHubProvider { + @Override + public boolean runParallel() { + return true; + } + @Override public StoreActionCategory getCategory() { return StoreActionCategory.CUSTOM; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreRestartActionProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreRestartActionProvider.java index 27757d54d..dd49a1c13 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreRestartActionProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreRestartActionProvider.java @@ -16,6 +16,11 @@ import lombok.extern.jackson.Jacksonized; public class StoreRestartActionProvider implements HubLeafProvider, BatchHubProvider { + @Override + public boolean runParallel() { + return true; + } + @Override public StoreActionCategory getCategory() { return StoreActionCategory.CUSTOM; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartActionProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartActionProvider.java index 910b0b2d4..a94e708e2 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartActionProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartActionProvider.java @@ -1,5 +1,6 @@ package io.xpipe.ext.base.store; +import io.xpipe.app.action.AbstractAction; import io.xpipe.app.core.AppI18n; import io.xpipe.app.hub.action.BatchHubProvider; import io.xpipe.app.hub.action.HubLeafProvider; @@ -13,6 +14,8 @@ import javafx.beans.value.ObservableValue; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; +import java.util.List; + public class StoreStartActionProvider implements HubLeafProvider, BatchHubProvider { @Override @@ -65,6 +68,11 @@ public class StoreStartActionProvider implements HubLeafProvider return "startStore"; } + @Override + public boolean runParallel() { + return true; + } + @Jacksonized @SuperBuilder public static class Action extends StoreAction { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopActionProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopActionProvider.java index adca0d3cf..bd4566ec0 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopActionProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopActionProvider.java @@ -15,6 +15,11 @@ import lombok.extern.jackson.Jacksonized; public class StoreStopActionProvider implements HubLeafProvider, BatchHubProvider { + @Override + public boolean runParallel() { + return true; + } + @Override public StoreActionCategory getCategory() { return StoreActionCategory.CUSTOM; diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index b97ab2443..9771cd65a 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -1964,7 +1964,7 @@ syncToPlainDirectory=Sync to plain directory syncToPlainDirectoryDescription=When syncing to a local directory, you can either treat this directory as another git repository or just as a plain directory. If the plain directory setting is enabled, the directory is not initialized as a git repository. openSpiceSession=Open SPICE session terminalBehaviour=Terminal behaviour -noScanPossible=No connection types are supported by the system +noScanPossible=No supported connections were found networkSwitchPorts=Network switch ports nswitchGroup.displayName=Network switch ports nswitchGroup.displayDescription=List available ports on a network switch device