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 2167f428a41a CAMEL-26695: camel-jbang - chaos from where you look: ! 
on an endpoint, a consumer or a diagram node (#29348)
2167f428a41a is described below

commit 2167f428a41a6b3cb0db6bf3d63b52c311ef8d33
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 17:19:32 2026 +0100

    CAMEL-26695: camel-jbang - chaos from where you look: ! on an endpoint, a 
consumer or a diagram node (#29348)
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../ROOT/images/jbang/camel-tui-chaos-here.png     | Bin 0 -> 349301 bytes
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |   6 +
 .../dsl/jbang/core/commands/tui/ChaosControl.java  |  24 ++-
 .../dsl/jbang/core/commands/tui/ChaosHere.java     | 151 ++++++++++++++++++
 .../jbang/core/commands/tui/ChaosRecipeForm.java   |  73 ++++++++-
 .../dsl/jbang/core/commands/tui/ChaosTab.java      |   5 +
 .../dsl/jbang/core/commands/tui/ConsumersTab.java  |  24 +++
 .../dsl/jbang/core/commands/tui/DiagramTab.java    |  23 +++
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  |  17 ++
 .../jbang/core/commands/tui/MonitorContext.java    |   3 +
 .../dsl/jbang/core/commands/tui/TabRegistry.java   |  17 +-
 .../src/main/resources/tui/help/chaos.md           |  13 ++
 .../dsl/jbang/core/commands/tui/ChaosHereTest.java | 176 +++++++++++++++++++++
 13 files changed, 524 insertions(+), 8 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-here.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-here.png
new file mode 100644
index 000000000000..15b1eab2c04e
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-here.png differ
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index c2b2a32e5b12..37467664d7db 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -272,6 +272,12 @@ values start with a default, and the rule is shown as it 
is built. The last entr
 +
 image::jbang/camel-tui-chaos-recipes.png[Adding a chaos rule from the known 
rules: the HTTP endpoint of the route is picked]
 * *e* changes the selected rule, starting from the rule as it is.
+* *!* on the Endpoint tab, the Consumers tab or a node of the Diagram opens 
the known rules for what is selected, with
+it filled in: for a `to` or an endpoint the routes send to, the faults of that 
endpoint (an HTTP status only for an
+HTTP endpoint) and of the step; for the `from` of a route, a consumer or an 
endpoint a route consumes, the faults of
+the route, and of its polls when it polls.
++
+image::jbang/camel-tui-chaos-here.png[! on the to callStock of the Diagram: 
the known rules for its HTTP endpoint and its step, with the endpoint filled in]
 * *x* removes the selected rule, *c* removes all rules (the report keeps what 
they did), and *R* starts a new experiment:
 no rules and a new report, with the same seed.
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
index 701cb2e422ed..56218a28bc28 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
@@ -203,6 +203,28 @@ final class ChaosControl {
         return false;
     }
 
+    /**
+     * Opens the known rules for a target ({@code !} on an endpoint, a 
consumer or a node of the diagram), or the rule
+     * typed as one line, starting with the target, when the catalog has no 
known rules.
+     */
+    void openAt(IntegrationInfo info, ChaosHere.Target target) {
+        if (!ChaosHere.available(info)) {
+            return;
+        }
+        pid = info.pid;
+        name = info.name;
+        id = null;
+        command = Command.ADD;
+        ChaosRecipes known = knownRules();
+        ChaosRecipeForm targeted = known != null ? new ChaosRecipeForm(known, 
info, target) : null;
+        if (targeted != null && !targeted.recipes().isEmpty()) {
+            form = targeted;
+            step = Step.RECIPES;
+        } else {
+            openInput(Command.ADD, target.rulePrefix());
+        }
+    }
+
     private ChaosRecipes knownRules() {
         if (!recipesLoaded) {
             recipes = recipeSource.get();
@@ -214,7 +236,7 @@ final class ChaosControl {
     private boolean handleRecipeKey(KeyEvent ke) {
         switch (form.handleKey(ke)) {
             case CLOSED -> close();
-            case TYPE_RULE -> openInput(Command.ADD, "");
+            case TYPE_RULE -> openInput(Command.ADD, form.target() != null ? 
form.target().rulePrefix() : "");
             case RULE -> {
                 rule = form.rule();
                 ask(Command.ADD);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosHere.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosHere.java
new file mode 100644
index 000000000000..53872d6f11c2
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosHere.java
@@ -0,0 +1,151 @@
+/*
+ * 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.List;
+
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyEvent;
+
+/**
+ * Chaos from where you look ({@code !}): on the Endpoint tab, the Consumers 
tab and a node of the Diagram, the key
+ * opens the known chaos rules (the Chaos screen, {@link ChaosRecipeForm}) for 
what is selected, with it filled in: the
+ * endpoint a step sends to, the endpoint a route polls, the route, or the 
step. Only when chaos is on in the
+ * integration.
+ */
+final class ChaosHere {
+
+    /**
+     * What is selected, as targets of a chaos rule.
+     *
+     * @param label    what the user sees, for the title (such as {@code to 
http://localhost:8765/stock})
+     * @param endpoint the endpoint a step sends to, without its query, or null
+     * @param polled   the endpoint a route consumes with a scheduled poll, 
without its query, or null
+     * @param route    the route, or null
+     * @param step     the id of the step, or null
+     */
+    record Target(String label, String endpoint, String polled, String route, 
String step) {
+
+        /** The target as the start of a rule typed as one line. */
+        String rulePrefix() {
+            if (step != null) {
+                return "step=" + step + " ";
+            }
+            if (endpoint != null) {
+                return "endpoint=" + endpoint + " ";
+            }
+            if (polled != null) {
+                return "endpoint=" + polled + " ";
+            }
+            return route != null ? "route=" + route + " " : "";
+        }
+    }
+
+    private ChaosHere() {
+    }
+
+    /** Whether {@code !} works for the integration: chaos is on. */
+    static boolean available(IntegrationInfo info) {
+        return info != null && info.chaos != null && info.chaos.enabled;
+    }
+
+    /**
+     * Handles {@code !}: opens the Chaos screen for the target.
+     *
+     * @return whether the key was used
+     */
+    static boolean handleKey(KeyEvent ke, MonitorContext ctx, IntegrationInfo 
info, Target target) {
+        if (!ke.isChar('!') || !available(info) || target == null || 
ctx.chaosHere == null) {
+            return false;
+        }
+        ctx.chaosHere.accept(target);
+        return true;
+    }
+
+    /** The {@code !} key in the footer, when it works. */
+    static void hint(List<Span> spans, IntegrationInfo info) {
+        if (available(info)) {
+            TuiHelper.hint(spans, "!", "chaos");
+        }
+    }
+
+    /** An endpoint of the Endpoint tab: what its routes send to, or what a 
route consumes from it. */
+    static Target forEndpoint(EndpointInfo ep, IntegrationInfo info) {
+        if (ep == null || ep.uri == null) {
+            return null;
+        }
+        String uri = withoutQuery(ep.uri);
+        if ("in".equalsIgnoreCase(ep.direction)) {
+            return new Target("from " + uri, null, polled(info, ep.uri), 
ep.routeId, null);
+        }
+        return new Target("to " + uri, uri, null, null, null);
+    }
+
+    /** A consumer of the Consumers tab: its route, and its endpoint when it 
polls. */
+    static Target forConsumer(ConsumerInfo consumer) {
+        if (consumer == null) {
+            return null;
+        }
+        String uri = consumer.uri != null ? withoutQuery(consumer.uri) : null;
+        return new Target(
+                "route " + consumer.id + (uri != null ? " (" + uri + ")" : 
""), null,
+                consumer.scheduled ? uri : null, consumer.id, null);
+    }
+
+    /**
+     * A node of a route on the Diagram: the step, and the endpoint it sends 
to (the uri of the node, else the producer
+     * of the step); the {@code from} of a route is the route, and its 
endpoint when it polls.
+     */
+    static Target forDiagramNode(String nodeId, String type, String uri, 
String routeId, IntegrationInfo info) {
+        if ("from".equals(type) || nodeId == null) {
+            if (routeId == null) {
+                return null;
+            }
+            String polled = null;
+            for (ConsumerInfo c : info.consumers) {
+                if (routeId.equals(c.id) && c.scheduled && c.uri != null) {
+                    polled = withoutQuery(c.uri);
+                }
+            }
+            return new Target("route " + routeId, null, polled, routeId, null);
+        }
+        String endpoint = uri != null && !uri.isBlank() ? withoutQuery(uri) : 
null;
+        for (ProducerInfo p : endpoint == null ? info.producers : 
List.<ProducerInfo> of()) {
+            if (nodeId.equals(p.stepId) && p.uri != null) {
+                endpoint = withoutQuery(p.uri);
+            }
+        }
+        // a step offers the faults of its endpoint and its own; the route is 
the target of the from of the route
+        return new Target(
+                type + " " + nodeId + (endpoint != null ? " (" + endpoint + 
")" : ""), endpoint, null, null,
+                nodeId);
+    }
+
+    private static String polled(IntegrationInfo info, String uri) {
+        for (ConsumerInfo c : info.consumers) {
+            if (c.scheduled && uri.equals(c.uri)) {
+                return withoutQuery(uri);
+            }
+        }
+        return null;
+    }
+
+    static String withoutQuery(String uri) {
+        int q = uri.indexOf('?');
+        return q > 0 ? uri.substring(0, q) : uri;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeForm.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeForm.java
index 4459cae0814e..6fef3adeb031 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeForm.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeForm.java
@@ -98,6 +98,8 @@ final class ChaosRecipeForm {
 
     private final List<ChaosRecipes.Recipe> recipes;
     private final IntegrationInfo info;
+    // what the form was opened for ({@code !} on an endpoint, a consumer or a 
node of the diagram), or null
+    private final ChaosHere.Target target;
     // the index in the list (the recipes, then the entry that types the rule)
     private int selected;
     private boolean inForm;
@@ -106,8 +108,64 @@ final class ChaosRecipeForm {
     private String rule;
 
     ChaosRecipeForm(ChaosRecipes recipes, IntegrationInfo info) {
-        this.recipes = recipes.recipes();
+        this(recipes, info, null);
+    }
+
+    /** The known rules for a target: only the recipes it fills in, with it 
filled in. */
+    ChaosRecipeForm(ChaosRecipes recipes, IntegrationInfo info, 
ChaosHere.Target target) {
+        this.recipes = target == null
+                ? recipes.recipes()
+                : recipes.recipes().stream().filter(r -> applies(r, 
target)).toList();
         this.info = info;
+        this.target = target;
+    }
+
+    ChaosHere.Target target() {
+        return target;
+    }
+
+    List<ChaosRecipes.Recipe> recipes() {
+        return recipes;
+    }
+
+    /** Whether the target fills in every placeholder of the recipe that names 
an endpoint, a route or a step. */
+    static boolean applies(ChaosRecipes.Recipe recipe, ChaosHere.Target 
target) {
+        boolean any = false;
+        for (ChaosRecipes.Placeholder p : recipe.placeholders()) {
+            if (isTargetKind(p)) {
+                if (preset(p, target) == null) {
+                    return false;
+                }
+                any = true;
+            }
+        }
+        return any;
+    }
+
+    private static boolean isTargetKind(ChaosRecipes.Placeholder p) {
+        String from = p.pickFrom();
+        return "producers".equals(from) || "pollingConsumers".equals(from) || 
"routes".equals(from)
+                || "steps".equals(from);
+    }
+
+    /** The value of the target for a placeholder, or null when the target 
does not fill it in. */
+    static String preset(ChaosRecipes.Placeholder p, ChaosHere.Target target) {
+        if (target == null || p.pickFrom() == null) {
+            return null;
+        }
+        return switch (p.pickFrom()) {
+            case "producers" -> target.endpoint() != null && 
ofScheme(target.endpoint(), p.schemes())
+                    ? target.endpoint() : null;
+            case "pollingConsumers" -> target.polled();
+            case "routes" -> target.route();
+            case "steps" -> target.step();
+            default -> null;
+        };
+    }
+
+    private static boolean ofScheme(String uri, List<String> schemes) {
+        int colon = uri.indexOf(':');
+        return schemes.isEmpty() || colon > 0 && 
schemes.contains(uri.substring(0, colon));
     }
 
     boolean inForm() {
@@ -151,7 +209,15 @@ final class ChaosRecipeForm {
     private void openForm(ChaosRecipes.Recipe recipe) {
         fields.clear();
         for (ChaosRecipes.Placeholder p : recipe.placeholders()) {
-            fields.add(new Field(p, ChaosRecipes.candidates(p, info)));
+            Field field = new Field(p, ChaosRecipes.candidates(p, info));
+            String preset = preset(p, target);
+            if (preset != null) {
+                // what the form was opened for
+                field.input.setText(preset);
+                field.input.moveCursorToEnd();
+                field.choice = field.choices.indexOf(preset);
+            }
+            fields.add(field);
         }
         focus = 0;
         inForm = true;
@@ -219,7 +285,8 @@ final class ChaosRecipeForm {
         Rect popup = DialogHelper.centered(area, width, height);
         frame.renderWidget(Clear.INSTANCE, popup);
         Block outer = 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(Theme.warning())
-                .title(" Add a chaos rule to " + info.name + " ").build();
+                .title(" Add a chaos rule to " + info.name + (target != null ? 
" at " + target.label() : "") + " ")
+                .build();
         frame.renderWidget(outer, popup);
         Rect inner = outer.inner(popup);
         List<Rect> cols = Layout.horizontal()
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
index 38a4e4ae630e..2127e8154b48 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
@@ -72,6 +72,11 @@ class ChaosTab extends AbstractTableTab {
         return control;
     }
 
+    /** Opens the known rules for a target ({@code !} on another screen). */
+    void openAt(ChaosHere.Target target) {
+        control.openAt(ctx.findSelectedIntegration(), target);
+    }
+
     @Override
     protected boolean handleTabKeyEvent(KeyEvent ke) {
         IntegrationInfo info = ctx.findSelectedIntegration();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
index edcdb8744e22..4f504c25bc0c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
@@ -26,6 +26,7 @@ import dev.tamboui.layout.Rect;
 import dev.tamboui.style.Style;
 import dev.tamboui.terminal.Frame;
 import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.MouseEvent;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
@@ -297,6 +298,29 @@ class ConsumersTab extends AbstractTableTab {
         return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Consumers");
     }
 
+    @Override
+    protected boolean handleTabKeyEvent(KeyEvent ke) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return ke.isChar('!') && ChaosHere.handleKey(ke, ctx, info, 
ChaosHere.forConsumer(selectedConsumer(info)));
+    }
+
+    @Override
+    public void renderFooter(List<Span> spans) {
+        super.renderFooter(spans);
+        ChaosHere.hint(spans, ctx.findSelectedIntegration());
+    }
+
+    /** The consumer selected in the table, as sorted. */
+    private ConsumerInfo selectedConsumer(IntegrationInfo info) {
+        Integer sel = tableState.selected();
+        if (info == null || sel == null) {
+            return null;
+        }
+        List<ConsumerInfo> sorted = new ArrayList<>(info.consumers);
+        sorted.sort(this::sortConsumer);
+        return sel >= 0 && sel < sorted.size() ? sorted.get(sel) : null;
+    }
+
     @Override
     public String description() {
         return "Consumer statistics (polling and event-driven consumers)";
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index b643834187ca..535779af9f0e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -38,6 +38,7 @@ import dev.tamboui.widgets.block.BorderType;
 import dev.tamboui.widgets.block.Borders;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import org.apache.camel.dsl.jbang.core.commands.ai.ProjectCapabilities;
+import org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
@@ -80,6 +81,11 @@ class DiagramTab extends AbstractDiagramTab {
             return true;
         }
 
+        // !: the known chaos rules for the selected node (or route)
+        if (diagram.isShowDiagram() && ke.isChar('!') && handleChaosKey(ke)) {
+            return true;
+        }
+
         // v moves through the zoom levels: architecture, topology, route 
(CAMEL-25147)
         if (diagram.isShowDiagram() && ke.isCharIgnoreCase('v')) {
             cycleLevel();
@@ -681,6 +687,22 @@ class DiagramTab extends AbstractDiagramTab {
                 area);
     }
 
+    /** {@code !} on the diagram: the selected node of a route, or the 
selected route of the topology. */
+    private boolean handleChaosKey(KeyEvent ke) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (!ChaosHere.available(info)) {
+            return false;
+        }
+        RouteDiagramWidget.EipNodeBox box = topologyMode ? null : 
diagram.getSelectedEipNodeBox();
+        // the endpoint of a to, from the model of the route
+        String uri = box != null && box.layoutNode() != null && 
box.layoutNode().treeNode != null
+                ? box.layoutNode().treeNode.info.uri : null;
+        ChaosHere.Target target = ChaosHere.forDiagramNode(
+                box != null ? box.nodeId() : null, box != null ? box.type() : 
null, uri, diagram.getSelectedRouteId(),
+                info);
+        return ChaosHere.handleKey(ke, ctx, info, target);
+    }
+
     @Override
     public void renderFooter(List<Span> spans) {
         if (renderGotoNodeFooter(spans)) {
@@ -716,6 +738,7 @@ class DiagramTab extends AbstractDiagramTab {
             } else {
                 diagram.renderFooterHints(spans);
             }
+            ChaosHere.hint(spans, ctx.findSelectedIntegration());
         }
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index c2a5483c2a0a..b8b8135cc2b8 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -119,6 +119,10 @@ class EndpointsTab extends AbstractTableTab {
 
     @Override
     protected boolean handleTabKeyEvent(KeyEvent ke) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (ke.isChar('!') && ChaosHere.handleKey(ke, ctx, info, 
ChaosHere.forEndpoint(selectedEndpoint(info), info))) {
+            return true;
+        }
         if (ke.isKey(KeyCode.TAB) && panelMode == PANEL_DETAIL) {
             detailFocused = !detailFocused;
             return true;
@@ -418,6 +422,19 @@ class EndpointsTab extends AbstractTableTab {
         if (panelMode == PANEL_DETAIL) {
             hint(spans, "Tab", detailFocused ? "table" : "detail");
         }
+        ChaosHere.hint(spans, ctx.findSelectedIntegration());
+    }
+
+    /** The endpoint selected in the table, as filtered and sorted. */
+    private EndpointInfo selectedEndpoint(IntegrationInfo info) {
+        Integer sel = tableState.selected();
+        if (info == null || sel == null) {
+            return null;
+        }
+        List<EndpointInfo> sorted = new ArrayList<>(info.endpoints);
+        sorted.removeIf(ep -> !matchesFilter(ep));
+        sorted.sort(this::sortEndpoint);
+        return sel >= 0 && sel < sorted.size() ? sorted.get(sel) : null;
     }
 
     int getFilter() {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index 28cda19a146b..d89ad2bb68bf 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
 import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -91,6 +92,8 @@ class MonitorContext {
     /** Collector behind the Ollama tab and the tui_get_ollama tool; set by 
CamelMonitor at startup. */
     OllamaMonitor ollamaMonitor;
     BiConsumer<String, Boolean> notificationCallback;
+    // opens the Chaos screen with the known rules for a target ({@code !} on 
an endpoint, a consumer or a diagram node)
+    Consumer<ChaosHere.Target> chaosHere;
     BiConsumer<String, String> openMarkdownCallback;
     /** Opens markdown in the doc viewer scrolled to a heading: title, 
markdown, heading (may be null). */
     MarkdownOpener openMarkdownAtCallback;
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 807a2740071c..e8bf01021d52 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
@@ -166,10 +166,7 @@ class TabRegistry {
                 ctx, dataService.metrics(), dataService.stoppingPids(),
                 resetIntegrationTabState);
 
-        sqlTraceTab.setEditSqlAction(sql -> {
-            selectMoreTab(moreTabIndex("SQL Query"));
-            sqlQueryTab.setInputValue("sql", sql);
-        });
+        wireCrossTabActions(ctx);
 
         // Single source of truth for the More submenu: icon, programmatic 
name, mnemonic label, tab instance, and group.
         // Tabs are ordered by group (Routing, Observability, AI, Data, JVM, 
Project), alphabetically within each group.
@@ -332,6 +329,18 @@ class TabRegistry {
         return true;
     }
 
+    /** The actions of a tab that open another tab: SQL Trace edits its query 
in SQL Query, ! opens Chaos. */
+    private void wireCrossTabActions(MonitorContext ctx) {
+        ctx.chaosHere = target -> {
+            selectMoreTab(moreTabIndex("Chaos"));
+            chaosTab.openAt(target);
+        };
+        sqlTraceTab.setEditSqlAction(sql -> {
+            selectMoreTab(moreTabIndex("SQL Query"));
+            sqlQueryTab.setInputValue("sql", sql);
+        });
+    }
+
     void selectMoreTab(int index) {
         callbacks.selectMorePopupEntry(index);
         activeMoreTab = index >= 0 && index < moreTabs.size() ? 
moreTabs.get(index).tab() : null;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
index 456b4aa87446..47bbbdb4d9fc 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
@@ -72,3 +72,16 @@ the rule as `camel.chaos.rules` is checked: a rule that is 
not valid comes
 back with the reason, and a rule that matches nothing in the integration
 (often a typo) with a warning. Press **e** in that dialog to change the
 rule.
+
+## Chaos from where you look
+
+Press **!** on the Endpoint tab, the Consumers tab or a node of the
+Diagram to open the known rules for what is selected, with it filled in:
+
+- a `to` (or an endpoint the routes send to): the faults of that endpoint,
+  such as an HTTP status for an HTTP endpoint, and the faults of the step;
+- the `from` of a route, a consumer, or an endpoint a route consumes: the
+  route stalls, flaps or gets corrupt messages, and the polls fail when it
+  polls.
+
+Only when chaos is on in the integration.
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosHereTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosHereTest.java
new file mode 100644
index 000000000000..f53b4a8608b9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosHereTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+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;
+
+/**
+ * Chaos from where you look ({@link ChaosHere}): the target of {@code !} on 
an endpoint, a consumer and a node of the
+ * diagram, and the known rules the Chaos screen offers for it, filled in.
+ */
+class ChaosHereTest {
+
+    private IntegrationInfo info;
+    private MonitorContext ctx;
+    private ChaosRecipes recipes;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        Theme.resetForTesting();
+        info = ChaosRecipesTest.chaosdemo();
+        info.chaos = ChaosInfo.parse((JsonObject) Jsoner.deserialize("""
+                {"enabled": true, "rules": [], "report": {"verdict": "PASS", 
"final": true, "seed": 42}}
+                """));
+        info.producers.get(0).stepId = "callStock";
+        ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new 
AtomicReference<>(List.of()));
+        ctx.selectedPid = "1234";
+        recipes = ChaosRecipes.parse(ChaosRecipesTest.MODEL);
+    }
+
+    private static List<String> ids(ChaosRecipeForm form) {
+        return form.recipes().stream().map(ChaosRecipes.Recipe::id).toList();
+    }
+
+    @Test
+    void theTargetOfAnEndpointAConsumerAndADiagramNode() {
+        EndpointInfo out = new EndpointInfo();
+        out.uri = "http://localhost:8765/stock?bridgeEndpoint=true";;
+        out.direction = "out";
+        ChaosHere.Target send = ChaosHere.forEndpoint(out, info);
+        assertThat(send.endpoint()).isEqualTo("http://localhost:8765/stock";);
+        
assertThat(send.rulePrefix()).isEqualTo("endpoint=http://localhost:8765/stock 
");
+
+        EndpointInfo in = new EndpointInfo();
+        in.uri = "file://inbox?delay=1000";
+        in.direction = "in";
+        in.routeId = "poller";
+        ChaosHere.Target from = ChaosHere.forEndpoint(in, info);
+        assertThat(from.route()).isEqualTo("poller");
+        assertThat(from.polled()).isEqualTo("file://inbox");
+
+        ConsumerInfo timer = info.consumers.get(0);
+        timer.id = "orders";
+        ChaosHere.Target route = ChaosHere.forConsumer(timer);
+        assertThat(route.route()).isEqualTo("orders");
+        assertThat(route.polled()).isNull();
+
+        // a to of the diagram: its step, and the endpoint it sends to (the 
producer of the step)
+        ChaosHere.Target node = ChaosHere.forDiagramNode("callStock", "to", 
null, "orders", info);
+        assertThat(node.step()).isEqualTo("callStock");
+        assertThat(node.endpoint()).isEqualTo("http://localhost:8765/stock";);
+        assertThat(node.route()).isNull();
+        assertThat(node.label()).isEqualTo("to callStock 
(http://localhost:8765/stock)");
+        // the uri of the node, when the diagram has it
+        assertThat(ChaosHere.forDiagramNode("toShipping", "to", 
"seda://shipping?size=10", "orders", info).endpoint())
+                .isEqualTo("seda://shipping");
+        // the from of a route is the route
+        assertThat(ChaosHere.forDiagramNode(null, "from", null, "orders", 
info).route()).isEqualTo("orders");
+    }
+
+    @Test
+    void theKnownRulesThatFitTheTargetWithItFilledIn() {
+        ChaosHere.Target http = new ChaosHere.Target("to stock", 
"http://localhost:8765/stock";, null, null, null);
+        ChaosRecipeForm form = new ChaosRecipeForm(recipes, info, http);
+        // the HTTP status and the missing header fit an HTTP endpoint; the 
route and step recipes do not
+        assertThat(ids(form)).containsExactly("http-error", "missing-header");
+
+        ChaosHere.Target seda = new ChaosHere.Target("to shipping", 
"seda://shipping", null, null, null);
+        // no HTTP status for seda
+        assertThat(ids(new ChaosRecipeForm(recipes, info, 
seda))).containsExactly("missing-header");
+
+        ChaosHere.Target route = new ChaosHere.Target("route orders", null, 
null, "orders", null);
+        assertThat(ids(new ChaosRecipeForm(recipes, info, 
route))).containsExactly("stall", "corrupt-entry");
+
+        ChaosHere.Target polled = new ChaosHere.Target("route poller", null, 
"file://inbox", "poller", null);
+        assertThat(ids(new ChaosRecipeForm(recipes, info, 
polled))).containsExactly("stall", "corrupt-entry",
+                "poll-error");
+
+        ChaosHere.Target step = new ChaosHere.Target("to callStock", 
"http://localhost:8765/stock";, null, null, "callStock");
+        assertThat(ids(new ChaosRecipeForm(recipes, info, 
step))).containsExactly("http-error", "missing-header",
+                "step-error");
+    }
+
+    @Test
+    void bangOnTheEndpointTabOpensTheChaosScreenForTheEndpoint() {
+        EndpointInfo out = new EndpointInfo();
+        out.uri = "http://localhost:8765/stock";;
+        out.direction = "out";
+        info.endpoints.add(out);
+        List<ChaosHere.Target> opened = new ArrayList<>();
+        ctx.chaosHere = opened::add;
+        EndpointsTab endpoints = new EndpointsTab(ctx, new MetricsCollector());
+        TuiTestHelper.renderToString(endpoints, 160, 30);
+        endpoints.navigateDown();
+        endpoints.handleKeyEvent(KeyEvent.ofChar('!', KeyModifiers.NONE));
+
+        assertThat(opened).hasSize(1);
+        
assertThat(opened.get(0).endpoint()).isEqualTo("http://localhost:8765/stock";);
+        List<Span> footer = new ArrayList<>();
+        endpoints.renderFooter(footer);
+        assertThat(footer.stream().map(Span::content).reduce("", 
String::concat)).contains("chaos");
+
+        // without chaos there is no !
+        info.chaos = null;
+        endpoints.handleKeyEvent(KeyEvent.ofChar('!', KeyModifiers.NONE));
+        assertThat(opened).hasSize(1);
+    }
+
+    @Test
+    void theChaosScreenOpensTheFormFilledInForTheTarget() {
+        List<JsonObject> requests = new ArrayList<>();
+        ctx.confirmActions = true;
+        ChaosTab tab = new ChaosTab(ctx, new ChaosControl(ctx, request -> {
+            requests.add(request);
+            return new JsonObject();
+        }, () -> recipes));
+        tab.openAt(new ChaosHere.Target("route orders", null, null, "orders", 
null));
+        assertThat(TuiTestHelper.renderToString(tab, 160, 40)).contains("Add a 
chaos rule to chaosdemo at route orders")
+                .contains("The route stalls").doesNotContain("Returns an 
error");
+
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+        tab.handleKeyEvent(KeyEvent.ofChar('y', KeyModifiers.NONE));
+        assertThat(requests.get(0).getString("rule")).isEqualTo("route=orders 
stall duration=20s");
+    }
+
+    @Test
+    void withoutAKnownRuleForTheTargetTheRuleIsTypedFromIt() {
+        ChaosTab tab = new ChaosTab(ctx, new ChaosControl(ctx, request -> new 
JsonObject(), () -> recipes));
+        // no recipe has a placeholder for a component pattern: the typed line 
starts with the target
+        tab.openAt(new ChaosHere.Target("to log", "log:audit", null, null, 
null));
+        assertThat(tab.control().step()).isEqualTo(ChaosControl.Step.RECIPES);
+        
assertThat(ids(tab.control().form())).containsExactly("missing-header");
+
+        ChaosTab plain = new ChaosTab(ctx, new ChaosControl(ctx, request -> 
new JsonObject()));
+        plain.openAt(new ChaosHere.Target("step enrich", null, null, null, 
"enrich"));
+        assertThat(plain.control().step()).isEqualTo(ChaosControl.Step.INPUT);
+        assertThat(TuiTestHelper.renderToString(plain, 160, 
40)).contains("step=enrich");
+    }
+}

Reply via email to