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 8bdcd36755e7 CAMEL-25438: camel-jbang - monitor tabs do not wait 
behind data loads of hidden tabs (#27544)
8bdcd36755e7 is described below

commit 8bdcd36755e7043ff53ec537c0429bf831a087ee
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 8 09:41:09 2026 +0200

    CAMEL-25438: camel-jbang - monitor tabs do not wait behind data loads of 
hidden tabs (#27544)
    
    An integration answers one action at a time, about one a second, and
    selecting an integration sent four actions for tabs that were not shown:
    the classpath (jvm) for Classpath, CVE Audit and Maven Dependencies each,
    and the route structure for Catalog. The tab the user opened waited
    behind them, 2-5 s, and longer when switching integrations.
    
    These tabs now load when shown, as onTabSelected already did; the shown
    More tab loads again after an integration change. The tabs that need the
    classpath share one jvm answer per integration for 30 s.
    
    
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../dsl/jbang/core/commands/tui/CatalogTab.java    |   5 +-
 .../dsl/jbang/core/commands/tui/ClasspathTab.java  |  10 +-
 .../dsl/jbang/core/commands/tui/CveAuditTab.java   |  13 +--
 .../jbang/core/commands/tui/HeapHistogramTab.java  |   5 +-
 .../core/commands/tui/MavenDependenciesTab.java    |   5 +-
 .../jbang/core/commands/tui/MonitorContext.java    |  30 ++++++
 .../dsl/jbang/core/commands/tui/TabRegistry.java   |   4 +
 .../core/commands/tui/MonitorTabLazyLoadTest.java  |  92 ++++++++++++++++++
 .../dsl/jbang/core/commands/tui/NoopBackend.java   | 104 +++++++++++++++++++++
 .../tui/TuiBackendHelperRecordingTest.java         |  83 ----------------
 10 files changed, 238 insertions(+), 113 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
index 2b71875c041b..aa95009e975f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
@@ -120,10 +120,7 @@ class CatalogTab extends AbstractTableTab {
         errorMessage = null;
         dataLoaded = false;
         loading.set(false);
-        if (ctx.selectedPid != null) {
-            lastPid = ctx.selectedPid;
-            loadCatalogData();
-        }
+        // loaded when the tab is shown (onTabSelected): a hidden tab must not 
hold up the one the user looks at
     }
 
     boolean isFilterInputActive() {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
index d517531efae8..f39604e02e61 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
@@ -111,10 +111,7 @@ class ClasspathTab extends AbstractTab {
         errorMessage = null;
         dataLoaded = false;
         loading.set(false);
-        if (ctx.selectedPid != null) {
-            lastPid = ctx.selectedPid;
-            loadClasspath();
-        }
+        // loaded when the tab is shown (onTabSelected): a hidden tab must not 
hold up the one the user looks at
     }
 
     @Override
@@ -318,10 +315,7 @@ class ClasspathTab extends AbstractTab {
         String pid = ctx.selectedPid;
         ctx.backgroundExecutor.execute(() -> {
             try {
-                JsonObject action = new JsonObject();
-                action.put("action", "jvm");
-
-                JsonObject response = ctx.executeAction(pid, action, 5000);
+                JsonObject response = ctx.jvmInfo(pid, 5000);
 
                 if (response == null) {
                     applyResult(Collections.emptyList(), "No response from 
integration");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
index 3186fad5b0d3..5f2f1f33f20b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
@@ -124,10 +124,7 @@ class CveAuditTab extends AbstractTableTab {
         dataLoaded = false;
         detailScroll = 0;
         loading.set(false);
-        if (ctx.selectedPid != null) {
-            lastPid = ctx.selectedPid;
-            loadAndScan();
-        }
+        // loaded when the tab is shown (onTabSelected): a hidden tab must not 
hold up the one the user looks at
     }
 
     @Override
@@ -488,9 +485,7 @@ class CveAuditTab extends AbstractTableTab {
             Map<String, Path> classpathJars = new HashMap<>();
             String pid = ctx.selectedPid;
             if (pid != null) {
-                JsonObject action = new JsonObject();
-                action.put("action", "jvm");
-                JsonObject response = ctx.executeAction(pid, action, 5000);
+                JsonObject response = ctx.jvmInfo(pid, 5000);
                 if (response != null) {
                     Object cp = response.get("classpath");
                     if (cp instanceof JsonArray arr) {
@@ -558,9 +553,7 @@ class CveAuditTab extends AbstractTableTab {
             if (pid == null) {
                 return Collections.emptyList();
             }
-            JsonObject action = new JsonObject();
-            action.put("action", "jvm");
-            JsonObject response = ctx.executeAction(pid, action, 5000);
+            JsonObject response = ctx.jvmInfo(pid, 5000);
             if (response == null) {
                 return Collections.emptyList();
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
index eb066ebab35c..47c792d58f94 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
@@ -479,10 +479,7 @@ class HeapHistogramTab extends AbstractTableTab {
     }
 
     private List<ClasspathTab.JarEntry> loadClasspathEntries(String pid) {
-        JsonObject action = new JsonObject();
-        action.put("action", "jvm");
-
-        JsonObject response = ctx.executeAction(pid, action, 5000);
+        JsonObject response = ctx.jvmInfo(pid, 5000);
 
         if (response == null) {
             return Collections.emptyList();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
index d834970d7ef8..fe6b7097846e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
@@ -122,10 +122,7 @@ class MavenDependenciesTab extends AbstractTableTab {
         transitiveLoaded = false;
         transitiveEntries = Collections.emptyList();
         loading.set(false);
-        if (ctx.selectedPid != null) {
-            lastPid = ctx.selectedPid;
-            loadDependencies();
-        }
+        // loaded when the tab is shown (onTabSelected): a hidden tab must not 
hold up the one the user looks at
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index f966d66ae5c1..46e566a6b693 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -184,6 +184,36 @@ class MonitorContext {
         }
     }
 
+    /** How long the jvm answer of an integration is kept for the tabs that 
need its classpath. */
+    static final long JVM_INFO_TTL_MS = 30_000;
+    private final ConcurrentHashMap<String, CachedAnswer> jvmInfo = new 
ConcurrentHashMap<>();
+
+    private record CachedAnswer(JsonObject answer, long time) {
+    }
+
+    /**
+     * The jvm answer of an integration (its classpath and JVM details), 
shared by the tabs that need it (Classpath, CVE
+     * Audit, Maven Dependencies, Heap Histogram): an action takes a second of 
the integration, so it is asked once, not
+     * once per tab.
+     */
+    JsonObject jvmInfo(String pid, long timeoutMs) {
+        Object lock = actionLocks.computeIfAbsent(pid, k -> new Object());
+        synchronized (lock) {
+            CachedAnswer cached = jvmInfo.get(pid);
+            long now = System.currentTimeMillis();
+            if (cached != null && now - cached.time() < JVM_INFO_TTL_MS) {
+                return cached.answer();
+            }
+            JsonObject request = new JsonObject();
+            request.put("action", "jvm");
+            JsonObject answer = executeAction(pid, request, timeoutMs);
+            if (answer != null) {
+                jvmInfo.put(pid, new CachedAnswer(answer, now));
+            }
+            return answer;
+        }
+    }
+
     JsonObject executeAction(String pid, JsonObject request, long timeoutMs) {
         Object lock = actionLocks.computeIfAbsent(pid, k -> new Object());
         synchronized (lock) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 3cfc2b5221f5..82963bfe0f75 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -379,6 +379,10 @@ class TabRegistry {
         for (MoreTab mt : moreTabs) {
             mt.tab().onIntegrationChanged();
         }
+        // the tabs load their data when shown: the one on screen loads now, 
for the new integration
+        if (activeMoreTab != null && tabsState.selected() == TAB_MORE) {
+            activeMoreTab.onTabSelected();
+        }
         dataService.otelSpans().set(List.of());
 
         filesBrowser.reset();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTabLazyLoadTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTabLazyLoadTest.java
new file mode 100644
index 000000000000..5efb1f081b58
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTabLazyLoadTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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 java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.tui.TuiRunner;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25438: the integration answers one action at a time, so a tab that is 
not shown must not ask it for data when
+ * the selected integration changes, and the tabs that need the classpath 
share one answer.
+ */
+// a TUI runner is created: not alongside the other tests that create one
+@Isolated
+class MonitorTabLazyLoadTest {
+
+    @Test
+    void hiddenTabsDoNotLoadWhenTheIntegrationChanges() throws Exception {
+        CountingContext ctx = new CountingContext();
+        try (TuiRunner runner = TuiBackendHelper.createTuiRunner(new 
NoopBackend())) {
+            ctx.runner = runner;
+            ctx.selectedPid = "1234";
+            List<MonitorTab> tabs = List.of(new ClasspathTab(ctx), new 
CveAuditTab(ctx), new MavenDependenciesTab(ctx),
+                    new CatalogTab(ctx));
+
+            tabs.forEach(MonitorTab::onIntegrationChanged);
+            Thread.sleep(500);
+            assertThat(ctx.actions).isEmpty();
+
+            // shown: the tab loads
+            tabs.get(0).onTabSelected();
+            long deadline = System.currentTimeMillis() + 5000;
+            while (ctx.actions.isEmpty() && System.currentTimeMillis() < 
deadline) {
+                Thread.sleep(20);
+            }
+            assertThat(ctx.actions).containsExactly("jvm");
+        }
+    }
+
+    @Test
+    void theTabsShareTheJvmAnswer() {
+        CountingContext ctx = new CountingContext();
+
+        JsonObject first = ctx.jvmInfo("1234", 5000);
+        JsonObject second = ctx.jvmInfo("1234", 5000);
+
+        assertThat(second).isSameAs(first);
+        assertThat(ctx.actions).containsExactly("jvm");
+        // another integration has its own
+        ctx.jvmInfo("5678", 5000);
+        assertThat(ctx.actions).containsExactly("jvm", "jvm");
+    }
+
+    private static final class CountingContext extends MonitorContext {
+
+        final List<String> actions = new CopyOnWriteArrayList<>();
+
+        CountingContext() {
+            super(new AtomicReference<>(List.of()), new 
AtomicReference<>(List.of()));
+        }
+
+        @Override
+        JsonObject executeAction(String pid, JsonObject request, long 
timeoutMs) {
+            actions.add(request.getString("action"));
+            JsonObject answer = new JsonObject();
+            answer.put("classpath", new JsonArray());
+            return answer;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/NoopBackend.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/NoopBackend.java
new file mode 100644
index 000000000000..de98fa454b6a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/NoopBackend.java
@@ -0,0 +1,104 @@
+/*
+ * 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.io.IOException;
+
+import dev.tamboui.buffer.DiffResult;
+import dev.tamboui.layout.Position;
+import dev.tamboui.layout.Size;
+import dev.tamboui.terminal.Backend;
+
+/**
+ * Minimal {@link Backend} stand-in. Only {@link #size()} needs a meaningful 
value, so that the test fails if the
+ * recording dimensions are taken from the delegate instead of the config.
+ */
+final class NoopBackend implements Backend {
+
+    @Override
+    public void draw(DiffResult diff) throws IOException {
+    }
+
+    @Override
+    public void flush() throws IOException {
+    }
+
+    @Override
+    public void clear() throws IOException {
+    }
+
+    @Override
+    public Size size() throws IOException {
+        return new Size(80, 24);
+    }
+
+    @Override
+    public void showCursor() throws IOException {
+    }
+
+    @Override
+    public void hideCursor() throws IOException {
+    }
+
+    @Override
+    public Position getCursorPosition() throws IOException {
+        return new Position(0, 0);
+    }
+
+    @Override
+    public void setCursorPosition(Position position) throws IOException {
+    }
+
+    @Override
+    public void enterAlternateScreen() throws IOException {
+    }
+
+    @Override
+    public void leaveAlternateScreen() throws IOException {
+    }
+
+    @Override
+    public void enableRawMode() throws IOException {
+    }
+
+    @Override
+    public void disableRawMode() throws IOException {
+    }
+
+    @Override
+    public void onResize(Runnable handler) {
+    }
+
+    @Override
+    public int read(int timeoutMs) throws IOException {
+        return -2;
+    }
+
+    @Override
+    public int peek(int timeoutMs) throws IOException {
+        return -2;
+    }
+
+    @Override
+    public void writeRaw(byte[] data) throws IOException {
+        // TuiRunner.create enables bracketed paste through this method
+    }
+
+    @Override
+    public void close() throws IOException {
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
index c95f2a274b38..0f446adf9ac7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
@@ -16,13 +16,10 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
-import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 
-import dev.tamboui.buffer.DiffResult;
 import dev.tamboui.internal.record.AnsiTerminalCapture;
-import dev.tamboui.layout.Position;
 import dev.tamboui.layout.Size;
 import dev.tamboui.terminal.Backend;
 import dev.tamboui.tui.TuiRunner;
@@ -92,84 +89,4 @@ class TuiBackendHelperRecordingTest {
             assertThat(result.size()).isEqualTo(new Size(120, 30));
         }
     }
-
-    /**
-     * Minimal {@link Backend} stand-in. Only {@link #size()} needs a 
meaningful value, so that the test fails if the
-     * recording dimensions are taken from the delegate instead of the config.
-     */
-    private static final class NoopBackend implements Backend {
-
-        @Override
-        public void draw(DiffResult diff) throws IOException {
-        }
-
-        @Override
-        public void flush() throws IOException {
-        }
-
-        @Override
-        public void clear() throws IOException {
-        }
-
-        @Override
-        public Size size() throws IOException {
-            return new Size(80, 24);
-        }
-
-        @Override
-        public void showCursor() throws IOException {
-        }
-
-        @Override
-        public void hideCursor() throws IOException {
-        }
-
-        @Override
-        public Position getCursorPosition() throws IOException {
-            return new Position(0, 0);
-        }
-
-        @Override
-        public void setCursorPosition(Position position) throws IOException {
-        }
-
-        @Override
-        public void enterAlternateScreen() throws IOException {
-        }
-
-        @Override
-        public void leaveAlternateScreen() throws IOException {
-        }
-
-        @Override
-        public void enableRawMode() throws IOException {
-        }
-
-        @Override
-        public void disableRawMode() throws IOException {
-        }
-
-        @Override
-        public void onResize(Runnable handler) {
-        }
-
-        @Override
-        public int read(int timeoutMs) throws IOException {
-            return -2;
-        }
-
-        @Override
-        public int peek(int timeoutMs) throws IOException {
-            return -2;
-        }
-
-        @Override
-        public void writeRaw(byte[] data) throws IOException {
-            // TuiRunner.create enables bracketed paste through this method
-        }
-
-        @Override
-        public void close() throws IOException {
-        }
-    }
 }

Reply via email to