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 }}
+ [src]="getCoverImage(entity)"
+ (error)="$any($event.target).src = defaultBackground" />