Skip to content

Commit 29503ea

Browse files
aicamMA77HEW820
authored andcommitted
Move export to HTTP (#3221)
This PR is part of the effort to move download functionality from front end to backend. ## New endpoint: /export/result Previously, `ResultExportService` was called from `WorkflowWebsocketResource`, with this PR, it is now called from a new endpoint defined in `ResultExportResource`. To unify export to dataset and export to local (download), we need to have an endpoint to remove front end from the process. Currently front end fetch all the result and then send them to the user but the goal is to use this new endpoint to stream result directly from backend to user. ## Current behavior The new endpoint only provides export to dataset currently. In code, it supports multiple formats but in front end, we only allow two formats: CSV and Arrow (for binary files). This limitation is based on the previous implementation and might be revised. ## Future work There are four main TODOs left in this PR: - Use `rowIndex` and `columnIndex` in frontend because already available in backend - Request multiple operators result in one HTTP request - Adjust endpoint to return the file itself if the export destination is local - Adjust endpoint to return a zip file if the export destination is local and multiple operators are selected https://github.com/user-attachments/assets/a86eb3e5-8550-4d56-b86f-a5805cc10ffe
1 parent 2eecfa7 commit 29503ea

File tree

7 files changed

+123
-20
lines changed

7 files changed

+123
-20
lines changed

core/amber/src/main/scala/edu/uci/ics/texera/web/TexeraWebApplication.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ class TexeraWebApplication
135135
environment.jersey.register(classOf[PublicProjectResource])
136136
environment.jersey.register(classOf[WorkflowAccessResource])
137137
environment.jersey.register(classOf[WorkflowResource])
138+
environment.jersey.register(classOf[ResultResource])
138139
environment.jersey.register(classOf[HubWorkflowResource])
139140
environment.jersey.register(classOf[WorkflowVersionResource])
140141
environment.jersey.register(classOf[DatasetResource])

core/amber/src/main/scala/edu/uci/ics/texera/web/model/websocket/request/ResultExportRequest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ case class ResultExportRequest(
66
workflowName: String,
77
operatorId: String,
88
operatorName: String,
9-
datasetIds: Array[Int],
9+
datasetIds: List[Int],
1010
rowIndex: Int,
1111
columnIndex: Int,
1212
filename: String
13-
) extends TexeraWebSocketRequest
13+
)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package edu.uci.ics.texera.web.resource
2+
3+
import com.typesafe.scalalogging.LazyLogging
4+
import edu.uci.ics.amber.core.virtualidentity.WorkflowIdentity
5+
import edu.uci.ics.texera.web.auth.SessionUser
6+
import edu.uci.ics.texera.web.model.websocket.request.ResultExportRequest
7+
import edu.uci.ics.texera.web.model.websocket.response.ResultExportResponse
8+
import edu.uci.ics.texera.web.service.{ResultExportService, WorkflowService}
9+
import io.dropwizard.auth.Auth
10+
11+
import javax.ws.rs._
12+
import javax.ws.rs.core.Response
13+
import scala.jdk.CollectionConverters._
14+
15+
@Path("/result")
16+
class ResultResource extends LazyLogging {
17+
18+
@POST
19+
@Path("/export")
20+
def exportResult(
21+
request: ResultExportRequest,
22+
@Auth user: SessionUser
23+
): Response = {
24+
25+
try {
26+
val resultExportService = new ResultExportService(WorkflowIdentity(request.workflowId))
27+
28+
val exportResponse: ResultExportResponse =
29+
resultExportService.exportResult(user.user, request)
30+
31+
Response.ok(exportResponse).build()
32+
33+
} catch {
34+
case ex: Exception =>
35+
Response
36+
.status(Response.Status.INTERNAL_SERVER_ERROR)
37+
.entity(Map("error" -> ex.getMessage).asJava)
38+
.build()
39+
}
40+
}
41+
42+
}

