diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/service/ResultExportService.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/service/ResultExportService.scala index 87b4d949a34..783505424f4 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/service/ResultExportService.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/service/ResultExportService.scala @@ -1,6 +1,7 @@ package edu.uci.ics.texera.web.service import java.io.{ByteArrayInputStream, ByteArrayOutputStream} +import java.nio.charset.StandardCharsets import java.util import java.util.concurrent.{Executors, ThreadPoolExecutor} import com.github.tototoshi.csv.CSVWriter @@ -74,8 +75,8 @@ class ResultExportService(opResultStorage: OpResultStorage, wId: UInteger) { handleGoogleSheetRequest(cache, request, results, attributeNames) case "csv" => handleCSVRequest(user, request, results, attributeNames) - case "binary" => - handleBinaryRequest(user, request, results) + case "data" => + handleDataRequest(user, request, results) case _ => ResultExportResponse("error", s"Unknown export type: ${request.exportType}") } @@ -176,7 +177,7 @@ class ResultExportService(opResultStorage: OpResultStorage, wId: UInteger) { targetSheet.getSpreadsheetId } - private def handleBinaryRequest( + private def handleDataRequest( user: User, request: ResultExportRequest, results: Iterable[Tuple] @@ -191,47 +192,32 @@ class ResultExportService(opResultStorage: OpResultStorage, wId: UInteger) { } val selectedRow = results.toSeq(rowIndex) - val field: Any = selectedRow.getField(columnIndex) - // Ensure the field is of type byte[] - val binaryData: Array[Byte] = field match { + // Convert the field to a byte array, regardless of its type + val dataBytes: Array[Byte] = field match { case data: Array[Byte] => data - case data: AnyRef - if data.getClass.isArray && data.getClass.getComponentType == classOf[Byte] => - data.asInstanceOf[Array[Byte]] - case _ => - return ResultExportResponse( - "error", - s"Expected binary data (Array[Byte]), but got: ${field.getClass}" - ) + case data: String => data.getBytes(StandardCharsets.UTF_8) + case data => data.toString.getBytes(StandardCharsets.UTF_8) } - // Save the binary file (similar to how files are saved in the CSV handler) - binaryData match { - case data: Array[Byte] => - val byteArray = data - val fileStream = new ByteArrayInputStream(byteArray) - - // Save the binary file - request.datasetIds.foreach { did => - val datasetPath = PathUtils.getDatasetPath(UInteger.valueOf(did)) - val filePath = datasetPath.resolve(filename) - createNewDatasetVersionByAddingFiles( - UInteger.valueOf(did), - user, - Map(filePath -> fileStream) - ) - } - - ResultExportResponse( - "success", - s"Binary file $filename saved to Datasets ${request.datasetIds.mkString(",")}" - ) + // Save the data file + val fileStream = new ByteArrayInputStream(dataBytes) - case _ => - ResultExportResponse("error", s"Selected field is not binary data") + request.datasetIds.foreach { did => + val datasetPath = PathUtils.getDatasetPath(UInteger.valueOf(did)) + val filePath = datasetPath.resolve(filename) + createNewDatasetVersionByAddingFiles( + UInteger.valueOf(did), + user, + Map(filePath -> fileStream) + ) } + + ResultExportResponse( + "success", + s"Data file $filename saved to Datasets ${request.datasetIds.mkString(",")}" + ) } /** diff --git a/core/gui/src/app/workspace/component/result-exportation/result-exportation.component.html b/core/gui/src/app/workspace/component/result-exportation/result-exportation.component.html index 092dd357b0b..1fe8284b2e0 100644 --- a/core/gui/src/app/workspace/component/result-exportation/result-exportation.component.html +++ b/core/gui/src/app/workspace/component/result-exportation/result-exportation.component.html @@ -1,6 +1,6 @@
Filename diff --git a/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html b/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html index 4ee60ebe4ce..5b9711eeae8 100644 --- a/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html +++ b/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html @@ -98,25 +98,25 @@
- + + (click)="open(i, row)"> + {{ column.getCell(row) }} - {{ column.getCell(row) }} diff --git a/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.scss b/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.scss index af1d072c066..ed2883e9690 100644 --- a/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.scss +++ b/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.scss @@ -60,3 +60,43 @@ th.header-size { background-color: rgba(0, 0, 0, 0.05); } } + +.table-cell { + position: relative; + padding-right: 28px; +} + +.cell-content { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.download-button { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + opacity: 0; + transition: opacity 0.2s ease-in-out; + padding: 4px; + background: transparent; + border: none; + cursor: pointer; + + i { + font-size: 16px; // Slightly larger to match the cloud icon + color: #1890ff; // Ant Design's primary blue color + } +} + +.table-row-hover:hover { + .download-button { + opacity: 0.7; + + &:hover { + opacity: 1; + } + } +} diff --git a/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts b/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts index fd58ba433eb..17dccdc0886 100644 --- a/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts +++ b/core/gui/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts @@ -358,21 +358,14 @@ export class ResultTableFrameComponent implements OnInit, OnChanges { return trimAndFormatData(cellContent, TABLE_COLUMN_TEXT_LIMIT); } - isBinaryData(cellContent: any): boolean { - if (typeof cellContent === "string") { - return isBase64(cellContent) || isBinary(cellContent); - } - return false; - } - - downloadBinaryData(binaryData: any, rowIndex: number, columnIndex: number, columnName: string): void { + downloadData(data: any, rowIndex: number, columnIndex: number, columnName: string): void { const realRowNumber = (this.currentPageIndex - 1) * this.pageSize + rowIndex; const defaultFileName = `${columnName}_${realRowNumber}`; const modal = this.modalService.create({ - nzTitle: "Export Binary Data and Save to a Dataset", + nzTitle: "Export Data and Save to a Dataset", nzContent: ResultExportationComponent, nzData: { - exportType: "binary", + exportType: "data", workflowName: this.workflowActionService.getWorkflowMetadata.name, defaultFileName: defaultFileName, rowIndex: realRowNumber,