This is an automated email from the ASF dual-hosted git repository.
ilgrosso pushed a commit to branch 4_1_X
in repository https://gitbox.apache.org/repos/asf/syncope.git
The following commit(s) were added to refs/heads/4_1_X by this push:
new 6979200e1d Improve Topology UX (#1373)
6979200e1d is described below
commit 6979200e1d9e995a456713e6e1e846dc00d3c9e6
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