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:04 UTC

[syncope] 02/02: Further Console extendability improvements

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

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

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

    Further Console extendability improvements
---
 .../client/console/panels/RealmChoicePanel.java    |  75 ++++++++----
 .../client/console/rest/RealmRestClient.java       |   9 +-
 .../client/console/status/ReconTaskPanel.java      |   2 +-
 .../console/tasks/SchedTaskWizardBuilder.java      |   2 +-
 .../client/console/widgets/NumberWidget.java       |  21 ++--
 .../client/console/wizards/any/Details.java        |   3 +-
 .../syncope/client/console/wizards/any/Groups.java |  15 +--
 .../client/console/wizards/any/Relationships.java  | 128 ++++++++++++---------
 .../wizards/resources/ConnectorDetailsPanel.java   |   3 +-
 .../console/wizards/role/RoleWizardBuilder.java    |   3 +-
 .../client/console/panels/RealmChoicePanel.html    |   6 +-
 .../apache/syncope/fit/console/RealmsITCase.java   |  14 ++-
 .../workingwithapachesyncope/customization.adoc    |   4 +-
 13 files changed, 172 insertions(+), 113 deletions(-)

diff --git a/client/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java b/client/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
index d3fa22ab26..11dedd50a2 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
@@ -25,6 +25,7 @@ import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.dropdown.DropDownButton;
 import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -48,6 +49,7 @@ import org.apache.syncope.common.lib.to.RealmTO;
 import org.apache.syncope.common.lib.types.StandardEntitlement;
 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;
@@ -56,11 +58,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;
@@ -94,9 +96,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);
@@ -172,33 +172,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<Void>("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(Arrays.asList(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));
     }
 
