Skip to content

Commit

Permalink
Move export to HTTP (#3221)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
aicam authored Jan 26, 2025
1 parent ca68ad6 commit 5e7568e
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ class TexeraWebApplication
environment.jersey.register(classOf[PublicProjectResource])
environment.jersey.register(classOf[WorkflowAccessResource])
environment.jersey.register(classOf[WorkflowResource])
environment.jersey.register(classOf[ResultResource])
environment.jersey.register(classOf[HubWorkflowResource])
environment.jersey.register(classOf[WorkflowVersionResource])
environment.jersey.register(classOf[DatasetResource])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ case class ResultExportRequest(
workflowName: String,
operatorId: String,
operatorName: String,
datasetIds: Array[Int],
datasetIds: List[Int],
rowIndex: Int,
columnIndex: Int,
filename: String
) extends TexeraWebSocketRequest
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package edu.uci.ics.texera.web.resource

import com.typesafe.scalalogging.LazyLogging
import edu.uci.ics.amber.core.virtualidentity.WorkflowIdentity
import edu.uci.ics.texera.web.auth.SessionUser
import edu.uci.ics.texera.web.model.websocket.request.ResultExportRequest
import edu.uci.ics.texera.web.model.websocket.response.ResultExportResponse
import edu.uci.ics.texera.web.service.{ResultExportService, WorkflowService}
import io.dropwizard.auth.Auth

import javax.ws.rs._
import javax.ws.rs.core.Response
import scala.jdk.CollectionConverters._

@Path("/result")
class ResultResource extends LazyLogging {

@POST
@Path("/export")
def exportResult(
request: ResultExportRequest,
@Auth user: SessionUser
): Response = {

try {
val resultExportService = new ResultExportService(WorkflowIdentity(request.workflowId))

val exportResponse: ResultExportResponse =
resultExportService.exportResult(user.user, request)

Response.ok(exportResponse).build()

} catch {
case ex: Exception =>
Response
.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map("error" -> ex.getMessage).asJava)
.build()
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,6 @@ class WorkflowWebsocketResource extends LazyLogging {
workflowStateOpt.foreach(state =>
sessionState.send(state.resultService.handleResultPagination(paginationRequest))
)
case resultExportRequest: ResultExportRequest =>
workflowStateOpt.foreach(state =>
sessionState.send(state.exportService.exportResult(userOpt.get, resultExportRequest))
)
case modifyLogicRequest: ModifyLogicRequest =>
if (workflowStateOpt.isDefined) {
val executionService = workflowStateOpt.get.executionService.getValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { Observable, throwError, of, forkJoin, from } from "rxjs";
import { map, tap, catchError, switchMap } from "rxjs/operators";
import { FileSaverService } from "../file/file-saver.service";
import { NotificationService } from "../../../../common/service/notification/notification.service";
import { DatasetService } from "../dataset/dataset.service";
import { DATASET_BASE_URL, DatasetService } from "../dataset/dataset.service";
import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service";
import * as JSZip from "jszip";
import { Workflow } from "../../../../common/type/workflow";
import { AppSettings } from "../../../../common/app-setting";
import { HttpClient } from "@angular/common/http";

export const EXPORT_BASE_URL = "result/export";

interface DownloadableItem {
blob: Blob;
Expand All @@ -21,7 +25,8 @@ export class DownloadService {
private fileSaverService: FileSaverService,
private notificationService: NotificationService,
private datasetService: DatasetService,
private workflowPersistService: WorkflowPersistService
private workflowPersistService: WorkflowPersistService,
private http: HttpClient
) {}

downloadWorkflow(id: number, name: string): Observable<DownloadableItem> {
Expand Down Expand Up @@ -83,6 +88,42 @@ export class DownloadService {
);
}

public exportWorkflowResult(
exportType: string,
workflowId: number,
workflowName: string,
operatorId: string,
operatorName: string,
datasetIds: number[],
rowIndex: number,
columnIndex: number,
filename: string
): Observable<any> {
const requestBody = {
exportType,
workflowId,
workflowName,
operatorId,
operatorName,
datasetIds,
rowIndex,
columnIndex,
filename,
};

/*
TODO: curently, the response is json because the backend does not return a file and export
the result into the database. Next, we will implement download feature (export to local).
*/
return this.http.post(`${AppSettings.getApiEndpoint()}/${EXPORT_BASE_URL}`, requestBody, {
responseType: "json",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
}

downloadOperatorsResult(
resultObservables: Observable<{ filename: string; blob: Blob }[]>[],
workflow: Workflow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@
</li>

<li
*ngIf="workflowResultExportService.hasResultToExportOnHighlightedOperators"
*ngIf="workflowResultExportService.hasResultToExportOnHighlightedOperators &&
this.workflowResultExportService.exportExecutionResultEnabled"
(click)="onClickExportHighlightedExecutionResult()"
nz-menu-item>
<span
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,21 +157,43 @@ export class WorkflowResultExportService {
this.notificationService.loading("exporting...");
operatorIds.forEach(operatorId => {
if (!this.workflowResultService.hasAnyResult(operatorId)) {
console.log(`Operator ${operatorId} has no result to export`);
return;
}
const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorId);
const operatorName = operator.customDisplayName ?? operator.operatorType;
this.workflowWebsocketService.send("ResultExportRequest", {
exportType,
workflowId,
workflowName,
operatorId,
operatorName,
datasetIds,
rowIndex,
columnIndex,
filename,
});

/*
* This function (and service) was previously used to export result
* into the local file system (downloading). Currently it is used to only
* export to the dataset.
* TODO: refactor this service to have export namespace and download should be
* an export type (export to local file system)
* TODO: rowIndex and columnIndex can be used to export a specific cells in the result
*/
this.downloadService
.exportWorkflowResult(
exportType,
workflowId,
workflowName,
operatorId,
operatorName,
[...datasetIds],
rowIndex,
columnIndex,
filename
)
.subscribe({
next: _ => {
this.notificationService.info("The result has been exported successfully");
},
error: (res: unknown) => {
const errorResponse = res as { error: { error: string } };
this.notificationService.error(
"An error happened in exporting operator results " + errorResponse.error.error
);
},
});
});
}

Expand Down

0 comments on commit 5e7568e

Please sign in to comment.