diff --git a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.html b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.html index 6167d367591..7fe7dca6777 100644 --- a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.html +++ b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.html @@ -472,28 +472,40 @@
-
-
Package
-
Version
+
+ + Fetching system packages…
-
-
- + +
+
Package
+
Version
+
- +
+
+ + + +
-
+
diff --git a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.spec.ts b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.spec.ts index 5702dfa9ba2..b5827ecb150 100644 --- a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.spec.ts +++ b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.spec.ts @@ -29,6 +29,13 @@ import { NzModalModule, NzModalService } from "ng-zorro-antd/modal"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; +import { Subject, of, throwError } from "rxjs"; +import { + PackageResponse, + PvePackageResponse, + WorkflowPveService, +} from "../../service/virtual-environment/virtual-environment.service"; +import { DashboardWorkflowComputingUnit } from "../../../common/type/workflow-computing-unit"; describe("PowerButtonComponent", () => { let component: ComputingUnitSelectionComponent; @@ -82,4 +89,69 @@ describe("PowerButtonComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); + + describe("getPVEs() systemPackagesLoading flag", () => { + const selectedUnit = { + computingUnit: { cuid: 123 }, + } as unknown as DashboardWorkflowComputingUnit; + + let pveService: WorkflowPveService; + + beforeEach(() => { + pveService = TestBed.inject(WorkflowPveService); + component.selectedComputingUnit = selectedUnit; + component.systemPackagesLoading = false; + component.pves = []; + component.systemPackages = []; + }); + + it("sets systemPackagesLoading=true immediately and keeps it true while /pve/system is in flight", () => { + const pvesResp: PvePackageResponse[] = [{ pveName: "env-a", userPackages: ["numpy==1.26.0"] }]; + const systemSubject = new Subject(); + vi.spyOn(pveService, "fetchPVEs").mockReturnValue(of(pvesResp)); + vi.spyOn(pveService, "getSystemPackages").mockReturnValue(systemSubject.asObservable()); + + component.getPVEs(); + + expect(component.systemPackagesLoading).toBe(true); + expect(component.pves.length).toBe(1); + expect(component.pves[0].name).toBe("env-a"); + + systemSubject.next({ system: ["pandas==2.0.0"] }); + systemSubject.complete(); + + expect(component.systemPackagesLoading).toBe(false); + expect(component.systemPackages).toEqual([{ name: "pandas", version: "2.0.0" }]); + }); + + it("clears systemPackagesLoading when /pve/system errors", () => { + vi.spyOn(pveService, "fetchPVEs").mockReturnValue(of([] as PvePackageResponse[])); + vi.spyOn(pveService, "getSystemPackages").mockReturnValue(throwError(() => new Error("system fetch failed"))); + vi.spyOn(console, "error").mockImplementation(() => {}); + + component.getPVEs(); + + expect(component.systemPackagesLoading).toBe(false); + expect(component.systemPackages).toEqual([]); + }); + + it("clears systemPackagesLoading and resets state when /pve/pves errors", () => { + const fetchPvesSpy = vi + .spyOn(pveService, "fetchPVEs") + .mockReturnValue(throwError(() => new Error("pves fetch failed"))); + const getSystemSpy = vi.spyOn(pveService, "getSystemPackages"); + vi.spyOn(console, "error").mockImplementation(() => {}); + + component.pves = [{ name: "stale" }] as any; + component.systemPackages = [{ name: "stale", version: "0.0.0" }]; + + component.getPVEs(); + + expect(fetchPvesSpy).toHaveBeenCalledWith(123); + expect(getSystemSpy).not.toHaveBeenCalled(); + expect(component.systemPackagesLoading).toBe(false); + expect(component.pves).toEqual([]); + expect(component.systemPackages).toEqual([]); + }); + }); }); diff --git a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.ts b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.ts index b9a41c5b5f6..5c7123a91fb 100644 --- a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.ts +++ b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.ts @@ -139,6 +139,10 @@ export class ComputingUnitSelectionComponent implements OnInit { // variables for creating a virtual environment pves: PveDraft[] = []; systemPackages: { name: string; version: string }[] = []; + // True while a /pve/system response is in flight. The server resolves + // the full pinned set with a `pip freeze` against a throwaway venv, + // which can take 30–60s on the first request after a server restart. + systemPackagesLoading = false; pveModalVisible = false; // current workflow's Id, will change with wid in the workflowActionService.metadata @@ -782,6 +786,7 @@ export class ComputingUnitSelectionComponent implements OnInit { getPVEs(): void { const cuId = this.selectedComputingUnit!.computingUnit.cuid; + this.systemPackagesLoading = true; this.workflowPveService .fetchPVEs(cuId) @@ -812,10 +817,12 @@ export class ComputingUnitSelectionComponent implements OnInit { version: (version ?? "").trim(), }; }); + this.systemPackagesLoading = false; }, error: (err: unknown) => { console.error("Failed to fetch system packages:", err); this.systemPackages = []; + this.systemPackagesLoading = false; }, }); }, @@ -823,6 +830,7 @@ export class ComputingUnitSelectionComponent implements OnInit { console.error("Failed to fetch PVEs:", err); this.pves = []; this.systemPackages = []; + this.systemPackagesLoading = false; }, }); } diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 5f4a00952a9..d761cf56a56 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -161,6 +161,15 @@ hr { gap: 6px; } + .system-loading { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 4px; + color: rgba(0, 0, 0, 0.55); + font-style: italic; + } + .package-header-row, .package-inputs { display: grid;