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


##########
frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html:
##########
@@ -20,10 +20,214 @@
 <div class="section-container subsection-grid-container">
   <nz-card class="section-title">
     <h2 class="page-title">Computing Units</h2>
+    <div class="button-group">
+      <button
+        nz-button
+        class="create-btn"
+        (click)="showAddComputeUnitModalVisible()"
+        title="Create Computing Unit">
+        <i
+          nz-icon
+          nzType="file-add"
+          nzTheme="outline"></i>
+        <span>Create Computing Unit</span>
+      </button>
+    </div>
   </nz-card>
 
   <nz-card
     class="section-list-container"
     [nzBodyStyle]="{ height: '100%'}">
+    <cdk-virtual-scroll-viewport
+      itemSize="70"
+      class="virtual-scroll-container">
+      <nz-list>
+        <ng-container *ngFor="let entry of entries">
+          <texera-user-computing-unit-list-item
+            
(deleted)="terminateComputingUnit(entry.computingUnit.computingUnit.cuid)"
+            [editable]="editable"
+            
[entry]="entry.computingUnit"></texera-user-computing-unit-list-item>
+        </ng-container>

Review Comment:
   The `cdk-virtual-scroll-viewport` isn’t actually virtualizing here because 
the list uses `*ngFor` instead of `*cdkVirtualFor`. With many computing units 
this will still render all rows and can hurt performance; consider switching to 
`*cdkVirtualFor` (or remove the virtual scroll viewport if virtualization isn’t 
intended).



##########
frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts:
##########
@@ -17,11 +17,371 @@
  * under the License.
  */
 
-import { Component } from "@angular/core";
+import { Component, Input, OnInit } from "@angular/core";
+import { ComputingUnitStatusService } from 
"../../../../workspace/service/computing-unit-status/computing-unit-status.service";
+import { DashboardEntry } from "../../../type/dashboard-entry";
+import {
+  DashboardWorkflowComputingUnit,
+  WorkflowComputingUnitType,
+} from "../../../../workspace/types/workflow-computing-unit";
+import { extractErrorMessage } from "../../../../common/util/error";
+import { NotificationService } from 
"../../../../common/service/notification/notification.service";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { UserService } from "../../../../common/service/user/user.service";
+import { WorkflowComputingUnitManagingService } from 
"../../../../workspace/service/workflow-computing-unit/workflow-computing-unit-managing.service";
+import {
+  parseResourceUnit,
+  parseResourceNumber,
+  findNearestValidStep,
+  unitTypeMessageTemplate,
+} from "../../../../common/util/computing-unit.util";
 
+@UntilDestroy()
 @Component({
   selector: "texera-computing-unit-section",
   templateUrl: "user-computing-unit.component.html",
   styleUrls: ["user-computing-unit.component.scss"],
 })