core/amber/src/main/scala/edu/uci/ics/texera/web/resource/WorkflowWebsocketResource.scala

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,6 @@ class WorkflowWebsocketResource extends LazyLogging {
6464
workflowStateOpt.foreach(state =>
6565
sessionState.send(state.resultService.handleResultPagination(paginationRequest))
6666
)
67-
case resultExportRequest: ResultExportRequest =>
68-
workflowStateOpt.foreach(state =>
69-
sessionState.send(state.exportService.exportResult(userOpt.get, resultExportRequest))
70-
)
7167
case modifyLogicRequest: ModifyLogicRequest =>
7268
if (workflowStateOpt.isDefined) {
7369
val executionService = workflowStateOpt.get.executionService.getValue

core/gui/src/app/dashboard/service/user/download/download.service.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import { Observable, throwError, of, forkJoin, from } from "rxjs";
33
import { map, tap, catchError, switchMap } from "rxjs/operators";
44
import { FileSaverService } from "../file/file-saver.service";
55
import { NotificationService } from "../../../../common/service/notification/notification.service";
6-
import { DatasetService } from "../dataset/dataset.service";
6+
import { DATASET_BASE_URL, DatasetService } from "../dataset/dataset.service";
77
import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service";
88
import * as JSZip from "jszip";
99
import { Workflow } from "../../../../common/type/workflow";
10+
import { AppSettings } from "../../../../common/app-setting";
11+
import { HttpClient } from "@angular/common/http";
12+
13+
export const EXPORT_BASE_URL = "result/export";
1014

1115
interface DownloadableItem {
1216
blob: Blob;
@@ -21,7 +25,8 @@ export class DownloadService {
2125
private fileSaverService: FileSaverService,
2226
private notificationService: NotificationService,
2327
private datasetService: DatasetService,
24-
private workflowPersistService: WorkflowPersistService
28+
private workflowPersistService: WorkflowPersistService,
29+
private http: HttpClient
2530
) {}
2631

2732
downloadWorkflow(id: number, name: string): Observable<DownloadableItem> {
@@ -83,6 +88,42 @@ export class DownloadService {
8388
);
8489
}
8590

91+
public exportWorkflowResult(
92+
exportType: string,
93+
workflowId: number,
94+
workflowName: string,
95+
operatorId: string,
96+
operatorName: string,
97+
datasetIds: number[],
98+
rowIndex: number,
99+
columnIndex: number,
100+
filename: string
101+
): Observable<any> {
102+
const requestBody = {
103+
exportType,
104+
workflowId,
105+
workflowName,
106+
operatorId,
107+
operatorName,
108+
datasetIds,
109+
rowIndex,
110+
columnIndex,
111+
filename,
112+
};
113+
114+
/*
115+
TODO: curently, the response is json because the backend does not return a file and export
116+
the result into the database. Next, we will implement download feature (export to local).
117+
*/
118+
return this.http.post(`${AppSettings.getApiEndpoint()}/${EXPORT_BASE_URL}`, requestBody, {
119+
responseType: "json",
120+
headers: {
121+
"Content-Type": "application/json",
122+
Accept: "application/json",
123+
},
124+
});
125+
}
126+
86127
downloadOperatorsResult(
87128
resultObservables: Observable<{ filename: string; blob: Blob }[]>[],
88129
workflow: Workflow

core/gui/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@
120120
</li>
121121

122122
<li
123-
*ngIf="workflowResultExportService.hasResultToExportOnHighlightedOperators"
123+
*ngIf="workflowResultExportService.hasResultToExportOnHighlightedOperators &&
124+
this.workflowResultExportService.exportExecutionResultEnabled"
124125
(click)="onClickExportHighlightedExecutionResult()"
125126
nz-menu-item>
126127
<span

core/gui/src/app/workspace/service/workflow-result-export/workflow-result-export.service.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -157,21 +157,43 @@ export class WorkflowResultExportService {
157157
this.notificationService.loading("exporting...");
158158
operatorIds.forEach(operatorId => {
159159
if (!this.workflowResultService.hasAnyResult(operatorId)) {
160+
console.log(`Operator ${operatorId} has no result to export`);
160161
return;
161162
}
162163
const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorId);
163164
const operatorName = operator.customDisplayName ?? operator.operatorType;
164-
this.workflowWebsocketService.send("ResultExportRequest", {
165-
exportType,
166-
workflowId,
167-
workflowName,
168-
operatorId,
169-
operatorName,
170-
datasetIds,
171-
rowIndex,
172-
columnIndex,
173-
filename,
174-
});
165+
166+
/*
167+
* This function (and service) was previously used to export result
168+
* into the local file system (downloading). Currently it is used to only
169+
* export to the dataset.
170+
* TODO: refactor this service to have export namespace and download should be
171+
* an export type (export to local file system)
172+
* TODO: rowIndex and columnIndex can be used to export a specific cells in the result
173+
*/
174+
this.downloadService
175+
.exportWorkflowResult(
176+
exportType,
177+
workflowId,
178+
workflowName,
179+
operatorId,
180+
operatorName,
181+
[...datasetIds],
182+
rowIndex,
183+
columnIndex,
184+
filename
185+
)
186+
.subscribe({
187+
next: _ => {
188+
this.notificationService.info("The result has been exported successfully");
189+
},
190+
error: (res: unknown) => {
191+
const errorResponse = res as { error: { error: string } };
192+
this.notificationService.error(
193+
"An error happened in exporting operator results " + errorResponse.error.error
194+
);
195+
},
196+
});
175197
});
176198
}
177199

0 commit comments

Comments
 (0)