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 f2d943eb43 Improvements to the metrics tab in pipelines, fixes #6451
(#6496)
f2d943eb43 is described below
commit f2d943eb4339ba415962694fb05f4d5b0ecfff7e
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Wed Feb 4 10:53:55 2026 +0100
Improvements to the metrics tab in pipelines, fixes #6451 (#6496)
---
.../hop/ui/core/widget/svg/SvgLabelFacadeImpl.java | 22 +
.../main/java/org/apache/hop/ui/hopgui/HopWeb.java | 13 +
.../apache/hop/ui/core/gui/GuiToolbarWidgets.java | 45 ++
.../hop/ui/core/widget/svg/SvgLabelFacade.java | 17 +
.../delegates/HopGuiPipelineGridDelegate.java | 690 +++++++++++++++------
.../delegates/HopGuiPipelineLogDelegate.java | 16 +-
.../delegates/HopGuiWorkflowLogDelegate.java | 6 +-
.../hopgui/selection/HopGuiSelectionTracker.java | 30 +-
.../org/apache/hop/ui/util/SwtSvgImageUtil.java | 13 +-
.../ui/hopgui/messages/messages_en_US.properties | 1 +
10 files changed, 652 insertions(+), 201 deletions(-)
diff --git
a/rap/src/main/java/org/apache/hop/ui/core/widget/svg/SvgLabelFacadeImpl.java
b/rap/src/main/java/org/apache/hop/ui/core/widget/svg/SvgLabelFacadeImpl.java
index 6c314c5555..2c8e03b827 100644
---
a/rap/src/main/java/org/apache/hop/ui/core/widget/svg/SvgLabelFacadeImpl.java
+++
b/rap/src/main/java/org/apache/hop/ui/core/widget/svg/SvgLabelFacadeImpl.java
@@ -96,6 +96,28 @@ public class SvgLabelFacadeImpl extends SvgLabelFacade {
"; }");
}
+ @Override
+ public void updateImageSourceInternal(String id, Label label, String
imagePath) {
+ try {
+ String src = RWT.getResourceManager().getLocation(imagePath);
+ if (src == null) {
+ return;
+ }
+ // Update the img src via JavaScript so the icon updates without
replacing label markup
+ // (setText with new markup may not re-render in RWT)
+ String escaped = src.replace("\\", "\\\\").replace("'", "\\'");
+ exec("var el = document.getElementById('", id, "'); if (el) { el.src='",
escaped, "'; }");
+ } catch (Exception e) {
+ System.err.println(
+ "Error updating image source for tool-item "
+ + id
+ + " for filename: "
+ + imagePath
+ + " - "
+ + Const.getSimpleStackTrace(e));
+ }
+ }
+
private static void exec(String... strings) {
StringBuilder builder = new StringBuilder();
builder.append("try {");
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
index edd3d02928..eab16461a8 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
@@ -76,6 +76,19 @@ public class HopWeb implements ApplicationConfiguration {
}
}
+ // Register alternate images for toolbar toggles (e.g. show/hide,
show-all/show-selected)
+ // so setToolbarItemImage() can switch icons in RWT without "Resource
does not exist"
+ ClassLoader uiClassLoader = HopWeb.class.getClassLoader();
+ for (String path :
+ new String[] {
+ "ui/images/show.svg",
+ "ui/images/hide.svg",
+ "ui/images/show-all.svg",
+ "ui/images/show-selected.svg"
+ }) {
+ addResource(application, path, uiClassLoader);
+ }
+
// Find metadata, perspective plugins
//
List<IPlugin> plugins =
PluginRegistry.getInstance().getPlugins(MetadataPluginType.class);
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 f63ea58ce0..12336abb61 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
@@ -364,6 +364,10 @@ public class GuiToolbarWidgets extends BaseGuiWidgets {
// This prevents ID collisions when multiple tabs have the same toolbar
items
String uniqueId = instanceId + "-" + toolbarItem.getId();
SvgLabelFacade.setData(uniqueId, imageLabel, imageFilename, size);
+ // Store so setToolbarItemImage() can update the icon when toggling state
(e.g. show/hide
+ // selected)
+ composite.setData("iconSize", size);
+ composite.setData("uniqueId", uniqueId);
GridData imageData = new GridData(SWT.LEFT, SWT.CENTER, false, false);
imageData.widthHint = size;
@@ -452,6 +456,9 @@ public class GuiToolbarWidgets extends BaseGuiWidgets {
String uniqueId = instanceId + "-" + id;
SvgLabelFacade.enable(toolItem, uniqueId, imageLabel, enabled);
}
+ // So that disabled buttons do not receive clicks in RWT
(ToolItem.setEnabled does not
+ // always prevent the control from receiving events)
+ composite.setEnabled(enabled);
}
// Also update the ToolItem state for consistency
if (enabled != toolItem.isEnabled()) {
@@ -508,6 +515,8 @@ public class GuiToolbarWidgets extends BaseGuiWidgets {
String uniqueId = instanceId + "-" + id;
SvgLabelFacade.enable(null, uniqueId, imageLabel, enable);
}
+ // So that disabled buttons do not receive clicks in RWT
+ composite.setEnabled(enable);
}
// Update ToolItem state so future checks work correctly
item.setEnabled(enable);
@@ -522,6 +531,42 @@ public class GuiToolbarWidgets extends BaseGuiWidgets {
return toolItemMap.get(id);
}
+ /**
+ * Set the image of a toolbar item by path. Use this when toggling between
two icons (e.g. show
+ * only selected / show all). In desktop SWT this sets the ToolItem's image;
in web RWT this
+ * updates the SVG in the label so the icon change is visible.
+ *
+ * @param id the toolbar item id (from @GuiToolbarElement)
+ * @param imagePath the image path (e.g. "ui/images/show-selected.svg")
+ */
+ public void setToolbarItemImage(String id, String imagePath) {
+ if (StringUtils.isEmpty(imagePath)) {
+ return;
+ }
+ ToolItem toolItem = toolItemMap.get(id);
+ if (toolItem == null || toolItem.isDisposed()) {
+ return;
+ }
+ if (EnvironmentUtils.getInstance().isWeb()) {
+ Control control = widgetsMap.get(id);
+ if (control instanceof Composite composite && !composite.isDisposed()) {
+ Control[] children = composite.getChildren();
+ if (children.length > 0 && children[0] instanceof Label imageLabel) {
+ String uniqueId = (String) composite.getData("uniqueId");
+ if (uniqueId != null) {
+ // Update img src via JavaScript so the icon updates in RWT
(setText may not re-render)
+ SvgLabelFacade.updateImageSource(uniqueId, imageLabel, imagePath);
+ }
+ }
+ }
+ } else {
+ Image image = GuiResource.getInstance().getImage(imagePath);
+ if (image != null) {
+ toolItem.setImage(image);
+ }
+ }
+ }
+
/**
* Set text on a toolbar item. Handles both SWT (desktop) and RWT (web)
environments. In SWT, sets
* text directly on the ToolItem. In RWT, updates the separate text Label
next to the image.
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/widget/svg/SvgLabelFacade.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/svg/SvgLabelFacade.java
index 80b6dad25e..d1f8af4475 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/svg/SvgLabelFacade.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/svg/SvgLabelFacade.java
@@ -55,4 +55,21 @@ public abstract class SvgLabelFacade {
}
public abstract void shadeSvgInternal(Label label, String id, boolean
shaded);
+
+ /**
+ * Update only the image source of an existing img element (e.g. when
toggling toolbar icon). In
+ * RWT this uses JavaScript to set the img src so the icon updates without
replacing the whole
+ * label markup. No-op on desktop.
+ *
+ * @param id the DOM element id of the img (same uniqueId used in setData)
+ * @param label the label widget (unused in RWT but required for API)
+ * @param imagePath the new image path (e.g. "ui/images/show-selected.svg")
+ */
+ public static void updateImageSource(String id, Label label, String
imagePath) {
+ synchronized (object) {
+ IMPL.updateImageSourceInternal(id, label, imagePath);
+ }
+ }
+
+ public abstract void updateImageSourceInternal(String id, Label label,
String imagePath);
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
index 263fc83095..0b17300468 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineGridDelegate.java
@@ -22,7 +22,9 @@ import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.ReentrantLock;
@@ -37,10 +39,12 @@ import org.apache.hop.core.row.value.ValueMetaString;
import org.apache.hop.core.util.ExecutorUtil;
import org.apache.hop.core.util.Utils;
import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.engine.EngineMetrics;
import org.apache.hop.pipeline.engine.IEngineComponent;
import org.apache.hop.pipeline.engine.IEngineMetric;
import org.apache.hop.pipeline.transform.ITransform;
+import org.apache.hop.pipeline.transform.TransformMeta;
import org.apache.hop.pipeline.transform.TransformStatus;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.gui.GuiResource;
@@ -50,23 +54,37 @@ import org.apache.hop.ui.core.widget.TableView;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
+import org.apache.hop.ui.hopgui.selection.HopGuiSelectionTracker;
import org.eclipse.swt.SWT;
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.Composite;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
+/**
+ * Delegate for the pipeline execution metrics grid tab. Manages the table of
transform metrics
+ * (rows read/written, duration, etc.), toolbar actions (open transform,
show/hide filters, copy),
+ * sorting, and double-click / selection behaviour to open the transform
configuration.
+ */
@GuiPlugin(description = "Pipeline Graph Grid Delegate")
public class HopGuiPipelineGridDelegate {
private static final Class<?> PKG = HopGui.class;
public static final String GUI_PLUGIN_TOOLBAR_PARENT_ID =
"HopGuiWorkflowGridDelegate-ToolBar";
+
+ /** Open-transform button; id 09900 so it appears first in the toolbar. */
+ public static final String TOOLBAR_ICON_OPEN_TRANSFORM =
"ToolbarIcon-09900-OpenTransform";
+
public static final String TOOLBAR_ICON_SHOW_HIDE_INACTIVE =
"ToolbarIcon-10000-ShowHideInactive";
public static final String TOOLBAR_ICON_SHOW_HIDE_SELECTED =
"ToolbarIcon-10010-ShowHideSelected";
+ public static final String TOOLBAR_ICON_COPY = "ToolbarIcon-10020-Copy";
public static final long UPDATE_TIME_VIEW = 1000L;
@@ -90,9 +108,28 @@ public class HopGuiPipelineGridDelegate {
private Timer refreshMetricsTimer;
+ /** Last sort column/direction from user (column header click); we re-apply
on each refresh. */
+ private int gridSortColumn = 0;
+
+ private boolean gridSortDescending = false;
+
+ /**
+ * Cached from last run so "show selected/inactive" filter still works after
the refresh timer
+ * stops.
+ */
+ private EngineMetrics lastEngineMetrics;
+
+ /**
+ * Search text for filtering the metrics table by transform name. Empty or
< 2 characters means
+ * no filter (show all). Matches case-insensitive substring, like the
settings panel search.
+ */
+ private String transformNameSearchText = "";
+
+ private Text searchText;
+
/**
- * @param hopGui
- * @param pipelineGraph
+ * @param hopGui Hop GUI instance
+ * @param pipelineGraph the pipeline graph that owns this delegate
*/
public HopGuiPipelineGridDelegate(HopGui hopGui, HopGuiPipelineGraph
pipelineGraph) {
this.hopGui = hopGui;
@@ -121,13 +158,20 @@ public class HopGuiPipelineGridDelegate {
pipelineGraph.addExtraView();
} else {
if (pipelineGridTab != null && !pipelineGridTab.isDisposed()) {
- // just set this one active and get out...
- // and activate the refresh timer
+ // Reusing existing grid for a new run: reset sort to default and
clear metrics cache
+ gridSortColumn = 0;
+ gridSortDescending = false;
+ lastEngineMetrics = null;
startRefreshMetricsTimer();
return;
}
}
+ gridSortColumn = 0;
+ gridSortDescending = false;
+ lastEngineMetrics = null;
+ transformNameSearchText = "";
+
pipelineGridTab = new CTabItem(pipelineGraph.extraViewTabFolder, SWT.NONE);
pipelineGridTab.setFont(GuiResource.getInstance().getFontDefault());
pipelineGridTab.setImage(GuiResource.getInstance().getImageShowGrid());
@@ -233,19 +277,21 @@ public class HopGuiPipelineGridDelegate {
SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI,
columns,
1,
- true, // readonly!
- null, // Listener
+ true, // readonly
+ null,
hopGui.getProps(),
true,
null,
false,
- false);
+ false); // no TableView toolbar; copy/filter are on our toolbar
FormData fdView = new FormData();
fdView.left = new FormAttachment(0, 0);
fdView.right = new FormAttachment(100, 0);
fdView.top = new FormAttachment(toolbar, 0);
fdView.bottom = new FormAttachment(100, 0);
pipelineGridView.setLayoutData(fdView);
+ pipelineGridView.setSortable(true);
+ attachMetricsTableListeners(pipelineGridView);
ColumnInfo numberColumn = pipelineGridView.getNumberColumn();
IValueMeta numberColumnValueMeta =
@@ -254,6 +300,18 @@ public class HopGuiPipelineGridDelegate {
startRefreshMetricsTimer();
pipelineGridTab.addDisposeListener(disposeEvent ->
stopRefreshMetricsTimer());
+ HopGuiSelectionTracker.getInstance()
+ .addSelectionListener(
+ HopGuiSelectionTracker.SelectionType.PIPELINE_GRAPH,
+ () -> {
+ if (!showSelectedTransforms) {
+ return;
+ }
+ if (pipelineGridView == null || pipelineGridView.isDisposed()) {
+ return;
+ }
+ hopGui.getDisplay().asyncExec(this::refreshView);
+ });
pipelineGridTab.setControl(pipelineGridComposite);
}
@@ -318,9 +376,39 @@ public class HopGuiPipelineGridDelegate {
toolbarWidget = new GuiToolbarWidgets();
toolbarWidget.registerGuiPluginObject(this);
toolbarWidget.createToolbarWidgets(toolbar, GUI_PLUGIN_TOOLBAR_PARENT_ID);
+
+ // Search bar: filter table by transform name (smart search, min 2 chars,
case-insensitive)
+ ToolItem searchSeparator = new ToolItem(toolbar, SWT.SEPARATOR);
+ searchText = new Text(toolbar, SWT.SEARCH | SWT.ICON_SEARCH |
SWT.ICON_CANCEL | SWT.BORDER);
+ searchText.setMessage(
+ BaseMessages.getString(PKG,
"PipelineLog.Search.TransformName.Placeholder"));
+ PropsUi.setLook(searchText, Props.WIDGET_STYLE_TOOLBAR);
+ searchText.addListener(
+ SWT.Modify,
+ e -> {
+ if (searchText == null || searchText.isDisposed()) {
+ return;
+ }
+ String raw = searchText.getText();
+ transformNameSearchText = raw != null ? raw.trim() : "";
+ refreshView();
+ });
+ searchSeparator.setControl(searchText);
+ searchSeparator.setWidth(260);
+
toolbar.pack();
}
+ /** Opens the transform configuration for the selected metrics row (same as
double-click). */
+ @GuiToolbarElement(
+ root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+ id = TOOLBAR_ICON_OPEN_TRANSFORM,
+ toolTip =
"i18n::HopGuiPipelineGridDelegate.Toolbar.OpenTransform.Tooltip",
+ image = "ui/images/edit.svg")
+ public void openSelectedTransform() {
+ openTransformForSelectedRow();
+ }
+
@GuiToolbarElement(
root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
id = TOOLBAR_ICON_SHOW_HIDE_INACTIVE,
@@ -329,14 +417,8 @@ public class HopGuiPipelineGridDelegate {
public void showHideInactive() {
hideInactiveTransforms = !hideInactiveTransforms;
- ToolItem toolItem =
toolbarWidget.findToolItem(TOOLBAR_ICON_SHOW_HIDE_INACTIVE);
- if (toolItem != null) {
- if (hideInactiveTransforms) {
- toolItem.setImage(GuiResource.getInstance().getImageHide());
- } else {
- toolItem.setImage(GuiResource.getInstance().getImageShow());
- }
- }
+ String imagePath = hideInactiveTransforms ? "ui/images/hide.svg" :
"ui/images/show.svg";
+ toolbarWidget.setToolbarItemImage(TOOLBAR_ICON_SHOW_HIDE_INACTIVE,
imagePath);
refreshView();
}
@@ -348,208 +430,438 @@ public class HopGuiPipelineGridDelegate {
public void showHideSelected() {
showSelectedTransforms = !showSelectedTransforms;
- ToolItem toolItem =
toolbarWidget.findToolItem(TOOLBAR_ICON_SHOW_HIDE_SELECTED);
- if (toolItem != null) {
- if (showSelectedTransforms) {
- toolItem.setImage(GuiResource.getInstance().getImageShowSelected());
- } else {
- toolItem.setImage(GuiResource.getInstance().getImageShowAll());
- }
- }
+ String imagePath =
+ showSelectedTransforms ? "ui/images/show-selected.svg" :
"ui/images/show-all.svg";
+ toolbarWidget.setToolbarItemImage(TOOLBAR_ICON_SHOW_HIDE_SELECTED,
imagePath);
refreshView();
}
+ @GuiToolbarElement(
+ root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+ id = TOOLBAR_ICON_COPY,
+ toolTip = "i18n::TableView.ToolBarWidget.CopySelected.ToolTip",
+ image = "ui/images/copy.svg",
+ separator = true)
+ public void copyMetricsToClipboard() {
+ if (pipelineGridView == null || pipelineGridView.isDisposed()) {
+ return;
+ }
+ Table table = pipelineGridView.table;
+ boolean hadSelection = table.getSelectionCount() > 0;
+ if (!hadSelection) {
+ table.selectAll();
+ }
+ pipelineGridView.clipSelected();
+ if (!hadSelection) {
+ table.deselectAll();
+ }
+ }
+
private void refreshView() {
refreshViewLock.lock();
try {
- if (pipelineGraph.pipeline == null
- || pipelineGridView == null
- || pipelineGridView.isDisposed()) {
+ if (pipelineGridView == null || pipelineGridView.isDisposed()) {
return;
}
- // Get the metrics from the engine
- //
- EngineMetrics engineMetrics = pipelineGraph.pipeline.getEngineMetrics();
- List<IEngineComponent> shownComponents = new ArrayList<>();
- for (IEngineComponent component : engineMetrics.getComponents()) {
- boolean select = true;
- // If we hide inactive components we only want to see stuff running
- //
- select = select && (!hideInactiveTransforms || component.isRunning());
-
- // If we opted to only see selected components...
- //
- select = select && (!showSelectedTransforms || component.isSelected());
-
- if (select) {
- shownComponents.add(component);
- }
+ EngineMetrics engineMetrics = null;
+ if (pipelineGraph.pipeline != null) {
+ engineMetrics = pipelineGraph.pipeline.getEngineMetrics();
+ lastEngineMetrics = engineMetrics;
+ } else if (lastEngineMetrics != null) {
+ engineMetrics = lastEngineMetrics;
}
+ if (engineMetrics == null) {
+ return;
+ }
+ Set<String> selectedTransformNames =
getSelectedTransformNamesFromCanvas();
+ List<IEngineComponent> shownComponents =
+ getShownComponents(engineMetrics, selectedTransformNames);
+ List<IEngineMetric> usedMetrics = getUsedMetrics(engineMetrics);
+ List<ColumnInfo> columns = buildColumnList(usedMetrics);
+ List<List<String>> componentStringsList =
+ buildComponentStringsList(engineMetrics, shownComponents,
usedMetrics);
- // Build a list of columns to show...
- //
- List<ColumnInfo> columns = new ArrayList<>();
+ recreateTableIfColumnsChanged(columns, shownComponents.size());
- // First the name of the component (transform):
- // Then the copy number
- //
- columns.add(
- new ColumnInfo(
- BaseMessages.getString(PKG, "PipelineLog.Column.TransformName"),
- ColumnInfo.COLUMN_TYPE_TEXT,
- false,
- true));
- ColumnInfo copyColumn =
- new ColumnInfo(
- BaseMessages.getString(PKG, "PipelineLog.Column.Copynr"),
- ColumnInfo.COLUMN_TYPE_TEXT,
- true,
- true);
- copyColumn.setAlignment(SWT.RIGHT);
- columns.add(copyColumn);
+ sortComponentStringsByColumn(componentStringsList, gridSortColumn,
gridSortDescending);
- List<IEngineMetric> usedMetrics = new
ArrayList<>(engineMetrics.getMetricsList());
- Collections.sort(
- usedMetrics, (o1, o2) ->
o1.getDisplayPriority().compareTo(o2.getDisplayPriority()));
+ fillTableRows(componentStringsList);
- for (IEngineMetric metric : usedMetrics) {
- ColumnInfo column =
- new ColumnInfo(
- metric.getHeader(), ColumnInfo.COLUMN_TYPE_TEXT,
metric.isNumeric(), true);
- column.setToolTip(metric.getTooltip());
- IValueMeta stringMeta = new ValueMetaString(metric.getCode());
- ValueMetaInteger valueMeta = new ValueMetaInteger(metric.getCode(),
15, 0);
- valueMeta.setConversionMask(METRICS_FORMAT);
- stringMeta.setConversionMetadata(valueMeta);
- column.setValueMeta(stringMeta);
- column.setAlignment(SWT.RIGHT);
- columns.add(column);
- }
+ setSortIndicator();
- IValueMeta stringMeta = new ValueMetaString("string");
+ pipelineGridView.optWidth(true);
+ previousRefreshColumns = columns;
+ updateEditButtonState();
+ } finally {
+ refreshViewLock.unlock();
+ }
+ }
- // Duration?
- //
- ColumnInfo durationColumn =
- new ColumnInfo(
- BaseMessages.getString(PKG, "PipelineLog.Column.Duration"),
- ColumnInfo.COLUMN_TYPE_TEXT,
- false,
- true);
- durationColumn.setValueMeta(stringMeta);
- durationColumn.setAlignment(SWT.RIGHT);
- columns.add(durationColumn);
-
- // Also add the status and speed
- //
- ValueMetaInteger speedMeta = new ValueMetaInteger("speed", 15, 0);
- speedMeta.setConversionMask(" ###,###,###,##0");
- stringMeta.setConversionMetadata(speedMeta);
- ColumnInfo speedColumn =
- new ColumnInfo(
- BaseMessages.getString(PKG, "PipelineLog.Column.Speed"),
- ColumnInfo.COLUMN_TYPE_TEXT,
- false,
- true);
- speedColumn.setValueMeta(stringMeta);
- speedColumn.setAlignment(SWT.RIGHT);
- columns.add(speedColumn);
+ /**
+ * Current canvas selection (transform names). Used so "only show selected"
reflects the latest
+ * selection even when the refresh timer has stopped and we're using cached
engine metrics.
+ */
+ private Set<String> getSelectedTransformNamesFromCanvas() {
+ if (pipelineGraph.getPipelineMeta() == null) {
+ return null;
+ }
+ List<TransformMeta> selected =
pipelineGraph.getPipelineMeta().getSelectedTransforms();
+ if (selected == null || selected.isEmpty()) {
+ return null;
+ }
+ Set<String> names = new HashSet<>();
+ for (TransformMeta t : selected) {
+ if (t != null && t.getName() != null) {
+ names.add(t.getName());
+ }
+ }
+ return names.isEmpty() ? null : names;
+ }
- columns.add(
- new ColumnInfo(
- BaseMessages.getString(PKG, "PipelineLog.Column.Status"),
- ColumnInfo.COLUMN_TYPE_TEXT,
- false,
- true));
-
- // The data in the grid...
- //
- List<List<String>> componentStringsList = new ArrayList<>();
- int row = 1;
- for (IEngineComponent component : shownComponents) {
- List<String> componentStrings = new ArrayList<>();
-
- componentStrings.add(Integer.toString(row++));
- componentStrings.add(Const.NVL(component.getName(), ""));
- componentStrings.add(Integer.toString(component.getCopyNr()));
-
- for (IEngineMetric metric : usedMetrics) {
- Long value = engineMetrics.getComponentMetric(component, metric);
- componentStrings.add(value == null ? "" : formatMetric(value));
+ private List<IEngineComponent> getShownComponents(
+ EngineMetrics engineMetrics, Set<String> selectedTransformNames) {
+ List<IEngineComponent> shownComponents = new ArrayList<>();
+ for (IEngineComponent component : engineMetrics.getComponents()) {
+ boolean select = true;
+ select = select && (!hideInactiveTransforms || component.isRunning());
+ if (showSelectedTransforms
+ && selectedTransformNames != null
+ && !selectedTransformNames.isEmpty()) {
+ select = select &&
selectedTransformNames.contains(component.getName());
+ }
+ // When "show selected" is on but no transforms are selected on canvas:
show all
+ if (select) {
+ shownComponents.add(component);
+ }
+ }
+ // Smart search filter by transform name (like settings panel: min 2
chars, case-insensitive)
+ if (transformNameSearchText != null && transformNameSearchText.length() >=
2) {
+ String lowerSearch = transformNameSearchText.toLowerCase();
+ List<IEngineComponent> filtered = new ArrayList<>();
+ for (IEngineComponent c : shownComponents) {
+ String name = c.getName();
+ if (name != null && name.toLowerCase().contains(lowerSearch)) {
+ filtered.add(c);
}
- String duration = calculateDuration(component);
- componentStrings.add(duration);
- String speed = engineMetrics.getComponentSpeedMap().get(component);
- componentStrings.add(Const.NVL(speed, ""));
- String status = engineMetrics.getComponentStatusMap().get(component);
- componentStrings.add(Const.NVL(status, ""));
-
- componentStringsList.add(componentStrings);
}
+ shownComponents = filtered;
+ }
+ return shownComponents;
+ }
- // So now we have the columns and the content of the grid...
- //
- // If the number of columns has changed since the last refresh we
rebuild the table.
- //
- if (haveColumnsChanged(columns)) {
- // Remove the old stuff on the composite...
- //
- pipelineGridView.dispose();
- pipelineGridView =
- new TableView(
- pipelineGraph.getVariables(),
- pipelineGridComposite,
- SWT.NONE,
- columns.toArray(new ColumnInfo[0]),
- shownComponents.size(),
- true,
- null,
- PropsUi.getInstance(),
- true,
- null,
- false,
- false);
- pipelineGridView.setSortable(false);
- FormData fdView = new FormData();
- fdView.left = new FormAttachment(0, 0);
- fdView.right = new FormAttachment(100, 0);
- fdView.top = new FormAttachment(toolbar, 0);
- fdView.bottom = new FormAttachment(100, 0);
- pipelineGridView.setLayoutData(fdView);
- pipelineGridComposite.layout(true, true);
- }
+ private List<IEngineMetric> getUsedMetrics(EngineMetrics engineMetrics) {
+ List<IEngineMetric> usedMetrics = new
ArrayList<>(engineMetrics.getMetricsList());
+ Collections.sort(
+ usedMetrics, (o1, o2) ->
o1.getDisplayPriority().compareTo(o2.getDisplayPriority()));
+ return usedMetrics;
+ }
+
+ private List<ColumnInfo> buildColumnList(List<IEngineMetric> usedMetrics) {
+ List<ColumnInfo> columns = new ArrayList<>();
+
+ columns.add(
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "PipelineLog.Column.TransformName"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ true));
+
+ ColumnInfo copyColumn =
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "PipelineLog.Column.Copynr"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ true,
+ true);
+ copyColumn.setAlignment(SWT.RIGHT);
+ columns.add(copyColumn);
+
+ for (IEngineMetric metric : usedMetrics) {
+ ColumnInfo column =
+ new ColumnInfo(metric.getHeader(), ColumnInfo.COLUMN_TYPE_TEXT,
metric.isNumeric(), true);
+ column.setToolTip(metric.getTooltip());
+ IValueMeta stringMeta = new ValueMetaString(metric.getCode());
+ ValueMetaInteger valueMeta = new ValueMetaInteger(metric.getCode(), 15,
0);
+ valueMeta.setConversionMask(METRICS_FORMAT);
+ stringMeta.setConversionMetadata(valueMeta);
+ column.setValueMeta(stringMeta);
+ column.setAlignment(SWT.RIGHT);
+ columns.add(column);
+ }
+
+ IValueMeta stringMeta = new ValueMetaString("string");
+
+ ColumnInfo durationColumn =
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "PipelineLog.Column.Duration"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ true);
+ durationColumn.setValueMeta(stringMeta);
+ durationColumn.setAlignment(SWT.RIGHT);
+ columns.add(durationColumn);
+
+ ValueMetaInteger speedMeta = new ValueMetaInteger("speed", 15, 0);
+ speedMeta.setConversionMask(" ###,###,###,##0");
+ stringMeta.setConversionMetadata(speedMeta);
+ ColumnInfo speedColumn =
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "PipelineLog.Column.Speed"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ true);
+ speedColumn.setValueMeta(stringMeta);
+ speedColumn.setAlignment(SWT.RIGHT);
+ columns.add(speedColumn);
+
+ columns.add(
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "PipelineLog.Column.Status"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ true));
- // If the number of rows in the table is different then we need to
remove all rows and
- // rebuild.
- // Otherwise we're just going to re-use the table items and put new
values on the cells...
- //
- while (pipelineGridView.table.getItemCount() >
componentStringsList.size()) {
- pipelineGridView.table.remove(pipelineGridView.table.getItemCount() -
1);
+ return columns;
+ }
+
+ private List<List<String>> buildComponentStringsList(
+ EngineMetrics engineMetrics,
+ List<IEngineComponent> shownComponents,
+ List<IEngineMetric> usedMetrics) {
+ List<List<String>> componentStringsList = new ArrayList<>();
+ int rowNum = 1;
+ for (IEngineComponent component : shownComponents) {
+ List<String> componentStrings = new ArrayList<>();
+ componentStrings.add(Integer.toString(rowNum++));
+ componentStrings.add(Const.NVL(component.getName(), ""));
+ componentStrings.add(Integer.toString(component.getCopyNr()));
+
+ for (IEngineMetric metric : usedMetrics) {
+ Long value = engineMetrics.getComponentMetric(component, metric);
+ componentStrings.add(value == null ? "" : formatMetric(value));
}
+ componentStrings.add(calculateDuration(component));
+
componentStrings.add(Const.NVL(engineMetrics.getComponentSpeedMap().get(component),
""));
+
componentStrings.add(Const.NVL(engineMetrics.getComponentStatusMap().get(component),
""));
- for (row = 0; row < componentStringsList.size(); row++) {
- List<String> componentStrings = componentStringsList.get(row);
+ componentStringsList.add(componentStrings);
+ }
+ return componentStringsList;
+ }
- TableItem item;
- if (row < pipelineGridView.table.getItemCount()) {
- item = pipelineGridView.table.getItem(row);
- } else {
- item = new TableItem(pipelineGridView.table, SWT.NONE);
- }
+ private void recreateTableIfColumnsChanged(List<ColumnInfo> columns, int
rowCount) {
+ if (!haveColumnsChanged(columns)) {
+ return;
+ }
+ pipelineGridView.dispose();
+ pipelineGridView =
+ new TableView(
+ pipelineGraph.getVariables(),
+ pipelineGridComposite,
+ SWT.NONE,
+ columns.toArray(new ColumnInfo[0]),
+ rowCount,
+ true,
+ null,
+ PropsUi.getInstance(),
+ true,
+ null,
+ false,
+ false); // no TableView toolbar; copy/filter are on our toolbar
+ pipelineGridView.setSortable(true);
+ attachMetricsTableListeners(pipelineGridView);
+ FormData fdView = new FormData();
+ fdView.left = new FormAttachment(0, 0);
+ fdView.right = new FormAttachment(100, 0);
+ fdView.top = new FormAttachment(toolbar, 0);
+ fdView.bottom = new FormAttachment(100, 0);
+ pipelineGridView.setLayoutData(fdView);
+ pipelineGridComposite.layout(true, true);
+ }
- for (int col = 0; col < componentStrings.size(); col++) {
- item.setText(col, componentStrings.get(col));
- }
+ private void fillTableRows(List<List<String>> componentStringsList) {
+ while (pipelineGridView.table.getItemCount() >
componentStringsList.size()) {
+ pipelineGridView.table.remove(pipelineGridView.table.getItemCount() - 1);
+ }
+ for (int row = 0; row < componentStringsList.size(); row++) {
+ List<String> componentStrings = componentStringsList.get(row);
+ TableItem item;
+ if (row < pipelineGridView.table.getItemCount()) {
+ item = pipelineGridView.table.getItem(row);
+ } else {
+ item = new TableItem(pipelineGridView.table, SWT.NONE);
+ }
+ for (int col = 0; col < componentStrings.size(); col++) {
+ item.setText(col, componentStrings.get(col));
}
+ }
+ }
- // Optimize the view...
- //
- pipelineGridView.optWidth(true);
+ /**
+ * Attaches all table listeners used by the metrics grid: sort capture,
double-click to open
+ * transform, and edit toolbar button enable/disable. Call after creating or
recreating the table.
+ */
+ private void attachMetricsTableListeners(TableView view) {
+ attachSortListener(view);
+ attachDoubleClickListener(view);
+ attachEditButtonStateListener(view);
+ }
- previousRefreshColumns = columns;
- } finally {
- refreshViewLock.unlock();
+ /**
+ * Attach listener to column headers so we remember sort column/direction
for the next refresh.
+ */
+ private void attachSortListener(TableView view) {
+ Table table = view.table;
+ for (int i = 0; i < table.getColumnCount(); i++) {
+ table.getColumn(i).addListener(SWT.Selection, e -> captureSortState());
+ }
+ }
+
+ /** On double-click, open the transform configuration for the selected row
(by transform name). */
+ private void attachDoubleClickListener(TableView view) {
+ Table table = view.table;
+ table.addListener(SWT.DefaultSelection, e ->
openTransformForSelectedRow());
+ }
+
+ /**
+ * Enable/disable the edit (open transform) toolbar button based on table
row selection. TableView
+ * sets selection inside its MouseDown (editSelected -> setSelection); the
Table often does not
+ * fire Selection until after mouse handling, so the first click does not
run our Selection
+ * listener. We run updateEditButtonState on MouseDown via asyncExec so it
runs after TableView
+ * has applied the selection.
+ */
+ private void attachEditButtonStateListener(TableView view) {
+ Table table = view.table;
+ table.addListener(SWT.Selection, e -> updateEditButtonState());
+ table.addListener(
+ SWT.MouseDown,
+ e -> {
+ if (table.isDisposed()) {
+ return;
+ }
+ hopGui.getDisplay().asyncExec(this::updateEditButtonState);
+ });
+ updateEditButtonState();
+ }
+
+ /**
+ * Enables or disables the open-transform toolbar button based on whether a
table row is selected.
+ */
+ private void updateEditButtonState() {
+ if (toolbarWidget == null || toolbar == null || toolbar.isDisposed()) {
+ return;
+ }
+ boolean linesSelected =
+ pipelineGridView != null
+ && !pipelineGridView.isDisposed()
+ && pipelineGridView.table.getSelectionCount() > 0;
+ toolbarWidget.enableToolbarItem(TOOLBAR_ICON_OPEN_TRANSFORM,
linesSelected);
+ }
+
+ /**
+ * Opens the transform configuration dialog for the selected table row.
Reads the transform name
+ * from column 1 (see {@link #buildComponentStringsList}); finds the {@link
TransformMeta} and
+ * calls {@link HopGuiPipelineGraph#editTransform(PipelineMeta,
TransformMeta)}.
+ */
+ private void openTransformForSelectedRow() {
+ if (pipelineGridView == null || pipelineGridView.isDisposed()) {
+ return;
+ }
+ Table table = pipelineGridView.table;
+ TableItem[] selection = table.getSelection();
+ if (selection == null || selection.length == 0) {
+ return;
+ }
+ // Column 0 = #, column 1 = transform name (matches
buildComponentStringsList)
+ String transformName = selection[0].getText(1);
+ if (Utils.isEmpty(transformName)) {
+ return;
+ }
+ PipelineMeta pipelineMeta = pipelineGraph.getPipelineMeta();
+ if (pipelineMeta == null) {
+ return;
+ }
+ TransformMeta transformMeta = pipelineMeta.findTransform(transformName);
+ if (transformMeta == null) {
+ return;
+ }
+ pipelineGraph.editTransform(pipelineMeta, transformMeta);
+ }
+
+ private void captureSortState() {
+ if (pipelineGridView == null || pipelineGridView.isDisposed()) {
+ return;
+ }
+ hopGui
+ .getDisplay()
+ .asyncExec(
+ () -> {
+ if (pipelineGridView == null || pipelineGridView.isDisposed()) {
+ return;
+ }
+ Table table = pipelineGridView.table;
+ TableColumn sortCol = table.getSortColumn();
+ if (sortCol != null) {
+ gridSortColumn = table.indexOf(sortCol);
+ gridSortDescending = (table.getSortDirection() == SWT.DOWN);
+ }
+ });
+ }
+
+ /**
+ * Sort the row data by the given column and direction before writing to the
table. Column index 0
+ * = #, 1 = name, 2 = copy, 3+ = metrics, then duration, speed, status.
Numeric columns (formatted
+ * with commas) are compared as numbers; others as strings.
+ */
+ private void sortComponentStringsByColumn(
+ List<List<String>> rows, int sortColumn, boolean descending) {
+ if (sortColumn < 0 || rows.isEmpty()) {
+ return;
+ }
+ int col = sortColumn;
+ boolean desc = descending;
+ rows.sort(
+ (a, b) -> {
+ String sa = col < a.size() ? Const.NVL(a.get(col), "") : "";
+ String sb = col < b.size() ? Const.NVL(b.get(col), "") : "";
+ int c = compareCellValues(sa, sb);
+ return desc ? -c : c;
+ });
+ }
+
+ private int compareCellValues(String a, String b) {
+ String sa = a != null ? a : "";
+ String sb = b != null ? b : "";
+ try {
+ long na = parseFormattedLong(sa);
+ long nb = parseFormattedLong(sb);
+ return Long.compare(na, nb);
+ } catch (NumberFormatException e) {
+ // not both numeric, compare as strings
+ }
+ return sa.compareToIgnoreCase(sb);
+ }
+
+ private static long parseFormattedLong(String s) {
+ if (s == null || s.isEmpty()) {
+ return 0L;
+ }
+ String trimmed = s.trim();
+ if (trimmed.isEmpty()) {
+ return 0L;
+ }
+ return Long.parseLong(trimmed.replace(",", ""));
+ }
+
+ /** Update the table header to show which column is sorted and in which
direction. */
+ private void setSortIndicator() {
+ if (pipelineGridView == null || pipelineGridView.isDisposed()) {
+ return;
+ }
+ Table table = pipelineGridView.table;
+ if (gridSortColumn >= 0 && gridSortColumn < table.getColumnCount()) {
+ table.setSortColumn(table.getColumn(gridSortColumn));
+ table.setSortDirection(gridSortDescending ? SWT.DOWN : SWT.UP);
}
}
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 41b7720563..11439a2b66 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
@@ -316,15 +316,21 @@ public class HopGuiPipelineLogDelegate {
image = "ui/images/pause.svg",
separator = true)
public void pauseLog() {
- ToolItem item = toolBarWidgets.findToolItem(TOOLBAR_ICON_LOG_PAUSE_RESUME);
if (logBrowser.isPaused()) {
logBrowser.setPaused(false);
- item.setImage(GuiResource.getInstance().getImagePause());
- item.setToolTipText(BaseMessages.getString(PKG,
"PipelineLog.Dialog.Pause.Tooltip"));
+ toolBarWidgets.setToolbarItemImage(TOOLBAR_ICON_LOG_PAUSE_RESUME,
"ui/images/pause.svg");
+ setPauseResumeTooltip("PipelineLog.Dialog.Pause.Tooltip");
} else {
logBrowser.setPaused(true);
- item.setImage(GuiResource.getInstance().getImageRun());
- item.setToolTipText(BaseMessages.getString(PKG,
"PipelineLog.Dialog.Resume.Tooltip"));
+ toolBarWidgets.setToolbarItemImage(TOOLBAR_ICON_LOG_PAUSE_RESUME,
"ui/images/run.svg");
+ setPauseResumeTooltip("PipelineLog.Dialog.Resume.Tooltip");
+ }
+ }
+
+ private void setPauseResumeTooltip(String messageKey) {
+ ToolItem item = toolBarWidgets.findToolItem(TOOLBAR_ICON_LOG_PAUSE_RESUME);
+ if (item != null && !item.isDisposed()) {
+ item.setToolTipText(BaseMessages.getString(PKG, messageKey));
}
}
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 50964aea83..772a48deb6 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
@@ -48,7 +48,6 @@ import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.ToolBar;
-import org.eclipse.swt.widgets.ToolItem;
@GuiPlugin(description = "Workflow Graph Log Delegate")
public class HopGuiWorkflowLogDelegate {
@@ -297,13 +296,12 @@ public class HopGuiWorkflowLogDelegate {
image = "ui/images/pause.svg",
separator = true)
public void pauseLog() {
- ToolItem item = toolBarWidgets.findToolItem(TOOLBAR_ICON_LOG_PAUSE_RESUME);
if (logBrowser.isPaused()) {
logBrowser.setPaused(false);
- item.setImage(GuiResource.getInstance().getImageRun());
+ toolBarWidgets.setToolbarItemImage(TOOLBAR_ICON_LOG_PAUSE_RESUME,
"ui/images/pause.svg");
} else {
logBrowser.setPaused(true);
- item.setImage(GuiResource.getInstance().getImagePause());
+ toolBarWidgets.setToolbarItemImage(TOOLBAR_ICON_LOG_PAUSE_RESUME,
"ui/images/run.svg");
}
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/selection/HopGuiSelectionTracker.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/selection/HopGuiSelectionTracker.java
index 6957873f95..151efe3e26 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/selection/HopGuiSelectionTracker.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/selection/HopGuiSelectionTracker.java
@@ -17,10 +17,18 @@
package org.apache.hop.ui.hopgui.selection;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
/**
* Tracks the last selected item type to help route keyboard shortcuts
correctly. This is needed
* when multiple components (like file explorer and pipeline graph) share the
same keyboard
* shortcuts but need to act on different types of items.
+ *
+ * <p>Listeners can register to be notified when a given selection type is set
(e.g. pipeline graph
+ * selection changed).
*/
public class HopGuiSelectionTracker {
@@ -40,8 +48,13 @@ public class HopGuiSelectionTracker {
private SelectionType lastSelectionType = SelectionType.NONE;
+ private final Map<SelectionType, List<Runnable>> selectionListeners =
+ new EnumMap<>(SelectionType.class);
+
private HopGuiSelectionTracker() {
- // Singleton
+ for (SelectionType t : SelectionType.values()) {
+ selectionListeners.put(t, new ArrayList<>());
+ }
}
public static HopGuiSelectionTracker getInstance() {
@@ -52,12 +65,25 @@ public class HopGuiSelectionTracker {
}
/**
- * Set the last selected item type
+ * Add a listener that runs when the given selection type is set.
+ *
+ * @param selectionType the type to listen for
+ * @param listener runnable to execute when
setLastSelectionType(selectionType) is called
+ */
+ public void addSelectionListener(SelectionType selectionType, Runnable
listener) {
+ selectionListeners.get(selectionType).add(listener);
+ }
+
+ /**
+ * Set the last selected item type and notify any listeners for this type.
*
* @param selectionType The type of item that was selected
*/
public void setLastSelectionType(SelectionType selectionType) {
this.lastSelectionType = selectionType;
+ for (Runnable listener : selectionListeners.get(selectionType)) {
+ listener.run();
+ }
}
/**
diff --git a/ui/src/main/java/org/apache/hop/ui/util/SwtSvgImageUtil.java
b/ui/src/main/java/org/apache/hop/ui/util/SwtSvgImageUtil.java
index e27f3cc50e..7a5c3b80e6 100644
--- a/ui/src/main/java/org/apache/hop/ui/util/SwtSvgImageUtil.java
+++ b/ui/src/main/java/org/apache/hop/ui/util/SwtSvgImageUtil.java
@@ -224,8 +224,14 @@ public class SwtSvgImageUtil {
return loadFromClassLoader(cl, location);
}
- /** Internal image loading from Hop's user.dir VFS. */
+ /**
+ * Internal image loading from Hop's user.dir VFS. Returns null if base is
null or on any
+ * exception (e.g. VFS files-cache not set), so callers can fall back to
other loaders.
+ */
private static SwtUniversalImage loadFromBasedVFS(Display display, String
location) {
+ if (base == null) {
+ return null;
+ }
try {
FileObject imageFileObject =
HopVfs.getFileSystemManager().resolveFile(base, location);
InputStream s = HopVfs.getInputStream(imageFileObject);
@@ -239,6 +245,11 @@ public class SwtSvgImageUtil {
}
} catch (FileSystemException ex) {
return null;
+ } catch (RuntimeException ex) {
+ // e.g. NPE when VFS files-cache is not set
(AbstractFileSystem.getFilesCache())
+ log.logDebug(
+ "VFS-based image load failed for [" + location + "], will try other
loaders", ex);
+ return null;
}
}
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 c7a7081a4f..fef49ce307 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
@@ -220,6 +220,7 @@ PipelineLog.Button.LogSettings=\ &Log settings
PipelineLog.Button.ShowErrorLines=\ &Show error lines
PipelineLog.Button.ShowOnlyActiveTransforms=Hide inactive
PipelineLog.Button.ShowOnlySelectedTransforms=Show only selected transforms
+PipelineLog.Search.TransformName.Placeholder=Filter by transform name...
PipelineLog.Column.Active=Active
PipelineLog.Column.BuffersInput=Buffers Input
PipelineLog.Column.BuffersOutput=Buffers Output