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 8fbfdf3769e5e60741e053167f6a92cb6d5b87e7
Author: Matteo Tatoni <[email protected]>
AuthorDate: Wed May 6 11:01:24 2026 +0200

    Improve Topology UX (#1373)
---
 .../syncope/client/console/topology/Topology.java  |   24 +
 .../META-INF/resources/css/idmTopology.scss        |  124 +++
 .../resources/META-INF/resources/js/idmTopology.js | 1005 ++++++++++++++++----
 .../wicket/markup/html/form/ActionLink.java        |    2 +
 .../wicket/markup/html/form/ActionPanel.java       |   13 +
 .../markup/html/form/ActionsPanel.properties       |    8 +
 .../markup/html/form/ActionsPanel_fr_CA.properties |    9 +
 .../markup/html/form/ActionsPanel_it.properties    |    8 +
 .../markup/html/form/ActionsPanel_ja.properties    |    8 +
 .../markup/html/form/ActionsPanel_pt_BR.properties |    9 +
 .../markup/html/form/ActionsPanel_ru.properties    |    8 +
 11 files changed, 1012 insertions(+), 206 deletions(-)

diff --git 
a/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/Topology.java
 
b/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/Topology.java
index 95bbcac912..4aae9400c5 100644
--- 
a/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/Topology.java
+++ 
b/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/Topology.java
@@ -197,6 +197,26 @@ public class Topology extends BasePage {
             }
         }, ActionLink.ActionType.ZOOM_OUT, 
IdMEntitlement.CONNECTOR_LIST).disableIndicator().hideLabel();
 
+        zoomActionPanel.add(new ActionLink<>() {
+
+            private static final long serialVersionUID = -3722207913631435501L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final 
Serializable ignore) {
+                target.appendJavaScript("autoLayoutTree({ centerInView: false 
});");
+            }
+        }, ActionLink.ActionType.AUTO_LAYOUT, 
IdMEntitlement.CONNECTOR_LIST).disableIndicator().hideLabel();
+
+        zoomActionPanel.add(new ActionLink<>() {
+
+            private static final long serialVersionUID = -3722207913631435501L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final 
Serializable ignore) {
+                target.appendJavaScript("recenterToTree();");
+            }
+        }, ActionLink.ActionType.RECENTER, 
IdMEntitlement.CONNECTOR_LIST).disableIndicator().hideLabel();
+
         body.add(zoomActionPanel);
         // -----------------------------------------
 
@@ -464,6 +484,10 @@ public class Topology extends BasePage {
                 jsPlumbConf.append(String.format(Locale.US, "activate(%.2f);", 
0.68f));
 
                 createConnections(connections).forEach(jsPlumbConf::append);
+                // Apply the tree layout on first load (when no saved node 
positions exist yet).
+                jsPlumbConf.append("var __topo=getTopology();var 
__hasPos=false;"
+                        + "for(var __k in 
__topo){if(__k!=='__zoom__'){__hasPos=true;break;}}"
+                        + "if(!__hasPos){autoLayoutTree({ centerInView: false 
});}");
 
                 
response.render(OnDomReadyHeaderItem.forScript(jsPlumbConf.toString()));
             }
diff --git 
a/client/idm/console/src/main/resources/META-INF/resources/css/idmTopology.scss 
b/client/idm/console/src/main/resources/META-INF/resources/css/idmTopology.scss
index bd244d07ab..092f501d45 100644
--- 
a/client/idm/console/src/main/resources/META-INF/resources/css/idmTopology.scss
+++ 
b/client/idm/console/src/main/resources/META-INF/resources/css/idmTopology.scss
@@ -17,3 +17,127 @@
  * under the License.
  */
 
