Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ The arrows of a branch are side by side, so the types beside them are cut to the
many more (`ByteArrayInputStreamCache +2`). The Info panel of the selected step lists them all with their kind. In the
topology, the Info panel of a route says how many of its steps were reached with more than one type
(`⚠ 1 mixed · 2 varied`), and the Routes tab has the same in its BODY TYPES column, with the types of each step in the
table of the processors of the selected route.
table of the processors of the selected route. The source view of the Source tab has them at the end of the line of each
step (see xref:camel-jbang-tui-source-editor.adoc#_body_types[Body Types]).

image::jbang/camel-tui-routes-body-types.png[The Routes tab: the orders route has four steps reached with bodies of more than one kind, and its processors list the types of each step]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ routes.
== Top Features

* <<_live_run_data,*Live run data*>> -- the exchanges and failures of each line of the running integration, next to
the code.
the code, and the body types each step has been reached with at the end of its line.
* <<_running_as_saved,*Running as saved*>> -- whether the integration runs the file as it is saved, or needs a restart.
* <<_problems_as_you_type,*Problems as you type*>> -- endpoint options, Simple expressions and more are checked as soon
as a file opens and while you type, in YAML, Java and XML routes; a YAML or XML file with problems is not saved.
Expand Down Expand Up @@ -55,6 +55,23 @@ In the example the timer fired 227 times; the `filter` let 137 of those through
The column keeps its width, so the code does not move as the numbers grow. It works for YAML, Java and XML routes, as
`camel run` records the source line of every processor.

=== Body Types

With the message history on, such as with `camel run --dev`, the body types each step has been reached with go at the
end of its line, dimmed and lined up. A step reached with bodies of more than one kind (`null`, `payload`, `data` or
`collection`) needs attention, as its body depends on the path a message takes: it has a `⚠` in the gutter, and its
types and kinds are in the warning color. The quick documentation of the line lists all the types, which the end of the
line cuts to the room there is.

image::jbang/camel-tui-source-body-types.png[The orders route in the source view: the steps after the choice are reached with a map and a stream, as only one branch marshals, so they have a warning in the gutter, and the quick documentation of the selected line says why]

The same body types are in the Routes and Diagram tabs (see
xref:camel-jbang-tui-diagram.adoc#_body_types[Body Types]).

The live run data and the body types are of the version of the file the integration runs. While it does not run the
file as saved (see <<_running_as_saved,Running as Saved>>), the numbers are dimmed and the body types are left out, as
the lines of the version that runs may be others.

== Running as Saved

The bottom border of the source says whether the integration runs the routes of the file as it is saved:
Expand All @@ -64,6 +81,8 @@ The bottom border of the source says whether the integration runs the routes of
* `○ not running yet` -- the file was saved after the integration loaded it. An integration that runs without `--dev`
does not reload a file when it is saved: restart it. The log of one in dev mode says why it did not reload the file.
* `⚠ the reload failed` -- the integration runs the routes it ran before the reload, until a reload succeeds.
* `○ the routes of this file are stopped` -- stopped (or suspended) by hand. The live run data and the body types still
show when the integration loaded the file as saved, which is not known for a route stopped after a reload.

image::jbang/camel-tui-source-running-as-saved.png[The bottom border of an edited route file: the integration runs without --dev, so the saved file is not running yet]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
package org.apache.camel.dsl.jbang.core.commands.tui;

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* The live run data of a source file: what the processors of a running integration on each line of it did, read from
Expand All @@ -33,10 +35,12 @@ private LiveRunLines() {
/**
* @param routes the routes of the running integration, with their processors
* @param filePath the file shown
* @return per 0-based line of the file, the exchanges, failures and mean time of the processors on it
* @return per 0-based line of the file, the exchanges, failures and mean time of the processors on it, and
* the body types they have been reached with
*/
static Map<Integer, SourceLiveColumn.LiveLine> of(List<RouteInfo> routes, String filePath) {
Map<Integer, long[]> sums = new HashMap<>();
Map<Integer, LineTypes> types = new HashMap<>();
String name = nameOf(filePath);
if (routes == null || name == null) {
return Map.of();
Expand All @@ -46,13 +50,48 @@ static Map<Integer, SourceLiveColumn.LiveLine> of(List<RouteInfo> routes, String
add(sums, name, r.source, r.total, r.failed, r.meanTime);
for (ProcessorInfo p : r.processors) {
add(sums, name, p.source, p.total, p.failed, p.meanTime);
addBodyTypes(types, name, p);
}
}
Map<Integer, SourceLiveColumn.LiveLine> answer = new HashMap<>();
sums.forEach((line, s) -> answer.put(line, new SourceLiveColumn.LiveLine(s[0], s[1], s[0] > 0 ? s[2] / s[0] : 0)));
sums.forEach((line, s) -> {
LineTypes t = types.get(line);
answer.put(line, t == null
? new SourceLiveColumn.LiveLine(s[0], s[1], s[0] > 0 ? s[2] / s[0] : 0)
: new SourceLiveColumn.LiveLine(
s[0], s[1], s[0] > 0 ? s[2] / s[0] : 0, List.copyOf(t.types), List.copyOf(t.kinds), t.mixed));
});
return answer;
}

/** The body types of the processors on a line, and whether one of them was reached with more than one kind. */
private static final class LineTypes {
final Set<String> types = new LinkedHashSet<>();
final Set<String> kinds = new LinkedHashSet<>();
boolean mixed;
}

/**
* Adds the body types a processor has been reached with to its line (CAMEL-25897): the types and the kinds of all
* the processors on the line, which is mixed when one of them was reached with bodies of more than one kind, not
* when two of them each had their own kind.
*/
private static void addBodyTypes(Map<Integer, LineTypes> types, String name, ProcessorInfo p) {
if (p.bodyTypes == null || p.bodyTypes.isEmpty()) {
return;
}
int line = lineOf(p.source, name);
if (line < 0) {
return;
}
LineTypes t = types.computeIfAbsent(line, k -> new LineTypes());
t.types.addAll(p.bodyTypes);
if (p.bodyKinds != null) {
t.kinds.addAll(p.bodyKinds);
}
t.mixed |= BodyTypes.mixed(p.bodyKinds);
}

/** Adds what a processor did to its line: totals and failures summed, the mean time weighted by the exchanges. */
private static void add(Map<Integer, long[]> sums, String name, String source, long total, long failed, long mean) {
int line = lineOf(source, name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,37 @@
import java.util.function.Supplier;

import dev.tamboui.style.Style;
import dev.tamboui.text.CharWidth;
import dev.tamboui.text.Span;

/**
* The live run data of the running integration in the source view ({@link SourceViewer}), a column at the end of the
* line numbers: how many exchanges the processors on a line handled, how many failed, and their mean time (a heat map
* of the source while it runs). A line that failed is what Shift+F8 asks the AI to fix when it has no problem.
* <p/>
* The body types the steps of a line have been reached with go at the end of the line, lined up, with a ⚠ in the gutter
* of a step reached with bodies of more than one kind (CAMEL-25897). They only hold while the view has the version of
* the file that runs: once the file is saved and the integration did not load it, the live run data is dimmed and the
* body types are left out.
*/
class SourceLiveColumn {

/**
* The most a line (its number and code) counts for lining up the body types: a long one does not push them away.
*/
private static final int MAX_BODY_TYPES_ALIGN = 72;

/**
* What a line of a running integration's source did: the exchanges of the processors on it, the failed ones and the
* mean processing time.
* mean processing time; and the body types they have been reached with, mixed when one of them was reached with
* bodies of more than one kind.
*/
record LiveLine(long total, long failed, long meanMillis) {
record LiveLine(long total, long failed, long meanMillis, List<String> bodyTypes, List<String> bodyKinds,
boolean mixed) {

LiveLine(long total, long failed, long meanMillis) {
this(total, failed, meanMillis, List.of(), List.of(), false);
}
}

/** The live run data of a file by its path: per 0-based line, what the processors on it did. */
Expand All @@ -54,6 +71,10 @@ interface LineFailures {

private final Supplier<String> loadedFile;
private final BooleanSupplier dirty;
private final Supplier<List<String>> lines;
private BooleanSupplier stale = () -> false;
// the column the body types of the lines line up at, from the lines that have them
private int bodyTypesAlign;
private LiveRunData liveRunData;
private LineFailures lineFailures;
private Map<Integer, LiveLine> liveLines = Collections.emptyMap();
Expand All @@ -62,14 +83,32 @@ interface LineFailures {
private int liveFailedWidth;
private int liveMeanWidth;
private String liveWidthsFile;
// the file the live run data was read for: another one is read at once, not after a second
private String liveLinesFile;

/**
* @param loadedFile the path of the file shown, whose live run data is read; null when none is
* @param dirty whether the file is edited and not saved, so the live run data is not of what is shown
* @param lines the lines shown (the line number, then the code), to line up the body types after them
*/
SourceLiveColumn(Supplier<String> loadedFile, BooleanSupplier dirty) {
SourceLiveColumn(Supplier<String> loadedFile, BooleanSupplier dirty, Supplier<List<String>> lines) {
this.loadedFile = loadedFile;
this.dirty = dirty;
this.lines = lines;
}

/**
* Whether the integration runs another version of the file than the one shown, such as one saved since it loaded
* the file (CAMEL-25771): the live run data is of other lines then, so it is dimmed and the body types are left
* out.
*/
void setStale(BooleanSupplier stale) {
this.stale = stale;
}

/** Whether what the integration did is of the lines shown: the file is not edited, and it runs as shown. */
private boolean current() {
return !dirty.getAsBoolean() && !stale.getAsBoolean();
}

/**
Expand Down Expand Up @@ -105,7 +144,7 @@ String runtimeFailure(int row) {
* of each frame asks it; the exception is only read on Shift+F8).
*/
boolean failsAtRuntime(int row) {
if (dirty.getAsBoolean() || row < 0) {
if (!current() || row < 0) {
return false;
}
LiveLine live = liveLines().get(row);
Expand All @@ -130,6 +169,11 @@ void addLiveColumn(List<Span> spans, int lineIndex, Style selBg) {
Style totalStyle = data ? Theme.label() : Style.EMPTY;
Style failedStyle = Theme.error().bold();
Style dim = Style.EMPTY.dim();
if (stale.getAsBoolean()) {
// of the version of the file that runs, whose lines may be others
totalStyle = dim;
failedStyle = dim;
}
if (selBg != null) {
totalStyle = totalStyle.patch(selBg);
failedStyle = failedStyle.patch(selBg);
Expand All @@ -145,14 +189,69 @@ void addLiveColumn(List<Span> spans, int lineIndex, Style selBg) {
spans.add(Span.styled(" │", dim));
}

/**
* The gutter of a line: a ⚠ when a step on it has been reached with bodies of more than one kind, which needs
* attention (CAMEL-25897); blank otherwise.
*/
Span gutter(int lineIndex) {
LiveLine live = current() ? liveLines().get(lineIndex) : null;
return live != null && live.mixed() ? Span.styled(" " + BodyTypes.MARK + " ", Theme.warning()) : Span.raw(" ");
}

/**
* The body types the steps of a line have been reached with, at the end of the line, dimmed (a warning when they
* are of more than one kind), lined up with those of the other lines, and cut to the room up to the right edge
* (CAMEL-25897). Nothing in the plain mode, or when the view does not have the version of the file that runs.
*
* @param spans the spans of the line so far: the gutter, the line number, the live run data and the code
* @param rightEdge the column of the right edge of the view, in the columns of the line
*/
void addBodyTypes(List<Span> spans, int lineIndex, boolean plain, Style selBg, int rightEdge) {
LiveLine live = plain || !current() ? null : liveLines().get(lineIndex);
if (live == null || live.bodyTypes().isEmpty()) {
return;
}
int width = 0;
for (Span span : spans) {
width += CharWidth.of(span.content());
}
int pad = Math.max(2, bodyTypesAlign - width);
String text = BodyTypes.fit(live.bodyTypes(), live.mixed() ? live.bodyKinds() : List.of(), rightEdge - width - pad);
if (text.isEmpty()) {
return;
}
Style style = live.mixed() ? Theme.warning() : Theme.muted();
spans.add(Span.styled(" ".repeat(pad), selBg != null ? selBg : Style.EMPTY));
spans.add(Span.styled(text, selBg != null ? style.patch(selBg) : style));
}

/**
* The body types of the steps of a line for the quick documentation (CAMEL-25897), all of them, and why a mix of
* kinds needs attention; null when the line has none, or the editor does not have the version of the file that
* runs.
*/
SourceViewer.DocEntry bodyTypesDoc(int row) {
LiveLine live = current() ? liveLines().get(row) : null;
if (live == null || live.bodyTypes().isEmpty()) {
return null;
}
if (live.mixed()) {
return SourceViewer.DocEntry.withTitle("Body types", BodyTypes.names(live.bodyTypes()) + " " + BodyTypes.MARK
+ " " + BodyTypes.kinds(live.bodyKinds())
+ ": the body here depends on the path a message takes");
}
String title = live.bodyKinds().size() == 1 ? "Body types (" + live.bodyKinds().get(0) + ")" : "Body types";
return SourceViewer.DocEntry.withTitle(title, BodyTypes.names(live.bodyTypes()));
}

/** The live run data of the loaded file, read again at most once a second (the status is polled about as often). */
private Map<Integer, LiveLine> liveLines() {
String loadedFilePath = loadedFile.get();
if (liveRunData == null || loadedFilePath == null) {
return Collections.emptyMap();
}
long now = MonotonicClock.millis();
if (now - liveLinesTime > 1000) {
if (now - liveLinesTime > 1000 || !loadedFilePath.equals(liveLinesFile)) {
refreshLiveLines(now);
}
return liveLines;
Expand All @@ -178,6 +277,7 @@ private void refreshLiveLines(long now) {
Map<Integer, LiveLine> lines = liveRunData.lines(loadedFilePath);
liveLines = lines != null ? lines : Collections.emptyMap();
liveLinesTime = now;
liveLinesFile = loadedFilePath;
// the widths of the column, for all the lines alike; they only grow while the same file is shown (from
// a minimum), so the column does not jitter when a number crosses 99 -> 100 and back (a mean time going
// up and down, a reset of the statistics); another file, or the integration stopping, starts afresh
Expand All @@ -198,6 +298,32 @@ private void refreshLiveLines(long now) {
}
}
}
bodyTypesAlign = bodyTypesAlign();
}
}

/**
* The column the body types line up at: after the longest of the lines that have them (the gutter, the line number,
* the live run data and the code), and at most {@link #MAX_BODY_TYPES_ALIGN} into the code, so a long line does not
* push the body types of the others off screen.
*/
private int bodyTypesAlign() {
List<String> shown = lines.get();
int longest = 0;
for (Map.Entry<Integer, LiveLine> e : liveLines.entrySet()) {
int i = e.getKey();
if (!e.getValue().bodyTypes().isEmpty() && shown != null && i >= 0 && i < shown.size()) {
longest = Math.max(longest, Math.min(CharWidth.of(shown.get(i)), MAX_BODY_TYPES_ALIGN));
}
}
if (longest == 0) {
return 0;
}
// the gutter before the line number, and the live run data after it
int live = liveTotalWidth == 0
? 0
: 1 + liveTotalWidth + (liveFailedWidth > 0 ? 1 + liveFailedWidth : 0)
+ (liveMeanWidth > 0 ? 1 + liveMeanWidth : 0) + 2;
return 3 + longest + live + 3;
}
}
Loading
Loading