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 b2da8ea93aa2ce249c8ebd3dacce3a7ba87c157a Author: Claus Ibsen <[email protected]> AuthorDate: Mon Jul 27 20:04:48 2026 +0200 CAMEL-24259: camel-tui - Add Data Type Transformers tab Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/cli/connector/LocalCliConnector.java | 14 ++ .../jbang/core/commands/tui/IntegrationInfo.java | 1 + .../dsl/jbang/core/commands/tui/StatusParser.java | 4 + .../dsl/jbang/core/commands/tui/TabRegistry.java | 8 +- .../jbang/core/commands/tui/TransformersTab.java | 275 +++++++++++++++++++++ .../dsl/jbang/core/commands/tui/TuiIcons.java | 1 + 6 files changed, 302 insertions(+), 1 deletion(-) 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 4da6c850f7b3..f25af3e88f69 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 @@ -394,6 +394,8 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C doActionSpringBootConfigurationTask(root); } else if ("type-converters".equals(action)) { doActionTypeConvertersTask(); + } else if ("transformers".equals(action)) { + doActionTransformersTask(); } } catch (Exception e) { LOG.warn("Error executing action: {} due to: {}. This exception is ignored.", action != null ? action : af, @@ -868,6 +870,18 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C } } + private void doActionTransformersTask() throws IOException { + DevConsole dc = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class) + .resolveById("transformers"); + if (dc != null) { + JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON); + LOG.trace("Updating output file: {}", outputFile); + IOHelper.writeText(json.toJson(), outputFile); + } else { + IOHelper.writeText("{}", outputFile); + } + } + private void doActionJvmTask() throws IOException { DevConsole dc = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class) .resolveById("jvm"); 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 622952024ac7..177dc100cb88 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 @@ -113,4 +113,5 @@ class IntegrationInfo { String readmeFiles; Set<String> devConsoles = Set.of(); boolean hasBrowseableEndpoints; + int transformerCount; } 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 37ce90e54c47..aa9883bf048d 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 @@ -712,6 +712,10 @@ final class StatusParser { if (root.containsKey("hasBrowseableEndpoints")) { info.hasBrowseableEndpoints = root.getBooleanOrDefault("hasBrowseableEndpoints", false); } + JsonObject trObj = (JsonObject) root.get("transformers"); + if (trObj != null) { + info.transformerCount = trObj.getIntegerOrDefault("size", 0); + } return info; } 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 19501cef973e..9bd91d6def4b 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 @@ -106,6 +106,7 @@ class TabRegistry { private InternalTasksTab internalTasksTab; private SqlTraceTab sqlTraceTab; private TypeConvertersTab typeConvertersTab; + private TransformersTab transformersTab; private MonitorTab activeMoreTab; private List<MoreTab> moreTabs; @@ -134,6 +135,7 @@ class TabRegistry { internalTasksTab = new InternalTasksTab(ctx); sqlTraceTab = new SqlTraceTab(ctx); typeConvertersTab = new TypeConvertersTab(ctx); + transformersTab = new TransformersTab(ctx); endpointsTab = new EndpointsTab(ctx, dataService.metrics()); networkTab = new NetworkTab(ctx, dataService.metrics()); httpTab = new HttpTab(ctx); @@ -238,7 +240,11 @@ class TabRegistry { "Project"), new MoreTab( TuiIcons.TAB_TYPE_CONVERTERS, "Type Converters", "T&ype Converters", - typeConvertersTab, "Project")); + typeConvertersTab, "Project"), + new MoreTab( + TuiIcons.TAB_TRANSFORMERS, "Transformers", "Trans&formers", + transformersTab, "Project", + List.of(), info -> info.transformerCount > 0)); } // ---- Tab access ---- diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java new file mode 100644 index 000000000000..4ab4369e7e89 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java @@ -0,0 +1,275 @@ +/* + * 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.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import dev.tamboui.layout.Constraint; +import dev.tamboui.layout.Rect; +import dev.tamboui.style.Style; +import dev.tamboui.terminal.Frame; +import dev.tamboui.text.Line; +import dev.tamboui.text.Span; +import dev.tamboui.text.Text; +import dev.tamboui.widgets.block.Block; +import dev.tamboui.widgets.block.BorderType; +import dev.tamboui.widgets.block.Borders; +import dev.tamboui.widgets.paragraph.Paragraph; +import dev.tamboui.widgets.table.Cell; +import dev.tamboui.widgets.table.Row; +import dev.tamboui.widgets.table.Table; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; + +import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*; + +class TransformersTab extends AbstractTableTab { + + private final AtomicBoolean loading = new AtomicBoolean(false); + + private List<TransformerData> allTransformers = Collections.emptyList(); + private String lastPid; + + TransformersTab(MonitorContext ctx) { + super(ctx, "name", "from", "to"); + } + + @Override + protected int getRowCount() { + return sortedTransformers().size(); + } + + @Override + public void onTabSelected() { + String pid = ctx.selectedPid; + if (pid != null && !pid.equals(lastPid)) { + lastPid = pid; + allTransformers = Collections.emptyList(); + } + if (allTransformers.isEmpty()) { + loadTransformers(); + } + } + + @Override + public void onIntegrationChanged() { + allTransformers = Collections.emptyList(); + lastPid = null; + } + + @Override + protected void renderContent(Frame frame, Rect area, IntegrationInfo info) { + if (loading.get() && allTransformers.isEmpty()) { + frame.renderWidget( + Paragraph.builder() + .text(Text.from(Line.from(Span.styled(" Loading transformers...", Style.EMPTY.dim())))) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .title(" Data Type Transformers ").build()) + .build(), + area); + return; + } + + List<TransformerData> visible = sortedTransformers(); + renderTable(frame, area, visible); + } + + private void renderTable(Frame frame, Rect area, List<TransformerData> visible) { + List<Row> rows = new ArrayList<>(); + for (TransformerData t : visible) { + rows.add(Row.from( + Cell.from(Span.styled(t.name != null ? t.name : "", Style.EMPTY.fg(Theme.accent()))), + Cell.from(Span.styled(t.from != null ? t.from : "*", Style.EMPTY)), + Cell.from(Span.styled(t.to != null ? t.to : "*", Style.EMPTY)))); + } + + if (rows.isEmpty()) { + rows.add(emptyRow("No data type transformers", 3)); + } + + String title = String.format(" Data Type Transformers [%d] ", visible.size()); + + Table table = Table.builder() + .rows(rows) + .header(Row.from( + Cell.from(Span.styled(sortLabel("NAME", "name"), sortStyle("name"))), + Cell.from(Span.styled(sortLabel("FROM", "from"), sortStyle("from"))), + Cell.from(Span.styled(sortLabel("TO", "to"), sortStyle("to"))))) + .widths( + Constraint.percentage(40), + Constraint.percentage(30), + Constraint.fill()) + .highlightStyle(Theme.selectionBg()) + .highlightSpacing(Table.HighlightSpacing.ALWAYS) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build()) + .build(); + + lastTableArea = area; + frame.renderStatefulWidget(table, area, tableState); + renderScrollbar(frame, visible.size()); + } + + @Override + public void renderFooter(List<Span> spans) { + hint(spans, "Esc", "back"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); + hintLast(spans, "s", "sort"); + } + + @Override + public SelectionContext getSelectionContext() { + List<TransformerData> visible = sortedTransformers(); + if (visible.isEmpty()) { + return null; + } + List<String> items = visible.stream().map(t -> t.name != null ? t.name : "?").toList(); + Integer sel = tableState.selected(); + return new SelectionContext("table", items, sel != null ? sel : -1, items.size(), "Data Type Transformers"); + } + + private List<TransformerData> sortedTransformers() { + List<TransformerData> result = new ArrayList<>(allTransformers); + result.sort((a, b) -> { + int cmp = switch (sort) { + case "from" -> compareStr(a.from, b.from); + case "to" -> compareStr(a.to, b.to); + default -> compareStr(a.name, b.name); + }; + return sortReversed ? -cmp : cmp; + }); + return result; + } + + private void loadTransformers() { + if (ctx.selectedPid == null || ctx.runner == null) { + return; + } + if (!loading.compareAndSet(false, true)) { + return; + } + String pid = ctx.selectedPid; + ctx.backgroundExecutor.execute(() -> { + try { + loadTransformersInBackground(pid); + } finally { + loading.set(false); + } + }); + } + + private void loadTransformersInBackground(String pid) { + JsonObject root = new JsonObject(); + root.put("action", "transformers"); + + JsonObject jo = ctx.executeAction(pid, root, 5000); + if (jo == null) { + return; + } + + JsonArray arr = jo.getCollection("transformers"); + if (arr == null) { + return; + } + + List<TransformerData> result = new ArrayList<>(); + for (int i = 0; i < arr.size(); i++) { + JsonObject tj = (JsonObject) arr.get(i); + TransformerData td = new TransformerData(); + td.name = tj.getString("name"); + td.from = tj.getString("from"); + td.to = tj.getString("to"); + result.add(td); + } + + if (ctx.runner != null) { + ctx.runner.runOnRenderThread(() -> { + allTransformers = result; + lastPid = pid; + }); + } + } + + static class TransformerData { + String name; + String from; + String to; + } + + @Override + public String description() { + return "Registered data type transformers in the Camel context"; + } + + @Override + public String getHelpText() { + return """ + # Data Type Transformers + + The Data Type Transformers tab shows all transformers registered in the + Camel transformer registry. Data type transformers are used to convert + message bodies between declared data types as part of Camel's contract + mechanism (inputType/outputType on routes). + + When a route declares an inputType or outputType, Camel automatically + looks up a matching transformer and applies it to convert the message + body between the source and target data types. + + ## Table Columns + + - **NAME** — Transformer name (e.g. `json:jackson`, `xml:jaxb`) + - **FROM** — Source data type (`*` means any input type) + - **TO** — Target data type (`*` means any output type) + + ## Keys + + - `Up/Down` — select transformer + - `s` — cycle sort column + - `S` — reverse sort order + - `Esc` — back + """; + } + + @Override + public JsonObject getTableDataAsJson() { + List<TransformerData> transformers = sortedTransformers(); + if (transformers.isEmpty()) { + return null; + } + JsonObject result = new JsonObject(); + result.put("tab", "Data Type Transformers"); + JsonArray rows = new JsonArray(); + for (TransformerData t : transformers) { + JsonObject row = new JsonObject(); + row.put("name", t.name); + if (t.from != null) { + row.put("from", t.from); + } + if (t.to != null) { + row.put("to", t.to); + } + rows.add(row); + } + result.put("rows", rows); + result.put("totalRows", transformers.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/TuiIcons.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java index 55e6e4f60db0..9858818d9a76 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java @@ -153,6 +153,7 @@ final class TuiIcons { static final String TAB_INTERNAL_TASKS = "🔩"; static final String TAB_ROUTE_CONTROLLER = "🚦"; static final String TAB_TYPE_CONVERTERS = "🔄"; + static final String TAB_TRANSFORMERS = "🔀"; /** Icons for {@link TabRegistry#TAB_OVERVIEW}..{@link TabRegistry#TAB_MORE} (in order). */ static final List<String> PRIMARY_TAB_ICONS = List.of(
