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 c474411cf4ec CAMEL-23648: camel-jbang - TUI add waterfall view and
history tab fixes (#23649)
c474411cf4ec is described below
commit c474411cf4ec2236b8d46790cc34634051d19f39
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat May 30 08:18:34 2026 +0200
CAMEL-23648: camel-jbang - TUI add waterfall view and history tab fixes
(#23649)
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 62 +++--
.../dsl/jbang/core/commands/tui/HistoryTab.java | 252 +++++++++++++++++++--
.../dsl/jbang/core/commands/tui/StartupTab.java | 16 +-
.../dsl/jbang/core/commands/tui/TuiHelper.java | 18 ++
4 files changed, 287 insertions(+), 61 deletions(-)
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 b51bb8b27a89..e70e6364e3fd 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
@@ -835,6 +835,17 @@ public class CamelMonitor extends CamelCommand {
}
}
+ private List<Long> selectedPidAsList() {
+ if (ctx.selectedPid == null) {
+ return Collections.emptyList();
+ }
+ try {
+ return List.of(Long.parseLong(ctx.selectedPid));
+ } catch (NumberFormatException e) {
+ return Collections.emptyList();
+ }
+ }
+
private void syncSelectedPid() {
List<IntegrationInfo> infos = sortedOverviewInfos();
List<InfraInfo> infras = infraData.get();
@@ -2273,18 +2284,21 @@ public class CamelMonitor extends CamelCommand {
}
}
+ // Scope history/error/trace refresh to the selected integration
only
+ List<Long> selectedPids = selectedPidAsList();
+
// Refresh error data only when the Errors tab is visible
- if (tabsState.selected() == TAB_ERRORS) {
- refreshErrorData(pids);
+ if (tabsState.selected() == TAB_ERRORS && !selectedPids.isEmpty())
{
+ refreshErrorData(selectedPids);
}
// Refresh trace data only when the History tab is visible
- if (tabsState.selected() == TAB_HISTORY) {
+ if (tabsState.selected() == TAB_HISTORY &&
!selectedPids.isEmpty()) {
if (historyTab.historyRefreshRequested) {
historyTab.historyRefreshRequested = false;
- refreshHistoryData(pids);
+ refreshHistoryData(selectedPids);
}
- refreshTraceData(pids);
+ refreshTraceData(selectedPids);
}
} catch (Exception e) {
// ignore refresh errors
@@ -2670,21 +2684,6 @@ public class CamelMonitor extends CamelCommand {
entry.last = json.getBooleanOrDefault("last", false);
entry.nodeLevel = json.getIntegerOrDefault("nodeLevel", 0);
- // timestamp is epoch millis (number)
- Object tsObj = json.get("timestamp");
- if (tsObj instanceof Number n) {
- long epochMs = n.longValue();
- entry.epochMs = epochMs;
- entry.timestamp = Instant.ofEpochMilli(epochMs)
- .atZone(ZoneId.systemDefault())
- .toLocalTime().toString();
- if (entry.timestamp.length() > 12) {
- entry.timestamp = entry.timestamp.substring(0, 12);
- }
- } else if (tsObj != null) {
- entry.timestamp = tsObj.toString();
- }
-
// Derive status from done/failed booleans
boolean done = Boolean.TRUE.equals(json.get("done"));
boolean failed = Boolean.TRUE.equals(json.get("failed"));
@@ -2708,6 +2707,24 @@ public class CamelMonitor extends CamelCommand {
}
}
+ // Timestamp — last entries carry the start time, so add elapsed to
get completion time
+ Object tsObj = json.get("timestamp");
+ if (tsObj instanceof Number n) {
+ long epochMs = n.longValue();
+ if (entry.last && entry.elapsed > 0) {
+ epochMs += entry.elapsed;
+ }
+ entry.epochMs = epochMs;
+ entry.timestamp = Instant.ofEpochMilli(epochMs)
+ .atZone(ZoneId.systemDefault())
+ .toLocalTime().toString();
+ if (entry.timestamp.length() > 12) {
+ entry.timestamp = entry.timestamp.substring(0, 12);
+ }
+ } else if (tsObj != null) {
+ entry.timestamp = tsObj.toString();
+ }
+
// Compute direction and processor label
if (entry.first || entry.last) {
entry.nodeLevel = Math.max(0, entry.nodeLevel - 1);
@@ -2841,10 +2858,13 @@ public class CamelMonitor extends CamelCommand {
entry.processor = indent + (entry.nodeLabel != null ?
entry.nodeLabel : "");
}
- // Timestamp
+ // Timestamp — last entries carry the start time, so add elapsed to
get completion time
Object tsObj = json.get("timestamp");
if (tsObj instanceof Number n) {
long epochMs = n.longValue();
+ if (entry.last && entry.elapsed > 0) {
+ epochMs += entry.elapsed;
+ }
entry.epochMs = epochMs;
entry.timestamp = Instant.ofEpochMilli(epochMs)
.atZone(ZoneId.systemDefault())
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 8fbfae4162af..4895759ceef9 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -80,6 +80,10 @@ class HistoryTab implements MonitorTab {
private int traceDetailScroll;
private int traceDetailHScroll;
+ private boolean showWaterfall;
+ private int waterfallScroll;
+ private final ScrollbarState waterfallScrollState = new ScrollbarState();
+
private final DiagramSupport diagram = new DiagramSupport();
volatile List<HistoryEntry> historyEntries = Collections.emptyList();
@@ -119,17 +123,33 @@ class HistoryTab implements MonitorTab {
if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
if (tracerActive && traceDetailView) {
- traceDetailScroll = Math.max(0, traceDetailScroll - 5);
+ if (showWaterfall) {
+ waterfallScroll = Math.max(0, waterfallScroll - 10);
+ } else {
+ traceDetailScroll = Math.max(0, traceDetailScroll - 5);
+ }
} else {
- historyDetailScroll = Math.max(0, historyDetailScroll - 5);
+ if (showWaterfall) {
+ waterfallScroll = Math.max(0, waterfallScroll - 10);
+ } else {
+ historyDetailScroll = Math.max(0, historyDetailScroll - 5);
+ }
}
return true;
}
if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
if (tracerActive && traceDetailView) {
- traceDetailScroll += 5;
+ if (showWaterfall) {
+ waterfallScroll += 10;
+ } else {
+ traceDetailScroll += 5;
+ }
} else {
- historyDetailScroll += 5;
+ if (showWaterfall) {
+ waterfallScroll += 10;
+ } else {
+ historyDetailScroll += 5;
+ }
}
return true;
}
@@ -175,6 +195,11 @@ class HistoryTab implements MonitorTab {
traceDetailHScroll = 0;
return true;
}
+ if (ke.isCharIgnoreCase('g')) {
+ showWaterfall = !showWaterfall;
+ waterfallScroll = 0;
+ return true;
+ }
} else if (tracerActive) {
if (ke.isChar('s')) {
traceSortIndex = (traceSortIndex + 1) %
TRACE_SORT_COLUMNS.length;
@@ -226,6 +251,11 @@ class HistoryTab implements MonitorTab {
historyDetailHScroll = 0;
return true;
}
+ if (ke.isCharIgnoreCase('g')) {
+ showWaterfall = !showWaterfall;
+ waterfallScroll = 0;
+ return true;
+ }
if (ke.isKey(KeyCode.F5)) {
historyEntries = Collections.emptyList();
historyDetailScroll = 0;
@@ -246,6 +276,8 @@ class HistoryTab implements MonitorTab {
traceDetailView = false;
traceSelectedExchangeId = null;
traceDetailScroll = 0;
+ showWaterfall = false;
+ waterfallScroll = 0;
return true;
}
return false;
@@ -349,16 +381,19 @@ class HistoryTab implements MonitorTab {
if (tracerActive && traceDetailView) {
hint(spans, "Esc", "back");
hint(spans, "↑↓", "navigate");
- hint(spans, "PgUp/PgDn", "scroll detail");
- if (!traceWordWrap) {
+ hint(spans, "PgUp/PgDn", "scroll");
+ if (!showWaterfall && !traceWordWrap) {
hint(spans, "←→", "h-scroll");
}
- hint(spans, "d", "diagram");
- hint(spans, "D", "text diagram");
- hint(spans, "p", "properties" + (showTraceProperties ? " [on]" : "
[off]"));
- hint(spans, "v", "variables" + (showTraceVariables ? " [on]" : "
[off]"));
- hint(spans, "h", "headers" + (showTraceHeaders ? " [on]" : "
[off]"));
- hint(spans, "b", "body" + (showTraceBody ? " [on]" : " [off]"));
+ hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : ""));
+ if (!showWaterfall) {
+ hint(spans, "d", "diagram");
+ hint(spans, "D", "text diagram");
+ hint(spans, "p", "properties" + (showTraceProperties ? " [on]"
: " [off]"));
+ hint(spans, "v", "variables" + (showTraceVariables ? " [on]" :
" [off]"));
+ hint(spans, "h", "headers" + (showTraceHeaders ? " [on]" : "
[off]"));
+ hint(spans, "b", "body" + (showTraceBody ? " [on]" : "
[off]"));
+ }
hintLast(spans, "w", "wrap" + (traceWordWrap ? " [on]" : "
[off]"));
} else if (tracerActive) {
hint(spans, "Esc", "back");
@@ -371,17 +406,20 @@ class HistoryTab implements MonitorTab {
} else {
hint(spans, "Esc", "back");
hint(spans, "↑↓", "navigate");
- hint(spans, "PgUp/PgDn", "scroll detail");
- if (!historyWordWrap) {
+ hint(spans, "PgUp/PgDn", "scroll");
+ if (!showWaterfall && !historyWordWrap) {
hint(spans, "←→", "h-scroll");
}
- hint(spans, "d", "diagram");
- hint(spans, "D", "text diagram");
- hint(spans, "p", "properties" + (showHistoryProperties ? " [on]" :
" [off]"));
- hint(spans, "v", "variables" + (showHistoryVariables ? " [on]" : "
[off]"));
- hint(spans, "h", "headers" + (showHistoryHeaders ? " [on]" : "
[off]"));
- hint(spans, "b", "body" + (showHistoryBody ? " [on]" : " [off]"));
- hint(spans, "w", "wrap" + (historyWordWrap ? " [on]" : " [off]"));
+ hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : ""));
+ if (!showWaterfall) {
+ hint(spans, "d", "diagram");
+ hint(spans, "D", "text diagram");
+ hint(spans, "p", "properties" + (showHistoryProperties ? "
[on]" : " [off]"));
+ hint(spans, "v", "variables" + (showHistoryVariables ? " [on]"
: " [off]"));
+ hint(spans, "h", "headers" + (showHistoryHeaders ? " [on]" : "
[off]"));
+ hint(spans, "b", "body" + (showHistoryBody ? " [on]" : "
[off]"));
+ hint(spans, "w", "wrap" + (historyWordWrap ? " [on]" : "
[off]"));
+ }
hintLast(spans, "F5", "refresh");
}
}
@@ -568,7 +606,7 @@ class HistoryTab implements MonitorTab {
List<TraceEntry> steps = getTraceSteps(traceSelectedExchangeId);
List<Rect> chunks = Layout.vertical()
- .constraints(Constraint.length(10), Constraint.fill())
+ .constraints(Constraint.length(10), Constraint.length(1),
Constraint.fill())
.split(area);
List<Row> rows = new ArrayList<>();
@@ -582,7 +620,11 @@ class HistoryTab implements MonitorTab {
frame.renderStatefulWidget(
buildStepTable(rows, stepTitle), chunks.get(0),
traceStepTableState);
- renderTraceStepDetail(frame, chunks.get(1), steps);
+ if (showWaterfall) {
+ renderWaterfall(frame, chunks.get(2),
steps.stream().map(WaterfallStep::fromTrace).toList());
+ } else {
+ renderTraceStepDetail(frame, chunks.get(2), steps);
+ }
}
private void renderTraceStepDetail(Frame frame, Rect area,
List<TraceEntry> steps) {
@@ -626,6 +668,162 @@ class HistoryTab implements MonitorTab {
traceDetailHScroll = hScroll[0];
}
+ record WaterfallStep(String nodeId, String processor, String direction,
boolean first, boolean last,
+ int nodeLevel, long elapsed) {
+
+ static WaterfallStep fromTrace(TraceEntry e) {
+ return new WaterfallStep(e.nodeId, e.processor, e.direction,
e.first, e.last, e.nodeLevel, e.elapsed);
+ }
+
+ static WaterfallStep fromHistory(HistoryEntry e) {
+ return new WaterfallStep(e.nodeId, e.processor, e.direction,
e.first, e.last, e.nodeLevel, e.elapsed);
+ }
+
+ WaterfallStep withElapsed(long newElapsed) {
+ return new WaterfallStep(nodeId, processor, direction, first,
last, nodeLevel, newElapsed);
+ }
+
+ String label() {
+ if (nodeId != null && !nodeId.isEmpty()) {
+ return nodeId;
+ }
+ if (processor != null) {
+ return processor.stripLeading();
+ }
+ return "";
+ }
+ }
+
+ private void renderWaterfall(Frame frame, Rect area, List<WaterfallStep>
allSteps) {
+ // Copy the elapsed from matching last entries onto first entries
+ // (first entries have elapsed=0, the total is on the last entry)
+ List<WaterfallStep> forward = new ArrayList<>();
+ for (WaterfallStep e : allSteps) {
+ if ("<--".equals(e.direction)) {
+ continue;
+ }
+ if (e.first) {
+ long totalElapsed = e.elapsed;
+ for (WaterfallStep other : allSteps) {
+ if (other.last && nodeIdEquals(e.nodeId, other.nodeId)) {
+ totalElapsed = other.elapsed;
+ break;
+ }
+ }
+ forward.add(totalElapsed != e.elapsed ?
e.withElapsed(totalElapsed) : e);
+ } else {
+ forward.add(e);
+ }
+ }
+
+ if (forward.isEmpty()) {
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(Line.from(
+ Span.styled(" No steps to display.",
Style.EMPTY.dim()))))
+
.block(Block.builder().borderType(BorderType.ROUNDED)
+ .title(" Waterfall ").build())
+ .build(),
+ area);
+ return;
+ }
+
+ long maxElapsed = 0;
+ long minDuration = Long.MAX_VALUE;
+ long maxDuration = 0;
+ for (WaterfallStep e : forward) {
+ maxElapsed = Math.max(maxElapsed, e.elapsed);
+ if (!e.first) {
+ minDuration = Math.min(minDuration, e.elapsed);
+ maxDuration = Math.max(maxDuration, e.elapsed);
+ }
+ }
+ if (minDuration == Long.MAX_VALUE) {
+ minDuration = 0;
+ }
+
+ String title = String.format(" Waterfall — %d steps ", forward.size());
+ Block block = Block.builder()
+ .borderType(BorderType.ROUNDED)
+ .title(title)
+ .build();
+ Rect inner = block.inner(area);
+ frame.renderWidget(block, area);
+
+ if (inner.height() < 1 || inner.width() < 10) {
+ return;
+ }
+
+ int visibleLines = inner.height();
+ int maxScroll = Math.max(0, forward.size() - visibleLines);
+ waterfallScroll = Math.min(waterfallScroll, maxScroll);
+
+ int labelWidth = 0;
+ for (WaterfallStep e : forward) {
+ int indent = e.nodeLevel * 2;
+ labelWidth = Math.max(labelWidth, indent + e.label().length());
+ }
+ labelWidth = Math.min(labelWidth + 2, inner.width() / 3);
+
+ int barMaxWidth = Math.max(10, inner.width() - labelWidth - 12);
+
+ int end = Math.min(waterfallScroll + visibleLines, forward.size());
+ List<Line> lines = new ArrayList<>();
+ for (int i = waterfallScroll; i < end; i++) {
+ lines.add(renderWaterfallStep(forward.get(i), labelWidth,
barMaxWidth,
+ maxElapsed, minDuration, maxDuration));
+ }
+
+ List<Rect> hChunks = Layout.horizontal()
+ .constraints(Constraint.fill(), Constraint.length(1))
+ .split(inner);
+
+ frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(),
hChunks.get(0));
+
+ if (forward.size() > visibleLines) {
+ waterfallScrollState
+ .contentLength(forward.size())
+ .viewportContentLength(visibleLines)
+ .position(waterfallScroll);
+ frame.renderStatefulWidget(Scrollbar.builder().build(),
hChunks.get(1), waterfallScrollState);
+ }
+ }
+
+ private static Line renderWaterfallStep(
+ WaterfallStep entry, int labelWidth, int maxBarWidth,
+ long maxElapsed, long minDuration, long maxDuration) {
+ String indent = " ".repeat(entry.nodeLevel);
+ String label = indent + entry.label();
+ if (label.length() > labelWidth) {
+ label = label.substring(0, labelWidth - 1) + "…";
+ } else {
+ label = String.format("%-" + labelWidth + "s", label);
+ }
+
+ boolean isRoute = entry.first;
+ Style bandStyle = isRoute ? Style.EMPTY.dim() :
TuiHelper.colorForDuration(entry.elapsed, minDuration, maxDuration);
+
+ double ratio = maxElapsed > 0 ? (double) entry.elapsed / maxElapsed :
0;
+ int barWidth = Math.max(1, (int) Math.round(ratio * maxBarWidth));
+ String bar = "█".repeat(barWidth);
+
+ String durationStr = entry.elapsed + "ms";
+ int pad = Math.max(1, 8 - durationStr.length());
+
+ return Line.from(
+ Span.styled(label, Style.EMPTY.fg(Color.CYAN)),
+ Span.styled(bar, bandStyle),
+ Span.raw(" ".repeat(pad)),
+ Span.styled(durationStr, isRoute ? Style.EMPTY.dim() :
Style.EMPTY.fg(Color.WHITE).bold()));
+ }
+
+ private static boolean nodeIdEquals(String a, String b) {
+ if (a == null || b == null) {
+ return a == b;
+ }
+ return a.equals(b);
+ }
+
// ---- History (Last) rendering ----
private void renderHistory(Frame frame, Rect area) {
@@ -638,7 +836,7 @@ class HistoryTab implements MonitorTab {
List<HistoryEntry> current = historyEntries;
List<Rect> chunks = Layout.vertical()
- .constraints(Constraint.length(10), Constraint.fill())
+ .constraints(Constraint.length(10), Constraint.length(1),
Constraint.fill())
.split(area);
List<Row> rows = new ArrayList<>();
@@ -652,7 +850,11 @@ class HistoryTab implements MonitorTab {
frame.renderStatefulWidget(
buildStepTable(rows, historyTitle), chunks.get(0),
historyTableState);
- renderHistoryDetail(frame, chunks.get(1), current);
+ if (showWaterfall) {
+ renderWaterfall(frame, chunks.get(2),
current.stream().map(WaterfallStep::fromHistory).toList());
+ } else {
+ renderHistoryDetail(frame, chunks.get(2), current);
+ }
}
private void renderHistoryDetail(Frame frame, Rect area,
List<HistoryEntry> current) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
index 6854935115d7..5595f4a377be 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
@@ -50,14 +50,6 @@ class StartupTab implements MonitorTab {
private static final Style VALUE = Style.EMPTY.fg(Color.WHITE).bold();
private static final Style HEADER = Style.EMPTY.fg(Color.YELLOW).bold();
- private static final Style[] BAND_STYLES = {
- Style.EMPTY.fg(Color.GREEN),
- Style.EMPTY.fg(Color.LIGHT_GREEN),
- Style.EMPTY.fg(Color.YELLOW),
- Style.EMPTY.fg(Color.rgb(0xFF, 0xA5, 0x00)),
- Style.EMPTY.fg(Color.RED),
- };
-
private final MonitorContext ctx;
private final ScrollbarState scrollbarState = new ScrollbarState();
private final AtomicBoolean loading = new AtomicBoolean(false);
@@ -250,13 +242,7 @@ class StartupTab implements MonitorTab {
}
private Style colorForDuration(long duration) {
- if (maxDurationColor <= minDurationColor) {
- return BAND_STYLES[0];
- }
- double ratio = (Math.log1p(duration) - Math.log1p(minDurationColor))
- / (Math.log1p(maxDurationColor) -
Math.log1p(minDurationColor));
- int bandIndex = Math.min((int) (ratio * 5), 4);
- return BAND_STYLES[bandIndex];
+ return TuiHelper.colorForDuration(duration, minDurationColor,
maxDurationColor);
}
@Override
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index 91f3d2b7630b..ea967b02d759 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -380,6 +380,24 @@ final class TuiHelper {
return style;
}
+ static final Style[] DURATION_BAND_STYLES = {
+ Style.EMPTY.fg(Color.GREEN),
+ Style.EMPTY.fg(Color.LIGHT_GREEN),
+ Style.EMPTY.fg(Color.YELLOW),
+ Style.EMPTY.fg(Color.rgb(0xFF, 0xA5, 0x00)),
+ Style.EMPTY.fg(Color.RED),
+ };
+
+ static Style colorForDuration(long duration, long minDuration, long
maxDuration) {
+ if (maxDuration <= minDuration) {
+ return DURATION_BAND_STYLES[0];
+ }
+ double ratio = (Math.log1p(duration) - Math.log1p(minDuration))
+ / (Math.log1p(maxDuration) - Math.log1p(minDuration));
+ int bandIndex = Math.min((int) (ratio * 5), 4);
+ return DURATION_BAND_STYLES[bandIndex];
+ }
+
static String shortTypeName(String type) {
if (type == null) {
return "null";