Copilot commented on code in PR #3753:
URL: https://github.com/apache/texera/pull/3753#discussion_r2434308905


##########
frontend/src/app/workspace/component/menu/menu.component.ts:
##########
@@ -469,6 +470,10 @@ export class MenuComponent implements OnInit, OnDestroy {
     
this.workflowActionService.getJointGraphWrapper().mainPaper.setGridSize(this.showGrid
 ? 2 : 1);
   }
 
+  public toggleRegion(): void {

Review Comment:
   This toggles 'hide-region' on mainPaper.el, but the class was initially 
added on a different element (this.editor). Align both to the same DOM element 
(recommend mainPaper.el) so toggling actually shows/hides regions.
   ```suggestion
     public toggleRegion(): void {
       // Ensure 'hide-region' is always toggled on mainPaper.el for consistency
   ```



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -330,6 +333,68 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
       });
   }
 
+  private handleRegion(): void {
+    this.editor.classList.add("hide-region");
+    const Region = joint.dia.Element.define(
+      "region",
+      {
+        attrs: {
+          body: {
+            fill: "rgba(255,213,79,0.2)",
+            stroke: "#FFD54F",
+            "stroke-width": 3,
+            "stroke-dasharray": "6,4",
+            pointerEvents: "none",
+            class: "region",
+          },
+        },
+      },
+      {
+        markup: [{ tagName: "path", selector: "body" }],
+      }
+    );
+
+    let regionMap: { regionShape: joint.dia.Element; operators: 
joint.dia.Cell[] }[] = [];
+    // update region shapes on execution
+    this.executeWorkflowService
+      .getRegionStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(regions => {
+        this.paper.model
+          .getCells()
+          .filter(shape => shape instanceof Region)
+          .forEach(shape => shape.remove());
+
+        regionMap = regions.map(region => {
+          const shape = new Region();
+          const ops = region.map(id => this.paper.getModelById(id));
+          this.paper.model.addCell(shape);
+          this.updateRegionShape(shape, ops);

Review Comment:
   getModelById can return undefined; later updateRegionShape calls 
op.getBBox(), which will throw if any op is undefined. Filter out undefined 
results before use, e.g., `const ops = region.map(id => 
this.paper.getModelById(id)).filter(isDefined);` and handle the empty case.
   ```suggestion
             const ops = region.map(id => 
this.paper.getModelById(id)).filter(isDefined);
             this.paper.model.addCell(shape);
             if (ops.length > 0) {
               this.updateRegionShape(shape, ops);
             }
   ```



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -330,6 +333,68 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
       });
   }
 
+  private handleRegion(): void {
+    this.editor.classList.add("hide-region");
+    const Region = joint.dia.Element.define(
+      "region",
+      {
+        attrs: {
+          body: {
+            fill: "rgba(255,213,79,0.2)",
+            stroke: "#FFD54F",
+            "stroke-width": 3,
+            "stroke-dasharray": "6,4",
+            pointerEvents: "none",
+            class: "region",
+          },
+        },
+      },
+      {
+        markup: [{ tagName: "path", selector: "body" }],
+      }
+    );
+
+    let regionMap: { regionShape: joint.dia.Element; operators: 
joint.dia.Cell[] }[] = [];
+    // update region shapes on execution
+    this.executeWorkflowService
+      .getRegionStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(regions => {
+        this.paper.model
+          .getCells()
+          .filter(shape => shape instanceof Region)
+          .forEach(shape => shape.remove());
+
+        regionMap = regions.map(region => {
+          const shape = new Region();
+          const ops = region.map(id => this.paper.getModelById(id));
+          this.paper.model.addCell(shape);
+          this.updateRegionShape(shape, ops);
+          return { regionShape: shape, operators: ops };
+        });
+      });
+
+    this.paper.model.on("change:position", operator => {

Review Comment:
   The JointJS event handler is never removed, which risks memory leaks across 
component lifecycles. Store the handler and remove it in ngOnDestroy (e.g., 
`this.paper.model.off('change:position', handler)`), or bind with a context and 
call `off` with that context.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -330,6 +333,68 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
       });
   }
 
+  private handleRegion(): void {
+    this.editor.classList.add("hide-region");
+    const Region = joint.dia.Element.define(
+      "region",
+      {
+        attrs: {
+          body: {
+            fill: "rgba(255,213,79,0.2)",
+            stroke: "#FFD54F",
+            "stroke-width": 3,
+            "stroke-dasharray": "6,4",
+            pointerEvents: "none",
+            class: "region",
+          },
+        },
+      },
+      {
+        markup: [{ tagName: "path", selector: "body" }],
+      }
+    );
+
+    let regionMap: { regionShape: joint.dia.Element; operators: 
joint.dia.Cell[] }[] = [];
+    // update region shapes on execution
+    this.executeWorkflowService
+      .getRegionStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(regions => {
+        this.paper.model
+          .getCells()
+          .filter(shape => shape instanceof Region)
+          .forEach(shape => shape.remove());
+
+        regionMap = regions.map(region => {
+          const shape = new Region();
+          const ops = region.map(id => this.paper.getModelById(id));
+          this.paper.model.addCell(shape);
+          this.updateRegionShape(shape, ops);
+          return { regionShape: shape, operators: ops };
+        });
+      });
+
+    this.paper.model.on("change:position", operator => {
+      regionMap
+        .filter(region => region.operators.includes(operator))
+        .forEach(region => this.updateRegionShape(region.regionShape, 
region.operators));

Review Comment:
   Recomputing the concave hull on every 'change:position' event can cause jank 
while dragging. Consider throttling via requestAnimationFrame or a debounce to 
batch updates, e.g., schedule one update per frame for affected regions.
   ```suggestion
       // Throttle region shape updates using requestAnimationFrame
       const pendingRegionUpdates = new Set<{ regionShape: joint.dia.Element; 
operators: joint.dia.Cell[] }>();
       let regionUpdateScheduled = false;
       this.paper.model.on("change:position", operator => {
         regionMap
           .filter(region => region.operators.includes(operator))
           .forEach(region => pendingRegionUpdates.add(region));
         if (!regionUpdateScheduled) {
           regionUpdateScheduled = true;
           requestAnimationFrame(() => {
             pendingRegionUpdates.forEach(region => 
this.updateRegionShape(region.regionShape, region.operators));
             pendingRegionUpdates.clear();
             regionUpdateScheduled = false;
           });
         }
   ```



##########
amber/src/main/scala/org/apache/amber/engine/architecture/controller/WorkflowScheduler.scala:
##########
@@ -32,7 +32,7 @@ class WorkflowScheduler(
     actorId: ActorVirtualIdentity
 ) extends java.io.Serializable {
   var physicalPlan: PhysicalPlan = _
-  private var schedule: Schedule = _
+  var schedule: Schedule = _
 

Review Comment:
   Exposing schedule as a public var weakens encapsulation and allows external 
mutation. Keep it private and add a read-only accessor (e.g., `def getSchedule: 
Schedule = schedule`) or expose a method that returns regions directly, then 
update Controller to use the accessor.
   ```suggestion
     private var schedule: Schedule = _
   
     def getSchedule: Schedule = schedule
   ```



##########
frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.ts:
##########
@@ -329,6 +334,10 @@ export class ExecuteWorkflowService {
     return this.executionStateStream.asObservable();
   }
 
+  public getRegionStream(): Observable<readonly string[][]> {
+    return this.regionStream.asObservable();
+  }
+
   public resetExecutionState(): void {
     this.currentState = {
       state: ExecutionState.Uninitialized,

Review Comment:
   Region overlays aren't explicitly cleared on reset; if no new 
RegionUpdateEvent arrives they may persist visually. Emit an empty regions 
update on reset, e.g., `this.regionStream.next([]);` so the editor can remove 
existing region shapes.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -330,6 +333,68 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
       });
   }
 
+  private handleRegion(): void {
+    this.editor.classList.add("hide-region");

Review Comment:
   The initial 'hide-region' class is applied to this.editor, but the menu 
toggle adds/removes the class on mainPaper.el, so the region layer remains 
hidden even when toggled on. Apply the class on the same element the toggle 
targets (mainPaper.el) to keep behavior consistent.
   ```suggestion
       this.mainPaper.el.classList.add("hide-region");
   ```



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -330,6 +333,68 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
       });
   }
 
+  private handleRegion(): void {
+    this.editor.classList.add("hide-region");
+    const Region = joint.dia.Element.define(
+      "region",
+      {
+        attrs: {
+          body: {
+            fill: "rgba(255,213,79,0.2)",
+            stroke: "#FFD54F",
+            "stroke-width": 3,
+            "stroke-dasharray": "6,4",
+            pointerEvents: "none",
+            class: "region",
+          },
+        },
+      },
+      {
+        markup: [{ tagName: "path", selector: "body" }],
+      }
+    );
+
+    let regionMap: { regionShape: joint.dia.Element; operators: 
joint.dia.Cell[] }[] = [];
+    // update region shapes on execution
+    this.executeWorkflowService
+      .getRegionStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(regions => {
+        this.paper.model
+          .getCells()
+          .filter(shape => shape instanceof Region)
+          .forEach(shape => shape.remove());
+
+        regionMap = regions.map(region => {
+          const shape = new Region();
+          const ops = region.map(id => this.paper.getModelById(id));
+          this.paper.model.addCell(shape);
+          this.updateRegionShape(shape, ops);
+          return { regionShape: shape, operators: ops };
+        });
+      });
+
+    this.paper.model.on("change:position", operator => {
+      regionMap
+        .filter(region => region.operators.includes(operator))
+        .forEach(region => this.updateRegionShape(region.regionShape, 
region.operators));
+    });
+  }
+
+  private updateRegionShape(regionShape: joint.dia.Element, operators: 
joint.dia.Cell[]) {
+    const points = operators.flatMap(op => {
+      const { x, y, width, height } = op.getBBox(),
+        padding = 20;
+      return [
+        [x - padding, y - padding],
+        [x + width + padding, y - padding],
+        [x - padding, y + height + padding + 10],
+        [x + width + padding, y + height + padding + 10],
+      ];
+    });
+    regionShape.attr("body/d", 
line().curve(curveCatmullRomClosed)(concaveman(points, 2, 0) as [number, 
number][]));
+  }

Review Comment:
   If operators is empty (or was filtered due to missing elements), this will 
generate an invalid hull and could raise errors. Add a guard to remove/hide the 
region when no operators are available: `if (!operators.length) { 
regionShape.remove(); return; }`.



##########
frontend/package.json:
##########
@@ -47,7 +47,9 @@
     "@types/plotly.js-basic-dist-min": "2.12.4",
     "ajv": "8.10.0",
     "backbone": "1.4.1",
+    "concaveman": "2.0.0",
     "content-disposition": "0.5.4",
+    "d3": "6.4.0",

Review Comment:
   The code imports from 'd3-shape', but only 'd3' is declared as a dependency. 
Add 'd3-shape' as a direct runtime dependency to match the import and avoid 
relying on transitive hoisting.
   ```suggestion
       "d3": "6.4.0",
       "d3-shape": "3.0.1",
   ```



-- 
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