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 937f2db3d8c1 CAMEL-25771: camel-jbang - the Routes and Diagram views
show the revision and checksum of a route, and the Source view whether the file
runs as saved (#28220)
937f2db3d8c1 is described below
commit 937f2db3d8c1cc6c3f977f92926669301f66e2bd
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 23 10:45:15 2026 +0200
CAMEL-25771: camel-jbang - the Routes and Diagram views show the revision
and checksum of a route, and the Source view whether the file runs as saved
(#28220)
Builds on #28213: the Routes view shows the revision of a route that
changed in this run, the Diagram Info panel its checksum and revision, and the
Source view whether the file runs as saved. tui_get_table includes the checksum
and revision.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../jbang/camel-tui-diagram-route-revision.png | Bin 0 -> 243251 bytes
.../images/jbang/camel-tui-routes-revision.png | Bin 0 -> 285743 bytes
.../jbang/camel-tui-source-running-as-saved.png | Bin 0 -> 278430 bytes
.../ROOT/pages/camel-jbang-tui-diagram.adoc | 15 ++
.../ROOT/pages/camel-jbang-tui-source-editor.adoc | 19 ++
.../core/commands/tui/DiagramDetailSupport.java | 21 ++
.../dsl/jbang/core/commands/tui/RouteInfo.java | 4 +
.../dsl/jbang/core/commands/tui/RoutesTab.java | 26 ++-
.../jbang/core/commands/tui/SourceRunState.java | 248 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/SourceTab.java | 17 ++
.../dsl/jbang/core/commands/tui/StatusParser.java | 3 +
.../src/main/resources/tui/help/diagram.md | 9 +
.../src/main/resources/tui/help/routes.md | 11 +-
.../src/main/resources/tui/help/source.md | 15 ++
.../core/commands/tui/RouteChecksumViewsTest.java | 149 +++++++++++++
.../core/commands/tui/SourceRunStateTest.java | 232 +++++++++++++++++++
16 files changed, 766 insertions(+), 3 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-diagram-route-revision.png
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-diagram-route-revision.png
new file mode 100644
index 000000000000..2c099b2e975b
Binary files /dev/null and
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-diagram-route-revision.png
differ
diff --git
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-routes-revision.png
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-routes-revision.png
new file mode 100644
index 000000000000..dc3febdb641f
Binary files /dev/null and
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-routes-revision.png
differ
diff --git
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-running-as-saved.png
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-running-as-saved.png
new file mode 100644
index 000000000000..b2b671a92195
Binary files /dev/null and
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-running-as-saved.png
differ
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
index 33693e0a9107..c50d9a84a421 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
@@ -217,6 +217,21 @@ table of the processors of the selected route.
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]
+=== Checksum and Revision
+
+The Info panel of a route in the topology has its checksum: a short hash of
the route as written, the same in every
+run (see xref:routes.adoc#_route_checksum_and_revision[Route Checksum and
Revision]). `camel cmd route-structure` gives
+the checksum of the routes of a source file without running them, so you can
tell whether a file has the route that
+runs. A route added again changed in this run, such as by a reload of its
file, has its revision under it
+(`Revision: 3 · 2 changes`), and the Routes tab has it after the id of the
route, dimmed: `orders (rev 3)`.
+
+image::jbang/camel-tui-diagram-route-revision.png[The Info panel of the orders
route in the topology: its checksum, and revision 3 after two reloads changed
it]
+
+image::jbang/camel-tui-routes-revision.png[The Routes tab: the orders route is
at revision 3, and the audit route from the same file did not change]
+
+An integration of a Camel version before 4.24, or without camel-xml-io, has
neither: `camel run` always has
+camel-xml-io, while a Spring Boot or Quarkus application may not.
+
== Keyboard Shortcuts
=== Diagram Tab
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
index 6048fce36cb6..55a7117b77c2 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
@@ -9,6 +9,7 @@ routes.
* <<_live_run_data,*Live run data*>> -- the exchanges and failures of each
line of the running integration, next to
the code.
+* <<_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.
* <<_quick_fixes,*Quick fixes*>> -- *Shift+F9* applies the fix a problem
names, such as an option typo.
@@ -54,6 +55,24 @@ 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.
+== Running as Saved
+
+The bottom border of the source says whether the integration runs the routes
of the file as it is saved:
+
+* `● running as saved` -- the integration loaded the file after it was last
saved.
+* `○ reloading...` -- the file was saved a moment ago, and the integration
runs in dev mode, which reloads it.
+* `○ 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.
+
+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]
+
+The Source tab compares when the file was saved with when the integration
loaded the routes it has from the file, and
+shows nothing for a file it has no route from. It does not load the file
itself, as loading a route file runs parts of
+it, such as creating its beans. Edits not saved yet are not part of it: the
title of the editor marks them with `*`.
+To compare a file with the running routes one by one, `camel cmd
route-structure <file>` gives the checksum of each
+route of the file, and `camel get route --checksum` the checksum of each
running route.
+
== Problems as You Type
The editor checks your routes while you type: endpoint URIs and their options
against the Camel catalog, Simple
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
index 60d3358369a8..82d2ddaa7ce9 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
@@ -319,6 +319,7 @@ final class DiagramDetailSupport {
Span.styled(" Coverage: ", Theme.muted()),
Span.raw(route.coverage)));
}
+ addRevision(lines, route);
lines.add(Line.from(Span.raw("")));
int w = numWidth(route.total, route.failed, route.inflight);
@@ -389,6 +390,26 @@ final class DiagramDetailSupport {
frame.renderWidget(paragraph, area);
}
+ /**
+ * The checksum of the route as written, to compare with the checksum
camel cmd route-structure gives for the routes
+ * of a file, and its revision when it was added again changed in this
run, such as by a reload (CAMEL-25771).
+ */
+ static void addRevision(List<Line> lines, RouteInfo route) {
+ if (route.checksum == null) {
+ return;
+ }
+ lines.add(Line.from(
+ Span.styled(" Checksum: ", Theme.muted()),
+ Span.raw(route.checksum)));
+ if (route.revision > 1) {
+ int changes = route.revision - 1;
+ lines.add(Line.from(
+ Span.styled(" Revision: ", Theme.muted()),
+ Span.raw(String.valueOf(route.revision)),
+ Span.styled(changes == 1 ? " · 1 change" : " · " + changes
+ " changes", Theme.muted())));
+ }
+ }
+
/**
* A system outside the integration, selected in the topology: where it
is, the route it is linked to, its counts.
*/
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
index cbb595c5f1a0..d9649e3a83bd 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
@@ -55,5 +55,9 @@ class RouteInfo {
int variedBodyTypes;
/** The steps reached with bodies of more than one kind, which need
attention (CAMEL-25750). */
int mixedBodyKinds;
+ /** The checksum of the route as written, the same in every run; null when
unknown (CAMEL-25771). */
+ String checksum;
+ /** The revision of the route in this run: 1 when added, one more each
time it is added again changed. */
+ int revision;
final List<ProcessorInfo> processors = new ArrayList<>();
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 60c1a8f75ccf..9663de324354 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -451,7 +451,7 @@ class RoutesTab extends AbstractDiagramTab {
: Style.EMPTY;
routeRows.add(Row.from(
- Cell.from(Span.styled(route.routeId != null ?
route.routeId : "", Style.EMPTY.fg(Theme.accent()))),
+ Cell.from(routeIdLine(route, 24)),
routeFromCell(route, aiDescriptions, Integer.MAX_VALUE),
rightCell(route.total > 0 ?
formatDurationMs(route.meanTime) : "", 8,
topTimeStyle(route.meanTime)),
@@ -600,7 +600,7 @@ class RoutesTab extends AbstractDiagramTab {
List<Cell> cells = new ArrayList<>(
List.of(
-
Cell.from(Span.styled(TuiHelper.truncate(route.routeId, 24),
Style.EMPTY.fg(Theme.accent()))),
+ Cell.from(routeIdLine(route, 24)),
routeFromCell(route, aiDescriptions, fromWidth),
Cell.from(Span.styled(route.state != null ?
route.state : "", stateStyle)),
rightCell(formatThroughput(route.throughput), 8),
@@ -681,6 +681,23 @@ class RoutesTab extends AbstractDiagramTab {
.build();
}
+ /**
+ * The route id, and the revision dimmed after it when the route was added
again changed in this run, such as by a
+ * reload (CAMEL-25771): "orders (rev 3)" runs the third version of its
route since the start. The id is cut to
+ * leave room for the revision.
+ */
+ static Line routeIdLine(RouteInfo route, int width) {
+ String id = route.routeId != null ? route.routeId : "";
+ Style idStyle = Style.EMPTY.fg(Theme.accent());
+ if (route.revision <= 1) {
+ return Line.from(Span.styled(TuiHelper.truncate(id, width),
idStyle));
+ }
+ String rev = " (rev " + route.revision + ")";
+ return Line.from(
+ Span.styled(TuiHelper.truncate(id, Math.max(4, width -
CharWidth.of(rev))), idStyle),
+ Span.styled(rev, Style.EMPTY.dim()));
+ }
+
/**
* The steps of a route reached with more than one body type, across the
messages (CAMEL-25750): "⚠ 1 mixed · 2
* varied" when some are reached with bodies of more than one kind, which
need attention; empty when none varied.
@@ -1403,6 +1420,11 @@ class RoutesTab extends AbstractDiagramTab {
if (ri.group != null) {
row.put("group", ri.group);
}
+ // the route as written, to compare with its source or an earlier
run, and its changes in this run
+ if (ri.checksum != null) {
+ row.put("checksum", ri.checksum);
+ row.put("revision", ri.revision);
+ }
rows.add(row);
}
result.put("rows", rows);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
new file mode 100644
index 000000000000..76ee51aaed46
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
@@ -0,0 +1,248 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * Whether the selected integration runs the routes of the file open in the
source editor as the file is saved, which
+ * the Source tab shows on the editor (CAMEL-25771).
+ * <p/>
+ * The TUI does not load the file to tell. Loading a YAML or XML file with
Camel runs parts of it, such as creating its
+ * beans, and an endpoint written with parameters is built by its component,
which the TUI does not have, so the
+ * checksums would not be those of the running routes. It compares when the
file was saved with when the integration
+ * loaded the routes it has from the file:
+ * <ul>
+ * <li>an integration that has not reloaded loaded them when it started (a
route stopped and started again keeps what it
+ * loaded);</li>
+ * <li>after a reload, a route started after the save runs what is saved. The
integration does not say when it loaded a
+ * route, so a route stopped and started again by hand after a reload is taken
as loaded then.</li>
+ * </ul>
+ * A failed reload puts back the routes that ran before it, so until a reload
succeeds the state is the reload error.
+ */
+final class SourceRunState {
+
+ /** How long after a save an integration in dev mode is taken to be
reloading the file. */
+ static final long RELOAD_GRACE_MILLIS = 5000;
+
+ enum Kind {
+ /** every route the integration has from the file was loaded after the
file was saved */
+ RUNNING,
+ /** saved after the routes were loaded, and the integration reloads on
save: it is about to */
+ RELOADING,
+ /** the last reload failed, so the routes that ran before it run */
+ RELOAD_FAILED,
+ /** saved after the routes were loaded, and the integration did not
load it again */
+ NOT_RUNNING
+ }
+
+ /**
+ * @param kind the state
+ * @param routes the routes the integration has from the file
+ * @param stale how many of them were loaded before the file was saved
+ * @param detail why not running, or the reload error; null when running
+ */
+ record State(Kind kind, int routes, int stale, String detail) {
+ }
+
+ private SourceRunState() {
+ }
+
+ /**
+ * @param info the selected integration
+ * @param filePath the file open in the editor
+ * @param fileModified when the file was last saved, as the file system
has it (the wall clock)
+ * @param now the wall clock when the status of the integration
was read
+ * @return the state, or null when the integration runs no
route from the file
+ */
+ static State of(IntegrationInfo info, String filePath, long fileModified,
long now) {
+ String name = LiveRunLines.nameOf(filePath);
+ if (info == null || name == null || fileModified <= 0) {
+ return null;
+ }
+ // an integration that has not reloaded loaded its routes when it
started; after a reload, when each started
+ boolean reloaded = info.reloaded > 0 || info.reloadError != null ||
info.uptime <= 0;
+ int routes = 0;
+ int stale = 0;
+ for (RouteInfo r : info.routes) {
+ if (!"Started".equals(r.state) || LiveRunLines.lineOf(r.source,
name) < 0) {
+ continue;
+ }
+ long loaded = info.uptime;
+ if (reloaded) {
+ long uptime = uptimeMillis(r.uptime);
+ if (uptime < 0) {
+ continue;
+ }
+ loaded = Math.max(info.uptime, now - uptime);
+ }
+ routes++;
+ if (loaded < fileModified) {
+ stale++;
+ }
+ }
+ if (routes == 0) {
+ return null;
+ }
+ if (info.reloadError != null) {
+ return new State(Kind.RELOAD_FAILED, routes, stale,
info.reloadError);
+ }
+ if (stale == 0) {
+ return new State(Kind.RUNNING, routes, 0, null);
+ }
+ if (info.devMode) {
+ if (now - fileModified < RELOAD_GRACE_MILLIS) {
+ return new State(Kind.RELOADING, routes, stale, null);
+ }
+ return new State(Kind.NOT_RUNNING, routes, stale, "not reloaded
since it was saved");
+ }
+ return new State(Kind.NOT_RUNNING, routes, stale, "the integration
runs without --dev, restart it");
+ }
+
+ /** The uptime of a route as the route console writes it (5s, 1m5s, 2h5m,
1d3h); -1 when it has none. */
+ static long uptimeMillis(String uptime) {
+ if (uptime == null || uptime.isBlank()) {
+ return -1;
+ }
+ try {
+ return TimeUtils.toMilliSeconds(uptime.trim());
+ } catch (IllegalArgumentException e) {
+ return -1;
+ }
+ }
+
+ /** The state as the editor shows it: "● running as saved", "○ not running
yet: ..." */
+ static Line line(State state) {
+ return switch (state.kind()) {
+ case RUNNING -> Line.from(Span.styled(TuiIcons.SELECTED + "
running as saved", Theme.success()));
+ case RELOADING -> Line.from(Span.styled(TuiIcons.IDLE + "
reloading...", Theme.muted()));
+ case RELOAD_FAILED -> Line.from(
+ Span.styled(TuiIcons.WARN + " the reload failed",
Theme.error()),
+ Span.styled(": " +
TuiHelper.fixControlChars(state.detail().lines().findFirst().orElse("")),
+ Theme.muted()));
+ case NOT_RUNNING -> Line.from(
+ Span.styled(TuiIcons.IDLE + " not running yet",
Theme.warning()),
+ Span.styled(": " + state.detail(), Theme.muted()));
+ };
+ }
+
+ /** The state for tui_get_table, so an AI agent can tell whether its edit
runs. */
+ static JsonObject toJson(State state) {
+ JsonObject jo = new JsonObject();
+ jo.put("state", switch (state.kind()) {
+ case RUNNING -> "running";
+ case RELOADING -> "reloading";
+ case RELOAD_FAILED -> "reloadFailed";
+ case NOT_RUNNING -> "notRunning";
+ });
+ jo.put("routes", state.routes());
+ jo.put("routesLoadedBeforeSave", state.stale());
+ if (state.detail() != null) {
+ jo.put("detail", state.detail());
+ }
+ return jo;
+ }
+
+ /**
+ * What the Source tab keeps of it: the file it last drew, and the state
computed for that file on the background
+ * thread of the data refresh, as it reads when the file was saved.
+ */
+ static final class Tracker {
+
+ private record Computed(String file, State state) {
+ }
+
+ private volatile String file;
+ private volatile Computed computed;
+
+ /** The file the source panel shows, on the UI thread when it draws;
null when none is. */
+ void shown(String file) {
+ this.file = file;
+ }
+
+ /** Computes the state of the file shown, on the background thread of
the data refresh. */
+ void refresh(IntegrationInfo info) {
+ String shown = file;
+ if (shown == null || info == null) {
+ computed = null;
+ return;
+ }
+ try {
+ // the file system keeps the time of the save by the wall
clock, so it is compared with the wall clock
+ long modified =
Files.getLastModifiedTime(Path.of(shown)).toMillis();
+ State state = of(info, shown, modified,
System.currentTimeMillis());
+ computed = state != null ? new Computed(shown, state) : null;
+ } catch (IOException | RuntimeException e) {
+ computed = null;
+ }
+ }
+
+ void reset() {
+ file = null;
+ computed = null;
+ }
+
+ /** The state of the file shown; null when unknown, or when it was
computed for another file. */
+ State current() {
+ Computed c = computed;
+ String shown = file;
+ return c != null && c.file().equals(shown) ? c.state() : null;
+ }
+
+ /** The state on the bottom border of the source panel, left of the
cursor position of the editor. */
+ void render(Frame frame, Rect area) {
+ State state = current();
+ // the cursor position takes the right of the border (" row:999
col:999 ")
+ int width = area.width() - 4 - 18;
+ if (state == null || area.height() < 3 || width < 12) {
+ return;
+ }
+ List<Span> spans = new ArrayList<>();
+ spans.add(Span.raw(" "));
+ spans.addAll(line(state).spans());
+ spans.add(Span.raw(" "));
+ Line line = Line.from(spans);
+
frame.renderWidget(Paragraph.builder().text(Text.from(line)).build(),
+ new Rect(area.x() + 2, area.y() + area.height() - 1,
Math.min(width, line.width()), 1));
+ }
+
+ /** The file shown and its state, for tui_get_table. */
+ void addTo(JsonObject json) {
+ String shown = file;
+ if (shown != null) {
+ json.put("openFile", shown);
+ State state = current();
+ if (state != null) {
+ json.put("runState", toJson(state));
+ }
+ }
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 18c068bc338d..8eb377e8f88f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -76,6 +76,7 @@ class SourceTab extends AbstractTab {
private final ScrollbarState listScrollState = new ScrollbarState();
private List<FilesBrowser.FileEntry> entries = Collections.emptyList();
private final SourceViewer sourceViewer = new SourceViewer();
+ private final SourceRunState.Tracker runState = new
SourceRunState.Tracker();
private boolean focusOnViewer;
private int leftPanelWidth = -1;
@@ -216,10 +217,18 @@ class SourceTab extends AbstractTab {
lastSeenPid = ctx.selectedPid;
refreshFiles();
}
+ // the run state of the file shown, now rather than at the next
refresh, which shows the previous one till then
+ ctx.backgroundExecutor.execute(() ->
runState.refresh(ctx.findSelectedIntegration()));
+ }
+
+ @Override
+ public void refreshWhileVisible() {
+ runState.refresh(ctx.findSelectedIntegration());
}
@Override
public void onIntegrationChanged() {
+ runState.reset();
lastSeenPid = ctx.selectedPid;
rootDir = null;
assist.setRootDir(null);
@@ -565,6 +574,10 @@ class SourceTab extends AbstractTab {
json.put("totalFiles", entries.size());
json.put("focusOnViewer", focusOnViewer);
json.put("viewerOpen", sourceViewer.isVisible());
+ // whether the integration runs the file open in the editor as it is
saved, so an agent can tell its edit runs
+ // (computed now: tui_get_table may read this tab while another one is
on screen, which it does not refresh)
+ runState.refresh(ctx.findSelectedIntegration());
+ runState.addTo(json);
return json;
}
@@ -1178,7 +1191,11 @@ class SourceTab extends AbstractTab {
sourceViewer.setBorderStyle(sourceBorderStyle);
sourceViewer.setFocused(focusOnViewer);
sourceViewer.render(frame, area);
+ // whether the integration runs the file shown as it is saved, in
the view and in the editor (CAMEL-25771)
+ runState.shown(sourceViewer.getCurrentFilePath());
+ runState.render(frame, area);
} else {
+ runState.shown(null);
List<Line> lines = emptySourceLines(routeIndex, rootDir);
frame.renderWidget(
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 1ba8daab4040..59fc543e072c 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
@@ -261,6 +261,9 @@ final class StatusParser {
// the steps reached with more than one body type, and with
more than one kind (CAMEL-25750)
ri.variedBodyTypes = rj.getIntegerOrDefault("variedBodyTypes",
0);
ri.mixedBodyKinds = rj.getIntegerOrDefault("mixedBodyKinds",
0);
+ // the route as written, and how many times it was added again
changed in this run (CAMEL-25771)
+ ri.checksum = rj.getString("checksum");
+ ri.revision = rj.getIntegerOrDefault("revision", 0);
Map<String, ?> rs = rj.getMap("statistics");
ri.statistics = rs;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
index 5d10011bb852..ecce7337aafb 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
@@ -65,6 +65,15 @@ When a route is selected, an **Info panel** appears on the
left
showing key metrics: state, uptime, throughput, exchange counts,
and processing times.
+The Info panel also has the **checksum** of the route: a short hash of
+the route as written, the same in every run. `camel cmd route-structure`
+gives the checksum of the routes of a source file, so it tells whether a
+file has the route that runs. A route added again changed in this run,
+such as by a reload, has its **revision** too: `Revision: 3 · 2 changes`.
+An integration of a Camel version before 4.24, or without camel-xml-io, has
+neither: `camel run` always has camel-xml-io, while a Spring Boot or Quarkus
+application may not.
+
Press `Enter` on a selected route to **drill down** into its
internal EIP structure (the route diagram). Press `Esc` to
return to the topology view.
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
index fc49acbdb236..e35f9ce0101a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
@@ -6,7 +6,7 @@ they are sent to. A typical integration has multiple routes
working together.
## Route Table Columns
-- **ROUTE** — Unique route identifier (e.g., `timer-to-log`, `seda-consumer`)
+- **ROUTE** — Unique route identifier (e.g., `timer-to-log`, `seda-consumer`).
A route added again changed in this run, such as by a reload of its file, has
its revision after the id, dimmed: `orders (rev 3)` runs the third version of
its route since the integration started
- **FROM** — The endpoint that triggers this route (e.g., `timer`, `kafka`,
`file`). This is the source of messages
- **STATUS** — Route state: `Started` (running), `Stopped` (not running), or
`Suspended` (paused, can be resumed)
- **MSG/S** or **MSG/M** — Current message throughput (per second or per
minute) for this route. Configure in Settings (F2)
@@ -81,6 +81,15 @@ When a route is selected, an **Info panel** appears on the
left
showing key metrics: state, uptime, throughput, exchange counts,
and processing times.
+The Info panel also has the **checksum** of the route: a short hash of
+the route as written, the same in every run. `camel cmd route-structure`
+gives the checksum of the routes of a source file, so it tells whether a
+file has the route that runs. A route added again changed in this run,
+such as by a reload, has its **revision** too: `Revision: 3 · 2 changes`.
+An integration of a Camel version before 4.24, or without camel-xml-io, has
+neither: `camel run` always has camel-xml-io, while a Spring Boot or Quarkus
+application may not.
+
Press `Enter` on a selected route to **drill down** into its
internal EIP structure (the route diagram). Press `Esc` to
return to the topology view.
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
index 31323cc96049..563f893b0ca8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
@@ -47,6 +47,21 @@ processors on each line do: the exchanges they handled
(`79`), how many failed
its width, so the code does not move as the numbers grow. The source reads as
a heat map of the route: where messages go, where they fail.
+## Running as Saved
+The bottom border of the source says whether the integration runs the routes
+of the file as it is saved:
+- `● running as saved` — the integration loaded the file after it was saved
+- `○ reloading...` — saved a moment ago, and the integration reloads on save
(`--dev`)
+- `○ not running yet` — saved after the integration loaded it: restart an
+ integration that runs without `--dev`, or see the log of one that did not
+ reload the file
+- `⚠ the reload failed` — the integration runs what it ran before the reload,
+ until a reload succeeds
+
+It compares when the file was saved with when the integration loaded the
+routes it has from it, and shows nothing for a file it has no route from.
+Edits not saved yet are not part of it: the title marks them with `*`.
+
## Edit Mode (Shortcuts)
- **Ctrl+Z** — undo
- **Ctrl+Y / Ctrl+Shift+Z** — redo
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteChecksumViewsTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteChecksumViewsTest.java
new file mode 100644
index 000000000000..bbf7358c9f63
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteChecksumViewsTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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 java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.text.Line;
+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.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The checksum and the revision of the routes in the Routes view, the Info
panel of the diagram and tui_get_table
+ * (CAMEL-25771).
+ */
+class RouteChecksumViewsTest {
+
+ private MonitorContext ctx;
+ private IntegrationInfo info;
+
+ @BeforeEach
+ void setUp() {
+ Theme.resetForTesting();
+ info = new IntegrationInfo();
+ info.pid = "5678";
+ info.name = "orders";
+ ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new
AtomicReference<>(List.of()));
+ ctx.selectedPid = "5678";
+ }
+
+ @Test
+ void theStatusFileHasThem() {
+ JsonObject status = Jsoner.deserialize("""
+ {"routes": [
+ {"routeId": "orders", "from": "timer:orders", "state":
"Started", "checksum": "aa4535cb1725",
+ "revision": 2},
+ {"routeId": "stock", "from": "timer:stock", "state":
"Started"}]}
+ """, (JsonObject) null);
+ IntegrationInfo parsed = new IntegrationInfo();
+
+ StatusParser.parseRoutes(parsed, status);
+
+ assertThat(parsed.routes.get(0).checksum).isEqualTo("aa4535cb1725");
+ assertThat(parsed.routes.get(0).revision).isEqualTo(2);
+ // an integration of an older Camel has neither
+ assertThat(parsed.routes.get(1).checksum).isNull();
+ assertThat(parsed.routes.get(1).revision).isZero();
+ }
+
+ @Test
+ void aRouteChangedInThisRunHasItsRevisionAfterTheId() {
+ route("orders", "aa4535cb1725", 3);
+ route("stock", "71afc6e9b46a", 1);
+
+ String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx),
160, 30);
+
+ assertThat(rendered).contains("orders (rev 3)");
+ assertThat(rendered).doesNotContain("stock (rev");
+ // not a column of hex
+ assertThat(rendered).doesNotContain("aa4535cb1725");
+ }
+
+ @Test
+ void aLongIdIsCutToLeaveRoomForTheRevision() {
+ RouteInfo route = route("a-route-with-a-rather-long-id",
"aa4535cb1725", 12);
+
+ assertThat(text(RoutesTab.routeIdLine(route,
24))).isEqualTo("a-route-with... (rev 12)");
+ assertThat(text(RoutesTab.routeIdLine(route("orders", null, 0),
24))).isEqualTo("orders");
+ }
+
+ @Test
+ void theInfoPanelHasTheChecksumAndTheRevision() {
+ RouteInfo route = route("orders", "aa4535cb1725", 3);
+ List<Line> lines = new ArrayList<>();
+
+ DiagramDetailSupport.addRevision(lines, route);
+
+
assertThat(lines).extracting(RouteChecksumViewsTest::text).containsExactly(
+ " Checksum: aa4535cb1725",
+ " Revision: 3 · 2 changes");
+
+ lines.clear();
+ DiagramDetailSupport.addRevision(lines, route("stock", "71afc6e9b46a",
2));
+
assertThat(lines).extracting(RouteChecksumViewsTest::text).containsExactly(
+ " Checksum: 71afc6e9b46a",
+ " Revision: 2 · 1 change");
+
+ // not changed in this run: the checksum only
+ lines.clear();
+ DiagramDetailSupport.addRevision(lines, route("invoices",
"0c5d1e6b2f9a", 1));
+
assertThat(lines).extracting(RouteChecksumViewsTest::text).containsExactly("
Checksum: 0c5d1e6b2f9a");
+
+ // unknown (an older Camel): nothing
+ lines.clear();
+ DiagramDetailSupport.addRevision(lines, route("legacy", null, 0));
+ assertThat(lines).isEmpty();
+ }
+
+ @Test
+ void theTableHasThemForAnAgent() {
+ route("orders", "aa4535cb1725", 3);
+ route("legacy", null, 0);
+
+ JsonArray rows = (JsonArray) new
RoutesTab(ctx).getTableDataAsJson().get("rows");
+
+ JsonObject orders = (JsonObject) rows.get(0);
+ assertThat(orders.getString("checksum")).isEqualTo("aa4535cb1725");
+ assertThat(orders.getInteger("revision")).isEqualTo(3);
+ JsonObject legacy = (JsonObject) rows.get(1);
+ assertThat(legacy).doesNotContainKeys("checksum", "revision");
+ }
+
+ private static String text(Line line) {
+ StringBuilder sb = new StringBuilder();
+ line.spans().forEach(span -> sb.append(span.content()));
+ return sb.toString();
+ }
+
+ private RouteInfo route(String id, String checksum, int revision) {
+ RouteInfo route = new RouteInfo();
+ route.routeId = id;
+ route.from = "timer:" + id;
+ route.state = "Started";
+ route.checksum = checksum;
+ route.revision = revision;
+ info.routes.add(route);
+ return route;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
new file mode 100644
index 000000000000..9539dea0cc03
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.text.Line;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Whether the integration runs the file open in the source editor as it is
saved (CAMEL-25771): when the file was
+ * saved, against when the integration loaded the routes it has from the file.
+ */
+class SourceRunStateTest {
+
+ private static final long NOW = 1_800_000_000_000L;
+ private static final String FILE = "/work/orders.camel.yaml";
+
+ @TempDir
+ Path dir;
+
+ private IntegrationInfo info;
+
+ @BeforeEach
+ void setUp() {
+ Theme.resetForTesting();
+ info = new IntegrationInfo();
+ info.pid = "5678";
+ info.name = "orders";
+ // started a minute ago
+ info.uptime = NOW - 60_000;
+ }
+
+ @Test
+ void savedBeforeTheIntegrationStarted() {
+ route("orders", "orders.camel.yaml:3", "59s");
+ route("audit", "file:/work/orders.camel.yaml:20", "59s");
+
+ SourceRunState.State state = SourceRunState.of(info, FILE, NOW -
3_600_000, NOW);
+
+ assertThat(state.kind()).isEqualTo(SourceRunState.Kind.RUNNING);
+ assertThat(state.routes()).isEqualTo(2);
+
assertThat(text(SourceRunState.line(state))).isEqualTo(TuiIcons.SELECTED + "
running as saved");
+ }
+
+ @Test
+ void savedAfterTheIntegrationStartedWithoutDevMode() {
+ route("orders", "orders.camel.yaml:3", "59s");
+
+ SourceRunState.State state = SourceRunState.of(info, FILE, NOW -
10_000, NOW);
+
+ assertThat(state.kind()).isEqualTo(SourceRunState.Kind.NOT_RUNNING);
+ assertThat(state.stale()).isEqualTo(1);
+ assertThat(text(SourceRunState.line(state)))
+ .isEqualTo(TuiIcons.IDLE + " not running yet: the integration
runs without --dev, restart it");
+ }
+
+ @Test
+ void aRouteStoppedAndStartedAgainKeepsWhatItLoaded() {
+ // started by hand 2s ago, but never reloaded: it runs what it loaded
when the integration started
+ route("orders", "orders.camel.yaml:3", "2s");
+
+ SourceRunState.State state = SourceRunState.of(info, FILE, NOW -
10_000, NOW);
+
+ assertThat(state.kind()).isEqualTo(SourceRunState.Kind.NOT_RUNNING);
+ }
+
+ @Test
+ void devModeIsAboutToReloadARecentSave() {
+ info.devMode = true;
+ route("orders", "orders.camel.yaml:3", "59s");
+
+ SourceRunState.State state = SourceRunState.of(info, FILE, NOW -
1_000, NOW);
+
+ assertThat(state.kind()).isEqualTo(SourceRunState.Kind.RELOADING);
+ assertThat(text(SourceRunState.line(state))).isEqualTo(TuiIcons.IDLE +
" reloading...");
+
+ // and when it did not
+ state = SourceRunState.of(info, FILE, NOW - 10_000, NOW);
+ assertThat(state.kind()).isEqualTo(SourceRunState.Kind.NOT_RUNNING);
+ assertThat(state.detail()).isEqualTo("not reloaded since it was
saved");
+ }
+
+ @Test
+ void devModeReloadedAfterTheSave() {
+ info.devMode = true;
+ info.reloaded = 1;
+ route("orders", "orders.camel.yaml:3", "9s");
+
+ assertThat(SourceRunState.of(info, FILE, NOW - 10_000,
NOW).kind()).isEqualTo(SourceRunState.Kind.RUNNING);
+
+ // the reload was before the save
+ assertThat(SourceRunState.of(info, FILE, NOW - 8_000,
NOW).kind()).isEqualTo(SourceRunState.Kind.NOT_RUNNING);
+ }
+
+ @Test
+ void aFailedReloadPutsBackWhatRanBefore() {
+ info.devMode = true;
+ info.reloaded = 1;
+ info.reloadError = "Cannot load orders.camel.yaml: line 7 is not a
route\n\tat org.apache.camel.Foo";
+ // the routes of before started again
+ route("orders", "orders.camel.yaml:3", "1s");
+
+ SourceRunState.State state = SourceRunState.of(info, FILE, NOW -
3_000, NOW);
+
+ assertThat(state.kind()).isEqualTo(SourceRunState.Kind.RELOAD_FAILED);
+ assertThat(text(SourceRunState.line(state)))
+ .isEqualTo(TuiIcons.WARN + " the reload failed: Cannot load
orders.camel.yaml: line 7 is not a route");
+ }
+
+ @Test
+ void onlyTheStartedRoutesOfTheFile() {
+ route("stock", "stock.camel.yaml:3", "59s");
+ route("orders", "orders.camel.yaml:3", "59s").state = "Stopped";
+
+ assertThat(SourceRunState.of(info, FILE, NOW - 10_000, NOW)).isNull();
+
+ // a file the integration has no route from
+ assertThat(SourceRunState.of(info, "/work/README.md", NOW - 10_000,
NOW)).isNull();
+ }
+
+ @Test
+ void theUptimeOfARoute() {
+ assertThat(SourceRunState.uptimeMillis("0s")).isZero();
+ assertThat(SourceRunState.uptimeMillis("5s")).isEqualTo(5_000);
+ assertThat(SourceRunState.uptimeMillis("1m5s")).isEqualTo(65_000);
+ assertThat(SourceRunState.uptimeMillis("2h5m")).isEqualTo(7_500_000);
+ assertThat(SourceRunState.uptimeMillis("1d3h")).isEqualTo(97_200_000);
+ assertThat(SourceRunState.uptimeMillis("")).isEqualTo(-1);
+ assertThat(SourceRunState.uptimeMillis(null)).isEqualTo(-1);
+ assertThat(SourceRunState.uptimeMillis("soon")).isEqualTo(-1);
+ }
+
+ @Test
+ void theStateForAnAgent() {
+ route("orders", "orders.camel.yaml:3", "59s");
+
+ JsonObject json = SourceRunState.toJson(SourceRunState.of(info, FILE,
NOW - 10_000, NOW));
+
+ assertThat(json.getString("state")).isEqualTo("notRunning");
+ assertThat(json.getInteger("routes")).isEqualTo(1);
+ assertThat(json.getInteger("routesLoadedBeforeSave")).isEqualTo(1);
+ assertThat(json.getString("detail")).isEqualTo("the integration runs
without --dev, restart it");
+ }
+
+ @Test
+ void theSourceTabShowsItUnderTheFile() throws Exception {
+ Path file = Files.writeString(dir.resolve("orders.camel.yaml"), "-
from:\n uri: timer:orders\n");
+ long now = System.currentTimeMillis();
+ info.uptime = now - 60_000;
+ route("orders", "orders.camel.yaml:1", "59s");
+ Files.setLastModifiedTime(file, FileTime.fromMillis(now - 3_600_000));
+ MonitorContext ctx = new MonitorContext(new
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+ ctx.selectedPid = "5678";
+ SourceTab tab = new SourceTab(ctx);
+ // the integration is taken in on the first draw
+ TuiTestHelper.renderToString(tab, 120, 30);
+ tab.openFileForReplay(file);
+ TuiTestHelper.renderToString(tab, 120, 30);
+
+ tab.refreshWhileVisible();
+ String rendered = TuiTestHelper.renderToString(tab, 120, 30);
+ assertThat(rendered).contains(TuiIcons.SELECTED + " running as saved");
+ JsonObject runState = (JsonObject)
tab.getTableDataAsJson().get("runState");
+ assertThat(runState.getString("state")).isEqualTo("running");
+
+ // saved again: the integration has not loaded it
+ Files.setLastModifiedTime(file, FileTime.fromMillis(now - 1_000));
+ tab.refreshWhileVisible();
+ rendered = TuiTestHelper.renderToString(tab, 120, 30);
+ assertThat(rendered).contains(TuiIcons.IDLE + " not running yet: the
integration runs without --dev, restart it");
+ }
+
+ @Test
+ void tuiGetTableComputesItWhileAnotherTabIsOnScreen() throws Exception {
+ Path file = Files.writeString(dir.resolve("orders.camel.yaml"), "-
from:\n uri: timer:orders\n");
+ long now = System.currentTimeMillis();
+ info.uptime = now - 60_000;
+ route("orders", "orders.camel.yaml:1", "59s");
+ Files.setLastModifiedTime(file, FileTime.fromMillis(now - 3_600_000));
+ MonitorContext ctx = new MonitorContext(new
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+ ctx.selectedPid = "5678";
+ SourceTab tab = new SourceTab(ctx);
+ TuiTestHelper.renderToString(tab, 120, 30);
+ tab.openFileForReplay(file);
+ TuiTestHelper.renderToString(tab, 120, 30);
+
+ // the tab is not refreshed (it is not on screen), yet the table has
the state of now
+ assertThat(((JsonObject)
tab.getTableDataAsJson().get("runState")).getString("state")).isEqualTo("running");
+ Files.setLastModifiedTime(file, FileTime.fromMillis(now - 1_000));
+ assertThat(((JsonObject)
tab.getTableDataAsJson().get("runState")).getString("state")).isEqualTo("notRunning");
+ }
+
+ private static String text(Line line) {
+ StringBuilder sb = new StringBuilder();
+ line.spans().forEach(span -> sb.append(span.content()));
+ return sb.toString();
+ }
+
+ private RouteInfo route(String id, String source, String uptime) {
+ RouteInfo route = new RouteInfo();
+ route.routeId = id;
+ route.source = source;
+ route.state = "Started";
+ route.uptime = uptime;
+ info.routes.add(route);
+ return route;
+ }
+}