Rework identities

This commit is contained in:
crschnick
2026-07-04 01:31:26 +00:00
parent 78776acad4
commit 32e89fef89
18 changed files with 180 additions and 39 deletions
@@ -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;
@@ -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());
@@ -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<T extends DataStore> extends SimpleRegionBuilder {
private StoreChoicePopover<T> popover;
@Setter
private boolean editable = true;
public StoreChoiceComp(
DataStoreEntry self,
ObjectProperty<DataStoreEntryRef<T>> selected,
@@ -200,7 +205,12 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleRegionBuilder {
}, selected));
editButton.describe(d -> d.nameKey("edit"));
var box = new InputGroupComp(List.of(RegionBuilder.of(() -> pane).hgrow(), editButton));
var l = new ArrayList<BaseRegionBuilder<?,?>>();
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();
@@ -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);
}
@@ -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<T extends DataStore> extends SimpleRegionBuilder {
private final ListProperty<DataStoreEntryRef<T>> selectedList;
protected final ListProperty<DataStoreEntryRef<T>> selectedList;
private final Class<T> storeClass;
private final Predicate<DataStoreEntryRef<T>> applicableCheck;
private final StoreCategoryWrapper initialCategory;
private final DataStoreCreationCategory creationCategory;
private final Predicate<DataStoreEntryRef<T>> activeCheck;
private boolean editable;
public StoreListChoiceComp(
@@ -35,14 +37,13 @@ public class StoreListChoiceComp<T extends DataStore> extends SimpleRegionBuilde
Class<T> storeClass,
Predicate<DataStoreEntryRef<T>> applicableCheck,
StoreCategoryWrapper initialCategory,
DataStoreCreationCategory creationCategory, Predicate<DataStoreEntryRef<T>> 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<T extends DataStore> extends SimpleRegionBuilde
return this;
}
protected ObservableValue<String> getName(DataStoreEntryRef<T> ref) {
var labelName = Bindings.createStringBinding(() -> {
var base = ref.get().getName();
return base;
}, selectedList, AppI18n.activeLanguage());
return labelName;
}
protected BaseRegionBuilder<?, ?> buildCustomButtons(DataStoreEntryRef<T> ref) {
return null;
}
@Override
protected Region createSimple() {
var listBox = new ListBoxViewComp<>(
@@ -61,12 +74,7 @@ public class StoreListChoiceComp<T extends DataStore> 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<T extends DataStore> 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<T extends DataStore> 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<T extends DataStore> 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<BaseRegionBuilder<?, ?>>();
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<T extends DataStore> extends SimpleRegionBuilde
.apply(struc -> ((VBox) struc.getContent()).setSpacing(5));
var selected = new SimpleObjectProperty<DataStoreEntryRef<T>>();
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))) {
@@ -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);
}
+1
View File
@@ -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
@@ -82,7 +82,11 @@ public class IdentityApplyHubLeafProvider implements HubLeafProvider<IdentitySto
&& !(ref.getStore().getSshIdentity() instanceof NoIdentityStrategy)
&& ref.getStore().getSshIdentity().getPublicKeyStrategy() == null) {
AppDialog.confirm("identityApplyMissingPublicKey");
StoreCreationDialog.showEdit(ref.get());
var toEdit = ref.getStore().getCustomEditTarget();
if (toEdit == null) {
toEdit = ref;
}
StoreCreationDialog.showEdit(toEdit.get());
return;
}
@@ -315,7 +315,7 @@ public class IdentitySelectComp extends RegionBuilder<HBox> {
null,
selectedReference,
IdentityStore.class,
null,
ref -> !MultiIdentityStore.isExclusivelyHeld(ref),
StoreViewState.get().getAllIdentitiesCategory(),
null,
true,
@@ -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<IdentityStore> getCustomEditTarget();
public abstract UsernameStrategy getUsername();
public abstract SecretRetrievalStrategy getPassword();
@@ -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;
@@ -27,6 +27,11 @@ public class LocalIdentityStore extends IdentityStore {
EncryptedValue<SecretRetrievalStrategy> password;
EncryptedValue<SshIdentityStrategy> sshIdentity;
@Override
public DataStoreEntryRef<IdentityStore> getCustomEditTarget() {
return null;
}
public UsernameStrategy.Fixed getUsername() {
return new UsernameStrategy.Fixed(username);
}
@@ -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<MultiIdentityStoreState>, UserScopeStore {
public static boolean isExclusivelyHeld(DataStoreEntryRef<IdentityStore> ref) {
return getExclusiveHolder(ref).isPresent();
}
public static Optional<DataStoreEntryRef<MultiIdentityStore>> getExclusiveHolder(DataStoreEntryRef<IdentityStore> 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<UUID> identities;
boolean perUser;
Boolean exclusive;
Boolean perUser;
@Override
public String getName() {
@@ -87,14 +101,19 @@ public class MultiIdentityStore extends IdentityStore
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return List.of();
return getAvailableIdentities().stream().<DataStoreEntryRef<?>>map(DataStoreEntryRef::asNeeded).toList();
}
@Override
public void checkComplete() {
public void checkComplete() throws ValidationException {
getSelectedOrThrow();
}
@Override
public DataStoreEntryRef<IdentityStore> getCustomEditTarget() {
return getSelected().orElse(null);
}
public UsernameStrategy getUsername() {
return getSelectedOrThrow().getStore().getUsername();
}
@@ -113,4 +132,9 @@ public class MultiIdentityStore extends IdentityStore
public Class<MultiIdentityStoreState> getStateClass() {
return MultiIdentityStoreState.class;
}
@Override
public boolean isPerUser() {
return perUser != null && perUser;
}
}
@@ -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<DataStoreEntryRef<IdentityStore>>(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<String> getName(DataStoreEntryRef<IdentityStore> 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<IdentityStore> 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
@@ -117,6 +117,11 @@ public class PasswordManagerIdentityStore extends IdentityStore
return r;
}
@Override
public DataStoreEntryRef<IdentityStore> getCustomEditTarget() {
return null;
}
public UsernameStrategy getUsername() {
return new UsernameStrategy.Dynamic(() -> {
var r = retrieve();
@@ -38,6 +38,11 @@ public class SyncedIdentityStore extends IdentityStore implements UserScopeStore
return getSelfEntry().getName();
}
@Override
public DataStoreEntryRef<IdentityStore> getCustomEditTarget() {
return null;
}
public UsernameStrategy.Fixed getUsername() {
return new UsernameStrategy.Fixed(username);
}
@@ -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(
() -> {
+5
View File
@@ -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