This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-8592-2ab8ee0f2859339458cd8f44659db989910b201d in repository https://gitbox.apache.org/repos/asf/texera.git
commit 5264df2861840bf8b1662ff2e1814b8aa80215db Author: Meng Wang <[email protected]> AuthorDate: Fri Sep 18 19:39:26 2026 +0000 fix(frontend): say the warehouse is missing on the form view's run button (#8592) ### What changes were proposed in this PR? The form view's run button enumerates every state a run cannot start from — invalid workflow, empty workflow, connecting, no computing unit, a read-only unit — so that, in its own words, "the reader is never sent to press a button that does nothing". The warehouse requirement added in #8551 was missing from that list: with the feature enabled and nothing picked, the button read `Run` and looked ready, and the click was refused deeper down in `ExecuteWorkflowService`, with a toast. It now names what is missing, ordered after the computing unit as the canvas orders them, and stays disabled — the warehouse is picked in the embedded selector, the same reasoning the no-unit case already gives. The condition it reads is the one `ExecuteWorkflowService` refuses on — the boot-time flag and the picked warehouse, both root-scoped — so the button predicts that refusal exactly. Flag off (the default): `warehouseRequiredButMissing` is never true, so the button behaves exactly as before. ### Any related issues, documentation, discussions? Closes #8591. Follows #8551. Part of #6870. ### How was this PR tested? - Two Vitest cases added: the button names the missing warehouse and stays disabled, and returns to `Run` once one is picked. The workspace suite passes in full: 3324 tests. - Failure path verified rather than assumed: the new case was removed on purpose and the suite confirmed to fail for the expected reason before being restored. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (claude-opus-5, claude-fable-5) --- .../workflow-form/workflow-form.component.spec.ts | 31 +++++++++++++++++++++- .../workflow-form/workflow-form.component.ts | 15 ++++++++++- .../workflow-form/workflow-form.spec-harness.ts | 10 ++++++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/workspace/component/workflow-form/workflow-form.component.spec.ts b/frontend/src/app/workspace/component/workflow-form/workflow-form.component.spec.ts index 50edda7f4b..0c3e5e1f8a 100644 --- a/frontend/src/app/workspace/component/workflow-form/workflow-form.component.spec.ts +++ b/frontend/src/app/workspace/component/workflow-form/workflow-form.component.spec.ts @@ -71,7 +71,8 @@ describe("WorkflowFormComponent", () => { h.datePipe as any, h.panelResizeService as any, h.validationWorkflowService as any, - h.config as any + h.config as any, + h.warehouseService as any ); return component; }; @@ -1506,6 +1507,34 @@ describe("WorkflowFormComponent", () => { expect(component.runButtonState).toEqual({ label: "Computing Unit", icon: "plus-circle", disabled: true }); }); + it("names the missing warehouse instead of offering a run that would be refused (#8591)", () => { + build(formViewWorkflow).ngOnInit(); + makeReady(); + h.config.env.warehouseEnabled = true; + h.warehouseService.selectWarehouse(undefined); + + expect(component.runButtonState).toEqual({ label: "Warehouse", icon: "plus-circle", disabled: true }); + }); + + it("says 'No access' before the warehouse, since picking one would not unblock a reader", () => { + build(formViewWorkflow).ngOnInit(); + makeReady(); + (component as any).selectedUnit = { accessPrivilege: "READ" }; + h.config.env.warehouseEnabled = true; + h.warehouseService.selectWarehouse(undefined); + + expect(component.runButtonState).toEqual({ label: "No access", icon: "lock", disabled: true }); + }); + + it("runs once a warehouse is picked", () => { + build(formViewWorkflow).ngOnInit(); + makeReady(); + h.config.env.warehouseEnabled = true; + h.warehouseService.selectWarehouse(7); + + expect(component.runButtonState).toEqual({ label: "Run", icon: "caret-right", disabled: false }); + }); + it("offers Run once a unit is up and the graph is valid", () => { build(formViewWorkflow).ngOnInit(); makeReady(); diff --git a/frontend/src/app/workspace/component/workflow-form/workflow-form.component.ts b/frontend/src/app/workspace/component/workflow-form/workflow-form.component.ts index 2b9433c482..0c3c182b12 100644 --- a/frontend/src/app/workspace/component/workflow-form/workflow-form.component.ts +++ b/frontend/src/app/workspace/component/workflow-form/workflow-form.component.ts @@ -53,6 +53,7 @@ import { FormBindingService, ResolvedField } from "../../service/form-binding/fo import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { ValidationWorkflowService } from "../../service/validation/validation-workflow.service"; import { GuiConfigService } from "../../../common/service/gui-config.service"; +import { WarehouseService } from "../../../common/service/warehouse/warehouse.service"; import { WorkflowConsoleService } from "../../service/workflow-console/workflow-console.service"; import { WorkflowResultService } from "../../service/workflow-result/workflow-result.service"; import { PanelResizeService } from "../../service/workflow-result/panel-resize/panel-resize.service"; @@ -299,7 +300,8 @@ export class WorkflowFormComponent implements OnInit, OnDestroy { // Same source the operator canvas reads its "Invalid" / "Empty" states from, so Run is // disabled here exactly when it is disabled there. private validationWorkflowService: ValidationWorkflowService, - private config: GuiConfigService + private config: GuiConfigService, + private warehouseService: WarehouseService ) {} ngOnInit(): void { @@ -1526,6 +1528,12 @@ export class WorkflowFormComponent implements OnInit, OnDestroy { ); } + /** The exact condition ExecuteWorkflowService refuses a run on, so the button can say it first + * instead of starting nothing and explaining in a toast. */ + public get hasNoWarehouse(): boolean { + return this.config.env.warehouseEnabled && this.warehouseService.getSelectedWarehouseIdValue() === undefined; + } + /** No unit chosen yet: the button names what is missing and stays disabled, because the unit is * picked in the embedded selector -- unlike the canvas, where that button is itself the click * target for creating one. */ @@ -1573,6 +1581,11 @@ export class WorkflowFormComponent implements OnInit, OnDestroy { if (!this.hasUnitWriteAccess) { return { label: "No access", icon: "lock", disabled: true }; } + // Last of the blocked states: picking a warehouse is the only one of them the reader can act + // on here, so naming it earlier would send a reader without write access to fix the wrong thing. + if (this.hasNoWarehouse) { + return { label: "Warehouse", icon: "plus-circle", disabled: true }; + } return { label: "Run", icon: "caret-right", disabled: false }; } diff --git a/frontend/src/app/workspace/component/workflow-form/workflow-form.spec-harness.ts b/frontend/src/app/workspace/component/workflow-form/workflow-form.spec-harness.ts index dc5cb7609c..9ca8be1001 100644 --- a/frontend/src/app/workspace/component/workflow-form/workflow-form.spec-harness.ts +++ b/frontend/src/app/workspace/component/workflow-form/workflow-form.spec-harness.ts @@ -308,7 +308,14 @@ export function setupHarness() { // default so a rebuild is never suppressed, and overridden by the tests that probe typing. const host = { nativeElement: { querySelector: () => null, contains: () => false } }; const datePipe = { transform: () => "01/01/2026 00:00:00" }; - const config = { env: { formViewEnabled: true } }; + const config = { env: { formViewEnabled: true, warehouseEnabled: false } }; + // The run button asks the same pair ExecuteWorkflowService refuses on: the flag above and the + // pick below. + let selectedWarehouseId: number | undefined = undefined; + const warehouseService = { + getSelectedWarehouseIdValue: () => selectedWarehouseId, + selectWarehouse: (whid: number | undefined) => (selectedWarehouseId = whid), + }; // Point the persist mock at `workflow`; each spec supplies the remaining constructor // arguments in its own order via the named mocks above. @@ -343,6 +350,7 @@ export function setupHarness() { host, datePipe, config, + warehouseService, workflowChangedStream, formBindingChanged, workflowMetaDataChangedStream,
