This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new 9264975c2d feat(frontend): add the on-canvas per-execution warehouse 
picker (#8551)
9264975c2d is described below

commit 9264975c2d05d2520593f84060b39db23ab8eae0
Author: Meng Wang <[email protected]>
AuthorDate: Fri Sep 18 04:13:10 2026 +0000

    feat(frontend): add the on-canvas per-execution warehouse picker (#8551)
    
    ### What changes were proposed in this PR?
    
    The workspace-side half of per-user warehouses (#6870): a warehouse
    picker on the canvas, beside the computing-unit selector it mirrors,
    choosing which warehouse the next execution writes to.
    
    - **The picker** (inside `ComputingUnitSelectionComponent`, like the
    computing-unit dropdown it sits beside) — shown only while the
    deployment enables the feature; each entry carries the owner avatar and
    a per-row delete, plus a create entry opening the shared create dialog.
    The list refreshes on every dropdown open, and preselection picks the
    latest execution's warehouse, falling back to the user's first one — so
    a run needs no explicit pick. A status failure clears the pick rather
    than letting a stale id ride the next request.
    - **Run gating** — with the feature enabled, every execution must have a
    warehouse: while none is selected the Run button becomes "Create
    Warehouse" and leads to the create dialog, the same shape as the Connect
    flow. (The backend-side requirement follows separately — #7751 stays the
    tracker.)
    - **The request** — the picked `warehouseId` rides the execute request
    (`ExecuteWorkflowService` reads the pick from `WarehouseService`, where
    it lives); executions expose `whId` so the preselect can read the latest
    run's warehouse.
    
    Flag off (the default): the picker never renders, the Run button is
    untouched, and requests carry no warehouseId — no user-visible change.
    
    ### demo
    
    
    
https://github.com/user-attachments/assets/20e81213-ba36-450c-861e-594112515b50
    
    ### Any related issues, documentation, discussions?
    
    Closes #7817. Part of #6870, on top of the dashboard tab (#8005); the
    backend enforcement (#7751) follows.
    
    ### How was this PR tested?
    
    - 20 new Vitest tests: 15 on the picker (preselect from the latest
    execution, first-warehouse fallback, disabled/failed states clearing the
    pick, manual pick surviving refreshes, create/delete flows, dropdown
    rendering), 3 on the Run gating, 1 pinning `warehouseId` on the execute
    request, 1 on the selection state. One pre-existing assertion
    modernized: the remembered-unit test asserted the latest-execution
    lookup's absence, which the warehouse preselect now legitimately
    performs — it asserts the unit choice directly instead.
    - The affected suites pass in full: 5083 tests across the workspace and
    dashboard trees.
    - Failure paths verified rather than assumed: the preselect, the Run
    gating, and the request field were each broken on purpose and the suite
    confirmed to fail for the expected reason before being restored.
    - Screenshots/video from a local flag-on deployment follow in the
    comments.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (claude-opus-5, claude-fable-5)
---
 .../service/warehouse/warehouse.service.spec.ts    |  15 +
 .../common/service/warehouse/warehouse.service.ts  |  20 +-
 .../workflow-execution-history.component.spec.ts   |   1 +
 .../dashboard/type/workflow-executions-entry.ts    |   2 +
 .../component/menu/menu.component.spec.ts          |  80 ++++
 .../app/workspace/component/menu/menu.component.ts |  44 +++
 .../computing-unit-selection.component.html        |  93 ++++-
 .../computing-unit-selection.component.scss        |  85 +++++
 .../computing-unit-selection.component.spec.ts     | 406 ++++++++++++++++++++-
 .../computing-unit-selection.component.ts          | 190 +++++++++-
 .../execute-workflow.service.spec.ts               |  62 ++++
 .../execute-workflow/execute-workflow.service.ts   |  33 +-
 12 files changed, 1021 insertions(+), 10 deletions(-)

diff --git 
a/frontend/src/app/common/service/warehouse/warehouse.service.spec.ts 
b/frontend/src/app/common/service/warehouse/warehouse.service.spec.ts
index 093aadcaaa..aea048c995 100644
--- a/frontend/src/app/common/service/warehouse/warehouse.service.spec.ts
+++ b/frontend/src/app/common/service/warehouse/warehouse.service.spec.ts
@@ -83,4 +83,19 @@ describe("WarehouseService", () => {
 
     expect(completed).toBe(true);
   });
+
+  it("holds the per-execution warehouse pick; undefined means no pick", () => {
+    expect(service.getSelectedWarehouseIdValue()).toBeUndefined();
+
+    const seen: (number | undefined)[] = [];
+    service.getSelectedWarehouseId().subscribe(whid => seen.push(whid));
+
+    service.selectWarehouse(7);
+    expect(service.getSelectedWarehouseIdValue()).toBe(7);
+
+    service.selectWarehouse(undefined);
+    expect(service.getSelectedWarehouseIdValue()).toBeUndefined();
+
+    expect(seen).toEqual([undefined, 7, undefined]);
+  });
 });
diff --git a/frontend/src/app/common/service/warehouse/warehouse.service.ts 
b/frontend/src/app/common/service/warehouse/warehouse.service.ts
index facc7879b3..dc78a339cf 100644
--- a/frontend/src/app/common/service/warehouse/warehouse.service.ts
+++ b/frontend/src/app/common/service/warehouse/warehouse.service.ts
@@ -19,7 +19,7 @@
 
 import { Injectable } from "@angular/core";
 import { HttpClient } from "@angular/common/http";
-import { Observable } from "rxjs";
+import { BehaviorSubject, Observable } from "rxjs";
 import { AppSettings } from "../../app-setting";
 import { DashboardWarehouse, WarehouseStatus } from "../../type/warehouse";
 
@@ -34,6 +34,12 @@ export const WAREHOUSE_STATUS_URL = 
`${WAREHOUSE_BASE_URL}/status`;
   providedIn: "root",
 })
 export class WarehouseService {
+  // The warehouse the next execution writes to; undefined = no pick. The
+  // backend still treats an absent warehouseId as the shared default storage;
+  // #7751 tightens that to a rejection while the feature is enabled — the
+  // picker's preselect and the Run gate keep it defined there.
+  private selectedWarehouseIdSubject = new BehaviorSubject<number | 
undefined>(undefined);
+
   constructor(private http: HttpClient) {}
 
   public getStatus(): Observable<WarehouseStatus> {
@@ -47,4 +53,16 @@ export class WarehouseService {
   public deleteWarehouse(whid: number): Observable<void> {
     return 
this.http.delete<void>(`${AppSettings.getApiEndpoint()}/${WAREHOUSE_BASE_URL}/${whid}`);
   }
+
+  public selectWarehouse(whid: number | undefined): void {
+    this.selectedWarehouseIdSubject.next(whid);
+  }
+
+  public getSelectedWarehouseId(): Observable<number | undefined> {
+    return this.selectedWarehouseIdSubject.asObservable();
+  }
+
+  public getSelectedWarehouseIdValue(): number | undefined {
+    return this.selectedWarehouseIdSubject.value;
+  }
 }
diff --git 
a/frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component.spec.ts
index 8605d07a6d..8fe249275d 100644
--- 
a/frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component.spec.ts
@@ -47,6 +47,7 @@ function makeEntry(overrides: 
Partial<WorkflowExecutionsEntry> = {}): WorkflowEx
     eId: 1,
     vId: 1,
     cuId: 1,
+    whId: 1,
     sId: 0,
     userName: "alice",
     avatar: "",
diff --git a/frontend/src/app/dashboard/type/workflow-executions-entry.ts 
b/frontend/src/app/dashboard/type/workflow-executions-entry.ts
index 8687416d83..9a9136c39d 100644
--- a/frontend/src/app/dashboard/type/workflow-executions-entry.ts
+++ b/frontend/src/app/dashboard/type/workflow-executions-entry.ts
@@ -22,6 +22,8 @@ export interface WorkflowExecutionsEntry {
   eId: number;
   vId: number;
   cuId: number;
+  /** null for runs on the shared default storage, and after the warehouse is 
deleted. */
+  whId: number | null;
   sId: number;
   userName: string;
   avatar: string;
diff --git a/frontend/src/app/workspace/component/menu/menu.component.spec.ts 
b/frontend/src/app/workspace/component/menu/menu.component.spec.ts
index 3e15706cf0..5996762420 100644
--- a/frontend/src/app/workspace/component/menu/menu.component.spec.ts
+++ b/frontend/src/app/workspace/component/menu/menu.component.spec.ts
@@ -56,6 +56,7 @@ import { GuiConfigService } from 
"../../../common/service/gui-config.service";
 import { MockGuiConfigService } from 
"../../../common/service/gui-config.service.mock";
 import { JupyterPanelService } from 
"../../service/jupyter-panel/jupyter-panel.service";
 import type { Mocked } from "vitest";
+import { WarehouseService } from 
"../../../common/service/warehouse/warehouse.service";
 
 describe("MenuComponent", () => {
   let component: MenuComponent;
@@ -410,6 +411,40 @@ describe("MenuComponent", () => {
       expect(behavior.disable).toBe(false);
       expect(runSpy).toHaveBeenCalledTimes(1);
     });
+
+    it("keeps Pause in control of a running execution even when the warehouse 
disappears", () => {
+      // Deleting the last warehouse mid-run flips warehouseRequiredButMissing;
+      // the primary button must stay Pause/Kill, not become "Create 
Warehouse".
+      component.isWorkflowValid = true;
+      component.isWorkflowEmpty = false;
+      component.computingUnitStatus = ComputingUnitState.Running;
+      Object.defineProperty(component.workflowWebsocketService, "isConnected", 
{ get: () => true, configurable: true });
+      component.executionState = ExecutionState.Running;
+      component.computingUnitSelectionComponent = {
+        warehouseRequiredButMissing: true,
+      } as unknown as Mocked<ComputingUnitSelectionComponent>;
+
+      const behavior = component.getRunButtonBehavior();
+
+      expect(behavior.text).toBe("Pause");
+    });
+
+    it("offers to create a warehouse when one is required but missing", () => {
+      component.isWorkflowValid = true;
+      component.isWorkflowEmpty = false;
+      component.computingUnitStatus = ComputingUnitState.Running;
+      Object.defineProperty(component.workflowWebsocketService, "isConnected", 
{ get: () => true, configurable: true });
+      component.executionState = ExecutionState.Uninitialized;
+      component.computingUnitSelectionComponent = {
+        warehouseRequiredButMissing: true,
+      } as unknown as Mocked<ComputingUnitSelectionComponent>;
+
+      const behavior = component.getRunButtonBehavior();
+
+      expect(behavior.text).toBe("Create Warehouse");
+      expect(behavior.icon).toBe("plus-circle");
+      expect(behavior.disable).toBe(false);
+    });
   });
 
   it("applyRunButtonBehavior copies the behavior onto the bound fields", () => 
{
@@ -591,6 +626,51 @@ describe("MenuComponent", () => {
 
       expect(executeSpy).toHaveBeenCalledWith("Untitled Execution", false);
     });
+
+    it("leads to the create-warehouse modal when a warehouse is required but 
missing", () => {
+      component.isWorkflowValid = true;
+      component.isWorkflowEmpty = false;
+      component.computingUnitStatus = ComputingUnitState.Running;
+      component.computingUnitSelectionComponent = {
+        showAddComputeUnitModalVisible: vi.fn(),
+        showAddWarehouseModalVisible: vi.fn(),
+        warehouseRequiredButMissing: true,
+      } as unknown as Mocked<ComputingUnitSelectionComponent>;
+      const executeSpy = vi.spyOn(executeWorkflowService, 
"executeWorkflowWithEmailNotification");
+
+      component.runWorkflow();
+
+      
expect(component.computingUnitSelectionComponent.showAddWarehouseModalVisible).toHaveBeenCalledTimes(1);
+      expect(executeSpy).not.toHaveBeenCalled();
+    });
+
+    it("recomputes the Run button snapshot when the warehouse pick changes", 
() => {
+      // The button text is a stored snapshot; without the subscription it
+      // would keep saying "Run" after the warehouse load leaves none.
+      const applySpy = vi.spyOn(component, "applyRunButtonBehavior");
+
+      TestBed.inject(WarehouseService).selectWarehouse(7);
+
+      expect(applySpy).toHaveBeenCalled();
+    });
+
+    it("submits the execution when a warehouse is selected", () => {
+      component.isWorkflowValid = true;
+      component.isWorkflowEmpty = false;
+      component.computingUnitStatus = ComputingUnitState.Running;
+      component.computingUnitSelectionComponent = {
+        showAddWarehouseModalVisible: vi.fn(),
+        warehouseRequiredButMissing: false,
+      } as unknown as Mocked<ComputingUnitSelectionComponent>;
+      const executeSpy = vi
+        .spyOn(executeWorkflowService, "executeWorkflowWithEmailNotification")
+        .mockImplementation(() => {});
+
+      component.runWorkflow();
+
+      expect(executeSpy).toHaveBeenCalledTimes(1);
+      
expect(component.computingUnitSelectionComponent.showAddWarehouseModalVisible).not.toHaveBeenCalled();
+    });
   });
 
   it("onWorkflowNameChange forwards the new name to the workflow action 
service", () => {
diff --git a/frontend/src/app/workspace/component/menu/menu.component.ts 
b/frontend/src/app/workspace/component/menu/menu.component.ts
index 3a46e987f3..04da8e2b08 100644
--- a/frontend/src/app/workspace/component/menu/menu.component.ts
+++ b/frontend/src/app/workspace/component/menu/menu.component.ts
@@ -47,6 +47,7 @@ import { ShareAccessComponent } from 
"src/app/dashboard/component/user/share-acc
 import { PanelService } from "../../service/panel/panel.service";
 import { USER_WORKFLOW, USER_WORKSPACE } from "../../../app-routing.constant";
 import { ComputingUnitStatusService } from 
"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
+import { WarehouseService } from 
"../../../common/service/warehouse/warehouse.service";
 import { ComputingUnitState } from 
"../../../common/type/computing-unit-connection.interface";
 import { ComputingUnitSelectionComponent } from 
"../power-button/computing-unit-selection.component";
 import { GuiConfigService } from "../../../common/service/gui-config.service";
@@ -190,6 +191,7 @@ export class MenuComponent implements OnInit, OnDestroy {
     private reportGenerationService: ReportGenerationService,
     private panelService: PanelService,
     private computingUnitStatusService: ComputingUnitStatusService,
+    private warehouseService: WarehouseService,
     protected config: GuiConfigService,
     private router: Router,
     private jupyterPanelService: JupyterPanelService,
@@ -285,6 +287,17 @@ export class MenuComponent implements OnInit, OnDestroy {
         this.computingUnitStatus = status;
         this.applyRunButtonBehavior(this.getRunButtonBehavior());
       });
+
+    // The warehouse pick also feeds getRunButtonBehavior (#7817); without this
+    // the snapshot keeps saying "Run" after the load leaves no warehouse, and
+    // "Create Warehouse" after one is created. Every relevant transition ends
+    // in a selectWarehouse call, so the pick stream covers them all.
+    this.warehouseService
+      .getSelectedWarehouseId()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        this.applyRunButtonBehavior(this.getRunButtonBehavior());
+      });
   }
 
   /**
@@ -408,6 +421,29 @@ export class MenuComponent implements OnInit, OnDestroy {
       };
     }
 
+    // Per-user warehouses enabled but none to write to (#7817): mirror the
+    // Connect state above — name the fixing action, and runWorkflow() routes
+    // the click into the create-warehouse modal. Only in the states whose
+    // button would start a run: mid-execution the button is Pause/Resume/Kill,
+    // and losing the last warehouse must not take that control away.
+    if (
+      this.computingUnitSelectionComponent?.warehouseRequiredButMissing &&
+      [
+        ExecutionState.Uninitialized,
+        ExecutionState.Completed,
+        ExecutionState.Terminated,
+        ExecutionState.Killed,
+        ExecutionState.Failed,
+      ].includes(this.executionState)
+    ) {
+      return {
+        text: "Create Warehouse",
+        icon: "plus-circle",
+        disable: false,
+        onClick: () => this.runWorkflow(),
+      };
+    }
+
     // Handle execution states when connected to a running computing unit
     switch (this.executionState) {
       case ExecutionState.Uninitialized:
@@ -910,6 +946,14 @@ export class MenuComponent implements OnInit, OnDestroy {
       return;
     }
 
+    // Per-user warehouses enabled but none to write to (#7817): an execution
+    // must have a warehouse, so lead to the create-warehouse modal instead of
+    // running — the same shape as the Connect flow above.
+    if (this.computingUnitSelectionComponent.warehouseRequiredButMissing) {
+      this.computingUnitSelectionComponent.showAddWarehouseModalVisible();
+      return;
+    }
+
     // Regular workflow execution - already connected
     this.executeWorkflowService.executeWorkflowWithEmailNotification(
       this.currentExecutionName || "Untitled Execution",
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 066e39c836..ddbd2249a4 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
@@ -19,7 +19,7 @@
 
 <div
   class="computing-units-selection"
-  [ngClass]="{ 'metrics-visible': isComputingUnitRunning() }">
+  [ngClass]="{ 'metrics-visible': isComputingUnitRunning(), 
'warehouse-visible': warehouseEnabled }">
   <div
     *ngIf="isComputingUnitRunning() && selectedComputingUnit && 
selectedComputingUnit.computingUnit.type !== 'local'"
     nz-button
@@ -58,6 +58,92 @@
     </div>
   </div>
 
+  <button
+    *ngIf="warehouseEnabled"
+    nz-button
+    nz-dropdown
+    nzTrigger="click"
+    [nzDropdownMenu]="warehouseMenu"
+    [nzPlacement]="'bottomRight'"
+    (nzVisibleChange)="onWarehouseDropdownVisibilityChange($event)"
+    class="warehouse-dropdown-button"
+    nz-tooltip
+    nzTooltipTitle="Warehouse this execution writes to">
+    <div class="button-content">
+      <texera-user-avatar
+        *ngIf="selectedWarehouse as selected"
+        [avatar]="selected.ownerAvatar || ''"
+        userColor="grey"
+        [userName]="selected.ownerName || ''"
+        [style.transform]="'scale(0.65)'"
+        [style.opacity]="0.7"
+        [style.padding-right.px]="2">
+      </texera-user-avatar>
+      <i
+        nz-icon
+        nzType="cloud-server"></i>
+      <span class="warehouse-name-text">{{ getWarehouseButtonText() }}</span>
+      <i
+        nz-icon
+        nzType="down"></i>
+    </div>
+  </button>
+
+  <nz-dropdown-menu #warehouseMenu="nzDropdownMenu">
+    <ul
+      nz-menu
+      class="warehouses-dropdown">
+      <li
+        nz-menu-item
+        *ngFor="let warehouse of warehouses; trackBy: trackByWhid"
+        [id]="'warehouse-option-' + warehouse.whid"
+        class="warehouse-option"
+        [ngClass]="{ 'warehouse-selected': warehouse.whid === 
selectedWarehouseId }"
+        (click)="onWarehouseSelected(warehouse.whid)">
+        <div class="warehouse-row">
+          <texera-user-avatar
+            [avatar]="warehouse.ownerAvatar || ''"
+            userColor="grey"
+            [userName]="warehouse.ownerName || ''"
+            [style.transform]="'scale(0.65)'"
+            [style.opacity]="0.7">
+          </texera-user-avatar>
+          <div class="warehouse-name">
+            <span
+              nz-tooltip
+              [nzTooltipTitle]="warehouse.name">
+              {{ warehouse.name }}
+            </span>
+          </div>
+          <i
+            nz-icon
+            nzType="delete"
+            class="warehouse-delete-icon"
+            nz-tooltip
+            [nzTooltipTitle]="'Delete warehouse and all data stored in it'"
+            (click)="confirmDeleteWarehouse(warehouse); 
$event.stopPropagation()"
+            role="button"
+            aria-label="Delete warehouse">
+          </i>
+        </div>
+      </li>
+
+      <li
+        *ngIf="warehouses.length > 0"
+        nz-menu-divider></li>
+      <li
+        nz-menu-item
+        (click)="showAddWarehouseModalVisible()">
+        <div class="create-warehouse">
+          <i
+            nz-icon
+            nzType="plus"></i>
+          <span> Warehouse</span>
+        </div>
+      </li>
+    </ul>
+  </nz-dropdown-menu>
+
   <button
     nz-button
     nz-dropdown
@@ -223,6 +309,11 @@
   [(visible)]="addComputeUnitModalVisible"
   
(unitCreated)="onComputingUnitCreated($event)"></texera-computing-unit-create-modal>
 
+<!-- Panel for creating the warehouse -->
+<texera-warehouse-create-modal
+  [(visible)]="addWarehouseModalVisible"
+  
(warehouseCreated)="onWarehouseCreated($event)"></texera-warehouse-create-modal>
+
 <ng-template #metricsTemplate>
   <div class="resource-metrics">
     <div class="cpu-metric general-metric">
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 1bae9769d7..ce27d35674 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 2fca86830e..9f861e0d37 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
@@ -56,6 +56,9 @@ import { ComputingUnitActionsService } from 
"../../../common/service/computing-u
 import { ComputingUnitMetadataComponent } from 
"../../../common/util/computing-unit.util";
 import { GuiConfigService } from "../../../common/service/gui-config.service";
 import { NzPopoverDirective } from "ng-zorro-antd/popover";
+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
@@ -1414,7 +1417,11 @@ describe("PowerButtonComponent", () => {
       emit(100);
 
       expect(selectSpy).toHaveBeenCalledWith(100, 77);
-      expect(latestSpy).not.toHaveBeenCalled();
+      // The warehouse preselect (#7817) legitimately reads the latest 
execution
+      // even on this path, so assert the unit choice directly instead of the
+      // lookup's absence: the latest execution's unit must not win.
+      expect(latestSpy).toHaveBeenCalled();
+      expect(selectSpy).not.toHaveBeenCalledWith(100, 55);
     });
 
     // A remembered unit that has since been terminated must not be chased: 
the status service
@@ -2437,4 +2444,401 @@ describe("PowerButtonComponent", () => {
       getItem.mockRestore();
     });
   });
+
+  describe("warehouse picker (#7817)", () => {
+    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<WorkflowExecutionsEntry> | "error";
+    }): {
+      comp: ComputingUnitSelectionComponent;
+      pickerFixture: ComponentFixture<ComputingUnitSelectionComponent>;
+      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<WorkflowMetadata>();
+      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("a status failure keeps the last known list and pick, and reports the 
error", () => {
+      // A transport failure is not an answer; only an authoritative response
+      // (enabled:false, or a list without the pick) may clear the pick.
+      const { comp, 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);
+      const errorSpy = vi.spyOn(TestBed.inject(NotificationService), 
"error").mockImplementation(() => {});
+      vi.spyOn(TestBed.inject(WarehouseService), "getStatus").mockReturnValue(
+        throwError(() => new Error("status unavailable"))
+      );
+
+      comp.onWarehouseDropdownVisibilityChange(true);
+
+      
expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(2);
+      expect(comp.warehouses.map(w => w.whid)).toEqual([1, 2]);
+      expect(errorSpy).toHaveBeenCalledWith("Failed to fetch warehouses: 
status unavailable");
+    });
+
+    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);
+    });
+
+    it("a stale last-execution warehouse never steers the next workflow's 
fallback", () => {
+      const { comp, 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);
+
+      // The next workflow has no history: its fallback must be the FIRST
+      // warehouse, not the previous workflow's.
+      const execService = TestBed.inject(WorkflowExecutionsService);
+      vi.spyOn(execService, "retrieveLatestWorkflowExecution").mockReturnValue(
+        throwError(() => new Error("no execution"))
+      );
+      emit(200);
+
+      
expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(1);
+    });
+
+    it("a late response from a superseded refresh cannot restore stale state", 
() => {
+      const first = new Subject<{ enabled: boolean; warehouses: 
DashboardWarehouse[] }>();
+      const second = new Subject<{ enabled: boolean; warehouses: 
DashboardWarehouse[] }>();
+      vi.spyOn(TestBed.inject(WarehouseService), "getStatus")
+        .mockReturnValueOnce(first.asObservable())
+        .mockReturnValueOnce(second.asObservable());
+      const execService = TestBed.inject(WorkflowExecutionsService);
+      vi.spyOn(execService, "retrieveLatestWorkflowExecution").mockReturnValue(
+        throwError(() => new Error("no execution"))
+      );
+      const pickerFixture = 
TestBed.createComponent(ComputingUnitSelectionComponent);
+      pickerFixture.detectChanges();
+      const comp = pickerFixture.componentInstance;
+
+      comp.onWarehouseDropdownVisibilityChange(true);
+      second.next({ enabled: true, warehouses: [makeWarehouse(2, "kept")] });
+      second.complete();
+      // The older request settles last; switchMap must already have dropped 
it.
+      first.next({ enabled: true, warehouses: [makeWarehouse(1, "stale"), 
makeWarehouse(9, "gone")] });
+      first.complete();
+
+      expect(comp.warehouses.map(w => w.name)).toEqual(["kept"]);
+    });
+
+    it("a status failure keeps the gate closed on an enabled deployment", () 
=> {
+      // Failing open would un-gate Run and let the execution write to the
+      // shared default storage with no warehouseId.
+      TestBed.inject(GuiConfigService).env.warehouseEnabled = true;
+      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(failedFixture.componentInstance.warehouseRequiredButMissing).toBe(true);
+      errorSpy.mockRestore();
+      TestBed.inject(GuiConfigService).env.warehouseEnabled = false;
+    });
+
+    it("a manual pick survives a late latest-execution answer", () => {
+      const inFlight = new Subject<WorkflowExecutionsEntry>();
+      vi.spyOn(TestBed.inject(WorkflowExecutionsService), 
"retrieveLatestWorkflowExecution").mockReturnValue(
+        inFlight.asObservable()
+      );
+      const { comp, emit } = bootPicker({
+        enabled: true,
+        warehouses: [makeWarehouse(1, "first"), makeWarehouse(2, "second")],
+      });
+      emit(100);
+
+      comp.onWarehouseSelected(2);
+      inFlight.next({ cuId: 55, whId: 1 } as unknown as 
WorkflowExecutionsEntry);
+      inFlight.complete();
+
+      
expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(2);
+    });
+
+    it("switching workflows clears the pick at once, before the new preselect 
answers", () => {
+      const { comp, emit } = bootPicker({
+        enabled: true,
+        warehouses: [makeWarehouse(1, "first"), makeWarehouse(2, "second")],
+        latest: { cuId: 55, whId: 2 },
+      });
+      emit(100);
+      comp.onWarehouseSelected(2);
+
+      // The new workflow's lookup stays pending: in that window nothing of the
+      // old workflow's pick may ride an execution.
+      const pending = new Subject<WorkflowExecutionsEntry>();
+      vi.spyOn(TestBed.inject(WorkflowExecutionsService), 
"retrieveLatestWorkflowExecution").mockReturnValue(
+        pending.asObservable()
+      );
+      emit(200);
+      
expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBeUndefined();
+
+      pending.error(new Error("no execution"));
+      
expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(1);
+    });
+
+    it("deleting the picked warehouse removes it locally and re-preselects, 
with no refetch", () => {
+      const { comp, 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);
+      const statusSpy = vi.spyOn(TestBed.inject(WarehouseService), 
"getStatus");
+      statusSpy.mockClear(); // drop the boot-time call; only the action below 
counts
+      const deleteSpy = vi
+        .spyOn(TestBed.inject(WarehouseActionsService), "confirmAndDelete")
+        .mockImplementation((_warehouse, onDeleted) => onDeleted());
+
+      comp.confirmDeleteWarehouse(makeWarehouse(2, "second"));
+
+      expect(deleteSpy).toHaveBeenCalledTimes(1);
+      expect(comp.warehouses.map(w => w.whid)).toEqual([1]);
+      
expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(1);
+      expect(statusSpy).not.toHaveBeenCalled();
+    });
+
+    it("a created warehouse is appended locally and becomes the pick, with no 
refetch", () => {
+      const { comp, emit } = bootPicker({
+        enabled: true,
+        warehouses: [makeWarehouse(1, "first")],
+        latest: "error",
+      });
+      emit(100);
+      const statusSpy = vi.spyOn(TestBed.inject(WarehouseService), 
"getStatus");
+      statusSpy.mockClear(); // drop the boot-time call; only the action below 
counts
+
+      comp.onWarehouseCreated(makeWarehouse(9, "fresh"));
+
+      expect(comp.warehouses.map(w => w.whid)).toEqual([1, 9]);
+      
expect(TestBed.inject(WarehouseService).getSelectedWarehouseIdValue()).toBe(9);
+      expect(statusSpy).not.toHaveBeenCalled();
+    });
+  });
 });
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 eb899448f3..566e1d9bed 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
@@ -18,7 +18,8 @@
  */
 
 import { ChangeDetectorRef, Component, OnInit, NgZone, ViewChild } from 
