From 0698dc6707a9ca9132839dc3379d5f52235256b5 Mon Sep 17 00:00:00 2001 From: Yang Zhang Date: Fri, 11 Sep 2026 16:46:39 -0700 Subject: [PATCH] fix(frontend): disable the per-cell result download when result export is switched off `export-execution-result-enabled` is a deployment switch. The top menu and the context menu already honour it, but the download button on every result cell rendered regardless, and clicking it returned silently from WorkflowResultExportService.performExport: no request, no message. A reader could not tell whether the feature was broken or off. Bind the button's `disabled` state to the switch and say why in its title, the same way the menu entry is disabled. The stylesheet hardcodes the icon color and cursor, so the disabled state also gets its own look; without it the disabled button was indistinguishable from the live one. Tests cover both states through the MockGuiConfigService that the component actually receives, and the existing click test now asserts it clicks a live button. Closes #8538 Co-Authored-By: Claude Fable 5.1 --- .../result-table-frame.component.html | 3 +- .../result-table-frame.component.scss | 10 +++++ .../result-table-frame.component.spec.ts | 37 +++++++++++++++++++ .../result-table-frame.component.ts | 10 +++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html index e9c5f83ee4d..e130f5ed813 100644 --- a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html +++ b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html @@ -198,7 +198,8 @@
+ [disabled]="!exportEnabled" + [title]="exportEnabled ? 'Download data' : 'Result export is disabled on this deployment'"> diff --git a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.scss b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.scss index 6326b83eb2c..15eda0957c7 100644 --- a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.scss +++ b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.scss @@ -115,6 +115,16 @@ th.header-size { font-size: 16px; // Slightly larger to match the cloud icon color: #1890ff; // Ant Design's primary blue color } + + // Disabled when result export is switched off. The icon color and cursor above are + // hardcoded, so without this the disabled button would look exactly like the live one. + &[disabled] { + cursor: not-allowed; + + i { + color: rgba(0, 0, 0, 0.25); // Ant Design's disabled text color + } + } } .table-row-hover:hover { diff --git a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts index d4c9e85b085..9e3df191f45 100644 --- a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts +++ b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts @@ -31,6 +31,7 @@ import { By, DomSanitizer } from "@angular/platform-browser"; import { of, Subject } from "rxjs"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; +import { MockGuiConfigService } from "../../../../common/service/gui-config.service.mock"; import { isAudioUrl, isImageUrl, isVideoUrl } from "../../../../common/util/media-type.util"; import { OperatorPaginationResultService, @@ -696,6 +697,11 @@ describe("ResultTableFrameComponent", () => { }); it("renders headers, per-column stats, and clickable row cells once results arrive", () => { + // The download click below must land on a live button: MockGuiConfigService ships with the + // export switch off, which would disable it. + (TestBed.inject(GuiConfigService) as unknown as MockGuiConfigService).setConfig({ + exportExecutionResultEnabled: true, + }); component.operatorId = "op1"; component.setupResultTable([SAMPLE_ROW], 1); component.isFrontPagination = false; @@ -741,11 +747,42 @@ describe("ResultTableFrameComponent", () => { expect(openSpy).toHaveBeenCalledWith(0, SAMPLE_ROW); const download = fixture.debugElement.query(By.css("button.download-button")); + expect((download.nativeElement as HTMLButtonElement).disabled).toBe(false); download.triggerEventHandler("click", { stopPropagation: vi.fn() }); expect(downloadSpy).toHaveBeenCalledWith("alice", 0, 0, "name"); }); }); + describe("per-cell download button and the export switch", () => { + // `commonTestProviders` supplies MockGuiConfigService (export switch off by default), so the + // switch is driven through it rather than through the `useValue` above. + function renderOneRow(exportEnabled: boolean): void { + (TestBed.inject(GuiConfigService) as unknown as MockGuiConfigService).setConfig({ + exportExecutionResultEnabled: exportEnabled, + }); + component.operatorId = "op1"; + component.setupResultTable([SAMPLE_ROW], 1); + component.isFrontPagination = false; + fixture.detectChanges(); + } + + it("is enabled and offers a download when result export is on", () => { + renderOneRow(true); + const button = fixture.debugElement.query(By.css("button.download-button")).nativeElement as HTMLButtonElement; + expect(button.disabled).toBe(false); + expect(button.title).toBe("Download data"); + }); + + it("is disabled and says why when result export is switched off", () => { + // The top menu and the context menu already honour the switch; before this the cell + // button rendered anyway and a click did nothing, with no request and no message. + renderOneRow(false); + const button = fixture.debugElement.query(By.css("button.download-button")).nativeElement as HTMLButtonElement; + expect(button.disabled).toBe(true); + expect(button.title).toBe("Result export is disabled on this deployment"); + }); + }); + it("should detect media URLs for result cells", () => { expect(component.isVideoCell("https://example.com/clip.mp4")).toBe(true); expect(component.isAudioCell("https://example.com/sound.wav")).toBe(true); diff --git a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts index e46fa49f51d..da8095cd235 100644 --- a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts +++ b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts @@ -463,6 +463,16 @@ export class ResultTableFrameComponent implements OnInit, OnChanges { })); } + /** + * Result export is a deployment switch (`export-execution-result-enabled`). The top menu and + * the context menu already honour it; the per-cell download button did not -- it rendered + * regardless, and clicking it returned silently from the export service with no request and + * no message. Disable it here so the reader sees why nothing happens. + */ + get exportEnabled(): boolean { + return this.guiConfigService.env.exportExecutionResultEnabled === true; + } + downloadData(data: any, rowIndex: number, columnIndex: number, columnName: string): void { const realRowNumber = (this.currentPageIndex - 1) * this.pageSize + rowIndex; const defaultFileName = `${columnName}_${realRowNumber}`;