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