From c0962de3bc52a4cab6ec09b3dd332abb86b3991f Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Thu, 26 Mar 2026 17:26:33 -0700 Subject: [PATCH 01/22] feat(gui): add computing unit types --- .../src/app/dashboard/type/dashboard-entry.ts | 39 ++++++++++++++++++- .../src/app/dashboard/type/search-result.ts | 2 +- .../src/app/dashboard/type/type-predicates.ts | 5 +++ frontend/src/app/hub/service/hub.service.ts | 1 + 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/dashboard/type/dashboard-entry.ts b/frontend/src/app/dashboard/type/dashboard-entry.ts index 6dfb46cc1cd..735249d64c8 100644 --- a/frontend/src/app/dashboard/type/dashboard-entry.ts +++ b/frontend/src/app/dashboard/type/dashboard-entry.ts @@ -21,7 +21,14 @@ import { DashboardFile } from "./dashboard-file.interface"; import { DashboardWorkflow } from "./dashboard-workflow.interface"; import { DashboardProject } from "./dashboard-project.interface"; import { DashboardDataset } from "./dashboard-dataset.interface"; -import { isDashboardDataset, isDashboardFile, isDashboardProject, isDashboardWorkflow } from "./type-predicates"; +import { DashboardWorkflowComputingUnit } from "../../workspace/types/workflow-computing-unit"; +import { + isDashboardDataset, + isDashboardFile, + isDashboardProject, + isDashboardWorkflow, + isDashboardWorkflowComputingUnit, +} from "./type-predicates"; import { EntityType } from "../../hub/service/hub.service"; export interface UserInfo { @@ -50,7 +57,14 @@ export class DashboardEntry { accessibleUserIds: number[]; coverImageUrl?: string; - constructor(public value: DashboardWorkflow | DashboardProject | DashboardFile | DashboardDataset) { + constructor( + public value: + | DashboardWorkflow + | DashboardProject + | DashboardFile + | DashboardDataset + | DashboardWorkflowComputingUnit + ) { if (isDashboardWorkflow(value)) { this.type = EntityType.Workflow; this.id = value.workflow.wid; @@ -124,6 +138,20 @@ export class DashboardEntry { this.isLiked = false; this.accessibleUserIds = []; this.coverImageUrl = value.dataset.coverImage; + } else if (isDashboardWorkflowComputingUnit(value)) { + this.type = EntityType.ComputingUnit; + this.id = value.computingUnit.cuid; + this.name = value.computingUnit.name; + this.creationTime = value.computingUnit.creationTime; + this.accessLevel = value.accessPrivilege; + this.ownerName = ""; + this.ownerGoogleAvatar = ""; + this.ownerId = value.computingUnit.uid; + this.viewCount = 0; + this.cloneCount = 0; + this.likeCount = 0; + this.isLiked = false; + this.accessibleUserIds = []; } else { throw new Error("Unexpected type in DashboardEntry."); } @@ -182,4 +210,11 @@ export class DashboardEntry { } return this.value; } + + get computingUnit(): DashboardWorkflowComputingUnit { + if (!isDashboardWorkflowComputingUnit(this.value)) { + throw new Error("Value is not of type DashboardWorkflowComputingUnit"); + } + return this.value; + } } diff --git a/frontend/src/app/dashboard/type/search-result.ts b/frontend/src/app/dashboard/type/search-result.ts index 268381b0a5d..8fc14361b17 100644 --- a/frontend/src/app/dashboard/type/search-result.ts +++ b/frontend/src/app/dashboard/type/search-result.ts @@ -24,7 +24,7 @@ import { DashboardDataset } from "./dashboard-dataset.interface"; import { DashboardEntry } from "./dashboard-entry"; export interface SearchResultItem { - resourceType: "workflow" | "project" | "file" | "dataset"; + resourceType: "workflow" | "project" | "file" | "dataset" | "computing-unit"; workflow?: DashboardWorkflow; project?: DashboardProject; file?: DashboardFile; diff --git a/frontend/src/app/dashboard/type/type-predicates.ts b/frontend/src/app/dashboard/type/type-predicates.ts index 6c9cf96263d..0ed825feb49 100644 --- a/frontend/src/app/dashboard/type/type-predicates.ts +++ b/frontend/src/app/dashboard/type/type-predicates.ts @@ -21,6 +21,7 @@ import { DashboardWorkflow } from "./dashboard-workflow.interface"; import { DashboardProject } from "./dashboard-project.interface"; import { DashboardFile } from "./dashboard-file.interface"; import { DashboardDataset } from "./dashboard-dataset.interface"; +import { DashboardWorkflowComputingUnit } from "../../workspace/types/workflow-computing-unit"; export function isDashboardWorkflow(value: any): value is DashboardWorkflow { return value && typeof value.workflow === "object"; @@ -37,3 +38,7 @@ export function isDashboardFile(value: any): value is DashboardFile { export function isDashboardDataset(value: any): value is DashboardDataset { return value && typeof value.dataset === "object"; } + +export function isDashboardWorkflowComputingUnit(value: any): value is DashboardWorkflowComputingUnit { + return value && typeof value.computingUnit === "object"; +} diff --git a/frontend/src/app/hub/service/hub.service.ts b/frontend/src/app/hub/service/hub.service.ts index 231fb5e3b8c..66cd08a9149 100644 --- a/frontend/src/app/hub/service/hub.service.ts +++ b/frontend/src/app/hub/service/hub.service.ts @@ -30,6 +30,7 @@ export enum EntityType { Dataset = "dataset", Project = "project", File = "file", + ComputingUnit = "computing-unit", } export enum ActionType { From 70860c4ffa4fda7686708fc25721b2e25b1f566b Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Thu, 26 Mar 2026 18:07:31 -0700 Subject: [PATCH 02/22] feat(gui): add computing unit shell --- frontend/src/app/app-routing.constant.ts | 1 + frontend/src/app/app-routing.module.ts | 5 ++++ frontend/src/app/app.module.ts | 2 ++ .../component/dashboard.component.html | 11 +++++++ .../component/dashboard.component.ts | 2 ++ .../user-computing-unit.component.html | 29 +++++++++++++++++++ .../user-computing-unit.component.scss | 29 +++++++++++++++++++ .../user-computing-unit.component.ts | 29 +++++++++++++++++++ 8 files changed, 108 insertions(+) create mode 100644 frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html create mode 100644 frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.scss create mode 100644 frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts diff --git a/frontend/src/app/app-routing.constant.ts b/frontend/src/app/app-routing.constant.ts index a22f6439e26..582f6d865b1 100644 --- a/frontend/src/app/app-routing.constant.ts +++ b/frontend/src/app/app-routing.constant.ts @@ -35,6 +35,7 @@ export const DASHBOARD_USER_WORKSPACE = `${DASHBOARD_USER}/workflow`; export const DASHBOARD_USER_WORKFLOW = `${DASHBOARD_USER}/workflow`; export const DASHBOARD_USER_DATASET = `${DASHBOARD_USER}/dataset`; export const DASHBOARD_USER_DATASET_CREATE = `${DASHBOARD_USER_DATASET}/create`; +export const DASHBOARD_USER_COMPUTING_UNIT = `${DASHBOARD_USER}/unit`; export const DASHBOARD_USER_QUOTA = `${DASHBOARD_USER}/quota`; export const DASHBOARD_USER_DISCUSSION = `${DASHBOARD_USER}/discussion`; diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index a44c55393f3..a6263a2c023 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -24,6 +24,7 @@ import { UserWorkflowComponent } from "./dashboard/component/user/user-workflow/ import { UserQuotaComponent } from "./dashboard/component/user/user-quota/user-quota.component"; import { UserProjectSectionComponent } from "./dashboard/component/user/user-project/user-project-section/user-project-section.component"; import { UserProjectComponent } from "./dashboard/component/user/user-project/user-project.component"; +import { UserComputingUnitComponent } from "./dashboard/component/user/user-computing-unit/user-computing-unit.component"; import { WorkspaceComponent } from "./workspace/component/workspace.component"; import { AboutComponent } from "./hub/component/about/about.component"; import { AuthGuardService } from "./common/service/user/auth-guard.service"; @@ -130,6 +131,10 @@ routes.push({ path: "dataset/create", component: DatasetDetailComponent, }, + { + path: "unit", + component: UserComputingUnitComponent, + }, { path: "quota", component: UserQuotaComponent, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index b41b1f80b73..6a92d4be2e9 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -185,6 +185,7 @@ import { NzCheckboxModule } from "ng-zorro-antd/checkbox"; import { NzRadioModule } from "ng-zorro-antd/radio"; import { RegistrationRequestModalComponent } from "./common/service/user/registration-request-modal/registration-request-modal.component"; import { MarkdownDescriptionComponent } from "./dashboard/component/user/markdown-description/markdown-description.component"; +import { UserComputingUnitComponent } from "./dashboard/component/user/user-computing-unit/user-computing-unit.component"; registerLocaleData(en); @@ -283,6 +284,7 @@ registerLocaleData(en); AdminSettingsComponent, RegistrationRequestModalComponent, MarkdownDescriptionComponent, + UserComputingUnitComponent, ], imports: [ BrowserModule, diff --git a/frontend/src/app/dashboard/component/dashboard.component.html b/frontend/src/app/dashboard/component/dashboard.component.html index b238f56b938..1b1bc910b43 100644 --- a/frontend/src/app/dashboard/component/dashboard.component.html +++ b/frontend/src/app/dashboard/component/dashboard.component.html @@ -97,6 +97,17 @@ nzType="database"> Datasets +
  • + + Computing Units +
  • + +
    + +

    Computing Units

    +
    + + + +
    diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.scss b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.scss new file mode 100644 index 00000000000..181db6355aa --- /dev/null +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.scss @@ -0,0 +1,29 @@ +/** + * 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 "../../dashboard.component.scss"; +@import "../../section-style"; +@import "../../button-style"; + +.subsection-grid-container { + min-width: 100%; + width: 100%; + min-height: 100%; + height: 100%; +} diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts new file mode 100644 index 00000000000..971a051934a --- /dev/null +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts @@ -0,0 +1,29 @@ +/** + * 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 { Component } from "@angular/core"; + +@Component({ + selector: "texera-computing-unit-section", + templateUrl: "user-computing-unit.component.html", + styleUrls: ["user-computing-unit.component.scss"], +}) +export class UserComputingUnitComponent { + +} From df3a9ba25e91cd50aa83fee4d950a5c36e20904a Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Fri, 27 Mar 2026 16:04:23 -0700 Subject: [PATCH 03/22] feat(gui): add computing unit full --- frontend/src/app/app.module.ts | 2 + .../app/common/util/computing-unit.util.ts | 185 +++++++++ ...er-computing-unit-list-item.component.html | 202 ++++++++++ ...er-computing-unit-list-item.component.scss | 230 +++++++++++ ...user-computing-unit-list-item.component.ts | 325 ++++++++++++++++ .../user-computing-unit.component.html | 204 ++++++++++ .../user-computing-unit.component.scss | 70 ++++ .../user-computing-unit.component.ts | 359 +++++++++++++++++- 8 files changed, 1575 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/common/util/computing-unit.util.ts create mode 100644 frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html create mode 100644 frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.scss create mode 100644 frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 6a92d4be2e9..b6364e781f9 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -186,6 +186,7 @@ import { NzRadioModule } from "ng-zorro-antd/radio"; import { RegistrationRequestModalComponent } from "./common/service/user/registration-request-modal/registration-request-modal.component"; import { MarkdownDescriptionComponent } from "./dashboard/component/user/markdown-description/markdown-description.component"; import { UserComputingUnitComponent } from "./dashboard/component/user/user-computing-unit/user-computing-unit.component"; +import { UserComputingUnitListItemComponent } from "./dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component"; registerLocaleData(en); @@ -285,6 +286,7 @@ registerLocaleData(en); RegistrationRequestModalComponent, MarkdownDescriptionComponent, UserComputingUnitComponent, + UserComputingUnitListItemComponent, ], imports: [ BrowserModule, diff --git a/frontend/src/app/common/util/computing-unit.util.ts b/frontend/src/app/common/util/computing-unit.util.ts new file mode 100644 index 00000000000..73eba77ca65 --- /dev/null +++ b/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 ` + + + + + + + + + + + + + +
    Name${unit.computingUnit.name}
    Status${unit.status}
    Type${unit.computingUnit.type}
    CPU Limit${unit.computingUnit.resource.cpuLimit}
    Memory Limit${unit.computingUnit.resource.memoryLimit}
    GPU Limit${unit.computingUnit.resource.gpuLimit || "None"}
    JVM Memory${unit.computingUnit.resource.jvmMemorySize}
    Shared Memory${unit.computingUnit.resource.shmSize}
    Created${new Date(unit.computingUnit.creationTime).toLocaleString()}
    Access${unit.isOwner ? "Owner" : unit.accessPrivilege}
    + `; +} + +export function parseResourceUnit(resource: string): string { + // check if has a capacity (is a number followed by a unit) + if (!resource || resource === "NaN") return "NaN"; + const re = /^(\d+(\.\d+)?)([a-zA-Z]*)$/; + const match = resource.match(re); + if (match) { + return match[3] || ""; + } + return ""; +} + +export function parseResourceNumber(resource: string): number { + // check if has a capacity (is a number followed by a unit) + if (!resource || resource === "NaN") return 0; + const re = /^(\d+(\.\d+)?)([a-zA-Z]*)$/; + const match = resource.match(re); + if (match) { + return parseFloat(match[1]); + } + return 0; +} + +export function cpuResourceConversion(from: string, toUnit: string): string { + // cpu conversions + type CpuUnit = "n" | "u" | "m" | ""; + const cpuScales: { [key in CpuUnit]: number } = { + n: 1, + u: 1_000, + m: 1_000_000, + "": 1_000_000_000, + }; + const fromUnit = parseResourceUnit(from) as CpuUnit; + const fromNumber = parseResourceNumber(from); + + // Handle empty unit in input (means cores) + const effectiveFromUnit = (fromUnit || "") as CpuUnit; + const effectiveToUnit = (toUnit || "") as CpuUnit; + + // Convert to base units (nanocores) then to target unit + const fromScaled = fromNumber * (cpuScales[effectiveFromUnit] || cpuScales["m"]); + const toScaled = fromScaled / (cpuScales[effectiveToUnit] || cpuScales[""]); + + // For display purposes, use appropriate precision + if (effectiveToUnit === "") { + return toScaled.toFixed(4); // 4 decimal places for cores + } else if (effectiveToUnit === "m") { + return toScaled.toFixed(2); // 2 decimal places for millicores + } else { + return Math.round(toScaled).toString(); // Whole numbers for smaller units + } +} + +export function memoryResourceConversion(from: string, toUnit: string): string { + // memory conversion + type MemoryUnit = "Ki" | "Mi" | "Gi" | ""; + const memoryScales: { [key in MemoryUnit]: number } = { + "": 1, + Ki: 1024, + Mi: 1024 * 1024, + Gi: 1024 * 1024 * 1024, + }; + const fromUnit = parseResourceUnit(from) as MemoryUnit; + const fromNumber = parseResourceNumber(from); + + // Handle empty unit in input (means bytes) + const effectiveFromUnit = (fromUnit || "") as MemoryUnit; + const effectiveToUnit = (toUnit || "") as MemoryUnit; + + // Convert to base units (bytes) then to target unit + const fromScaled = fromNumber * (memoryScales[effectiveFromUnit] || 1); + const toScaled = fromScaled / (memoryScales[effectiveToUnit] || 1); + + // For memory, we want to show in the same format as the limit (typically X.XXX Gi) + return toScaled.toFixed(4); +} + +export function cpuPercentage(usage: string, limit: string): number { + if (usage === "N/A" || limit === "N/A") return 0; + + // Convert to the same unit for comparison + const displayUnit = ""; // Convert to cores for percentage calculation + + // Use our existing conversion method to get values in the same unit + const usageValue = parseFloat(cpuResourceConversion(usage, displayUnit)); + const limitValue = parseFloat(cpuResourceConversion(limit, displayUnit)); + + if (limitValue <= 0) return 0; + + // Calculate percentage and ensure it doesn't exceed 100% + const percentage = (usageValue / limitValue) * 100; + + return Math.min(percentage, 100); +} + +export function memoryPercentage(usage: string, limit: string): number { + if (usage === "N/A" || limit === "N/A") return 0; + + // Convert to the same unit for comparison + const displayUnit = "Gi"; // Convert to GiB for percentage calculation + + // Use our existing conversion method to get values in the same unit + const usageValue = parseFloat(memoryResourceConversion(usage, displayUnit)); + const limitValue = parseFloat(memoryResourceConversion(limit, displayUnit)); + + if (limitValue <= 0) return 0; + + // Calculate percentage and ensure it doesn't exceed 100% + const percentage = (usageValue / limitValue) * 100; + + return Math.min(percentage, 100); +} + +export function findNearestValidStep(value: number, jvmMemorySteps: number[]): number { + if (jvmMemorySteps.length === 0) return 1; + if (jvmMemorySteps.includes(value)) return value; + + // Find the closest step value + return jvmMemorySteps.reduce((prev, curr) => { + return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev; + }); +} + +export const unitTypeMessageTemplate = { + local: { + createTitle: "Connect to a Local Computing Unit", + terminateTitle: "Disconnect from Local Computing Unit", + terminateWarning: "", // no red warning + createSuccess: "Successfully connected to the local computing unit", + createFailure: "Failed to connect to the local computing unit", + terminateSuccess: "Disconnected from the local computing unit", + terminateFailure: "Failed to disconnect from the local computing unit", + terminateTooltip: "Disconnect from this computing unit", + }, + kubernetes: { + createTitle: "Create Computing Unit", + terminateTitle: "Terminate Computing Unit", + terminateWarning: + "

    Warning: All execution results in this computing unit will be lost.

    ", + createSuccess: "Successfully created the Kubernetes computing unit", + createFailure: "Failed to create the Kubernetes computing unit", + terminateSuccess: "Terminated Kubernetes computing unit", + terminateFailure: "Failed to terminate Kubernetes computing unit", + terminateTooltip: "Terminate this computing unit", + }, +} as const; diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html new file mode 100644 index 00000000000..b6da2699bc1 --- /dev/null +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html @@ -0,0 +1,202 @@ + + + +
    +
    + +
    + +
    + +
    + #{{ unit.cuid }} +
    + +
    +
    + +
    +
    + +
    +
    + {{ unit.name }} + +
    + + +
    + +
    + +
    + +
    +
    + CPU +
    + +
    +
    + +
    + Memory +
    + +
    +
    +
    + +
    + Created:
    + {{ formatTime(unit.creationTime) }} +
    + +
    + CPU Limit:
    + {{ unit.resource.cpuLimit }} +
    + +
    + Memory Limit:
    + {{ unit.resource.memoryLimit }} +
    +
    +
    + + +
    +
    +

    CPU

    +

    + {{getCpuValue() | number:'1.4-4'}} + / {{getCpuLimit()}} {{getCpuLimitUnit()}} + ({{getCpuPercentage() | number:'1.1-1'}}%) +

    +
    +
    +

    RAM

    +

    + {{getMemoryValue() | number:'1.4-4'}} + / {{getMemoryLimit()}} {{getMemoryLimitUnit()}} + ({{getMemoryPercentage() | number:'1.1-1'}}%) +

    +
    +
    +

    GPU

    +

    {{getGpuLimit()}} GPU(s)

    +
    +
    +

    JVM Memory Size

    +

    {{getJvmMemorySize()}}

    +
    +
    +

    Shared Memory Size

    +

    {{getSharedMemorySize()}}

    +
    +
    +
    diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.scss b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.scss new file mode 100644 index 00000000000..1bc6ee14d28 --- /dev/null +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.scss @@ -0,0 +1,230 @@ +/** + * 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 "../../../section-style"; +@import "../../../dashboard.component.scss"; + +.computing-unit-list-item-card { + padding: 3px; + width: 100%; + background-color: white; + position: relative; + min-height: 65px; + height: auto; + + &:hover { + background-color: #f0f0f0; + + .edit-button { + display: flex; + } + + &.resource-info { + opacity: 0.3; + } + } +} + +.computing-unit-list-item-card:hover .button-group { + display: flex; + background-color: transparent; +} + +.edit-button { + display: none; + + &:hover { + display: block; + background-color: #d9d9d9; + } + + button { + height: 25px; + } +} + +.type-icon { + font-size: 30px; +} + +.unit-id { + padding: 6px; +} + +.resource-name-group { + min-width: 0; +} + +.resource-name { + font-size: 17px; + font-weight: 600; + cursor: pointer; + text-decoration: none; +} + +.resource-name:hover { + text-decoration: underline; +} + +.resource-info { + font-size: 13px; + color: grey; +} + +.truncate-single-line { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.unit-name-edit-input { + width: 100%; + max-width: 200px; + font-size: inherit; + border: 1px solid #d9d9d9; + border-radius: 2px; + padding: 2px 6px; + background: white; +} + +.resource-metrics { + width: 250px; + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(1, 1fr); + justify-content: start; + align-items: center; + gap: 5px; +} + +.general-metric { + display: flex; + flex-direction: column; + width: 100%; + background-color: #f9fafb; + border-radius: 3px; + padding: 10px; + gap: 3px; +} + +.metrics-container { + margin-right: 20px; +} + +.metric-unit { + color: #888; + font-size: 0.9em; + margin-left: 4px; +} + +.metric-percentage { + color: #555; + font-size: 0.9em; + margin-left: 6px; + font-weight: 500; +} + +.metric-name { + font-size: 10px; + margin: 0; +} + +.metric-value { + margin: 0; +} + +.metric-item { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 6px; + height: 32px; + width: 120px; + min-width: 120px; + padding: 0; + border: none; + flex-shrink: 0; /* Prevent shrinking */ +} + +.metric-label { + font-size: 10px; + width: 45px; + flex-shrink: 0; + line-height: 1; + text-align: center; +} + +.metric-bar-wrapper { + flex-grow: 1; + width: 90px; + min-width: 60px; + display: flex; + align-items: center; + padding: 0; + height: 8px; +} + +#cpu-progress-bar, +#memory-progress-bar { + width: 100%; + margin: 0 !important; + padding: 0 !important; + vertical-align: middle; +} + +.button-group { + display: none; + position: absolute; + height: 70px; + min-width: 150px; + right: 0; + bottom: 0; + justify-content: right; + align-items: center; + transition: none; + z-index: 10; + + button { + margin-right: 32px; + transition: none; + background-color: #e0e0e0; + border: 1px solid #d0d0d0; + border-radius: 8px; + } + + button:hover { + background-color: #c7c7c7; + } +} + +:host ::ng-deep .ant-progress { + width: 100%; +} + +:host ::ng-deep .ant-progress-inner { + width: 100% !important; + background-color: #cacaca !important; +} + +:host ::ng-deep .ant-badge-status-dot { + position: relative; + top: -1px; + vertical-align: middle; +} diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts new file mode 100644 index 00000000000..68fe9dc7fd5 --- /dev/null +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts @@ -0,0 +1,325 @@ +/** + * 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(); + + @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); + } + + confirmUpdateUnitName(cuid: number, newName: string): void { + const trimmedName = newName.trim(); + + if (!trimmedName) { + this.notificationService.error("Computing unit name cannot be empty"); + this.editingNameOfUnit = null; + return; + } + + if (trimmedName.length > 128) { + this.notificationService.error("Computing unit name cannot exceed 128 characters"); + this.editingNameOfUnit = null; + return; + } + + this.computingUnitService + .renameComputingUnit(cuid, trimmedName) + .pipe(untilDestroyed(this)) + .subscribe({ + next: () => { + if (this.entry.computingUnit.cuid === cuid) { + this.entry.computingUnit.name = trimmedName; + } + // Refresh the computing units list + this.computingUnitStatusService.refreshComputingUnitList(); + }, + error: (err: unknown) => { + this.notificationService.error(`Failed to rename computing unit: ${extractErrorMessage(err)}`); + }, + }) + .add(() => { + this.editingNameOfUnit = null; + this.editingUnitName = ""; + }); + } + + cancelEditingUnitName(): void { + this.editingNameOfUnit = null; + this.editingUnitName = ""; + } + + openComputingUnitMetadataModal(entry: DashboardWorkflowComputingUnit) { + this.modalService.create({ + nzTitle: "Computing Unit Information", + nzContent: buildComputingUnitMetadataTable(entry), + nzFooter: null, + nzMaskClosable: true, + nzWidth: "600px", + }); + } + + getBadgeColor(status: string): string { + switch (status) { + case "Running": + return "green"; + case "Pending": + return "gold"; + default: + return "red"; + } + } + + getUnitStatusTooltip(entry: DashboardWorkflowComputingUnit): string { + switch (entry.status) { + case "Running": + return "Ready to use"; + case "Pending": + return "Computing unit is starting up"; + default: + return entry.status; + } + } + + getCpuPercentage(): number { + return cpuPercentage( + this.getCurrentComputingUnitCpuUsage(), + this.getCurrentComputingUnitCpuLimit() + ); + } + + getMemoryPercentage(): number { + return memoryPercentage( + this.getCurrentComputingUnitMemoryUsage(), + this.getCurrentComputingUnitMemoryLimit() + ); + } + + getCpuStatus(): "success" | "exception" | "active" | "normal" { + const percentage = this.getCpuPercentage(); + if (percentage > 90) return "exception"; + if (percentage > 50) return "normal"; + return "success"; + } + + getMemoryStatus(): "success" | "exception" | "active" | "normal" { + const percentage = this.getMemoryPercentage(); + if (percentage > 90) return "exception"; + if (percentage > 50) return "normal"; + return "success"; + } + + getCurrentComputingUnitCpuUsage(): string { + return this.entry?.metrics?.cpuUsage ?? "N/A"; + } + + getCurrentComputingUnitMemoryUsage(): string { + return this.entry?.metrics?.memoryUsage ?? "N/A"; + } + + getCurrentComputingUnitCpuLimit(): string { + return this.unit?.resource?.cpuLimit ?? "N/A"; + } + + getCurrentComputingUnitMemoryLimit(): string { + return this.unit?.resource?.memoryLimit ?? "N/A"; + } + + getCurrentComputingUnitGpuLimit(): string { + return this.unit?.resource?.gpuLimit ?? "N/A"; + } + + getCurrentComputingUnitJvmMemorySize(): string { + return this.unit?.resource?.jvmMemorySize ?? "N/A"; + } + + getCurrentSharedMemorySize(): string { + return this.unit?.resource?.shmSize ?? "N/A"; + } + + getCpuLimit(): number { + return parseResourceNumber(this.getCurrentComputingUnitCpuLimit()); + } + + getGpuLimit(): string { + return this.getCurrentComputingUnitGpuLimit(); + } + + getJvmMemorySize(): string { + return this.getCurrentComputingUnitJvmMemorySize(); + } + + getSharedMemorySize(): string { + return this.getCurrentSharedMemorySize(); + } + + getCpuLimitUnit(): string { + const unit = parseResourceUnit(this.getCurrentComputingUnitCpuLimit()); + if (unit === "") { + return "CPU"; + } + return unit; + } + + getMemoryLimit(): number { + return parseResourceNumber(this.getCurrentComputingUnitMemoryLimit()); + } + + getMemoryLimitUnit(): string { + return parseResourceUnit(this.getCurrentComputingUnitMemoryLimit()); + } + + getCpuValue(): number { + const usage = this.getCurrentComputingUnitCpuUsage(); + const limit = this.getCurrentComputingUnitCpuLimit(); + if (usage === "N/A" || limit === "N/A") return 0; + const displayUnit = this.getCpuLimitUnit() === "CPU" ? "" : this.getCpuLimitUnit(); + const usageValue = cpuResourceConversion(usage, displayUnit); + return parseFloat(usageValue); + } + + getMemoryValue(): number { + const usage = this.getCurrentComputingUnitMemoryUsage(); + const limit = this.getCurrentComputingUnitMemoryLimit(); + if (usage === "N/A" || limit === "N/A") return 0; + const displayUnit = this.getMemoryLimitUnit(); + const usageValue = memoryResourceConversion(usage, displayUnit); + return parseFloat(usageValue); + } + + showGpuSelection(): boolean { + return this.gpuOptions.length > 1 || (this.gpuOptions.length === 1 && this.gpuOptions[0] !== "0"); + } + + formatTime(timestamp: number | undefined): string { + if (timestamp === undefined) { + return "Unknown"; // Return "Unknown" if the timestamp is undefined + } + + const currentTime = new Date().getTime(); + const timeDifference = currentTime - timestamp; + + const minutesAgo = Math.floor(timeDifference / (1000 * 60)); + const hoursAgo = Math.floor(timeDifference / (1000 * 60 * 60)); + const daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + const weeksAgo = Math.floor(daysAgo / 7); + + if (minutesAgo < 60) { + return `${minutesAgo} minutes ago`; + } else if (hoursAgo < 24) { + return `${hoursAgo} hours ago`; + } else if (daysAgo < 7) { + return `${daysAgo} days ago`; + } else if (weeksAgo < 4) { + return `${weeksAgo} weeks ago`; + } else { + return new Date(timestamp).toLocaleDateString(); + } + } +} diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html index 53c7cb3deda..a6a200e7bac 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html @@ -20,10 +20,214 @@

    Computing Units

    +
    + +
    + + + + + + +
    + + + + Create Computing Unit + +
    + +
    + Computing Unit Type + + + + +
    + + + +
    + Computing Unit Name + +
    +
    + Select RAM Size + + + + +
    + +
    + Select #CPU Core(s) + + + + +
    + +
    + Select #GPU(s) + + + + +
    + +
    + + Adjust the Shared Memory Size + + + +
    + + + + + +
    +
    + Shared memory cannot be greater than total memory. +
    +
    + +
    + JVM Memory Size: {{selectedJvmMemorySize}} + + +
    + + +
    +
    +
    + + + +
    + Computing Unit Name + +
    +
    + Computing Unit URI + +
    +
    +
    +
    + + + + +
    diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.scss b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.scss index 181db6355aa..772bdf21aa3 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.scss +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.scss @@ -27,3 +27,73 @@ min-height: 100%; height: 100%; } + +.memory-selection, +.cpu-selection, +.gpu-selection { + width: 100%; +} + +.jvm-memory-slider { + width: 100%; + margin: 10px 0; +} + +.memory-warning { + margin-top: 10px; + font-size: 0.9em; +} + +.create-compute-unit-container { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + justify-content: start; + align-items: center; +} + +.select-unit { + display: flex; + flex-direction: column; + gap: 10px; + justify-content: center; + align-items: start; +} + +.select-unit.name-field { + grid-column: span 2; +} + +.unit-name-input { + width: 100%; +} + +.shared-memory-group { + width: 100%; + + .shm-input-row { + display: flex; + gap: 8px; + align-items: center; + width: 100%; + } + + .shm-size-input { + width: 60px; + min-width: 50px; + flex-shrink: 0; + } + + .shm-unit-select { + width: 80px; + min-width: 70px; + flex-shrink: 0; + } + + .shm-warning { + margin-top: 4px; + font-size: 12px; + color: #faad14; + white-space: nowrap; + } +} diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts index 971a051934a..53d43e84a6d 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts @@ -17,13 +17,368 @@ * 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 + ? ` +

    Are you sure you want to terminate ${unitName}?

    + ${templates.terminateWarning} + ` + : ` +

    Are you sure you want to disconnect from ${unitName}?

    + `, + 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; + } + + isShmTooLarge(): boolean { + const total = parseResourceNumber(this.selectedMemory); + const unit = parseResourceUnit(this.selectedMemory); + const memoryInMi = unit === "Gi" ? total * 1024 : total; + const shmInMi = this.shmSizeUnit === "Gi" ? this.shmSizeValue * 1024 : this.shmSizeValue; + + return shmInMi > memoryInMi; + } + + updateJvmMemorySlider(): void { + this.resetJvmMemorySlider(); + } + + onJvmMemorySliderChange(value: number): void { + // Ensure the value is one of the valid steps + const validStep = findNearestValidStep(value, this.jvmMemorySteps); + this.jvmMemorySliderValue = validStep; + this.selectedJvmMemorySize = `${validStep}G`; + } + + isMaxJvmMemorySelected(): boolean { + // Only show warning for larger memory sizes (>=4GB) where the slider is shown + // AND when the maximum value is selected + return this.showJvmMemorySlider && this.jvmMemorySliderValue === this.jvmMemoryMax && this.jvmMemoryMax >= 4; + } + + // Completely reset the JVM memory slider based on the selected CU memory + resetJvmMemorySlider(): void { + // Parse memory limit to determine max JVM memory + const memoryValue = parseResourceNumber(this.selectedMemory); + const memoryUnit = parseResourceUnit(this.selectedMemory); + + // Set max JVM memory to the total memory selected (in GB) + let cuMemoryInGb = 1; // Default to 1GB + if (memoryUnit === "Gi") { + cuMemoryInGb = memoryValue; + } else if (memoryUnit === "Mi") { + cuMemoryInGb = Math.max(1, Math.floor(memoryValue / 1024)); + } + + this.jvmMemoryMax = cuMemoryInGb; + + // Special cases for smaller memory sizes (1-3GB) + if (cuMemoryInGb <= 3) { + // Don't show slider for small memory sizes + this.showJvmMemorySlider = false; + + // Set JVM memory size to 1GB when CU memory is 1GB, otherwise set to 2GB + if (cuMemoryInGb === 1) { + this.jvmMemorySliderValue = 1; + this.selectedJvmMemorySize = "1G"; + } else { + // For 2-3GB instances, use 2GB for JVM + this.jvmMemorySliderValue = 2; + this.selectedJvmMemorySize = "2G"; + } + + // Still calculate steps for completeness + this.jvmMemorySteps = []; + let value = 1; + while (value <= this.jvmMemoryMax) { + this.jvmMemorySteps.push(value); + value = value * 2; + } + + // Update marks + this.jvmMemoryMarks = {}; + this.jvmMemorySteps.forEach(step => { + this.jvmMemoryMarks[step] = `${step}G`; + }); + + return; + } + + // For larger memory sizes (4GB+), show the slider + this.showJvmMemorySlider = true; + + // Calculate binary steps (2,4,8,...) starting from 2GB + this.jvmMemorySteps = []; + let value = 2; // Start from 2GB for larger instances + while (value <= this.jvmMemoryMax) { + this.jvmMemorySteps.push(value); + value = value * 2; + } + + // Update slider marks + this.jvmMemoryMarks = {}; + this.jvmMemorySteps.forEach(step => { + this.jvmMemoryMarks[step] = `${step}G`; + }); + + // Always default to 2GB for larger memory sizes + this.jvmMemorySliderValue = 2; + this.selectedJvmMemorySize = "2G"; + } + + onMemorySelectionChange(): void { + // Store current JVM memory value for potential reuse + const previousJvmMemory = this.jvmMemorySliderValue; + + // Reset slider configuration based on the new memory selection + this.resetJvmMemorySlider(); + + // For CU memory > 3GB, preserve previous value if valid and >= 2GB + // Get the current memory in GB + const memoryValue = parseResourceNumber(this.selectedMemory); + const memoryUnit = parseResourceUnit(this.selectedMemory); + let cuMemoryInGb = memoryUnit === "Gi" ? memoryValue : memoryUnit === "Mi" ? Math.floor(memoryValue / 1024) : 1; + + // Only try to preserve previous value for larger memory sizes where slider is shown + if ( + cuMemoryInGb > 3 && + previousJvmMemory >= 2 && + previousJvmMemory <= this.jvmMemoryMax && + this.jvmMemorySteps.includes(previousJvmMemory) + ) { + this.jvmMemorySliderValue = previousJvmMemory; + this.selectedJvmMemorySize = `${previousJvmMemory}G`; + } + } + + getCreateModalTitle(): string { + if (!this.selectedComputingUnitType) return "Create Computing Unit"; + return unitTypeMessageTemplate[this.selectedComputingUnitType].createTitle; + } } From ac4dddce0e7035c6e11260a3a85d6a25c7b3fb09 Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Sun, 29 Mar 2026 14:52:11 -0700 Subject: [PATCH 04/22] fix(gui): change "Computing Units" to "Compute" on dashboard --- frontend/src/app/dashboard/component/dashboard.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/dashboard/component/dashboard.component.html b/frontend/src/app/dashboard/component/dashboard.component.html index 1b1bc910b43..4813f0b4e2c 100644 --- a/frontend/src/app/dashboard/component/dashboard.component.html +++ b/frontend/src/app/dashboard/component/dashboard.component.html @@ -106,7 +106,7 @@ - Computing Units + Compute
  • Date: Thu, 2 Apr 2026 21:09:11 -0700 Subject: [PATCH 05/22] test(gui): add test for dashboard computing unit tab --- .../user-computing-unit.component.spec.ts | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts index a171078e9a2..7c449357310 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts +++ b/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; + let mockComputingUnitService: jasmine.SpyObj - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + mockComputingUnitService = jasmine.createSpyObj([ + "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(); - }); - beforeEach(() => { fixture = TestBed.createComponent(UserComputingUnitComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it("should create", () => { From 749222d2cb188ff1857ccad5ee208796c6ca8f4b Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Thu, 2 Apr 2026 23:07:33 -0700 Subject: [PATCH 06/22] style: fix linting errors in computing unit components --- .../user-computing-unit-list-item.component.html | 3 ++- .../user-computing-unit-list-item.component.ts | 12 +++--------- .../user-computing-unit.component.spec.ts | 4 ++-- .../user-computing-unit.component.ts | 9 ++++++--- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html index b6da2699bc1..b0656a43487 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html @@ -82,7 +82,8 @@ class="unit-name-edit-input" maxlength="128" (click)="$event.stopPropagation()" - autofocus/> + autofocus + />
    diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts index 68fe9dc7fd5..fef4cb9df27 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts @@ -35,7 +35,7 @@ import { cpuResourceConversion, memoryResourceConversion, cpuPercentage, - memoryPercentage + memoryPercentage, } from "../../../../../common/util/computing-unit.util"; @UntilDestroy() @@ -188,17 +188,11 @@ export class UserComputingUnitListItemComponent implements OnInit { } getCpuPercentage(): number { - return cpuPercentage( - this.getCurrentComputingUnitCpuUsage(), - this.getCurrentComputingUnitCpuLimit() - ); + return cpuPercentage(this.getCurrentComputingUnitCpuUsage(), this.getCurrentComputingUnitCpuLimit()); } getMemoryPercentage(): number { - return memoryPercentage( - this.getCurrentComputingUnitMemoryUsage(), - this.getCurrentComputingUnitMemoryLimit() - ); + return memoryPercentage(this.getCurrentComputingUnitMemoryUsage(), this.getCurrentComputingUnitMemoryLimit()); } getCpuStatus(): "success" | "exception" | "active" | "normal" { diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts index 7c449357310..895ce03e562 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts @@ -32,7 +32,7 @@ import { MockComputingUnitStatusService } from "../../../../workspace/service/co describe("UserComputingUnitComponent", () => { let component: UserComputingUnitComponent; let fixture: ComponentFixture; - let mockComputingUnitService: jasmine.SpyObj + let mockComputingUnitService: jasmine.SpyObj; beforeEach(() => { mockComputingUnitService = jasmine.createSpyObj([ @@ -50,7 +50,7 @@ describe("UserComputingUnitComponent", () => { { provide: UserService, useClass: StubUserService }, { provide: WorkflowComputingUnitManagingService, useValue: mockComputingUnitService }, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService }, - ...commonTestProviders + ...commonTestProviders, ], imports: [NzCardModule], }).compileComponents(); diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts index 53d43e84a6d..d599ba911b4 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts @@ -20,7 +20,10 @@ 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 { + 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"; @@ -31,7 +34,7 @@ import { parseResourceUnit, parseResourceNumber, findNearestValidStep, - unitTypeMessageTemplate + unitTypeMessageTemplate, } from "../../../../common/util/computing-unit.util"; @UntilDestroy() @@ -48,7 +51,7 @@ export class UserComputingUnitComponent implements OnInit { allComputingUnits: DashboardWorkflowComputingUnit[] = []; - // variables for creating a computing unit + // variables for creating a computing unit addComputeUnitModalVisible = false; newComputingUnitName: string = ""; selectedMemory: string = ""; From 6c6d957286a6e5cbce918fb81478314cb3572bde Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Sat, 4 Apr 2026 18:08:32 -0700 Subject: [PATCH 07/22] fix(ui): remove xss vector by turning metadata table into a component --- .../app/common/util/computing-unit.util.ts | 62 +++++++++++++++---- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/common/util/computing-unit.util.ts b/frontend/src/app/common/util/computing-unit.util.ts index 73eba77ca65..28c55d67a82 100644 --- a/frontend/src/app/common/util/computing-unit.util.ts +++ b/frontend/src/app/common/util/computing-unit.util.ts @@ -17,25 +17,61 @@ * under the License. */ +import { Component, inject } from "@angular/core"; +import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; import { DashboardWorkflowComputingUnit } from "../../workspace/types/workflow-computing-unit"; -export function buildComputingUnitMetadataTable(unit: DashboardWorkflowComputingUnit): string { - return ` +@Component({ + template: ` - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Name${unit.computingUnit.name}
    Status${unit.status}
    Type${unit.computingUnit.type}
    CPU Limit${unit.computingUnit.resource.cpuLimit}
    Memory Limit${unit.computingUnit.resource.memoryLimit}
    GPU Limit${unit.computingUnit.resource.gpuLimit || "None"}
    JVM Memory${unit.computingUnit.resource.jvmMemorySize}
    Shared Memory${unit.computingUnit.resource.shmSize}
    Created${new Date(unit.computingUnit.creationTime).toLocaleString()}
    Access${unit.isOwner ? "Owner" : unit.accessPrivilege}
    Name{{ unit.computingUnit.name }}
    Status{{ unit.status }}
    Type{{ unit.computingUnit.type }}
    CPU Limit{{ unit.computingUnit.resource.cpuLimit }}
    Memory Limit{{ unit.computingUnit.resource.memoryLimit }}
    GPU Limit{{ unit.computingUnit.resource.gpuLimit || "None" }}
    JVM Memory{{ unit.computingUnit.resource.jvmMemorySize }}
    Shared Memory{{ unit.computingUnit.resource.shmSize }}
    Created{{ createdAt }}
    Access{{ unit.isOwner ? "Owner" : unit.accessPrivilege }}
    - `; + `, +}) +export class ComputingUnitMetadataComponent { + readonly unit: DashboardWorkflowComputingUnit = inject(NZ_MODAL_DATA); + readonly createdAt = new Date(this.unit.computingUnit.creationTime).toLocaleString(); } export function parseResourceUnit(resource: string): string { From de266c4c0447f5abc75bf5933439fd27704de4c5 Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Sat, 4 Apr 2026 18:09:19 -0700 Subject: [PATCH 08/22] fix(ui): use optimized cdkVirtualFor instead of ngFor --- .../user-computing-unit/user-computing-unit.component.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html index a6a200e7bac..4a17246a182 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html @@ -42,10 +42,9 @@

    Computing Units

    itemSize="70" class="virtual-scroll-container"> - + From dd6d2c426f45e4dbf60714053d025d79d99fa6ec Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Sat, 4 Apr 2026 18:10:35 -0700 Subject: [PATCH 09/22] test(ui): add async and await to handle asynchronous compileComponents call --- .../user-computing-unit/user-computing-unit.component.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts index 895ce03e562..53b6ecca1c3 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts @@ -34,7 +34,7 @@ describe("UserComputingUnitComponent", () => { let fixture: ComponentFixture; let mockComputingUnitService: jasmine.SpyObj; - beforeEach(() => { + beforeEach(async () => { mockComputingUnitService = jasmine.createSpyObj([ "getComputingUnitTypes", "getComputingUnitLimitOptions", @@ -42,7 +42,7 @@ describe("UserComputingUnitComponent", () => { "createLocalComputingUnit", ]); - TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ declarations: [UserComputingUnitComponent], providers: [ NzModalService, From 435386247b669371c44849a76ac9bb59fadb173c Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Sat, 4 Apr 2026 18:11:50 -0700 Subject: [PATCH 10/22] fix(ui): change buildComputingUnitMetadataTable to ComputingUnitMetadataComponent --- ...user-computing-unit-list-item.component.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts index fef4cb9df27..d4b0ab9b2bc 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts @@ -17,7 +17,16 @@ * under the License. */ -import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, + ElementRef, +} 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"; @@ -29,7 +38,7 @@ import { } from "../../../../../workspace/types/workflow-computing-unit"; import { WorkflowComputingUnitManagingService } from "../../../../../workspace/service/workflow-computing-unit/workflow-computing-unit-managing.service"; import { - buildComputingUnitMetadataTable, + ComputingUnitMetadataComponent, parseResourceUnit, parseResourceNumber, cpuResourceConversion, @@ -49,7 +58,6 @@ export class UserComputingUnitListItemComponent implements OnInit { editingNameOfUnit: number | null = null; editingUnitName: string = ""; gpuOptions: string[] = []; - @Input() editable = false; @Output() deleted = new EventEmitter(); @Input() @@ -94,6 +102,8 @@ export class UserComputingUnitListItemComponent implements OnInit { }); } + @ViewChild("unitNameInput") unitNameInputRef?: ElementRef; + startEditingUnitName(entry: DashboardWorkflowComputingUnit): void { if (!entry.isOwner) { this.notificationService.error("Only owners can rename computing units"); @@ -108,8 +118,8 @@ export class UserComputingUnitListItemComponent implements OnInit { setTimeout(() => { const input = document.querySelector(".unit-name-edit-input") as HTMLInputElement; if (input) { - input.focus(); - input.select(); + this.unitNameInputRef?.nativeElement.focus(); + this.unitNameInputRef?.nativeElement.select(); } }, 0); } @@ -158,7 +168,8 @@ export class UserComputingUnitListItemComponent implements OnInit { openComputingUnitMetadataModal(entry: DashboardWorkflowComputingUnit) { this.modalService.create({ nzTitle: "Computing Unit Information", - nzContent: buildComputingUnitMetadataTable(entry), + nzContent: ComputingUnitMetadataComponent, + nzData: entry, nzFooter: null, nzMaskClosable: true, nzWidth: "600px", From e2f78c5088e9b1b4439601a3ea60234679ad6ceb Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Sat, 4 Apr 2026 18:13:02 -0700 Subject: [PATCH 11/22] refactor(ui): remove unused editable field --- .../user/user-computing-unit/user-computing-unit.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts index d599ba911b4..e777641e8b4 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts @@ -47,7 +47,6 @@ export class UserComputingUnitComponent implements OnInit { public entries: DashboardEntry[] = []; public isLogin = this.userService.isLogin(); public currentUid = this.userService.getCurrentUser()?.uid; - @Input() editable = false; allComputingUnits: DashboardWorkflowComputingUnit[] = []; From b7fdd1d357aa84fe53a8000820e5bde049c1eb2b Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Sat, 4 Apr 2026 18:13:32 -0700 Subject: [PATCH 12/22] feat(ui): add default URI to local computing units --- .../user/user-computing-unit/user-computing-unit.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts index e777641e8b4..5252e0f0325 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts @@ -93,6 +93,7 @@ export class UserComputingUnitComponent implements OnInit { } ngOnInit() { + this.localComputingUnitUri = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ""}/wsapi`; this.newComputingUnitName = "My Computing Unit"; this.computingUnitService .getComputingUnitTypes() From c00782a9ac05d4635c7a426e6f6bca637e83e3ba Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Sat, 4 Apr 2026 18:14:16 -0700 Subject: [PATCH 13/22] feat(ui): remove extra delete computing unit confirmation --- .../user-computing-unit-list-item.component.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html index b0656a43487..0921d10bfd8 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html @@ -91,9 +91,7 @@ nz-button nzType="text" title="Delete" - (nzOnConfirm)="deleted.emit()" - nz-popconfirm - nzPopconfirmTitle="Confirm to delete this item."> + (click)="deleted.emit()"> From ccd6d0ba7255e6fa9cce49b712f6283ab2eaa4ed Mon Sep 17 00:00:00 2001 From: Grace Chia Date: Sat, 4 Apr 2026 18:15:01 -0700 Subject: [PATCH 14/22] fix(ui): replace duplicate id's with classes --- .../user-computing-unit-list-item.component.html | 5 ++--- .../user-computing-unit-list-item.component.scss | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html index 0921d10bfd8..9e3c40f152e 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html @@ -104,13 +104,12 @@ [nzPopoverContent]="metricsTemplate" nzPopoverTrigger="hover" nzPopoverPlacement="bottom" - id="metrics-container-id" class="metrics-container">
    CPU
    Memory
    Date: Sun, 5 Apr 2026 22:22:54 -0700 Subject: [PATCH 15/22] feat(ui): add sharing to dashboard computing units --- ...er-computing-unit-list-item.component.html | 11 ++++++++ ...user-computing-unit-list-item.component.ts | 10 ++++++- .../computing-unit-actions.service.ts | 26 +++++++++++++++++++ .../computing-unit-selection.component.ts | 18 +++---------- 4 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 frontend/src/app/dashboard/service/user/computing-unit-actions/computing-unit-actions.service.ts diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html index 9e3c40f152e..68933e4325c 100644 --- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html +++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html @@ -87,6 +87,17 @@
    +