+:root {
+  --topology-node-border: rgba(15, 23, 42, 0.14);
+  --topology-node-shadow: 0 10px 28px rgba(2, 6, 23, 0.10);
+  --topology-node-shadow-hover: 0 14px 34px rgba(2, 6, 23, 0.14);
+
+  --topology-toolbar-bg: rgba(255, 255, 255, 0.92);
+  --topology-toolbar-border: rgba(15, 23, 42, 0.10);
+  --topology-toolbar-fg: rgba(15, 23, 42, 0.78);
+  --topology-toolbar-fg-hover: rgba(15, 23, 42, 0.92);
+}
+
+#zoom {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 6px;
+
+  ul.menu {
+    display: flex;
+    gap: 6px;
+    margin: 0;
+    padding: 0;
+    list-style: none;
+  }
+
+  ul.menu > li {
+    margin: 0;
+    padding: 0;
+  }
+
+  ul.menu > li > a.btn,
+  .topology-toolbar-btn {
+    appearance: none;
+    border: 1px solid var(--topology-toolbar-border);
+    background: var(--topology-toolbar-bg);
+    color: var(--topology-toolbar-fg);
+    border-radius: 999px;
+    padding: 6px 10px;
+    line-height: 1;
+    box-shadow: 0 8px 18px rgba(2, 6, 23, 0.10);
+    transition: transform 120ms ease, box-shadow 120ms ease, background 120ms 
ease, color 120ms ease;
+  }
+
+  ul.menu > li > a.btn:hover,
+  .topology-toolbar-btn:hover {
+    color: var(--topology-toolbar-fg-hover);
+    background: rgba(255, 255, 255, 0.98);
+    box-shadow: 0 10px 22px rgba(2, 6, 23, 0.14);
+    transform: translateY(-1px);
+    text-decoration: none;
+  }
+
+  ul.menu > li > a.btn:focus,
+  .topology-toolbar-btn:focus {
+    outline: none;
+    box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.25), 0 10px 22px rgba(2, 6, 23, 
0.14);
+  }
+
+  ul.menu > li > a.btn i,
+  .topology-toolbar-btn i {
+    font-size: 1.05rem;
+    padding-right: 0;
+  }
+}
+
+#topology {
+  cursor: grab;
+}
+
+#topology.topology-panning {
+  cursor: grabbing;
+}
+
+#drawing .window {
+  opacity: 1;
+  border: 1px solid var(--topology-node-border);
+  box-shadow: var(--topology-node-shadow);
+  border-radius: 14px;
+  width: 200px;
+  min-height: 72px;
+  height: auto;
+  line-height: normal;
+  padding: 10px 12px;
+
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+#drawing .window:hover {
+  box-shadow: var(--topology-node-shadow);
+}
+
+#drawing .window[data-original-title]:hover::after {
+  content: none !important;
+  display: none !important;
+}
+
+#drawing .window.topology_root {
+  background-color: rgba(22, 163, 74, 0.18);
+}
+
+#drawing .window.topology_cs {
+  background-color: rgba(14, 165, 233, 0.12);
+}
+
+#drawing .window.topology_conn {
+  background-color: rgba(139, 92, 246, 0.10);
+}
+
+#drawing .window.topology_conn_errored {
+  background-color: rgba(244, 63, 94, 0.18);
+}
+
+#drawing .window.topology_res {
+  background-color: rgba(245, 158, 11, 0.14);
+}
+
+#drawing .window p {
+  font-weight: 700;
+  letter-spacing: 0.2px;
+  font-size: 1.25em;
+  line-height: 1.25;
+}
diff --git 
a/client/idm/console/src/main/resources/META-INF/resources/js/idmTopology.js 
b/client/idm/console/src/main/resources/META-INF/resources/js/idmTopology.js
index 8592edb372..b604f532c2 100644
--- a/client/idm/console/src/main/resources/META-INF/resources/js/idmTopology.js
+++ b/client/idm/console/src/main/resources/META-INF/resources/js/idmTopology.js
@@ -16,296 +16,889 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-var def = {
-  paintStyle: {
-    lineWidth: 2,
-    strokeStyle: "rgba(204,204,204, 1)",
-    outlineColor: "#666",
-    outlineWidth: 1
-  },
-  connectorPaintStyle: {
-    lineWidth: 2
-  },
-  anchor: "AutoDefault",
-  detachable: false,
-  endpointStyle: {
-    gradient: {
-      stops: [
-        [0, "rgba(204,204,204, 1)"], [1, "rgba(180, 180, 200, 1)"]
-      ],
-      offset: 5.5,
-      innerRadius: 3.5
+const def = {
+    paintStyle: {
+        lineWidth: 2,
+        strokeStyle: "#94a3b8",
+        outlineColor: "rgba(15, 23, 42, 0.18)",
+        outlineWidth: 1.5,
+        lineCap: "round",
+        lineJoin: "round"
+    },
+    connectorPaintStyle: {
+        lineWidth: 2
     },
-    radius: 3.5
-  }
+    anchor: "AutoDefault",
+    detachable: false,
+    endpointStyle: {
+        fillStyle: "#cbd5e1",
+        outlineColor: "rgba(15, 23, 42, 0.18)",
+        outlineWidth: 1,
+        radius: 3
+    }
 };
 
-var failedConnectorStyle = {
-  lineWidth: 2,
-  strokeStyle: "rgba(220, 220, 220, 1)",
-  outlineColor: "#666",
-  outlineWidth: 1
+const failedConnectorStyle = {
+    lineWidth: 2,
+    strokeStyle: "rgba(148, 163, 184, 0.55)",
+    outlineColor: "rgba(15, 23, 42, 0.18)",
+    outlineWidth: 1.5
 };
 
-var failedConnectorHoverStyle = {
-  strokeStyle: "#FFFFFF"
+const failedConnectorHoverStyle = {
+    strokeStyle: "#FFFFFF"
 };
 
-var failedEndpointStyle = {
-  gradient: {
-    stops: [
-      [0, "rgba(220, 220, 220, 1)"], [1, "rgba(180, 180, 200, 1)"]
-    ],
-    offset: 5.5,
-    innerRadius: 0
-  },
-  radius: 0
+const failedEndpointStyle = {
+    fillStyle: "rgba(148, 163, 184, 0.45)",
+    outlineColor: "rgba(15, 23, 42, 0.18)",
+    outlineWidth: 1,
+    radius: 3
 };
 
-var disabledConnectorStyle = {
-  lineWidth: 2,
-  strokeStyle: "rgba(255, 69, 0, 1)",
-  outlineColor: "#666",
-  outlineWidth: 1
+const disabledConnectorStyle = {
+    lineWidth: 2,
+    strokeStyle: "#fb7185",
+    outlineColor: "rgba(15, 23, 42, 0.18)",
+    outlineWidth: 1.5
 };
 
-var disabledConnectorHoverStyle = {
-  strokeStyle: "#FF8C00"
+const disabledConnectorHoverStyle = {
+    strokeStyle: "#FF8C00"
 };
 
-var disabledEndpointStyle = {
-  gradient: {
-    stops: [
-      [0, "rgba(255, 69, 0, 1)"], [1, "rgba(180, 180, 200, 1)"]
-    ],
-    offset: 5.5,
-    innerRadius: 1
-  },
-  radius: 1
+const disabledEndpointStyle = {
+    fillStyle: "rgba(251, 113, 133, 0.8)",
+    outlineColor: "rgba(15, 23, 42, 0.18)",
+    outlineWidth: 1,
+    radius: 3
 };
 
-var enabledConnectorStyle = {
-  lineWidth: 2,
-  strokeStyle: "rgba(65, 155, 30, 1)",
-  outlineColor: "#666",
-  outlineWidth: 1
+const enabledConnectorStyle = {
+    lineWidth: 2,
+    strokeStyle: "#22c55e",
+    outlineColor: "rgba(15, 23, 42, 0.18)",
+    outlineWidth: 1.5
 };
 
-var enabledConnectorHoverStyle = {
-  strokeStyle: "#00FF00"
+const enabledConnectorHoverStyle = {
+    strokeStyle: "#16a34a"
 };
 
-var enabledEndpointStyle = {
-  gradient: {
-    stops: [
-      [0, "rgba(65, 155, 30, 0.1)"], [1, "rgba(180, 180, 200, 0.1)"]
-    ],
-    offset: 5.5,
-    innerRadius: 2
-  },
-  radius: 2
+const enabledEndpointStyle = {
+    fillStyle: "rgba(34, 197, 94, 0.35)",
+    outlineColor: "rgba(15, 23, 42, 0.18)",
+    outlineWidth: 1,
+    radius: 3
 };
 
 window.disable = function (targetName) {
-  jsPlumb.ready(function () {
-    jsPlumb.select({target: 
targetName}).setPaintStyle(disabledConnectorStyle).setHoverPaintStyle(disabledConnectorHoverStyle);
-    jsPlumb.selectEndpoints({element: 
[targetName]}).setPaintStyle(disabledEndpointStyle);
-  });
+    jsPlumb.ready(function () {
+        jsPlumb.select({target: 
targetName}).setPaintStyle(disabledConnectorStyle).setHoverPaintStyle(disabledConnectorHoverStyle);
+        jsPlumb.selectEndpoints({element: 
[targetName]}).setPaintStyle(disabledEndpointStyle);
+    });
 }
 
 window.enable = function (targetName) {
-  jsPlumb.ready(function () {
-    jsPlumb.select({target: 
targetName}).setPaintStyle(enabledConnectorStyle).setHoverPaintStyle(enabledConnectorHoverStyle);
-    jsPlumb.selectEndpoints({element: 
[targetName]}).setPaintStyle(enabledEndpointStyle);
-  });
+    jsPlumb.ready(function () {
+        jsPlumb.select({target: 
targetName}).setPaintStyle(enabledConnectorStyle).setHoverPaintStyle(enabledConnectorHoverStyle);
+        jsPlumb.selectEndpoints({element: 
[targetName]}).setPaintStyle(enabledEndpointStyle);
+    });
 }
 
 window.failure = function (targetName) {
-  jsPlumb.ready(function () {
-    jsPlumb.select({target: 
targetName}).setPaintStyle(failedConnectorStyle).setHoverPaintStyle(failedConnectorHoverStyle);
-    jsPlumb.selectEndpoints({element: 
[targetName]}).setPaintStyle(failedEndpointStyle);
-  });
+    jsPlumb.ready(function () {
+        jsPlumb.select({target: 
targetName}).setPaintStyle(failedConnectorStyle).setHoverPaintStyle(failedConnectorHoverStyle);
+        jsPlumb.selectEndpoints({element: 
[targetName]}).setPaintStyle(failedEndpointStyle);
+    });
 }
 
 window.unknown = function (targetName) {
 }
 
 function getTopology() {
-  var topology = $.cookie("topology");
-
-  if (topology == null) {
-    var val = {};
-  } else {
-    var val = JSON.parse(decodeURIComponent(topology));
-  }
+    const topology = $.cookie("topology");
+    let val;
+    if (topology == null) {
+        val = {};
+    } else {
+        val = JSON.parse(decodeURIComponent(topology));
+    }
 
-  return val;
+    return val;
 }
 
 window.refreshPosition = function (element) {
-  var val = getTopology();
+    const val = getTopology();
 
-  var id = $(element).attr('id');
-  var left = $(element).css('left');
-  var top = $(element).css('top');
+    const id = $(element).attr('id');
+    const left = $(element).css('left');
+    const top = $(element).css('top');
 
-  if (val[id] == null) {
-    val[id] = {'top': top, 'left': left};
-  } else {
-    val[id].top = top;
-    val[id].left = left;
-  }
+    if (val[id] == null) {
+        val[id] = {'top': top, 'left': left};
+    } else {
+        val[id].top = top;
+        val[id].left = left;
+    }
 
-  $.cookie("topology", JSON.stringify(val), {expires: 9999});
+    $.cookie("topology", JSON.stringify(val), {expires: 9999});
 }
 
 window.setPosition = function (id, x, y) {
-  var val = getTopology();
-
-  try {
-    // We cannot use jQuery selector for id since the syntax of connector 
server id
-    var element = $(document.getElementById(id));
-
-    if (val[id] == null) {
-      element.css("left", x + "px");
-      element.css("top", y + "px");
-    } else {
-      element.css("left", val[id].left);
-      element.css("top", val[id].top);
+    const val = getTopology();
+
+    try {
+        // We cannot use jQuery selector for id since the syntax of connector 
server id
+        const element = $(document.getElementById(id));
+
+        if (val[id] == null) {
+            element.css("left", x + "px");
+            element.css("top", y + "px");
+        } else {
+            element.css("left", val[id].left);
+            element.css("top", val[id].top);
+        }
+    } catch (err) {
+        console.log("Failure setting position for ", id);
     }
-  } catch (err) {
-    console.log("Failure setting position for ", id);
-  }
 }
 
 window.setZoom = function (el, zoom, instance, transformOrigin) {
-  transformOrigin = transformOrigin || [0.5, 0.5];
-  instance = instance || jsPlumb;
-  el = el || instance.getContainer();
+    transformOrigin = transformOrigin || [0.5, 0.5];
+    instance = instance || jsPlumb;
+    el = el || instance.getContainer();
 
-  var p = ["webkit", "moz", "ms", "o"],
-          s = "scale(" + zoom + ")",
-          oString = (transformOrigin[0] * 100) + "% " + (transformOrigin[1] * 
100) + "%";
+    const p = ["webkit", "moz", "ms", "o"],
+        s = "scale(" + zoom + ")",
+        oString = (transformOrigin[0] * 100) + "% " + (transformOrigin[1] * 
100) + "%";
 
-  for (var i = 0; i < p.length; i++) {
-    el.style[p[i] + "Transform"] = s;
-    el.style[p[i] + "TransformOrigin"] = oString;
-  }
+    for (let i = 0; i < p.length; i++) {
+        el.style[p[i] + "Transform"] = s;
+        el.style[p[i] + "TransformOrigin"] = oString;
+    }
 
-  el.style["transform"] = s;
-  el.style["transformOrigin"] = oString;
+    el.style["transform"] = s;
+    el.style["transformOrigin"] = oString;
 
-  instance.setZoom(zoom);
+    instance.setZoom(zoom);
 };
 
 window.zoomIn = function (el, instance, transformOrigin) {
-  var val = getTopology();
-  if (val.__zoom__ == null) {
-    var zoom = 0.69;
-  } else {
-    var zoom = val.__zoom__ + 0.01;
-  }
+    const val = getTopology();
+    let zoom;
+    if (val.__zoom__ == null) {
+        zoom = 0.69;
+    } else {
+        zoom = val.__zoom__ + 0.01;
+    }
 
-  setZoom(el, zoom, instance, transformOrigin);
+    setZoom(el, zoom, instance, transformOrigin);
 
-  val['__zoom__'] = zoom;
-  $.cookie("topology", JSON.stringify(val), {expires: 9999});
+    val['__zoom__'] = zoom;
+    $.cookie("topology", JSON.stringify(val), {expires: 9999});
 };
 
 window.zoomOut = function (el, instance, transformOrigin) {
-  var val = getTopology();
-  if (val.__zoom__ == null) {
-    var zoom = 0.67;
-  } else {
-    var zoom = val.__zoom__ - 0.01;
-  }
+    const val = getTopology();
+    let zoom;
+    if (val.__zoom__ == null) {
+        zoom = 0.67;
+    } else {
+        zoom = val.__zoom__ - 0.01;
+    }
 
-  setZoom(el, zoom, instance, transformOrigin);
+    setZoom(el, zoom, instance, transformOrigin);
 
-  val['__zoom__'] = zoom;
-  $.cookie("topology", JSON.stringify(val), {expires: 9999});
+    val['__zoom__'] = zoom;
+    $.cookie("topology", JSON.stringify(val), {expires: 9999});
 };
 
 window.connect = function (source, target, scope) {
-  jsPlumb.ready(function () {
-    if (jsPlumb.select({source: source, target: target, scope: scope}) != 
null) {
-      jsPlumb.connect({source: source, target: target, scope: scope}, def);
-    }
-  });
+    jsPlumb.ready(function () {
+        if (jsPlumb.select({source: source, target: target, scope: scope}) != 
null) {
+            jsPlumb.connect({source: source, target: target, scope: scope}, 
def);
+        }
+    });
 }
 
 window.activate = function (zoom) {
-  jsPlumb.ready(function () {
-    jsPlumb.draggable(jsPlumb.getSelector(".window"));
-    jsPlumb.setContainer("drawing");
+    jsPlumb.ready(function () {
+        jsPlumb.draggable(jsPlumb.getSelector(".window"));
+        jsPlumb.setContainer("drawing");
+
+        jsPlumb.Defaults.MaxConnections = 1000;
+
+        (function initTopologyPanning() {
+            const $topology = $("#topology");
+            const $drawing = $("#drawing");
+            if ($topology.length === 0 || $drawing.length === 0) {
+                return;
+            }
+
+            $topology.off(".topopanning");
+            $(document).off(".topopanning");
+
+            let dragging = false;
+            let startX = 0;
+            let startY = 0;
+            let startLeft = 0;
+            let startTop = 0;
+
+            function panBlocked(target) {
+                return $(target).closest(".window, ._jsPlumb_connector, 
._jsPlumb_endpoint, ._jsPlumb_overlay").length > 0;
+            }
+
+            $topology.on("mousedown.topopanning", function (e) {
+                if (e.button !== 0) {
+                    return;
+                }
+                if (panBlocked(e.target)) {
+                    return;
+                }
+
+                dragging = true;
+                startX = e.clientX;
+                startY = e.clientY;
+                startLeft = topologyParsePx($drawing.css("left"), 0);
+                startTop = topologyParsePx($drawing.css("top"), 0);
+                $topology.addClass("topology-panning");
+                e.preventDefault();
+            });
+
+            $(document).on("mousemove.topopanning", function (e) {
+                if (!dragging) {
+                    return;
+                }
+                const dx = e.clientX - startX;
+                const dy = e.clientY - startY;
+                $drawing.css("left", (startLeft + dx) + "px");
+                $drawing.css("top", (startTop + dy) + "px");
+            });
+
+            $(document).on("mouseup.topopanning", function () {
+                if (!dragging) {
+                    return;
+                }
+                dragging = false;
+                $("#topology").removeClass("topology-panning");
+                jsPlumb.repaintEverything();
+            });
+        })();
+
+        const val = getTopology();
+        if (val.__zoom__ == null) {
+            setZoom($("#drawing")[0], zoom);
+        } else {
+            setZoom($("#drawing")[0], val.__zoom__);
+        }
+    });
+}
 
-    jsPlumb.Defaults.MaxConnections = 1000;
+window.checkConnection = function () {
+    jsPlumb.ready(function () {
+        const items = [];
+
+        jsPlumb.select({scope: "CONNECTOR"}).each(function (connection) {
+            const id = connection && connection.target ? connection.target.id 
: null;
+            if (id) {
+                items.push({kind: "CHECK_CONNECTOR", id: id, conn: 
connection});
+            }
+        });
+        jsPlumb.select({scope: "RESOURCE"}).each(function (connection) {
+            const id = connection && connection.target ? connection.target.id 
: null;
+            if (id) {
+                items.push({kind: "CHECK_RESOURCE", id: id, conn: connection});
+            }
+        });
+
+        window.__topologyCheckRunId = (window.__topologyCheckRunId || 0) + 1;
+        const runId = window.__topologyCheckRunId;
+
+        const batchSize = 25;
+        let idx = 0;
+
+        function step() {
+            if (window.__topologyCheckRunId !== runId) {
+                return;
+            }
+            const end = Math.min(idx + batchSize, items.length);
+
+            if (jsPlumb.setSuspendDrawing) {
+                jsPlumb.setSuspendDrawing(true);
+            }
+            try {
+                for (let i = idx; i < end; i++) {
+                    const it = items[i];
+                    if (it.conn) {
+                        it.conn.setPaintStyle(def.paintStyle);
+                    }
+                    jsPlumb.selectEndpoints({element: 
[it.id]}).setPaintStyle(def.endpointStyle);
+
+                    Wicket.WebSocket.send("{ \"kind\":\"" + it.kind + "\", 
\"target\":\"" + it.id + "\" }");
+                }
+            } finally {
+                if (jsPlumb.setSuspendDrawing) {
+                    jsPlumb.setSuspendDrawing(false, true);
+                } else {
+                    jsPlumb.repaintEverything();
+                }
+            }
+
+            idx = end;
+            if (idx < items.length) {
+                window.setTimeout(step, 0);
+            }
+        }
+
+        step();
+    });
+}
+
+window.addEndpoint = function (source, target, scope) {
+    const sourceElement = $(document.getElementById(source));
 
-    $("#drawing").draggable({
-      containment: 'topology',
-      cursor: 'move'
+    const top = parseFloat(sourceElement.css("top")) + 10;
+    const left = parseFloat(sourceElement.css("left")) - 150;
+
+    setPosition(target, left, top);
+    jsPlumb.ready(function () {
+        
jsPlumb.draggable(jsPlumb.getSelector(document.getElementById(target)));
+        jsPlumb.connect({source: source, target: target, scope: scope}, def);
     });
+}
 
-    var val = getTopology();
-    if (val.__zoom__ == null) {
-      setZoom($("#drawing")[0], zoom);
-    } else {
-      setZoom($("#drawing")[0], val.__zoom__);
+function topologyParsePx(val, fallback) {
+    if (val == null) {
+        return fallback;
+    }
+    if (typeof val === 'number') {
+        return val;
     }
-  });
+    const n = parseFloat(String(val).replace("px", ""));
+    return isNaN(n) ? fallback : n;
 }
 
-window.checkConnection = function () {
-  jsPlumb.ready(function () {
-    jsPlumb.select({scope: "CONNECTOR"}).each(function (connection) {
-      Wicket.WebSocket.send("{ \"kind\":\"CHECK_CONNECTOR\", \"target\":\"" + 
connection.target.id + "\" }");
-    });
-    jsPlumb.select({scope: "RESOURCE"}).each(function (connection) {
-      Wicket.WebSocket.send("{ \"kind\":\"CHECK_RESOURCE\", \"target\":\"" + 
connection.target.id + "\" }");
+function topologyUniqPush(arr, value) {
+    for (let i = 0; i < arr.length; i++) {
+        if (arr[i] === value) {
+            return;
+        }
+    }
+    arr.push(value);
+}
+
+function topologyBuildChildren(connections) {
+    const childrenById = {};
+    const indegree = {};
+    for (let i = 0; i < connections.length; i++) {
+        const c = connections[i];
+        if (!c || !c.source || !c.target) {
+            continue;
+        }
+        const sourceId = c.source.id;
+        const targetId = c.target.id;
+        if (!sourceId || !targetId || sourceId === targetId) {
+            continue;
+        }
+
+        if (!childrenById[sourceId]) {
+            childrenById[sourceId] = [];
+        }
+        topologyUniqPush(childrenById[sourceId], targetId);
+
+        if (indegree[targetId] == null) {
+            indegree[targetId] = 1;
+        } else {
+            indegree[targetId] = indegree[targetId] + 1;
+        }
+        if (indegree[sourceId] == null) {
+            indegree[sourceId] = 0;
+        }
+    }
+    return {childrenById: childrenById, indegree: indegree};
+}
+
+function topologyPickRoot(allNodeIds, childrenById, indegree) {
+    const rootEl = $("#drawing .window.topology_root")[0];
+    if (rootEl && rootEl.id) {
+        return rootEl.id;
+    }
+
+    for (let i = 0; i < allNodeIds.length; i++) {
+        const id = allNodeIds[i];
+        if ((indegree[id] == null || indegree[id] === 0) && childrenById[id] 
&& childrenById[id].length > 0) {
+            return id;
+        }
+    }
+
+    return allNodeIds.length > 0 ? allNodeIds[0] : null;
+}
+
+function topologyComputeSubtreeWidth(nodeId, childrenById, nodeSizeById, hGap, 
visiting, memo) {
+    if (memo[nodeId] != null) {
+        return memo[nodeId];
+    }
+    if (visiting[nodeId]) {
+        memo[nodeId] = nodeSizeById[nodeId] ? nodeSizeById[nodeId].w : 140;
+        return memo[nodeId];
+    }
+
+    visiting[nodeId] = true;
+    const children = childrenById[nodeId] || [];
+    const ownW = nodeSizeById[nodeId] ? nodeSizeById[nodeId].w : 140;
+    if (children.length === 0) {
+        memo[nodeId] = ownW;
+        visiting[nodeId] = false;
+        return memo[nodeId];
+    }
+
+    let total = 0;
+    for (let i = 0; i < children.length; i++) {
+        total += topologyComputeSubtreeWidth(children[i], childrenById, 
nodeSizeById, hGap, visiting, memo);
+    }
+    total += hGap * (children.length - 1);
+    memo[nodeId] = Math.max(ownW, total);
+    visiting[nodeId] = false;
+    return memo[nodeId];
+}
+
+function topologyAssignPositions(nodeId, childrenById, nodeSizeById, 
subtreeWById, levelY, xLeft, hGap, vGap, outPos, visited) {
+    if (visited[nodeId]) {
+        return;
+    }
+    visited[nodeId] = true;
+
+    const nodeSize = nodeSizeById[nodeId] || {w: 140, h: 55};
+    const subtreeW = subtreeWById[nodeId] != null ? subtreeWById[nodeId] : 
nodeSize.w;
+    const xCenter = xLeft + subtreeW / 2;
+    outPos[nodeId] = {x: xCenter - nodeSize.w / 2, y: levelY};
+
+    const children = childrenById[nodeId] || [];
+    if (children.length === 0) {
+        return;
+    }
+
+    const nextY = levelY + nodeSize.h + vGap;
+    let childX = xLeft;
+    for (let i = 0; i < children.length; i++) {
+        const childId = children[i];
+        const childSubtreeW = subtreeWById[childId] != null
+            ? subtreeWById[childId]
+            : (nodeSizeById[childId] ? nodeSizeById[childId].w : 140);
+
+        topologyAssignPositions(childId, childrenById, nodeSizeById, 
subtreeWById, nextY, childX, hGap, vGap, outPos, visited);
+        childX += childSubtreeW + hGap;
+    }
+}
+
+function topologyShowVeil() {
+    const veil = document.getElementById("veil");
+    if (veil) {
+        veil.style.display = "block";
+    }
+}
+
+function topologyHideVeil() {
+    const veil = document.getElementById("veil");
+    if (veil) {
+        veil.style.display = "none";
+    }
+}
+
+function topologyRunWithOptionalVeil(fn, options) {
+    options = options || {};
+    const delayMs = options.delayMs != null ? options.delayMs : 120;
+    let shown = false;
+
+    const t = window.setTimeout(function () {
+        topologyShowVeil();
+        shown = true;
+    }, delayMs);
+
+    window.setTimeout(function () {
+        try {
+            fn();
+        } finally {
+            window.clearTimeout(t);
+            if (shown) {
+                topologyHideVeil();
+            }
+        }
+    }, 0);
+}
+
+window.autoLayoutTree = function (options) {
+    options = options || {};
+    const hGap = options.hGap != null ? options.hGap : 70;
+    const vGap = options.vGap != null ? options.vGap : 80;
+    const padding = options.padding != null ? options.padding : 10;
+    const centerInView = options.centerInView != null ? options.centerInView : 
false;
+
+    topologyRunWithOptionalVeil(function () {
+        jsPlumb.ready(function () {
+            try {
+                const nodeEls = $("#drawing .window").toArray();
+                if (!nodeEls || nodeEls.length === 0) {
+                    return;
+                }
+
+                // Build node sizes (unscaled), and stable list of ids.
+                const nodeSizeById = {};
+                const nodeIds = [];
+                for (let i = 0; i < nodeEls.length; i++) {
+                    const el = nodeEls[i];
+                    if (!el || !el.id) {
+                        continue;
+                    }
+                    nodeIds.push(el.id);
+                    nodeSizeById[el.id] = {w: el.offsetWidth || 140, h: 
el.offsetHeight || 55};
+                }
+
+                const connections = jsPlumb.getAllConnections ? 
jsPlumb.getAllConnections() : [];
+                const graph = topologyBuildChildren(connections);
+                const rootId = topologyPickRoot(nodeIds, graph.childrenById, 
graph.indegree);
+                if (!rootId) {
+                    return;
+                }
+
+                // Compute subtree widths.
+                const subtreeWById = {};
+                for (let j = 0; j < nodeIds.length; j++) {
+                    topologyComputeSubtreeWidth(
+                        nodeIds[j],
+                        graph.childrenById,
+                        nodeSizeById,
+                        hGap,
+                        {},
+                        subtreeWById);
+                }
+
+                // Assign relative positions starting from root.
+                const relPos = {};
+                topologyAssignPositions(
+                    rootId,
+                    graph.childrenById,
+                    nodeSizeById,
+                    subtreeWById,
+                    0,
+                    0,
+                    hGap,
+                    vGap,
+                    relPos,
+                    {});
+
+                // Translate either to the current viewport center or keep 
root on its current position (default).
+                let dx = 0;
+                let dy = 0;
+                if (centerInView) {
+                    let minX = null, minY = null, maxX = null, maxY = null;
+                    for (let bb = 0; bb < nodeIds.length; bb++) {
+                        const bid = nodeIds[bb];
+                        const rp = relPos[bid];
+                        if (!rp) {
+                            continue;
+                        }
+                        const sz = nodeSizeById[bid] || {w: 140, h: 55};
+                        minX = minX == null ? rp.x : Math.min(minX, rp.x);
+                        minY = minY == null ? rp.y : Math.min(minY, rp.y);
+                        maxX = maxX == null ? (rp.x + sz.w) : Math.max(maxX, 
rp.x + sz.w);
+                        maxY = maxY == null ? (rp.y + sz.h) : Math.max(maxY, 
rp.y + sz.h);
+                    }
+
+                    if (minX != null && minY != null && maxX != null && maxY 
!= null) {
+                        const layoutCx = (minX + maxX) / 2;
+                        const layoutCy = (minY + maxY) / 2;
+
+                        const $topology = $("#topology");
+                        const $drawing = $("#drawing");
+                        const topoW = $topology.width() || 0;
+                        const topoH = $topology.height() || 0;
+                        const drawingLeft = 
topologyParsePx($drawing.css("left"), 0);
+                        const drawingTop = 
topologyParsePx($drawing.css("top"), 0);
+
+                        const topoCookie = getTopology();
+                        const z = (jsPlumb.getZoom && jsPlumb.getZoom()) || 
topoCookie.__zoom__ || 1 || 1;
+
+                        const viewCx = (topoW / 2 - drawingLeft) / z;
+                        const viewCy = (topoH / 2 - drawingTop) / z;
+
+                        dx = viewCx - layoutCx;
+                        dy = viewCy - layoutCy;
+                    }
+                } else {
+                    const rootEl = document.getElementById(rootId);
+                    let rootLeft = topologyParsePx(rootEl && rootEl.style ? 
rootEl.style.left : null, 0);
+                    let rootTop = topologyParsePx(rootEl && rootEl.style ? 
rootEl.style.top : null, 0);
+                    if (isNaN(rootLeft) || isNaN(rootTop) || (rootLeft === 0 
&& rootTop === 0)) {
+                        rootLeft = topologyParsePx($(rootEl).css("left"), 0);
+                        rootTop = topologyParsePx($(rootEl).css("top"), 0);
+                    }
+                    const rootRel = relPos[rootId];
+                    dx = rootLeft - (rootRel ? rootRel.x : 0);
+                    dy = rootTop - (rootRel ? rootRel.y : 0);
+                }
+
+                const topo = getTopology();
+                let placedMinX = null;
+                let placedMaxX = null;
+                let placedMaxY = null;
+
+                if (jsPlumb.setSuspendDrawing) {
+                    jsPlumb.setSuspendDrawing(true);
+                }
+
+                for (let k = 0; k < nodeIds.length; k++) {
+                    const id = nodeIds[k];
+                    const p = relPos[id];
+                    if (!p) {
+                        continue;
+                    }
+                    const x = Math.round(p.x + dx + padding);
+                    const y = Math.round(p.y + dy + padding);
+
+                    const nEl = document.getElementById(id);
+                    if (!nEl) {
+                        continue;
+                    }
+                    nEl.style.left = x + "px";
+                    nEl.style.top = y + "px";
+
+                    placedMinX = placedMinX == null ? x : Math.min(placedMinX, 
x);
+                    placedMaxX = placedMaxX == null ? x : Math.max(placedMaxX, 
x + (nodeSizeById[id] ? nodeSizeById[id].w : 140));
+                    placedMaxY = placedMaxY == null ? y : Math.max(placedMaxY, 
y + (nodeSizeById[id] ? nodeSizeById[id].h : 55));
+
+                    if (topo[id] == null) {
+                        topo[id] = {top: y + "px", left: x + "px"};
+                    } else {
+                        topo[id].top = y + "px";
+                        topo[id].left = x + "px";
+                    }
+                }
+
+                const unplaced = [];
+                for (let u = 0; u < nodeIds.length; u++) {
+                    if (!relPos[nodeIds[u]]) {
+                        unplaced.push(nodeIds[u]);
+                    }
+                }
+                if (unplaced.length > 0) {
+                    let startX = (placedMaxX == null ? 0 : placedMaxX) + hGap 
* 2;
+                    let startY = (placedMinX == null ? 0 : (placedMinX - 20));
+                    let colX = startX;
+                    let rowY = startY;
+                    let colW = 0;
+                    const maxColH = Math.max(420, (placedMaxY == null ? 420 : 
(placedMaxY - startY)));
+                    for (let ui = 0; ui < unplaced.length; ui++) {
+                        const uid = unplaced[ui];
+                        const us = nodeSizeById[uid] || {w: 140, h: 55};
+                        if ((rowY - startY) + us.h > maxColH) {
+                            colX = colX + colW + hGap;
+                            rowY = startY;
+                            colW = 0;
+                        }
+
+                        const ux = Math.round(colX + padding);
+                        const uy = Math.round(rowY + padding);
+                        const uEl = document.getElementById(uid);
+                        if (uEl) {
+                            uEl.style.left = ux + "px";
+                            uEl.style.top = uy + "px";
+                        }
+                        topo[uid] = {top: uy + "px", left: ux + "px"};
+
+                        rowY += us.h + 12;
+                        colW = Math.max(colW, us.w);
+                    }
+                }
+
+                $.cookie("topology", JSON.stringify(topo), {expires: 9999});
+
+                let treeMinX = null, treeMinY = null, treeMaxX = null, 
treeMaxY = null;
+                for (let tb = 0; tb < nodeIds.length; tb++) {
+                    const tid = nodeIds[tb];
+                    const tp = relPos[tid];
+                    if (!tp) {
+                        continue;
+                    }
+                    const ts = nodeSizeById[tid] || {w: 140, h: 55};
+                    const ax = tp.x + dx + padding;
+                    const ay = tp.y + dy + padding;
+                    treeMinX = treeMinX == null ? ax : Math.min(treeMinX, ax);
+                    treeMinY = treeMinY == null ? ay : Math.min(treeMinY, ay);
+                    treeMaxX = treeMaxX == null ? (ax + ts.w) : 
Math.max(treeMaxX, ax + ts.w);
+                    treeMaxY = treeMaxY == null ? (ay + ts.h) : 
Math.max(treeMaxY, ay + ts.h);
+                }
+                window.__topologyTreeBBox = (treeMinX == null) ? null : {
+                    minX: treeMinX,
+                    minY: treeMinY,
+                    maxX: treeMaxX,
+                    maxY: treeMaxY
+                };
+
+                if (jsPlumb.setSuspendDrawing) {
+                    jsPlumb.setSuspendDrawing(false, true);
+                } else {
+                    jsPlumb.repaintEverything();
+                }
+
+                if (window.__topologyTreeBBox) {
+                    recenterToTree();
+                }
+            } catch (err) {
+                console.debug("autoLayoutTree failed", err);
+            }
+        });
     });
-  });
+};
+
+function topologyGetZoom() {
+    const topoCookie = getTopology();
+    const z = (jsPlumb.getZoom && jsPlumb.getZoom()) || topoCookie.__zoom__ || 
1;
+    return z || 1;
 }
 
-window.addEndpoint = function (source, target, scope) {
-  var sourceElement = $(document.getElementById(source));
+function topologyRecenterOnBBox(bbox) {
+    if (!bbox) {
+        return;
+    }
+    const $topology = $("#topology");
+    const $drawing = $("#drawing");
+    if ($topology.length === 0 || $drawing.length === 0) {
+        return;
+    }
+
+    const topoW = $topology.width() || 0;
+    const topoH = $topology.height() || 0;
+    if (topoW <= 0 || topoH <= 0) {
+        return;
+    }
+
+    const z = topologyGetZoom();
+    const drawingW = $drawing.width() || 0;
+    const drawingH = $drawing.height() || 0;
+    const originX = drawingW * 0.5;
+    const originY = drawingH * 0.5;
 
-  var top = parseFloat(sourceElement.css("top")) + 10;
-  var left = parseFloat(sourceElement.css("left")) - 150;
+    const cx = (bbox.minX + bbox.maxX) / 2;
+    const cy = (bbox.minY + bbox.maxY) / 2;
 
-  setPosition(target, left, top);
-  jsPlumb.ready(function () {
-    jsPlumb.draggable(jsPlumb.getSelector(document.getElementById(target)));
-    jsPlumb.connect({source: source, target: target, scope: scope}, def);
-  });
+    const viewportCenterX = topoW / 2;
+    const viewportCenterY = topoH / 2;
+
+    const newLeft = viewportCenterX - originX - (cx - originX) * z;
+    const newTop = viewportCenterY - originY - (cy - originY) * z;
+
+    $drawing.css("left", Math.round(newLeft) + "px");
+    $drawing.css("top", Math.round(newTop) + "px");
+    jsPlumb.repaintEverything();
 }
 
+window.recenterToTree = function () {
+    topologyRunWithOptionalVeil(function () {
+        jsPlumb.ready(function () {
+            let bbox = window.__topologyTreeBBox;
+            if (!bbox) {
+                const nodeEls = $("#drawing .window").toArray();
+                let minX = null, minY = null, maxX = null, maxY = null;
+                for (let i = 0; i < nodeEls.length; i++) {
+                    const el = nodeEls[i];
+                    if (!el) {
+                        continue;
+                    }
+                    const x = topologyParsePx($(el).css("left"), 0);
+                    const y = topologyParsePx($(el).css("top"), 0);
+                    const w = el.offsetWidth || 140;
+                    const h = el.offsetHeight || 55;
+                    minX = minX == null ? x : Math.min(minX, x);
+                    minY = minY == null ? y : Math.min(minY, y);
+                    maxX = maxX == null ? (x + w) : Math.max(maxX, x + w);
+                    maxY = maxY == null ? (y + h) : Math.max(maxY, y + h);
+                }
+                bbox = (minX == null) ? null : {minX: minX, minY: minY, maxX: 
maxX, maxY: maxY};
+            }
+
+            topologyRecenterOnBBox(bbox);
+        });
+    });
+};
+
 jsPlumb.importDefaults({
-  Connector: ["Straight"],
-  DragOptions: {
-    cursor: "pointer",
-    zIndex: 2000
-  },
-  HoverClass: "connector-hover"
+    Connector: ["Bezier", { curviness: 10 }],
+    DragOptions: {
+        cursor: "pointer",
+        zIndex: 2000
+    },
+    HoverClass: "connector-hover"
 });
 
-jQuery(function ($) {
-  Wicket.Event.subscribe("/websocket/message", function (jqEvent, message) {
-    var val = JSON.parse(decodeURIComponent(message));
-    switch (val.status) {
-      case 'UNKNOWN':
-        unknown(val.target);
-        break;
-      case 'REACHABLE':
-        enable(val.target);
-        break;
-      case 'UNREACHABLE':
-        disable(val.target);
-        break;
-      case 'FAILURE':
-        failure(val.target);
-        break;
-      default:
-        break;
+jQuery(function () {
+    const pendingStatusByTarget = {};
+    let flushScheduled = false;
+
+    function applyStatus(targetId, status) {
+        if (!targetId) {
+            return;
+        }
+        switch (status) {
+            case 'UNKNOWN':
+                unknown(targetId);
+                break;
+            case 'REACHABLE':
+                enable(targetId);
+                break;
+            case 'UNREACHABLE':
+                disable(targetId);
+                break;
+            case 'FAILURE':
+                failure(targetId);
+                break;
+            default:
+                break;
+        }
     }
-  });
+
+    function flushStatuses() {
+        flushScheduled = false;
+        const entries = Object.entries(pendingStatusByTarget);
+        if (entries.length === 0) {
+            return;
+        }
+
+        for (const [k] of entries) {
+            delete pendingStatusByTarget[k];
+        }
+
+        jsPlumb.ready(function () {
+            if (jsPlumb.setSuspendDrawing) {
+                jsPlumb.setSuspendDrawing(true);
+            }
+            try {
+                for (let i = 0; i < entries.length; i++) {
+                    const [targetId, status] = entries[i];
+                    applyStatus(targetId, status);
+                }
+            } finally {
+                if (jsPlumb.setSuspendDrawing) {
+                    jsPlumb.setSuspendDrawing(false, true);
+                } else {
+                    jsPlumb.repaintEverything();
+                }
+            }
+        });
+    }
+
+    function scheduleFlush() {
+        if (flushScheduled) {
+            return;
+        }
+        flushScheduled = true;
+        window.requestAnimationFrame(flushStatuses);
+    }
+
+    Wicket.Event.subscribe("/websocket/message", function (_jqEvent, message) {
+        const val = JSON.parse(decodeURIComponent(message));
+        pendingStatusByTarget[val.target] = val.status;
+        scheduleFlush();
+    });
 });
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java
index 3757adc5e6..922295c25a 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java
@@ -105,6 +105,8 @@ public abstract class ActionLink<T extends Serializable> 
implements Serializable
         PUSH_TASKS("read"),
         ZOOM_IN("zoomin"),
         ZOOM_OUT("zoomout"),
+        AUTO_LAYOUT("autoLayout"),
+        RECENTER("recenter"),
         VIEW_EXECUTIONS("read"),
         VIEW_DETAILS("read"),
         MANAGE_APPROVAL("edit"),
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionPanel.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionPanel.java
index 8b7e01ed8a..684d622877 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionPanel.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionPanel.java
@@ -27,7 +27,9 @@ import 
org.apache.syncope.client.console.wicket.markup.html.link.VeilPopupSettin
 import org.apache.syncope.client.ui.commons.Constants;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.IndicatingOnConfirmAjaxLink;
 import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.ajax.AjaxChannel;
 import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
 import 
org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
 import org.apache.wicket.event.Broadcast;
 import org.apache.wicket.extensions.ajax.markup.html.IndicatingAjaxLink;
@@ -117,6 +119,17 @@ public final class ActionPanel<T extends Serializable> 
extends Panel {
 
                 private static final long serialVersionUID = 
-7978723352517770644L;
 
+                @Override
+                protected void updateAjaxAttributes(final 
AjaxRequestAttributes attributes) {
+                    super.updateAjaxAttributes(attributes);
+                    switch (action.getType()) {
+                        case ZOOM_IN, ZOOM_OUT, AUTO_LAYOUT, RECENTER ->
+                            attributes.setChannel(new 
AjaxChannel("ui-fast-actions", AjaxChannel.Type.DROP));
+                        default -> {
+                        }
+                    }
+                }
+
                 @Override
                 public void onClick(final AjaxRequestTarget target) {
                     beforeOnClick(target);
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
index d23e63a0d9..f840c70ddc 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
@@ -261,6 +261,14 @@ zoom_out.class=fa fa-search-minus
 zoom_out.title=zoom-out
 zoom_out.alt=zoom-out icon
 
+auto_layout.class=fas fa-sitemap
+auto_layout.title=auto layout
+auto_layout.alt=auto layout icon
+
+recenter.class=fas fa-crosshairs
+recenter.title=recenter
+recenter.alt=recenter icon
+
 manage_accounts.class=fas fa-users
 manage_accounts.title=manage accounts
 manage_accounts.alt=manage accounts icon
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_fr_CA.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_fr_CA.properties
index 2ac8854265..66e3a6bc1f 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_fr_CA.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_fr_CA.properties
@@ -205,6 +205,15 @@ zoom_in.alt=ic\u00f4ne zoom-in
 zoom_out.class=fa fa-search-minus
 zoom_out.title=zoom-out
 zoom_out.alt=ic\u00f4ne zoom-out
+
+auto_layout.class=fas fa-sitemap
+auto_layout.title=réorganiser
+auto_layout.alt=icône de disposition automatique
+
+recenter.class=fas fa-crosshairs
+recenter.title=recentrer
+recenter.alt=icône de recentrage
+
 manage_accounts.class=fas fa-users
 manage_accounts.title=g\u00e9rer comptes
 manage_accounts.alt=ic\u00f4ne g\u00e9rer groupes
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
index c0ee2d4265..bb9cc1f1f2 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
@@ -241,6 +241,14 @@ zoom_in.alt=zoom-in icon
 zoom_out.class=fa fa-search-minus
 zoom_out.title=rimpicciolisci
 zoom_out.alt=zoom-out icon
+
+auto_layout.class=fas fa-sitemap
+auto_layout.title=riordina
+auto_layout.alt=auto layout icon
+
+recenter.class=fas fa-crosshairs
+recenter.title=ricentra
+recenter.alt=recenter icon
 reconciliation_push.class=fa fa-chevron-circle-right
 reconciliation_push.title=push
 reconciliation_push.alt=reconciliation push icon
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
index 18c6d2465a..930eb52fdb 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
@@ -242,6 +242,14 @@ zoom_out.class=fa fa-search-minus
 zoom_out.title=\u30ba\u30fc\u30e0\u30a2\u30a6\u30c8
 zoom_out.alt=\u30ba\u30fc\u30e0\u30a2\u30a6\u30c8
 
+auto_layout.class=fas fa-sitemap
+auto_layout.title=\u518d\u914d\u7f6e
+auto_layout.alt=\u81ea\u52d5\u30ec\u30a4\u30a2\u30a6\u30c8\u30a2\u30a4\u30b3\u30f3
+
+recenter.class=fas fa-crosshairs
+recenter.title=\u4e2d\u592e\u306b\u623b\u3059
+recenter.alt=\u518d\u30bb\u30f3\u30bf\u30fc\u30a2\u30a4\u30b3\u30f3
+
 reconciliation_push.class=fa fa-chevron-circle-right
 reconciliation_push.title=\u30d7\u30c3\u30b7\u30e5
 reconciliation_push.alt=\u7167\u5408\u30d7\u30c3\u30b7\u30e5 icon
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
index 1290d247d5..506345d269 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
@@ -256,6 +256,15 @@ zoom_in.alt=zoom-in icon
 zoom_out.class=fa fa-search-minus
 zoom_out.title=zoom-out
 zoom_out.alt=zoom-out icon
+
+auto_layout.class=fas fa-sitemap
+auto_layout.title=reorganizar
+auto_layout.alt=ícone de layout automático
+
+recenter.class=fas fa-crosshairs
+recenter.title=recentralizar
+recenter.alt=ícone de recentralização
+
 reconciliation_push.class=fa fa-chevron-circle-right
 reconciliation_push.title=push
 reconciliation_push.alt=reconciliation push icon
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
index d2fb2dfd01..421d055f5f 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
@@ -242,6 +242,14 @@ zoom_out.class=fa fa-search-minus
 zoom_out.title=zoom-out
 zoom_out.alt=zoom-out icon
 
+auto_layout.class=fas fa-sitemap
+auto_layout.title=\u043f\u0435\u0440\u0435\u0441\u0442\u0440\u043e\u0438\u0442\u044c
+auto_layout.alt=\u0438\u043a\u043e\u043d\u043a\u0430 
\u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439
 \u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0438
+
+recenter.class=fas fa-crosshairs
+recenter.title=\u0446\u0435\u043d\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c
+recenter.alt=\u0438\u043a\u043e\u043d\u043a\u0430 
\u0446\u0435\u043d\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f
+
 reconciliation_push.class=fa fa-chevron-circle-right
 reconciliation_push.title=push
 reconciliation_push.alt=reconciliation push icon

Reply via email to