You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2022/05/26 11:55:09 UTC

[syncope] 04/04: Further Console extendability improvements

This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git

commit 382738527a32c01907f01982097c996dd672f14d
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Thu May 26 13:22:41 2022 +0200

    Further Console extendability improvements
---
 .../client/console/panels/RealmChoicePanel.java    |  72 ++++++++----
 .../client/console/widgets/NumberWidget.java       |  22 ++--
 .../syncope/client/console/wizards/any/Groups.java |  70 ++++++------
 .../client/console/wizards/any/Relationships.java  | 123 ++++++++++++---------
 .../client/console/panels/RealmChoicePanel.html    |   6 +-
 .../apache/syncope/fit/console/RealmsITCase.java   |  14 ++-
 .../reference-guide/usage/customization.adoc       |   4 +-
 7 files changed, 182 insertions(+), 129 deletions(-)

diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
index 9313bacd1e..07d30b2ff6 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
@@ -47,6 +47,7 @@ import org.apache.syncope.common.lib.to.RealmTO;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
 import org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
 import org.apache.wicket.event.Broadcast;
 import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AbstractAutoCompleteRenderer;
@@ -55,11 +56,11 @@ import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteSe
 import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteTextField;
 import org.apache.wicket.extensions.ajax.markup.html.autocomplete.IAutoCompleteRenderer;
 import org.apache.wicket.markup.ComponentTag;
-import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.link.AbstractLink;
+import org.apache.wicket.markup.html.list.ListItem;
+import org.apache.wicket.markup.html.list.ListView;
 import org.apache.wicket.markup.html.panel.Fragment;
 import org.apache.wicket.markup.html.panel.Panel;
