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

davsclaus pushed a commit to branch feature/CAMEL-23837-datasource-devconsole
in repository https://gitbox.apache.org/repos/asf/camel.git

commit e7beff6b7f952d1f29423a040bce34bb23ecb428
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jun 27 20:59:39 2026 +0200

    CAMEL-23837: Add DataSource connection pool dev-console and TUI tab
    
    Adds a new dev-console that discovers javax.sql.DataSource beans from the
    Camel registry and extracts connection pool metrics via reflection for
    HikariCP (Spring Boot) and Agroal (Quarkus). Includes a TUI DataSource
    tab showing pool name, active/idle/total connections, max pool size, and
    waiting threads with color-coded pool exhaustion warnings.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../console/DataSourceDevConsoleConfigurer.java    |  63 +++++
 .../org/apache/camel/dev-console/datasource.json   |  15 ++
 ....apache.camel.impl.console.DataSourceDevConsole |   2 +
 .../org/apache/camel/dev-console/datasource        |   2 +
 .../org/apache/camel/dev-consoles.properties       |   2 +-
 .../camel/impl/console/DataSourceDevConsole.java   | 225 ++++++++++++++++
 .../camel/cli/connector/LocalCliConnector.java     |   7 +
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  57 +++--
 .../jbang/core/commands/tui/DataSourceInfo.java    |  32 +++
 .../dsl/jbang/core/commands/tui/DataSourceTab.java | 283 +++++++++++++++++++++
 .../jbang/core/commands/tui/IntegrationInfo.java   |   1 +
 .../dsl/jbang/core/commands/tui/StatusParser.java  |  25 ++
 12 files changed, 689 insertions(+), 25 deletions(-)

diff --git 
a/core/camel-console/src/generated/java/org/apache/camel/impl/console/DataSourceDevConsoleConfigurer.java
 
b/core/camel-console/src/generated/java/org/apache/camel/impl/console/DataSourceDevConsoleConfigurer.java
new file mode 100644
index 000000000000..f84a33a843d1
--- /dev/null
+++ 
b/core/camel-console/src/generated/java/org/apache/camel/impl/console/DataSourceDevConsoleConfigurer.java
@@ -0,0 +1,63 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.impl.console;
+
+import javax.annotation.processing.Generated;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.impl.console.DataSourceDevConsole;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class DataSourceDevConsoleConfigurer extends 
org.apache.camel.support.component.PropertyConfigurerSupport implements 
GeneratedPropertyConfigurer, ExtendedPropertyConfigurerGetter {
+
+    private static final Map<String, Object> ALL_OPTIONS;
+    static {
+        Map<String, Object> map = new CaseInsensitiveMap();
+        map.put("CamelContext", org.apache.camel.CamelContext.class);
+        ALL_OPTIONS = map;
+    }
+
+    @Override
+    public boolean configure(CamelContext camelContext, Object obj, String 
name, Object value, boolean ignoreCase) {
+        org.apache.camel.impl.console.DataSourceDevConsole target = 
(org.apache.camel.impl.console.DataSourceDevConsole) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": target.setCamelContext(property(camelContext, 
org.apache.camel.CamelContext.class, value)); return true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public Map<String, Object> getAllOptions(Object target) {
+        return ALL_OPTIONS;
+    }
+
+    @Override
+    public Class<?> getOptionType(String name, boolean ignoreCase) {
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": return org.apache.camel.CamelContext.class;
+        default: return null;
+        }
+    }
+
+    @Override
+    public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+        org.apache.camel.impl.console.DataSourceDevConsole target = 
(org.apache.camel.impl.console.DataSourceDevConsole) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": return target.getCamelContext();
+        default: return null;
+        }
+    }
+}
+
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/datasource.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/datasource.json
new file mode 100644
index 000000000000..8889232ffa1a
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/datasource.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "datasource",
+    "title": "DataSource",
+    "description": "Displays DataSource connection pool metrics",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.DataSourceDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.21.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.DataSourceDevConsole
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.DataSourceDevConsole
new file mode 100644
index 000000000000..891a3bb086d7
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.DataSourceDevConsole
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.DataSourceDevConsoleConfigurer
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/datasource
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/datasource
new file mode 100644
index 000000000000..84c6e3b35cca
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/datasource
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.DataSourceDevConsole
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
index 84bbd4e88131..e25def6b5f32 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
@@ -1,5 +1,5 @@
 # Generated by camel build tools - do NOT edit this file!