"@angular/core";
-import { filter, take } from "rxjs/operators";
+import { catchError, filter, switchMap, take } from "rxjs/operators";
+import { EMPTY, Subject } from "rxjs";
 import { WorkflowComputingUnitManagingService } from 
"../../../common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service";
 import { DashboardWorkflowComputingUnit } from 
"../../../common/type/workflow-computing-unit";
 import { NotificationService } from 
"../../../common/service/notification/notification.service";
@@ -27,6 +28,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 +77,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 +133,7 @@ type PveDraft = {
     NzCollapsePanelComponent,
     DecimalPipe,
     ComputingUnitCreateModalComponent,
+    WarehouseCreateModalComponent,
   ],
 })
 export class ComputingUnitSelectionComponent implements OnInit {
@@ -153,9 +159,31 @@ export class ComputingUnitSelectionComponent implements 
OnInit {
   selectedComputingUnit: DashboardWorkflowComputingUnit | null = null;
   allComputingUnits: DashboardWorkflowComputingUnit[] = [];
 
+  // Per-user warehouse picker (#7817): 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;
+  // An explicit pick from the dropdown (or a create). Preselection never
+  // overrides it: a late latest-execution answer must not undo what the user
+  // chose in the meantime. Reset when the workflow changes.
+  private manualWarehousePick = false;
+  // 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;
+  // All warehouse refreshes flow through one switchMap'd stream (like
+  // UserWarehouseComponent): a new request cancels the in-flight one, so an
+  // older response can never restore a deleted row or clobber a newer answer.
+  private readonly warehouseRefreshRequested$ = new Subject<void>();
+
   // 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 +208,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 +256,43 @@ export class ComputingUnitSelectionComponent implements 
OnInit {
         this.allComputingUnits = units;
       });
 
+    // Warehouse picker state (#7817). The pick itself lives in 
WarehouseService,
+    // where ExecuteWorkflowService reads it at execution time.
+    this.warehouseRefreshRequested$
+      .pipe(
+        switchMap(() =>
+          this.warehouseService.getStatus().pipe(
+            // Caught inside the switchMap so a failure ends only this request,
+            // not the stream.
+            catchError((err: unknown) => {
+              // A transport failure is not an answer: the last known list and
+              // pick stay (clearing them is reserved for an authoritative
+              // response — enabled:false, or a list without the pick). The 
flag
+              // only falls back to the boot-time config, never to false: 
failing
+              // open would un-gate Run on an enabled deployment just because 
one
+              // status request failed.
+              this.warehouseEnabled = this.config.env.warehouseEnabled;
+              this.notificationService.error(`Failed to fetch warehouses: 
${extractErrorMessage(err)}`);
+              return EMPTY;
+            })
+          )
+        ),
+        untilDestroyed(this)
+      )
+      .subscribe(status => {
+        this.warehouseEnabled = status.enabled;
+        this.warehouses = [...status.warehouses];
+        this.applyWarehousePreselect();
+      });
+    this.refreshWarehouses();
+
+    this.warehouseService
+      .getSelectedWarehouseId()
+      .pipe(untilDestroyed(this))
+      .subscribe(whid => {
+        this.selectedWarehouseId = whid;
+      });
+
     this.registerWorkflowMetadataSubscription();
   }
 