@@ -404,7 +431,7 @@ public class RealmChoicePanel extends Panel {
     protected Map<String, Pair<RealmTO, List<RealmTO>>> reloadRealmParentMap() {
         List<RealmTO> realmsToList = isSearchEnabled
                 ? realmRestClient.search(RealmsUtils.buildQuery(searchQuery)).getResult()
-                : realmRestClient.list();
+                : realmRestClient.list(SyncopeConstants.ROOT_REALM);
 
         return reloadRealmParentMap(realmsToList.stream().
                 sorted(Comparator.comparing(RealmTO::getName)).
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/rest/RealmRestClient.java b/client/console/src/main/java/org/apache/syncope/client/console/rest/RealmRestClient.java
index 8e1b5d7cfb..70ebcc7f0c 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/rest/RealmRestClient.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/rest/RealmRestClient.java
@@ -21,7 +21,6 @@ package org.apache.syncope.client.console.rest;
 import java.util.List;
 import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.Response;
-import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.DynRealmTO;
 import org.apache.syncope.common.lib.to.PagedResult;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
@@ -36,13 +35,13 @@ import org.apache.syncope.common.rest.api.service.RealmService;
 public class RealmRestClient extends BaseRestClient {
 
     private static final long serialVersionUID = -8549081557283519638L;
-    
+
     public PagedResult<RealmTO> search(final RealmQuery query) {
         return getService(RealmService.class).search(query);
     }
-    
-    public List<RealmTO> list() {
-        return getService(RealmService.class).list(SyncopeConstants.ROOT_REALM);
+
+    public List<RealmTO> list(final String fullpath) {
+        return getService(RealmService.class).list(fullpath);
     }
 
     public List<DynRealmTO> listDynRealms() {
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java b/client/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
index 89efaf202c..32c6b2fd6c 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
@@ -139,7 +139,7 @@ public class ReconTaskPanel extends MultilevelPanel.SecondLevel {
                     return (RealmsUtils.checkInput(input)
                             ? (isSearchEnabled
                                     ? realmRestClient.search(RealmsUtils.buildQuery(input)).getResult()
-                                    : realmRestClient.list())
+                                    : realmRestClient.list(SyncopeConstants.ROOT_REALM))
                             : Collections.<RealmTO>emptyList()).stream().
                             sorted(Comparator.comparing(RealmTO::getName)).
                             map(RealmTO::getFullPath).collect(Collectors.toList()).iterator();
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java b/client/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
index c1927d7772..937bd2ba97 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
@@ -113,7 +113,7 @@ public class SchedTaskWizardBuilder<T extends SchedTaskTO> extends AjaxWizardBui
     private List<RealmTO> searchRealms(final String realmQuery) {
         return isSearchEnabled
                 ? realmRestClient.search(RealmsUtils.buildQuery(realmQuery)).getResult()
-                : realmRestClient.list();
+                : realmRestClient.list(SyncopeConstants.ROOT_REALM);
     }
 
     public class Profile extends WizardStep {
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/widgets/NumberWidget.java b/client/console/src/main/java/org/apache/syncope/client/console/widgets/NumberWidget.java
index 425cec1460..caac674db7 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/widgets/NumberWidget.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/widgets/NumberWidget.java
@@ -21,6 +21,7 @@ package org.apache.syncope.client.console.widgets;
 import java.util.List;
 import org.apache.syncope.client.console.SyncopeConsoleApplication;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.pages.BasePage;
 import org.apache.syncope.client.console.rest.AnyTypeRestClient;
 import org.apache.syncope.client.console.topology.TabularTopology;
 import org.apache.wicket.behavior.AttributeAppender;
@@ -32,7 +33,6 @@ import org.apache.syncope.client.console.pages.Realms;
 import org.apache.syncope.client.console.pages.Security;
 import org.apache.syncope.client.console.topology.Topology;
 import org.apache.syncope.common.lib.types.StandardEntitlement;
-import org.apache.wicket.request.component.IRequestablePage;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 
 public class NumberWidget extends BaseWidget {
@@ -51,20 +51,27 @@ public class NumberWidget extends BaseWidget {
         WebMarkupContainer box = new WebMarkupContainer("box");
         box.add(new AttributeAppender("class", " " + bg));
 
+        @SuppressWarnings("unchecked")
+        Class<? extends BasePage> realmsPage =
+                (Class<? extends BasePage>) SyncopeConsoleApplication.get().getPageClass("realms");
+        if (realmsPage == null) {
+            realmsPage = Realms.class;
+        }
+
         boolean isAuthorized = true;
-        final PageParameters pageParameters = new PageParameters();
-        final Class<? extends IRequestablePage> responsePage;
+        PageParameters pageParameters = new PageParameters();
+        Class<? extends BasePage> responsePage;
         List<String> anyTypes = new AnyTypeRestClient().list();
         switch (id) {
             case "totalUsers":
                 pageParameters.add(Realms.SELECTED_INDEX, 1);
-                responsePage = Realms.class;
+                responsePage = realmsPage;
                 isAuthorized = SyncopeConsoleSession.get().owns(StandardEntitlement.USER_SEARCH);
                 break;
 
             case "totalGroups":
                 pageParameters.add(Realms.SELECTED_INDEX, 2);
-                responsePage = Realms.class;
+                responsePage = realmsPage;
                 isAuthorized = SyncopeConsoleSession.get().owns(StandardEntitlement.GROUP_SEARCH);
                 break;
 
@@ -77,7 +84,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;
@@ -109,7 +116,7 @@ public class NumberWidget extends BaseWidget {
 
             default:
                 pageParameters.add(Realms.SELECTED_INDEX, 0);
-                responsePage = Realms.class;
+                responsePage = realmsPage;
         }
 
         AjaxEventBehavior clickToRealms = new AjaxEventBehavior("mousedown") {
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java b/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
index 27039c3576..c7411ae647 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
@@ -29,6 +29,7 @@ import org.apache.syncope.client.console.rest.RealmRestClient;
 import org.apache.syncope.client.console.wicket.markup.html.form.AjaxSearchFieldPanel;
 import org.apache.syncope.client.console.wicket.markup.html.form.AjaxTextFieldPanel;
 import org.apache.syncope.client.console.wicket.markup.html.form.FieldPanel;
+import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.AnyTO;
 import org.apache.syncope.common.lib.to.RealmTO;
 import org.apache.wicket.Component;
@@ -86,7 +87,7 @@ public class Details<T extends AnyTO> extends WizardStep {
                             ? realmRestClient.search(RealmsUtils.buildQuery(input)).getResult()
                             : pageRef.getPage() instanceof Realms
                             ? getRealmsFromLinks(Realms.class.cast(pageRef.getPage()).getRealmChoicePanel().getLinks())
-                            : realmRestClient.list()).
+                            : realmRestClient.list(SyncopeConstants.ROOT_REALM)).
                             stream().filter(realm -> authRealms.stream().anyMatch(
                             authRealm -> realm.getFullPath().startsWith(authRealm))).
                             map(item -> item.getFullPath()).collect(Collectors.toList()).iterator();
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Groups.java b/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Groups.java
index 347c92c3c2..8038828dfa 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Groups.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Groups.java
@@ -87,6 +87,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 addGroupsPanel() {
         if (anyTO instanceof GroupTO) {
@@ -128,7 +132,6 @@ public class Groups extends AbstractGroups {
                 public List<MembershipTO> getObject() {
                     return Groups.this.groupsModel.getMemberships();
                 }
-
             }, new AjaxPalettePanel.Builder.Query<MembershipTO>() {
 
                 private static final long serialVersionUID = -7223078772249308813L;
@@ -139,8 +142,7 @@ public class Groups extends AbstractGroups {
                             ? Collections.emptyList()
                             : ("*".equals(filter)
                                     ? groupsModel.getObject()
-                                    : syncopeRestClient.searchAssignableGroups(
-                                            anyTO.getRealm(), filter, 1, Constants.MAX_GROUP_LIST_SIZE)).stream().
+                                    : searchAssignable(anyTO.getRealm(), filter)).stream().
                                     map(group -> new MembershipTO.Builder().
                                     group(group.getKey(), group.getName()).build()).
                                     collect(Collectors.toList());
@@ -156,7 +158,6 @@ public class Groups extends AbstractGroups {
                 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));
         }
@@ -213,11 +214,7 @@ public class Groups extends AbstractGroups {
          * Retrieve the first MAX_GROUP_LIST_SIZE assignable.
          */
         protected void reloadObject() {
-            groups = syncopeRestClient.searchAssignableGroups(
-                    realm,
-                    null,
-                    1,
-                    Constants.MAX_GROUP_LIST_SIZE);
+            groups = searchAssignable(realm, null);
         }
 
         public List<MembershipTO> getMemberships() {
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java b/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java
index 967e4904bc..46e5b3652c 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java
@@ -81,15 +81,17 @@ public class Relationships extends WizardStep implements ICondition {
 
     private static final long serialVersionUID = 855618618337931784L;
 
-    private final PageReference pageRef;
+    protected final AnyTypeRestClient anyTypeRestClient = new AnyTypeRestClient();
 
-    private final AnyTypeRestClient anyTypeRestClient = new AnyTypeRestClient();
+    protected final AnyTypeClassRestClient anyTypeClassRestClient = new AnyTypeClassRestClient();
 
-    private final AnyTypeClassRestClient anyTypeClassRestClient = new AnyTypeClassRestClient();
+    protected final RelationshipTypeRestClient relationshipTypeRestClient = new RelationshipTypeRestClient();
 
-    private final AnyTO anyTO;
+    protected final AnyTO anyTO;
 
-    private final RelationshipTypeRestClient relationshipTypeRestClient = new RelationshipTypeRestClient();
+    protected final Specification specification;
+
+    protected final PageReference pageRef;
 
     public Relationships(final AnyWrapper<?> modelObject, final PageReference pageRef) {
         super();
@@ -106,6 +108,7 @@ public class Relationships extends WizardStep implements ICondition {
         }
 
         this.anyTO = modelObject.getInnerObject();
+        this.specification = new Specification();
         this.pageRef = pageRef;
 
         // ------------------------
@@ -120,11 +123,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(new RelationshipTO[] {}));
 
-        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().map(relationship -> {
@@ -164,7 +167,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<RelationshipTO>() {
@@ -175,7 +178,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();
@@ -183,16 +186,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()
                 : Collections.<RelationshipTO>emptyList();
     }
 
-    private void addRelationship(final Map<String, List<RelationshipTO>> relationships, final RelationshipTO... rels) {
+    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 {
@@ -203,17 +208,18 @@ public class Relationships extends WizardStep implements ICondition {
         }
     }
 
-    private void addNewRelationships(final RelationshipTO... rels) {
+    protected void addNewRelationships(final RelationshipTO... rels) {
         getCurrentRelationships().addAll(Arrays.asList(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());
@@ -232,29 +238,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 = new AjaxDropDownChoicePanel<>(
                     "otherType", "otherType", new PropertyModel<AnyTypeTO>(rel, "otherType") {
 
                 private static final long serialVersionUID = -5861057041758169508L;
@@ -297,22 +312,22 @@ public class Relationships extends WizardStep implements ICondition {
             });
             // 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
@@ -328,46 +343,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/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java b/client/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java
index 2471b162e0..de5932887d 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java
@@ -33,6 +33,7 @@ import org.apache.syncope.client.console.wicket.markup.html.form.AjaxDropDownCho
 import org.apache.syncope.client.console.wicket.markup.html.form.AjaxSearchFieldPanel;
 import org.apache.syncope.client.console.wicket.markup.html.form.AjaxSpinnerFieldPanel;
 import org.apache.syncope.client.console.wicket.markup.html.form.AjaxTextFieldPanel;
+import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.ConnBundleTO;
 import org.apache.syncope.common.lib.to.ConnInstanceTO;
 import org.apache.syncope.common.lib.to.ConnPoolConfTO;
@@ -66,7 +67,7 @@ public class ConnectorDetailsPanel extends WizardStep {
             protected Iterator<String> getChoices(final String input) {
                 return (isSearchEnabled
                         ? realmRestClient.search(RealmsUtils.buildQuery(input)).getResult()
-                        : realmRestClient.list()).
+                        : realmRestClient.list(SyncopeConstants.ROOT_REALM)).
                         stream().filter(realm -> SyncopeConsoleSession.get().getAuthRealms().stream().anyMatch(
                         authRealm -> realm.getFullPath().startsWith(authRealm))).
                         map(item -> item.getFullPath()).collect(Collectors.toList()).iterator();
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java b/client/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java
index 8df8cddccf..1d274da1c2 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java
@@ -34,6 +34,7 @@ import org.apache.syncope.client.console.wicket.markup.html.bootstrap.tabs.Accor
 import org.apache.syncope.client.console.wicket.markup.html.form.AjaxPalettePanel;
 import org.apache.syncope.client.console.wicket.markup.html.form.AjaxTextFieldPanel;
 import org.apache.syncope.client.console.wizards.AjaxWizardBuilder;
+import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.EntityTO;
 import org.apache.syncope.common.lib.to.RealmTO;
 import org.apache.syncope.common.lib.to.RoleTO;
@@ -165,7 +166,7 @@ public class RoleWizardBuilder extends AjaxWizardBuilder<RoleWrapper> {
             setTitleModel(new ResourceModel("realms"));
             add(new AjaxPalettePanel.Builder<>().build("realms",
                     new PropertyModel<>(modelObject, "realms"),
-                    new ListModel<>(new RealmRestClient().list().stream().
+                    new ListModel<>(new RealmRestClient().list(SyncopeConstants.ROOT_REALM).stream().
                             map(RealmTO::getFullPath).collect(Collectors.toList()))).
                     hideLabel().setOutputMarkupId(true));
         }
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/RealmChoicePanel.html b/client/console/src/main/resources/org/apache/syncope/client/console/panels/RealmChoicePanel.html
index bfbb60600e..73d57b0044 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/RealmChoicePanel.html
+++ b/client/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 f89c96686f..34a3a740f8 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.console.commons.Constants;
@@ -69,7 +71,8 @@ public class RealmsITCase extends AbstractConsoleITCase {
         TESTER.executeAjaxEvent("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(
@@ -81,7 +84,8 @@ public class RealmsITCase extends AbstractConsoleITCase {
         TESTER.assertLabel("body:content:body:container:content:tabbedPanel:panel:container:accountPolicy:field-label",
                 "Account Policy");
 
-        TESTER.assertLabel("body:content:realmChoicePanel:container:realmDisplayValue", "/");
+        assertTrue(TESTER.getLastResponseAsString().contains(">/</a>"));
+        assertFalse(TESTER.getLastResponseAsString().contains(">testRealm</a>"));
     }
 
     @Test
@@ -107,7 +111,8 @@ public class RealmsITCase extends AbstractConsoleITCase {
         TESTER.executeAjaxEvent("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");
@@ -127,7 +132,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/workingwithapachesyncope/customization.adoc b/src/main/asciidoc/reference-guide/workingwithapachesyncope/customization.adoc
index f16bf420ae..d38ee26bf1 100644
--- a/src/main/asciidoc/reference-guide/workingwithapachesyncope/customization.adoc
+++ b/src/main/asciidoc/reference-guide/workingwithapachesyncope/customization.adoc
@@ -33,11 +33,11 @@ adding new features or replacing existing components.
 .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/SyncopeAuthenticationProvider.java
+ core/src/main/java/org/apache/syncope/core/spring/security/SyncopeAuthenticationProvider.java
 
 in the local project, this file will be picked up instead of
 ifeval::["{snapshotOrRelease}" == "release"]