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
commit ce1a91857c3e0a30e96e9fce106b2057e7768c6a Author: Claus Ibsen <[email protected]> AuthorDate: Thu Oct 15 13:18:08 2026 +0200 chore: camel-jbang - camel get endpoint and the Endpoint tab show the scope of an endpoint Besides remote, a component can now be marked internal (CAMEL-25652): Camel plumbing such as direct, seda, log, timer and mock that stays inside the integration. That makes three kinds of endpoint, which exclude each other: remote (an external system), internal, and local (neither, such as exec, bean or class, which can run code or a process on this host). camel get endpoint and the Endpoint tab show them in a SCOPE column instead of REMOTE; the JSON of camel get endpoint keeps remote and adds internal, and tui_get_table adds scope. Remote comes from the runtime, internal from the catalog by the scheme of the URI, through the new EndpointScope, which the tool confirms of camel ask and the TUI now use as well. A component the catalog does not know is never internal. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 8 ++ .../dsl/jbang/core/commands/ai/ToolConfirms.java | 27 ++----- .../jbang/core/commands/process/ListEndpoint.java | 11 ++- .../camel/dsl/jbang/core/common/EndpointScope.java | 85 ++++++++++++++++++++++ .../core/commands/process/ListEndpointTest.java | 66 +++++++++++++++++ .../dsl/jbang/core/common/EndpointScopeTest.java | 53 ++++++++++++++ .../dsl/jbang/core/commands/tui/EndpointInfo.java | 2 + .../dsl/jbang/core/commands/tui/EndpointsTab.java | 19 ++++- .../dsl/jbang/core/commands/tui/StatusParser.java | 3 + .../src/main/resources/tui/help/endpoints.md | 12 +-- .../core/commands/tui/EndpointsTabRenderTest.java | 26 +++++++ .../jbang/core/commands/tui/StatusParserTest.java | 23 ++++++ 12 files changed, 302 insertions(+), 33 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index 147092569b76..5d1d302fc766 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -159,6 +159,14 @@ local model gets a core set of tools (`--tools=auto`, the default), a hosted mod of `--max-iterations` is now 25 instead of 10, and `--model llama3.2` now selects that model also when a model better at tool calling, such as `qwen3.6`, is installed. +=== camel-jbang - camel get endpoint shows the scope of an endpoint + +`camel get endpoint` replaces the `REMOTE` column with `SCOPE`, which tells three kinds of endpoint apart: +`remote` (an external system, as before), `internal` (Camel plumbing that stays inside the integration, such as +`direct`, `seda` or `log`, as the catalog marks its component internal) and `local` (neither, such as `exec` or +`bean`). Scripts that read the `REMOTE` column of the table must read `SCOPE` instead. The `--json` output keeps +`remote` and adds `internal`. + === camel-jpa, camel-hazelcast - an exchange marked rollback only is not consumed When a route marks the exchange rollback only (for example with `markRollbackOnly()` or `markRollbackOnlyLast()`) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java index 2d56e29b9de6..019b506a08c2 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java @@ -30,9 +30,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import org.apache.camel.catalog.CamelCatalog; -import org.apache.camel.catalog.DefaultCamelCatalog; -import org.apache.camel.tooling.model.ComponentModel; +import org.apache.camel.dsl.jbang.core.common.EndpointScope; import org.apache.camel.util.URISupport; /** @@ -66,27 +64,12 @@ public final class ToolConfirms { } /** - * Whether a component stays inside the integration, as the catalog marks it internal: its routes, in-memory state - * and log, and no file, network, process, or code or resource named in its URI, by any option. This is narrower - * than remote=false, which exec, class, bean, language and the components that load a template or a stylesheet by - * URI are too. The catalog is the CLI's own, so it has the flag; a component it does not know (a custom one, or one - * newer than the CLI) is not internal. + * Whether a component stays inside the integration, as the catalog marks it internal ({@link EndpointScope}): a + * message to it needs no confirm. This is narrower than remote=false, which exec, class, bean, language and the + * components that load a template or a stylesheet by URI are too. */ public static boolean isInternal(String scheme) { - if (!scheme.matches("[a-z0-9-]+")) { - return false; - } - try { - ComponentModel model = Catalog.INSTANCE.componentModel(scheme); - return model != null && model.isInternal(); - } catch (RuntimeException e) { - return false; - } - } - - // loaded on the first send, not when a call only needs the SQL or language checks - private static final class Catalog { - static final CamelCatalog INSTANCE = new DefaultCamelCatalog(true); + return EndpointScope.isInternal(scheme); } /** diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java index b50d83401420..ebf926f93dec 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java @@ -28,6 +28,7 @@ import com.github.freva.asciitable.Column; import com.github.freva.asciitable.HorizontalAlign; import com.github.freva.asciitable.OverflowBehaviour; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.dsl.jbang.core.common.EndpointScope; import org.apache.camel.dsl.jbang.core.common.ProcessHelper; import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper; import org.apache.camel.support.PatternHelper; @@ -127,6 +128,7 @@ public class ListEndpoint extends ProcessWatchCommand { row.endpoint = o.getString("uri"); row.stub = o.getBooleanOrDefault("stub", false); row.remote = o.getBooleanOrDefault("remote", true); + row.scope = EndpointScope.of(row.endpoint, row.remote); row.direction = o.getString("direction"); row.total = o.getString("hits"); row.meanBodySize = o.getLongOrDefault("meanBodySize", -1L); @@ -190,6 +192,7 @@ public class ListEndpoint extends ProcessWatchCommand { jo.put("total", r.total); jo.put("stub", r.stub); jo.put("remote", r.remote); + jo.put("internal", EndpointScope.INTERNAL.equals(r.scope)); jo.put("uri", r.endpoint); if (r.meanBodySize >= 0) { jo.put("minBodySize", r.minBodySize); @@ -207,8 +210,8 @@ public class ListEndpoint extends ProcessWatchCommand { } boolean hasSize = rows.stream().anyMatch(r -> r.meanBodySize >= 0 || r.meanHeadersSize >= 0); // Flexible column: URI (90/140) - // Fixed columns: PID(8)+NAME(30)+AGE(8)+DIR(3)+TOTAL(5)+STUB(4)+REMOTE(6) ~= 64 - int fixedW = 64; + // Fixed columns: PID(8)+NAME(30)+AGE(8)+DIR(3)+TOTAL(5)+STUB(4)+SCOPE(8) ~= 66 + int fixedW = 66; if (hasSize) { // BODY-SIZE(9)+HDR-SIZE(8) = 17 fixedW += 17; @@ -229,7 +232,7 @@ public class ListEndpoint extends ProcessWatchCommand { new Column().header("DIR").with(r -> r.direction), new Column().header("TOTAL").with(r -> r.total), new Column().header("STUB").dataAlign(HorizontalAlign.CENTER).with(r -> r.stub ? "x" : ""), - new Column().header("REMOTE").dataAlign(HorizontalAlign.CENTER).with(r -> r.remote ? "x" : ""), + new Column().header("SCOPE").dataAlign(HorizontalAlign.LEFT).with(r -> r.scope), new Column().header("BODY-SIZE").visible(hasSize).dataAlign(HorizontalAlign.RIGHT) .with(r -> sizeToString(r.meanBodySize)), new Column().header("HDR-SIZE").visible(hasSize).dataAlign(HorizontalAlign.RIGHT) @@ -307,6 +310,8 @@ public class ListEndpoint extends ProcessWatchCommand { String total; boolean stub; boolean remote; + /** remote, internal or local ({@link EndpointScope}) */ + String scope; long meanBodySize = -1; long meanHeadersSize = -1; long minBodySize = -1; diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EndpointScope.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EndpointScope.java new file mode 100644 index 000000000000..60479ced1ad8 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EndpointScope.java @@ -0,0 +1,85 @@ +/* + * 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.common; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.tooling.model.ComponentModel; + +/** + * How far an endpoint reaches, as {@code camel get endpoint} and the Endpoint tab show it: {@link #REMOTE}, an external + * system; {@link #INTERNAL}, Camel plumbing that stays inside the integration; or {@link #LOCAL}, neither. The three + * exclude each other, as an internal component is never remote. + */ +public final class EndpointScope { + + /** An external system, over the network or a file system: the endpoint says so at runtime. */ + public static final String REMOTE = "remote"; + /** Stays inside the integration (direct, seda, log, mock): the catalog marks its component internal. */ + public static final String INTERNAL = "internal"; + /** Neither: the component can run code or a process, or read a file or a resource, on this host. */ + public static final String LOCAL = "local"; + + private EndpointScope() { + } + + /** + * The scope of an endpoint. + * + * @param uri the endpoint URI + * @param remote whether the endpoint is remote, as the runtime reports it + */ + public static String of(String uri, boolean remote) { + if (remote) { + return REMOTE; + } + return isInternal(scheme(uri)) ? INTERNAL : LOCAL; + } + + /** The component of an endpoint URI: what comes before its first colon. */ + public static String scheme(String uri) { + if (uri == null) { + return null; + } + int colon = uri.indexOf(':'); + return colon > 0 ? uri.substring(0, colon) : uri; + } + + /** + * Whether a component stays inside the integration, as the catalog marks it internal: its routes, in-memory state + * and log, and no file, network, process, or code or resource named in its URI, by any option. This is narrower + * than remote=false, which exec, class, bean, language and the components that load a template or a stylesheet by + * URI are too. The catalog is the CLI's own, so it has the flag; a component it does not know (a custom one, or one + * newer than the CLI) is not internal. + */ + public static boolean isInternal(String scheme) { + if (scheme == null || !scheme.matches("[a-z0-9-]+")) { + return false; + } + try { + ComponentModel model = Catalog.INSTANCE.componentModel(scheme); + return model != null && model.isInternal(); + } catch (RuntimeException e) { + return false; + } + } + + // loaded when a scope is first asked for, not when the CLI starts + private static final class Catalog { + static final CamelCatalog INSTANCE = new DefaultCamelCatalog(true); + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpointTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpointTest.java index e2605a1340f3..c5d57b255db7 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpointTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpointTest.java @@ -22,6 +22,7 @@ import java.util.stream.Stream; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsoner; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.MockedStatic; @@ -132,6 +133,71 @@ class ListEndpointTest extends ProcessCommandTestSupport { } } + @Test + void testScopeTellsRemoteInternalAndLocalApart() throws Exception { + JsonObject root = buildEndpointStatus("myApp", + endpoint("kafka:orders", "in", "3", true), + endpoint("log:payment", "out", "3", false), + endpoint("exec:wc", "out", "1", false)); + writeStatusFile(TEST_PID, root); + + ListEndpoint command = new ListEndpoint(new CamelJBangMain().withPrinter(printer)); + command.sort = "pid"; + + try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) { + ProcessHandle ph = mockProcessHandle(TEST_PID); + ProcessHandle currentHandle = mockCurrentHandle(); + mocked.when(ProcessHandle::current).thenReturn(currentHandle); + mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph)); + + assertEquals(0, command.doCall()); + String output = printer.getOutput(); + assertTrue(output.contains("SCOPE"), output); + assertFalse(output.contains("REMOTE"), output); + assertTrue(lineOf(output, "kafka:orders").contains("remote"), output); + assertTrue(lineOf(output, "log:payment").contains("internal"), output); + assertTrue(lineOf(output, "exec:wc").contains("local"), output); + } + } + + @Test + void testJsonOutputKeepsRemoteAndAddsInternal() throws Exception { + JsonObject root = buildEndpointStatus("myApp", + endpoint("log:payment", "out", "3", false), + endpoint("exec:wc", "out", "1", false)); + writeStatusFile(TEST_PID, root); + + ListEndpoint command = new ListEndpoint(new CamelJBangMain().withPrinter(printer)); + command.sort = "pid"; + command.jsonOutput = true; + + try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) { + ProcessHandle ph = mockProcessHandle(TEST_PID); + ProcessHandle currentHandle = mockCurrentHandle(); + mocked.when(ProcessHandle::current).thenReturn(currentHandle); + mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph)); + + assertEquals(0, command.doCall()); + JsonArray rows = (JsonArray) Jsoner.deserialize(printer.getOutput().trim()); + JsonObject log = (JsonObject) rows.get(0); + JsonObject exec = (JsonObject) rows.get(1); + assertEquals("log:payment", log.getString("uri")); + assertEquals(false, log.get("remote")); + assertEquals(true, log.get("internal")); + assertEquals(false, exec.get("internal")); + } + } + + private static String lineOf(String output, String uri) { + return output.lines().filter(l -> l.contains(uri)).findFirst().orElse(""); + } + + private static JsonObject endpoint(String uri, String direction, String hits, boolean remote) { + JsonObject ep = endpoint(uri, direction, hits); + ep.put("remote", remote); + return ep; + } + private static JsonObject buildEndpointStatus(String contextName, JsonObject... endpoints) { JsonArray epArr = new JsonArray(); Collections.addAll(epArr, endpoints); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/EndpointScopeTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/EndpointScopeTest.java new file mode 100644 index 000000000000..636a132548f7 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/EndpointScopeTest.java @@ -0,0 +1,53 @@ +/* + * 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.common; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class EndpointScopeTest { + + @Test + void remoteAsTheRuntimeSaysElseInternalOrLocalByTheCatalog() { + assertThat(EndpointScope.of("kafka://orders", true)).isEqualTo(EndpointScope.REMOTE); + assertThat(EndpointScope.of("file://orders?noop=true", true)).isEqualTo(EndpointScope.REMOTE); + // Camel plumbing that stays inside the integration + assertThat(EndpointScope.of("direct://charge", false)).isEqualTo(EndpointScope.INTERNAL); + assertThat(EndpointScope.of("log:payment", false)).isEqualTo(EndpointScope.INTERNAL); + assertThat(EndpointScope.of("timer://tick?period=1000", false)).isEqualTo(EndpointScope.INTERNAL); + // not remote, yet it runs a process or code on this host + assertThat(EndpointScope.of("exec://wc", false)).isEqualTo(EndpointScope.LOCAL); + assertThat(EndpointScope.of("bean:greeter", false)).isEqualTo(EndpointScope.LOCAL); + } + + @Test + void aComponentTheCatalogDoesNotKnowIsNeverInternal() { + assertThat(EndpointScope.of("my-custom://thing", false)).isEqualTo(EndpointScope.LOCAL); + assertThat(EndpointScope.of(null, false)).isEqualTo(EndpointScope.LOCAL); + assertThat(EndpointScope.isInternal("Direct")).isFalse(); + assertThat(EndpointScope.isInternal("../direct")).isFalse(); + } + + @Test + void theSchemeIsWhatComesBeforeTheFirstColon() { + assertThat(EndpointScope.scheme("seda://queue?size=10")).isEqualTo("seda"); + assertThat(EndpointScope.scheme("log:info")).isEqualTo("log"); + assertThat(EndpointScope.scheme("direct")).isEqualTo("direct"); + assertThat(EndpointScope.scheme(null)).isNull(); + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java index 21bef41c3e1c..214e2b6a6034 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java @@ -24,6 +24,8 @@ class EndpointInfo { long hits; boolean stub; boolean remote; + /** remote, internal or local, as EndpointScope tells them apart */ + String scope; long minBodySize = -1; long maxBodySize = -1; long meanBodySize = -1; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java index 4b64ba726e07..c2a5483c2a0a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java @@ -44,6 +44,7 @@ import dev.tamboui.widgets.table.Cell; import dev.tamboui.widgets.table.Row; import dev.tamboui.widgets.table.Table; import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.dsl.jbang.core.common.EndpointScope; import org.apache.camel.tooling.model.BaseOptionModel; import org.apache.camel.tooling.model.ComponentModel; import org.apache.camel.util.json.JsonArray; @@ -255,7 +256,7 @@ class EndpointsTab extends AbstractTableTab { cells.add(rightCell(FlowHelper.sizeToString(ep.meanHeadersSize), 10)); } cells.add(centerCell(ep.stub ? TuiIcons.CHECK : "", 6)); - cells.add(centerCell(ep.remote ? TuiIcons.CHECK : "", 8)); + cells.add(scopeCell(ep.scope)); cells.add(Cell.from(ep.uri != null ? TuiHelper.displayUri(ep.uri) : "")); rows.add(Row.from(cells)); } @@ -275,7 +276,7 @@ class EndpointsTab extends AbstractTableTab { headerCells.add(rightCell(sortLabel("HDR", "hdr"), 10, sortStyle("hdr"))); } headerCells.add(centerCell("STUB", 6, Style.EMPTY.bold())); - headerCells.add(centerCell("REMOTE", 8, Style.EMPTY.bold())); + headerCells.add(Cell.from(Span.styled("SCOPE", Style.EMPTY.bold()))); headerCells.add(Cell.from(Span.styled(sortLabel("URI", "uri"), sortStyle("uri")))); List<Constraint> widths = new ArrayList<>(); @@ -747,6 +748,19 @@ class EndpointsTab extends AbstractTableTab { return DocHelper.loadHelpText("endpoints"); } + /** The scope of an endpoint: remote in the colour the diagrams draw the edges of the integration in. */ + private static Cell scopeCell(String scope) { + if (scope == null) { + return Cell.from(""); + } + Style style = switch (scope) { + case EndpointScope.REMOTE -> Style.EMPTY.fg(Theme.diagramExternal()); + case EndpointScope.INTERNAL -> Theme.muted(); + default -> Style.EMPTY; + }; + return Cell.from(Span.styled(scope, style)); + } + @Override public JsonObject getTableDataAsJson() { IntegrationInfo info = ctx.findSelectedIntegration(); @@ -764,6 +778,7 @@ class EndpointsTab extends AbstractTableTab { row.put("routeId", ei.routeId); row.put("hits", ei.hits); row.put("remote", ei.remote); + row.put("scope", ei.scope); row.put("stub", ei.stub); if (ei.meanBodySize >= 0) { row.put("meanBodySize", ei.meanBodySize); 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 777a76008d72..bd714eeee0fd 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 @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import org.apache.camel.dsl.jbang.core.common.EndpointScope; import org.apache.camel.dsl.jbang.core.common.ProcessHelper; import org.apache.camel.dsl.jbang.core.common.VersionHelper; import org.apache.camel.util.TimeUtils; @@ -562,6 +563,8 @@ final class StatusParser { int idx = ep.uri.indexOf(':'); ep.component = idx > 0 ? ep.uri.substring(0, idx) : ep.uri; } + // here on the refresh thread, as the first look at the catalog loads it + ep.scope = EndpointScope.of(ep.uri, ep.remote); info.endpoints.add(ep); } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/endpoints.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/endpoints.md index 35ea3cffc357..68d24c3f8440 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/endpoints.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/endpoints.md @@ -14,17 +14,17 @@ identifies both the component and the specific resource. - **BODY** — Average message body size in bytes passing through this endpoint. Helps identify which endpoints handle large payloads - **HDR** — Average message headers size in bytes. Large headers may indicate excessive metadata being passed - **STUB** — Marked `x` if the endpoint has been replaced by a stub component (useful during testing to avoid connecting to real external systems) -- **REMOTE** — Marked `x` if the endpoint connects to an external system outside the JVM (e.g., `kafka`, `http`, `ftp`). Local endpoints like `seda`, `direct`, `log` are not remote +- **SCOPE** — How far the endpoint reaches: `remote`, an external system outside the JVM (e.g., `kafka`, `http`, `ftp`, `file`); `internal`, Camel plumbing that stays inside the integration (`direct`, `seda`, `log`, `timer`, `mock`); or `local`, neither: it can run code or a process, or read a file or a resource, on this host (`exec`, `bean`, `class`, `language`, the template components) - **URI** — Full endpoint URI with parameters (e.g., `timer://hello?period=2000`) ## Example Screen ``` - COMPONENT ROUTE DIR TOTAL BODY HDR STUB REMOTE URI - timer timer-to-log in 21 0 0 timer://hello?period=2000 - timer timer-to-seda in 14 0 0 timer://pump?period=3000 - seda seda-consumer in 14 14 0 seda://queue - seda timer-to-seda out 14 14 0 seda://queue + COMPONENT ROUTE DIR TOTAL BODY HDR STUB SCOPE URI + timer timer-to-log in 21 0 0 internal timer://hello?period=2000 + timer timer-to-seda in 14 0 0 internal timer://pump?period=3000 + seda seda-consumer in 14 14 0 internal seda://queue + seda timer-to-seda out 14 14 0 internal seda://queue ``` Notice that `seda://queue` appears twice — once as `in` (the diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java index 5c99b27db1a6..5355400b4c7c 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java @@ -27,9 +27,12 @@ import dev.tamboui.terminal.Frame; import dev.tamboui.text.Span; import dev.tamboui.tui.event.KeyEvent; import dev.tamboui.tui.event.KeyModifiers; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -204,6 +207,29 @@ class EndpointsTabRenderTest { assertTrue(bar.views().stream().anyMatch(v -> v.label().equals("Detail")), "View bar should list Detail"); } + @Test + void theScopeColumnTellsRemoteInternalAndLocalApart() { + addEndpoint("kafka", "kafka://orders", "in", "intake", 3).scope = "remote"; + addEndpoint("direct", "direct://charge", "out", "intake", 3).scope = "internal"; + addEndpoint("exec", "exec://wc", "out", "report", 1).scope = "local"; + + EndpointsTab tab = new EndpointsTab(ctx, new MetricsCollector()); + String rendered = TuiTestHelper.renderToString(tab, 140, 30); + + assertThat(rendered).contains("SCOPE").doesNotContain("REMOTE"); + assertThat(line(rendered, "kafka://orders")).contains("remote"); + assertThat(line(rendered, "direct://charge")).contains("internal"); + assertThat(line(rendered, "exec://wc")).contains("local"); + // and for the tui_get_table tool + JsonArray rows = (JsonArray) tab.getTableDataAsJson().get("rows"); + assertThat(rows).extracting(r -> ((JsonObject) r).getString("scope")) + .containsExactlyInAnyOrder("remote", "internal", "local"); + } + + private static String line(String rendered, String uri) { + return rendered.lines().filter(l -> l.contains(uri)).findFirst().orElse(""); + } + // ---- Helper methods ---- private EndpointInfo addEndpoint(String component, String uri, String direction, String routeId, long hits) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java index d2b290c910e5..fd1177b48885 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java @@ -17,6 +17,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.apache.camel.util.json.JsonArray; @@ -52,6 +53,28 @@ class StatusParserTest { assertEquals(5, info.state); } + @Test + void anEndpointGetsItsScopeFromTheRemoteFlagAndTheCatalog() { + JsonArray endpoints = new JsonArray(); + for (String[] e : new String[][] { + { "kafka://orders", "true" }, { "log://payment", "false" }, + { "exec://wc", "false" } }) { + JsonObject ep = new JsonObject(); + ep.put("uri", e[0]); + ep.put("remote", Boolean.parseBoolean(e[1])); + endpoints.add(ep); + } + JsonObject holder = new JsonObject(); + holder.put("endpoints", endpoints); + JsonObject root = new JsonObject(); + root.put("context", new JsonObject()); + root.put("endpoints", holder); + + IntegrationInfo info = StatusParser.parseIntegration(ProcessHandle.current(), root); + + assertEquals(List.of("remote", "internal", "local"), info.endpoints.stream().map(ep -> ep.scope).toList()); + } + @Test void aFailedReloadIsReadAsCamelPsReadsIt() { JsonObject root = new JsonObject();
