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