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