This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 6ac7d011d2 Drill down on running workflow/pipeline, fixes #5304 (#6523)
6ac7d011d2 is described below

commit 6ac7d011d2c4cc64ec1dd47eb88f790c6b832580
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Feb 10 10:43:23 2026 +0100

    Drill down on running workflow/pipeline, fixes #5304 (#6523)
---
 assemblies/debug/pom.xml                           |  12 +
 .../hop/pipeline/transform/ITransformMeta.java     |  11 +
 .../org/apache/hop/workflow/action/IAction.java    |  11 +
 .../workflow/actions/pipeline/ActionPipeline.java  |   5 +
 .../apache/hop/workflow/actions/repeat/Repeat.java |   5 +
 .../workflow/actions/workflow/ActionWorkflow.java  |   5 +
 .../workflow/messages/messages_en_US.properties    |   2 +-
 .../kafka/consumer/KafkaConsumerInputMeta.java     |   5 +
 .../consumer/messages/messages_en_US.properties    |   5 +-
 .../transforms/mapping/SimpleMappingMeta.java      |   5 +
 .../pipelineexecutor/PipelineExecutorMeta.java     |   5 +
 .../messages/messages_en_US.properties             |   2 +-
 .../workflowexecutor/WorkflowExecutorMeta.java     |   5 +
 .../org/apache/hop/ui/hopgui/HopWebEntryPoint.java |   3 +
 .../main/java/org/apache/hop/ui/hopgui/HopGui.java |   1 +
 .../ui/hopgui/file/pipeline/HopGuiLogBrowser.java  |   9 +
 .../hopgui/file/pipeline/HopGuiPipelineGraph.java  | 191 +++++-----
 .../ui/hopgui/file/shared/DrillDownGuiPlugin.java  | 415 +++++++++++++++++++++
 .../DrillDownRegistrationExtensionPoint.java       |  50 +++
 ...rillDownWorkflowRegistrationExtensionPoint.java |  49 +++
 .../file/shared/PipelineRowSamplerHelper.java      | 146 ++++++++
 .../hopgui/file/workflow/HopGuiWorkflowGraph.java  |  60 ++-
 .../delegates/HopGuiWorkflowLogDelegate.java       |   3 +-
 .../file/shared/messages/messages_en_US.properties |  23 ++
 ui/src/main/resources/ui/images/running.svg        |   1 +
 25 files changed, 915 insertions(+), 114 deletions(-)

diff --git a/assemblies/debug/pom.xml b/assemblies/debug/pom.xml
index 2099ff3d13..2fea07e0f7 100644
--- a/assemblies/debug/pom.xml
+++ b/assemblies/debug/pom.xml
@@ -421,6 +421,12 @@
             <version>${project.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.hop</groupId>
+            <artifactId>hop-transform-addsequence</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.hop</groupId>
             <artifactId>hop-transform-blockuntiltransformsfinish</artifactId>
@@ -481,6 +487,12 @@
             <version>${project.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.hop</groupId>
+            <artifactId>hop-transform-rowstoresult</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.hop</groupId>
             <artifactId>hop-transform-selectvalues</artifactId>
diff --git 
a/engine/src/main/java/org/apache/hop/pipeline/transform/ITransformMeta.java 
b/engine/src/main/java/org/apache/hop/pipeline/transform/ITransformMeta.java
index 4eb16f37d5..2b40b8bcdd 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/transform/ITransformMeta.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/transform/ITransformMeta.java
@@ -533,4 +533,15 @@ public interface ITransformMeta {
       return 
PluginRegistry.getInstance().getPluginId(TransformPluginType.class, object);
     }
   }
+
+  /**
+   * Returns whether this transform supports drill-down functionality to view 
executing
+   * sub-pipelines or sub-workflows.
+   *
+   * @return true if this transform executes pipelines/workflows that can be 
drilled into, false
+   *     otherwise (default)
+   */
+  default boolean supportsDrillDown() {
+    return false;
+  }
 }
diff --git a/engine/src/main/java/org/apache/hop/workflow/action/IAction.java 
b/engine/src/main/java/org/apache/hop/workflow/action/IAction.java
index 1caee2adbb..c8e3f2dd9d 100644
--- a/engine/src/main/java/org/apache/hop/workflow/action/IAction.java
+++ b/engine/src/main/java/org/apache/hop/workflow/action/IAction.java
@@ -436,4 +436,15 @@ public interface IAction extends IVariables, 
IHasLogChannel, ICheckResultSource,
     throw new UnsupportedOperationException(
         "Attempted access of parent workflow metadata is not supported by the 
default IAction implementation");
   }
+
+  /**
+   * Returns whether this action supports drill-down functionality to view 
executing sub-pipelines
+   * or sub-workflows.
+   *
+   * @return true if this action executes pipelines/workflows that can be 
drilled into, false
+   *     otherwise (default)
+   */
+  default boolean supportsDrillDown() {
+    return false;
+  }
 }
diff --git 
a/plugins/actions/pipeline/src/main/java/org/apache/hop/workflow/actions/pipeline/ActionPipeline.java
 
b/plugins/actions/pipeline/src/main/java/org/apache/hop/workflow/actions/pipeline/ActionPipeline.java
index 74deb5f4c4..28acf63f59 100644
--- 
a/plugins/actions/pipeline/src/main/java/org/apache/hop/workflow/actions/pipeline/ActionPipeline.java
+++ 
b/plugins/actions/pipeline/src/main/java/org/apache/hop/workflow/actions/pipeline/ActionPipeline.java
@@ -971,4 +971,9 @@ public class ActionPipeline extends ActionBase implements 
Cloneable, IAction {
   public void setClearResultFiles(boolean clearResultFiles) {
     this.clearResultFiles = clearResultFiles;
   }
+
+  @Override
+  public boolean supportsDrillDown() {
+    return true;
+  }
 }
diff --git 
a/plugins/actions/repeat/src/main/java/org/apache/hop/workflow/actions/repeat/Repeat.java
 
b/plugins/actions/repeat/src/main/java/org/apache/hop/workflow/actions/repeat/Repeat.java
index 15d67bb156..97ae52a2b5 100644
--- 
a/plugins/actions/repeat/src/main/java/org/apache/hop/workflow/actions/repeat/Repeat.java
+++ 
b/plugins/actions/repeat/src/main/java/org/apache/hop/workflow/actions/repeat/Repeat.java
@@ -609,4 +609,9 @@ public class Repeat extends ActionBase implements IAction, 
Cloneable {
   public String getFilename() {
     return filename;
   }
+
+  @Override
+  public boolean supportsDrillDown() {
+    return true;
+  }
 }
diff --git 
a/plugins/actions/workflow/src/main/java/org/apache/hop/workflow/actions/workflow/ActionWorkflow.java
 
