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 d4cea5c834 issue #2267 : show failed and stalled icons in execution 
information (#7951)
d4cea5c834 is described below

commit d4cea5c8346c075fa189c7fb7363b023a4c2dd19
Author: Matt Casters <[email protected]>
AuthorDate: Sat Aug 15 10:19:27 2026 +0200

    issue #2267 : show failed and stalled icons in execution information (#7951)
    
    Add pipeline/workflow error and stalled SVG icons and use them in the
    execution information tree and viewer tabs. Keep the existing row
    background colors as a secondary cue.
---
 .../hop-gui/perspective-execution-information.adoc | 16 +++++
 .../org/apache/hop/ui/core/gui/GuiResource.java    | 36 ++++++++++
 .../execution/ExecutionPerspective.java            | 31 ++++++--
 .../perspective/execution/ExecutionStatusIcon.java | 68 ++++++++++++++++++
 .../execution/PipelineExecutionViewer.java         |  3 +-
 .../execution/WorkflowExecutionViewer.java         |  3 +-
 .../hop/ui/hopgui/shared/BaseExecutionViewer.java  | 15 ++++
 ui/src/main/resources/ui/images/pipeline-error.svg |  9 +++
 .../main/resources/ui/images/pipeline-stalled.svg  |  9 +++
 ui/src/main/resources/ui/images/workflow-error.svg |  8 +++
 .../main/resources/ui/images/workflow-stalled.svg  |  8 +++
 .../execution/ExecutionStatusIconTest.java         | 82 ++++++++++++++++++++++
 12 files changed, 279 insertions(+), 9 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-execution-information.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-execution-information.adoc
index 89f70207ab..c875a808b6 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-execution-information.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-execution-information.adoc
@@ -56,6 +56,22 @@ For example:
 
 image::execution-information-perspective-left-panel-example.png[An example 
left panel]
 
+The pipeline and workflow icons also show execution status:
+
+|===
+|Icon |Meaning
+
+|Default pipeline or workflow icon
+|Finished successfully, or still running. Running rows also use a light-blue 
background.
+
+|Pipeline or workflow icon with a red exclamation mark
+|The execution failed. Failed rows also use a light-red background.
+
+|Pipeline or workflow icon with an orange question mark
+|The execution is stalled: no state update was received within the location 
logging interval. Stalled rows also use a light-gray background.
+
+|===
+
 Possible action:
 
 |===
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
index ab6bda3f1c..9a9960c5a6 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
@@ -149,9 +149,13 @@ public class GuiResource {
   private SwtUniversalImage imageHash;
   private SwtUniversalImage imagePipeline;
   private SwtUniversalImage imagePipelineDisabled;
+  private SwtUniversalImage imagePipelineError;
+  private SwtUniversalImage imagePipelineStalled;
   private SwtUniversalImage imagePartitionSchema;
   private SwtUniversalImage imageWorkflow;
   private SwtUniversalImage imageWorkflowDisabled;
+  private SwtUniversalImage imageWorkflowError;
+  private SwtUniversalImage imageWorkflowStalled;
   private SwtUniversalImage imageArrowDefault;
   private SwtUniversalImage imageArrowTrue;
   private SwtUniversalImage imageArrowFalse;
@@ -459,9 +463,13 @@ public class GuiResource {
     imageHash.dispose();
     imagePipeline.dispose();
     imagePipelineDisabled.dispose();
+    imagePipelineError.dispose();
+    imagePipelineStalled.dispose();
     imagePartitionSchema.dispose();
     imageWorkflow.dispose();
     imageWorkflowDisabled.dispose();
+    imageWorkflowError.dispose();
+    imageWorkflowStalled.dispose();
     imageCopyRows.dispose();
     imageCopyRowsDisabled.dispose();
     imageError.dispose();
@@ -843,9 +851,17 @@ public class GuiResource {
     imagePipeline = SwtSvgImageUtil.getImageAsResource(display, 
"ui/images/pipeline.svg");
     imagePipelineDisabled =
         SwtSvgImageUtil.getImageAsResource(display, 
"ui/images/pipeline-disabled.svg");
+    imagePipelineError =
+        SwtSvgImageUtil.getImageAsResource(display, 
"ui/images/pipeline-error.svg");
+    imagePipelineStalled =
+        SwtSvgImageUtil.getImageAsResource(display, 
"ui/images/pipeline-stalled.svg");
     imageWorkflow = SwtSvgImageUtil.getImageAsResource(display, 
"ui/images/workflow.svg");
     imageWorkflowDisabled =
         SwtSvgImageUtil.getImageAsResource(display, 
"ui/images/workflow-disabled.svg");
+    imageWorkflowError =
+        SwtSvgImageUtil.getImageAsResource(display, 
"ui/images/workflow-error.svg");
+    imageWorkflowStalled =
+        SwtSvgImageUtil.getImageAsResource(display, 
"ui/images/workflow-stalled.svg");
     imageServer = SwtSvgImageUtil.getImageAsResource(display, 
"ui/images/server.svg");
     imagePreview = SwtSvgImageUtil.getImageAsResource(display, 
"ui/images/preview.svg");
     imageTrue = SwtSvgImageUtil.getImageAsResource(display, 
"ui/images/true.svg");
@@ -1250,6 +1266,16 @@ public class GuiResource {
         imagePipelineDisabled, display, ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE);
   }
 
+  public Image getImagePipelineError() {
+    return getZoomedImaged(
+        imagePipelineError, display, ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE);
+  }
+
+  public Image getImagePipelineStalled() {
+    return getZoomedImaged(
+        imagePipelineStalled, display, ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE);
+  }
+
   @Deprecated
   public Image getImageClosePanel() {
     return imageClose;
@@ -1276,6 +1302,16 @@ public class GuiResource {
         imageWorkflowDisabled, display, ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE);
   }
 
+  public Image getImageWorkflowError() {
+    return getZoomedImaged(
+        imageWorkflowError, display, ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE);
+  }
+
+  public Image getImageWorkflowStalled() {
+    return getZoomedImaged(
+        imageWorkflowStalled, display, ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE);
+  }
+
   /**
    * @return the imageArrow
    */
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 c3ff71765c..2be21509b4 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
@@ -1139,14 +1139,12 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
       Execution execution,
       ExecutionState state) {
     try {
-      executionItem.setImage(GuiResource.getInstance().getImagePipeline());
-
       String label = execution.getName();
       label += " - " + 
START_DATE_FORMAT.format(execution.getExecutionStartDate());
       executionItem.setText(label);
       executionItem.setData(execution);
 
-      decorateItemWithState(executionItem, location, state);
+      decorateItemWithState(executionItem, location, execution, state);
     } catch (Exception e) {
       new ErrorDialog(
           getShell(), CONST_ERROR1, "Error drawing pipeline execution 
information tree item", e);
@@ -1159,8 +1157,6 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
       Execution execution,
       ExecutionState state) {
     try {
-      executionItem.setImage(GuiResource.getInstance().getImageWorkflow());
-
       String label = execution.getName();
       label +=
           " - "
@@ -1168,7 +1164,7 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
       executionItem.setText(label);
       executionItem.setData(execution);
 
-      decorateItemWithState(executionItem, location, state);
+      decorateItemWithState(executionItem, location, execution, state);
     } catch (Exception e) {
       new ErrorDialog(
           getShell(), CONST_ERROR1, "Error drawing workflow execution 
information tree item", e);
@@ -1176,9 +1172,17 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
   }
 
   private static void decorateItemWithState(
-      TreeItem executionItem, ExecutionInfoLocation location, ExecutionState 
state) {
+      TreeItem executionItem,
+      ExecutionInfoLocation location,
+      Execution execution,
+      ExecutionState state) {
     long loggingInterval = Const.toLong(location.getDataLoggingInterval(), 
20000);
+    ExecutionStatusIcon statusIcon = ExecutionStatusIcon.from(state, 
loggingInterval);
+    executionItem.setImage(statusIcon.toImage(execution.getExecutionType()));
 
+    if (state == null) {
+      return;
+    }
     if (state.isFailed()) {
       
executionItem.setBackground(GuiResource.getInstance().getColorLightRed());
     } else if (state.isStale(loggingInterval)) {
@@ -1188,6 +1192,19 @@ public class ExecutionPerspective implements 
IHopPerspective, TabClosable {
     }
   }
 
+  /** Refresh the tab image after a viewer reloads execution state (failed / 
stalled / default). */
+  public void updateViewerTabImage(IExecutionViewer viewer) {
+    if (tabFolder == null || tabFolder.isDisposed() || viewer == null) {
+      return;
+    }
+    for (CTabItem item : tabFolder.getItems()) {
+      if (viewer.equals(item.getData()) && !item.isDisposed()) {
+        item.setImage(viewer.getTitleImage());
+        return;
+      }
+    }
+  }
+
   @Override
   public boolean remove(IHopFileTypeHandler typeHandler) {
     if (typeHandler instanceof MetadataEditor<?> editor) {
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionStatusIcon.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionStatusIcon.java
new file mode 100644
index 0000000000..b841e4035f
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionStatusIcon.java
@@ -0,0 +1,68 @@
+/*
+ * 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 org.apache.hop.execution.ExecutionState;
+import org.apache.hop.execution.ExecutionType;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * Tree and tab icon for a pipeline or workflow execution. Failed wins over 
stalled; everything else
+ * uses the default pipeline or workflow image.
+ */
+public enum ExecutionStatusIcon {
+  DEFAULT,
+  ERROR,
+  STALLED;
+
+  public static ExecutionStatusIcon from(ExecutionState state, long 
loggingInterval) {
+    if (state == null) {
+      return DEFAULT;
+    }
+    if (state.isFailed()) {
+      return ERROR;
+    }
+    if (state.isStale(loggingInterval)) {
+      return STALLED;
+    }
+    return DEFAULT;
+  }
+
+  public Image toImage(ExecutionType type) {
+    boolean pipeline = type == ExecutionType.Pipeline;
+    return switch (this) {
+      case ERROR ->
+          pipeline
+              ? GuiResource.getInstance().getImagePipelineError()
+              : GuiResource.getInstance().getImageWorkflowError();
+      case STALLED ->
+          pipeline
+              ? GuiResource.getInstance().getImagePipelineStalled()
+              : GuiResource.getInstance().getImageWorkflowStalled();
+      case DEFAULT ->
+          pipeline
+              ? GuiResource.getInstance().getImagePipeline()
+              : GuiResource.getInstance().getImageWorkflow();
+    };
+  }
+
+  public static Image imageFor(ExecutionType type, ExecutionState state, long 
loggingInterval) {
+    return from(state, loggingInterval).toImage(type);
+  }
+}
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 e50060547d..d5e915dd1d 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
@@ -680,7 +680,7 @@ public class PipelineExecutionViewer extends 
BaseExecutionViewer
 
   @Override
   public Image getTitleImage() {
-    return GuiResource.getInstance().getImagePipeline();
+    return ExecutionStatusIcon.imageFor(ExecutionType.Pipeline, 
executionState, loggingInterval());
   }
 
   @Override
@@ -822,6 +822,7 @@ public class PipelineExecutionViewer extends 
BaseExecutionViewer
     refreshStatus();
     refreshMetrics();
     refreshTransformData();
+    perspective.updateViewerTabImage(this);
     redraw();
   }
 
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 f65ef47aa1..5e2b41cb79 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
@@ -528,7 +528,7 @@ public class WorkflowExecutionViewer extends 
BaseExecutionViewer
 
   @Override
   public Image getTitleImage() {
-    return GuiResource.getInstance().getImageWorkflow();
+    return ExecutionStatusIcon.imageFor(ExecutionType.Workflow, 
executionState, loggingInterval());
   }
 
   @Override
@@ -712,6 +712,7 @@ public class WorkflowExecutionViewer extends 
BaseExecutionViewer
   public void refresh() {
     refreshStatus();
     refreshActionData();
+    perspective.updateViewerTabImage(this);
     redraw();
   }
 
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 a8a726be21..7d6816afae 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
@@ -169,6 +169,21 @@ public abstract class BaseExecutionViewer extends 
DragViewZoomBase
     return Utils.getDurationHMS(durationMs / 1000.0);
   }
 
+  /**
+   * Logging interval from the execution information location, used to decide 
whether state is
+   * stalled. Defaults to 20s when the location is not loaded yet.
+   */
+  protected long loggingInterval() {
+    if (perspective == null || perspective.getLocationMap() == null) {
+      return 20000;
+    }
+    ExecutionInfoLocation location = 
perspective.getLocationMap().get(locationName);
+    if (location == null) {
+      return 20000;
+    }
+    return Const.toLong(location.getDataLoggingInterval(), 20000);
+  }
+
   public abstract void drillDownOnLocation(Point location);
 
   @Override
diff --git a/ui/src/main/resources/ui/images/pipeline-error.svg 
b/ui/src/main/resources/ui/images/pipeline-error.svg
new file mode 100644
index 0000000000..11fad44dbc
--- /dev/null
+++ b/ui/src/main/resources/ui/images/pipeline-error.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"; width="24" height="24">
+    <path d="M16.429 4a7.52 7.52 0 0 0-.682.028c-1.782.155-3.29.865-4.273 
1.328-1.033.486-2.334 1.25-3.703 1.585s-2.682.284-3.976-.82a1.118 1.29 0 1 
0-1.33 2.074c1.886 1.609 4.02 1.697 5.769 1.27s3.242-1.323 4.085-1.72c.969-.456 
2.237-1.026 3.597-1.144s2.775.161 4.259 1.507a1.118 1.29 0 1 0 
1.382-2.027C19.865 4.545 18.051 4.007 16.429 4zm0 5.159a7.52 7.52 0 0 
0-.682.028c-1.782.155-3.29.865-4.273 1.328-1.033.486-2.334 1.25-3.703 
1.585s-2.682.284-3.976-.82a1.118 1.29 0 1 0-1.33 2.074c1.886 [...]
+          fill="#53c3c4"/>
+    <circle cx="17.2" cy="17.2" r="6.8" fill="#ffffff"/>
+    <circle cx="17.2" cy="17.2" r="5.8" fill="#EA102A"/>
+    <rect x="16.2" y="13.2" width="2" height="5.2" rx="0.4" fill="#ffffff"/>
+    <circle cx="17.2" cy="20.2" r="1" fill="#ffffff"/>
+</svg>
diff --git a/ui/src/main/resources/ui/images/pipeline-stalled.svg 
b/ui/src/main/resources/ui/images/pipeline-stalled.svg
new file mode 100644
index 0000000000..2d97dee418
--- /dev/null
+++ b/ui/src/main/resources/ui/images/pipeline-stalled.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"; width="24" height="24">
+    <path d="M16.429 4a7.52 7.52 0 0 0-.682.028c-1.782.155-3.29.865-4.273 
1.328-1.033.486-2.334 1.25-3.703 1.585s-2.682.284-3.976-.82a1.118 1.29 0 1 
0-1.33 2.074c1.886 1.609 4.02 1.697 5.769 1.27s3.242-1.323 4.085-1.72c.969-.456 
2.237-1.026 3.597-1.144s2.775.161 4.259 1.507a1.118 1.29 0 1 0 
1.382-2.027C19.865 4.545 18.051 4.007 16.429 4zm0 5.159a7.52 7.52 0 0 
0-.682.028c-1.782.155-3.29.865-4.273 1.328-1.033.486-2.334 1.25-3.703 
1.585s-2.682.284-3.976-.82a1.118 1.29 0 1 0-1.33 2.074c1.886 [...]
+          fill="#53c3c4"/>
+    <circle cx="17.2" cy="17.2" r="6.8" fill="#ffffff"/>
+    <circle cx="17.2" cy="17.2" r="5.8" fill="#FFA500"/>
+    <path fill="#ffffff" d="M15.35 15.05c0-1.15.95-2.05 2.05-2.05 1.15 0 
2.05.85 2.05 1.95 0 .85-.45 1.35-1.2 1.85-.7.45-.95.8-.95 
1.45v.25h-1.7v-.3c0-1.05.4-1.6 1.15-2.1.55-.4.85-.7.85-1.15 
0-.5-.35-.85-.85-.85-.5 0-.85.3-.95.9h-1.45z"/>
+    <circle cx="17.2" cy="20.25" r="0.95" fill="#ffffff"/>
+</svg>
diff --git a/ui/src/main/resources/ui/images/workflow-error.svg 
b/ui/src/main/resources/ui/images/workflow-error.svg
new file mode 100644
index 0000000000..2d93bc29d6
--- /dev/null
+++ b/ui/src/main/resources/ui/images/workflow-error.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"; width="24" height="24">
+    <path fill="#033d5d" d="M12.033 2c-.401 0-.802.153-1.109.461L7.726 5.659 
8.86 6.792l2.73-2.73c.123-.123.283-.184.443-.184s.321.061.443.184l2.134 
2.134-1.154 1.154H16.9V3.906l-1.157 1.157-2.602-2.602C12.835 2.154 12.434 2 
12.033 2zM3.905 7.1l1.157 1.157-2.602 2.602c-.307.307-.461.708-.461 
1.109s.153.802.461 1.109l3.198 3.198 
1.134-1.134-2.73-2.73c-.123-.123-.184-.283-.184-.443s.061-.321.184-.443L6.196 
9.39l1.154 1.154V7.1zm14.436.627l-1.134 1.134 2.73 
2.73c.123.123.184.283.184.443s-.0 [...]
+    <circle cx="17.2" cy="17.2" r="6.8" fill="#ffffff"/>
+    <circle cx="17.2" cy="17.2" r="5.8" fill="#EA102A"/>
+    <rect x="16.2" y="13.2" width="2" height="5.2" rx="0.4" fill="#ffffff"/>
+    <circle cx="17.2" cy="20.2" r="1" fill="#ffffff"/>
+</svg>
diff --git a/ui/src/main/resources/ui/images/workflow-stalled.svg 
b/ui/src/main/resources/ui/images/workflow-stalled.svg
new file mode 100644
index 0000000000..be457c4c8f
--- /dev/null
+++ b/ui/src/main/resources/ui/images/workflow-stalled.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"; width="24" height="24">
+    <path fill="#033d5d" d="M12.033 2c-.401 0-.802.153-1.109.461L7.726 5.659 
8.86 6.792l2.73-2.73c.123-.123.283-.184.443-.184s.321.061.443.184l2.134 
2.134-1.154 1.154H16.9V3.906l-1.157 1.157-2.602-2.602C12.835 2.154 12.434 2 
12.033 2zM3.905 7.1l1.157 1.157-2.602 2.602c-.307.307-.461.708-.461 
1.109s.153.802.461 1.109l3.198 3.198 
1.134-1.134-2.73-2.73c-.123-.123-.184-.283-.184-.443s.061-.321.184-.443L6.196 
9.39l1.154 1.154V7.1zm14.436.627l-1.134 1.134 2.73 
2.73c.123.123.184.283.184.443s-.0 [...]
+    <circle cx="17.2" cy="17.2" r="6.8" fill="#ffffff"/>
+    <circle cx="17.2" cy="17.2" r="5.8" fill="#FFA500"/>
+    <path fill="#ffffff" d="M15.35 15.05c0-1.15.95-2.05 2.05-2.05 1.15 0 
2.05.85 2.05 1.95 0 .85-.45 1.35-1.2 1.85-.7.45-.95.8-.95 
1.45v.25h-1.7v-.3c0-1.05.4-1.6 1.15-2.1.55-.4.85-.7.85-1.15 
0-.5-.35-.85-.85-.85-.5 0-.85.3-.95.9h-1.45z"/>
+    <circle cx="17.2" cy="20.25" r="0.95" fill="#ffffff"/>
+</svg>
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionStatusIconTest.java
 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionStatusIconTest.java
new file mode 100644
index 0000000000..449d3ebb1c
--- /dev/null
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionStatusIconTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Date;
+import org.apache.hop.execution.ExecutionState;
+import org.junit.jupiter.api.Test;
+
+class ExecutionStatusIconTest {
+
+  private static final long LOGGING_INTERVAL = 20_000L;
+
+  @Test
+  void nullStateIsDefault() {
+    assertEquals(ExecutionStatusIcon.DEFAULT, ExecutionStatusIcon.from(null, 
LOGGING_INTERVAL));
+  }
+
+  @Test
+  void failedWinsOverStale() {
+    ExecutionState state = new ExecutionState();
+    state.setFailed(true);
+    state.setUpdateTime(new Date(System.currentTimeMillis() - 60_000L));
+
+    assertEquals(ExecutionStatusIcon.ERROR, ExecutionStatusIcon.from(state, 
LOGGING_INTERVAL));
+  }
+
+  @Test
+  void unfinishedWithoutRecentUpdateIsStalled() {
+    ExecutionState state = new ExecutionState();
+    state.setFailed(false);
+    state.setExecutionEndDate(null);
+    state.setUpdateTime(new Date(System.currentTimeMillis() - 60_000L));
+
+    assertEquals(ExecutionStatusIcon.STALLED, ExecutionStatusIcon.from(state, 
LOGGING_INTERVAL));
+  }
+
+  @Test
+  void finishedIsNeverStalled() {
+    ExecutionState state = new ExecutionState();
+    state.setFailed(false);
+    state.setExecutionEndDate(new Date(System.currentTimeMillis() - 60_000L));
+    state.setUpdateTime(new Date(System.currentTimeMillis() - 60_000L));
+
+    assertEquals(ExecutionStatusIcon.DEFAULT, ExecutionStatusIcon.from(state, 
LOGGING_INTERVAL));
+  }
+
+  @Test
+  void freshRunningIsDefault() {
+    ExecutionState state = new ExecutionState();
+    state.setFailed(false);
+    state.setStatusDescription("Running");
+    state.setUpdateTime(new Date());
+
+    assertEquals(ExecutionStatusIcon.DEFAULT, ExecutionStatusIcon.from(state, 
LOGGING_INTERVAL));
+  }
+
+  @Test
+  void missingUpdateTimeIsNotStalled() {
+    ExecutionState state = new ExecutionState();
+    state.setFailed(false);
+    state.setUpdateTime(null);
+
+    assertEquals(ExecutionStatusIcon.DEFAULT, ExecutionStatusIcon.from(state, 
LOGGING_INTERVAL));
+  }
+}

Reply via email to