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..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 @@ -1372,4 +1372,44 @@ class DatasetResource { Right(response) } } + + @POST + @RolesAllowed(Array("REGULAR", "ADMIN")) + @Path("/{did}/update/cover") + def updateDatasetCoverImage( + @PathParam("did") did: Integer, + coverImage: String, + @Auth sessionUser: SessionUser + ): Response = { + 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/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/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..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 @@ -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: HttpErrorResponse) => { + this.notificationService.error(err.error?.message || "Failed to set cover image"); + }, + }); + } } 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..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 @@ -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,14 @@ 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); + 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..a289c28315f 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,8 @@ 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/${did}/update/cover`, coverImagePath); + } } 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..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" />