-dev-consoles=bean blocked browse circuit-breaker consumer context debug 
endpoint errors eval-language event gc health inflight internal-tasks 
java-security jvm log memory message-history processor producer properties 
receive reload rest rest-spec route route-controller route-dump route-group 
route-structure route-topology send service simple-language source 
startup-recorder system-properties thread top trace transformers 
type-converters variables
+dev-consoles=bean blocked browse circuit-breaker consumer context datasource 
debug endpoint errors eval-language event gc health inflight internal-tasks 
java-security jvm log memory message-history processor producer properties 
receive reload rest rest-spec route route-controller route-dump route-group 
route-structure route-topology send service simple-language source 
startup-recorder system-properties thread top trace transformers 
type-converters variables
 groupId=org.apache.camel
 artifactId=camel-console
 version=4.21.0-SNAPSHOT
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/DataSourceDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/DataSourceDevConsole.java
new file mode 100644
index 000000000000..496add4835f7
--- /dev/null
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/DataSourceDevConsole.java
@@ -0,0 +1,225 @@
+/*
+ * 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.impl.console;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+@DevConsole(name = "datasource", displayName = "DataSource", description = 
"Displays DataSource connection pool metrics")
+@Configurer(extended = true)
+public class DataSourceDevConsole extends AbstractDevConsole {
+
+    private static final String HIKARI_CLASS = 
"com.zaxxer.hikari.HikariDataSource";
+    private static final String AGROAL_CLASS = 
"io.agroal.api.AgroalDataSource";
+
+    public DataSourceDevConsole() {
+        super("camel", "datasource", "DataSource", "Displays DataSource 
connection pool metrics");
+    }
+
+    @Override
+    protected String doCallText(Map<String, Object> options) {
+        StringBuilder sb = new StringBuilder();
+
+        Map<String, DataSource> dataSources
+                = 
getCamelContext().getRegistry().findByTypeWithName(DataSource.class);
+
+        if (dataSources.isEmpty()) {
+            sb.append("No DataSources found in registry\n");
+            return sb.toString();
+        }
+
+        for (Map.Entry<String, DataSource> entry : dataSources.entrySet()) {
+            DataSource ds = entry.getValue();
+            String poolType = detectPoolType(ds);
+
+            sb.append(String.format("DataSource: %s (%s)%n", entry.getKey(), 
poolType));
+            sb.append(String.format("  Type: %s%n", ds.getClass().getName()));
+
+            if ("HikariCP".equals(poolType)) {
+                appendHikariText(sb, ds);
+            } else if ("Agroal".equals(poolType)) {
+                appendAgroalText(sb, ds);
+            }
+            sb.append("\n");
+        }
+
+        return sb.toString();
+    }
+
+    @Override
+    protected JsonObject doCallJson(Map<String, Object> options) {
+        JsonObject root = new JsonObject();
+        JsonArray arr = new JsonArray();
+
+        Map<String, DataSource> dataSources
+                = 
getCamelContext().getRegistry().findByTypeWithName(DataSource.class);
+
+        for (Map.Entry<String, DataSource> entry : dataSources.entrySet()) {
+            DataSource ds = entry.getValue();
+            String poolType = detectPoolType(ds);
+
+            JsonObject jo = new JsonObject();
+            jo.put("name", entry.getKey());
+            jo.put("type", ds.getClass().getName());
+            jo.put("poolType", poolType);
+
+            if ("HikariCP".equals(poolType)) {
+                collectHikariMetrics(jo, ds);
+            } else if ("Agroal".equals(poolType)) {
+                collectAgroalMetrics(jo, ds);
+            }
+
+            arr.add(jo);
+        }
+
+        root.put("dataSources", arr);
+        return root;
+    }
+
+    private static String detectPoolType(DataSource ds) {
+        String className = ds.getClass().getName();
+        if (HIKARI_CLASS.equals(className)) {
+            return "HikariCP";
+        } else if (AGROAL_CLASS.equals(className)) {
+            return "Agroal";
+        }
+        return "Unknown";
+    }
+
+    // ---- HikariCP ----
+
+    private void collectHikariMetrics(JsonObject jo, DataSource ds) {
+        Object poolName = invokeMethod(ds, "getPoolName");
+        if (poolName != null) {
+            jo.put("poolName", String.valueOf(poolName));
+        }
+        Object maxPoolSize = invokeMethod(ds, "getMaximumPoolSize");
+        if (maxPoolSize instanceof Number n) {
+            jo.put("maxPoolSize", n.intValue());
+        }
+
+        Object mxBean = invokeMethod(ds, "getHikariPoolMXBean");
+        if (mxBean != null) {
+            putInt(jo, "active", invokeMethod(mxBean, "getActiveConnections"));
+            putInt(jo, "idle", invokeMethod(mxBean, "getIdleConnections"));
+            putInt(jo, "total", invokeMethod(mxBean, "getTotalConnections"));
+            putInt(jo, "waiting", invokeMethod(mxBean, 
"getThreadsAwaitingConnection"));
+        }
+    }
+
+    private void appendHikariText(StringBuilder sb, DataSource ds) {
+        Object poolName = invokeMethod(ds, "getPoolName");
+        if (poolName != null) {
+            sb.append(String.format("  Pool Name: %s%n", poolName));
+        }
+        Object maxPoolSize = invokeMethod(ds, "getMaximumPoolSize");
+        if (maxPoolSize != null) {
+            sb.append(String.format("  Max Pool Size: %s%n", maxPoolSize));
+        }
+
+        Object mxBean = invokeMethod(ds, "getHikariPoolMXBean");
+        if (mxBean != null) {
+            sb.append(String.format("  Active: %s%n", invokeMethod(mxBean, 
"getActiveConnections")));
+            sb.append(String.format("  Idle: %s%n", invokeMethod(mxBean, 
"getIdleConnections")));
+            sb.append(String.format("  Total: %s%n", invokeMethod(mxBean, 
"getTotalConnections")));
+            sb.append(String.format("  Waiting: %s%n", invokeMethod(mxBean, 
"getThreadsAwaitingConnection")));
+        } else {
+            sb.append("  Pool not yet initialized\n");
+        }
+    }
+
+    // ---- Agroal ----
+
+    private void collectAgroalMetrics(JsonObject jo, DataSource ds) {
+        Object metrics = invokeMethod(ds, "getMetrics");
+        if (metrics != null) {
+            putLong(jo, "active", invokeMethod(metrics, "activeCount"));
+            putLong(jo, "idle", invokeMethod(metrics, "availableCount"));
+            putLong(jo, "maxUsed", invokeMethod(metrics, "maxUsedCount"));
+            putLong(jo, "leakDetection", invokeMethod(metrics, 
"leakDetectionCount"));
+            putLong(jo, "created", invokeMethod(metrics, "creationCount"));
+        }
+
+        // max pool size via configuration chain
+        Object config = invokeMethod(ds, "getConfiguration");
+        if (config != null) {
+            Object poolConfig = invokeMethod(config, 
"connectionPoolConfiguration");
+            if (poolConfig != null) {
+                Object maxSize = invokeMethod(poolConfig, "maxSize");
+                if (maxSize instanceof Number n) {
+                    jo.put("maxPoolSize", n.intValue());
+                }
+            }
+        }
+
+        // compute total from active + idle
+        Object active = jo.get("active");
+        Object idle = jo.get("idle");
+        if (active instanceof Number a && idle instanceof Number i) {
+            jo.put("total", a.longValue() + i.longValue());
+        }
+    }
+
+    private void appendAgroalText(StringBuilder sb, DataSource ds) {
+        Object metrics = invokeMethod(ds, "getMetrics");
+        if (metrics != null) {
+            sb.append(String.format("  Active: %s%n", invokeMethod(metrics, 
"activeCount")));
+            sb.append(String.format("  Available: %s%n", invokeMethod(metrics, 
"availableCount")));
+            sb.append(String.format("  Max Used: %s%n", invokeMethod(metrics, 
"maxUsedCount")));
+            sb.append(String.format("  Leak Detection: %s%n", 
invokeMethod(metrics, "leakDetectionCount")));
+        }
+
+        Object config = invokeMethod(ds, "getConfiguration");
+        if (config != null) {
+            Object poolConfig = invokeMethod(config, 
"connectionPoolConfiguration");
+            if (poolConfig != null) {
+                sb.append(String.format("  Max Pool Size: %s%n", 
invokeMethod(poolConfig, "maxSize")));
+            }
+        }
+    }
+
+    // ---- Reflection helpers ----
+
+    private static Object invokeMethod(Object target, String methodName) {
+        try {
+            Method m = target.getClass().getMethod(methodName);
+            return m.invoke(target);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static void putInt(JsonObject jo, String key, Object value) {
+        if (value instanceof Number n) {
+            jo.put(key, n.intValue());
+        }
+    }
+
+    private static void putLong(JsonObject jo, String key, Object value) {
+        if (value instanceof Number n) {
+            jo.put(key, n.longValue());
+        }
+    }
+}
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index 98a2f5b5366d..21688dde8ca6 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -1443,6 +1443,13 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                         root.put("errors", summary);
                     }
                 }
+                DevConsole dc27 = dcr.resolveById("datasource");
+                if (dc27 != null) {
+                    JsonObject json = (JsonObject) 
dc27.call(DevConsole.MediaType.JSON);
+                    if (json != null && !json.isEmpty()) {
+                        root.put("dataSources", json);
+                    }
+                }
             }
             // various details
             JsonObject mem = collectMemory();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index adb40965dca7..1ea44c3e486d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -224,6 +224,7 @@ public class CamelMonitor extends CamelCommand {
     private SpansTab spansTab;
     private ProcessTab processTab;
     private OverviewTab overviewTab;
+    private DataSourceTab dataSourceTab;
 
     // "Switch integration" popup state
     private boolean showSwitchPopup;
@@ -277,6 +278,7 @@ public class CamelMonitor extends CamelCommand {
         diagramTab = new DiagramTab(ctx);
         routesTab = new RoutesTab(ctx);
         consumersTab = new ConsumersTab(ctx);
+        dataSourceTab = new DataSourceTab(ctx);
         endpointsTab = new EndpointsTab(ctx, metrics);
         httpTab = new HttpTab(ctx);
         healthTab = new HealthTab(ctx);
@@ -436,7 +438,7 @@ public class CamelMonitor extends CamelCommand {
                 return true;
             }
             if (ke.isDown()) {
-                morePopupState.selectNext(13);
+                morePopupState.selectNext(14);
                 return true;
             }
             int shortcutSel = morePopupShortcut(ke);
@@ -455,13 +457,14 @@ public class CamelMonitor extends CamelCommand {
                         case 3 -> classpathTab;
                         case 4 -> configurationTab;
                         case 5 -> consumersTab;
-                        case 6 -> inflightTab;
-                        case 7 -> memoryTab;
-                        case 8 -> metricsTab;
-                        case 9 -> spansTab;
-                        case 10 -> processTab;
-                        case 11 -> startupTab;
-                        case 12 -> threadsTab;
+                        case 6 -> dataSourceTab;
+                        case 7 -> inflightTab;
+                        case 8 -> memoryTab;
+                        case 9 -> metricsTab;
+                        case 10 -> spansTab;
+                        case 11 -> processTab;
+                        case 12 -> startupTab;
+                        case 13 -> threadsTab;
                         default -> null;
                     };
                     if (activeMoreTab != null) {
@@ -932,6 +935,7 @@ public class CamelMonitor extends CamelCommand {
         startupTab.onIntegrationChanged();
         configurationTab.onIntegrationChanged();
         consumersTab.onIntegrationChanged();
+        dataSourceTab.onIntegrationChanged();
         circuitBreakerTab.onIntegrationChanged();
         inflightTab.onIntegrationChanged();
         spansTab.onIntegrationChanged();
@@ -1234,7 +1238,7 @@ public class CamelMonitor extends CamelCommand {
 
     private void renderMorePopup(Frame frame, Rect area) {
         int popupW = 22;
-        int popupH = 15;
+        int popupH = 16;
         // Position just below the "0 More▾" tab label
         int dividerW = CharWidth.of(" | ");
         int tabBarX = 0;
@@ -1262,6 +1266,7 @@ public class CamelMonitor extends CamelCommand {
                 ListItem.from(Line.from(Span.raw("  Cl"), Span.styled("a", 
keyStyle), Span.raw("sspath"))),
                 ListItem.from(Line.from(Span.raw("  Confi"), Span.styled("g", 
keyStyle), Span.raw("uration"))),
                 ListItem.from(Line.from(Span.raw("  Co"), Span.styled("n", 
keyStyle), Span.raw("sumers"))),
+                ListItem.from(Line.from(Span.raw("  "), Span.styled("D", 
keyStyle), Span.raw("ataSource"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("I", 
keyStyle), Span.raw("nflight"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("M", 
keyStyle), Span.raw("emory"))),
                 ListItem.from(Line.from(Span.raw("  M"), Span.styled("e", 
keyStyle), Span.raw("trics"))),
@@ -1364,27 +1369,30 @@ public class CamelMonitor extends CamelCommand {
         if (ke.isChar('n')) {
             return 5;
         }
-        if (ke.isChar('i')) {
+        if (ke.isChar('d')) {
             return 6;
         }
-        if (ke.isChar('m')) {
+        if (ke.isChar('i')) {
             return 7;
         }
-        if (ke.isChar('e')) {
+        if (ke.isChar('m')) {
             return 8;
         }
-        if (ke.isChar('o')) {
+        if (ke.isChar('e')) {
             return 9;
         }
-        if (ke.isChar('p')) {
+        if (ke.isChar('o')) {
             return 10;
         }
-        if (ke.isChar('s')) {
+        if (ke.isChar('p')) {
             return 11;
         }
-        if (ke.isChar('t')) {
+        if (ke.isChar('s')) {
             return 12;
         }
+        if (ke.isChar('t')) {
+            return 13;
+        }
         return -1;
     }
 
@@ -2464,7 +2472,7 @@ public class CamelMonitor extends CamelCommand {
 
     private static final String[] MORE_TAB_NAMES = {
             "Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration",
-            "Consumers", "Inflight", "Memory", "Metrics", "Spans", "Process", 
"Startup", "Threads"
+            "Consumers", "DataSource", "Inflight", "Memory", "Metrics", 
"Spans", "Process", "Startup", "Threads"
     };
 
     String navigateToTab(String tabName) {
@@ -2486,13 +2494,14 @@ public class CamelMonitor extends CamelCommand {
                     case 3 -> classpathTab;
                     case 4 -> configurationTab;
                     case 5 -> consumersTab;
-                    case 6 -> inflightTab;
-                    case 7 -> memoryTab;
-                    case 8 -> metricsTab;
-                    case 9 -> spansTab;
-                    case 10 -> processTab;
-                    case 11 -> startupTab;
-                    case 12 -> threadsTab;
+                    case 6 -> dataSourceTab;
+                    case 7 -> inflightTab;
+                    case 8 -> memoryTab;
+                    case 9 -> metricsTab;
+                    case 10 -> spansTab;
+                    case 11 -> processTab;
+                    case 12 -> startupTab;
+                    case 13 -> threadsTab;
                     default -> null;
                 };
                 if (activeMoreTab != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceInfo.java
new file mode 100644
index 000000000000..83cbfd3be650
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceInfo.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+class DataSourceInfo {
+    String name;
+    String type;
+    String poolType;
+    String poolName;
+    int active;
+    int idle;
+    int total;
+    int waiting;
+    int maxPoolSize;
+    int maxUsed;
+    int leakDetection;
+    int created;
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
new file mode 100644
index 000000000000..79db403af498
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
@@ -0,0 +1,283 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import dev.tamboui.widgets.table.TableState;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
+class DataSourceTab implements MonitorTab {
+
+    private static final String[] SORT_COLUMNS = { "name", "pool", "active", 
"idle", "total", "max", "waiting" };
+
+    private final MonitorContext ctx;
+    private final TableState tableState = new TableState();
+    private String sort = "name";
+    private int sortIndex;
+    private boolean sortReversed;
+
+    DataSourceTab(MonitorContext ctx) {
+        this.ctx = ctx;
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isChar('s')) {
+            sortIndex = (sortIndex + 1) % SORT_COLUMNS.length;
+            sort = SORT_COLUMNS[sortIndex];
+            sortReversed = false;
+            return true;
+        }
+        if (ke.isChar('S')) {
+            sortReversed = !sortReversed;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean handleEscape() {
+        return false;
+    }
+
+    @Override
+    public void navigateUp() {
+    }
+
+    @Override
+    public void navigateDown() {
+    }
+
+    @Override
+    public void render(Frame frame, Rect area) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        List<DataSourceInfo> sorted = new ArrayList<>(info.dataSources);
+        sorted.sort(this::sortDataSource);
+
+        List<Row> rows = new ArrayList<>();
+        for (DataSourceInfo di : sorted) {
+            boolean exhausted = di.maxPoolSize > 0 && di.active >= 
di.maxPoolSize;
+            boolean waiters = di.waiting > 0;
+
+            Style activeStyle = exhausted
+                    ? Style.EMPTY.fg(Color.LIGHT_RED)
+                    : Style.EMPTY.fg(Color.CYAN);
+            Style waitingStyle = waiters
+                    ? Style.EMPTY.fg(Color.YELLOW)
+                    : Style.EMPTY;
+
+            String poolLabel = di.poolName != null ? di.poolName : 
(di.poolType != null ? di.poolType : "");
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(di.name != null ? di.name : "", 
Style.EMPTY.fg(Color.CYAN))),
+                    Cell.from(poolLabel),
+                    rightCell(String.valueOf(di.active), 8, activeStyle),
+                    rightCell(String.valueOf(di.idle), 8),
+                    rightCell(String.valueOf(di.total), 8),
+                    rightCell(di.maxPoolSize > 0 ? 
String.valueOf(di.maxPoolSize) : "", 8),
+                    rightCell(String.valueOf(di.waiting), 8, waitingStyle),
+                    Cell.from(Span.styled(shortType(di.type), 
Style.EMPTY.dim()))));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(Row.from(
+                    Cell.from(Span.styled("No DataSources", 
Style.EMPTY.dim())),
+                    Cell.from(""), Cell.from(""), Cell.from(""),
+                    Cell.from(""), Cell.from(""), Cell.from(""), 
Cell.from("")));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(sortLabel("NAME", "name"), 
sortStyle("name"))),
+                        Cell.from(Span.styled(sortLabel("POOL", "pool"), 
sortStyle("pool"))),
+                        rightCell(sortLabel("ACTIVE", "active"), 8, 
sortStyle("active")),
+                        rightCell(sortLabel("IDLE", "idle"), 8, 
sortStyle("idle")),
+                        rightCell(sortLabel("TOTAL", "total"), 8, 
sortStyle("total")),
+                        rightCell(sortLabel("MAX", "max"), 8, 
sortStyle("max")),
+                        rightCell(sortLabel("WAITING", "waiting"), 8, 
sortStyle("waiting")),
+                        Cell.from(Span.styled("TYPE", Style.EMPTY.bold()))))
+                .widths(
+                        Constraint.length(22),
+                        Constraint.length(16),
+                        Constraint.length(8),
+                        Constraint.length(8),
+                        Constraint.length(8),
+                        Constraint.length(8),
+                        Constraint.length(8),
+                        Constraint.fill())
+                .block(Block.builder().borderType(BorderType.ROUNDED)
+                        .title(" DataSource sort:" + sort + " ").build())
+                .build();
+
+        frame.renderStatefulWidget(table, area, tableState);
+    }
+
+    @Override
+    public void renderFooter(List<Span> spans) {
+        hint(spans, "Esc", "back");
+        hint(spans, "s", "sort");
+    }
+
+    private String sortLabel(String label, String column) {
+        return MonitorContext.sortLabel(label, column, sort, sortReversed);
+    }
+
+    private Style sortStyle(String column) {
+        return MonitorContext.sortStyle(column, sort);
+    }
+
+    private int sortDataSource(DataSourceInfo a, DataSourceInfo b) {
+        int result = switch (sort) {
+            case "pool" -> {
+                String pa = a.poolType != null ? a.poolType : "";
+                String pb = b.poolType != null ? b.poolType : "";
+                yield pa.compareToIgnoreCase(pb);
+            }
+            case "active" -> Integer.compare(b.active, a.active);
+            case "idle" -> Integer.compare(b.idle, a.idle);
+            case "total" -> Integer.compare(b.total, a.total);
+            case "max" -> Integer.compare(b.maxPoolSize, a.maxPoolSize);
+            case "waiting" -> Integer.compare(b.waiting, a.waiting);
+            default -> { // "name"
+                String na = a.name != null ? a.name : "";
+                String nb = b.name != null ? b.name : "";
+                yield na.compareToIgnoreCase(nb);
+            }
+        };
+        return sortReversed ? -result : result;
+    }
+
+    private static String shortType(String type) {
+        if (type == null) {
+            return "";
+        }
+        int dot = type.lastIndexOf('.');
+        return dot >= 0 ? type.substring(dot + 1) : type;
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.dataSources.isEmpty()) {
+            return null;
+        }
+        List<DataSourceInfo> sorted = new ArrayList<>(info.dataSources);
+        sorted.sort(this::sortDataSource);
+        List<String> items = sorted.stream().map(d -> d.name != null ? d.name 
: "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "DataSource");
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # DataSource
+
+                Displays connection pool metrics for all 
`javax.sql.DataSource` beans
+                registered in the Camel registry. Supports **HikariCP** 
(Spring Boot default)
+                and **Agroal** (Quarkus default) connection pools.
+
+                ## Table Columns
+
+                - **NAME** — The bean name of the DataSource in the registry
+                - **POOL** — The pool name (HikariCP pool name) or pool type 
(HikariCP / Agroal / Unknown)
+                - **ACTIVE** — Number of connections currently in use. Shown 
in red when equal to MAX (pool exhausted)
+                - **IDLE** — Number of idle connections available in the pool
+                - **TOTAL** — Total connections (active + idle) currently 
managed by the pool
+                - **MAX** — Maximum pool size configured. When ACTIVE reaches 
MAX, new connection requests will wait
+                - **WAITING** — Number of threads waiting for a connection. 
Shown in yellow when > 0
+                - **TYPE** — The Java class of the DataSource implementation
+
+                ## Pool Exhaustion
+
+                When **ACTIVE = MAX**, the pool is exhausted — no idle 
connections remain.
+                New requests must wait for a connection to be released. The 
ACTIVE column
+                turns red to signal this condition. If WAITING also increases, 
the application
+                may experience timeouts.
+
+                ## Connection Pools
+
+                ### HikariCP
+                The default connection pool for Spring Boot. Known for its 
speed and small
+                footprint. Default max pool size is 10.
+
+                ### Agroal
+                The default connection pool for Quarkus. Provides leak 
detection and
+                connection validation. Shows additional metrics like max-used 
and leak count.
+
+                ## Keys
+
+                - `Up/Down` — select datasource
+                - `s` — cycle sort column
+                - `S` — reverse sort order
+                """;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("tab", "DataSource");
+        JsonArray rows = new JsonArray();
+        for (DataSourceInfo di : info.dataSources) {
+            JsonObject row = new JsonObject();
+            row.put("name", di.name);
+            row.put("type", di.type);
+            row.put("poolType", di.poolType);
+            if (di.poolName != null) {
+                row.put("poolName", di.poolName);
+            }
+            row.put("active", di.active);
+            row.put("idle", di.idle);
+            row.put("total", di.total);
+            row.put("maxPoolSize", di.maxPoolSize);
+            row.put("waiting", di.waiting);
+            rows.add(row);
+        }
+        result.put("rows", rows);
+        result.put("totalRows", info.dataSources.size());
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
+        return result;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index e51122f1e4c2..a4009ab251c4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -82,6 +82,7 @@ class IntegrationInfo {
     final List<MicrometerMeterInfo> meters = new ArrayList<>();
     final List<HttpEndpointInfo> httpEndpoints = new ArrayList<>();
     final List<ConfigurationTab.ConfigProperty> configProperties = new 
ArrayList<>();
+    final List<DataSourceInfo> dataSources = new ArrayList<>();
     String httpServer;
     String readmeFiles;
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index 930a966f75ae..515c080554fb 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -497,6 +497,31 @@ final class StatusParser {
             }
         }
 
+        // Parse dataSources
+        JsonObject dsObj = (JsonObject) root.get("dataSources");
+        if (dsObj != null) {
+            JsonArray dsList = (JsonArray) dsObj.get("dataSources");
+            if (dsList != null) {
+                for (Object d : dsList) {
+                    JsonObject dj = (JsonObject) d;
+                    DataSourceInfo di = new DataSourceInfo();
+                    di.name = dj.getString("name");
+                    di.type = dj.getString("type");
+                    di.poolType = dj.getString("poolType");
+                    di.poolName = dj.getString("poolName");
+                    di.active = dj.getIntegerOrDefault("active", 0);
+                    di.idle = dj.getIntegerOrDefault("idle", 0);
+                    di.total = dj.getIntegerOrDefault("total", 0);
+                    di.waiting = dj.getIntegerOrDefault("waiting", 0);
+                    di.maxPoolSize = dj.getIntegerOrDefault("maxPoolSize", 0);
+                    di.maxUsed = dj.getIntegerOrDefault("maxUsed", 0);
+                    di.leakDetection = dj.getIntegerOrDefault("leakDetection", 
0);
+                    di.created = dj.getIntegerOrDefault("created", 0);
+                    info.dataSources.add(di);
+                }
+            }
+        }
+
         return info;
     }
 


Reply via email to