diff --git a/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java b/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java index b3519aeab..ab7f1912d 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java @@ -56,7 +56,7 @@ public class ActionConfigComp extends SimpleRegionBuilder { }); var choice = new StoreListChoiceComp<>( - listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null, null); + listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null); choice.hide(listProp.emptyProperty()); choice.maxHeight(450); return choice; diff --git a/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java b/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java index 795efcfc7..810eccb16 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java @@ -54,7 +54,7 @@ public class ActionConfirmComp extends SimpleRegionBuilder { } var choice = new StoreListChoiceComp<>( - listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null, null); + listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null); choice.maxHeight(450); choice.setEditable(false); choice.hide(listProp.emptyProperty()); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreChoiceComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreChoiceComp.java index 5836d07f7..d68178754 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreChoiceComp.java @@ -24,8 +24,10 @@ import javafx.scene.layout.Region; import atlantafx.base.theme.Styles; import lombok.RequiredArgsConstructor; +import lombok.Setter; import org.kordamp.ikonli.javafx.FontIcon; +import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; @@ -36,6 +38,9 @@ public class StoreChoiceComp extends SimpleRegionBuilder { private StoreChoicePopover popover; + @Setter + private boolean editable = true; + public StoreChoiceComp( DataStoreEntry self, ObjectProperty> selected, @@ -200,7 +205,12 @@ public class StoreChoiceComp extends SimpleRegionBuilder { }, selected)); editButton.describe(d -> d.nameKey("edit")); - var box = new InputGroupComp(List.of(RegionBuilder.of(() -> pane).hgrow(), editButton)); + var l = new ArrayList>(); + l.add(RegionBuilder.of(() -> pane).hgrow()); + if (editable) { + l.add(editButton); + } + var box = new InputGroupComp(l); box.setMainReference(0); box.style("store-choice-comp"); return box.build(); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java index a874b5191..b1cff4039 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java @@ -572,6 +572,7 @@ public class StoreEntryWrapper { } l.add(information.getValue()); l.add(summary.getValue()); + l.add(notes.getValue()); l.addAll(tags); return filter.matches(l); } diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreListChoiceComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreListChoiceComp.java index 5edd3ff77..f69a20a91 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreListChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreListChoiceComp.java @@ -7,14 +7,17 @@ import io.xpipe.app.comp.base.*; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.DataStore; import io.xpipe.app.ext.DataStoreCreationCategory; +import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; import javafx.beans.binding.Bindings; import javafx.beans.property.ListProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; +import org.int4.fx.builders.common.AbstractRegionBuilder; import java.util.ArrayList; import java.util.List; @@ -22,12 +25,11 @@ import java.util.function.Predicate; public class StoreListChoiceComp extends SimpleRegionBuilder { - private final ListProperty> selectedList; + protected final ListProperty> selectedList; private final Class storeClass; private final Predicate> applicableCheck; private final StoreCategoryWrapper initialCategory; private final DataStoreCreationCategory creationCategory; - private final Predicate> activeCheck; private boolean editable; public StoreListChoiceComp( @@ -35,14 +37,13 @@ public class StoreListChoiceComp extends SimpleRegionBuilde Class storeClass, Predicate> applicableCheck, StoreCategoryWrapper initialCategory, - DataStoreCreationCategory creationCategory, Predicate> activeCheck + DataStoreCreationCategory creationCategory ) { this.selectedList = selectedList; this.storeClass = storeClass; this.applicableCheck = applicableCheck; this.initialCategory = initialCategory; this.creationCategory = creationCategory; - this.activeCheck = activeCheck; this.editable = true; } @@ -51,6 +52,18 @@ public class StoreListChoiceComp extends SimpleRegionBuilde return this; } + protected ObservableValue getName(DataStoreEntryRef ref) { + var labelName = Bindings.createStringBinding(() -> { + var base = ref.get().getName(); + return base; + }, selectedList, AppI18n.activeLanguage()); + return labelName; + } + + protected BaseRegionBuilder buildCustomButtons(DataStoreEntryRef ref) { + return null; + } + @Override protected Region createSimple() { var listBox = new ListBoxViewComp<>( @@ -61,12 +74,7 @@ public class StoreListChoiceComp extends SimpleRegionBuilde return null; } - var labelName = Bindings.createStringBinding(() -> { - var base = t.get().getName(); - var active = activeCheck != null && activeCheck.test(t); - return base + (active ? " (" + AppI18n.get("active") + ")" : ""); - }, selectedList, AppI18n.activeLanguage()); - + var labelName = getName(t); var label = new LabelComp(labelName).apply(struc -> { struc.setGraphic(PrettyImageHelper.ofFixedSizeSquare( t.get().getEffectiveIconFile(), 16) @@ -82,6 +90,7 @@ public class StoreListChoiceComp extends SimpleRegionBuilde selectedList.get().add(prior, t); } }); + up.describe(d -> d.nameKey("moveUp")); up.disable(Bindings.createBooleanBinding( () -> { return selectedList.get().indexOf(t) == 0; @@ -96,6 +105,7 @@ public class StoreListChoiceComp extends SimpleRegionBuilde selectedList.get().add(next, t); } }); + down.describe(d -> d.nameKey("moveDown")); down.disable(Bindings.createBooleanBinding( () -> { return selectedList.get().indexOf(t) == selectedList.size() - 1; @@ -105,10 +115,20 @@ public class StoreListChoiceComp extends SimpleRegionBuilde var delete = new IconButtonComp("mdal-delete_outline", () -> { selectedList.remove(t); }); - var row = editable - ? new HorizontalComp(List.of(label, RegionBuilder.hspacer(), up, down, delete)) - .spacing(5) - : new HorizontalComp(List.of(label, RegionBuilder.hspacer())); + delete.describe(d -> d.nameKey("delete")); + var l = new ArrayList>(); + l.add(label); + l.add(RegionBuilder.hspacer()); + var custom = buildCustomButtons(t); + if (custom != null) { + l.add(custom); + } + if (editable) { + l.add(up); + l.add(down); + l.add(delete); + } + var row = new HorizontalComp(l).spacing(5); return row.style("entry"); }, false) @@ -117,6 +137,7 @@ public class StoreListChoiceComp extends SimpleRegionBuilde .apply(struc -> ((VBox) struc.getContent()).setSpacing(5)); var selected = new SimpleObjectProperty>(); var add = new StoreChoiceComp<>(null, selected, storeClass, applicableCheck, initialCategory, creationCategory); + add.setEditable(false); selected.addListener((observable, oldValue, newValue) -> { if (newValue != null) { if (!selectedList.contains(newValue) && (applicableCheck == null || applicableCheck.test(newValue))) { diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index 8a68390ce..5bfcb630f 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -362,6 +362,7 @@ public abstract class DataStorage { finalizeWithDependencies(entry); + var flatChildren = getStoreChildren(entry); var children = getDeepStoreChildren(entry); if (!sameParent) { var toRemove = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new); @@ -407,6 +408,12 @@ public abstract class DataStorage { listeners.forEach(storageListener -> storageListener.onStoreListUpdate()); } + entry.setChildrenCache(null); + var newFlatChildren = getStoreChildren(entry); + if (!flatChildren.equals(newFlatChildren)) { + listeners.forEach(storageListener -> storageListener.onStoreListUpdate()); + } + if (userScopeChanged) { updateUserScope(entry); } diff --git a/dist/changelog/24.0.md b/dist/changelog/24.0.md index 0184cbe11..138b7e9e8 100644 --- a/dist/changelog/24.0.md +++ b/dist/changelog/24.0.md @@ -46,6 +46,7 @@ The latest release of the [apple container runtime](https://github.com/apple/con - Add support for the wayland versions of freerdp - You can now configure additional custom launch arguments/options for Remmina and FreeRDP in the settings menu - Improve key mapping settings for Hashicorp Vault and OpenBao +- The search filter now also checks for any associated connection notes ## Fixes diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityApplyHubLeafProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityApplyHubLeafProvider.java index 08a3874bc..9545ac0a2 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityApplyHubLeafProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityApplyHubLeafProvider.java @@ -82,7 +82,11 @@ public class IdentityApplyHubLeafProvider implements HubLeafProvider { null, selectedReference, IdentityStore.class, - null, + ref -> !MultiIdentityStore.isExclusivelyHeld(ref), StoreViewState.get().getAllIdentitiesCategory(), null, true, diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStore.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStore.java index c040af7ba..f68c3182d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStore.java @@ -7,6 +7,7 @@ import io.xpipe.app.ext.SelfReferentialStore; import io.xpipe.app.ext.ValidationException; import io.xpipe.app.secret.SecretRetrievalStrategy; +import io.xpipe.app.storage.DataStoreEntryRef; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -18,6 +19,8 @@ import lombok.experimental.SuperBuilder; @Getter public abstract class IdentityStore implements SelfReferentialStore, DataStore { + public abstract DataStoreEntryRef getCustomEditTarget(); + public abstract UsernameStrategy getUsername(); public abstract SecretRetrievalStrategy getPassword(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStoreProvider.java index c8babb328..1a34df474 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityStoreProvider.java @@ -4,6 +4,8 @@ import io.xpipe.app.comp.BaseRegionBuilder; import io.xpipe.app.ext.*; import io.xpipe.app.hub.comp.StoreSection; import io.xpipe.app.hub.comp.SystemStateComp; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.DocumentationLink; import javafx.beans.binding.Bindings; @@ -14,6 +16,11 @@ import java.util.List; public abstract class IdentityStoreProvider implements DataStoreProvider { + @Override + public DataStoreEntry getDisplayParent(DataStoreEntry store) { + return MultiIdentityStore.getExclusiveHolder(store.ref()).map(DataStoreEntryRef::get).orElse(null); + } + @Override public DocumentationLink getHelpLink() { return DocumentationLink.IDENTITIES; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/LocalIdentityStore.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/LocalIdentityStore.java index e07102c99..e96a98924 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/LocalIdentityStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/LocalIdentityStore.java @@ -27,6 +27,11 @@ public class LocalIdentityStore extends IdentityStore { EncryptedValue password; EncryptedValue sshIdentity; + @Override + public DataStoreEntryRef getCustomEditTarget() { + return null; + } + public UsernameStrategy.Fixed getUsername() { return new UsernameStrategy.Fixed(username); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStore.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStore.java index acc3d3188..ee3ebc7bd 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStore.java @@ -4,12 +4,14 @@ import io.xpipe.app.cred.SshIdentityStrategy; import io.xpipe.app.cred.UsernameStrategy; import io.xpipe.app.ext.StatefulDataStore; import io.xpipe.app.ext.UserScopeStore; +import io.xpipe.app.ext.ValidationException; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.secret.SecretRetrievalStrategy; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntryRef; import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.util.Validators; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.Value; @@ -29,8 +31,20 @@ import java.util.UUID; public class MultiIdentityStore extends IdentityStore implements StatefulDataStore, UserScopeStore { + public static boolean isExclusivelyHeld(DataStoreEntryRef ref) { + return getExclusiveHolder(ref).isPresent(); + } + + public static Optional> getExclusiveHolder(DataStoreEntryRef ref) { + var exclusiveHolder = DataStorage.get().getStoreEntries().stream() + .filter(entry -> entry.getValidity().isUsable() && entry.getStore() instanceof MultiIdentityStore m && + m.getExclusive() != null && m.getExclusive() && m.getAvailableIdentities().contains(ref)).findFirst(); + return exclusiveHolder.map(entry -> entry.ref()); + } + List identities; - boolean perUser; + Boolean exclusive; + Boolean perUser; @Override public String getName() { @@ -87,14 +101,19 @@ public class MultiIdentityStore extends IdentityStore @Override public List> getDependencies() { - return List.of(); + return getAvailableIdentities().stream().>map(DataStoreEntryRef::asNeeded).toList(); } @Override - public void checkComplete() { + public void checkComplete() throws ValidationException { getSelectedOrThrow(); } + @Override + public DataStoreEntryRef getCustomEditTarget() { + return getSelected().orElse(null); + } + public UsernameStrategy getUsername() { return getSelectedOrThrow().getStore().getUsername(); } @@ -113,4 +132,9 @@ public class MultiIdentityStore extends IdentityStore public Class getStateClass() { return MultiIdentityStoreState.class; } + + @Override + public boolean isPerUser() { + return perUser != null && perUser; + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStoreProvider.java index 016adbc71..3863aa2f6 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/MultiIdentityStoreProvider.java @@ -1,5 +1,9 @@ package io.xpipe.ext.base.identity; +import io.xpipe.app.comp.BaseRegionBuilder; +import io.xpipe.app.comp.RegionBuilder; +import io.xpipe.app.comp.base.HorizontalComp; +import io.xpipe.app.comp.base.IconButtonComp; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.DataStore; import io.xpipe.app.ext.DataStoreCreationCategory; @@ -10,9 +14,13 @@ import io.xpipe.app.hub.comp.StoreViewState; import io.xpipe.app.platform.OptionsBuilder; import io.xpipe.app.storage.*; +import io.xpipe.app.util.ObservableSubscriber; +import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import java.util.ArrayList; @@ -43,22 +51,52 @@ public class MultiIdentityStoreProvider extends IdentityStoreProvider { uuid, DataStorage.DEFAULT_CATEGORY_UUID, AppI18n.get("unknown"), null))); } } - var perUser = new SimpleBooleanProperty(st.isPerUser()); + var exclusive = new SimpleObjectProperty<>(st.getExclusive()); + var perUser = new SimpleObjectProperty<>(st.getPerUser()); - return new OptionsBuilder() + var listUpdate = new ObservableSubscriber(); + var selected = new SimpleObjectProperty>(st.getSelected().orElse(null)); + identities.addListener((observable, oldValue, newValue) -> { + var hasActive = identities.contains(selected.get()); + if (!hasActive) { + selected.set(identities.stream().filter(ref -> ref.get().getValidity().isUsable()).findFirst().orElse(null)); + listUpdate.trigger(); + } + }); + + var choice = new StoreListChoiceComp<>(identities, IdentityStore.class, + ref -> !(ref.get().equals(model.getExistingEntry())) && !identities.contains(ref) && !MultiIdentityStore.isExclusivelyHeld(ref), + StoreViewState.get().getAllIdentitiesCategory(), + DataStoreCreationCategory.IDENTITY) { + + @Override + protected ObservableValue getName(DataStoreEntryRef ref) { + var labelName = Bindings.createStringBinding(() -> { + var base = ref.get().getName(); + var active = ref.equals(selected.get()); + return base + (active ? " (" + AppI18n.get("active") + ")" : ""); + }, selectedList, AppI18n.activeLanguage(), listUpdate); + return labelName; + } + + @Override + protected BaseRegionBuilder buildCustomButtons(DataStoreEntryRef ref) { + var select = new IconButtonComp("mdi2i-image-filter-center-focus", () -> { + st.select(ref); + selected.set(ref); + listUpdate.trigger(); + }); + select.disable(ref.get().getProvider() == null); + select.describe(d -> d.nameKey("makeActive")); + return new HorizontalComp(List.of(select, RegionBuilder.hspacer(5))); + } + }; + var options = new OptionsBuilder() .nameAndDescription("multiIdentityList") - .addComp( - new StoreListChoiceComp<>( - identities, - IdentityStore.class, - ref -> !(ref.getStore() instanceof MultiIdentityStore) && !identities.contains(ref), - StoreViewState.get().getAllIdentitiesCategory(), - DataStoreCreationCategory.IDENTITY, - ref -> { - var l = identities.stream().filter(r -> r.getStore() != null).toList(); - return l.size() > 0 && l.getFirst() == ref; - }), + .addComp(choice, identities) + .nameAndDescription("multiIdentityExclusive") + .addToggle(exclusive) .nameAndDescription( DataStorageUserHandler.getInstance().getActiveUser() != null ? "identityPerUser" @@ -75,6 +113,7 @@ public class MultiIdentityStoreProvider extends IdentityStoreProvider { return MultiIdentityStore.builder() .identities(st.getIdentities()) .perUser(perUser.get()) + .exclusive(exclusive.get()) .build(); } @@ -85,11 +124,16 @@ public class MultiIdentityStoreProvider extends IdentityStoreProvider { return MultiIdentityStore.builder() .identities(all) + .exclusive(exclusive.get()) .perUser(perUser.get()) .build(); }, - store) - .buildDialog(); + store); + var dialog = new GuiDialog(options, entry -> { + var finalStore = (MultiIdentityStore) entry.getStore(); + finalStore.select(selected.get()); + }); + return dialog; } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStore.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStore.java index 79de30e68..1917f4a90 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStore.java @@ -117,6 +117,11 @@ public class PasswordManagerIdentityStore extends IdentityStore return r; } + @Override + public DataStoreEntryRef getCustomEditTarget() { + return null; + } + public UsernameStrategy getUsername() { return new UsernameStrategy.Dynamic(() -> { var r = retrieve(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java index f896ad2f1..acf6d76bb 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/SyncedIdentityStore.java @@ -38,6 +38,11 @@ public class SyncedIdentityStore extends IdentityStore implements UserScopeStore return getSelfEntry().getName(); } + @Override + public DataStoreEntryRef getCustomEditTarget() { + return null; + } + public UsernameStrategy.Fixed getUsername() { return new UsernameStrategy.Fixed(username); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreProvider.java index 347189ba3..314ef2866 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreProvider.java @@ -152,8 +152,7 @@ public class ScriptStoreProvider implements DataStoreProvider { scriptStore -> !scriptStore.get().equals(model.getExistingEntry()) && !others.contains(scriptStore), StoreViewState.get().getAllScriptsCategory(), - DataStoreCreationCategory.SCRIPT, - null), + DataStoreCreationCategory.SCRIPT), others) .bind( () -> { diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 4abaa2366..f4e921d83 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -2361,3 +2361,8 @@ webtopQrCodeTitle=Scan the code for $NAME$ with the XPipe app done=Done webtopConnectHostChoice=Host address webtopConnectHostChoiceDescription=The externally reachable hostname/IP and the webtop container SSH port +moveDown=Move down +moveUp=Move up +makeActive=Make active +multiIdentityExclusive=Make identities exclusive +multiIdentityExclusiveDescription=Show all contained identities as children of this multi identity for better organization