diff --git a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.scss b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.scss
index 1213a861a5e..2eb5265aa3f 100644
--- a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.scss
+++ b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.scss
@@ -29,6 +29,10 @@
min-width: 220px;
max-width: 280px;
+ &.warehouse-visible {
+ max-width: none;
+ }
+
&.metrics-visible {
min-width: 290px;
max-width: none;
@@ -139,6 +143,87 @@
justify-content: flex-start;
}
+.warehouses-dropdown {
+ width: 350px;
+ max-height: 50vh;
+ overflow-y: auto;
+}
+
+.warehouse-option {
+ display: block;
+ width: 100%;
+ padding: 0 !important;
+}
+
+.warehouse-row,
+.warehouse-name,
+.create-warehouse {
+ display: flex;
+ align-items: center;
+}
+
+.warehouse-row {
+ justify-content: space-between;
+ width: 100%;
+ gap: 10px;
+ padding: 5px 12px;
+ box-sizing: border-box;
+}
+
+.warehouse-name {
+ flex-grow: 1;
+ gap: 8px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.warehouse-name span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.warehouse-delete-icon {
+ margin-left: auto;
+ flex-shrink: 0;
+ opacity: 0.85;
+ color: #ff4d4f;
+ cursor: pointer;
+
+ &:hover {
+ opacity: 1;
+ transform: scale(1.1);
+ }
+}
+
+.create-warehouse {
+ gap: 10px;
+ justify-content: flex-start;
+}
+
+.warehouse-dropdown-button {
+ display: inline-flex;
+ align-items: center;
+ min-width: 220px;
+ max-width: 280px;
+ margin-right: 4px;
+ padding: 0 8px;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.warehouse-name-text {
+ display: inline-block;
+ flex: 1 1 auto;
+ min-width: 0;
+ max-width: 220px;
+ margin: 0 4px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.resource-metrics {
display: grid;
}
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 ef8b4f02a64..0ee8cac44a8 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
@@ -54,6 +54,9 @@ import { WorkflowMetadata } from "../../../dashboard/type/workflow-metadata.inte
import { ExecutionState } from "../../types/execute-workflow.interface";
import { ComputingUnitActionsService } from "../../../common/service/computing-unit/computing-unit-actions/computing-unit-actions.service";
import { ComputingUnitMetadataComponent } from "../../../common/util/computing-unit.util";
+import { WarehouseService } from "../../../common/service/warehouse/warehouse.service";
+import { WarehouseActionsService } from "../../../common/service/warehouse/warehouse-actions.service";
+import { DashboardWarehouse } from "../../../common/type/warehouse";
/**
* Builds a fully-populated DashboardWorkflowComputingUnit for driving the
@@ -1091,6 +1094,258 @@ describe("PowerButtonComponent", () => {
});
});
+ describe("warehouse picker (#6933)", () => {
+ function makeWarehouse(whid: number, name: string): DashboardWarehouse {
+ return {
+ whid,
+ name,
+ lakekeeperWarehouseName: `user-1-${name}`,
+ flavor: "local",
+ createdAtMillis: 0,
+ ownerName: "Alice",
+ ownerAvatar: "",
+ };
+ }
+
+ // Mirrors bootWithMetaStream, additionally pinning the warehouse status and
+ // the latest-execution response the preselection logic consumes.
+ function bootPicker(opts: {
+ enabled: boolean;
+ warehouses: DashboardWarehouse[];
+ latest?: Partial | "error";
+ }): {
+ comp: ComputingUnitSelectionComponent;
+ pickerFixture: ComponentFixture;
+ emit: (wid: number) => void;
+ } {
+ vi.spyOn(TestBed.inject(WarehouseService), "getStatus").mockReturnValue(
+ of({ enabled: opts.enabled, warehouses: opts.warehouses })
+ );
+ const execService = TestBed.inject(WorkflowExecutionsService);
+ if (opts.latest === "error") {
+ vi.spyOn(execService, "retrieveLatestWorkflowExecution").mockReturnValue(
+ throwError(() => new Error("no execution"))
+ );
+ } else if (opts.latest !== undefined) {
+ vi.spyOn(execService, "retrieveLatestWorkflowExecution").mockReturnValue(
+ of(opts.latest as WorkflowExecutionsEntry)
+ );
+ }
+ const actionService = TestBed.inject(WorkflowActionService);
+ const meta$ = new Subject();
+ vi.spyOn(actionService, "workflowMetaDataChanged").mockReturnValue(meta$.asObservable());
+ let currentMeta: WorkflowMetadata = { ...DEFAULT_WORKFLOW };
+ vi.spyOn(actionService, "getWorkflowMetadata").mockImplementation(() => currentMeta);
+ const pickerFixture = TestBed.createComponent(ComputingUnitSelectionComponent);
+ pickerFixture.detectChanges();
+ const comp = pickerFixture.componentInstance;
+ vi.spyOn(comp, "selectComputingUnit").mockImplementation(() => {});
+ const emit = (wid: number) => {
+ currentMeta = { ...DEFAULT_WORKFLOW, wid };
+ meta$.next(currentMeta);
+ };
+ return { comp, pickerFixture, emit };
+ }
+
+ it("preselects the latest execution's warehouse when it still exists", () => {
+ const { emit } = bootPicker({
+ enabled: true,
+ warehouses: [makeWarehouse(1, "first"), makeWarehouse(2, "second")],
+ latest: { cuId: 55, whId: 2 },
+ });
+
+ emit(100);
+
+ expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(2);
+ });
+
+ it("falls back to the first warehouse when the latest execution used none", () => {
+ const { emit } = bootPicker({
+ enabled: true,
+ warehouses: [makeWarehouse(1, "first"), makeWarehouse(2, "second")],
+ latest: { cuId: 55, whId: null },
+ });
+
+ emit(100);
+
+ expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(1);
+ });
+
+ it("still preselects the first warehouse when there is no execution history", () => {
+ const { emit } = bootPicker({
+ enabled: true,
+ warehouses: [makeWarehouse(1, "first")],
+ latest: "error",
+ });
+
+ emit(100);
+
+ expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(1);
+ });
+
+ it("never picks a warehouse while the feature is disabled, and hides the picker", () => {
+ // Warehouses alongside enabled=false cannot come from the real backend; the
+ // artificial combination pins that the flag alone suppresses preselection.
+ const { pickerFixture, emit } = bootPicker({
+ enabled: false,
+ warehouses: [makeWarehouse(1, "first")],
+ latest: { cuId: 55, whId: 1 },
+ });
+
+ emit(100);
+
+ expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBeUndefined();
+ expect(pickerFixture.nativeElement.querySelector(".warehouse-dropdown-button")).toBeNull();
+ });
+
+ it("clears any stale pick when the status request fails", () => {
+ // The pick outlives the component (root-scoped service), so a failure that only
+ // hides the picker would still send a previous workflow's warehouse id.
+ TestBed.inject(WarehouseService).selectWarehouse(9);
+ vi.spyOn(TestBed.inject(WarehouseService), "getStatus").mockReturnValue(
+ throwError(() => new Error("status unavailable"))
+ );
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+
+ const failedFixture = TestBed.createComponent(ComputingUnitSelectionComponent);
+ failedFixture.detectChanges();
+
+ expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBeUndefined();
+ expect(failedFixture.componentInstance.warehouseEnabled).toBe(false);
+ errorSpy.mockRestore();
+ });
+
+ it("clears any stale pick when the feature is disabled or no warehouse exists", () => {
+ TestBed.inject(WarehouseService).selectWarehouse(9);
+
+ const { emit } = bootPicker({ enabled: true, warehouses: [], latest: { cuId: 55, whId: 9 } });
+ emit(100);
+
+ expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBeUndefined();
+ });
+
+ it("renders the dropdown trigger when enabled, and a manual pick writes through to the service", () => {
+ const { comp, pickerFixture } = bootPicker({
+ enabled: true,
+ warehouses: [makeWarehouse(1, "first"), makeWarehouse(2, "second")],
+ });
+
+ pickerFixture.detectChanges();
+ expect(pickerFixture.nativeElement.querySelector(".warehouse-dropdown-button")).toBeTruthy();
+
+ comp.onWarehouseSelected(2);
+ expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(2);
+ expect(comp.trackByWhid(0, makeWarehouse(2, "second"))).toBe(2);
+ });
+
+ it("shows the trigger with the generic label when enabled with zero warehouses", () => {
+ const { comp, pickerFixture } = bootPicker({ enabled: true, warehouses: [] });
+
+ pickerFixture.detectChanges();
+
+ expect(pickerFixture.nativeElement.querySelector(".warehouse-dropdown-button")).toBeTruthy();
+ expect(comp.getWarehouseButtonText()).toBe("Warehouse");
+ expect(comp.warehouseRequiredButMissing).toBe(true);
+ });
+
+ it("reports no missing warehouse once the preselect has picked one", () => {
+ const { comp } = bootPicker({ enabled: true, warehouses: [makeWarehouse(1, "first")] });
+
+ expect(comp.warehouseRequiredButMissing).toBe(false);
+ });
+
+ it("shows the selected warehouse's name on the dropdown trigger", () => {
+ const { comp, emit } = bootPicker({
+ enabled: true,
+ warehouses: [makeWarehouse(1, "first"), makeWarehouse(2, "second")],
+ latest: { cuId: 55, whId: 2 },
+ });
+
+ emit(100);
+ expect(comp.getWarehouseButtonText()).toBe("second");
+
+ // An id that matches no warehouse falls back to the generic label.
+ TestBed.inject(WarehouseService).selectWarehouse(999);
+ expect(comp.getWarehouseButtonText()).toBe("Warehouse");
+ });
+
+ it("refreshes the warehouse list when the dropdown opens, not when it closes", () => {
+ const { comp } = bootPicker({ enabled: true, warehouses: [makeWarehouse(1, "first")] });
+ const statusSpy = vi.spyOn(TestBed.inject(WarehouseService), "getStatus");
+ statusSpy.mockClear();
+
+ comp.onWarehouseDropdownVisibilityChange(true);
+ expect(statusSpy).toHaveBeenCalledTimes(1);
+
+ comp.onWarehouseDropdownVisibilityChange(false);
+ expect(statusSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("keeps a manual pick across a dropdown-open refresh", () => {
+ const { comp } = bootPicker({
+ enabled: true,
+ warehouses: [makeWarehouse(1, "first"), makeWarehouse(2, "second")],
+ });
+ comp.onWarehouseSelected(2);
+
+ comp.onWarehouseDropdownVisibilityChange(true);
+
+ expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(2);
+ });
+
+ it("re-preselects when the picked warehouse no longer exists", () => {
+ const { comp } = bootPicker({
+ enabled: true,
+ warehouses: [makeWarehouse(1, "first"), makeWarehouse(2, "second")],
+ });
+ comp.onWarehouseSelected(2);
+ vi.spyOn(TestBed.inject(WarehouseService), "getStatus").mockReturnValue(
+ of({ enabled: true, warehouses: [makeWarehouse(1, "first")] })
+ );
+
+ comp.onWarehouseDropdownVisibilityChange(true);
+
+ expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(1);
+ });
+
+ it("opens the create modal from the menu, and selects a warehouse created there", () => {
+ const { comp } = bootPicker({ enabled: true, warehouses: [makeWarehouse(1, "first")] });
+
+ expect(comp.addWarehouseModalVisible).toBe(false);
+ comp.showAddWarehouseModalVisible();
+ expect(comp.addWarehouseModalVisible).toBe(true);
+
+ vi.spyOn(TestBed.inject(WarehouseService), "getStatus").mockReturnValue(
+ of({ enabled: true, warehouses: [makeWarehouse(1, "first"), makeWarehouse(9, "fresh")] })
+ );
+ comp.onWarehouseCreated(makeWarehouse(9, "fresh"));
+
+ expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(9);
+ });
+
+ it("hands the warehouse to the actions service, and re-preselects after the delete", () => {
+ const { comp } = bootPicker({
+ enabled: true,
+ warehouses: [makeWarehouse(1, "first"), makeWarehouse(2, "second")],
+ });
+ const actionsService = TestBed.inject(WarehouseActionsService);
+ const confirmAndDeleteSpy = vi.spyOn(actionsService, "confirmAndDelete").mockImplementation(() => {});
+ const doomed = makeWarehouse(1, "first");
+
+ comp.confirmDeleteWarehouse(doomed);
+
+ expect(confirmAndDeleteSpy).toHaveBeenCalledTimes(1);
+ expect(confirmAndDeleteSpy.mock.calls[0][0]).toEqual(doomed);
+
+ vi.spyOn(TestBed.inject(WarehouseService), "getStatus").mockReturnValue(
+ of({ enabled: true, warehouses: [makeWarehouse(2, "second")] })
+ );
+ (confirmAndDeleteSpy.mock.calls[0][1] as () => void)();
+
+ expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(2);
+ });
+ });
+
describe("selectComputingUnit guards", () => {
it("does nothing when the cuid is undefined", () => {
const selectSpy = vi.spyOn(TestBed.inject(ComputingUnitStatusService), "selectComputingUnit");
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 b633e6eca31..9540d64731a 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
@@ -27,6 +27,9 @@ import { isDefined } from "../../../common/util/predicate";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { extractErrorMessage } from "../../../common/util/error";
import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
+import { WarehouseService } from "../../../common/service/warehouse/warehouse.service";
+import { WarehouseActionsService } from "../../../common/service/warehouse/warehouse-actions.service";
+import { DashboardWarehouse } from "../../../common/type/warehouse";
import { NzModalService, NzModalComponent, NzModalContentDirective } from "ng-zorro-antd/modal";
import { WorkflowExecutionsService } from "../../../dashboard/service/user/workflow-executions/workflow-executions.service";
import { WorkflowExecutionsEntry } from "../../../dashboard/type/workflow-executions-entry";
@@ -73,6 +76,7 @@ import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select";
import { FormsModule } from "@angular/forms";
import { NzCollapseComponent, NzCollapsePanelComponent } from "ng-zorro-antd/collapse";
import { ComputingUnitCreateModalComponent } from "../../../common/component/computing-unit-create-modal/computing-unit-create-modal.component";
+import { WarehouseCreateModalComponent } from "../../../common/component/warehouse-create-modal/warehouse-create-modal.component";
type PveUserPackageRow = {
name: string;
@@ -128,6 +132,7 @@ type PveDraft = {
NzCollapsePanelComponent,
DecimalPipe,
ComputingUnitCreateModalComponent,
+ WarehouseCreateModalComponent,
],
})
export class ComputingUnitSelectionComponent implements OnInit {
@@ -153,9 +158,23 @@ export class ComputingUnitSelectionComponent implements OnInit {
selectedComputingUnit: DashboardWorkflowComputingUnit | null = null;
allComputingUnits: DashboardWorkflowComputingUnit[] = [];
+ // Per-user warehouse picker (#6933): shown whenever the deployment reports
+ // the feature enabled — with zero warehouses it still offers the create
+ // entry, and the Run button leads there too.
+ warehouseEnabled: boolean = false;
+ warehouses: DashboardWarehouse[] = [];
+ selectedWarehouseId?: number;
+ // The latest execution's warehouse; the warehouse list and the latest
+ // execution are fetched concurrently, so preselection re-runs after
+ // whichever response lands last.
+ private lastExecutionWhid?: number;
+
// visibility of the shared create-computing-unit modal
addComputeUnitModalVisible = false;
+ // visibility of the shared create-warehouse modal
+ addWarehouseModalVisible = false;
+
@ViewChild(ComputingUnitCreateModalComponent)
private computingUnitCreateModal?: ComputingUnitCreateModalComponent;
@@ -180,7 +199,9 @@ export class ComputingUnitSelectionComponent implements OnInit {
private cdr: ChangeDetectorRef,
private computingUnitActionsService: ComputingUnitActionsService,
private workflowPveService: WorkflowPveService,
- private ngZone: NgZone
+ private ngZone: NgZone,
+ private warehouseService: WarehouseService,
+ private warehouseActionsService: WarehouseActionsService
) {}
ngOnInit(): void {
@@ -226,6 +247,17 @@ export class ComputingUnitSelectionComponent implements OnInit {
this.allComputingUnits = units;
});
+ // Warehouse picker state (#6933). The pick itself lives in WarehouseService,
+ // where ExecuteWorkflowService reads it at execution time.
+ this.refreshWarehouses();
+
+ this.warehouseService
+ .getSelectedWarehouseId()
+ .pipe(untilDestroyed(this))
+ .subscribe(whid => {
+ this.selectedWarehouseId = whid;
+ });
+
this.registerWorkflowMetadataSubscription();
}
@@ -275,12 +307,16 @@ export class ComputingUnitSelectionComponent implements OnInit {
.subscribe({
next: (latestWorkflowExecution: WorkflowExecutionsEntry) => {
this.selectComputingUnit(this.workflowId, latestWorkflowExecution.cuId);
+ this.lastExecutionWhid = latestWorkflowExecution.whId ?? undefined;
+ this.applyWarehousePreselect();
},
error: (err: unknown) => {
const runningUnit = this.allComputingUnits.find(unit => unit.status === "Running");
if (runningUnit) {
this.selectComputingUnit(this.workflowId, runningUnit.computingUnit.cuid);
}
+ // No execution history: still preselect a warehouse (the first one).
+ this.applyWarehousePreselect();
},
});
}
@@ -297,6 +333,102 @@ export class ComputingUnitSelectionComponent implements OnInit {
}
}
+ /**
+ * Fetches the warehouse list, on init and on every dropdown open (mirroring
+ * onDropdownVisibilityChange). Preselection re-runs only when the current
+ * pick is gone (first load, or the picked warehouse was deleted), so a
+ * routine refresh cannot override a manual pick.
+ */
+ private refreshWarehouses(): void {
+ this.warehouseService
+ .getStatus()
+ .pipe(untilDestroyed(this))
+ .subscribe({
+ next: status => {
+ this.warehouseEnabled = status.enabled;
+ this.warehouses = [...status.warehouses];
+ if (
+ this.selectedWarehouseId === undefined ||
+ !this.warehouses.some(warehouse => warehouse.whid === this.selectedWarehouseId)
+ ) {
+ this.applyWarehousePreselect();
+ }
+ },
+ error: (err: unknown) => {
+ // The pick lives in the root-scoped service, so hiding the picker is not
+ // enough: a stale id from a previous workflow would still ride the next
+ // execution request. Clear it whenever the picker cannot be shown.
+ this.warehouseEnabled = false;
+ this.warehouses = [];
+ this.warehouseService.selectWarehouse(undefined);
+ console.error("Failed to fetch warehouse status", err);
+ },
+ });
+ }
+
+ /**
+ * Mirrors the CU preselection for warehouses (#6933): pick the latest
+ * execution's warehouse when it still exists, else the user's first
+ * warehouse — so a run needs no explicit pick.
+ */
+ private applyWarehousePreselect(): void {
+ if (!this.warehouseEnabled || this.warehouses.length === 0) {
+ // Nothing selectable: drop any pick the root-scoped service still holds, so a
+ // stale id cannot ride the next execution while the picker stays hidden.
+ this.warehouseService.selectWarehouse(undefined);
+ return;
+ }
+ const lastUsed = this.warehouses.find(warehouse => warehouse.whid === this.lastExecutionWhid);
+ this.warehouseService.selectWarehouse((lastUsed ?? this.warehouses[0]).whid);
+ }
+
+ onWarehouseSelected(whid: number): void {
+ this.warehouseService.selectWarehouse(whid);
+ }
+
+ public trackByWhid(_idx: number, warehouse: DashboardWarehouse): number {
+ return warehouse.whid;
+ }
+
+ onWarehouseDropdownVisibilityChange(visible: boolean): void {
+ if (visible) {
+ this.refreshWarehouses();
+ }
+ }
+
+ get selectedWarehouse(): DashboardWarehouse | undefined {
+ return this.warehouses.find(warehouse => warehouse.whid === this.selectedWarehouseId);
+ }
+
+ /**
+ * True when the deployment enables per-user warehouses but none is selected.
+ * The menu's Run button redirects to the create-warehouse modal in this
+ * state, mirroring the computing-unit Connect flow: with the feature on,
+ * every execution must have a warehouse to write to.
+ */
+ get warehouseRequiredButMissing(): boolean {
+ return this.warehouseEnabled && this.selectedWarehouseId === undefined;
+ }
+
+ getWarehouseButtonText(): string {
+ return this.selectedWarehouse?.name ?? "Warehouse";
+ }
+
+ showAddWarehouseModalVisible(): void {
+ this.addWarehouseModalVisible = true;
+ }
+
+ onWarehouseCreated(warehouse: DashboardWarehouse): void {
+ // Mirrors onComputingUnitCreated: a warehouse created from the workspace is
+ // what the next execution should write to.
+ this.warehouseService.selectWarehouse(warehouse.whid);
+ this.refreshWarehouses();
+ }
+
+ confirmDeleteWarehouse(warehouse: DashboardWarehouse): void {
+ this.warehouseActionsService.confirmAndDelete(warehouse, () => this.refreshWarehouses());
+ }
+
isComputingUnitRunning(): boolean {
return this.selectedComputingUnit != null && this.selectedComputingUnit.status === "Running";
}
diff --git a/frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts b/frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts
index 6b1b5106344..101fc39cc1c 100644
--- a/frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts
+++ b/frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts
@@ -39,6 +39,7 @@ import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.servic
import { WorkflowSettings } from "src/app/common/type/workflow";
import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
+import { WarehouseService } from "../../../common/service/warehouse/warehouse.service";
import { AuthService } from "src/app/common/service/user/auth.service";
import { StubAuthService } from "src/app/common/service/user/stub-auth.service";
import { UserService } from "src/app/common/service/user/user.service";
@@ -397,6 +398,27 @@ describe("ExecuteWorkflowService", () => {
);
}));
+ it("sendExecutionRequest carries the picked warehouse id, and none when unset (#6933)", fakeAsync(() => {
+ const warehouseService = TestBed.inject(WarehouseService);
+ const wsSendSpy = vi.spyOn(service["workflowWebsocketService"], "send");
+ const settings = service["workflowActionService"].getWorkflowSettings();
+
+ warehouseService.selectWarehouse(7);
+ service.sendExecutionRequest("exec", {} as LogicalPlan, settings, false, undefined);
+ tick(FORM_DEBOUNCE_TIME_MS + 1);
+ flush();
+ expect(wsSendSpy).toHaveBeenLastCalledWith("WorkflowExecuteRequest", expect.objectContaining({ warehouseId: 7 }));
+
+ warehouseService.selectWarehouse(undefined);
+ service.sendExecutionRequest("exec", {} as LogicalPlan, settings, false, undefined);
+ tick(FORM_DEBOUNCE_TIME_MS + 1);
+ flush();
+ expect(wsSendSpy).toHaveBeenLastCalledWith(
+ "WorkflowExecuteRequest",
+ expect.objectContaining({ warehouseId: undefined })
+ );
+ }));
+
it("sendExecutionRequest flags stored pagination info as belonging to a new execution", fakeAsync(() => {
sessionSetObject(PAGINATION_INFO_STORAGE_KEY, { newWorkflowExecuted: false });
const settings = service["workflowActionService"].getWorkflowSettings();
diff --git a/frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.ts b/frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.ts
index c2ab3eac0d3..278a336832a 100644
--- a/frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.ts
+++ b/frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.ts
@@ -48,6 +48,7 @@ import { intersection } from "../../../common/util/set";
import { WorkflowSettings } from "../../../common/type/workflow";
import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
+import { WarehouseService } from "../../../common/service/warehouse/warehouse.service";
// TODO: change this declaration
export const FORM_DEBOUNCE_TIME_MS = 150;
@@ -100,7 +101,8 @@ export class ExecuteWorkflowService {
private workflowStatusService: WorkflowStatusService,
private notificationService: NotificationService,
@Inject(DOCUMENT) private document: Document,
- private computingUnitStatusService: ComputingUnitStatusService
+ private computingUnitStatusService: ComputingUnitStatusService,
+ private warehouseService: WarehouseService
) {
workflowWebsocketService.websocketEvent().subscribe(event => {
switch (event.type) {
@@ -241,6 +243,11 @@ export class ExecuteWorkflowService {
const selectedUnit = this.computingUnitStatusService.getSelectedComputingUnitValue();
const computingUnitId = selectedUnit?.computingUnit.cuid;
+ // The warehouse this execution writes to (#6933); undefined serializes away,
+ // which the backend today reads as the shared default storage (#7751
+ // tightens that to a rejection while the feature is enabled).
+ const warehouseId = this.warehouseService.getSelectedWarehouseIdValue();
+
// Log a warning if no computing unit is selected
if (computingUnitId === undefined) {
console.warn("No computing unit selected for workflow execution");
@@ -254,6 +261,7 @@ export class ExecuteWorkflowService {
workflowSettings: workflowSettings,
emailNotificationEnabled: emailNotificationEnabled,
computingUnitId: computingUnitId, // Include the computing unit ID
+ warehouseId: warehouseId,
};
// wait for the form debounce to complete, then send
window.setTimeout(() => {