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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new f3ad4fab862c CAMEL-23935: Route diagram mini tree should scroll to 
keep selected node visible (#24505)
f3ad4fab862c is described below

commit f3ad4fab862ce07ca3e5df8e2904b03b8f646fe3
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jul 8 07:39:51 2026 +0200

    CAMEL-23935: Route diagram mini tree should scroll to keep selected node 
visible (#24505)
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../jbang/core/commands/tui/RouteTreePreview.java  |  52 +++---
 .../core/commands/tui/RouteTreePreviewTest.java    | 184 +++++++++++++++++++++
 2 files changed, 215 insertions(+), 21 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePreview.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePreview.java
index f2f950f52c99..83a43a19b2a0 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePreview.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePreview.java
@@ -54,41 +54,50 @@ class RouteTreePreview {
             return List.of(Line.from(Span.styled("(no structure)", 
Style.EMPTY.dim())));
         }
 
-        List<Line> result = new ArrayList<>();
-        addLine(result, " ", root, maxLines, maxWidth, selectedNode);
-        addChildren(root.children, " ", result, maxLines, maxWidth, 
selectedNode);
-        return result;
+        List<Line> allLines = new ArrayList<>();
+        List<TreeNode> lineNodes = new ArrayList<>();
+        addLine(allLines, lineNodes, " ", root, maxWidth, selectedNode);
+        addChildren(root.children, " ", allLines, lineNodes, maxWidth, 
selectedNode);
+
+        if (allLines.size() <= maxLines) {
+            return allLines;
+        }
+
+        // find the selected node's line and scroll to keep it visible
+        int selectedIdx = -1;
+        if (selectedNode != null) {
+            selectedIdx = lineNodes.indexOf(selectedNode);
+        }
+
+        int start;
+        if (selectedIdx < 0) {
+            start = 0;
+        } else {
+            // center the selected node in the window, but keep root visible 
when near the top
+            start = selectedIdx - maxLines / 2;
+            start = Math.max(0, Math.min(start, allLines.size() - maxLines));
+        }
+
+        return allLines.subList(start, start + maxLines);
     }
 
     private static void addChildren(
             List<TreeNode> children, String parentIndent,
-            List<Line> result, int maxLines, int maxWidth, TreeNode 
selectedNode) {
+            List<Line> result, List<TreeNode> lineNodes, int maxWidth, 
TreeNode selectedNode) {
         for (int i = 0; i < children.size(); i++) {
-            if (result.size() >= maxLines) {
-                return;
-            }
             boolean last = (i == children.size() - 1);
-
-            if (result.size() >= maxLines - 1 && !last) {
-                result.add(Line.from(Span.styled(parentIndent + "...", 
Style.EMPTY.dim())));
-                return;
-            }
-
             String connector = last ? "└─" : "├─";
             String childCont = last ? "  " : "│ ";
 
             TreeNode child = children.get(i);
-            addLine(result, parentIndent + connector, child, maxLines, 
maxWidth, selectedNode);
-            addChildren(child.children, parentIndent + childCont, result, 
maxLines, maxWidth, selectedNode);
+            addLine(result, lineNodes, parentIndent + connector, child, 
maxWidth, selectedNode);
+            addChildren(child.children, parentIndent + childCont, result, 
lineNodes, maxWidth, selectedNode);
         }
     }
 
     private static void addLine(
-            List<Line> result, String prefix, TreeNode node,
-            int maxLines, int maxWidth, TreeNode selectedNode) {
-        if (result.size() >= maxLines) {
-            return;
-        }
+            List<Line> result, List<TreeNode> lineNodes, String prefix, 
TreeNode node,
+            int maxWidth, TreeNode selectedNode) {
         String label = buildLabel(node);
         boolean selected = (node == selectedNode);
 
@@ -96,6 +105,7 @@ class RouteTreePreview {
         spans.add(Span.styled(prefix, Style.EMPTY.fg(Color.DARK_GRAY)));
         spans.add(styledLabel(node.info.type, truncate(label, maxWidth - 
prefix.length()), selected));
         result.add(Line.from(spans));
+        lineNodes.add(node);
     }
 
     private static Span styledLabel(String type, String text, boolean 
selected) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePreviewTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePreviewTest.java
new file mode 100644
index 000000000000..56021200fce7
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePreviewTest.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+
+import dev.tamboui.text.Line;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.TreeNode;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class RouteTreePreviewTest {
+
+    @Test
+    void testSmallTreeFitsWithoutScrolling() {
+        LayoutRoute layout = buildContentBasedRouterLayout();
+        TreeNode root = layout.nodes.get(0).treeNode;
+
+        // with enough lines, all nodes are shown
+        List<Line> lines = RouteTreePreview.buildTree(layout, 20, 40, null);
+        assertTrue(lines.size() > 5, "Tree should have multiple lines");
+    }
+
+    @Test
+    void testTreeScrollsToSelectedNode() {
+        LayoutRoute layout = buildContentBasedRouterLayout();
+        TreeNode root = layout.nodes.get(0).treeNode;
+
+        // find the last node deep in the tree (last child of "otherwise")
+        TreeNode choice = root.children.get(2); // choice
+        TreeNode otherwise = choice.children.get(2); // otherwise
+        TreeNode lastLog = otherwise.children.get(1); // log (last node)
+
+        // build full tree to know total size
+        List<Line> fullTree = RouteTreePreview.buildTree(layout, 100, 40, 
null);
+        int totalLines = fullTree.size();
+
+        // with a small window, the selected node at the bottom must be visible
+        int maxLines = 6;
+        assertTrue(totalLines > maxLines, "Tree must be taller than window for 
this test");
+
+        List<Line> windowedTree = RouteTreePreview.buildTree(layout, maxLines, 
40, lastLog);
+        assertEquals(maxLines, windowedTree.size());
+
+        // the selected node's line should be present in the windowed output
+        String lastLineText = lineToPlainText(windowedTree);
+        assertTrue(lastLineText.contains("log"), "Selected 'log' node should 
be visible in the scrolled tree");
+    }
+
+    @Test
+    void testTreeScrollsToSelectedNodeInMiddle() {
+        LayoutRoute layout = buildContentBasedRouterLayout();
+        TreeNode root = layout.nodes.get(0).treeNode;
+
+        // select a node in the middle of the tree
+        TreeNode choice = root.children.get(2); // choice
+        TreeNode when2 = choice.children.get(1); // second when
+
+        int maxLines = 6;
+        List<Line> windowedTree = RouteTreePreview.buildTree(layout, maxLines, 
40, when2);
+        assertEquals(maxLines, windowedTree.size());
+
+        String allText = lineToPlainText(windowedTree);
+        assertTrue(allText.contains("when"), "Selected 'when' node should be 
visible");
+    }
+
+    @Test
+    void testNoSelectedNodeShowsFromTop() {
+        LayoutRoute layout = buildContentBasedRouterLayout();
+
+        int maxLines = 6;
+        List<Line> lines = RouteTreePreview.buildTree(layout, maxLines, 40, 
null);
+        assertEquals(maxLines, lines.size());
+
+        // first line should be the root (from)
+        String firstLine = lines.get(0).rawContent();
+        assertTrue(firstLine.contains("from"), "Without selection, tree should 
start from the root");
+    }
+
+    /**
+     * Builds a layout matching the content-based-router example: from(timer) 
-> setBody -> convertBodyTo -> choice when
+     * -> setHeader -> log when -> setHeader -> log otherwise -> setHeader -> 
log
+     */
+    private LayoutRoute buildContentBasedRouterLayout() {
+        LayoutRoute layout = new LayoutRoute();
+        layout.routeId = "sensor";
+
+        // build tree structure
+        TreeNode root = node("from", "timer:sensor");
+        TreeNode setBody = node("setBody", "${random(0,40)}");
+        TreeNode convertBody = node("convertBodyTo", "int");
+        TreeNode choice = node("choice", null);
+
+        TreeNode when1 = node("when", "${body} >= 30");
+        TreeNode setHeader1 = node("setHeader", "level=hot");
+        TreeNode log1 = node("log", "Hot alert");
+
+        TreeNode when2 = node("when", "${body} >= 15");
+        TreeNode setHeader2 = node("setHeader", "level=normal");
+        TreeNode log2 = node("log", "Normal");
+
+        TreeNode otherwise = node("otherwise", null);
+        TreeNode setHeader3 = node("setHeader", "level=cold");
+        TreeNode log3 = node("log", "Cold alert");
+
+        // wire parent-child relationships
+        addChild(root, setBody);
+        addChild(root, convertBody);
+        addChild(root, choice);
+        addChild(choice, when1);
+        addChild(when1, setHeader1);
+        addChild(when1, log1);
+        addChild(choice, when2);
+        addChild(when2, setHeader2);
+        addChild(when2, log2);
+        addChild(choice, otherwise);
+        addChild(otherwise, setHeader3);
+        addChild(otherwise, log3);
+
+        // create layout nodes and link to tree nodes
+        addLayoutNode(layout, root);
+        addLayoutNode(layout, setBody);
+        addLayoutNode(layout, convertBody);
+        addLayoutNode(layout, choice);
+        addLayoutNode(layout, when1);
+        addLayoutNode(layout, setHeader1);
+        addLayoutNode(layout, log1);
+        addLayoutNode(layout, when2);
+        addLayoutNode(layout, setHeader2);
+        addLayoutNode(layout, log2);
+        addLayoutNode(layout, otherwise);
+        addLayoutNode(layout, setHeader3);
+        addLayoutNode(layout, log3);
+
+        return layout;
+    }
+
+    private TreeNode node(String type, String code) {
+        NodeInfo info = new NodeInfo();
+        info.type = type;
+        info.code = code;
+        return new TreeNode(info);
+    }
+
+    private void addChild(TreeNode parent, TreeNode child) {
+        child.parent = parent;
+        parent.children.add(child);
+    }
+
+    private void addLayoutNode(LayoutRoute layout, TreeNode treeNode) {
+        LayoutNode ln = new LayoutNode();
+        ln.type = treeNode.info.type;
+        ln.treeNode = treeNode;
+        treeNode.layoutNode = ln;
+        layout.nodes.add(ln);
+    }
+
+    private String lineToPlainText(List<Line> lines) {
+        StringBuilder sb = new StringBuilder();
+        for (Line line : lines) {
+            sb.append(line.rawContent()).append("\n");
+        }
+        return sb.toString();
+    }
+}

Reply via email to