b/plugins/actions/workflow/src/main/java/org/apache/hop/workflow/actions/workflow/ActionWorkflow.java
index d80b974401..29807ab860 100644
--- 
a/plugins/actions/workflow/src/main/java/org/apache/hop/workflow/actions/workflow/ActionWorkflow.java
+++ 
b/plugins/actions/workflow/src/main/java/org/apache/hop/workflow/actions/workflow/ActionWorkflow.java
@@ -955,4 +955,9 @@ public class ActionWorkflow extends ActionBase implements 
Cloneable, IAction {
   public void setSetAppendLogfile(boolean setAppendLogfile) {
     this.setAppendLogfile = setAppendLogfile;
   }
+
+  @Override
+  public boolean supportsDrillDown() {
+    return true;
+  }
 }
diff --git 
a/plugins/actions/workflow/src/main/resources/org/apache/hop/workflow/actions/workflow/messages/messages_en_US.properties
 
b/plugins/actions/workflow/src/main/resources/org/apache/hop/workflow/actions/workflow/messages/messages_en_US.properties
index 95a2ee990a..ba6a0a1430 100644
--- 
a/plugins/actions/workflow/src/main/resources/org/apache/hop/workflow/actions/workflow/messages/messages_en_US.properties
+++ 
b/plugins/actions/workflow/src/main/resources/org/apache/hop/workflow/actions/workflow/messages/messages_en_US.properties
@@ -42,4 +42,4 @@ ActionWorkflowError.Recursive=Endless loop detected\: A 
Action in this workflow
 ActionWorkflowDialog.FilenameMissing.Header=Warning
 ActionWorkflowDialog.FilenameMissing.Message=The workflow filename is missing
 ActionWorkflowDialog.SelfReference.Header=Warning
-ActionWorkflowDialog.SelfReference.Message=This workflow can''t run itself. 
Please select another workflow to run.
\ No newline at end of file
+ActionWorkflowDialog.SelfReference.Message=This workflow can''t run itself. 
Please select another workflow to run.
diff --git 
a/plugins/transforms/kafka/src/main/java/org/apache/hop/pipeline/transforms/kafka/consumer/KafkaConsumerInputMeta.java
 
b/plugins/transforms/kafka/src/main/java/org/apache/hop/pipeline/transforms/kafka/consumer/KafkaConsumerInputMeta.java
index 5a15f9af20..f5f9f7c608 100644
--- 
a/plugins/transforms/kafka/src/main/java/org/apache/hop/pipeline/transforms/kafka/consumer/KafkaConsumerInputMeta.java
+++ 
b/plugins/transforms/kafka/src/main/java/org/apache/hop/pipeline/transforms/kafka/consumer/KafkaConsumerInputMeta.java
@@ -560,4 +560,9 @@ public class KafkaConsumerInputMeta
   public boolean supportsErrorHandling() {
     return true;
   }
+
+  @Override
+  public boolean supportsDrillDown() {
+    return true;
+  }
 }
diff --git 
a/plugins/transforms/kafka/src/main/resources/org/apache/hop/pipeline/transforms/kafka/consumer/messages/messages_en_US.properties
 
b/plugins/transforms/kafka/src/main/resources/org/apache/hop/pipeline/transforms/kafka/consumer/messages/messages_en_US.properties
index 779a70e137..423440ead2 100644
--- 
a/plugins/transforms/kafka/src/main/resources/org/apache/hop/pipeline/transforms/kafka/consumer/messages/messages_en_US.properties
+++ 
b/plugins/transforms/kafka/src/main/resources/org/apache/hop/pipeline/transforms/kafka/consumer/messages/messages_en_US.properties
@@ -75,11 +75,8 @@ KafkaConsumerInputMeta.UnableToCreateValueType=Unable to 
create output field val
 KafkaConsumerInputDialog.FilenameMissing.Header=Warning
 KafkaConsumerInputDialog.FilenameMissing.Message=The Kafka pipeline filename 
is missing.
 KafkaConsumerInputDialog.SelfReference.Header=Warning
-KafkaConsumerInputDialog.SelfReference.Message=This pipeline can''t be used as 
the Kafka pipeline. Please choose another Kafka pipeline. 
-
+KafkaConsumerInputDialog.SelfReference.Message=This pipeline can''t be used as 
the Kafka pipeline. Please choose another Kafka pipeline.
 KafkaConsumerInputDialog.Location.Label = Execution Information Location
 KafkaConsumerInputDialog.Location.Tooltip = The location to send execution 
information to
 KafkaConsumerInputDialog.Profile.Label = Execution Data Profile
 KafkaConsumerInputDialog.Profile.Tooltip = Method to use to gather data 
profiling in the pipeline
-
-
diff --git 
a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingMeta.java
 
b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingMeta.java
index d0de812e4b..985d2d78a3 100644
--- 
a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingMeta.java
+++ 
b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingMeta.java
@@ -384,4 +384,9 @@ public class SimpleMappingMeta extends 
TransformWithMappingMeta<SimpleMapping, S
   public void setIoMappings(IOMappings ioMappings) {
     this.ioMappings = ioMappings;
   }
+
+  @Override
+  public boolean supportsDrillDown() {
+    return true;
+  }
 }
diff --git 
a/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMeta.java
 
b/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMeta.java
index 2038d736f1..dbcd84db0e 100644
--- 
a/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMeta.java
+++ 
b/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMeta.java
@@ -632,4 +632,9 @@ public class PipelineExecutorMeta
     }
     return hasChanged;
   }
+
+  @Override
+  public boolean supportsDrillDown() {
+    return true;
+  }
 }
diff --git 
a/plugins/transforms/pipelineexecutor/src/main/resources/org/apache/hop/pipeline/transforms/pipelineexecutor/messages/messages_en_US.properties
 
b/plugins/transforms/pipelineexecutor/src/main/resources/org/apache/hop/pipeline/transforms/pipelineexecutor/messages/messages_en_US.properties
index d4840d38f6..9b6df349db 100644
--- 
a/plugins/transforms/pipelineexecutor/src/main/resources/org/apache/hop/pipeline/transforms/pipelineexecutor/messages/messages_en_US.properties
+++ 
b/plugins/transforms/pipelineexecutor/src/main/resources/org/apache/hop/pipeline/transforms/pipelineexecutor/messages/messages_en_US.properties
@@ -90,4 +90,4 @@ PipelineExecutorMeta.ValueMetaInterfaceCreation=Could not 
create ValueMetaInterf
 PipelineExecutorDialog.FilenameMissing.Header=Warning
 PipelineExecutorDialog.FilenameMissing.Message=The pipeline filename to 
execute is empty.
 PipelineExecutorDialog.SelfReference.Header=Warning
-PipelineExecutorDialog.SelfReference.Message=This pipeline can''t execute 
itself. Please choose another pipeline.
\ No newline at end of file
+PipelineExecutorDialog.SelfReference.Message=This pipeline can''t execute 
itself. Please choose another pipeline.
diff --git 
a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java
 
b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java
index c49ff9c0d9..527199e2d2 100644
--- 
a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java
+++ 
b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java
@@ -744,4 +744,9 @@ public class WorkflowExecutorMeta
     }
     return hasChanged;
   }
