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 f872034407 issue #3222 : log view filter, highlight, exclude; 
execution log toolbar; fix tree filter (#7703)
f872034407 is described below

commit f8720344074427b84c6c53a9ba991af5aa572e90
Author: Matt Casters <[email protected]>
AuthorDate: Fri Jul 31 09:45:53 2026 +0200

    issue #3222 : log view filter, highlight, exclude; execution log toolbar; 
fix tree filter (#7703)
    
    Add free-text filter with highlight, match-case, and exclude options to
    pipeline and workflow log tabs, reuse the same controls on execution
    viewer log tabs, and fix the execution perspective tree filter so typed
    text is not wiped and Enter works reliably on GTK toolbars.
---
 .../hop/execution/DefaultExecutionSelector.java    |  16 +-
 .../execution/DefaultExecutionSelectorTest.java    |  22 ++
 .../apache/hop/ui/core/gui/GuiToolbarWidgets.java  |  32 ++
 .../ui/hopgui/file/pipeline/HopGuiLogBrowser.java  | 367 +++++++++++++------
 .../delegates/HopGuiPipelineLogDelegate.java       |  83 +++++
 .../delegates/HopGuiWorkflowLogDelegate.java       |  83 +++++
 .../perspective/execution/ExecutionLogPanel.java   | 401 +++++++++++++++++++++
 .../execution/ExecutionPerspective.java            | 128 +++++--
 .../execution/PipelineExecutionViewer.java         |   7 +-
 .../execution/WorkflowExecutionViewer.java         |   7 +-
 .../hop/ui/hopgui/shared/BaseExecutionViewer.java  |  14 +-
 .../ui/hopgui/messages/messages_en_US.properties   |   8 +
 12 files changed, 1010 insertions(+), 158 deletions(-)

diff --git 
a/engine/src/main/java/org/apache/hop/execution/DefaultExecutionSelector.java 
b/engine/src/main/java/org/apache/hop/execution/DefaultExecutionSelector.java
index 2d6077da2e..40ec89d116 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/DefaultExecutionSelector.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/DefaultExecutionSelector.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopException;
 
 public record DefaultExecutionSelector(
@@ -37,6 +38,9 @@ public record DefaultExecutionSelector(
   public static final SimpleDateFormat START_DATE_FORMAT = new 
SimpleDateFormat("yyyy/MM/dd HH:mm");
 
   public boolean isSelected(Execution execution) {
+    if (execution == null) {
+      return false;
+    }
     if (isSelectingWorkflows && 
!execution.getExecutionType().equals(ExecutionType.Workflow)) {
       return false;
     }
@@ -44,11 +48,14 @@ public record DefaultExecutionSelector(
       return false;
     }
     if (isSelectingByUuid()) {
-      return execution.getId().equalsIgnoreCase(filterText);
+      return StringUtils.isNotEmpty(execution.getId())
+          && execution.getId().equalsIgnoreCase(filterText);
     }
     if (StringUtils.isNotEmpty(filterText)) {
-      boolean match = 
execution.getName().toLowerCase().contains(filterText.toLowerCase());
-      match = match || execution.getId().contains(filterText);
+      String name = Const.NVL(execution.getName(), "");
+      String id = Const.NVL(execution.getId(), "");
+      boolean match = name.toLowerCase().contains(filterText.toLowerCase());
+      match = match || id.contains(filterText);
       if (execution.getExecutionStartDate() != null) {
         String startDateString = 
START_DATE_FORMAT.format(execution.getExecutionStartDate());
         match = match || startDateString.contains(filterText);
@@ -60,7 +67,8 @@ public record DefaultExecutionSelector(
     if (startDateFilter != null && execution.getExecutionStartDate() != null) {
       return startDateFilter.matches(execution.getExecutionStartDate());
     }
-    return false;
+    // No start date: only accept when there is no time-window constraint 
(NONE/null).
+    return startDateFilter == null || startDateFilter == LastPeriod.NONE;
   }
 
   public boolean isSelected(ExecutionState executionState) {
diff --git 
a/engine/src/test/java/org/apache/hop/execution/DefaultExecutionSelectorTest.java
 
b/engine/src/test/java/org/apache/hop/execution/DefaultExecutionSelectorTest.java
index 843f9850bd..9214c765d9 100644
--- 
a/engine/src/test/java/org/apache/hop/execution/DefaultExecutionSelectorTest.java
+++ 
b/engine/src/test/java/org/apache/hop/execution/DefaultExecutionSelectorTest.java
@@ -125,4 +125,26 @@ class DefaultExecutionSelectorTest {
     assertTrue(selector.isSelected(execution(UUID.randomUUID().toString(), 
null, new Date())));
     assertFalse(selector.isSelected(execution(UUID.randomUUID().toString(), 
null, daysAgo(1))));
   }
+
+  @Test
+  void nullNameAndIdDoNotThrowAndStillMatchOnOtherFields() {
+    Execution execution = new Execution();
+    execution.setId(null);
+    execution.setName(null);
+    execution.setExecutionType(ExecutionType.Pipeline);
+    execution.setExecutionStartDate(new Date());
+    // Free-text match against start date string is still possible; empty 
name/id must not NPE.
+    DefaultExecutionSelector selector = 
selectorWithFilter("no-such-substring-xyz");
+    assertFalse(selector.isSelected(execution));
+  }
+
+  @Test
+  void nullNameStillMatchesIdSubstring() {
+    Execution execution = new Execution();
+    execution.setId("abc-pipeline-id");
+    execution.setName(null);
+    execution.setExecutionType(ExecutionType.Pipeline);
+    execution.setExecutionStartDate(new Date());
+    assertTrue(selectorWithFilter("pipeline-id").isSelected(execution));
+  }
 }
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java
index 8c1234b6cf..c8a9687efa 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java
@@ -349,9 +349,37 @@ public class GuiToolbarWidgets extends BaseGuiWidgets 
implements IToolbarWidgetR
     Listener listener = getListener(toolbarItem);
     text.addListener(SWT.Selection, listener);
     text.addListener(SWT.DefaultSelection, listener);
+    addTextEnterKeyListener(text, listener);
     widgetsMap.put(toolbarItem.getId(), text);
   }
 
+  /**
+   * Fire the toolbar action when the user presses Enter in a single-line text 
field. Needed because
+   * {@link SWT#DefaultSelection} is not consistently delivered for Text 
widgets embedded in a
+   * toolbar on all platforms (notably GTK).
+   */
+  private static void addTextEnterKeyListener(Text text, Listener listener) {
+    if (text == null || listener == null) {
+      return;
+    }
+    text.addListener(
+        SWT.KeyDown,
+        event -> {
+          if (event.keyCode == SWT.CR || event.keyCode == SWT.KEYPAD_CR) {
+            listener.handleEvent(event);
+            event.doit = false;
+          }
+        });
+    text.addListener(
+        SWT.Traverse,
+        event -> {
+          if (event.detail == SWT.TRAVERSE_RETURN) {
+            listener.handleEvent(event);
+            event.doit = false;
+          }
+        });
+  }
+
   private void addWebToolbarCheckbox(GuiToolbarItem toolbarItem, Composite 
parent) {
     Button checkbox =
         new Button(parent, SWT.CHECK | (toolbarItem.isAlignRight() ? SWT.RIGHT 
: SWT.LEFT));
@@ -510,6 +538,10 @@ public class GuiToolbarWidgets extends BaseGuiWidgets 
implements IToolbarWidgetR
     Listener listener = getListener(toolbarItem);
     text.addListener(SWT.Selection, listener);
     text.addListener(SWT.DefaultSelection, listener);
+    // On Linux/GTK, DefaultSelection is unreliable for Text controls hosted 
in a ToolBar
+    // SEPARATOR. Explicit CR / KEYPAD_CR ensures Enter applies the filter 
(e.g. Execution
+    // perspective).
+    addTextEnterKeyListener(text, listener);
     toolItemMap.put(toolbarItem.getId(), textSeparator);
     widgetsMap.put(toolbarItem.getId(), text);
     PropsUi.setLook(text, Props.WIDGET_STYLE_TOOLBAR);
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 399b5b43d2..02e43a7728 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
@@ -42,7 +42,9 @@ import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.DescribedVariable;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.ui.core.ConstUi;
+import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.widget.OsHelper;
 import org.apache.hop.ui.core.widget.StyledTextVar;
 import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.hopgui.HopGui;
@@ -65,19 +67,94 @@ public class HopGuiLogBrowser {
   private Date lastLogRegistryChange;
   private AtomicBoolean paused;
   private final AtomicInteger lastLogId = new AtomicInteger(-1);
+
+  /** First log buffer id included in the current view (advanced when the user 
clears the log). */
+  private final AtomicInteger firstLogId = new AtomicInteger(-1);
+
   private final AtomicBoolean busy = new AtomicBoolean(false);
 
+  private final FixedWidthLogLayout logLayout = new FixedWidthLogLayout(true);
+
+  private volatile String filterText = "";
+  private volatile boolean highlightMatches;
+  private volatile boolean caseSensitive;
+  private volatile boolean excludeMatches;
+
   public HopGuiLogBrowser(final TextComposite text, final ILogParentProvided 
logProvider) {
     this.text = text;
     this.logProvider = logProvider;
     this.paused = new AtomicBoolean(false);
   }
 
-  public void installLogSniffer() {
+  /**
+   * Update log filter options used when appending and when rebuilding the 
view.
+   *
+   * @param text filter string; empty disables filtering
+   * @param highlight when true and filter is set, show all lines and 
highlight matches
+   * @param caseSensitive case-sensitive contains match when true
+   * @param exclude when true and filter is set, hide matching lines (wins 
over highlight)
+   */
+  public void setFilter(String text, boolean highlight, boolean caseSensitive, 
boolean exclude) {
+    this.filterText = Const.NVL(text, "").trim();
+    this.highlightMatches = highlight;
+    this.caseSensitive = caseSensitive;
+    this.excludeMatches = exclude;
+  }
 
-    // Create a new buffer appender to the log and capture that directly...
-    //
-    final FixedWidthLogLayout logLayout = new FixedWidthLogLayout(true);
+  /**
+   * Rebuild the log text widget from the central log buffer using the current 
filter settings.
+   * Called after the user changes the filter text or options.
+   */
+  public void refreshFilteredView() {
+    if (text == null || text.isDisposed()) {
+      return;
+    }
+
+    Runnable rebuild =
+        () -> {
+          if (text.isDisposed()) {
+            return;
+          }
+          busy.set(true);
+          try {
+            ensureChildIds();
+            int lastNr = HopLogStore.getLastBufferLineNr();
+            List<HopLoggingEvent> logLines =
+                HopLogStore.getLogBufferFromTo(childIds, false, 
firstLogId.get(), lastNr);
+
+            StyledText styledText = getStyledText();
+            synchronized (text) {
+              String initial = OsHelper.isMac() ? Const.CR : "";
+              if (styledText != null && !styledText.isDisposed()) {
+                styledText.setText(initial);
+              } else {
+                text.setText(initial);
+              }
+
+              for (HopLoggingEvent event : logLines) {
+                appendLogEvent(event, styledText);
+              }
+
+              trimToMaxSize(styledText);
+
+              if (!text.isDisposed()) {
+                text.setSelection(text.getCharCount());
+              }
+            }
+            lastLogId.set(lastNr);
+          } finally {
+            busy.set(false);
+          }
+        };
+
+    if (text.getDisplay().getThread() == Thread.currentThread()) {
+      rebuild.run();
+    } else {
+      text.getDisplay().asyncExec(rebuild);
+    }
+  }
+
+  public void installLogSniffer() {
 
     // Refresh the log every second or so
     //
@@ -107,18 +184,7 @@ public class HopGuiLogBrowser {
                         // It happens with slow writing of execution 
information to a location.
                         //
                         if (logChannel != null) {
-                          String parentLogChannelId = 
logChannel.getLogChannelId();
-                          LoggingRegistry registry = 
LoggingRegistry.getInstance();
-                          Date registryModDate = 
registry.getLastModificationTime();
-
-                          if (childIds == null
-                              || lastLogRegistryChange == null
-                              || 
registryModDate.compareTo(lastLogRegistryChange) > 0) {
-                            lastLogRegistryChange = 
registry.getLastModificationTime();
-                            childIds =
-                                LoggingRegistry.getInstance()
-                                    .getLogChannelChildren(parentLogChannelId);
-                          }
+                          ensureChildIds();
 
                           // See if we need to log any lines...
                           //
@@ -130,99 +196,19 @@ public class HopGuiLogBrowser {
                                 HopLogStore.getLogBufferFromTo(
                                     childIds, false, lastLogId.get(), lastNr);
 
-                            // The maximum size of the log buffer
-                            //
-                            int maxSize;
-                            DescribedVariable describedVariable =
-                                HopConfig.getInstance()
-                                    
.findDescribedVariable(Const.HOP_MAX_LOG_SIZE_IN_LINES);
-                            if (describedVariable == null) {
-                              maxSize = Const.MAX_NR_LOG_LINES;
-                            } else {
-                              maxSize =
-                                  Const.toInt(describedVariable.getValue(), 
Const.MAX_NR_LOG_LINES);
-                            }
-
-                            // Get the StyledText widget if available for 
direct style application
-                            StyledText styledText = null;
-                            if (text instanceof StyledTextVar) {
-                              styledText = ((StyledTextVar) 
text).getTextWidget();
-                            }
+                            StyledText styledText = getStyledText();
 
                             synchronized (text) {
                               for (HopLoggingEvent event : logLines) {
-                                String line = logLayout.format(event).trim();
-                                int length = line.length();
-
-                                if (length > 0) {
-                                  boolean isError =
-                                      event.getLevel() != null
-                                          && event.getLevel().getLevel()
-                                              == LogLevel.ERROR.getLevel();
-
-                                  if (styledText != null && 
!styledText.isDisposed()) {
-                                    try {
-                                      // Get the current text length (this is 
where we'll insert)
-                                      int startOffset = 
styledText.getCharCount();
-                                      String textToAdd = line + Const.CR;
-
-                                      // Use replaceTextRange to add text at 
the end
-                                      styledText.replaceTextRange(startOffset, 
0, textToAdd);
-
-                                      // Apply red color directly if this is 
an ERROR level event
-                                      if (isError) {
-                                        StyleRange styleRange = new 
StyleRange();
-                                        styleRange.start = startOffset;
-                                        styleRange.length = line.length();
-                                        styleRange.foreground =
-                                            
GuiResource.getInstance().getColorRed();
-                                        styleRange.fontStyle = SWT.NORMAL;
-                                        styledText.setStyleRange(styleRange);
-                                      }
-                                    } catch (Exception e) {
-                                      // Fallback to setText if there's any 
error
-                                      String currentText = text.getText();
-                                      text.setText(currentText + line + 
Const.CR);
-                                    }
-                                  } else {
-                                    // Fallback for non-StyledText widgets 
(e.g., web mode)
-                                    String currentText = text.getText();
-                                    text.setText(currentText + line + 
Const.CR);
-                                  }
-                                }
+                                appendLogEvent(event, styledText);
                               }
-                            }
 
-                            // Trim old lines if needed to stay within maxSize
-                            // Calculate line count
-                            String textContent = text.getText();
-                            int size;
-                            if (textContent == null || textContent.isEmpty()) {
-                              size = 0;
-                            } else {
-                              size = 1;
-                              for (int i = 0; i < textContent.length(); i++) {
-                                if (textContent.charAt(i) == '\n') {
-                                  size++;
-                                }
-                              }
-                            }
+                              trimToMaxSize(styledText);
 
-                            if (maxSize > 0 && size > maxSize) {
-                              int dropIndex =
-                                  StringUtils.lastOrdinalIndexOf(textContent, 
"\n", maxSize + 1);
-                              if (styledText != null && 
!styledText.isDisposed()) {
-                                // Use replaceTextRange to preserve styles on 
remaining text
-                                styledText.replaceTextRange(0, dropIndex + 1, 
"");
-                              } else {
-                                // Fallback for non-StyledText widgets
-                                text.setText(textContent.substring(dropIndex + 
1));
+                              if (!text.isDisposed()) {
+                                text.setSelection(text.getCharCount());
                               }
                             }
-
-                            if (!text.isDisposed()) {
-                              text.setSelection(text.getCharCount());
-                            }
                             lastLogId.set(lastNr);
                           }
                         }
@@ -273,6 +259,181 @@ public class HopGuiLogBrowser {
         });
   }
 
+  private void ensureChildIds() {
+    IHasLogChannel provider = logProvider.getLogChannelProvider();
+    if (provider == null) {
+      return;
+    }
+    ILogChannel logChannel = provider.getLogChannel();
+    if (logChannel == null) {
+      return;
+    }
+    String parentLogChannelId = logChannel.getLogChannelId();
+    LoggingRegistry registry = LoggingRegistry.getInstance();
+    Date registryModDate = registry.getLastModificationTime();
+
+    if (childIds == null
+        || lastLogRegistryChange == null
+        || registryModDate.compareTo(lastLogRegistryChange) > 0) {
+      lastLogRegistryChange = registry.getLastModificationTime();
+      childIds = 
LoggingRegistry.getInstance().getLogChannelChildren(parentLogChannelId);
+    }
+  }
+
+  private StyledText getStyledText() {
+    if (text instanceof StyledTextVar) {
+      return ((StyledTextVar) text).getTextWidget();
+    }
+    return null;
+  }
+
+  private void appendLogEvent(HopLoggingEvent event, StyledText styledText) {
+    String line = logLayout.format(event).trim();
+    if (line.isEmpty() || !shouldDisplayLine(line)) {
+      return;
+    }
+
+    boolean isError =
+        event.getLevel() != null && event.getLevel().getLevel() == 
LogLevel.ERROR.getLevel();
+
+    if (styledText != null && !styledText.isDisposed()) {
+      try {
+        int startOffset = styledText.getCharCount();
+        String textToAdd = line + Const.CR;
+        styledText.replaceTextRange(startOffset, 0, textToAdd);
+        applyLineStyles(styledText, startOffset, line, isError);
+      } catch (Exception e) {
+        String currentText = text.getText();
+        text.setText(currentText + line + Const.CR);
+      }
+    } else {
+      // Fallback for non-StyledText widgets (e.g., web mode)
+      String currentText = text.getText();
+      text.setText(currentText + line + Const.CR);
+    }
+  }
+
+  /**
+   * Decide whether a log line should appear in the view given the current 
filter options.
+   *
+   * <ul>
+   *   <li>Empty filter: show all
+   *   <li>Exclude on: hide matching lines
+   *   <li>Highlight on: show all (matches get styles elsewhere)
+   *   <li>Otherwise: only-matching mode
+   * </ul>
+   */
+  boolean shouldDisplayLine(String line) {
+    if (StringUtils.isEmpty(filterText)) {
+      return true;
+    }
+    boolean matches = lineMatches(line);
+    if (excludeMatches) {
+      return !matches;
+    }
+    if (highlightMatches) {
+      return true;
+    }
+    return matches;
+  }
+
+  boolean lineMatches(String line) {
+    if (StringUtils.isEmpty(filterText) || line == null) {
+      return false;
+    }
+    if (caseSensitive) {
+      return line.contains(filterText);
+    }
+    return StringUtils.containsIgnoreCase(line, filterText);
+  }
+
+  private void applyLineStyles(
+      StyledText styledText, int startOffset, String line, boolean isError) {
+    if (isError) {
+      StyleRange styleRange = new StyleRange();
+      styleRange.start = startOffset;
+      styleRange.length = line.length();
+      styleRange.foreground = GuiResource.getInstance().getColorRed();
+      styleRange.fontStyle = SWT.NORMAL;
+      styledText.setStyleRange(styleRange);
+    }
+
+    if (highlightMatches && StringUtils.isNotEmpty(filterText) && 
lineMatches(line)) {
+      applyHighlightRanges(styledText, startOffset, line, isError);
+    }
+  }
+
+  private void applyHighlightRanges(
+      StyledText styledText, int startOffset, String line, boolean isError) {
+    String haystack = caseSensitive ? line : line.toLowerCase();
+    String needle = caseSensitive ? filterText : filterText.toLowerCase();
+    if (needle.isEmpty()) {
+      return;
+    }
+
+    int from = 0;
+    while (from <= haystack.length() - needle.length()) {
+      int idx = haystack.indexOf(needle, from);
+      if (idx < 0) {
+        break;
+      }
+      StyleRange range = new StyleRange();
+      range.start = startOffset + idx;
+      range.length = needle.length();
+      // Use getColor(r,g,b) so dark-mode contrast remapping does not invert 
black/yellow.
+      // Dark mode: dark orange background with default (light) text is 
readable.
+      // Light mode: bright yellow; keep red text on error lines.
+      if (PropsUi.getInstance().isDarkMode()) {
+        range.background = GuiResource.getInstance().getColor(180, 90, 0);
+      } else {
+        range.background = GuiResource.getInstance().getColorYellow();
+        if (isError) {
+          range.foreground = GuiResource.getInstance().getColorRed();
+        }
+      }
+      range.fontStyle = SWT.NORMAL;
+      styledText.setStyleRange(range);
+      from = idx + Math.max(needle.length(), 1);
+    }
+  }
+
+  private void trimToMaxSize(StyledText styledText) {
+    int maxSize = getMaxLogSize();
+    String textContent = text.getText();
+    int size;
+    if (textContent == null || textContent.isEmpty()) {
+      size = 0;
+    } else {
+      size = 1;
+      for (int i = 0; i < textContent.length(); i++) {
+        if (textContent.charAt(i) == '\n') {
+          size++;
+        }
+      }
+    }
+
+    if (maxSize > 0 && size > maxSize) {
+      int dropIndex = StringUtils.lastOrdinalIndexOf(textContent, "\n", 
maxSize + 1);
+      if (dropIndex < 0) {
+        return;
+      }
+      if (styledText != null && !styledText.isDisposed()) {
+        styledText.replaceTextRange(0, dropIndex + 1, "");
+      } else {
+        text.setText(textContent.substring(dropIndex + 1));
+      }
+    }
+  }
+
+  private int getMaxLogSize() {
+    DescribedVariable describedVariable =
+        
HopConfig.getInstance().findDescribedVariable(Const.HOP_MAX_LOG_SIZE_IN_LINES);
+    if (describedVariable == null) {
+      return Const.MAX_NR_LOG_LINES;
+    }
+    return Const.toInt(describedVariable.getValue(), Const.MAX_NR_LOG_LINES);
+  }
+
   public boolean isPaused() {
     return paused.get();
   }
@@ -292,6 +453,8 @@ public class HopGuiLogBrowser {
 
   /** Skip log lines already in the central buffer (e.g. after the user clears 
the log view). */
   public void resetLogPosition() {
-    lastLogId.set(HopLogStore.getLastBufferLineNr());
+    int nr = HopLogStore.getLastBufferLineNr();
+    lastLogId.set(nr);
+    firstLogId.set(nr);
   }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineLogDelegate.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineLogDelegate.java
index fc91168e51..0ca0089aaa 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineLogDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineLogDelegate.java
@@ -25,6 +25,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.Props;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
+import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElementType;
 import org.apache.hop.core.logging.HopLogStore;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.pipeline.PipelineMeta;
@@ -50,8 +51,10 @@ import org.eclipse.swt.custom.CTabItem;
 import org.eclipse.swt.layout.FormAttachment;
 import org.eclipse.swt.layout.FormData;
 import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
 import org.eclipse.swt.widgets.ToolItem;
 
 @GuiPlugin(description = "Pipeline Graph Log Delegate")
@@ -67,6 +70,12 @@ public class HopGuiPipelineLogDelegate {
   public static final String TOOLBAR_ICON_LOG_INCREASE_FONT = 
"ToolbarIcon-10040-LogIncreaseFont";
   public static final String TOOLBAR_ICON_LOG_DECREASE_FONT = 
"ToolbarIcon-10050-LogDecreaseFont";
   public static final String TOOLBAR_ICON_LOG_RESET_FONT = 
"ToolbarIcon-10060-LogResetFont";
+  public static final String TOOLBAR_ICON_LOG_FILTER_TEXT = 
"ToolbarIcon-10070-LogFilterText";
+  public static final String TOOLBAR_ICON_LOG_FILTER_HIGHLIGHT =
+      "ToolbarIcon-10080-LogFilterHighlight";
+  public static final String TOOLBAR_ICON_LOG_FILTER_CASE_SENSITIVE =
+      "ToolbarIcon-10090-LogFilterCaseSensitive";
+  public static final String TOOLBAR_ICON_LOG_FILTER_EXCLUDE = 
"ToolbarIcon-10100-LogFilterExclude";
 
   private final HopGuiPipelineGraph pipelineGraph;
 
@@ -197,6 +206,11 @@ public class HopGuiPipelineLogDelegate {
     toolBarWidgets = new GuiToolbarWidgets();
     toolBarWidgets.registerGuiPluginObject(this);
     toolBarWidgets.createToolbarWidgets(toolBarContainer, 
GUI_PLUGIN_TOOLBAR_PARENT_ID);
+    // Apply filter while typing (not only on Enter) so only-matching mode 
updates live.
+    Control filterControl = 
toolBarWidgets.getControlForMenu(TOOLBAR_ICON_LOG_FILTER_TEXT);
+    if (filterControl instanceof Text filterText) {
+      filterText.addListener(SWT.Modify, event -> applyLogFilterFromToolbar());
+    }
     toolbar.pack();
   }
 
@@ -372,6 +386,75 @@ public class HopGuiPipelineLogDelegate {
     this.textZoom.resetFont();
   }
 
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_TEXT,
+      type = GuiToolbarElementType.TEXT,
+      label = "i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Text.Label",
+      toolTip = "i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Text.Tooltip",
+      separator = true)
+  public void filterTextChanged() {
+    applyLogFilterFromToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_HIGHLIGHT,
+      type = GuiToolbarElementType.CHECKBOX,
+      label = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Highlight.Label",
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Highlight.Tooltip")
+  public void filterHighlightChanged() {
+    applyLogFilterFromToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_CASE_SENSITIVE,
+      type = GuiToolbarElementType.CHECKBOX,
+      label = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.CaseSensitive.Label",
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.CaseSensitive.Tooltip")
+  public void filterCaseSensitiveChanged() {
+    applyLogFilterFromToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_EXCLUDE,
+      type = GuiToolbarElementType.CHECKBOX,
+      label = "i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Exclude.Label",
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Exclude.Tooltip")
+  public void filterExcludeChanged() {
+    applyLogFilterFromToolbar();
+  }
+
+  private void applyLogFilterFromToolbar() {
+    if (logBrowser == null || toolBarWidgets == null) {
+      return;
+    }
+
+    String filter = "";
+    Control filterControl = 
toolBarWidgets.getControlForMenu(TOOLBAR_ICON_LOG_FILTER_TEXT);
+    if (filterControl instanceof Text textWidget && !textWidget.isDisposed()) {
+      filter = Const.NVL(textWidget.getText(), "").trim();
+    }
+
+    boolean highlight = 
isToolbarCheckboxSelected(TOOLBAR_ICON_LOG_FILTER_HIGHLIGHT);
+    boolean caseSensitive = 
isToolbarCheckboxSelected(TOOLBAR_ICON_LOG_FILTER_CASE_SENSITIVE);
+    boolean exclude = 
isToolbarCheckboxSelected(TOOLBAR_ICON_LOG_FILTER_EXCLUDE);
+
+    // Without highlight (and without exclude), only matching lines remain in 
the log view.
+    logBrowser.setFilter(filter, highlight, caseSensitive, exclude);
+    logBrowser.refreshFilteredView();
+  }
+
+  private boolean isToolbarCheckboxSelected(String id) {
+    Control control = toolBarWidgets.getControlForMenu(id);
+    if (control instanceof Button button && !button.isDisposed()) {
+      return button.getSelection();
+    }
+    return false;
+  }
+
   public boolean hasSelectedText() {
     if (pipelineLogText == null || pipelineLogText.isDisposed()) {
       return false;
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 e551464a0b..6cec4b4d52 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
@@ -24,6 +24,7 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.Props;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
+import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElementType;
 import org.apache.hop.core.logging.HopLogStore;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.ui.core.PropsUi;
@@ -49,8 +50,10 @@ import org.eclipse.swt.custom.CTabItem;
 import org.eclipse.swt.layout.FormAttachment;
 import org.eclipse.swt.layout.FormData;
 import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
 
 @GuiPlugin(description = "Workflow Graph Log Delegate")
 public class HopGuiWorkflowLogDelegate {
@@ -65,6 +68,12 @@ public class HopGuiWorkflowLogDelegate {
   public static final String TOOLBAR_ICON_LOG_INCREASE_FONT = 
"ToolbarIcon-10040-LogIncreaseFont";
   public static final String TOOLBAR_ICON_LOG_DECREASE_FONT = 
"ToolbarIcon-10050-LogDecreaseFont";
   public static final String TOOLBAR_ICON_LOG_RESET_FONT = 
"ToolbarIcon-10060-LogResetFont";
+  public static final String TOOLBAR_ICON_LOG_FILTER_TEXT = 
"ToolbarIcon-10070-LogFilterText";
+  public static final String TOOLBAR_ICON_LOG_FILTER_HIGHLIGHT =
+      "ToolbarIcon-10080-LogFilterHighlight";
+  public static final String TOOLBAR_ICON_LOG_FILTER_CASE_SENSITIVE =
+      "ToolbarIcon-10090-LogFilterCaseSensitive";
+  public static final String TOOLBAR_ICON_LOG_FILTER_EXCLUDE = 
"ToolbarIcon-10100-LogFilterExclude";
 
   private HopGui hopGui;
   private HopGuiWorkflowGraph workflowGraph;
@@ -198,6 +207,11 @@ public class HopGuiWorkflowLogDelegate {
     toolBarWidgets = new GuiToolbarWidgets();
     toolBarWidgets.registerGuiPluginObject(this);
     toolBarWidgets.createToolbarWidgets(toolBarContainer, 
GUI_PLUGIN_TOOLBAR_PARENT_ID);
+    // Apply filter while typing (not only on Enter) so only-matching mode 
updates live.
+    Control filterControl = 
toolBarWidgets.getControlForMenu(TOOLBAR_ICON_LOG_FILTER_TEXT);
+    if (filterControl instanceof Text filterText) {
+      filterText.addListener(SWT.Modify, event -> applyLogFilterFromToolbar());
+    }
     toolbar.pack();
   }
 
@@ -344,6 +358,75 @@ public class HopGuiWorkflowLogDelegate {
     this.textZoom.resetFont();
   }
 
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_TEXT,
+      type = GuiToolbarElementType.TEXT,
+      label = "i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Text.Label",
+      toolTip = "i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Text.Tooltip",
+      separator = true)
+  public void filterTextChanged() {
+    applyLogFilterFromToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_HIGHLIGHT,
+      type = GuiToolbarElementType.CHECKBOX,
+      label = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Highlight.Label",
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Highlight.Tooltip")
+  public void filterHighlightChanged() {
+    applyLogFilterFromToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_CASE_SENSITIVE,
+      type = GuiToolbarElementType.CHECKBOX,
+      label = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.CaseSensitive.Label",
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.CaseSensitive.Tooltip")
+  public void filterCaseSensitiveChanged() {
+    applyLogFilterFromToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_EXCLUDE,
+      type = GuiToolbarElementType.CHECKBOX,
+      label = "i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Exclude.Label",
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Exclude.Tooltip")
+  public void filterExcludeChanged() {
+    applyLogFilterFromToolbar();
+  }
+
+  private void applyLogFilterFromToolbar() {
+    if (logBrowser == null || toolBarWidgets == null) {
+      return;
+    }
+
+    String filter = "";
+    Control filterControl = 
toolBarWidgets.getControlForMenu(TOOLBAR_ICON_LOG_FILTER_TEXT);
+    if (filterControl instanceof Text textWidget && !textWidget.isDisposed()) {
+      filter = Const.NVL(textWidget.getText(), "").trim();
+    }
+
+    boolean highlight = 
isToolbarCheckboxSelected(TOOLBAR_ICON_LOG_FILTER_HIGHLIGHT);
+    boolean caseSensitive = 
isToolbarCheckboxSelected(TOOLBAR_ICON_LOG_FILTER_CASE_SENSITIVE);
+    boolean exclude = 
isToolbarCheckboxSelected(TOOLBAR_ICON_LOG_FILTER_EXCLUDE);
+
+    // Without highlight (and without exclude), only matching lines remain in 
the log view.
+    logBrowser.setFilter(filter, highlight, caseSensitive, exclude);
+    logBrowser.refreshFilteredView();
+  }
+
+  private boolean isToolbarCheckboxSelected(String id) {
+    Control control = toolBarWidgets.getControlForMenu(id);
+    if (control instanceof Button button && !button.isDisposed()) {
+      return button.getSelection();
+    }
+    return false;
+  }
+
   public boolean hasSelectedText() {
     if (workflowLogText == null || workflowLogText.isDisposed()) {
       return false;
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionLogPanel.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionLogPanel.java
new file mode 100644
index 0000000000..aeeb6902c1
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionLogPanel.java
@@ -0,0 +1,401 @@
+/*
+ * 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.perspective.execution;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.Props;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
+import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElementType;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
+import org.apache.hop.ui.core.gui.IToolbarContainer;
+import org.apache.hop.ui.core.widget.StyledTextComp;
+import org.apache.hop.ui.core.widget.StyledTextVar;
+import org.apache.hop.ui.core.widget.TextComposite;
+import org.apache.hop.ui.hopgui.ToolbarFacade;
+import org.apache.hop.ui.hopgui.file.shared.TextZoom;
+import org.apache.hop.ui.util.EnvironmentUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyleRange;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Log tab content for pipeline/workflow execution viewers: toolbar (copy, 
zoom, filter) plus the
+ * log text widget. Snapshot-based (not a live log sniffer).
+ */
+@GuiPlugin(description = "Execution Log Panel")
+public class ExecutionLogPanel {
+
+  private static final String GUI_PLUGIN_TOOLBAR_PARENT_ID = 
"ExecutionLogPanel-ToolBar";
+
+  public static final String TOOLBAR_ICON_LOG_COPY_TO_CLIPBOARD =
+      "ToolbarIcon-10000-LogCopyToClipboard";
+  public static final String TOOLBAR_ICON_LOG_INCREASE_FONT = 
"ToolbarIcon-10010-LogIncreaseFont";
+  public static final String TOOLBAR_ICON_LOG_DECREASE_FONT = 
"ToolbarIcon-10020-LogDecreaseFont";
+  public static final String TOOLBAR_ICON_LOG_RESET_FONT = 
"ToolbarIcon-10030-LogResetFont";
+  public static final String TOOLBAR_ICON_LOG_FILTER_TEXT = 
"ToolbarIcon-10040-LogFilterText";
+  public static final String TOOLBAR_ICON_LOG_FILTER_HIGHLIGHT =
+      "ToolbarIcon-10050-LogFilterHighlight";
+  public static final String TOOLBAR_ICON_LOG_FILTER_CASE_SENSITIVE =
+      "ToolbarIcon-10060-LogFilterCaseSensitive";
+  public static final String TOOLBAR_ICON_LOG_FILTER_EXCLUDE = 
"ToolbarIcon-10070-LogFilterExclude";
+
+  private Composite composite;
+  private Control toolbar;
+  private GuiToolbarWidgets toolBarWidgets;
+  private TextComposite logText;
+  private TextZoom textZoom;
+
+  private String rawLoggingText = "";
+  private String filterText = "";
+  private boolean highlightMatches;
+  private boolean caseSensitive;
+  private boolean excludeMatches;
+
+  @Getter private boolean created;
+
+  /**
+   * Build the log panel UI under the given parent (typically the execution 
viewer tab folder).
+   *
+   * @param parent parent composite
+   * @return the root composite to set as the log tab control
+   */
+  public Control create(Composite parent) {
+    composite = new Composite(parent, SWT.NONE);
+    composite.setLayout(new FormLayout());
+    PropsUi.setLook(composite);
+
+    addToolBar();
+
+    FormData fdToolbar = new FormData();
+    fdToolbar.left = new FormAttachment(0, 0);
+    fdToolbar.top = new FormAttachment(0, 0);
+    fdToolbar.right = new FormAttachment(100, 0);
+    toolbar.setLayoutData(fdToolbar);
+
+    // Desktop: StyledText for highlight ranges; web: plain Text 
(StyledTextComp)
+    if (EnvironmentUtils.getInstance().isWeb()) {
+      logText =
+          new StyledTextComp(
+              Variables.getADefaultVariableSpace(),
+              composite,
+              SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL);
+    } else {
+      logText =
+          new StyledTextVar(
+              Variables.getADefaultVariableSpace(),
+              composite,
+              SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
+              false,
+              false);
+    }
+    PropsUi.setLook(logText);
+    FormData fdText = new FormData();
+    fdText.left = new FormAttachment(0, 0);
+    fdText.right = new FormAttachment(100, 0);
+    fdText.top = new FormAttachment(toolbar, 0);
+    fdText.bottom = new FormAttachment(100, 0);
+    logText.setLayoutData(fdText);
+
+    textZoom = new TextZoom(logText, GuiResource.getInstance().getFontFixed());
+    textZoom.resetFont();
+
+    created = true;
+    return composite;
+  }
+
+  private void addToolBar() {
+    IToolbarContainer toolBarContainer =
+        ToolbarFacade.createToolbarContainer(composite, SWT.WRAP | SWT.LEFT | 
SWT.HORIZONTAL);
+    toolbar = toolBarContainer.getControl();
+    FormData fdToolBar = new FormData();
+    fdToolBar.left = new FormAttachment(0, 0);
+    fdToolBar.top = new FormAttachment(0, 0);
+    fdToolBar.right = new FormAttachment(100, 0);
+    toolbar.setLayoutData(fdToolBar);
+    PropsUi.setLook(toolbar, Props.WIDGET_STYLE_TOOLBAR);
+
+    toolBarWidgets = new GuiToolbarWidgets();
+    toolBarWidgets.registerGuiPluginObject(this);
+    toolBarWidgets.createToolbarWidgets(toolBarContainer, 
GUI_PLUGIN_TOOLBAR_PARENT_ID);
+
+    Control filterControl = 
toolBarWidgets.getControlForMenu(TOOLBAR_ICON_LOG_FILTER_TEXT);
+    if (filterControl instanceof Text filterField) {
+      filterField.addListener(SWT.Modify, event -> applyFilterFromToolbar());
+    }
+
+    toolbar.pack();
+  }
+
+  /**
+   * Store the unfiltered log text from the execution location and refresh the 
display with the
+   * current filter options.
+   */
+  public void setRawLoggingText(String text) {
+    this.rawLoggingText = Const.NVL(text, "");
+    applyDisplayFromRaw();
+  }
+
+  public String getDisplayedText() {
+    if (logText == null || logText.isDisposed()) {
+      return "";
+    }
+    return Const.NVL(logText.getText(), "");
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_COPY_TO_CLIPBOARD,
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:PipelineLog.Button.LogCopyToClipboard",
+      image = "ui/images/copy.svg")
+  public void copyToClipboard() {
+    GuiResource.getInstance().toClipboard(getDisplayedText());
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_INCREASE_FONT,
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:WorkflowLog.Button.IncreaseFont",
+      image = "ui/images/zoom-in.svg",
+      separator = true)
+  public void increaseFont() {
+    if (textZoom != null) {
+      textZoom.increaseFont();
+    }
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_DECREASE_FONT,
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:WorkflowLog.Button.DecreaseFont",
+      image = "ui/images/zoom-out.svg")
+  public void decreaseFont() {
+    if (textZoom != null) {
+      textZoom.decreaseFont();
+    }
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_RESET_FONT,
+      toolTip = "i18n:org.apache.hop.ui.hopgui:WorkflowLog.Button.ResetFont",
+      image = "ui/images/zoom-100.svg")
+  public void resetFont() {
+    if (textZoom != null) {
+      textZoom.resetFont();
+    }
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_TEXT,
+      type = GuiToolbarElementType.TEXT,
+      label = "i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Text.Label",
+      toolTip = "i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Text.Tooltip",
+      separator = true)
+  public void filterTextChanged() {
+    applyFilterFromToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_HIGHLIGHT,
+      type = GuiToolbarElementType.CHECKBOX,
+      label = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Highlight.Label",
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Highlight.Tooltip")
+  public void filterHighlightChanged() {
+    applyFilterFromToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_CASE_SENSITIVE,
+      type = GuiToolbarElementType.CHECKBOX,
+      label = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.CaseSensitive.Label",
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.CaseSensitive.Tooltip")
+  public void filterCaseSensitiveChanged() {
+    applyFilterFromToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+      id = TOOLBAR_ICON_LOG_FILTER_EXCLUDE,
+      type = GuiToolbarElementType.CHECKBOX,
+      label = "i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Exclude.Label",
+      toolTip = 
"i18n:org.apache.hop.ui.hopgui:LogBrowser.Filter.Exclude.Tooltip")
+  public void filterExcludeChanged() {
+    applyFilterFromToolbar();
+  }
+
+  private void applyFilterFromToolbar() {
+    if (toolBarWidgets == null || logText == null || logText.isDisposed()) {
+      return;
+    }
+
+    String filter = "";
+    Control filterControl = 
toolBarWidgets.getControlForMenu(TOOLBAR_ICON_LOG_FILTER_TEXT);
+    if (filterControl instanceof Text textWidget && !textWidget.isDisposed()) {
+      filter = Const.NVL(textWidget.getText(), "").trim();
+    }
+
+    this.filterText = filter;
+    this.highlightMatches = 
isToolbarCheckboxSelected(TOOLBAR_ICON_LOG_FILTER_HIGHLIGHT);
+    this.caseSensitive = 
isToolbarCheckboxSelected(TOOLBAR_ICON_LOG_FILTER_CASE_SENSITIVE);
+    this.excludeMatches = 
isToolbarCheckboxSelected(TOOLBAR_ICON_LOG_FILTER_EXCLUDE);
+
+    applyDisplayFromRaw();
+  }
+
+  private boolean isToolbarCheckboxSelected(String id) {
+    Control control = toolBarWidgets.getControlForMenu(id);
+    if (control instanceof Button button && !button.isDisposed()) {
+      return button.getSelection();
+    }
+    return false;
+  }
+
+  private void applyDisplayFromRaw() {
+    if (logText == null || logText.isDisposed()) {
+      return;
+    }
+
+    // No filter: show the raw log as-is (no rebuild, no highlight styles).
+    if (StringUtils.isEmpty(filterText)) {
+      logText.setText(rawLoggingText);
+      if (!logText.isDisposed()) {
+        logText.setSelection(logText.getCharCount());
+      }
+      return;
+    }
+
+    String[] lines = splitLines(rawLoggingText);
+    StringBuilder display = new StringBuilder(rawLoggingText.length());
+    List<int[]> highlightSpans = new ArrayList<>(); // [start, length] in 
display text
+
+    for (String line : lines) {
+      if (!shouldDisplayLine(line)) {
+        continue;
+      }
+      int lineStart = display.length();
+      display.append(line).append(Const.CR);
+
+      if (highlightMatches && lineMatches(line)) {
+        collectHighlightSpans(line, lineStart, highlightSpans);
+      }
+    }
+
+    String displayText = display.toString();
+    StyledText styledText = getStyledText();
+    if (styledText != null && !styledText.isDisposed()) {
+      styledText.setText(displayText);
+      for (int[] span : highlightSpans) {
+        applyHighlightRange(styledText, span[0], span[1]);
+      }
+    } else {
+      logText.setText(displayText);
+    }
+
+    if (!logText.isDisposed()) {
+      logText.setSelection(logText.getCharCount());
+    }
+  }
+
+  private static String[] splitLines(String text) {
+    if (text == null || text.isEmpty()) {
+      return new String[0];
+    }
+    // Preserve empty trailing behavior similar to String.split(-1)
+    return text.split("\\r?\\n", -1);
+  }
+
+  private void collectHighlightSpans(String line, int lineStart, List<int[]> 
highlightSpans) {
+    String haystack = caseSensitive ? line : line.toLowerCase();
+    String needle = caseSensitive ? filterText : filterText.toLowerCase();
+    if (needle.isEmpty()) {
+      return;
+    }
+    int from = 0;
+    while (from <= haystack.length() - needle.length()) {
+      int idx = haystack.indexOf(needle, from);
+      if (idx < 0) {
+        break;
+      }
+      highlightSpans.add(new int[] {lineStart + idx, needle.length()});
+      from = idx + Math.max(needle.length(), 1);
+    }
+  }
+
+  private void applyHighlightRange(StyledText styledText, int start, int 
length) {
+    StyleRange range = new StyleRange();
+    range.start = start;
+    range.length = length;
+    range.fontStyle = SWT.NORMAL;
+    // Avoid contrast-remapped colors for reliable dark-mode readability
+    if (PropsUi.getInstance().isDarkMode()) {
+      range.background = GuiResource.getInstance().getColor(180, 90, 0);
+    } else {
+      range.background = GuiResource.getInstance().getColorYellow();
+    }
+    styledText.setStyleRange(range);
+  }
+
+  private StyledText getStyledText() {
+    if (logText instanceof StyledTextVar) {
+      return ((StyledTextVar) logText).getTextWidget();
+    }
+    return null;
+  }
+
+  private boolean shouldDisplayLine(String line) {
+    if (StringUtils.isEmpty(filterText)) {
+      return true;
+    }
+    boolean matches = lineMatches(line);
+    if (excludeMatches) {
+      return !matches;
+    }
+    if (highlightMatches) {
+      return true;
+    }
+    return matches;
+  }
+
+  private boolean lineMatches(String line) {
+    if (StringUtils.isEmpty(filterText) || line == null) {
+      return false;
+    }
+    if (caseSensitive) {
+      return line.contains(filterText);
+    }
+    return StringUtils.containsIgnoreCase(line, filterText);
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
index cf4531b4bd..c3ff71765c 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
@@ -307,20 +307,69 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
   @Override
   public void clearSearchFilters() {
     filterText = "";
-    if (toolBarWidgets != null) {
-      ToolItem item = toolBarWidgets.findToolItem(TOOLBAR_ITEM_FILTER_TEXT);
-      if (item != null && !item.isDisposed()) {
-        org.eclipse.swt.widgets.Control control = item.getControl();
-        if (control != null && !control.isDisposed() && control instanceof 
Text text) {
-          text.setText("");
-        }
-      }
-    }
+    setFilterTextOnToolbar("");
     if (hopGui != null && hopGui.isActivePerspective(this)) {
       refresh();
     }
   }
 
+  /**
+   * Read the free-text filter from the toolbar into {@link #filterText}. 
Treats the placeholder
+   * label as empty. Does not modify the widget.
+   */
+  private void syncFilterTextFromToolbar() {
+    if (toolBarWidgets == null) {
+      return;
+    }
+    Text text = getToolbarText(TOOLBAR_ITEM_FILTER_TEXT);
+    if (text == null) {
+      return;
+    }
+    String value = Const.NVL(text.getText(), "").trim();
+    if (FILTER_NAME_DATE_ID.equals(value)) {
+      value = "";
+    }
+    this.filterText = value;
+  }
+
+  private void setFilterTextOnToolbar(String value) {
+    Text text = getToolbarText(TOOLBAR_ITEM_FILTER_TEXT);
+    if (text != null) {
+      text.setText(Const.NVL(value, ""));
+    }
+  }
+
+  private Text getToolbarText(String id) {
+    if (toolBarWidgets == null) {
+      return null;
+    }
+    Control control = toolBarWidgets.getControlForMenu(id);
+    if (control instanceof Text text && !text.isDisposed()) {
+      return text;
+    }
+    // Desktop fallback: SEPARATOR ToolItem holding the Text
+    ToolItem item = toolBarWidgets.findToolItem(id);
+    if (item != null && !item.isDisposed() && item.getControl() instanceof 
Text text) {
+      return text;
+    }
+    return null;
+  }
+
+  private Combo getToolbarCombo(String id) {
+    if (toolBarWidgets == null) {
+      return null;
+    }
+    Control control = toolBarWidgets.getControlForMenu(id);
+    if (control instanceof Combo combo && !combo.isDisposed()) {
+      return combo;
+    }
+    ToolItem item = toolBarWidgets.findToolItem(id);
+    if (item != null && !item.isDisposed() && item.getControl() instanceof 
Combo combo) {
+      return combo;
+    }
+    return null;
+  }
+
   protected MetadataManager<IHopMetadata> getMetadataManager(String objectKey) 
throws HopException {
     IHopMetadataProvider metadataProvider = hopGui.getMetadataProvider();
     Class<IHopMetadata> metadataClass = 
metadataProvider.getMetadataClassForKey(objectKey);
@@ -355,6 +404,21 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
     toolBar.pack();
     PropsUi.setLook(toolBar, Props.WIDGET_STYLE_TOOLBAR);
 
+    // Ensure Enter in the free-text filter applies the filter (GTK toolbar 
Text often does not
+    // fire DefaultSelection). GuiToolbarWidgets also wires this; keep a local 
handler as a
+    // fallback when the toolbar listener instance mapping is incomplete.
+    Text filterTextWidget = getToolbarText(TOOLBAR_ITEM_FILTER_TEXT);
+    if (filterTextWidget != null && !filterTextWidget.isDisposed()) {
+      filterTextWidget.addListener(
+          SWT.KeyDown,
+          event -> {
+            if (event.keyCode == SWT.CR || event.keyCode == SWT.KEYPAD_CR) {
+              selectTextFilter();
+              event.doit = false;
+            }
+          });
+    }
+
     tree = new Tree(composite, SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL);
     tree.setHeaderVisible(false);
     tree.addListener(SWT.Selection, event -> updateSelection());
@@ -704,23 +768,14 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
       }
     }
 
-    // Time filter
-    item = toolBarWidgets.findToolItem(TOOLBAR_ITEM_TIME_FILTER);
-    if (item != null && !item.isDisposed()) {
-      org.eclipse.swt.widgets.Control control = item.getControl();
-      if (control != null && !control.isDisposed() && control instanceof 
Combo) {
-        ((Combo) control).setText(timeFilter.getDescription());
-      }
+    // Time filter combo (safe to push model → widget)
+    Combo timeCombo = getToolbarCombo(TOOLBAR_ITEM_TIME_FILTER);
+    if (timeCombo != null) {
+      timeCombo.setText(timeFilter.getDescription());
     }
 
-    // Filter string
-    item = toolBarWidgets.findToolItem(TOOLBAR_ITEM_FILTER_TEXT);
-    if (item != null && !item.isDisposed()) {
-      org.eclipse.swt.widgets.Control control = item.getControl();
-      if (control != null && !control.isDisposed() && control instanceof Text) 
{
-        ((Text) control).setText(Const.NVL(filterText, ""));
-      }
-    }
+    // Do not overwrite the free-text filter box here: the user may have typed 
text that is not
+    // yet committed via Enter. refresh() always syncs model ← widget instead.
 
     final IHopFileTypeHandler activeHandler = getActiveFileTypeHandler();
     if (activeHandler != null) {
@@ -769,6 +824,9 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
       return;
     }
 
+    // Always pick up whatever is currently in the filter box (including text 
typed without Enter).
+    syncFilterTextFromToolbar();
+
     Cursor busyCursor = getBusyCursor();
 
     try {
@@ -1026,8 +1084,8 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
       comboValuesMethod = "getLastPeriodDescriptions",
       toolTip = "i18n::ExecutionPerspective.ToolbarElement.TimeFilter.Tooltip")
   public void selectTimeFilter() {
-    ToolItem item = toolBarWidgets.findToolItem(TOOLBAR_ITEM_TIME_FILTER);
-    if (item == null || !(item.getControl() instanceof Combo combo)) {
+    Combo combo = getToolbarCombo(TOOLBAR_ITEM_TIME_FILTER);
+    if (combo == null) {
       return;
     }
     this.timeFilter = LastPeriod.lookupDescription(combo.getText());
@@ -1051,13 +1109,10 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
     this.onlyShowingFailed = false;
     this.filterText = "";
 
+    // Clear the box (empty, not the placeholder string as a real filter 
value).
     // The filter box itself can be switched off in disabledGuiElements.xml 
while this button is
     // not, in which case there is no box to clear.
-    //
-    ToolItem item = toolBarWidgets.findToolItem(TOOLBAR_ITEM_FILTER_TEXT);
-    if (item != null && item.getControl() instanceof Text text) {
-      text.setText(FILTER_NAME_DATE_ID);
-    }
+    setFilterTextOnToolbar("");
 
     // Update the icon && apply the filter
     updateGui();
@@ -1071,11 +1126,7 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
       type = GuiToolbarElementType.TEXT,
       defaultText = FILTER_NAME_DATE_ID)
   public void selectTextFilter() {
-    ToolItem item = toolBarWidgets.findToolItem(TOOLBAR_ITEM_FILTER_TEXT);
-    if (item == null || !(item.getControl() instanceof Text text)) {
-      return;
-    }
-    this.filterText = text.getText();
+    syncFilterTextFromToolbar();
 
     // Update the icon && apply the filter
     updateGui();
@@ -1612,10 +1663,15 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
       onlyShowingWorkflows = toolbarState.extractBoolean(AUDIT_ONLY_WORKFLOWS, 
false);
       onlyShowingPipelines = toolbarState.extractBoolean(AUDIT_ONLY_PIPELINES, 
false);
       filterText = toolbarState.extractString(AUDIT_FILTER_TEXT, "");
+      if (FILTER_NAME_DATE_ID.equals(filterText)) {
+        filterText = "";
+      }
       String timeFilterName = toolbarState.extractString(AUDIT_TIME_FILTER, 
"");
       timeFilter = IEnumHasCode.lookupCode(LastPeriod.class, timeFilterName, 
LastPeriod.ONE_HOUR);
       String activeKey = toolbarState.extractString(AUDIT_ACTIVE_TAB, "");
 
+      // Restore filter text onto the widget before updateGui/refresh 
(updateGui does not write it).
+      setFilterTextOnToolbar(filterText);
       updateGui();
       refresh();
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
index df791e09d3..75378d7530 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
@@ -114,7 +114,6 @@ import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Listener;
 import org.eclipse.swt.widgets.TableItem;
-import org.eclipse.swt.widgets.Text;
 import org.w3c.dom.Node;
 
 @GuiPlugin(name = "i18n::PipelineExecutionViewer.Name")
@@ -665,10 +664,8 @@ public class PipelineExecutionViewer extends 
BaseExecutionViewer
     logTab.setImage(GuiResource.getInstance().getImageShowLog());
     logTab.setText(BaseMessages.getString(PKG, 
"PipelineExecutionViewer.LogTab.Title"));
 
-    loggingText = new Text(tabFolder, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL 
| SWT.READ_ONLY);
-    PropsUi.setLook(loggingText);
-
-    logTab.setControl(loggingText);
+    executionLogPanel = new ExecutionLogPanel();
+    logTab.setControl(executionLogPanel.create(tabFolder));
 
     // When the logging tab comes into focus, re-load the logging text
     //
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
index 5203e8899c..387cca3a3a 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
@@ -103,7 +103,6 @@ import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Listener;
 import org.eclipse.swt.widgets.TableItem;
-import org.eclipse.swt.widgets.Text;
 import org.w3c.dom.Node;
 
 @GuiPlugin(name = "i18n::WorkflowExecutionViewer.Name")
@@ -474,10 +473,8 @@ public class WorkflowExecutionViewer extends 
BaseExecutionViewer
     logTab.setImage(GuiResource.getInstance().getImageShowLog());
     logTab.setText(BaseMessages.getString(PKG, 
"WorkflowExecutionViewer.LogTab.Title"));
 
-    loggingText = new Text(tabFolder, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL 
| SWT.READ_ONLY);
-    PropsUi.setLook(loggingText);
-
-    logTab.setControl(loggingText);
+    executionLogPanel = new ExecutionLogPanel();
+    logTab.setControl(executionLogPanel.create(tabFolder));
 
     // When the logging tab comes into focus, re-load the logging text
     //
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java
index 316ce7327f..73f6834c5c 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java
@@ -48,6 +48,7 @@ import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
 import org.apache.hop.ui.core.metadata.MetadataManager;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.hopgui.perspective.execution.DragViewZoomBase;
+import org.apache.hop.ui.hopgui.perspective.execution.ExecutionLogPanel;
 import org.apache.hop.ui.hopgui.perspective.execution.ExecutionPerspective;
 import org.apache.hop.ui.util.EnvironmentUtils;
 import org.eclipse.swt.SWT;
@@ -60,7 +61,6 @@ import org.eclipse.swt.graphics.Cursor;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Display;
-import org.eclipse.swt.widgets.Text;
 
 @Getter
 @Setter
@@ -84,7 +84,7 @@ public abstract class BaseExecutionViewer extends 
DragViewZoomBase
   protected SashForm sash;
   protected CTabFolder tabFolder;
 
-  protected Text loggingText;
+  protected ExecutionLogPanel executionLogPanel;
 
   protected Point lastClick;
 
@@ -319,14 +319,16 @@ public abstract class BaseExecutionViewer extends 
DragViewZoomBase
           iLocation.getExecutionStateLoggingText(
               execution.getId(), props.getMaxExecutionLoggingTextSize());
 
-      loggingText.setText(Const.NVL(shownLogText, ""));
-
-      // Scroll to the bottom
-      loggingText.setSelection(loggingText.getCharCount());
+      if (executionLogPanel != null) {
+        executionLogPanel.setRawLoggingText(Const.NVL(shownLogText, ""));
+      }
     } catch (Exception e) {
       new ErrorDialog(getShell(), "Error", "Error refreshing logging text", e);
     } finally {
       getShell().setCursor(null);
+      if (busyCursor != null && !busyCursor.isDisposed()) {
+        busyCursor.dispose();
+      }
     }
   }
 
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
index 14db42b96d..928a94b858 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
@@ -144,6 +144,14 @@ 
HopGuiPipelineGraph.ContextualActionDialog.InsertTransform.Header=Select the tra
 HopGuiPipelineGraph.OpenReferencedObject.Selection.Message=Select the 
referenced file to open:
 HopGuiPipelineGraph.OpenReferencedObject.Selection.Title=Open referenced file
 LogBrowser.CopySelectionToClipboard.MenuItem=Copy selected text to clipboard
+LogBrowser.Filter.Text.Label=Filter text\: 
+LogBrowser.Filter.Text.Tooltip=Filter log lines. Without Highlight or Exclude, 
only matching lines remain. Highlight shows all lines with matches marked. 
Exclude hides matching lines.
+LogBrowser.Filter.Highlight.Label=Highlight
+LogBrowser.Filter.Highlight.Tooltip=Show all log lines and highlight matches 
for the filter text
+LogBrowser.Filter.CaseSensitive.Label=Match case
+LogBrowser.Filter.CaseSensitive.Tooltip=Match the filter text with case 
sensitivity
+LogBrowser.Filter.Exclude.Label=Exclude
+LogBrowser.Filter.Exclude.Tooltip=Hide log lines that match the filter text 
(exclude from the log view)
 PipelineGraph.Check.Tab.Name=Problems
 PipelineGraph.Check.ErrorCheckingPipeline.Exception=Error checking pipeline: 
{0}
 PipelineGraph.Check.ErrorCheckingPipeline.Message=Error checking pipeline

Reply via email to