-import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.LoadableDetachableModel;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.model.ResourceModel;
@@ -91,9 +92,7 @@ public class RealmChoicePanel extends Panel {
 
     protected final boolean isSearchEnabled;
 
-    protected final Label realmDisplayKey;
-
-    protected final Label realmDisplayValue;
+    protected final ListView<String> breadcrumb;
 
     public RealmChoicePanel(final String id, final String initialRealm, final PageReference pageRef) {
         super(id);
@@ -169,33 +168,60 @@ public class RealmChoicePanel extends Panel {
         container = new WebMarkupContainerNoVeil("container", realmTree);
         add(container.setOutputMarkupId(true));
 
-        realmDisplayKey = new Label("realmDisplayKey", realmDisplayKeyModel(null));
-        container.addOrReplace(realmDisplayKey.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
-        realmDisplayValue = new Label("realmDisplayValue", realmDisplayValueText());
-        container.addOrReplace(realmDisplayValue.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
+        breadcrumb = new ListView<String>("breadcrumb") {
+
+            private static final long serialVersionUID = -8746795666847966508L;
+
+            @Override
+            protected void populateItem(final ListItem<String> item) {
+                AjaxLink<Void> bcitem = new AjaxLink<>("bcitem") {
+
+                    private static final long serialVersionUID = -817438685948164787L;
+
+                    @Override
+                    public void onClick(final AjaxRequestTarget target) {
+                        RealmRestClient.list(item.getModelObject()).stream().
+                                filter(r -> item.getModelObject().equals(r.getFullPath())).
+                                findFirst().ifPresent(t -> chooseRealm(t, target));
+                    }
+                };
+                bcitem.setBody(Model.of(SyncopeConstants.ROOT_REALM.equals(item.getModelObject())
+                        ? SyncopeConstants.ROOT_REALM
+                        : StringUtils.substringAfterLast(item.getModelObject(), "/")));
+                bcitem.setEnabled(!model.getObject().getFullPath().equals(item.getModelObject()));
+                item.add(bcitem);
+            }
+        };
+        container.addOrReplace(breadcrumb.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
+        setBreadcrumb(model.getObject());
 
         reloadRealmTree();
     }
 
-    protected IModel<String> realmDisplayKeyModel(final Boolean dynamic) {
-        return dynamic == null
-                ? model.getObject().getFullPath().startsWith(SyncopeConstants.ROOT_REALM)
-                ? new ResourceModel("realmDisplayKey", "Realm")
-                : new ResourceModel("dynRealmLabel", "Dynamic Realm")
-                : dynamic
-                        ? new ResourceModel("dynRealmLabel", "Dynamic Realm")
-                        : new ResourceModel("realmDisplayKey", "Realm");
-    }
+    protected void setBreadcrumb(final RealmTO realm) {
+        if (SyncopeConstants.ROOT_REALM.equals(realm.getFullPath())) {
+            breadcrumb.setList(List.of(realm.getFullPath()));
+        } else {
+            List<String> bcitems = new ArrayList<>();
+            bcitems.add(SyncopeConstants.ROOT_REALM);
+
+            String[] split = realm.getFullPath().split("/");
+            for (int i = 1; i < split.length; i++) {
+                StringBuilder bcitem = new StringBuilder();
+                for (int j = 1; j <= i; j++) {
+                    bcitem.append('/').append(split[j]);
+                }
+                bcitems.add(bcitem.toString());
+            }
 
-    protected String realmDisplayValueText() {
-        return RealmsUtils.getFullPath(model.getObject().getFullPath());
+            breadcrumb.setList(bcitems);
+        }
     }
 
     protected void chooseRealm(final RealmTO realm, final AjaxRequestTarget target) {
         model.setObject(realm);
-        realmDisplayValue.setDefaultModelObject(realmDisplayValueText());
-        realmDisplayKey.setDefaultModel(realmDisplayKeyModel(false));
-        target.add(realmDisplayValue);
+        setBreadcrumb(realm);
+        target.add(container);
         send(pageRef.getPage(), Broadcast.EXACT, new ChosenRealm<>(realm, target));
     }
 
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/NumberWidget.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/NumberWidget.java
index 1f720a6f32..dd6933301d 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/NumberWidget.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/NumberWidget.java
@@ -20,6 +20,8 @@ package org.apache.syncope.client.console.widgets;
 
 import java.util.List;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.SyncopeWebApplication;
+import org.apache.syncope.client.console.pages.BasePage;
 import org.apache.syncope.client.console.rest.AnyTypeRestClient;
 import org.apache.wicket.behavior.AttributeAppender;
 import org.apache.wicket.markup.html.WebMarkupContainer;
@@ -29,7 +31,6 @@ import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.syncope.client.console.pages.Realms;
 import org.apache.syncope.client.console.pages.Security;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
-import org.apache.wicket.request.component.IRequestablePage;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 
 public class NumberWidget extends BaseWidget {
@@ -48,20 +49,27 @@ public class NumberWidget extends BaseWidget {
         WebMarkupContainer box = new WebMarkupContainer("card");
         box.add(new AttributeAppender("class", ' ' + bg));
 
+        @SuppressWarnings("unchecked")
+        Class<? extends BasePage> realmsPage =
+                (Class<? extends BasePage>) SyncopeWebApplication.get().getPageClass("realms");
+        if (realmsPage == null) {
+            realmsPage = Realms.class;
+        }
+
         boolean isAuthorized = true;
         PageParameters pageParameters = new PageParameters();
-        Class<? extends IRequestablePage> responsePage;
+        Class<? extends BasePage> responsePage;
         List<String> anyTypes = AnyTypeRestClient.list();
         switch (id) {
             case "totalUsers":
                 pageParameters.add(Realms.SELECTED_INDEX, 1);
-                responsePage = Realms.class;
+                responsePage = realmsPage;
                 isAuthorized = SyncopeConsoleSession.get().owns(IdRepoEntitlement.USER_SEARCH);
                 break;
 
             case "totalGroups":
                 pageParameters.add(Realms.SELECTED_INDEX, 2);
-                responsePage = Realms.class;
+                responsePage = realmsPage;
                 isAuthorized = SyncopeConsoleSession.get().owns(IdRepoEntitlement.GROUP_SEARCH);
                 break;
 
@@ -74,7 +82,7 @@ public class NumberWidget extends BaseWidget {
                             pageParameters.add(Realms.SELECTED_INDEX, selectedIndex);
                         }
                     }
-                    responsePage = Realms.class;
+                    responsePage = realmsPage;
                     isAuthorized = SyncopeConsoleSession.get().owns(label + "_SEARCH");
                 } else {
                     responsePage = Security.class;
@@ -90,13 +98,13 @@ public class NumberWidget extends BaseWidget {
                         pageParameters.add(Realms.SELECTED_INDEX, selectedIndex);
                     }
                 }
-                responsePage = Realms.class;
+                responsePage = realmsPage;
                 isAuthorized = SyncopeConsoleSession.get().owns(label + "_SEARCH");
                 break;
 
             default:
                 pageParameters.add(Realms.SELECTED_INDEX, 0);
-                responsePage = Realms.class;
+                responsePage = realmsPage;
         }
 
         AjaxEventBehavior clickToRealms = new AjaxEventBehavior("mousedown") {
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Groups.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Groups.java
index e1a0c2988a..b4d88faf04 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Groups.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Groups.java
@@ -86,6 +86,10 @@ public class Groups extends AbstractGroups {
         addDynamicRealmsContainer();
     }
 
+    protected List<GroupTO> searchAssignable(final String realm, final String term) {
+        return SyncopeRestClient.searchAssignableGroups(realm, term, 1, Constants.MAX_GROUP_LIST_SIZE);
+    }
+
     @Override
     protected void addDynamicRealmsContainer() {
         dynrealmsContainer = new WebMarkupContainer("dynrealmsContainer");
@@ -123,53 +127,49 @@ public class Groups extends AbstractGroups {
 
                         @Override
                         public MembershipTO getObject(
-                            final String id, final IModel<? extends List<? extends MembershipTO>> choices) {
+                                final String id, final IModel<? extends List<? extends MembershipTO>> choices) {
 
                             return choices.getObject().stream().
-                                filter(object -> id.equalsIgnoreCase(object.getGroupName())).findAny().orElse(null);
+                                    filter(object -> id.equalsIgnoreCase(object.getGroupName())).findAny().orElse(null);
                         }
                     });
 
-            groupsContainer.add(builder.setAllowOrder(true).withFilter("*").build("groups",
-                new ListModel<>() {
+            groupsContainer.add(builder.setAllowOrder(true).withFilter("*").build("groups", new ListModel<>() {
 
-                    private static final long serialVersionUID = -2583290457773357445L;
+                private static final long serialVersionUID = -2583290457773357445L;
 
-                    @Override
-                    public List<MembershipTO> getObject() {
-                        return Groups.this.groupsModel.getMemberships();
-                    }
-
-                }, new AjaxPalettePanel.Builder.Query<>() {
+                @Override
+                public List<MembershipTO> getObject() {
+                    return Groups.this.groupsModel.getMemberships();
+                }
+            }, new AjaxPalettePanel.Builder.Query<>() {
 
-                    private static final long serialVersionUID = -7223078772249308813L;
+                private static final long serialVersionUID = -7223078772249308813L;
 
-                    @Override
-                    public List<MembershipTO> execute(final String filter) {
-                        return StringUtils.isEmpty(filter)
+                @Override
+                public List<MembershipTO> execute(final String filter) {
+                    return StringUtils.isEmpty(filter)
                             ? List.of()
                             : ("*".equals(filter)
-                            ? groupsModel.getObject()
-                            : SyncopeRestClient.searchAssignableGroups(
-                            anyTO.getRealm(), filter, 1, Constants.MAX_GROUP_LIST_SIZE)).stream().
-                            map(group -> new MembershipTO.Builder(group.getKey()).
-                                groupName(group.getName()).build()).
-                            collect(Collectors.toList());
-                    }
-                }).hideLabel().setOutputMarkupId(true));
+                                    ? groupsModel.getObject()
+                                    : searchAssignable(anyTO.getRealm(), filter)).stream().
+                                    map(group -> new MembershipTO.Builder(group.getKey()).
+                                    groupName(group.getName()).build()).
+                                    collect(Collectors.toList());
+                }
+            }).hideLabel().setOutputMarkupId(true));
 
             dyngroupsContainer.add(new AjaxPalettePanel.Builder<String>().setAllowOrder(true).build("dyngroups",
-                new ListModel<>() {
-
-                    private static final long serialVersionUID = -2583290457773357445L;
+                    new ListModel<>() {
 
-                    @Override
-                    public List<String> getObject() {
-                        return Groups.this.groupsModel.getDynMemberships();
-                    }
+                private static final long serialVersionUID = -2583290457773357445L;
 
-                }, new ListModel<>(groupsModel.getObject().stream().
-                        map(GroupTO::getName).collect(Collectors.toList()))).
+                @Override
+                public List<String> getObject() {
+                    return Groups.this.groupsModel.getDynMemberships();
+                }
+            }, new ListModel<>(groupsModel.getObject().stream().
+                            map(GroupTO::getName).collect(Collectors.toList()))).
                     hideLabel().setEnabled(false).setOutputMarkupId(true));
         }
     }
@@ -214,11 +214,7 @@ public class Groups extends AbstractGroups {
          */
         @Override
         protected void reloadObject() {
-            groupsObj = SyncopeRestClient.searchAssignableGroups(
-                    realmObj,
-                    null,
-                    1,
-                    Constants.MAX_GROUP_LIST_SIZE);
+            groupsObj = searchAssignable(realmObj, null);
         }
 
         @Override
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java
index 159d3a9296..91b84b4e38 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java
@@ -82,9 +82,11 @@ public class Relationships extends WizardStep implements ICondition {
 
     private static final long serialVersionUID = 855618618337931784L;
 
-    private final PageReference pageRef;
+    protected final AnyTO anyTO;
 
-    private final AnyTO anyTO;
+    protected final Specification specification;
+
+    protected final PageReference pageRef;
 
     public Relationships(final AnyWrapper<?> modelObject, final PageReference pageRef) {
         super();
@@ -101,6 +103,7 @@ public class Relationships extends WizardStep implements ICondition {
         }
 
         this.anyTO = modelObject.getInnerObject();
+        this.specification = new Specification();
         this.pageRef = pageRef;
 
         // ------------------------
@@ -115,11 +118,11 @@ public class Relationships extends WizardStep implements ICondition {
         return super.getHeader(id, parent, wizard).setVisible(false);
     }
 
-    private Fragment getViewFragment() {
-        final Map<String, List<RelationshipTO>> relationships = new HashMap<>();
+    protected Fragment getViewFragment() {
+        Map<String, List<RelationshipTO>> relationships = new HashMap<>();
         addRelationship(relationships, getCurrentRelationships().toArray(RelationshipTO[]::new));
 
-        final Fragment viewFragment = new Fragment("relationships", "viewFragment", this);
+        Fragment viewFragment = new Fragment("relationships", "viewFragment", this);
         viewFragment.setOutputMarkupId(true);
 
         viewFragment.add(new Accordion("relationships", relationships.keySet().stream().
@@ -158,7 +161,7 @@ public class Relationships extends WizardStep implements ICondition {
             }
         });
 
-        final ActionsPanel<RelationshipTO> panel = new ActionsPanel<>("actions", null);
+        ActionsPanel<RelationshipTO> panel = new ActionsPanel<>("actions", null);
         viewFragment.add(panel);
 
         panel.add(new ActionLink<>() {
@@ -169,7 +172,7 @@ public class Relationships extends WizardStep implements ICondition {
             public void onClick(final AjaxRequestTarget target, final RelationshipTO ignore) {
                 Fragment addFragment = new Fragment("relationships", "addFragment", Relationships.this);
                 addOrReplace(addFragment);
-                addFragment.add(new Specification().setRenderBodyOnly(true));
+                addFragment.add(specification.setRenderBodyOnly(true));
                 target.add(Relationships.this);
             }
         }, ActionType.CREATE, AnyEntitlement.UPDATE.getFor(anyTO.getType())).hideLabel();
@@ -177,18 +180,18 @@ public class Relationships extends WizardStep implements ICondition {
         return viewFragment;
     }
 
-    private List<RelationshipTO> getCurrentRelationships() {
+    protected List<RelationshipTO> getCurrentRelationships() {
         return anyTO instanceof GroupableRelatableTO
                 ? GroupableRelatableTO.class.cast(anyTO).getRelationships()
                 : List.of();
     }
 
-    private static void addRelationship(
+    protected void addRelationship(
             final Map<String, List<RelationshipTO>> relationships,
             final RelationshipTO... rels) {
 
         for (RelationshipTO relationship : rels) {
-            final List<RelationshipTO> listrels;
+            List<RelationshipTO> listrels;
             if (relationships.containsKey(relationship.getType())) {
                 listrels = relationships.get(relationship.getType());
             } else {
@@ -199,17 +202,18 @@ public class Relationships extends WizardStep implements ICondition {
         }
     }
 
-    private void addNewRelationships(final RelationshipTO... rels) {
+    protected void addNewRelationships(final RelationshipTO... rels) {
         getCurrentRelationships().addAll(List.of(rels));
     }
 
-    private void removeRelationships(
+    protected void removeRelationships(
             final Map<String, List<RelationshipTO>> relationships, final RelationshipTO... rels) {
-        final List<RelationshipTO> currentRels = getCurrentRelationships();
+
+        List<RelationshipTO> currentRels = getCurrentRelationships();
         for (RelationshipTO relationship : rels) {
             currentRels.remove(relationship);
             if (relationships.containsKey(relationship.getType())) {
-                final List<RelationshipTO> rellist = relationships.get(relationship.getType());
+                List<RelationshipTO> rellist = relationships.get(relationship.getType());
                 rellist.remove(relationship);
                 if (rellist.isEmpty()) {
                     relationships.remove(relationship.getType());
@@ -228,30 +232,38 @@ public class Relationships extends WizardStep implements ICondition {
 
         private static final long serialVersionUID = 6199050589175839467L;
 
-        private final RelationshipTO rel;
+        protected final RelationshipTO rel;
+
+        protected final AjaxDropDownChoicePanel<String> type;
+
+        protected final AjaxDropDownChoicePanel<AnyTypeTO> otherType;
+
+        protected final WebMarkupContainer container;
+
+        protected final Fragment emptyFragment;
 
-        private AnyObjectSearchPanel anyObjectSearchPanel;
+        protected final Fragment fragment;
 
-        private WizardMgtPanel<AnyWrapper<AnyObjectTO>> anyObjectDirectoryPanel;
+        protected AnyObjectSearchPanel anyObjectSearchPanel;
+
+        protected WizardMgtPanel<AnyWrapper<AnyObjectTO>> anyObjectDirectoryPanel;
 
         public Specification() {
             super("specification");
             rel = new RelationshipTO();
 
-            final List<String> availableRels = RelationshipTypeRestClient.list().stream().
+            List<String> availableRels = RelationshipTypeRestClient.list().stream().
                     map(EntityTO::getKey).collect(Collectors.toList());
 
-            final AjaxDropDownChoicePanel<String> type = new AjaxDropDownChoicePanel<>(
-                    "type", "type", new PropertyModel<>(rel, "type"));
+            type = new AjaxDropDownChoicePanel<>("type", "type", new PropertyModel<>(rel, "type"));
             type.setChoices(availableRels);
-            add(type.setRenderBodyOnly(true));
+            add(type.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true).setRenderBodyOnly(true));
 
-            final List<AnyTypeTO> availableTypes = AnyTypeRestClient.listAnyTypes().stream().
+            List<AnyTypeTO> availableTypes = AnyTypeRestClient.listAnyTypes().stream().
                     filter(anyType -> anyType.getKind() != AnyTypeKind.GROUP
                     && anyType.getKind() != AnyTypeKind.USER).collect(Collectors.toList());
 
-            final AjaxDropDownChoicePanel<AnyTypeTO> otherType = new AjaxDropDownChoicePanel<>(
-                    "otherType", "otherType", new PropertyModel<>(rel, "otherType") {
+            otherType = new AjaxDropDownChoicePanel<>("otherType", "otherType", new PropertyModel<>(rel, "otherType") {
 
                 private static final long serialVersionUID = -5861057041758169508L;
 
@@ -288,27 +300,27 @@ public class Relationships extends WizardStep implements ICondition {
                 @Override
                 public AnyTypeTO getObject(final String id, final IModel<? extends List<? extends AnyTypeTO>> choices) {
                     return choices.getObject().stream().
-                        filter(anyTypeTO -> id.equals(anyTypeTO.getKey())).findAny().orElse(null);
+                            filter(anyTypeTO -> id.equals(anyTypeTO.getKey())).findAny().orElse(null);
                 }
             });
             // enable "otherType" dropdown only if "type" option is selected - SYNCOPE-1140
             otherType.setEnabled(false);
-            add(otherType);
+            add(otherType.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
 
-            final WebMarkupContainer container = new WebMarkupContainer("searchPanelContainer");
-            container.setOutputMarkupId(true);
-            add(container);
+            container = new WebMarkupContainer("searchPanelContainer");
+            add(container.setOutputMarkupId(true));
 
-            Fragment emptyFragment = new Fragment("searchPanel", "emptyFragment", this);
+            emptyFragment = new Fragment("searchPanel", "emptyFragment", this);
             container.add(emptyFragment.setRenderBodyOnly(true));
 
+            fragment = new Fragment("searchPanel", "searchFragment", Specification.this);
+
             type.getField().add(new IndicatorAjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
 
                 private static final long serialVersionUID = -1107858522700306810L;
 
                 @Override
                 protected void onUpdate(final AjaxRequestTarget target) {
-                    Fragment emptyFragment = new Fragment("searchPanel", "emptyFragment", Specification.this);
                     container.addOrReplace(emptyFragment.setRenderBodyOnly(true));
                     otherType.setModelObject(null);
                     // enable "otherType" dropdown only if "type" option is selected - SYNCOPE-1140
@@ -324,46 +336,47 @@ public class Relationships extends WizardStep implements ICondition {
 
                 @Override
                 protected void onUpdate(final AjaxRequestTarget target) {
-                    final AnyTypeTO anyType = otherType.getModelObject();
+                    AnyTypeTO anyType = otherType.getModelObject();
                     if (anyType == null) {
-                        Fragment emptyFragment = new Fragment("searchPanel", "emptyFragment", Specification.this);
                         container.addOrReplace(emptyFragment.setRenderBodyOnly(true));
                     } else {
-                        final Fragment fragment = new Fragment("searchPanel", "searchFragment", Specification.this);
+                        setupFragment(anyType);
                         container.addOrReplace(fragment.setRenderBodyOnly(true));
-
-                        anyObjectSearchPanel = new AnyObjectSearchPanel.Builder(
-                                anyType.getKey(),
-                                new ListModel<>(new ArrayList<>())).
-                                enableSearch(Specification.this).
-                                build("searchPanel");
-                        fragment.add(anyObjectSearchPanel.setRenderBodyOnly(true));
-
-                        anyObjectDirectoryPanel = new AnyObjectSelectionDirectoryPanel.Builder(
-                                AnyTypeClassRestClient.list(anyType.getClasses()),
-                                anyType.getKey(),
-                                pageRef).
-                                setFiql(SyncopeClient.getAnyObjectSearchConditionBuilder(anyType.getKey()).
-                                        is(Constants.KEY_FIELD_NAME).notNullValue().query()).
-                                setWizardInModal(true).build("searchResultPanel");
-                        fragment.add(anyObjectDirectoryPanel.setRenderBodyOnly(true));
                     }
                     target.add(container);
                 }
             });
         }
 
+        protected void setupFragment(final AnyTypeTO anyType) {
+            anyObjectSearchPanel = new AnyObjectSearchPanel.Builder(
+                    anyType.getKey(),
+                    new ListModel<>(new ArrayList<>())).
+                    enableSearch(Specification.this).
+                    build("searchPanel");
+            fragment.addOrReplace(anyObjectSearchPanel.setRenderBodyOnly(true));
+
+            anyObjectDirectoryPanel = new AnyObjectSelectionDirectoryPanel.Builder(
+                    AnyTypeClassRestClient.list(anyType.getClasses()),
+                    anyType.getKey(),
+                    pageRef).
+                    setFiql(SyncopeClient.getAnyObjectSearchConditionBuilder(anyType.getKey()).
+                            is(Constants.KEY_FIELD_NAME).notNullValue().query()).
+                    setWizardInModal(true).build("searchResultPanel");
+            fragment.addOrReplace(anyObjectDirectoryPanel.setRenderBodyOnly(true));
+        }
+
         @Override
         public void onEvent(final IEvent<?> event) {
             if (event.getPayload() instanceof SearchClausePanel.SearchEvent) {
-                final AjaxRequestTarget target = SearchClausePanel.SearchEvent.class.cast(event.getPayload()).
-                        getTarget();
-                final String fiql = SearchUtils.buildFIQL(anyObjectSearchPanel.getModel().getObject(),
+                AjaxRequestTarget target =
+                        SearchClausePanel.SearchEvent.class.cast(event.getPayload()).getTarget();
+                String fiql = SearchUtils.buildFIQL(anyObjectSearchPanel.getModel().getObject(),
                         SyncopeClient.getAnyObjectSearchConditionBuilder(anyObjectSearchPanel.getBackObjectType()));
                 AnyDirectoryPanel.class.cast(Specification.this.anyObjectDirectoryPanel).search(fiql, target);
             } else if (event.getPayload() instanceof AnySelectionDirectoryPanel.ItemSelection) {
-                final AjaxRequestTarget target = AnySelectionDirectoryPanel.ItemSelection.class.cast(event.
-                        getPayload()).getTarget();
+                AjaxRequestTarget target =
+                        AnySelectionDirectoryPanel.ItemSelection.class.cast(event.getPayload()).getTarget();
 
                 AnyTO right = AnySelectionDirectoryPanel.ItemSelection.class.cast(event.getPayload()).getSelection();
                 rel.setOtherEndKey(right.getKey());
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RealmChoicePanel.html b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RealmChoicePanel.html
index 0691389902..b250636658 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RealmChoicePanel.html
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RealmChoicePanel.html
@@ -20,7 +20,11 @@ under the License.
   <wicket:panel>
     <div wicket:id="container" class="realm-header">
       <div class="realm-label">
-        <label wicket:id="realmDisplayKey"/>: <label wicket:id="realmDisplayValue"/>
+        <nav aria-label="breadcrumb">
+          <ol class="breadcrumb">
+            <li class="breadcrumb-item" wicket:id="breadcrumb"><a wicket:id="bcitem"/></li>
+          </ol>
+        </nav>
       </div>
       <span wicket:id="realmsFragment"></span>
 
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/console/RealmsITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/console/RealmsITCase.java
index 47ee9853ab..8f2b63b0aa 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/console/RealmsITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/console/RealmsITCase.java
@@ -18,7 +18,9 @@
  */
 package org.apache.syncope.fit.console;
 
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
 import org.apache.syncope.client.ui.commons.Constants;
@@ -73,7 +75,8 @@ public class RealmsITCase extends AbstractConsoleITCase {
                 "body:content:realmChoicePanel:container:realmsFragment:realms:dropdown-menu:buttons:5:button",
                 Constants.ON_CLICK);
 
-        TESTER.assertLabel("body:content:realmChoicePanel:container:realmDisplayValue", "/testRealm");
+        assertTrue(TESTER.getLastResponseAsString().contains(">/</a>"));
+        assertTrue(TESTER.getLastResponseAsString().contains(">testRealm</a>"));
 
         TESTER.getRequest().addParameter("confirm", "true");
         TESTER.clickLink(
@@ -86,7 +89,8 @@ public class RealmsITCase extends AbstractConsoleITCase {
                 "body:content:body:container:content:tabbedPanel:panel:container:policies:1:field-label",
                 "Account Policy");
 
-        TESTER.assertLabel("body:content:realmChoicePanel:container:realmDisplayValue", "/");
+        assertTrue(TESTER.getLastResponseAsString().contains(">/</a>"));
+        assertFalse(TESTER.getLastResponseAsString().contains(">testRealm</a>"));
     }
 
     @Test
@@ -116,7 +120,8 @@ public class RealmsITCase extends AbstractConsoleITCase {
                 "body:content:realmChoicePanel:container:realmsFragment:realms:dropdown-menu:buttons:4:button",
                 Constants.ON_CLICK);
 
-        TESTER.assertLabel("body:content:realmChoicePanel:container:realmDisplayValue", "/odd");
+        assertTrue(TESTER.getLastResponseAsString().contains(">/</a>"));
+        assertTrue(TESTER.getLastResponseAsString().contains(">odd</a>"));
 
         TESTER.clickLink(
                 "body:content:body:container:content:tabbedPanel:panel:actions:actions:actionRepeater:2:action:action");
@@ -136,7 +141,8 @@ public class RealmsITCase extends AbstractConsoleITCase {
         assertSuccessMessage();
         TESTER.cleanupFeedbackMessages();
 
-        TESTER.assertLabel("body:content:realmChoicePanel:container:realmDisplayValue", "/odd");
+        assertTrue(TESTER.getLastResponseAsString().contains(">/</a>"));
+        assertTrue(TESTER.getLastResponseAsString().contains(">odd</a>"));
 
         TESTER.clickLink(
                 "body:content:body:container:content:tabbedPanel:panel:actions:actions:actionRepeater:2:action:action");
diff --git a/src/main/asciidoc/reference-guide/usage/customization.adoc b/src/main/asciidoc/reference-guide/usage/customization.adoc
index 4e54b98ac5..6ced0aaef9 100644
--- a/src/main/asciidoc/reference-guide/usage/customization.adoc
+++ b/src/main/asciidoc/reference-guide/usage/customization.adoc
@@ -120,11 +120,11 @@ At this point your favourite IDE can be attached to the port `8000`.
 
 ===== Override behavior
 As a rule of thumb, any file of the local project will take precedence over a file with the same name in the same
-directory of the standard Apache Syncope release.
+package directory of the standard Apache Syncope release.
 
 For example, if you place
 
- core/spring/src/main/java/org/apache/syncope/core/spring/security/UsernamePasswordAuthenticationProvider.java
+ core/src/main/java/org/apache/syncope/core/spring/security/UsernamePasswordAuthenticationProvider.java
 
 in the local project, this file will be picked up instead of
 ifeval::["{snapshotOrRelease}" == "release"]