+
+  @Override
+  public boolean supportsDrillDown() {
+    return true;
+  }
 }
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
index 39ab75a41a..0ee7d8ea2b 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
@@ -22,6 +22,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.apache.hop.core.Const;
 import org.apache.hop.core.extension.ExtensionPointHandler;
 import org.apache.hop.core.extension.HopExtensionPoint;
 import org.apache.hop.core.gui.plugin.GuiRegistry;
@@ -41,6 +42,8 @@ public class HopWebEntryPoint extends AbstractEntryPoint {
 
   @Override
   protected void createContents(Composite parent) {
+    // So drill-down and other GUI checks can use 
Const.getHopPlatformRuntime() from any thread
+    System.setProperty(Const.HOP_PLATFORM_RUNTIME, "GUI");
     ResourceManager resourceManager = RWT.getResourceManager();
     JavaScriptLoader jsLoader = 
RWT.getClient().getService(JavaScriptLoader.class);
 
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index 1bb4dd60bb..7916529226 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -295,6 +295,7 @@ public class HopGui
   }
 
   private HopGui(Display display) {
+    System.setProperty(Const.HOP_PLATFORM_RUNTIME, "GUI");
     this.display = display;
     this.id = UUID.randomUUID().toString();
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiLogBrowser.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiLogBrowser.java
index 0c376a31fc..fa939b33db 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiLogBrowser.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiLogBrowser.java
@@ -276,4 +276,13 @@ public class HopGuiLogBrowser {
   public void setPaused(boolean paused) {
     this.paused.set(paused);
   }
+
+  /**
+   * Reset cached log channel state so the next refresh will use the current 
log channel provider
+   * (e.g. after attaching to a different running pipeline or workflow).
+   */
+  public void resetLogChannels() {
+    childIds = new ArrayList<>();
+    lastLogRegistryChange = null;
+  }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index d1b7e7a255..c4554a0185 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -25,7 +25,6 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -51,8 +50,6 @@ import org.apache.hop.core.action.GuiContextActionFilter;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.exception.HopMissingPluginsException;
 import org.apache.hop.core.exception.HopPluginException;
-import org.apache.hop.core.exception.HopTransformException;
-import org.apache.hop.core.exception.HopValueException;
 import org.apache.hop.core.exception.HopXmlException;
 import org.apache.hop.core.extension.ExtensionPointHandler;
 import org.apache.hop.core.extension.HopExtensionPoint;
@@ -121,7 +118,6 @@ import 
org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration.Sampl
 import org.apache.hop.pipeline.transform.IRowDistribution;
 import org.apache.hop.pipeline.transform.ITransformIOMeta;
 import org.apache.hop.pipeline.transform.ITransformMeta;
-import org.apache.hop.pipeline.transform.RowAdapter;
 import org.apache.hop.pipeline.transform.RowDistributionPluginType;
 import org.apache.hop.pipeline.transform.TransformErrorMeta;
 import org.apache.hop.pipeline.transform.TransformMeta;
@@ -176,8 +172,10 @@ import 
org.apache.hop.ui.hopgui.file.pipeline.delegates.HopGuiPipelineUndoDelega
 import 
org.apache.hop.ui.hopgui.file.pipeline.extension.HopGuiPipelineFinishedExtension;
 import 
org.apache.hop.ui.hopgui.file.pipeline.extension.HopGuiPipelineGraphExtension;
 import 
org.apache.hop.ui.hopgui.file.pipeline.extension.PipelineRenamedExtension;
+import org.apache.hop.ui.hopgui.file.shared.DrillDownGuiPlugin;
 import org.apache.hop.ui.hopgui.file.shared.HopGuiAbstractGraph;
 import org.apache.hop.ui.hopgui.file.shared.HopGuiTooltipExtension;
+import org.apache.hop.ui.hopgui.file.shared.PipelineRowSamplerHelper;
 import org.apache.hop.ui.hopgui.perspective.execution.ExecutionPerspective;
 import org.apache.hop.ui.hopgui.perspective.execution.IExecutionViewer;
 import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
@@ -4011,9 +4009,13 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
         return true;
       }
 
-      // Check if the file is saved. If not, ask for it to be stopped before 
closing
+      // Check if the file is saved. If not, ask for it to be stopped before 
closing.
+      // Only show for top-level runs; sub-pipelines are managed by their 
parent.
       //
-      if (pipeline != null && (pipeline.isRunning() || pipeline.isPaused())) {
+      if (pipeline != null
+          && (pipeline.isRunning() || pipeline.isPaused())
+          && pipeline.getParentPipeline() == null
+          && pipeline.getParentWorkflow() == null) {
         MessageBox messageDialog =
             new MessageBox(hopShell(), SWT.ICON_QUESTION | SWT.YES | SWT.NO | 
SWT.CANCEL);
         messageDialog.setText(
@@ -4310,6 +4312,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
           // Also make sure to clear the log entries in the central log store 
& registry
           //
           if (pipeline != null) {
+            DrillDownGuiPlugin.cleanupOnRunStart();
             HopLogStore.discardLines(pipeline.getLogChannelId(), true);
           }
 
@@ -4437,109 +4440,25 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
   }
 
   private void addRowsSamplerToPipeline(IPipelineEngine<PipelineMeta> 
pipeline) {
-
-    if (!(pipeline.getPipelineRunConfiguration().getEngineRunConfiguration()
-        instanceof LocalPipelineRunConfiguration)) {
-      return;
-    }
-    LocalPipelineRunConfiguration lprConfig =
-        (LocalPipelineRunConfiguration)
-            pipeline.getPipelineRunConfiguration().getEngineRunConfiguration();
-
-    if (StringUtils.isEmpty(lprConfig.getSampleTypeInGui())) {
-      return;
-    }
-
     try {
-      SampleType sampleType = 
SampleType.valueOf(lprConfig.getSampleTypeInGui());
-      if (sampleType == SampleType.None) {
-        return;
-      }
-
-      final int sampleSize = 
Const.toInt(pipeline.resolve(lprConfig.getSampleSize()), 100);
-      if (sampleSize <= 0) {
-        return;
-      }
-
       outputRowsMap = new HashMap<>();
-      final Random random = new Random();
-
-      for (final String transformName : pipelineMeta.getTransformNames()) {
-        IEngineComponent component = pipeline.findComponent(transformName, 0);
-        if (component != null) {
-          component.addRowListener(
-              new RowAdapter() {
-                int nrRows = 0;
-
-                @Override
-                public void rowWrittenEvent(IRowMeta rowMeta, Object[] row)
-                    throws HopTransformException {
-                  RowBuffer rowBuffer = outputRowsMap.get(transformName);
-                  if (rowBuffer == null) {
-                    rowBuffer = new RowBuffer(rowMeta);
-                    outputRowsMap.put(transformName, rowBuffer);
-
-                    // Linked list for faster adding and removing at the front 
and end of the list
-                    //
-                    if (sampleType == SampleType.Last) {
-                      rowBuffer.setBuffer(Collections.synchronizedList(new 
LinkedList<>()));
-                    } else {
-                      rowBuffer.setBuffer(Collections.synchronizedList(new 
ArrayList<>()));
-                    }
-                  }
-
-                  // Clone the row to make sure we capture the correct values
-                  //
-                  if (sampleType != SampleType.None) {
-                    try {
-                      row = rowMeta.cloneRow(row);
-                    } catch (HopValueException e) {
-                      throw new HopTransformException("Error copying row for 
preview purposes", e);
-                    }
-                  }
-
-                  switch (sampleType) {
-                    case First:
-                      {
-                        if (rowBuffer.size() < sampleSize) {
-                          rowBuffer.addRow(row);
-                        }
-                      }
-                      break;
-                    case Last:
-                      {
-                        rowBuffer.addRow(0, row);
-                        if (rowBuffer.size() > sampleSize) {
-                          rowBuffer.removeRow(rowBuffer.size() - 1);
-                        }
-                      }
-                      break;
-                    case Random:
-                      {
-                        // Reservoir sampling
-                        //
-                        nrRows++;
-                        if (rowBuffer.size() < sampleSize) {
-                          rowBuffer.addRow(row);
-                        } else {
-                          int randomIndex = random.nextInt(nrRows);
-                          if (randomIndex < sampleSize) {
-                            rowBuffer.setRow(randomIndex, row);
-                          }
-                        }
-                      }
-                      break;
-                    default:
-                      break;
-                  }
-                }
-              });
-        }
-      }
-
+      PipelineRowSamplerHelper.addRowSamplersToPipeline(pipeline, 
outputRowsMap);
     } catch (Exception e) {
-      // Ignore : simply not recognized or empty
+      // Ignore: run config not local or sample type not set
+    }
+  }
+
+  /**
+   * Attach data sniffers when we attach to a running pipeline but no central 
buffers exist (e.g.
+   * run config had sample type None at start). Uses the shared helper with 
the pipeline's run
+   * configuration.
+   */
+  private void addRowsSamplerToAttachedPipeline() {
+    if (pipeline == null) {
+      return;
     }
+    outputRowsMap = new HashMap<>();
+    PipelineRowSamplerHelper.addRowSamplersToPipeline(pipeline, outputRowsMap);
   }
 
   public void showSaveFileMessage() {
@@ -4586,6 +4505,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
         // Do we have a previous execution to clean up in the logging registry?
         //
         if (pipeline != null) {
+          DrillDownGuiPlugin.cleanupOnRunStart();
           HopLogStore.discardLines(pipeline.getLogChannelId(), false);
           
LoggingRegistry.getInstance().removeIncludingChildren(pipeline.getLogChannelId());
         }
@@ -4842,6 +4762,67 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     }
   }
 
+  /**
+   * Attach to a pipeline engine (running or finished) to display its 
execution state. This sets up
+   * all the necessary GUI components and listeners to monitor the pipeline or 
view its final state.
+   *
+   * @param runningPipeline The pipeline engine instance (running, finished, 
or stopped)
+   */
+  public void attachToRunningPipeline(IPipelineEngine<PipelineMeta> 
runningPipeline) {
+    if (runningPipeline == null) {
+      return;
+    }
+
+    // Set the pipeline instance
+    this.pipeline = runningPipeline;
+
+    // Add all the execution result tabs (logging, metrics, etc.)
+    addAllTabs();
+
+    // Use central sniffer buffers if we attached at pipeline start (so we see 
data even if tab
+    // wasn't open)
+    Map<String, RowBuffer> existingBuffers =
+        
DrillDownGuiPlugin.getDataSnifferBuffersForPipeline(runningPipeline.getLogChannelId());
+    if (existingBuffers != null) {
+      outputRowsMap = existingBuffers;
+    } else {
+      addRowsSamplerToAttachedPipeline();
+    }
+
+    // Force refresh of the log browser to use the new pipeline's log channel
+    // This ensures logs are filtered correctly for this specific pipeline
+    if (pipelineLogDelegate != null && pipelineLogDelegate.getLogBrowser() != 
null) {
+      pipelineLogDelegate.getLogBrowser().resetLogChannels();
+    }
+
+    // Check if pipeline is still running or already finished
+    boolean isRunning = runningPipeline.isRunning() || 
runningPipeline.isPreparing();
+    boolean isFinished = runningPipeline.isFinished();
+
+    if (isRunning) {
+      // Add listeners for when the pipeline finishes (only if still running)
+      pipeline.addExecutionFinishedListener(
+          p -> {
+            checkPipelineEnded();
+            checkErrorVisuals();
+            stopRedrawTimer();
+          });
+
+      pipeline.addExecutionStoppedListener(e -> pipelineStopped());
+
+      // Start the redraw timer to continuously update the GUI
+      startRedrawTimer();
+    } else if (isFinished) {
+      // Pipeline already finished, just show final state
+      checkPipelineEnded();
+      checkErrorVisuals();
+    }
+
+    // Trigger an immediate update
+    updateGui();
+    redraw();
+  }
+
   private void startRedrawTimer() {
 
     redrawTimer = new Timer("HopGuiPipelineGraph: redraw timer");
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
new file mode 100644
index 0000000000..5ef8525bb3
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
@@ -0,0 +1,415 @@
+/*
+ * 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.hop.ui.hopgui.file.shared;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.hop.core.action.GuiContextAction;
+import org.apache.hop.core.action.GuiContextActionFilter;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.action.GuiActionType;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.logging.LoggingRegistry;
+import org.apache.hop.core.row.RowBuffer;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engine.IPipelineEngine;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.dialog.MessageBox;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
+import 
org.apache.hop.ui.hopgui.file.pipeline.context.HopGuiPipelineTransformContext;
+import org.apache.hop.ui.hopgui.file.workflow.HopGuiWorkflowGraph;
+import 
org.apache.hop.ui.hopgui.file.workflow.context.HopGuiWorkflowActionContext;
+import org.apache.hop.ui.hopgui.perspective.TabItemHandler;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.action.ActionMeta;
+import org.apache.hop.workflow.engine.IWorkflowEngine;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Unified GUI Plugin for drill-down functionality. Works for ALL transforms 
and actions that
+ * support drill-down by checking the supportsDrillDown() method.
+ */
+@GuiPlugin
+public class DrillDownGuiPlugin {
+
+  private static final Class<?> PKG = DrillDownGuiPlugin.class;
+
+  // Global registries to track running instances
+  private static final Map<String, IPipelineEngine<PipelineMeta>> 
runningPipelines =
+      new ConcurrentHashMap<>();
+  private static final Map<String, IWorkflowEngine<WorkflowMeta>> 
runningWorkflows =
+      new ConcurrentHashMap<>();
+  public static final String DRILL_DOWN_ERROR_OPENING_EXECUTION =
+      "DrillDown.Error.OpeningExecution";
+  public static final String DRILL_DOWN_ERROR_TITLE = "DrillDown.Error.Title";
+
+  public static void registerRunningPipeline(
+      String logChannelId, IPipelineEngine<PipelineMeta> pipeline) {
+    runningPipelines.put(logChannelId, pipeline);
+  }
+
+  public static void registerRunningWorkflow(
+      String logChannelId, IWorkflowEngine<WorkflowMeta> workflow) {
+    runningWorkflows.put(logChannelId, workflow);
+  }
+
+  /**
+   * Clears all drill-down and sample-row state. Call when starting a new run 
to get a clean slate
+   * and avoid leaking memory.
+   */
+  public static void cleanupOnRunStart() {
+    runningPipelines.clear();
+    runningWorkflows.clear();
+    dataSnifferBuffersByLogChannelId.clear();
+  }
+
+  /**
+   * Per-run data sniffer buffers (logChannelId -> transform name -> 
RowBuffer). Filled when
+   * pipelines start so we have row data even if the pipeline tab is never 
opened. Only the latest
+   * run's data is kept per execution (each run has its own logChannelId).
+   */
+  private static final Map<String, Map<String, RowBuffer>> 
dataSnifferBuffersByLogChannelId =
+      new ConcurrentHashMap<>();
+
+  /**
+   * Attach row listeners to a pipeline at start so we capture output rows for 
debugging. Uses the
+   * pipeline's run configuration (sample type and size in GUI) when 
available; otherwise no
+   * sniffers are attached. Data is stored in {@link 
#dataSnifferBuffersByLogChannelId} so it is
+   * available when the user later opens the pipeline tab. Only applies to 
local pipeline engine.
+   */
+  public static void 
attachDataSniffersToPipeline(IPipelineEngine<PipelineMeta> pipeline) {
+    if (!(pipeline instanceof LocalPipelineEngine)) {
+      return;
+    }
+    String logChannelId = pipeline.getLogChannelId();
+    Map<String, RowBuffer> buffers =
+        dataSnifferBuffersByLogChannelId.computeIfAbsent(
+            logChannelId, k -> new ConcurrentHashMap<>());
+    PipelineRowSamplerHelper.addRowSamplersToPipeline(pipeline, buffers);
+  }
+
+  /**
+   * Return the data sniffer buffers for a pipeline run (if any). Used by the 
pipeline graph when
+   * attaching to a running/finished pipeline so the UI can show the rows that 
flowed through.
+   */
+  public static Map<String, RowBuffer> getDataSnifferBuffersForPipeline(String 
logChannelId) {
+    return dataSnifferBuffersByLogChannelId.get(logChannelId);
+  }
+
+  // ==================== TRANSFORM CONTEXT ====================
+
+  @GuiContextAction(
+      id = "pipeline-graph-transform-09990-open-execution",
+      parentId = HopGuiPipelineTransformContext.CONTEXT_ID,
+      type = GuiActionType.Info,
+      name = "i18n::DrillDown.OpenExecution.Name",
+      tooltip = "i18n::DrillDown.OpenExecution.Tooltip",
+      image = "ui/images/running.svg",
+      category = "Basic",
+      categoryOrder = "1")
+  public void openTransformExecution(HopGuiPipelineTransformContext context) {
+    TransformMeta transformMeta = context.getTransformMeta();
+    HopGuiPipelineGraph pipelineGraph = context.getPipelineGraph();
+
+    if (!transformMeta.getTransform().supportsDrillDown()) {
+      return;
+    }
+
+    try {
+      IPipelineEngine<?> parent = pipelineGraph.getPipeline();
+      if (parent == null) {
+        return;
+      }
+      findRunningChildAndOpen(
+          parent, transformMeta.getName(), pipelineGraph.getDisplay(), 
pipelineGraph.getHopGui());
+    } catch (Exception e) {
+      new ErrorDialog(
+          pipelineGraph.getShell(),
+          BaseMessages.getString(PKG, DRILL_DOWN_ERROR_TITLE),
+          BaseMessages.getString(PKG, DRILL_DOWN_ERROR_OPENING_EXECUTION),
+          e);
+    }
+  }
+
+  @GuiContextActionFilter(parentId = HopGuiPipelineTransformContext.CONTEXT_ID)
+  public boolean filterTransformDrillDown(
+      String contextActionId, HopGuiPipelineTransformContext context) {
+    if 
(contextActionId.equals("pipeline-graph-transform-09990-open-execution")) {
+      TransformMeta transformMeta = context.getTransformMeta();
+      return transformMeta.getTransform().supportsDrillDown()
+          && context.getPipelineGraph().getPipeline() != null;
+    }
+    return true;
+  }
+
+  // ==================== ACTION CONTEXT ====================
+
+  @GuiContextAction(
+      id = "workflow-graph-action-09990-open-execution",
+      parentId = HopGuiWorkflowActionContext.CONTEXT_ID,
+      type = GuiActionType.Info,
+      name = "i18n::DrillDown.OpenExecution.Name",
+      tooltip = "i18n::DrillDown.OpenExecution.Tooltip",
+      image = "ui/images/running.svg",
+      category = "Basic",
+      categoryOrder = "1")
+  public void openActionExecution(HopGuiWorkflowActionContext context) {
+    ActionMeta actionMeta = context.getActionMeta();
+    HopGuiWorkflowGraph workflowGraph = context.getWorkflowGraph();
+
+    if (!actionMeta.getAction().supportsDrillDown()) {
+      return;
+    }
+
+    try {
+      IWorkflowEngine<?> parent = workflowGraph.getWorkflow();
+      if (parent == null) {
+        return;
+      }
+      findRunningChildAndOpen(
+          parent, actionMeta.getName(), workflowGraph.getDisplay(), 
workflowGraph.getHopGui());
+    } catch (Exception e) {
+      new ErrorDialog(
+          workflowGraph.getShell(),
+          BaseMessages.getString(PKG, DRILL_DOWN_ERROR_TITLE),
+          BaseMessages.getString(PKG, DRILL_DOWN_ERROR_OPENING_EXECUTION),
+          e);
+    }
+  }
+
+  @GuiContextActionFilter(parentId = HopGuiWorkflowActionContext.CONTEXT_ID)
+  public boolean filterActionDrillDown(
+      String contextActionId, HopGuiWorkflowActionContext context) {
+    if (contextActionId.equals("workflow-graph-action-09990-open-execution")) {
+      ActionMeta actionMeta = context.getActionMeta();
+      return actionMeta.getAction().supportsDrillDown()
+          && context.getWorkflowGraph().getWorkflow() != null;
+    }
+    return true;
+  }
+
+  // ==================== HELPER METHODS ====================
+
+  /**
+   * Polls for a running child (pipeline or workflow) of the given parent, 
then opens a tab with
+   * meta from that engine and attaches. Runs in a background thread; UI 
updates are done via
+   * display.asyncExec. If no running child is found within the timeout, shows 
an info message.
+   */
+  private void findRunningChildAndOpen(
+      Object parentEngine, String childName, Display display, HopGui hopGui) {
+    new Thread(
+            () -> {
+              try {
+                for (int i = 0; i < 50; i++) {
+                  RunningChild child = findRunningChild(parentEngine, 
childName);
+                  if (child != null) {
+                    if (child.pipeline != null) {
+                      PipelineMeta meta = child.pipeline.getPipelineMeta();
+                      if (meta != null) {
+                        display.asyncExec(
+                            () -> openTabAndAttachPipeline(hopGui, meta, 
child.pipeline));
+                        return;
+                      }
+                    } else {
+                      WorkflowMeta meta = child.workflow.getWorkflowMeta();
+                      if (meta != null) {
+                        display.asyncExec(
+                            () -> openTabAndAttachWorkflow(hopGui, meta, 
child.workflow));
+                        return;
+                      }
+                    }
+                  }
+                  Thread.sleep(100);
+                }
+                display.asyncExec(() -> showNoRunningExecution(hopGui));
+              } catch (Exception e) {
+                // Ignore
+              }
+            })
+        .start();
+  }
+
+  /**
+   * Result of a single scan for a running child; exactly one of pipeline or 
workflow is non-null.
+   */
+  private static final class RunningChild {
+    final IPipelineEngine<PipelineMeta> pipeline;
+    final IWorkflowEngine<WorkflowMeta> workflow;
+
+    RunningChild(IPipelineEngine<PipelineMeta> pipeline, 
IWorkflowEngine<WorkflowMeta> workflow) {
+      this.pipeline = pipeline;
+      this.workflow = workflow;
+    }
+  }
+
+  private void openTabAndAttachPipeline(
+      HopGui hopGui, PipelineMeta meta, IPipelineEngine<PipelineMeta> 
toAttach) {
+    try {
+      ExplorerPerspective perspective =
+          
hopGui.getPerspectiveManager().findPerspective(ExplorerPerspective.class);
+      if (perspective == null) {
+        return;
+      }
+      HopGuiPipelineGraph pipelineGraph =
+          findOpenPipelineGraph(perspective, meta.getName(), 
meta.getFilename());
+      if (pipelineGraph == null) {
+        pipelineGraph = (HopGuiPipelineGraph) perspective.addPipeline(meta);
+      } else {
+        perspective.setActiveFileTypeHandler(pipelineGraph);
+      }
+      perspective.activate();
+      pipelineGraph.attachToRunningPipeline(toAttach);
+      if (!pipelineGraph.isExecutionResultsPaneVisible()) {
+        pipelineGraph.showExecutionResults();
+      }
+    } catch (HopException e) {
+      new ErrorDialog(
+          hopGui.getShell(),
+          BaseMessages.getString(PKG, DRILL_DOWN_ERROR_TITLE),
+          BaseMessages.getString(PKG, DRILL_DOWN_ERROR_OPENING_EXECUTION),
+          e);
+    }
+  }
+
+  private void openTabAndAttachWorkflow(
+      HopGui hopGui, WorkflowMeta meta, IWorkflowEngine<WorkflowMeta> 
toAttach) {
+    try {
+      ExplorerPerspective perspective =
+          
hopGui.getPerspectiveManager().findPerspective(ExplorerPerspective.class);
+      if (perspective == null) {
+        return;
+      }
+      HopGuiWorkflowGraph workflowGraph =
+          findOpenWorkflowGraph(perspective, meta.getName(), 
meta.getFilename());
+      if (workflowGraph == null) {
+        workflowGraph = (HopGuiWorkflowGraph) perspective.addWorkflow(meta);
+      } else {
+        perspective.setActiveFileTypeHandler(workflowGraph);
+      }
+      perspective.activate();
+      workflowGraph.attachToRunningWorkflow(toAttach);
+      if (!workflowGraph.isExecutionResultsPaneVisible()) {
+        workflowGraph.showExecutionResults();
+      }
+    } catch (HopException e) {
+      new ErrorDialog(
+          hopGui.getShell(),
+          BaseMessages.getString(PKG, DRILL_DOWN_ERROR_TITLE),
+          BaseMessages.getString(PKG, DRILL_DOWN_ERROR_OPENING_EXECUTION),
+          e);
+    }
+  }
+
+  private void showNoRunningExecution(HopGui hopGui) {
+    MessageBox mb = new MessageBox(hopGui.getShell(), SWT.OK | 
SWT.ICON_INFORMATION);
+    mb.setText(BaseMessages.getString(PKG, 
"DrillDown.NoRunningExecution.Title"));
+    mb.setMessage(BaseMessages.getString(PKG, 
"DrillDown.NoRunningExecution.Message"));
+    mb.open();
+  }
+
+  /**
+   * Single scan over parent's children: looks up each child in both pipeline 
and workflow maps,
+   * returns the one matching run (pipeline preferred over workflow, most 
recent by start time).
+   */
+  private RunningChild findRunningChild(Object parentEngine, String name) {
+    String parentLogChannelId;
+    if (parentEngine instanceof IPipelineEngine) {
+      parentLogChannelId = ((IPipelineEngine<?>) 
parentEngine).getLogChannelId();
+    } else if (parentEngine instanceof IWorkflowEngine) {
+      parentLogChannelId = ((IWorkflowEngine<?>) 
parentEngine).getLogChannelId();
+    } else {
+      return null;
+    }
+
+    LoggingRegistry registry = LoggingRegistry.getInstance();
+    List<String> childLogChannelIds = 
registry.getLogChannelChildren(parentLogChannelId);
+
+    List<IPipelineEngine<PipelineMeta>> pipelines = new ArrayList<>();
+    List<IWorkflowEngine<WorkflowMeta>> workflows = new ArrayList<>();
+    for (String logChannelId : childLogChannelIds) {
+      if (logChannelId.equals(parentLogChannelId)) {
+        continue;
+      }
+      IPipelineEngine<PipelineMeta> pipeline = 
runningPipelines.get(logChannelId);
+      if (pipeline != null && pipeline.getPipelineMeta() != null) {
+        ILoggingObject parent = pipeline.getParent();
+        if (name == null || (parent != null && 
name.equals(parent.getObjectName()))) {
+          pipelines.add(pipeline);
+        }
+      }
+      IWorkflowEngine<WorkflowMeta> workflow = 
runningWorkflows.get(logChannelId);
+      if (workflow != null && workflow.getWorkflowMeta() != null) {
+        ILoggingObject parent = workflow.getParent();
+        if (name == null || (parent != null && 
name.equals(parent.getObjectName()))) {
+          workflows.add(workflow);
+        }
+      }
+    }
+
+    pipelines.sort(
+        Comparator.comparing(
+            IPipelineEngine::getExecutionStartDate,
+            Comparator.nullsLast(Comparator.reverseOrder())));
+    workflows.sort(
+        Comparator.comparing(
+            IWorkflowEngine::getExecutionStartDate,
+            Comparator.nullsLast(Comparator.reverseOrder())));
+
+    if (!pipelines.isEmpty()) {
+      return new RunningChild(pipelines.get(0), null);
+    }
+    if (!workflows.isEmpty()) {
+      return new RunningChild(null, workflows.get(0));
+    }
+    return null;
+  }
+
+  private HopGuiPipelineGraph findOpenPipelineGraph(
+      ExplorerPerspective perspective, String name, String filename) {
+    for (TabItemHandler item : perspective.getItems()) {
+      if (item.getTypeHandler() instanceof HopGuiPipelineGraph graph
+          && (graph.getMeta().getName().equals(name)
+              || (filename != null && 
filename.equals(graph.getMeta().getFilename())))) {
+        return graph;
+      }
+    }
+    return null;
+  }
+
+  private HopGuiWorkflowGraph findOpenWorkflowGraph(
+      ExplorerPerspective perspective, String name, String filename) {
+    for (TabItemHandler item : perspective.getItems()) {
+      if (item.getTypeHandler() instanceof HopGuiWorkflowGraph graph
+          && (graph.getMeta().getName().equals(name)
+              || (filename != null && 
filename.equals(graph.getMeta().getFilename())))) {
+        return graph;
+      }
+    }
+    return null;
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownRegistrationExtensionPoint.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownRegistrationExtensionPoint.java
new file mode 100644
index 0000000000..ce52d1a45e
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownRegistrationExtensionPoint.java
@@ -0,0 +1,50 @@
+/*
+ * 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.hop.ui.hopgui.file.shared;
+
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.extension.ExtensionPoint;
+import org.apache.hop.core.extension.IExtensionPoint;
+import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engine.IPipelineEngine;
+
+/**
+ * Registers pipeline executions for drill-down support when the GUI is 
active. Skips registration
+ * when running headless (e.g. hop run, API) to avoid holding references and 
reduce memory use.
+ */
+@ExtensionPoint(
+    id = "DrillDownPipelineRegistrationExtensionPoint",
+    extensionPointId = "PipelineStartThreads",
+    description = "Registers pipeline executions for drill-down support when 
GUI is active")
+public class DrillDownRegistrationExtensionPoint
+    implements IExtensionPoint<IPipelineEngine<PipelineMeta>> {
+
+  @Override
+  public void callExtensionPoint(
+      ILogChannel log, IVariables variables, IPipelineEngine<PipelineMeta> 
pipeline)
+      throws HopException {
+    if (pipeline == null || 
!"GUI".equalsIgnoreCase(Const.getHopPlatformRuntime())) {
+      return;
+    }
+    DrillDownGuiPlugin.registerRunningPipeline(pipeline.getLogChannelId(), 
pipeline);
+    DrillDownGuiPlugin.attachDataSniffersToPipeline(pipeline);
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownWorkflowRegistrationExtensionPoint.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownWorkflowRegistrationExtensionPoint.java
new file mode 100644
index 0000000000..44c8e95cba
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownWorkflowRegistrationExtensionPoint.java
@@ -0,0 +1,49 @@
+/*
+ * 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.hop.ui.hopgui.file.shared;
+
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.extension.ExtensionPoint;
+import org.apache.hop.core.extension.IExtensionPoint;
+import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.engine.IWorkflowEngine;
+
+/**
+ * Registers workflow executions for drill-down support when the GUI is 
active. Skips registration
+ * when running headless (e.g. hop run, API) to avoid holding references and 
reduce memory use.
+ */
+@ExtensionPoint(
+    id = "DrillDownWorkflowRegistrationExtensionPoint",
+    extensionPointId = "WorkflowStart",
+    description = "Registers workflow executions for drill-down support when 
GUI is active")
+public class DrillDownWorkflowRegistrationExtensionPoint
+    implements IExtensionPoint<IWorkflowEngine<WorkflowMeta>> {
+
+  @Override
+  public void callExtensionPoint(
+      ILogChannel log, IVariables variables, IWorkflowEngine<WorkflowMeta> 
workflow)
+      throws HopException {
+    if (workflow == null || 
!"GUI".equalsIgnoreCase(Const.getHopPlatformRuntime())) {
+      return;
+    }
+    DrillDownGuiPlugin.registerRunningWorkflow(workflow.getLogChannelId(), 
workflow);
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/PipelineRowSamplerHelper.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/PipelineRowSamplerHelper.java
new file mode 100644
index 0000000000..f715214d89
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/PipelineRowSamplerHelper.java
@@ -0,0 +1,146 @@
+/*
+ * 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.hop.ui.hopgui.file.shared;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Random;
+import org.apache.commons.lang.StringUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowBuffer;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engine.IEngineComponent;
+import org.apache.hop.pipeline.engine.IPipelineEngine;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration;
+import 
org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration.SampleType;
+import org.apache.hop.pipeline.transform.RowAdapter;
+
+/**
+ * Helper to attach row listeners to a pipeline so output rows are sampled 
into a target map (e.g.
+ * for execution preview or drill-down). Uses the pipeline's run configuration 
(sample type and size
+ * in GUI).
+ */
+public final class PipelineRowSamplerHelper {
+
+  private PipelineRowSamplerHelper() {}
+
+  /**
+   * Attach row listeners to a pipeline so output rows are sampled into {@code 
targetMap} (transform
+   * name → RowBuffer). Uses the pipeline's run configuration (sample type and 
size in GUI). Does
+   * nothing if not a local pipeline, run config is missing, or sample type is 
None/empty.
+   *
+   * @param pipeline the pipeline engine (must be {@link LocalPipelineEngine})
+   * @param targetMap map to fill; transform name → RowBuffer (last N rows per 
transform)
+   */
+  public static void addRowSamplersToPipeline(
+      IPipelineEngine<PipelineMeta> pipeline, Map<String, RowBuffer> 
targetMap) {
+    if (pipeline == null || targetMap == null || !(pipeline instanceof 
LocalPipelineEngine)) {
+      return;
+    }
+    if (pipeline.getPipelineRunConfiguration() == null
+        || !(pipeline.getPipelineRunConfiguration().getEngineRunConfiguration()
+            instanceof LocalPipelineRunConfiguration)) {
+      return;
+    }
+    LocalPipelineRunConfiguration lprConfig =
+        (LocalPipelineRunConfiguration)
+            pipeline.getPipelineRunConfiguration().getEngineRunConfiguration();
+    if (StringUtils.isEmpty(lprConfig.getSampleTypeInGui())) {
+      return;
+    }
+    SampleType sampleType;
+    try {
+      sampleType = SampleType.valueOf(lprConfig.getSampleTypeInGui());
+    } catch (Exception e) {
+      return;
+    }
+    if (sampleType == SampleType.None) {
+      return;
+    }
+    final int sampleSize = 
Const.toInt(pipeline.resolve(lprConfig.getSampleSize()), 100);
+    if (sampleSize <= 0) {
+      return;
+    }
+    PipelineMeta meta = pipeline.getPipelineMeta();
+    if (meta == null) {
+      return;
+    }
+    final Random random = new Random();
+    for (final String transformName : meta.getTransformNames()) {
+      IEngineComponent component = pipeline.findComponent(transformName, 0);
+      if (component != null) {
+        component.addRowListener(
+            new RowAdapter() {
+              int nrRows = 0;
+
+              @Override
+              public void rowWrittenEvent(IRowMeta rowMeta, Object[] row)
+                  throws HopTransformException {
+                RowBuffer rowBuffer = targetMap.get(transformName);
+                if (rowBuffer == null) {
+                  rowBuffer = new RowBuffer(rowMeta);
+                  if (sampleType == SampleType.Last) {
+                    rowBuffer.setBuffer(Collections.synchronizedList(new 
LinkedList<>()));
+                  } else {
+                    rowBuffer.setBuffer(Collections.synchronizedList(new 
ArrayList<>()));
+                  }
+                  targetMap.put(transformName, rowBuffer);
+                }
+                try {
+                  row = rowMeta.cloneRow(row);
+                } catch (HopValueException e) {
+                  throw new HopTransformException("Error copying row for 
preview", e);
+                }
+                switch (sampleType) {
+                  case First:
+                    if (rowBuffer.size() < sampleSize) {
+                      rowBuffer.addRow(row);
+                    }
+                    break;
+                  case Last:
+                    rowBuffer.addRow(0, row);
+                    if (rowBuffer.size() > sampleSize) {
+                      rowBuffer.removeRow(rowBuffer.size() - 1);
+                    }
+                    break;
+                  case Random:
+                    nrRows++;
+                    if (rowBuffer.size() < sampleSize) {
+                      rowBuffer.addRow(row);
+                    } else {
+                      int randomIndex = random.nextInt(nrRows);
+                      if (randomIndex < sampleSize) {
+                        rowBuffer.setRow(randomIndex, row);
+                      }
+                    }
+                    break;
+                  default:
+                    break;
+                }
+              }
+            });
+      }
+    }
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index a3016922dc..5d7b66b7b0 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -119,6 +119,7 @@ import org.apache.hop.ui.hopgui.dialog.NotePadDialog;
 import org.apache.hop.ui.hopgui.file.IHopFileType;
 import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
 import org.apache.hop.ui.hopgui.file.delegates.HopGuiNotePadDelegate;
+import org.apache.hop.ui.hopgui.file.shared.DrillDownGuiPlugin;
 import org.apache.hop.ui.hopgui.file.shared.HopGuiAbstractGraph;
 import org.apache.hop.ui.hopgui.file.shared.HopGuiTooltipExtension;
 import 
org.apache.hop.ui.hopgui.file.workflow.context.HopGuiWorkflowActionContext;
@@ -3792,9 +3793,13 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
         return true;
       }
 
-      // Check if the file is saved. If not, ask for it to be stopped before 
closing
+      // Check if the file is saved. If not, ask for it to be stopped before 
closing.
+      // Only show for top-level runs; sub-workflows are managed by their 
parent.
       //
-      if (workflow != null && (workflow.isActive())) {
+      if (workflow != null
+          && workflow.isActive()
+          && workflow.getParentPipeline() == null
+          && workflow.getParentWorkflow() == null) {
         MessageBox messageDialog =
             new MessageBox(hopShell(), SWT.ICON_QUESTION | SWT.YES | SWT.NO | 
SWT.CANCEL);
         messageDialog.setText(
@@ -3898,6 +3903,7 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
           // store & registry
           //
           if (workflow != null) {
+            DrillDownGuiPlugin.cleanupOnRunStart();
             HopLogStore.discardLines(workflow.getLogChannelId(), true);
           }
 
@@ -4514,6 +4520,56 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
     return true;
   }
 
+  /**
+   * Attach to an already-running workflow engine to display its execution 
state. This sets up all
+   * the necessary GUI components and listeners to monitor the workflow.
+   *
+   * @param runningWorkflow The workflow engine instance that is already 
executing
+   */
+  public void attachToRunningWorkflow(IWorkflowEngine<WorkflowMeta> 
runningWorkflow) {
+    if (runningWorkflow == null) {
+      return;
+    }
+
+    // Set the workflow instance
+    this.setWorkflow(runningWorkflow);
+
+    // Add all the execution result tabs (logging, metrics, etc.)
+    addAllTabs();
+
+    // Force refresh of the log browser to use the new workflow's log channel
+    // This ensures logs are filtered correctly for this specific workflow
+    if (workflowLogDelegate != null && workflowLogDelegate.getLogBrowser() != 
null) {
+      workflowLogDelegate.getLogBrowser().resetLogChannels();
+    }
+
+    // Set the workflow tracker for the metrics/grid tab
+    if (workflowGridDelegate != null && workflow != null && 
workflow.getWorkflowTracker() != null) {
+      workflowGridDelegate.setWorkflowTracker(workflow.getWorkflowTracker());
+    }
+
+    // Check if workflow is still running or already finished
+    boolean isRunning = runningWorkflow.isActive() || 
runningWorkflow.isInitialized();
+    boolean isFinished = runningWorkflow.isFinished();
+
+    if (isRunning) {
+      // Add listeners for when the workflow finishes (only if still running)
+      workflow.addExecutionFinishedListener(e -> 
HopGuiWorkflowGraph.this.workflowFinished());
+
+      workflow.addExecutionStoppedListener(e -> 
HopGuiWorkflowGraph.this.workflowStopped());
+
+      // Start the redraw timer to continuously update the GUI
+      startRedrawTimer();
+    } else if (isFinished) {
+      // Workflow already finished, just show final state
+      workflowFinished();
+    }
+
+    // Trigger an immediate update
+    updateGui();
+    redraw();
+  }
+
   private void startRedrawTimer() {
     redrawTimer = new Timer("WorkflowGraph auto refresh: " + 
workflow.getWorkflowName());
     TimerTask timerTask =
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowLogDelegate.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowLogDelegate.java
index ca01523f0a..0d16dde3d2 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowLogDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowLogDelegate.java
@@ -18,6 +18,7 @@
 package org.apache.hop.ui.hopgui.file.workflow.delegates;
 
 import java.util.ArrayList;
+import lombok.Getter;
 import org.apache.commons.lang.StringUtils;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.Props;
@@ -79,7 +80,7 @@ public class HopGuiWorkflowLogDelegate {
   private Control toolbar;
   private GuiToolbarWidgets toolBarWidgets;
 
-  private HopGuiLogBrowser logBrowser;
+  @Getter private HopGuiLogBrowser logBrowser;
 
   /**
    * @param hopGui
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/file/shared/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/file/shared/messages/messages_en_US.properties
new file mode 100644
index 0000000000..ea8cce5e59
--- /dev/null
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/file/shared/messages/messages_en_US.properties
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+
+DrillDown.OpenExecution.Name=Open execution
+DrillDown.OpenExecution.Tooltip=Open the executed pipeline or workflow and 
attach its running execution state
+DrillDown.Error.Title=Error
+DrillDown.Error.OpeningExecution=Error opening execution for drill-down
+DrillDown.NoRunningExecution.Title=No running execution
+DrillDown.NoRunningExecution.Message=No running pipeline or workflow execution 
was found. The child may not have started yet, or it may have already finished.
diff --git a/ui/src/main/resources/ui/images/running.svg 
b/ui/src/main/resources/ui/images/running.svg
new file mode 100644
index 0000000000..d9a1c66c06
--- /dev/null
+++ b/ui/src/main/resources/ui/images/running.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"; height="24px" viewBox="0 -960 960 960" 
width="24px" fill="#1f1f1f"><path 
d="M480-480ZM370-80l-16-128q-13-5-24.5-12T307-235l-119 
50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 
23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-74 
56q-22-11-45-18.5T714-558l63-48-39-68-99 
42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 
23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 
99-42 [...]
\ No newline at end of file

Reply via email to