Xiao-zhen-Liu commented on code in PR #5146:
URL: https://github.com/apache/texera/pull/5146#discussion_r3320350416


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -358,6 +358,28 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
             });
         }
       });
+
+    // When operators are (re)added to the graph — e.g. after navigating back 
to
+    // the workflow page, where WorkspaceComponent calls reloadWorkflow and
+    // operators are recreated from the workflow JSON — restore their visual
+    // state from the cached status so completed runs don't appear to reset.
+    this.workflowActionService

Review Comment:
   This new block looks up the saved status and repaints the operator's border 
color. The validation code further down (the other change in this PR) now does 
the same look-up-and-repaint. So the same logic lives in two places, which is 
easy to break later: someone updates one copy and forgets the other.
   
   Stepping back: the original bug is that two separate parts of the code both 
set the border color and end up overwriting each other. This fix makes them 
agree in the normal case, but they're still two separate things setting the 
same color. It would be cleaner to have one function decide the final border 
color (taking into account both whether the operator is valid and what its run 
status is) and call that from a single place.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -949,15 +971,31 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
   }
 
   /**
-   * if the operator is valid , the border of the box will be default
+   * Applies the validation result to the operator's border.
+   * - Invalid operators are drawn with a red border (validation takes 
priority).
+   * - Valid operators with a cached execution status keep their 
execution-state
+   *   border so e.g. a Completed (green) run isn't repainted gray when the 
user
+   *   navigates back to the workflow and reloadWorkflow re-adds the operators,
+   *   which triggers a validation pass that would otherwise overwrite the
+   *   execution-state stroke set by handleOperatorStatisticsUpdate.
+   * - Valid operators with no cached status get the default valid border.
    */
   private handleOperatorValidation(): void {
     this.validationWorkflowService
       .getOperatorValidationStream()
       .pipe(untilDestroyed(this))
-      .subscribe(value =>
-        this.jointUIService.changeOperatorColor(this.paper, value.operatorID, 
value.validation.isValid)
-      );
+      .subscribe(value => {
+        if (!value.validation.isValid) {
+          this.jointUIService.changeOperatorColor(this.paper, 
value.operatorID, false);
+          return;
+        }
+        const statistics = 
this.workflowStatusService.getCurrentStatus()[value.operatorID];
+        if (statistics) {
+          this.jointUIService.changeOperatorState(this.paper, 
value.operatorID, statistics.operatorState);

Review Comment:
   Edge case worth thinking about: an operator that is both invalid AND has a 
saved "completed" status. Red (invalid) is supposed to win over green here. But 
whether it actually does depends on which of the two color-setting code paths 
happens to run last, and nothing in the code guarantees that order: they are 
triggered through two different chains of events. It happens to work today, but 
if that order ever changed, an invalid operator could wrongly show up green 
instead of red.
   
   Having one place decide the final color (validation first, then saved 
status, then default) would remove this dependence on timing.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts:
##########
@@ -962,5 +975,75 @@ describe("WorkflowEditorComponent", () => {
       fixture.detectChanges();
       expect(redoSpy).toHaveBeenCalledTimes(4);
     });
+
+    /**
+     * Regression coverage for the bug where the operator border resets to the
+     * default (gray) when the user navigates away from and back to a workflow
+     * that has already finished executing. The visual state is driven by two
+     * separate streams that both touch rect.body/stroke: the execution status
+     * stream (sets state-derived color) and the validation stream (sets red on
+     * invalid, gray on valid). When operators are re-added by reloadWorkflow,
+     * the validation pass fires after the status repaint and used to overwrite
+     * it. handleOperatorValidation now consults the cached status before
+     * deciding which color to apply.
+     */
+    describe("operator border restoration after navigation", () => {
+      let workflowStatusService: WorkflowStatusService;
+      const cachedCompleted = {
+        [mockScanPredicate.operatorID]: {
+          operatorState: OperatorState.Completed,
+          aggregatedInputRowCount: 0,
+          inputPortMetrics: {},
+          aggregatedOutputRowCount: 0,
+          outputPortMetrics: {},
+        },
+      };
+
+      beforeEach(() => {
+        workflowStatusService = TestBed.inject(WorkflowStatusService);
+      });
+
+      it("repaints execution-state stroke for valid operators with a cached 
status", () => {

Review Comment:
   This test is meant to verify the validation fix, but it only checks "was the 
green-painting function called?". That same function also gets called by the 
other new block (the operator-add code above), so this test would still pass 
even if the validation fix were removed entirely. In other words, it does not 
actually test the change it is paired with.
   
   Checking the operator's actual final border color would test the real 
behavior.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts:
##########
@@ -962,5 +975,75 @@ describe("WorkflowEditorComponent", () => {
       fixture.detectChanges();
       expect(redoSpy).toHaveBeenCalledTimes(4);
     });
+
+    /**
+     * Regression coverage for the bug where the operator border resets to the
+     * default (gray) when the user navigates away from and back to a workflow
+     * that has already finished executing. The visual state is driven by two
+     * separate streams that both touch rect.body/stroke: the execution status
+     * stream (sets state-derived color) and the validation stream (sets red on
+     * invalid, gray on valid). When operators are re-added by reloadWorkflow,
+     * the validation pass fires after the status repaint and used to overwrite
+     * it. handleOperatorValidation now consults the cached status before
+     * deciding which color to apply.
+     */
+    describe("operator border restoration after navigation", () => {
+      let workflowStatusService: WorkflowStatusService;
+      const cachedCompleted = {
+        [mockScanPredicate.operatorID]: {
+          operatorState: OperatorState.Completed,
+          aggregatedInputRowCount: 0,
+          inputPortMetrics: {},
+          aggregatedOutputRowCount: 0,
+          outputPortMetrics: {},
+        },
+      };
+
+      beforeEach(() => {
+        workflowStatusService = TestBed.inject(WorkflowStatusService);
+      });
+
+      it("repaints execution-state stroke for valid operators with a cached 
status", () => {
+        vi.spyOn(workflowStatusService, 
"getCurrentStatus").mockReturnValue(cachedCompleted);
+        vi.spyOn(validationWorkflowService, 
"validateOperator").mockReturnValue({ isValid: true });
+        const changeOperatorStateSpy = vi.spyOn(jointUIService, 
"changeOperatorState");
+
+        workflowActionService.addOperator(mockScanPredicate, mockPoint);
+        fixture.detectChanges();
+
+        expect(changeOperatorStateSpy).toHaveBeenCalledWith(
+          component.paper,
+          mockScanPredicate.operatorID,
+          OperatorState.Completed
+        );
+      });
+
+      it("falls back to the default valid stroke when no cached status 
exists", () => {
+        vi.spyOn(workflowStatusService, 
"getCurrentStatus").mockReturnValue({});
+        vi.spyOn(validationWorkflowService, 
"validateOperator").mockReturnValue({ isValid: true });
+        const changeOperatorColorSpy = vi.spyOn(jointUIService, 
"changeOperatorColor");
+
+        workflowActionService.addOperator(mockScanPredicate, mockPoint);
+        fixture.detectChanges();
+
+        expect(changeOperatorColorSpy).toHaveBeenCalledWith(component.paper, 
mockScanPredicate.operatorID, true);
+      });
+
+      it("paints the invalid stroke (red) for invalid operators regardless of 
cached status", () => {

Review Comment:
   This checks "was the red-painting function called?", but calling it is not 
enough on its own: red also has to be the color that actually ends up on the 
operator (see my note on the validation handler about the ordering). The test 
does not confirm the operator ends up red, so it would not catch the timing 
problem described there.
   
   Checking the final border color would make this test reliable.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to