-export class UserComputingUnitComponent {}
+export class UserComputingUnitComponent implements OnInit {
+  public entries: DashboardEntry[] = [];
+  public isLogin = this.userService.isLogin();
+  public currentUid = this.userService.getCurrentUser()?.uid;
+  @Input() editable = false;
+
+  allComputingUnits: DashboardWorkflowComputingUnit[] = [];
+
+  // variables for creating a computing unit
+  addComputeUnitModalVisible = false;
+  newComputingUnitName: string = "";
+  selectedMemory: string = "";
+  selectedCpu: string = "";
+  selectedGpu: string = "0"; // Default to no GPU
+  selectedJvmMemorySize: string = "1G"; // Initial JVM memory size
+  selectedComputingUnitType?: WorkflowComputingUnitType; // Selected computing 
unit type
+  selectedShmSize: string = "64Mi"; // Shared memory size
+  shmSizeValue: number = 64; // default to 64
+  shmSizeUnit: "Mi" | "Gi" = "Mi"; // default unit
+  availableComputingUnitTypes: WorkflowComputingUnitType[] = [];
+  localComputingUnitUri: string = ""; // URI for local computing unit
+
+  // JVM memory slider configuration
+  jvmMemorySliderValue: number = 1; // Initial value in GB
+  jvmMemoryMarks: { [key: number]: string } = { 1: "1G" };
+  jvmMemoryMax: number = 1;
+  jvmMemorySteps: number[] = [1]; // Available steps in binary progression 
(1,2,4,8...)
+  showJvmMemorySlider: boolean = false; // Whether to show the slider
+
+  // cpu&memory limit options from backend
+  cpuOptions: string[] = [];
+  memoryOptions: string[] = [];
+  gpuOptions: string[] = []; // Add GPU options array
+
+  constructor(
+    private notificationService: NotificationService,
+    private modalService: NzModalService,
+    private userService: UserService,
+    private computingUnitService: WorkflowComputingUnitManagingService,
+    private computingUnitStatusService: ComputingUnitStatusService
+  ) {
+    this.userService
+      .userChanged()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        this.isLogin = this.userService.isLogin();
+        this.currentUid = this.userService.getCurrentUser()?.uid;
+      });
+  }
+
+  ngOnInit() {
+    this.newComputingUnitName = "My Computing Unit";
+    this.computingUnitService
+      .getComputingUnitTypes()
+      .pipe(untilDestroyed(this))
+      .subscribe({
+        next: ({ typeOptions }) => {
+          this.availableComputingUnitTypes = typeOptions;
+          // Set default selected type if available
+          if (typeOptions.includes("kubernetes")) {
+            this.selectedComputingUnitType = "kubernetes";
+          } else if (typeOptions.length > 0) {
+            this.selectedComputingUnitType = typeOptions[0];
+          }
+        },
+        error: (err: unknown) =>
+          this.notificationService.error(`Failed to fetch computing unit 
types: ${extractErrorMessage(err)}`),
+      });
+
+    this.computingUnitService
+      .getComputingUnitLimitOptions()
+      .pipe(untilDestroyed(this))
+      .subscribe({
+        next: ({ cpuLimitOptions, memoryLimitOptions, gpuLimitOptions }) => {
+          this.cpuOptions = cpuLimitOptions;
+          this.memoryOptions = memoryLimitOptions;
+          this.gpuOptions = gpuLimitOptions;
+
+          // fallback defaults
+          this.selectedCpu = this.cpuOptions[0] ?? "1";
+          this.selectedMemory = this.memoryOptions[0] ?? "1Gi";
+          this.selectedGpu = this.gpuOptions[0] ?? "0";
+
+          // Initialize JVM memory slider based on selected memory
+          this.updateJvmMemorySlider();
+        },
+        error: (err: unknown) =>
+          this.notificationService.error(`Failed to fetch resource options: 
${extractErrorMessage(err)}`),
+      });
+
+    this.computingUnitStatusService
+      .getAllComputingUnits()
+      .pipe(untilDestroyed(this))
+      .subscribe(units => {
+        this.allComputingUnits = units;
+        this.entries = units.map(u => new DashboardEntry(u));
+      });
+  }
+
+  terminateComputingUnit(cuid: number): void {
+    const unit = this.allComputingUnits.find(u => u.computingUnit.cuid === 
cuid);
+
+    if (!unit || !unit.computingUnit.uri) {
+      this.notificationService.error("Invalid computing unit.");
+      return;
+    }
+
+    const unitName = unit.computingUnit.name;
+    const unitType = unit?.computingUnit.type || "kubernetes"; // fallback
+    const templates = unitTypeMessageTemplate[unitType];
+
+    // Show confirmation modal
+    this.modalService.confirm({
+      nzTitle: templates.terminateTitle,
+      nzContent: templates.terminateWarning
+        ? `
+      <p>Are you sure you want to terminate <strong>${unitName}</strong>?</p>
+      ${templates.terminateWarning}
+    `
+        : `
+      <p>Are you sure you want to disconnect from 
<strong>${unitName}</strong>?</p>
+    `,
+      nzOkText: unitType === "local" ? "Disconnect" : "Terminate",
+      nzOkType: "primary",
+      nzOnOk: () => {
+        // Use the ComputingUnitStatusService to handle termination
+        // This will properly close the websocket before terminating the unit
+        this.computingUnitStatusService
+          .terminateComputingUnit(cuid)
+          .pipe(untilDestroyed(this))
+          .subscribe({
+            next: (success: boolean) => {
+              if (success) {
+                this.notificationService.success(`Terminated Computing Unit: 
${unitName}`);
+              } else {
+                this.notificationService.error("Failed to terminate computing 
unit");
+              }
+            },
+            error: (err: unknown) => {
+              this.notificationService.error(`Failed to terminate computing 
unit: ${extractErrorMessage(err)}`);
+            },
+          });
+      },
+      nzCancelText: "Cancel",
+    });
+  }
+
+  startComputingUnit(): void {
+    // Validate based on computing unit type
+    if (this.selectedComputingUnitType === "kubernetes") {
+      if (this.newComputingUnitName.trim() == "") {
+        this.notificationService.error("Name of the computing unit cannot be 
empty");
+        return;
+      }
+
+      this.selectedShmSize = `${this.shmSizeValue}${this.shmSizeUnit}`;
+
+      this.computingUnitService
+        .createKubernetesBasedComputingUnit(
+          this.newComputingUnitName,
+          this.selectedCpu,
+          this.selectedMemory,
+          this.selectedGpu,
+          this.selectedJvmMemorySize,
+          this.selectedShmSize
+        )
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: () => {
+            this.notificationService.success("Successfully created the new 
Kubernetes compute unit");
+            this.computingUnitStatusService.refreshComputingUnitList();
+          },
+          error: (err: unknown) =>
+            this.notificationService.error(`Failed to start Kubernetes 
computing unit: ${extractErrorMessage(err)}`),
+        });
+    } else if (this.selectedComputingUnitType === "local") {
+      // For local computing units, validate the URI
+      if (!this.localComputingUnitUri || this.localComputingUnitUri.trim() === 
"") {
+        this.notificationService.error("URI for local computing unit cannot be 
empty");
+        return;
+      }
+
+      this.computingUnitService
+        .createLocalComputingUnit(this.newComputingUnitName, 
this.localComputingUnitUri)
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: () => {
+            this.notificationService.success("Successfully created the new 
local compute unit");
+            this.computingUnitStatusService.refreshComputingUnitList();
+          },
+          error: (err: unknown) =>
+            this.notificationService.error(`Failed to start local computing 
unit: ${extractErrorMessage(err)}`),
+        });
+    } else {
+      this.notificationService.error("Please select a valid computing unit 
type");
+    }
+  }
+
+  showGpuSelection(): boolean {
+    // Don't show GPU selection if there are no options or only "0" option
+    return this.gpuOptions.length > 1 || (this.gpuOptions.length === 1 && 
this.gpuOptions[0] !== "0");
+  }
+
+  showAddComputeUnitModalVisible(): void {
+    this.addComputeUnitModalVisible = true;
+  }
+
+  handleAddComputeUnitModalOk(): void {
+    this.startComputingUnit();
+    this.addComputeUnitModalVisible = false;
+  }
+
+  handleAddComputeUnitModalCancel(): void {
+    this.addComputeUnitModalVisible = false;
+  }

Review Comment:
   `handleAddComputeUnitModalOk()` always hides the modal even when 
`startComputingUnit()` fails validation and returns early (e.g., empty 
name/URI). This makes the user lose the form state without creating anything. 
Consider only closing the modal after a successful create (e.g., have 
`startComputingUnit` return an Observable/Promise and close on `next`, or 
return a boolean for synchronous validation failures).



##########
frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts:
##########
@@ -0,0 +1,319 @@
+/**
+ * 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.
+ */
+
+import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } 
from "@angular/core";
+import { ComputingUnitStatusService } from 
"../../../../../workspace/service/computing-unit-status/computing-unit-status.service";
+import { extractErrorMessage } from "../../../../../common/util/error";
+import { NotificationService } from 
"../../../../../common/service/notification/notification.service";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import {
+  DashboardWorkflowComputingUnit,
+  WorkflowComputingUnit,
+} from "../../../../../workspace/types/workflow-computing-unit";
+import { WorkflowComputingUnitManagingService } from 
"../../../../../workspace/service/workflow-computing-unit/workflow-computing-unit-managing.service";
+import {
+  buildComputingUnitMetadataTable,
+  parseResourceUnit,
+  parseResourceNumber,
+  cpuResourceConversion,
+  memoryResourceConversion,
+  cpuPercentage,
+  memoryPercentage,
+} from "../../../../../common/util/computing-unit.util";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-user-computing-unit-list-item",
+  templateUrl: "./user-computing-unit-list-item.component.html",
+  styleUrls: ["./user-computing-unit-list-item.component.scss"],
+})
+export class UserComputingUnitListItemComponent implements OnInit {
+  private _entry?: DashboardWorkflowComputingUnit;
+  editingNameOfUnit: number | null = null;
+  editingUnitName: string = "";
+  gpuOptions: string[] = [];
+  @Input() editable = false;
+  @Output() deleted = new EventEmitter<void>();
+
+  @Input()

Review Comment:
   `@Input() editable` is passed from the parent but isn’t used to gate 
rename/delete UI or actions. As a result, the component will still render 
rename/delete controls even when `editable` is false. Please conditionally 
render/enable those controls based on `editable` (and keep the existing 
`entry.isOwner` checks).



##########
frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts:
##########
@@ -17,11 +17,371 @@
  * under the License.
  */
 
-import { Component } from "@angular/core";
+import { Component, Input, OnInit } from "@angular/core";
+import { ComputingUnitStatusService } from 
"../../../../workspace/service/computing-unit-status/computing-unit-status.service";
+import { DashboardEntry } from "../../../type/dashboard-entry";
+import {
+  DashboardWorkflowComputingUnit,
+  WorkflowComputingUnitType,
+} from "../../../../workspace/types/workflow-computing-unit";
+import { extractErrorMessage } from "../../../../common/util/error";
+import { NotificationService } from 
"../../../../common/service/notification/notification.service";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { UserService } from "../../../../common/service/user/user.service";
+import { WorkflowComputingUnitManagingService } from 
"../../../../workspace/service/workflow-computing-unit/workflow-computing-unit-managing.service";
+import {
+  parseResourceUnit,
+  parseResourceNumber,
+  findNearestValidStep,
+  unitTypeMessageTemplate,
+} from "../../../../common/util/computing-unit.util";
 
+@UntilDestroy()
 @Component({
   selector: "texera-computing-unit-section",
   templateUrl: "user-computing-unit.component.html",
   styleUrls: ["user-computing-unit.component.scss"],
 })
-export class UserComputingUnitComponent {}
+export class UserComputingUnitComponent implements OnInit {
+  public entries: DashboardEntry[] = [];
+  public isLogin = this.userService.isLogin();
+  public currentUid = this.userService.getCurrentUser()?.uid;
+  @Input() editable = false;
+
+  allComputingUnits: DashboardWorkflowComputingUnit[] = [];
+
+  // variables for creating a computing unit
+  addComputeUnitModalVisible = false;
+  newComputingUnitName: string = "";
+  selectedMemory: string = "";
+  selectedCpu: string = "";
+  selectedGpu: string = "0"; // Default to no GPU
+  selectedJvmMemorySize: string = "1G"; // Initial JVM memory size
+  selectedComputingUnitType?: WorkflowComputingUnitType; // Selected computing 
unit type
+  selectedShmSize: string = "64Mi"; // Shared memory size
+  shmSizeValue: number = 64; // default to 64
+  shmSizeUnit: "Mi" | "Gi" = "Mi"; // default unit
+  availableComputingUnitTypes: WorkflowComputingUnitType[] = [];
+  localComputingUnitUri: string = ""; // URI for local computing unit
+
+  // JVM memory slider configuration
+  jvmMemorySliderValue: number = 1; // Initial value in GB
+  jvmMemoryMarks: { [key: number]: string } = { 1: "1G" };
+  jvmMemoryMax: number = 1;
+  jvmMemorySteps: number[] = [1]; // Available steps in binary progression 
(1,2,4,8...)
+  showJvmMemorySlider: boolean = false; // Whether to show the slider
+
+  // cpu&memory limit options from backend
+  cpuOptions: string[] = [];
+  memoryOptions: string[] = [];
+  gpuOptions: string[] = []; // Add GPU options array
+
+  constructor(
+    private notificationService: NotificationService,
+    private modalService: NzModalService,
+    private userService: UserService,
+    private computingUnitService: WorkflowComputingUnitManagingService,
+    private computingUnitStatusService: ComputingUnitStatusService
+  ) {
+    this.userService
+      .userChanged()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        this.isLogin = this.userService.isLogin();
+        this.currentUid = this.userService.getCurrentUser()?.uid;
+      });
+  }
+
+  ngOnInit() {
+    this.newComputingUnitName = "My Computing Unit";
+    this.computingUnitService
+      .getComputingUnitTypes()
+      .pipe(untilDestroyed(this))
+      .subscribe({
+        next: ({ typeOptions }) => {
+          this.availableComputingUnitTypes = typeOptions;
+          // Set default selected type if available
+          if (typeOptions.includes("kubernetes")) {
+            this.selectedComputingUnitType = "kubernetes";
+          } else if (typeOptions.length > 0) {
+            this.selectedComputingUnitType = typeOptions[0];
+          }
+        },
+        error: (err: unknown) =>
+          this.notificationService.error(`Failed to fetch computing unit 
types: ${extractErrorMessage(err)}`),
+      });
+
+    this.computingUnitService
+      .getComputingUnitLimitOptions()
+      .pipe(untilDestroyed(this))
+      .subscribe({
+        next: ({ cpuLimitOptions, memoryLimitOptions, gpuLimitOptions }) => {
+          this.cpuOptions = cpuLimitOptions;
+          this.memoryOptions = memoryLimitOptions;
+          this.gpuOptions = gpuLimitOptions;
+
+          // fallback defaults
+          this.selectedCpu = this.cpuOptions[0] ?? "1";
+          this.selectedMemory = this.memoryOptions[0] ?? "1Gi";
+          this.selectedGpu = this.gpuOptions[0] ?? "0";
+
+          // Initialize JVM memory slider based on selected memory
+          this.updateJvmMemorySlider();
+        },
+        error: (err: unknown) =>
+          this.notificationService.error(`Failed to fetch resource options: 
${extractErrorMessage(err)}`),
+      });
+
+    this.computingUnitStatusService
+      .getAllComputingUnits()
+      .pipe(untilDestroyed(this))
+      .subscribe(units => {
+        this.allComputingUnits = units;
+        this.entries = units.map(u => new DashboardEntry(u));
+      });
+  }
+
+  terminateComputingUnit(cuid: number): void {
+    const unit = this.allComputingUnits.find(u => u.computingUnit.cuid === 
cuid);
+
+    if (!unit || !unit.computingUnit.uri) {
+      this.notificationService.error("Invalid computing unit.");
+      return;
+    }
+
+    const unitName = unit.computingUnit.name;
+    const unitType = unit?.computingUnit.type || "kubernetes"; // fallback
+    const templates = unitTypeMessageTemplate[unitType];
+
+    // Show confirmation modal
+    this.modalService.confirm({
+      nzTitle: templates.terminateTitle,
+      nzContent: templates.terminateWarning
+        ? `
+      <p>Are you sure you want to terminate <strong>${unitName}</strong>?</p>
+      ${templates.terminateWarning}
+    `
+        : `
+      <p>Are you sure you want to disconnect from 
<strong>${unitName}</strong>?</p>
+    `,
+      nzOkText: unitType === "local" ? "Disconnect" : "Terminate",
+      nzOkType: "primary",
+      nzOnOk: () => {
+        // Use the ComputingUnitStatusService to handle termination
+        // This will properly close the websocket before terminating the unit
+        this.computingUnitStatusService
+          .terminateComputingUnit(cuid)
+          .pipe(untilDestroyed(this))
+          .subscribe({
+            next: (success: boolean) => {
+              if (success) {
+                this.notificationService.success(`Terminated Computing Unit: 
${unitName}`);
+              } else {
+                this.notificationService.error("Failed to terminate computing 
unit");
+              }
+            },
+            error: (err: unknown) => {
+              this.notificationService.error(`Failed to terminate computing 
unit: ${extractErrorMessage(err)}`);
+            },
+          });
+      },
+      nzCancelText: "Cancel",
+    });
+  }
+
+  startComputingUnit(): void {
+    // Validate based on computing unit type
+    if (this.selectedComputingUnitType === "kubernetes") {
+      if (this.newComputingUnitName.trim() == "") {
+        this.notificationService.error("Name of the computing unit cannot be 
empty");
+        return;
+      }
+
+      this.selectedShmSize = `${this.shmSizeValue}${this.shmSizeUnit}`;
+
+      this.computingUnitService
+        .createKubernetesBasedComputingUnit(
+          this.newComputingUnitName,
+          this.selectedCpu,
+          this.selectedMemory,
+          this.selectedGpu,
+          this.selectedJvmMemorySize,
+          this.selectedShmSize
+        )

Review Comment:
   `isShmTooLarge()` is only used for showing a warning, but 
`startComputingUnit()` still proceeds to create the unit even when shared 
memory exceeds total memory. This will likely fail server-side or create an 
invalid configuration; consider blocking creation (disable the Create button 
and/or add a guard in `startComputingUnit()`).



##########
frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts:
##########
@@ -17,11 +17,371 @@
  * under the License.
  */
 
-import { Component } from "@angular/core";
+import { Component, Input, OnInit } from "@angular/core";
+import { ComputingUnitStatusService } from 
"../../../../workspace/service/computing-unit-status/computing-unit-status.service";
+import { DashboardEntry } from "../../../type/dashboard-entry";
+import {
+  DashboardWorkflowComputingUnit,
+  WorkflowComputingUnitType,
+} from "../../../../workspace/types/workflow-computing-unit";
+import { extractErrorMessage } from "../../../../common/util/error";
+import { NotificationService } from 
"../../../../common/service/notification/notification.service";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { UserService } from "../../../../common/service/user/user.service";
+import { WorkflowComputingUnitManagingService } from 
"../../../../workspace/service/workflow-computing-unit/workflow-computing-unit-managing.service";
+import {
+  parseResourceUnit,
+  parseResourceNumber,
+  findNearestValidStep,
+  unitTypeMessageTemplate,
+} from "../../../../common/util/computing-unit.util";
 
+@UntilDestroy()
 @Component({
   selector: "texera-computing-unit-section",
   templateUrl: "user-computing-unit.component.html",
   styleUrls: ["user-computing-unit.component.scss"],
 })
-export class UserComputingUnitComponent {}
+export class UserComputingUnitComponent implements OnInit {
+  public entries: DashboardEntry[] = [];
+  public isLogin = this.userService.isLogin();
+  public currentUid = this.userService.getCurrentUser()?.uid;
+  @Input() editable = false;
+
+  allComputingUnits: DashboardWorkflowComputingUnit[] = [];
+
+  // variables for creating a computing unit
+  addComputeUnitModalVisible = false;
+  newComputingUnitName: string = "";
+  selectedMemory: string = "";
+  selectedCpu: string = "";
+  selectedGpu: string = "0"; // Default to no GPU
+  selectedJvmMemorySize: string = "1G"; // Initial JVM memory size
+  selectedComputingUnitType?: WorkflowComputingUnitType; // Selected computing 
unit type
+  selectedShmSize: string = "64Mi"; // Shared memory size
+  shmSizeValue: number = 64; // default to 64
+  shmSizeUnit: "Mi" | "Gi" = "Mi"; // default unit
+  availableComputingUnitTypes: WorkflowComputingUnitType[] = [];
+  localComputingUnitUri: string = ""; // URI for local computing unit
+
+  // JVM memory slider configuration
+  jvmMemorySliderValue: number = 1; // Initial value in GB
+  jvmMemoryMarks: { [key: number]: string } = { 1: "1G" };
+  jvmMemoryMax: number = 1;
+  jvmMemorySteps: number[] = [1]; // Available steps in binary progression 
(1,2,4,8...)
+  showJvmMemorySlider: boolean = false; // Whether to show the slider
+
+  // cpu&memory limit options from backend
+  cpuOptions: string[] = [];
+  memoryOptions: string[] = [];
+  gpuOptions: string[] = []; // Add GPU options array
+
+  constructor(
+    private notificationService: NotificationService,
+    private modalService: NzModalService,
+    private userService: UserService,
+    private computingUnitService: WorkflowComputingUnitManagingService,
+    private computingUnitStatusService: ComputingUnitStatusService
+  ) {
+    this.userService
+      .userChanged()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        this.isLogin = this.userService.isLogin();
+        this.currentUid = this.userService.getCurrentUser()?.uid;
+      });
+  }
+
+  ngOnInit() {
+    this.newComputingUnitName = "My Computing Unit";
+    this.computingUnitService
+      .getComputingUnitTypes()
+      .pipe(untilDestroyed(this))
+      .subscribe({
+        next: ({ typeOptions }) => {
+          this.availableComputingUnitTypes = typeOptions;
+          // Set default selected type if available
+          if (typeOptions.includes("kubernetes")) {
+            this.selectedComputingUnitType = "kubernetes";
+          } else if (typeOptions.length > 0) {
+            this.selectedComputingUnitType = typeOptions[0];
+          }
+        },
+        error: (err: unknown) =>
+          this.notificationService.error(`Failed to fetch computing unit 
types: ${extractErrorMessage(err)}`),
+      });
+
+    this.computingUnitService
+      .getComputingUnitLimitOptions()
+      .pipe(untilDestroyed(this))
+      .subscribe({
+        next: ({ cpuLimitOptions, memoryLimitOptions, gpuLimitOptions }) => {
+          this.cpuOptions = cpuLimitOptions;
+          this.memoryOptions = memoryLimitOptions;
+          this.gpuOptions = gpuLimitOptions;
+
+          // fallback defaults
+          this.selectedCpu = this.cpuOptions[0] ?? "1";
+          this.selectedMemory = this.memoryOptions[0] ?? "1Gi";
+          this.selectedGpu = this.gpuOptions[0] ?? "0";
+
+          // Initialize JVM memory slider based on selected memory
+          this.updateJvmMemorySlider();
+        },
+        error: (err: unknown) =>
+          this.notificationService.error(`Failed to fetch resource options: 
${extractErrorMessage(err)}`),
+      });
+
+    this.computingUnitStatusService
+      .getAllComputingUnits()
+      .pipe(untilDestroyed(this))
+      .subscribe(units => {
+        this.allComputingUnits = units;
+        this.entries = units.map(u => new DashboardEntry(u));
+      });
+  }
+
+  terminateComputingUnit(cuid: number): void {
+    const unit = this.allComputingUnits.find(u => u.computingUnit.cuid === 
cuid);
+
+    if (!unit || !unit.computingUnit.uri) {
+      this.notificationService.error("Invalid computing unit.");
+      return;
+    }
+
+    const unitName = unit.computingUnit.name;
+    const unitType = unit?.computingUnit.type || "kubernetes"; // fallback
+    const templates = unitTypeMessageTemplate[unitType];
+
+    // Show confirmation modal
+    this.modalService.confirm({
+      nzTitle: templates.terminateTitle,
+      nzContent: templates.terminateWarning
+        ? `
+      <p>Are you sure you want to terminate <strong>${unitName}</strong>?</p>
+      ${templates.terminateWarning}
+    `
+        : `
+      <p>Are you sure you want to disconnect from 
<strong>${unitName}</strong>?</p>
+    `,
+      nzOkText: unitType === "local" ? "Disconnect" : "Terminate",
+      nzOkType: "primary",
+      nzOnOk: () => {
+        // Use the ComputingUnitStatusService to handle termination
+        // This will properly close the websocket before terminating the unit
+        this.computingUnitStatusService
+          .terminateComputingUnit(cuid)
+          .pipe(untilDestroyed(this))
+          .subscribe({
+            next: (success: boolean) => {
+              if (success) {
+                this.notificationService.success(`Terminated Computing Unit: 
${unitName}`);
+              } else {
+                this.notificationService.error("Failed to terminate computing 
unit");
+              }
+            },
+            error: (err: unknown) => {
+              this.notificationService.error(`Failed to terminate computing 
unit: ${extractErrorMessage(err)}`);
+            },
+          });
+      },
+      nzCancelText: "Cancel",
+    });
+  }
+
+  startComputingUnit(): void {
+    // Validate based on computing unit type
+    if (this.selectedComputingUnitType === "kubernetes") {
+      if (this.newComputingUnitName.trim() == "") {
+        this.notificationService.error("Name of the computing unit cannot be 
empty");
+        return;
+      }
+
+      this.selectedShmSize = `${this.shmSizeValue}${this.shmSizeUnit}`;
+
+      this.computingUnitService
+        .createKubernetesBasedComputingUnit(
+          this.newComputingUnitName,
+          this.selectedCpu,
+          this.selectedMemory,
+          this.selectedGpu,
+          this.selectedJvmMemorySize,
+          this.selectedShmSize
+        )
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: () => {
+            this.notificationService.success("Successfully created the new 
Kubernetes compute unit");
+            this.computingUnitStatusService.refreshComputingUnitList();
+          },
+          error: (err: unknown) =>
+            this.notificationService.error(`Failed to start Kubernetes 
computing unit: ${extractErrorMessage(err)}`),
+        });
+    } else if (this.selectedComputingUnitType === "local") {
+      // For local computing units, validate the URI
+      if (!this.localComputingUnitUri || this.localComputingUnitUri.trim() === 
"") {
+        this.notificationService.error("URI for local computing unit cannot be 
empty");
+        return;
+      }
+
+      this.computingUnitService
+        .createLocalComputingUnit(this.newComputingUnitName, 
this.localComputingUnitUri)
+        .pipe(untilDestroyed(this))

Review Comment:
   In the `local` computing unit branch, only the URI is validated, but 
`createLocalComputingUnit` still requires a non-empty `name`. Please add the 
same non-empty (and possibly max-length) validation used elsewhere before 
calling the service.



##########
frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html:
##########
@@ -0,0 +1,203 @@
+<!--
+ 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.
+-->
+
+<nz-card
+  [nzBodyStyle]="{padding: '3px'}"
+  class="computing-unit-list-item-card">
+  <div
+    nz-row
+    nzAlign="middle">
+    <div
+      nz-col
+      nzFlex="20px"></div>
+
+    <div
+      nz-col
+      nzFlex="0"
+      class="type-icon">
+      <i
+        nz-icon
+        nzType="deployment-unit"></i>
+    </div>
+
+    <div
+      nz-col
+      nzFlex="0"
+      class="unit-id">
+      <i>#{{ unit.cuid }}</i>
+    </div>
+
+    <div
+      nz-col
+      nzFlex="30px">
+      <div class="edit-button">
+        <button
+          nz-button
+          nzType="text"
+          title="Rename"
+          (click)="startEditingUnitName(entry); $event.stopPropagation()">
+          <i
+            nz-icon
+            nzType="edit"></i>
+        </button>
+      </div>
+    </div>
+
+    <div
+      nz-col
+      nzFlex="1"
+      class="resource-name-group">
+      <div
+        class="resource-name truncate-single-line"
+        *ngIf="editingNameOfUnit !== unit.cuid; else editableUnitName"
+        (click)="openComputingUnitMetadataModal(entry)">
+        {{ unit.name }}
+        <nz-badge
+          [nzColor]="getBadgeColor(entry.status)"
+          [nz-tooltip]="getUnitStatusTooltip(entry)"></nz-badge>
+      </div>
+      <ng-template #editableUnitName>
+        <input
+          #unitNameInput
+          (keydown.enter)="confirmUpdateUnitName(unit.cuid, 
unitNameInput.value)"
+          (keydown.escape)="cancelEditingUnitName()"
+          [value]="editingUnitName"
+          nz-input
+          class="unit-name-edit-input"
+          maxlength="128"
+          (click)="$event.stopPropagation()"
+          autofocus
+      /></ng-template>
+    </div>
+
+    <div class="button-group">
+      <button
+        nz-button
+        nzType="text"
+        title="Delete"
+        (nzOnConfirm)="deleted.emit()"
+        nz-popconfirm
+        nzPopconfirmTitle="Confirm to delete this item.">
+        <i
+          nz-icon
+          nzType="delete"></i>
+      </button>
+    </div>

Review Comment:
   The delete button uses `nz-popconfirm` (first confirmation) and then the 
parent handler `terminateComputingUnit(...)` opens another 
`modalService.confirm` (second confirmation). This results in a double-confirm 
UX. Consider removing the popconfirm here and letting the parent modal handle 
confirmation (it also includes type-specific warnings).



##########
frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html:
##########
@@ -0,0 +1,203 @@
+<!--
+ 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.
+-->
+
+<nz-card
+  [nzBodyStyle]="{padding: '3px'}"
+  class="computing-unit-list-item-card">
+  <div
+    nz-row
+    nzAlign="middle">
+    <div
+      nz-col
+      nzFlex="20px"></div>
+
+    <div
+      nz-col
+      nzFlex="0"
+      class="type-icon">
+      <i
+        nz-icon
+        nzType="deployment-unit"></i>
+    </div>
+
+    <div
+      nz-col
+      nzFlex="0"
+      class="unit-id">
+      <i>#{{ unit.cuid }}</i>
+    </div>
+
+    <div
+      nz-col
+      nzFlex="30px">
+      <div class="edit-button">
+        <button
+          nz-button
+          nzType="text"
+          title="Rename"
+          (click)="startEditingUnitName(entry); $event.stopPropagation()">
+          <i
+            nz-icon
+            nzType="edit"></i>
+        </button>
+      </div>
+    </div>
+
+    <div
+      nz-col
+      nzFlex="1"
+      class="resource-name-group">
+      <div
+        class="resource-name truncate-single-line"
+        *ngIf="editingNameOfUnit !== unit.cuid; else editableUnitName"
+        (click)="openComputingUnitMetadataModal(entry)">
+        {{ unit.name }}
+        <nz-badge
+          [nzColor]="getBadgeColor(entry.status)"
+          [nz-tooltip]="getUnitStatusTooltip(entry)"></nz-badge>
+      </div>
+      <ng-template #editableUnitName>
+        <input
+          #unitNameInput
+          (keydown.enter)="confirmUpdateUnitName(unit.cuid, 
unitNameInput.value)"
+          (keydown.escape)="cancelEditingUnitName()"
+          [value]="editingUnitName"
+          nz-input
+          class="unit-name-edit-input"
+          maxlength="128"
+          (click)="$event.stopPropagation()"
+          autofocus
+      /></ng-template>
+    </div>
+
+    <div class="button-group">
+      <button
+        nz-button
+        nzType="text"
+        title="Delete"
+        (nzOnConfirm)="deleted.emit()"
+        nz-popconfirm
+        nzPopconfirmTitle="Confirm to delete this item.">
+        <i
+          nz-icon
+          nzType="delete"></i>
+      </button>
+    </div>
+
+    <div
+      nz-button
+      nz-popover
+      [nzPopoverContent]="metricsTemplate"
+      nzPopoverTrigger="hover"
+      nzPopoverPlacement="bottom"
+      id="metrics-container-id"
+      class="metrics-container">
+      <div class="metric-item">
+        <span class="metric-label">CPU</span>
+        <div class="metric-bar-wrapper">
+          <nz-progress
+            id="cpu-progress-bar"
+            [nzPercent]="getCpuPercentage()"
+            [nzStrokeColor]="'#52c41a'"
+            [nzStatus]="getCpuStatus()"
+            nzType="line"
+            [nzStrokeWidth]="8"
+            [nzShowInfo]="false"></nz-progress>
+        </div>
+      </div>
+
+      <div class="metric-item">
+        <span class="metric-label">Memory</span>
+        <div class="metric-bar-wrapper">
+          <nz-progress
+            id="memory-progress-bar"
+            [nzPercent]="getMemoryPercentage()"
+            [nzStrokeColor]="'#1890ff'"
+            [nzStatus]="getMemoryStatus()"
+            nzType="line"
+            [nzStrokeWidth]="8"
+            [nzShowInfo]="false"></nz-progress>
+        </div>
+      </div>
+    </div>

Review Comment:
   This list item template assigns fixed `id` values like 
`metrics-container-id`, `cpu-progress-bar`, and `memory-progress-bar`. When 
rendering multiple computing units, the DOM will contain duplicate IDs, which 
is invalid HTML and can break styling/behavior that relies on ID selectors. Use 
classes instead of IDs (and update the SCSS selectors accordingly).



##########
frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts:
##########
@@ -0,0 +1,319 @@
+/**
+ * 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.
+ */
+
+import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } 
from "@angular/core";
+import { ComputingUnitStatusService } from 
"../../../../../workspace/service/computing-unit-status/computing-unit-status.service";
+import { extractErrorMessage } from "../../../../../common/util/error";
+import { NotificationService } from 
"../../../../../common/service/notification/notification.service";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import {
+  DashboardWorkflowComputingUnit,
+  WorkflowComputingUnit,
+} from "../../../../../workspace/types/workflow-computing-unit";
+import { WorkflowComputingUnitManagingService } from 
"../../../../../workspace/service/workflow-computing-unit/workflow-computing-unit-managing.service";
+import {
+  buildComputingUnitMetadataTable,
+  parseResourceUnit,
+  parseResourceNumber,
+  cpuResourceConversion,
+  memoryResourceConversion,
+  cpuPercentage,
+  memoryPercentage,
+} from "../../../../../common/util/computing-unit.util";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-user-computing-unit-list-item",
+  templateUrl: "./user-computing-unit-list-item.component.html",
+  styleUrls: ["./user-computing-unit-list-item.component.scss"],
+})
+export class UserComputingUnitListItemComponent implements OnInit {
+  private _entry?: DashboardWorkflowComputingUnit;
+  editingNameOfUnit: number | null = null;
+  editingUnitName: string = "";
+  gpuOptions: string[] = [];
+  @Input() editable = false;
+  @Output() deleted = new EventEmitter<void>();
+
+  @Input()
+  get entry(): DashboardWorkflowComputingUnit {
+    if (!this._entry) {
+      throw new Error("entry property must be provided to 
UserComputingUnitListItemComponent.");
+    }
+    return this._entry;
+  }
+
+  set entry(value: DashboardWorkflowComputingUnit) {
+    this._entry = value;
+  }
+
+  get unit(): WorkflowComputingUnit {
+    if (!this.entry.computingUnit) {
+      throw new Error(
+        "Incorrect type of DashboardEntry provided to 
UserComputingUnitListItemComponent. Entry must be computing unit."
+      );
+    }
+    return this.entry.computingUnit;
+  }
+
+  constructor(
+    private cdr: ChangeDetectorRef,
+    private modalService: NzModalService,
+    private notificationService: NotificationService,
+    private computingUnitService: WorkflowComputingUnitManagingService,
+    private computingUnitStatusService: ComputingUnitStatusService
+  ) {}
+
+  ngOnInit(): void {
+    this.computingUnitService
+      .getComputingUnitLimitOptions()
+      .pipe(untilDestroyed(this))
+      .subscribe({
+        next: ({ gpuLimitOptions }) => {
+          this.gpuOptions = gpuLimitOptions ?? [];
+        },
+        error: (err: unknown) =>
+          this.notificationService.error(`Failed to fetch resource options: 
${extractErrorMessage(err)}`),
+      });
+  }
+
+  startEditingUnitName(entry: DashboardWorkflowComputingUnit): void {
+    if (!entry.isOwner) {
+      this.notificationService.error("Only owners can rename computing units");
+      return;
+    }
+
+    this.editingNameOfUnit = entry.computingUnit.cuid;
+    this.editingUnitName = entry.computingUnit.name;
+
+    // Force change detection and focus the input
+    this.cdr.detectChanges();
+    setTimeout(() => {
+      const input = document.querySelector(".unit-name-edit-input") as 
HTMLInputElement;
+      if (input) {
+        input.focus();
+        input.select();
+      }
+    }, 0);
+  }

Review Comment:
   `startEditingUnitName()` focuses the edit input via 
`document.querySelector('.unit-name-edit-input')`, which can select the wrong 
element if multiple list items are simultaneously in edit mode (or if the DOM 
structure changes). Prefer scoping the lookup to this component (e.g., 
`@ViewChild` + `ElementRef`) so the correct input is focused reliably.



##########
frontend/src/app/common/util/computing-unit.util.ts:
##########
@@ -0,0 +1,185 @@
+/**
+ * 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.
+ */
+
+import { DashboardWorkflowComputingUnit } from 
"../../workspace/types/workflow-computing-unit";
+
+export function buildComputingUnitMetadataTable(unit: 
DashboardWorkflowComputingUnit): string {
+  return `
+    <table class="ant-table">
+      <tbody>
+        <tr><th style="width: 
150px;">Name</th><td>${unit.computingUnit.name}</td></tr>
+        <tr><th>Status</th><td>${unit.status}</td></tr>
+        <tr><th>Type</th><td>${unit.computingUnit.type}</td></tr>
+        <tr><th>CPU 
Limit</th><td>${unit.computingUnit.resource.cpuLimit}</td></tr>
+        <tr><th>Memory 
Limit</th><td>${unit.computingUnit.resource.memoryLimit}</td></tr>
+        <tr><th>GPU Limit</th><td>${unit.computingUnit.resource.gpuLimit || 
"None"}</td></tr>
+        <tr><th>JVM 
Memory</th><td>${unit.computingUnit.resource.jvmMemorySize}</td></tr>
+        <tr><th>Shared 
Memory</th><td>${unit.computingUnit.resource.shmSize}</td></tr>
+        <tr><th>Created</th><td>${new 
Date(unit.computingUnit.creationTime).toLocaleString()}</td></tr>
+        <tr><th>Access</th><td>${unit.isOwner ? "Owner" : 
unit.accessPrivilege}</td></tr>

Review Comment:
   `buildComputingUnitMetadataTable()` returns an HTML string with unescaped 
values (e.g., `unit.computingUnit.name`, `ownerName`). If any of these fields 
contain user-controlled content, this can become an XSS vector when rendered as 
HTML in a modal. Prefer rendering the table via an Angular component/template 
with normal bindings (auto-escaped), or explicitly escape the interpolated 
values before constructing the HTML string.
   ```suggestion
   function escapeHtml(value: string | number | boolean | null | undefined): 
string {
     return String(value ?? "")
       .replace(/&/g, "&amp;")
       .replace(/</g, "&lt;")
       .replace(/>/g, "&gt;")
       .replace(/"/g, "&quot;")
       .replace(/'/g, "&#39;");
   }
   
   export function buildComputingUnitMetadataTable(unit: 
DashboardWorkflowComputingUnit): string {
     return `
       <table class="ant-table">
         <tbody>
           <tr><th style="width: 
150px;">Name</th><td>${escapeHtml(unit.computingUnit.name)}</td></tr>
           <tr><th>Status</th><td>${escapeHtml(unit.status)}</td></tr>
           <tr><th>Type</th><td>${escapeHtml(unit.computingUnit.type)}</td></tr>
           <tr><th>CPU 
Limit</th><td>${escapeHtml(unit.computingUnit.resource.cpuLimit)}</td></tr>
           <tr><th>Memory 
Limit</th><td>${escapeHtml(unit.computingUnit.resource.memoryLimit)}</td></tr>
           <tr><th>GPU 
Limit</th><td>${escapeHtml(unit.computingUnit.resource.gpuLimit || 
"None")}</td></tr>
           <tr><th>JVM 
Memory</th><td>${escapeHtml(unit.computingUnit.resource.jvmMemorySize)}</td></tr>
           <tr><th>Shared 
Memory</th><td>${escapeHtml(unit.computingUnit.resource.shmSize)}</td></tr>
           <tr><th>Created</th><td>${escapeHtml(new 
Date(unit.computingUnit.creationTime).toLocaleString())}</td></tr>
           <tr><th>Access</th><td>${escapeHtml(unit.isOwner ? "Owner" : 
unit.accessPrivilege)}</td></tr>
   ```



##########
frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts:
##########
@@ -20,22 +20,43 @@
 import { ComponentFixture, TestBed } from "@angular/core/testing";
 import { UserComputingUnitComponent } from "./user-computing-unit.component";
 import { NzCardModule } from "ng-zorro-antd/card";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { HttpClient } from "@angular/common/http";
+import { UserService } from "../../../../common/service/user/user.service";
+import { StubUserService } from 
"../../../../common/service/user/stub-user.service";
+import { commonTestProviders } from "../../../../common/testing/test-utils";
+import { WorkflowComputingUnitManagingService } from 
"../../../../workspace/service/workflow-computing-unit/workflow-computing-unit-managing.service";
+import { ComputingUnitStatusService } from 
"../../../../workspace/service/computing-unit-status/computing-unit-status.service";
+import { MockComputingUnitStatusService } from 
"../../../../workspace/service/computing-unit-status/mock-computing-unit-status.service";
 
 describe("UserComputingUnitComponent", () => {
   let component: UserComputingUnitComponent;
   let fixture: ComponentFixture<UserComputingUnitComponent>;
+  let mockComputingUnitService: 
jasmine.SpyObj<WorkflowComputingUnitManagingService>;
 
-  beforeEach(async () => {
-    await TestBed.configureTestingModule({
+  beforeEach(() => {
+    mockComputingUnitService = 
jasmine.createSpyObj<WorkflowComputingUnitManagingService>([
+      "getComputingUnitTypes",
+      "getComputingUnitLimitOptions",
+      "createKubernetesBasedComputingUnit",
+      "createLocalComputingUnit",
+    ]);
+
+    TestBed.configureTestingModule({
       declarations: [UserComputingUnitComponent],
+      providers: [
+        NzModalService,
+        HttpClient,
+        { provide: UserService, useClass: StubUserService },
+        { provide: WorkflowComputingUnitManagingService, useValue: 
mockComputingUnitService },
+        { provide: ComputingUnitStatusService, useClass: 
MockComputingUnitStatusService },
+        ...commonTestProviders,
+      ],
       imports: [NzCardModule],
     }).compileComponents();

Review Comment:
   The test setup calls 
`TestBed.configureTestingModule(...).compileComponents()` but doesn’t 
`await`/return the promise. Angular compilation is async, so this can create 
race conditions/flaky tests. Please use `beforeEach(async () => { await 
TestBed...compileComponents(); })` (or `waitForAsync`) before creating the 
fixture.



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