@@ -268,6 +335,12 @@ export class ComputingUnitSelectionComponent implements 
OnInit {
         const wid = this.workflowActionService.getWorkflowMetadata()?.wid;
         if (wid !== this.workflowId) {
           this.workflowId = wid;
+          // The previous workflow's execution — and pick — must not steer this
+          // one: with the stale value, a workflow without history would fall
+          // back to the OLD workflow's warehouse instead of the first one.
+          this.lastExecutionWhid = undefined;
+          this.manualWarehousePick = false;
+          this.warehouseService.selectWarehouse(undefined);
           if (isDefined(this.workflowId) && this.workflowId !== 
DEFAULT_WORKFLOW.wid) {
             this.selectInitialUnit(this.workflowId);
           }
@@ -304,6 +377,9 @@ export class ComputingUnitSelectionComponent implements 
OnInit {
         }
         if (units.some(unit => unit.computingUnit.cuid === remembered)) {
           this.selectComputingUnit(wid, remembered);
+          // The remembered shortcut skips the execution lookup the warehouse
+          // preselect rides on, so run the lookup for the warehouse alone.
+          this.selectFromLastExecution(wid, false);
         } else {
           this.forgetComputingUnit(wid);
           this.selectFromLastExecution(wid);
@@ -311,8 +387,12 @@ export class ComputingUnitSelectionComponent implements 
OnInit {
       });
   }
 
-  /** The unit the workflow last ran on, else any unit that is running. */
-  private selectFromLastExecution(wid: number): void {
+  /**
+   * The unit the workflow last ran on, else any unit that is running — and 
the warehouse it last
+   * wrote to, for the preselect (#7817). `selectUnit` is false when the unit 
was already settled by
+   * a remembered choice and only the warehouse still needs the lookup.
+   */
+  private selectFromLastExecution(wid: number, selectUnit: boolean = true): 
void {
     // The workflow can change while the lookup is out; that later change 
decided for itself, so an
     // answer (or a failure) that arrives for the earlier one is stale.
     const stillShown = () => wid === this.workflowId;
@@ -322,14 +402,22 @@ export class ComputingUnitSelectionComponent implements 
OnInit {
       .subscribe({
         next: (latestWorkflowExecution: WorkflowExecutionsEntry) => {
           if (stillShown()) {
-            this.selectComputingUnit(wid, latestWorkflowExecution.cuId);
+            if (selectUnit) {
+              this.selectComputingUnit(wid, latestWorkflowExecution.cuId);
+            }
+            this.lastExecutionWhid = latestWorkflowExecution.whId ?? undefined;
+            this.applyWarehousePreselect();
           }
         },
         error: () => {
           const runningUnit = this.allComputingUnits.find(unit => unit.status 
=== "Running");
-          if (stillShown() && runningUnit) {
+          if (selectUnit && stillShown() && runningUnit) {
             this.selectComputingUnit(wid, runningUnit.computingUnit.cuid);
           }
+          // No execution history: still preselect a warehouse (the first one).
+          if (stillShown()) {
+            this.applyWarehousePreselect();
+          }
         },
       });
   }
@@ -409,6 +497,96 @@ export class ComputingUnitSelectionComponent implements 
OnInit {
     }
   }
 
+  /**
+   * Fetches the warehouse list, on init and on every dropdown open (mirroring
+   * onDropdownVisibilityChange). Every answer re-runs the preselect, whose
+   * manual-pick guard keeps a routine refresh from overriding a user's choice.
+   */
+  private refreshWarehouses(): void {
+    this.warehouseRefreshRequested$.next();
+  }
+
+  /**
+   * Mirrors the CU preselection for warehouses (#7817): 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.manualWarehousePick &&
+      this.selectedWarehouseId !== undefined &&
+      this.warehouses.some(warehouse => warehouse.whid === 
this.selectedWarehouseId)
+    ) {
+      return;
+    }
+    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.manualWarehousePick = true;
+    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 — as explicit a choice as a 
pick.
+    this.manualWarehousePick = true;
+    // Appended locally, as the dashboard tab does: the backend lists by
+    // created_at ascending, so no round trip is needed to keep the order.
+    this.warehouses = [...this.warehouses, warehouse];
+    this.warehouseService.selectWarehouse(warehouse.whid);
+  }
+
+  confirmDeleteWarehouse(warehouse: DashboardWarehouse): void {
+    this.warehouseActionsService.confirmAndDelete(warehouse, () => {
+      this.warehouses = this.warehouses.filter(entry => entry.whid !== 
warehouse.whid);
+      // Deleting the picked warehouse leaves the pick dangling; the preselect
+      // moves it (its manual-pick guard no longer holds for a gone id).
+      if (this.selectedWarehouseId === warehouse.whid) {
+        this.applyWarehousePreselect();
+      }
+    });
+  }
+
   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 6b1b510634..c097fbf5a1 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
@@ -32,6 +32,9 @@ import { StubOperatorMetadataService } from 
"../operator-metadata/stub-operator-
 import { JointUIService } from "../joint-ui/joint-ui.service";
 import { of, Subject } from "rxjs";
 import { WorkflowWebsocketService } from 
"../workflow-websocket/workflow-websocket.service";
+import { WorkflowStatusService } from 
"../workflow-status/workflow-status.service";
+import { NotificationService } from 
"../../../common/service/notification/notification.service";
+import { GuiConfigService } from "../../../common/service/gui-config.service";
 
 import { mockLogicalPlan_scan_result, mockWorkflowPlan_scan_result } from 
"./mock-workflow-plan";
 import { HttpClientTestingModule } from "@angular/common/http/testing";
@@ -39,6 +42,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 +401,64 @@ describe("ExecuteWorkflowService", () => {
     );
   }));
 
+  it("a refused run leaves the previous execution's state untouched (#7817)", 
() => {
+    TestBed.inject(GuiConfigService).env.warehouseEnabled = true;
+    try {
+      TestBed.inject(WarehouseService).selectWarehouse(undefined);
+      const resetSpy = vi.spyOn(service, "resetExecutionState");
+      const statusResetSpy = vi.spyOn(TestBed.inject(WorkflowStatusService), 
"resetStatus");
+      vi.spyOn(TestBed.inject(NotificationService), 
"error").mockReturnValue(undefined as never);
+
+      service.executeWorkflowWithEmailNotification("exec", false);
+
+      expect(resetSpy).not.toHaveBeenCalled();
+      expect(statusResetSpy).not.toHaveBeenCalled();
+    } finally {
+      TestBed.inject(GuiConfigService).env.warehouseEnabled = false;
+    }
+  });
+
+  it("refuses to run without a warehouse while the deployment requires one 
(#7817)", fakeAsync(() => {
+    // Paths that bypass the menu gate (form view, run-up-to, replay) all 
funnel
+    // through sendExecutionRequest; the shared storage must not catch them.
+    TestBed.inject(GuiConfigService).env.warehouseEnabled = true;
+    try {
+      TestBed.inject(WarehouseService).selectWarehouse(undefined);
+      const wsSendSpy = vi.spyOn(service["workflowWebsocketService"], "send");
+      const errorSpy = vi.spyOn(TestBed.inject(NotificationService), 
"error").mockReturnValue(undefined as never);
+
+      service.executeWorkflowWithEmailNotification("exec", false);
+      tick(FORM_DEBOUNCE_TIME_MS + 1);
+      flush();
+
+      expect(wsSendSpy).not.toHaveBeenCalledWith("WorkflowExecuteRequest", 
expect.anything());
+      expect(errorSpy).toHaveBeenCalledWith("Create or select a warehouse 
before running.");
+    } finally {
+      TestBed.inject(GuiConfigService).env.warehouseEnabled = false;
+    }
+  }));
+
+  it("sendExecutionRequest carries the picked warehouse id, and none when 
unset (#7817)", 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 c2ab3eac0d..3f9f5c53a1 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,8 @@ 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";
+import { GuiConfigService } from "../../../common/service/gui-config.service";
 
 // TODO: change this declaration
 export const FORM_DEBOUNCE_TIME_MS = 150;
@@ -100,7 +102,9 @@ export class ExecuteWorkflowService {
     private workflowStatusService: WorkflowStatusService,
     private notificationService: NotificationService,
     @Inject(DOCUMENT) private document: Document,
-    private computingUnitStatusService: ComputingUnitStatusService
+    private computingUnitStatusService: ComputingUnitStatusService,
+    private warehouseService: WarehouseService,
+    private config: GuiConfigService
   ) {
     workflowWebsocketService.websocketEvent().subscribe(event => {
       switch (event.type) {
@@ -207,6 +211,9 @@ export class ExecuteWorkflowService {
       targetOperatorId
     );
     const settings = this.workflowActionService.getWorkflowSettings();
+    if (this.refuseToRunWithoutWarehouse()) {
+      return;
+    }
     this.resetExecutionState();
     this.workflowStatusService.resetStatus();
     this.sendExecutionRequest(executionName, logicalPlan, settings, 
emailNotificationEnabled);
@@ -219,6 +226,9 @@ export class ExecuteWorkflowService {
   public executeWorkflowWithReplay(replayExecutionInfo: ReplayExecutionInfo): 
void {
     const logicalPlan = 
ExecuteWorkflowService.getLogicalPlanRequest(this.workflowActionService.getTexeraGraph());
     const settings = this.workflowActionService.getWorkflowSettings();
+    if (this.refuseToRunWithoutWarehouse()) {
+      return;
+    }
     this.resetExecutionState();
     this.workflowStatusService.resetStatus();
     this.sendExecutionRequest(
@@ -230,6 +240,21 @@ export class ExecuteWorkflowService {
     );
   }
 
+  /**
+   * While the deployment requires a warehouse (#7817) and none is picked,
+   * refuses with a toast and returns true. Checked at every public entry
+   * point before it resets the previous execution's state — a refused click
+   * must not wipe the results already on screen (#7751 adds the backend-side
+   * rejection).
+   */
+  private refuseToRunWithoutWarehouse(): boolean {
+    if (!this.config.env.warehouseEnabled || 
this.warehouseService.getSelectedWarehouseIdValue() !== undefined) {
+      return false;
+    }
+    this.notificationService.error("Create or select a warehouse before 
running.");
+    return true;
+  }
+
   public sendExecutionRequest(
     executionName: string,
     logicalPlan: LogicalPlan,
@@ -241,6 +266,11 @@ export class ExecuteWorkflowService {
     const selectedUnit = 
this.computingUnitStatusService.getSelectedComputingUnitValue();
     const computingUnitId = selectedUnit?.computingUnit.cuid;
 
+    // The warehouse this execution writes to (#7817); 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 +284,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(() => {

Reply via email to