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}`;