From 39fb4e301f7530548ab3c952d2885e7c788c9d38 Mon Sep 17 00:00:00 2001 From: Xuan Gu <162244362+xuang7@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:31:44 -0800 Subject: [PATCH 1/4] save... --- .../service/resource/DatasetResource.scala | 24 +++++++++++++ frontend/src/app/common/type/dataset.ts | 1 + .../user-dataset-file-renderer.component.ts | 3 ++ .../user-dataset-version-creator.component.ts | 1 + ...er-dataset-version-filetree.component.html | 15 +++++++- ...er-dataset-version-filetree.component.scss | 2 +- ...user-dataset-version-filetree.component.ts | 19 ++++++++++- .../service/user/dataset/dataset.service.ts | 10 ++++++ .../src/app/dashboard/type/dashboard-entry.ts | 2 ++ .../browse-section.component.html | 2 +- .../browse-section.component.ts | 4 +++ sql/updates/16.sql | 34 +++++++++++++++++++ 12 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 sql/updates/16.sql diff --git a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala index 2a67440cf0e..b49fa8cc476 100644 --- a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala +++ b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala @@ -1372,4 +1372,28 @@ class DatasetResource { Right(response) } } + + @POST + @RolesAllowed(Array("REGULAR", "ADMIN")) + @Path("/{did}/update/cover") + @Consumes(Array(MediaType.TEXT_PLAIN)) + def updateDatasetCoverImage( + @PathParam("did") did: Integer, + coverImage: String, + @Auth sessionUser: SessionUser + ): Response = { + withTransaction(context) { ctx => + val uid = sessionUser.getUid + val datasetDao = new DatasetDao(ctx.configuration()) + val dataset = getDatasetByID(ctx, did) + + if (!userHasWriteAccess(ctx, did, uid)) { + throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) + } + + dataset.setCoverImage(coverImage) + datasetDao.update(dataset) + Response.ok().build() + } + } } diff --git a/frontend/src/app/common/type/dataset.ts b/frontend/src/app/common/type/dataset.ts index 7825ca27976..97ff370302c 100644 --- a/frontend/src/app/common/type/dataset.ts +++ b/frontend/src/app/common/type/dataset.ts @@ -38,4 +38,5 @@ export interface Dataset { storagePath: string | undefined; description: string; creationTime: number | undefined; + coverImage: string | undefined; } diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts index c851a8284ea..576de576dff 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts @@ -131,6 +131,9 @@ export class UserDatasetFileRendererComponent implements OnInit, OnChanges, OnDe @Output() loadFile = new EventEmitter<{ file: string; prefix: string }>(); + @Output() + setCoverImage = new EventEmitter(); + constructor( private datasetService: DatasetService, private sanitizer: DomSanitizer, diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.ts index c1f9cffc352..1d59e851e34 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.ts @@ -174,6 +174,7 @@ export class UserDatasetVersionCreatorComponent implements OnInit { ownerUid: undefined, storagePath: undefined, creationTime: undefined, + coverImage: undefined, }; this.datasetService .createDataset(ds) diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.html b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.html index f09cdb37d4f..ee74c294410 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.html +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.html @@ -41,13 +41,26 @@ nz-button nzType="link" *ngIf="isTreeNodeDeletable && !node.data.children" - class="delete-button" + class="icon-button" (click)="onNodeDeleted(node.data)"> + + diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.scss b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.scss index edfaab61285..54cbcd44af4 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.scss +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.scss @@ -22,7 +22,7 @@ } /* Styles for the delete button */ -.delete-button { +.icon-button { width: 15px; margin-left: 5px; } diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts index f3e3e67e1af..cc0820ea544 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts @@ -19,7 +19,10 @@ import { UntilDestroy } from "@ngneat/until-destroy"; import { AfterViewInit, Component, EventEmitter, Input, Output, ViewChild } from "@angular/core"; -import { DatasetFileNode } from "../../../../../../common/type/datasetVersionFileTree"; +import { + DatasetFileNode, + getRelativePathFromDatasetFileNode, +} from "../../../../../../common/type/datasetVersionFileTree"; import { ITreeOptions, TREE_ACTIONS } from "@ali-hm/angular-tree-component"; @UntilDestroy() @@ -40,6 +43,9 @@ export class UserDatasetVersionFiletreeComponent implements AfterViewInit { @ViewChild("tree") tree: any; + @Output() + setCoverImage = new EventEmitter(); + public fileTreeDisplayOptions: ITreeOptions = { displayField: "name", hasChildrenField: "children", @@ -74,4 +80,15 @@ export class UserDatasetVersionFiletreeComponent implements AfterViewInit { this.tree.treeModel.expandAll(); } } + + isImageFile(fileName: string): boolean { + const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".webp"]; + return imageExts.some(ext => fileName.toLowerCase().endsWith(ext)); + } + + onSetCover(nodeData: DatasetFileNode): void { + const path = getRelativePathFromDatasetFileNode(nodeData); + console.log('Setting cover to:', path); + this.setCoverImage.emit(path); + } } diff --git a/frontend/src/app/dashboard/service/user/dataset/dataset.service.ts b/frontend/src/app/dashboard/service/user/dataset/dataset.service.ts index c09125d73b1..d5e458337ac 100644 --- a/frontend/src/app/dashboard/service/user/dataset/dataset.service.ts +++ b/frontend/src/app/dashboard/service/user/dataset/dataset.service.ts @@ -539,4 +539,14 @@ export class DatasetService { public retrieveOwners(): Observable { return this.http.get(`${AppSettings.getApiEndpoint()}/${DATASET_GET_OWNERS_URL}`); } + + public updateDatasetCoverImage(did: number, coverImagePath: string): Observable { + return this.http.post( + `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/update/cover`, + coverImagePath, + { + headers: { "Content-Type": "text/plain" }, + } + ); + } } diff --git a/frontend/src/app/dashboard/type/dashboard-entry.ts b/frontend/src/app/dashboard/type/dashboard-entry.ts index e526ea01bae..6dfb46cc1cd 100644 --- a/frontend/src/app/dashboard/type/dashboard-entry.ts +++ b/frontend/src/app/dashboard/type/dashboard-entry.ts @@ -48,6 +48,7 @@ export class DashboardEntry { likeCount: number; isLiked: boolean; accessibleUserIds: number[]; + coverImageUrl?: string; constructor(public value: DashboardWorkflow | DashboardProject | DashboardFile | DashboardDataset) { if (isDashboardWorkflow(value)) { @@ -122,6 +123,7 @@ export class DashboardEntry { this.likeCount = 0; this.isLiked = false; this.accessibleUserIds = []; + this.coverImageUrl = value.dataset.coverImage; } else { throw new Error("Unexpected type in DashboardEntry."); } diff --git a/frontend/src/app/hub/component/browse-section/browse-section.component.html b/frontend/src/app/hub/component/browse-section/browse-section.component.html index 3d7080e0eb7..71e308b5f89 100644 --- a/frontend/src/app/hub/component/browse-section/browse-section.component.html +++ b/frontend/src/app/hub/component/browse-section/browse-section.component.html @@ -44,7 +44,7 @@

{{ entity.name }}

example + [src]="getCoverImage(entity)" /> Date: Sat, 6 Dec 2025 22:27:31 -0800 Subject: [PATCH 2/4] update. --- .../service/resource/DatasetResource.scala | 12 +++++------- .../dataset-detail.component.html | 3 ++- .../dataset-detail.component.ts | 18 ++++++++++++++++++ .../user-dataset-version-filetree.component.ts | 1 - .../service/user/dataset/dataset.service.ts | 8 +------- .../browse-section.component.html | 3 ++- .../browse-section/browse-section.component.ts | 6 +++++- sql/texera_ddl.sql | 1 + sql/updates/16.sql | 2 +- 9 files changed, 35 insertions(+), 19 deletions(-) diff --git a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala index b49fa8cc476..05aa4178ed7 100644 --- a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala +++ b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala @@ -1376,15 +1376,13 @@ class DatasetResource { @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/update/cover") - @Consumes(Array(MediaType.TEXT_PLAIN)) def updateDatasetCoverImage( - @PathParam("did") did: Integer, - coverImage: String, - @Auth sessionUser: SessionUser - ): Response = { + @PathParam("did") did: Integer, + coverImage: String, + @Auth sessionUser: SessionUser + ): Response = { withTransaction(context) { ctx => val uid = sessionUser.getUid - val datasetDao = new DatasetDao(ctx.configuration()) val dataset = getDatasetByID(ctx, did) if (!userHasWriteAccess(ctx, did, uid)) { @@ -1392,7 +1390,7 @@ class DatasetResource { } dataset.setCoverImage(coverImage) - datasetDao.update(dataset) + new DatasetDao(ctx.configuration()).update(dataset) Response.ok().build() } } diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html index d4dddf94f6d..79ced02f864 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html @@ -263,7 +263,8 @@
Choose a Version:
[fileTreeNodes]="fileTreeNodeList" [isTreeNodeDeletable]="true" (selectedTreeNode)="onVersionFileTreeNodeSelected($event)" - (deletedTreeNode)="onPreviouslyUploadedFileDeleted($event)"> + (deletedTreeNode)="onPreviouslyUploadedFileDeleted($event)" + (setCoverImage)="onSetCoverImage($event)"> diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts index b4d12f5a28e..ebbd3a94a75 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts @@ -657,4 +657,22 @@ export class DatasetDetailComponent implements OnInit { changeViewDisplayStyle() { this.displayPreciseViewCount = !this.displayPreciseViewCount; } + + onSetCoverImage(filePath: string): void { + if (!this.did || !this.selectedVersion) { + return; + } + + this.datasetService + .updateDatasetCoverImage(this.did, `${this.selectedVersion.name}/${filePath}`) + .pipe(untilDestroyed(this)) + .subscribe({ + next: () => { + this.notificationService.success("Cover image set successfully"); + }, + error: (err: unknown) => { + this.notificationService.error("Failed to set cover image"); + }, + }); + } } diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts index cc0820ea544..cbf490e0c61 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts @@ -88,7 +88,6 @@ export class UserDatasetVersionFiletreeComponent implements AfterViewInit { onSetCover(nodeData: DatasetFileNode): void { const path = getRelativePathFromDatasetFileNode(nodeData); - console.log('Setting cover to:', path); this.setCoverImage.emit(path); } } diff --git a/frontend/src/app/dashboard/service/user/dataset/dataset.service.ts b/frontend/src/app/dashboard/service/user/dataset/dataset.service.ts index d5e458337ac..a289c28315f 100644 --- a/frontend/src/app/dashboard/service/user/dataset/dataset.service.ts +++ b/frontend/src/app/dashboard/service/user/dataset/dataset.service.ts @@ -541,12 +541,6 @@ export class DatasetService { } public updateDatasetCoverImage(did: number, coverImagePath: string): Observable { - return this.http.post( - `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/update/cover`, - coverImagePath, - { - headers: { "Content-Type": "text/plain" }, - } - ); + return this.http.post(`${AppSettings.getApiEndpoint()}/dataset/${did}/update/cover`, coverImagePath); } } diff --git a/frontend/src/app/hub/component/browse-section/browse-section.component.html b/frontend/src/app/hub/component/browse-section/browse-section.component.html index 71e308b5f89..2fd8f37525b 100644 --- a/frontend/src/app/hub/component/browse-section/browse-section.component.html +++ b/frontend/src/app/hub/component/browse-section/browse-section.component.html @@ -44,7 +44,8 @@

{{ entity.name }}

example + [src]="getCoverImage(entity)" + (error)="$any($event.target).src = defaultBackground" /> Date: Sat, 6 Dec 2025 22:38:35 -0800 Subject: [PATCH 3/4] update.. --- .../user-dataset-file-renderer.component.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts index 576de576dff..c851a8284ea 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts @@ -131,9 +131,6 @@ export class UserDatasetFileRendererComponent implements OnInit, OnChanges, OnDe @Output() loadFile = new EventEmitter<{ file: string; prefix: string }>(); - @Output() - setCoverImage = new EventEmitter(); - constructor( private datasetService: DatasetService, private sanitizer: DomSanitizer, From 430fa9b69f3fb5e187eaced785f30d96e1908f66 Mon Sep 17 00:00:00 2001 From: Xuan Gu <162244362+xuang7@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:04:54 -0800 Subject: [PATCH 4/4] update with cover file size limit. --- .../service/resource/DatasetResource.scala | 20 ++++++++++++++++++- .../dataset-detail.component.ts | 4 ++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala index 05aa4178ed7..16fad2fa3cf 100644 --- a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala +++ b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala @@ -1384,11 +1384,29 @@ class DatasetResource { withTransaction(context) { ctx => val uid = sessionUser.getUid val dataset = getDatasetByID(ctx, did) - if (!userHasWriteAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } + val document = DocumentFactory + .openReadonlyDocument( + FileResolver.resolve(s"${getOwner(ctx, did).getEmail}/${dataset.getName}/$coverImage") + ) + .asInstanceOf[OnDataset] + + val file = LakeFSStorageClient.getFileFromRepo( + document.getRepositoryName(), + document.getVersionHash(), + document.getFileRelativePath() + ) + val coverSizeLimit = 10 * 1024 * 1024 // 10 MB + + if (file.length() > coverSizeLimit) { + throw new BadRequestException( + s"Cover image must be less than ${coverSizeLimit / (1024 * 1024)} MB" + ) + } + dataset.setCoverImage(coverImage) new DatasetDao(ctx.configuration()).update(dataset) Response.ok().build() diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts index ebbd3a94a75..dc495599af4 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts @@ -670,8 +670,8 @@ export class DatasetDetailComponent implements OnInit { next: () => { this.notificationService.success("Cover image set successfully"); }, - error: (err: unknown) => { - this.notificationService.error("Failed to set cover image"); + error: (err: HttpErrorResponse) => { + this.notificationService.error(err.error?.message || "Failed to set cover image"); }, }); }