(`${this.BASE_URL}/unlike`, [workflowId, userId]);
+ }
}
From 2347c265c33a97811a445b3b8e9035308822aa1a Mon Sep 17 00:00:00 2001
From: gspikehalo <2318002579@qq.com>
Date: Fri, 4 Oct 2024 14:10:44 -0700
Subject: [PATCH 06/44] add live button in detail page
---
.../user/list-item/list-item.component.html | 8 +--
.../user/list-item/list-item.component.scss | 30 ++++++++
.../user/list-item/list-item.component.ts | 1 -
.../detail/hub-workflow-detail.component.html | 14 ++++
.../detail/hub-workflow-detail.component.scss | 72 +++++++++++++++++++
.../detail/hub-workflow-detail.component.ts | 38 ++++++++++
6 files changed, 157 insertions(+), 6 deletions(-)
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
index 47041ef6260..42fe4dee23d 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
@@ -190,17 +190,15 @@
+ [ngClass]="isLiked ? 'liked' : 'not-liked'"
+ class="like-icon">
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
index a0921331622..5f4fbaeadf2 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
@@ -157,3 +157,33 @@
}
}
}
+
+.like-button {
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ margin: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ background-color: #f0f0f0;
+ }
+}
+
+.like-icon {
+ color: black;
+ stroke-width: 2px;
+ font-size: 18px;
+
+ &.liked {
+ color: red;
+ }
+
+ &.not-liked {
+ color: black;
+ }
+}
+
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
index 841ee077f81..fbb2a5a839f 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
@@ -276,7 +276,6 @@ export class ListItemComponent implements OnInit, OnChanges {
}
}
-
toggleLike(workflowId: number | undefined, userId: number | undefined): void {
if (workflowId === undefined || userId === undefined) {
return;
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
index a61019990eb..4a510370236 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
@@ -17,6 +17,20 @@
Workflow Detail Page
+
+
+
+
segment.path === "detail") ||
this.route.snapshot.url.some(segment => segment.path === "detail");
+
this.hubWorkflowService
.getOwnerUser(this.wid)
.pipe(untilDestroyed(this))
@@ -122,6 +126,12 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
.subscribe(workflowDescription => {
this.workflowDescription = workflowDescription || "No description available";
});
+ if (this.wid !== undefined && this.currentUid != undefined) {
+ // eslint-disable-next-line rxjs-angular/prefer-takeuntil
+ this.hubWorkflowService.isWorkflowLiked(this.wid, this.currentUid).subscribe((isLiked: boolean) => {
+ this.isLiked = isLiked;
+ });
+ }
}
ngAfterViewInit(): void {
@@ -244,4 +254,32 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
this.router.navigate([`/workflow/${this.clonedWorklowId}`]);
});
}
+
+ toggleLike(workflowId: number | undefined, userId: number | undefined): void {
+ if (workflowId === undefined || userId === undefined) {
+ return;
+ }
+
+ if (this.isLiked) {
+ // eslint-disable-next-line rxjs-angular/prefer-takeuntil
+ this.hubWorkflowService.postUnlikeWorkflow(workflowId, userId).subscribe((success: boolean) => {
+ if (success) {
+ this.isLiked = false;
+ console.log("Successfully unliked the workflow");
+ } else {
+ console.error("Error unliking the workflow");
+ }
+ });
+ } else {
+ // eslint-disable-next-line rxjs-angular/prefer-takeuntil
+ this.hubWorkflowService.postLikeWorkflow(workflowId, userId).subscribe((success: boolean) => {
+ if (success) {
+ this.isLiked = true;
+ console.log("Successfully liked the workflow");
+ } else {
+ console.error("Error liking the workflow");
+ }
+ });
+ }
+ }
}
From 09c9649f183f550d3ee347f556374e758354a3e6 Mon Sep 17 00:00:00 2001
From: gspikehalo <2318002579@qq.com>
Date: Fri, 4 Oct 2024 14:16:09 -0700
Subject: [PATCH 07/44] fmt fix
---
.../hub/workflow/HubWorkflowResource.scala | 41 +++++++++++++------
.../user/list-item/list-item.component.scss | 1 -
.../user/list-item/list-item.component.ts | 1 -
.../detail/hub-workflow-detail.component.ts | 2 +-
.../service/workflow/hub-workflow.service.ts | 2 +-
5 files changed, 31 insertions(+), 16 deletions(-)
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
index 40f26602a06..4cd4f9417f4 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
@@ -108,11 +108,17 @@ class HubWorkflowResource {
@GET
@Path("/isLiked")
@Produces(Array(MediaType.APPLICATION_JSON))
- def isLiked(@QueryParam("workflowId") workflowId: UInteger, @QueryParam("userId") userId: UInteger): Boolean = {
+ def isLiked(
+ @QueryParam("workflowId") workflowId: UInteger,
+ @QueryParam("userId") userId: UInteger
+ ): Boolean = {
val existingLike = context
.selectFrom(WORKFLOW_USER_LIKES)
- .where(WORKFLOW_USER_LIKES.UID.eq(userId)
- .and(WORKFLOW_USER_LIKES.WID.eq(workflowId)))
+ .where(
+ WORKFLOW_USER_LIKES.UID
+ .eq(userId)
+ .and(WORKFLOW_USER_LIKES.WID.eq(workflowId))
+ )
.fetchOne()
existingLike != null
@@ -121,7 +127,7 @@ class HubWorkflowResource {
@POST
@Path("/like")
@Consumes(Array(MediaType.APPLICATION_JSON))
- def likeWorkflow(likeRequest: Array[UInteger]): Boolean = {
+ def likeWorkflow(likeRequest: Array[UInteger]): Boolean = {
if (likeRequest.length != 2) {
return false
}
@@ -131,12 +137,16 @@ class HubWorkflowResource {
val existingLike = context
.selectFrom(WORKFLOW_USER_LIKES)
- .where(WORKFLOW_USER_LIKES.UID.eq(userId)
- .and(WORKFLOW_USER_LIKES.WID.eq(workflowId)))
+ .where(
+ WORKFLOW_USER_LIKES.UID
+ .eq(userId)
+ .and(WORKFLOW_USER_LIKES.WID.eq(workflowId))
+ )
.fetchOne()
if (existingLike == null) {
- context.insertInto(WORKFLOW_USER_LIKES)
+ context
+ .insertInto(WORKFLOW_USER_LIKES)
.set(WORKFLOW_USER_LIKES.UID, userId)
.set(WORKFLOW_USER_LIKES.WID, workflowId)
.execute()
@@ -159,14 +169,21 @@ class HubWorkflowResource {
val existingLike = context
.selectFrom(WORKFLOW_USER_LIKES)
- .where(WORKFLOW_USER_LIKES.UID.eq(userId)
- .and(WORKFLOW_USER_LIKES.WID.eq(workflowId)))
+ .where(
+ WORKFLOW_USER_LIKES.UID
+ .eq(userId)
+ .and(WORKFLOW_USER_LIKES.WID.eq(workflowId))
+ )
.fetchOne()
if (existingLike != null) {
- context.deleteFrom(WORKFLOW_USER_LIKES)
- .where(WORKFLOW_USER_LIKES.UID.eq(userId)
- .and(WORKFLOW_USER_LIKES.WID.eq(workflowId)))
+ context
+ .deleteFrom(WORKFLOW_USER_LIKES)
+ .where(
+ WORKFLOW_USER_LIKES.UID
+ .eq(userId)
+ .and(WORKFLOW_USER_LIKES.WID.eq(workflowId))
+ )
.execute()
true
} else {
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
index 5f4fbaeadf2..e8079ea65f5 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
@@ -186,4 +186,3 @@
color: black;
}
}
-
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
index fbb2a5a839f..5f40b55dc54 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
@@ -24,7 +24,6 @@ import { SearchService } from "../../../service/user/search.service";
import { HubWorkflowDetailComponent } from "../../../../hub/component/workflow/detail/hub-workflow-detail.component";
import { HubWorkflowService } from "../../../../hub/service/workflow/hub-workflow.service";
-
@UntilDestroy()
@Component({
selector: "texera-list-item",
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
index 9393c063bea..f642993bd5a 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
@@ -100,7 +100,7 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
this.isLogin = this.userService.isLogin();
});
this.currentUser = this.userService.getCurrentUser();
- this.currentUid = this.currentUser?.uid
+ this.currentUid = this.currentUser?.uid;
}
ngOnInit() {
diff --git a/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts b/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts
index 4acc9fd3202..3525ad21856 100644
--- a/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts
+++ b/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts
@@ -54,7 +54,7 @@ export class HubWorkflowService {
public isWorkflowLiked(workflowId: number, userId: number): Observable {
return this.http.get(`${this.BASE_URL}/isLiked`, {
- params: { workflowId: workflowId.toString(), userId: userId.toString() }
+ params: { workflowId: workflowId.toString(), userId: userId.toString() },
});
}
From 43fdf267012c53f412425e7220a8648424f2ef5c Mon Sep 17 00:00:00 2001
From: gspikehalo <2318002579@qq.com>
Date: Sat, 5 Oct 2024 10:59:44 -0700
Subject: [PATCH 08/44] quick fix
---
.../detail/hub-workflow-detail.component.html | 1 +
.../detail/hub-workflow-detail.component.scss | 44 +++----------------
2 files changed, 8 insertions(+), 37 deletions(-)
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
index 4a510370236..bc28403a0c9 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
@@ -33,6 +33,7 @@ Workflow Detail Page
Date: Sat, 5 Oct 2024 12:26:46 -0700
Subject: [PATCH 09/44] Implement statistics for cloneworkflow and add counts
for like and clone to the detail interface.
---
.../hub/workflow/HubWorkflowResource.scala | 26 +++++++
.../user/workflow/WorkflowResource.scala | 14 ++++
.../component/dashboard.component.ts | 2 +-
.../user/list-item/list-item.component.ts | 15 ++--
.../detail/hub-workflow-detail.component.html | 4 +
.../detail/hub-workflow-detail.component.scss | 78 +++++++++----------
.../detail/hub-workflow-detail.component.ts | 38 +++++++--
.../service/workflow/hub-workflow.service.ts | 8 ++
8 files changed, 125 insertions(+), 60 deletions(-)
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
index 4cd4f9417f4..be950997b5b 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
@@ -190,4 +190,30 @@ class HubWorkflowResource {
false
}
}
+
+ @GET
+ @Path("/likeCount/{wid}")
+ @Produces(Array(MediaType.APPLICATION_JSON))
+ def getLikeCount(@PathParam("wid") wid: UInteger): Int = {
+ val likeCount = context
+ .selectCount()
+ .from(WORKFLOW_USER_LIKES)
+ .where(WORKFLOW_USER_LIKES.WID.eq(wid))
+ .fetchOne(0, classOf[Int])
+
+ likeCount
+ }
+
+ @GET
+ @Path("/cloneCount/{wid}")
+ @Produces(Array(MediaType.APPLICATION_JSON))
+ def getCloneCount(@PathParam("wid") wid: UInteger): Int = {
+ val cloneCount = context
+ .selectCount()
+ .from(WORKFLOW_USER_CLONES)
+ .where(WORKFLOW_USER_CLONES.WID.eq(wid))
+ .fetchOne(0, classOf[Int])
+
+ cloneCount
+ }
}
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala
index 757c573c3de..1f62c32e890 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala
@@ -399,6 +399,20 @@ class WorkflowResource extends LazyLogging {
),
sessionUser
)
+
+ val existingCloneRecord = context
+ .selectFrom(WORKFLOW_USER_CLONES)
+ .where(WORKFLOW_USER_CLONES.UID.eq(sessionUser.getUid))
+ .and(WORKFLOW_USER_CLONES.WID.eq(wid))
+ .fetchOne()
+
+ if (existingCloneRecord == null) {
+ context.insertInto(WORKFLOW_USER_CLONES)
+ .set(WORKFLOW_USER_CLONES.UID, sessionUser.getUid)
+ .set(WORKFLOW_USER_CLONES.WID, wid)
+ .execute()
+ }
+
//TODO: copy the environment as well
newWorkflow.workflow.getWid
}
diff --git a/core/gui/src/app/dashboard/component/dashboard.component.ts b/core/gui/src/app/dashboard/component/dashboard.component.ts
index 3fcb8e9c393..741992d188e 100644
--- a/core/gui/src/app/dashboard/component/dashboard.component.ts
+++ b/core/gui/src/app/dashboard/component/dashboard.component.ts
@@ -60,7 +60,7 @@ export class DashboardComponent implements OnInit {
return throwError(() => e);
})
)
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil
+ .pipe(untilDestroyed(this))
.subscribe(() =>
this.router.navigateByUrl(this.route.snapshot.queryParams["returnUrl"] || "/dashboard/user/workflow")
);
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
index 5f40b55dc54..0362d90ce36 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
@@ -77,8 +77,7 @@ export class ListItemComponent implements OnInit, OnChanges {
initializeEntry() {
if (this.entry.type === "workflow") {
if (typeof this.entry.id === "number") {
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil
- this.searchService.getWorkflowOwners(this.entry.id).subscribe((data: number[]) => {
+ this.searchService.getWorkflowOwners(this.entry.id).pipe(untilDestroyed(this)).subscribe((data: number[]) => {
this.owners = data;
if (this.currentUid !== undefined && this.owners.includes(this.currentUid)) {
this.entryLink = [this.ROUTER_WORKFLOW_BASE_URL, String(this.entry.id)];
@@ -106,8 +105,7 @@ export class ListItemComponent implements OnInit, OnChanges {
ngOnInit(): void {
this.initializeEntry();
if (this.entry.id !== undefined && this.currentUid !== undefined) {
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil
- this.hubWorkflowService.isWorkflowLiked(this.entry.id, this.currentUid).subscribe((isLiked: boolean) => {
+ this.hubWorkflowService.isWorkflowLiked(this.entry.id, this.currentUid).pipe(untilDestroyed(this)).subscribe((isLiked: boolean) => {
this.isLiked = isLiked;
});
}
@@ -118,8 +116,7 @@ export class ListItemComponent implements OnInit, OnChanges {
this.initializeEntry();
}
if (this.entry.id !== undefined && this.currentUid !== undefined) {
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil
- this.hubWorkflowService.isWorkflowLiked(this.entry.id, this.currentUid).subscribe((isLiked: boolean) => {
+ this.hubWorkflowService.isWorkflowLiked(this.entry.id, this.currentUid).pipe(untilDestroyed(this)).subscribe((isLiked: boolean) => {
this.isLiked = isLiked;
});
}
@@ -281,8 +278,7 @@ export class ListItemComponent implements OnInit, OnChanges {
}
if (this.isLiked) {
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil
- this.hubWorkflowService.postUnlikeWorkflow(workflowId, userId).subscribe((success: boolean) => {
+ this.hubWorkflowService.postUnlikeWorkflow(workflowId, userId).pipe(untilDestroyed(this)).subscribe((success: boolean) => {
if (success) {
this.isLiked = false;
console.log("Successfully unliked the workflow");
@@ -291,8 +287,7 @@ export class ListItemComponent implements OnInit, OnChanges {
}
});
} else {
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil
- this.hubWorkflowService.postLikeWorkflow(workflowId, userId).subscribe((success: boolean) => {
+ this.hubWorkflowService.postLikeWorkflow(workflowId, userId).pipe(untilDestroyed(this)).subscribe((success: boolean) => {
if (success) {
this.isLiked = true;
console.log("Successfully liked the workflow");
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
index bc28403a0c9..d6120da676d 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
@@ -22,6 +22,8 @@ Workflow Detail Page
nzType="text"
class="dropdown-item like-button"
title="Like"
+ *ngIf="isLogin"
+ [ngClass]="{ liked: isLiked }"
(click)="toggleLike(this.wid, this.currentUid)">
Workflow Detail Page
[ngClass]="isLiked ? 'liked' : 'not-liked'"
class="like-icon">
+ {{ formatLikeCount(likeCount) }}
Workflow Detail Page
nz-icon
nzType="copy"
nzTheme="outline">
+ {{ formatLikeCount(cloneCount) }}
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
index 8768d634cea..097adda2ccc 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
@@ -57,9 +57,9 @@ texera-mini-map {
//z-index: 3;
}
-//.prevent-select {
-// user-select: none;
-//}
+.prevent-select {
+ user-select: none;
+}
.workflow-description {
padding: 10px;
@@ -86,64 +86,56 @@ texera-mini-map {
gap: 10px;
}
-.clone-button {
+.clone-button, .like-button {
background-color: white;
color: black;
- width: 38px;
+ width: auto;
+ min-width: 90px;
height: 38px;
- border-radius: 0;
- padding: 0;
+ border-radius: 8px;
+ padding: 0 12px;
cursor: pointer;
- border: none;
- box-shadow: none;
+ border: 1px solid #e0e0e0;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
- justify-content: center;
+ justify-content: space-between;
margin-right: 10px;
margin-bottom: 10px;
- transition: background-color 0.3s ease;
+ transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}
-.clone-button:hover {
- background-color: #e0e0e0;
+.clone-button:hover, .like-button:hover {
+ background-color: #f0f0f0;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
-.clone-button i {
- font-size: 20px;
+.like-button.liked {
+ border-color: red;
}
-.like-button {
- background-color: white;
- color: black;
- width: 38px;
- height: 38px;
- border-radius: 0;
- padding: 0;
- cursor: pointer;
- border: none;
- box-shadow: none;
- display: flex;
- align-items: center;
- justify-content: center;
+.clone-button i, .like-icon {
+ font-size: 20px;
margin-right: 10px;
- margin-bottom: 10px;
- transition: background-color 0.3s ease;
+}
- &:hover {
- background-color: #e0e0e0;
- }
+.like-icon.liked {
+ color: red;
}
-.like-icon {
- font-size: 20px;
- color: black;
- stroke-width: 2px;
+.separator {
+ width: 1px;
+ height: 60%;
+ background-color: #ccc;
+ margin: 0 8px;
+}
- &.liked {
- color: red;
- }
+.clone-button span, .like-button span {
+ font-size: 14px;
+ color: #888;
+ margin-left: 8px;
+}
- &.not-liked {
- color: black;
- }
+.like-button.liked span {
+ color: red;
}
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
index f642993bd5a..8874c6ae245 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
@@ -43,6 +43,8 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
isLogin = this.userService.isLogin();
isLiked: boolean = false;
currentUid: number | undefined;
+ likeCount: number = 0;
+ cloneCount: number = 0;
workflow = {
steps: [
@@ -108,6 +110,20 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
this.route.parent?.snapshot.url.some(segment => segment.path === "detail") ||
this.route.snapshot.url.some(segment => segment.path === "detail");
+ if (this.wid) {
+ this.hubWorkflowService.getLikeCount(this.wid)
+ .pipe(untilDestroyed(this))
+ .subscribe(count => {
+ this.likeCount = count;
+ });
+
+ this.hubWorkflowService.getCloneCount(this.wid)
+ .pipe(untilDestroyed(this))
+ .subscribe(count => {
+ this.cloneCount = count;
+ });
+ }
+
this.hubWorkflowService
.getOwnerUser(this.wid)
.pipe(untilDestroyed(this))
@@ -127,8 +143,7 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
this.workflowDescription = workflowDescription || "No description available";
});
if (this.wid !== undefined && this.currentUid != undefined) {
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil
- this.hubWorkflowService.isWorkflowLiked(this.wid, this.currentUid).subscribe((isLiked: boolean) => {
+ this.hubWorkflowService.isWorkflowLiked(this.wid, this.currentUid).pipe(untilDestroyed(this)).subscribe((isLiked: boolean) => {
this.isLiked = isLiked;
});
}
@@ -261,20 +276,24 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
}
if (this.isLiked) {
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil
- this.hubWorkflowService.postUnlikeWorkflow(workflowId, userId).subscribe((success: boolean) => {
+ this.hubWorkflowService.postUnlikeWorkflow(workflowId, userId).pipe(untilDestroyed(this)).subscribe((success: boolean) => {
if (success) {
this.isLiked = false;
+ this.hubWorkflowService.getLikeCount(workflowId).pipe(untilDestroyed(this)).subscribe((count: number) => {
+ this.likeCount = count;
+ });
console.log("Successfully unliked the workflow");
} else {
console.error("Error unliking the workflow");
}
});
} else {
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil
- this.hubWorkflowService.postLikeWorkflow(workflowId, userId).subscribe((success: boolean) => {
+ this.hubWorkflowService.postLikeWorkflow(workflowId, userId).pipe(untilDestroyed(this)).subscribe((success: boolean) => {
if (success) {
this.isLiked = true;
+ this.hubWorkflowService.getLikeCount(workflowId).pipe(untilDestroyed(this)).subscribe((count: number) => {
+ this.likeCount = count;
+ });
console.log("Successfully liked the workflow");
} else {
console.error("Error liking the workflow");
@@ -282,4 +301,11 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
});
}
}
+
+ formatLikeCount(count: number): string {
+ if (count >= 1000) {
+ return (count / 1000).toFixed(1) + "k";
+ }
+ return count.toString();
+ }
}
diff --git a/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts b/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts
index 3525ad21856..d9f2a0cf673 100644
--- a/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts
+++ b/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts
@@ -65,4 +65,12 @@ export class HubWorkflowService {
public postUnlikeWorkflow(workflowId: number, userId: number): Observable {
return this.http.post(`${this.BASE_URL}/unlike`, [workflowId, userId]);
}
+
+ public getLikeCount(wid: number): Observable {
+ return this.http.get(`${this.BASE_URL}/likeCount/${wid}`);
+ }
+
+ public getCloneCount(wid: number): Observable {
+ return this.http.get(`${this.BASE_URL}/cloneCount/${wid}`);
+ }
}
From 3c04c01a2caa17f869dcf7167956e75c4a0f8d10 Mon Sep 17 00:00:00 2001
From: gspikehalo <2318002579@qq.com>
Date: Sat, 5 Oct 2024 12:37:52 -0700
Subject: [PATCH 10/44] Disable the copy button for the owner preview detail
interface
---
.../detail/hub-workflow-detail.component.html | 16 ++++++++++++++++
.../detail/hub-workflow-detail.component.scss | 7 +++++++
2 files changed, 23 insertions(+)
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
index d6120da676d..0e52e0a1789 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
@@ -45,6 +45,22 @@ Workflow Detail Page
nzTheme="outline">
{{ formatLikeCount(cloneCount) }}
+
+
+
+ {{ formatLikeCount(cloneCount) }}
+
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
index 097adda2ccc..7614517e886 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
@@ -139,3 +139,10 @@ texera-mini-map {
.like-button.liked span {
color: red;
}
+
+.disabled-button {
+ background-color: #f0f0f0;
+ color: #a0a0a0;
+ border-color: #d0d0d0;
+ cursor: not-allowed;
+}
From eb29b7bd503ee2106e1799292431ed01402c4e54 Mon Sep 17 00:00:00 2001
From: gspikehalo <2318002579@qq.com>
Date: Sat, 5 Oct 2024 12:42:29 -0700
Subject: [PATCH 11/44] fmt fix
---
.../user/workflow/WorkflowResource.scala | 3 +-
.../user/list-item/list-item.component.ts | 75 +++++++++++-------
.../detail/hub-workflow-detail.component.html | 14 ++--
.../detail/hub-workflow-detail.component.scss | 18 +++--
.../detail/hub-workflow-detail.component.ts | 79 +++++++++++--------
5 files changed, 115 insertions(+), 74 deletions(-)
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala
index 1f62c32e890..30dc578a363 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala
@@ -407,7 +407,8 @@ class WorkflowResource extends LazyLogging {
.fetchOne()
if (existingCloneRecord == null) {
- context.insertInto(WORKFLOW_USER_CLONES)
+ context
+ .insertInto(WORKFLOW_USER_CLONES)
.set(WORKFLOW_USER_CLONES.UID, sessionUser.getUid)
.set(WORKFLOW_USER_CLONES.WID, wid)
.execute()
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
index 0362d90ce36..f694774ea05 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
@@ -77,14 +77,17 @@ export class ListItemComponent implements OnInit, OnChanges {
initializeEntry() {
if (this.entry.type === "workflow") {
if (typeof this.entry.id === "number") {
- this.searchService.getWorkflowOwners(this.entry.id).pipe(untilDestroyed(this)).subscribe((data: number[]) => {
- this.owners = data;
- if (this.currentUid !== undefined && this.owners.includes(this.currentUid)) {
- this.entryLink = [this.ROUTER_WORKFLOW_BASE_URL, String(this.entry.id)];
- } else {
- this.entryLink = [this.ROUTER_WORKFLOW_DETAIL_BASE_URL, String(this.entry.id)];
- }
- });
+ this.searchService
+ .getWorkflowOwners(this.entry.id)
+ .pipe(untilDestroyed(this))
+ .subscribe((data: number[]) => {
+ this.owners = data;
+ if (this.currentUid !== undefined && this.owners.includes(this.currentUid)) {
+ this.entryLink = [this.ROUTER_WORKFLOW_BASE_URL, String(this.entry.id)];
+ } else {
+ this.entryLink = [this.ROUTER_WORKFLOW_DETAIL_BASE_URL, String(this.entry.id)];
+ }
+ });
}
// this.entryLink = this.ROUTER_WORKFLOW_BASE_URL + "/" + this.entry.id;
this.iconType = "project";
@@ -105,9 +108,12 @@ export class ListItemComponent implements OnInit, OnChanges {
ngOnInit(): void {
this.initializeEntry();
if (this.entry.id !== undefined && this.currentUid !== undefined) {
- this.hubWorkflowService.isWorkflowLiked(this.entry.id, this.currentUid).pipe(untilDestroyed(this)).subscribe((isLiked: boolean) => {
- this.isLiked = isLiked;
- });
+ this.hubWorkflowService
+ .isWorkflowLiked(this.entry.id, this.currentUid)
+ .pipe(untilDestroyed(this))
+ .subscribe((isLiked: boolean) => {
+ this.isLiked = isLiked;
+ });
}
}
@@ -116,9 +122,12 @@ export class ListItemComponent implements OnInit, OnChanges {
this.initializeEntry();
}
if (this.entry.id !== undefined && this.currentUid !== undefined) {
- this.hubWorkflowService.isWorkflowLiked(this.entry.id, this.currentUid).pipe(untilDestroyed(this)).subscribe((isLiked: boolean) => {
- this.isLiked = isLiked;
- });
+ this.hubWorkflowService
+ .isWorkflowLiked(this.entry.id, this.currentUid)
+ .pipe(untilDestroyed(this))
+ .subscribe((isLiked: boolean) => {
+ this.isLiked = isLiked;
+ });
}
}
@@ -278,23 +287,29 @@ export class ListItemComponent implements OnInit, OnChanges {
}
if (this.isLiked) {
- this.hubWorkflowService.postUnlikeWorkflow(workflowId, userId).pipe(untilDestroyed(this)).subscribe((success: boolean) => {
- if (success) {
- this.isLiked = false;
- console.log("Successfully unliked the workflow");
- } else {
- console.error("Error unliking the workflow");
- }
- });
+ this.hubWorkflowService
+ .postUnlikeWorkflow(workflowId, userId)
+ .pipe(untilDestroyed(this))
+ .subscribe((success: boolean) => {
+ if (success) {
+ this.isLiked = false;
+ console.log("Successfully unliked the workflow");
+ } else {
+ console.error("Error unliking the workflow");
+ }
+ });
} else {
- this.hubWorkflowService.postLikeWorkflow(workflowId, userId).pipe(untilDestroyed(this)).subscribe((success: boolean) => {
- if (success) {
- this.isLiked = true;
- console.log("Successfully liked the workflow");
- } else {
- console.error("Error liking the workflow");
- }
- });
+ this.hubWorkflowService
+ .postLikeWorkflow(workflowId, userId)
+ .pipe(untilDestroyed(this))
+ .subscribe((success: boolean) => {
+ if (success) {
+ this.isLiked = true;
+ console.log("Successfully liked the workflow");
+ } else {
+ console.error("Error liking the workflow");
+ }
+ });
}
}
}
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
index 0e52e0a1789..e845079d145 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
@@ -53,13 +53,13 @@ Workflow Detail Page
title="Copy"
*ngIf="isLogin"
[ngClass]="{ 'disabled-button': !isHub }"
- [disabled]="!isHub"
- (click)="cloneWorkflow()">
-
- {{ formatLikeCount(cloneCount) }}
+ [disabled]="!isHub"
+ (click)="cloneWorkflow()">
+
+ {{ formatLikeCount(cloneCount) }}
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
index 7614517e886..eddbda10d83 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
@@ -86,7 +86,8 @@ texera-mini-map {
gap: 10px;
}
-.clone-button, .like-button {
+.clone-button,
+.like-button {
background-color: white;
color: black;
width: auto;
@@ -102,10 +103,15 @@ texera-mini-map {
justify-content: space-between;
margin-right: 10px;
margin-bottom: 10px;
- transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
+ transition:
+ background-color 0.3s ease,
+ color 0.3s ease,
+ border-color 0.3s ease,
+ box-shadow 0.3s ease;
}
-.clone-button:hover, .like-button:hover {
+.clone-button:hover,
+.like-button:hover {
background-color: #f0f0f0;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
@@ -114,7 +120,8 @@ texera-mini-map {
border-color: red;
}
-.clone-button i, .like-icon {
+.clone-button i,
+.like-icon {
font-size: 20px;
margin-right: 10px;
}
@@ -130,7 +137,8 @@ texera-mini-map {
margin: 0 8px;
}
-.clone-button span, .like-button span {
+.clone-button span,
+.like-button span {
font-size: 14px;
color: #888;
margin-left: 8px;
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
index 8874c6ae245..de6767889ad 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
@@ -111,17 +111,19 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
this.route.snapshot.url.some(segment => segment.path === "detail");
if (this.wid) {
- this.hubWorkflowService.getLikeCount(this.wid)
+ this.hubWorkflowService
+ .getLikeCount(this.wid)
.pipe(untilDestroyed(this))
.subscribe(count => {
- this.likeCount = count;
- });
+ this.likeCount = count;
+ });
- this.hubWorkflowService.getCloneCount(this.wid)
+ this.hubWorkflowService
+ .getCloneCount(this.wid)
.pipe(untilDestroyed(this))
.subscribe(count => {
- this.cloneCount = count;
- });
+ this.cloneCount = count;
+ });
}
this.hubWorkflowService
@@ -143,9 +145,12 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
this.workflowDescription = workflowDescription || "No description available";
});
if (this.wid !== undefined && this.currentUid != undefined) {
- this.hubWorkflowService.isWorkflowLiked(this.wid, this.currentUid).pipe(untilDestroyed(this)).subscribe((isLiked: boolean) => {
- this.isLiked = isLiked;
- });
+ this.hubWorkflowService
+ .isWorkflowLiked(this.wid, this.currentUid)
+ .pipe(untilDestroyed(this))
+ .subscribe((isLiked: boolean) => {
+ this.isLiked = isLiked;
+ });
}
}
@@ -276,29 +281,41 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
}
if (this.isLiked) {
- this.hubWorkflowService.postUnlikeWorkflow(workflowId, userId).pipe(untilDestroyed(this)).subscribe((success: boolean) => {
- if (success) {
- this.isLiked = false;
- this.hubWorkflowService.getLikeCount(workflowId).pipe(untilDestroyed(this)).subscribe((count: number) => {
- this.likeCount = count;
- });
- console.log("Successfully unliked the workflow");
- } else {
- console.error("Error unliking the workflow");
- }
- });
+ this.hubWorkflowService
+ .postUnlikeWorkflow(workflowId, userId)
+ .pipe(untilDestroyed(this))
+ .subscribe((success: boolean) => {
+ if (success) {
+ this.isLiked = false;
+ this.hubWorkflowService
+ .getLikeCount(workflowId)
+ .pipe(untilDestroyed(this))
+ .subscribe((count: number) => {
+ this.likeCount = count;
+ });
+ console.log("Successfully unliked the workflow");
+ } else {
+ console.error("Error unliking the workflow");
+ }
+ });
} else {
- this.hubWorkflowService.postLikeWorkflow(workflowId, userId).pipe(untilDestroyed(this)).subscribe((success: boolean) => {
- if (success) {
- this.isLiked = true;
- this.hubWorkflowService.getLikeCount(workflowId).pipe(untilDestroyed(this)).subscribe((count: number) => {
- this.likeCount = count;
- });
- console.log("Successfully liked the workflow");
- } else {
- console.error("Error liking the workflow");
- }
- });
+ this.hubWorkflowService
+ .postLikeWorkflow(workflowId, userId)
+ .pipe(untilDestroyed(this))
+ .subscribe((success: boolean) => {
+ if (success) {
+ this.isLiked = true;
+ this.hubWorkflowService
+ .getLikeCount(workflowId)
+ .pipe(untilDestroyed(this))
+ .subscribe((count: number) => {
+ this.likeCount = count;
+ });
+ console.log("Successfully liked the workflow");
+ } else {
+ console.error("Error liking the workflow");
+ }
+ });
}
}
From 298f1e40fac0dbf0a4a3db6390b73767679ae03d Mon Sep 17 00:00:00 2001
From: gspikehalo <2318002579@qq.com>
Date: Sun, 6 Oct 2024 16:02:00 -0700
Subject: [PATCH 12/44] quick fix
---
.../detail/hub-workflow-detail.component.html | 11 -----------
1 file changed, 11 deletions(-)
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
index e845079d145..c0ddecdeaf0 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
@@ -34,17 +34,6 @@ Workflow Detail Page
{{ formatLikeCount(likeCount) }}
-
-
- {{ formatLikeCount(cloneCount) }}
-
Date: Tue, 8 Oct 2024 15:12:31 -0700
Subject: [PATCH 13/44] add like count in list item
---
.../user/list-item/list-item.component.html | 5 +-
.../user/list-item/list-item.component.scss | 59 +++++++++++++------
.../user/list-item/list-item.component.ts | 26 ++++++++
.../detail/hub-workflow-detail.component.html | 2 +-
.../detail/hub-workflow-detail.component.scss | 7 ---
5 files changed, 72 insertions(+), 27 deletions(-)
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
index 42fe4dee23d..0846821f930 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
@@ -192,14 +192,17 @@
nzType="text"
class="dropdown-item like-button"
title="Like"
+ *ngIf="currentUid"
+ [ngClass]="{ liked: isLiked }"
(click)="toggleLike(this.entry.id, this.currentUid)">
+ {{ formatLikeCount(likeCount) }}
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
index e8079ea65f5..f0337e222cd 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
@@ -159,30 +159,53 @@
}
.like-button {
- background-color: transparent;
- border: none;
+ background-color: white;
+ color: black;
+ width: auto;
+ min-width: 70px;
+ height: 30px;
+ border-radius: 6px;
+ padding: 0 8px;
cursor: pointer;
- padding: 0;
- margin: 0;
- display: inline-flex;
+ border: 1px solid #e0e0e0;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ display: flex;
align-items: center;
- justify-content: center;
+ justify-content: space-between;
+ margin-right: 8px;
+ margin-bottom: 8px;
+ transition:
+ background-color 0.3s ease,
+ color 0.3s ease,
+ border-color 0.3s ease,
+ box-shadow 0.3s ease;
+}
- &:hover {
- background-color: #f0f0f0;
- }
+.like-button:hover {
+ background-color: #f0f0f0;
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
+}
+
+.like-button.liked {
+ border-color: red;
}
.like-icon {
- color: black;
- stroke-width: 2px;
- font-size: 18px;
+ font-size: 16px;
+ margin-right: 6px;
+}
- &.liked {
- color: red;
- }
+.like-icon.liked {
+ color: red;
+}
- &.not-liked {
- color: black;
- }
+.like-button span {
+ font-size: 12px;
+ color: #888;
+ margin-left: 6px;
}
+
+.like-button.liked span {
+ color: red;
+}
+
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
index f694774ea05..2f5343c58c8 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
@@ -37,6 +37,7 @@ export class ListItemComponent implements OnInit, OnChanges {
@ViewChild("descriptionInput") descriptionInput!: ElementRef;
editingName = false;
editingDescription = false;
+ likeCount: number = 0;
ROUTER_WORKFLOW_BASE_URL = "/dashboard/user/workspace";
ROUTER_USER_PROJECT_BASE_URL = "/dashboard/user/project";
@@ -88,6 +89,12 @@ export class ListItemComponent implements OnInit, OnChanges {
this.entryLink = [this.ROUTER_WORKFLOW_DETAIL_BASE_URL, String(this.entry.id)];
}
});
+ this.hubWorkflowService
+ .getLikeCount(this.entry.id)
+ .pipe(untilDestroyed(this))
+ .subscribe(count => {
+ this.likeCount = count;
+ });
}
// this.entryLink = this.ROUTER_WORKFLOW_BASE_URL + "/" + this.entry.id;
this.iconType = "project";
@@ -293,6 +300,12 @@ export class ListItemComponent implements OnInit, OnChanges {
.subscribe((success: boolean) => {
if (success) {
this.isLiked = false;
+ this.hubWorkflowService
+ .getLikeCount(workflowId)
+ .pipe(untilDestroyed(this))
+ .subscribe((count: number) => {
+ this.likeCount = count;
+ });
console.log("Successfully unliked the workflow");
} else {
console.error("Error unliking the workflow");
@@ -305,6 +318,12 @@ export class ListItemComponent implements OnInit, OnChanges {
.subscribe((success: boolean) => {
if (success) {
this.isLiked = true;
+ this.hubWorkflowService
+ .getLikeCount(workflowId)
+ .pipe(untilDestroyed(this))
+ .subscribe((count: number) => {
+ this.likeCount = count;
+ });
console.log("Successfully liked the workflow");
} else {
console.error("Error liking the workflow");
@@ -312,4 +331,11 @@ export class ListItemComponent implements OnInit, OnChanges {
});
}
}
+
+ formatLikeCount(count: number): string {
+ if (count >= 1000) {
+ return (count / 1000).toFixed(1) + "k";
+ }
+ return count.toString();
+ }
}
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
index c0ddecdeaf0..95aaf738c3b 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
@@ -28,7 +28,7 @@ Workflow Detail Page
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
index eddbda10d83..59fdeb8b158 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss
@@ -130,13 +130,6 @@ texera-mini-map {
color: red;
}
-.separator {
- width: 1px;
- height: 60%;
- background-color: #ccc;
- margin: 0 8px;
-}
-
.clone-button span,
.like-button span {
font-size: 14px;
From b34c9f1eab86766bcf9f96eeadab180649267fbd Mon Sep 17 00:00:00 2001
From: shenghao fu <2318002579@qq.com>
Date: Wed, 9 Oct 2024 12:17:13 -0700
Subject: [PATCH 14/44] enable the like button on hub list item when user is
not logged in
---
.../user/list-item/list-item.component.html | 25 +++++++++----------
.../user/list-item/list-item.component.scss | 12 +++++++++
2 files changed, 24 insertions(+), 13 deletions(-)
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
index 0846821f930..9985b26c0d1 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
@@ -183,26 +183,25 @@
nzType="delete">
-
-
-
- {{ formatLikeCount(likeCount) }}
+ [ngClass]="{ liked: isLiked, 'disabled-like': !currentUid }"
+ (click)="toggleLike(this.entry.id, this.currentUid)"
+ [attr.disabled]="!currentUid ? true : null">
+
+
+ {{ formatLikeCount(likeCount) }}
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
index f0337e222cd..4498475282c 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
@@ -209,3 +209,15 @@
color: red;
}
+.disabled-like {
+ background-color: #e0e0e0;
+ color: #a0a0a0;
+ border-color: #d0d0d0;
+ cursor: not-allowed;
+}
+
+.disabled-like:hover {
+ background-color: #f0f0f0;
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
+ color: #a0a0a0;
+}
From 7684f1c2dba64ae20aedcb032eaef5657a7c7843 Mon Sep 17 00:00:00 2001
From: gspikehalo <2318002579@qq.com>
Date: Wed, 9 Oct 2024 12:43:20 -0700
Subject: [PATCH 15/44] display but disable like and clone button in detail
page
---
.../user/list-item/list-item.component.html | 20 +++++++++----------
.../user/list-item/list-item.component.scss | 2 +-
.../detail/hub-workflow-detail.component.html | 11 +++++-----
.../detail/hub-workflow-detail.component.scss | 2 +-
4 files changed, 18 insertions(+), 17 deletions(-)
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
index 9985b26c0d1..cc22c01251a 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.html
@@ -192,16 +192,16 @@
class="dropdown-item like-button"
title="Like"
[ngClass]="{ liked: isLiked, 'disabled-like': !currentUid }"
- (click)="toggleLike(this.entry.id, this.currentUid)"
- [attr.disabled]="!currentUid ? true : null">
-
-
- {{ formatLikeCount(likeCount) }}
+ (click)="toggleLike(this.entry.id, this.currentUid)"
+ [attr.disabled]="!currentUid ? true : null">
+
+
+ {{ formatLikeCount(likeCount) }}
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
index 4498475282c..9a3d6df3ca2 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.scss
@@ -213,7 +213,7 @@
background-color: #e0e0e0;
color: #a0a0a0;
border-color: #d0d0d0;
- cursor: not-allowed;
+ //cursor: not-allowed;
}
.disabled-like:hover {
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
index 95aaf738c3b..007552897e0 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
@@ -17,13 +17,14 @@
Workflow Detail Page
+
Workflow Detail Page
{{ formatLikeCount(likeCount) }}
+
Date: Wed, 9 Oct 2024 15:07:32 -0700
Subject: [PATCH 16/44] A dialog box is displayed when the clone is successful.
---
.../user-workflow/user-workflow.component.ts | 16 ++++++++++++----
.../detail/hub-workflow-detail.component.ts | 3 ++-
2 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/core/gui/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts b/core/gui/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts
index b5b62abcd3e..7e1c12d48e9 100644
--- a/core/gui/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts
+++ b/core/gui/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts
@@ -1,5 +1,5 @@
-import { AfterViewInit, Component, Input, ViewChild } from "@angular/core";
-import { Router } from "@angular/router";
+import { AfterViewInit, Component, Input, OnInit, ViewChild } from "@angular/core";
+import { ActivatedRoute, Router } from "@angular/router";
import { NzModalService } from "ng-zorro-antd/modal";
import { firstValueFrom, of } from "rxjs";
import {
@@ -57,7 +57,7 @@ import { DashboardWorkflow } from "../../../type/dashboard-workflow.interface";
templateUrl: "user-workflow.component.html",
styleUrls: ["user-workflow.component.scss"],
})
-export class UserWorkflowComponent implements AfterViewInit {
+export class UserWorkflowComponent implements AfterViewInit, OnInit {
public ROUTER_WORKFLOW_BASE_URL = "/dashboard/user/workspace";
private _searchResultsComponent?: SearchResultsComponent;
public isLogin = this.userService.isLogin();
@@ -99,7 +99,7 @@ export class UserWorkflowComponent implements AfterViewInit {
private modalService: NzModalService,
private router: Router,
private fileSaverService: FileSaverService,
- private searchService: SearchService
+ private searchService: SearchService,
) {
this.userService
.userChanged()
@@ -118,6 +118,14 @@ export class UserWorkflowComponent implements AfterViewInit {
}
}
+ ngOnInit(): void {
+ const cloneSuccess = sessionStorage.getItem("cloneSuccess");
+ if (cloneSuccess === "true") {
+ this.notificationService.success("Clone Successful");
+ sessionStorage.removeItem("cloneSuccess");
+ }
+ }
+
ngAfterViewInit() {
this.userService
.userChanged()
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
index de6767889ad..2af1eb0ba21 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts
@@ -271,7 +271,8 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI
.pipe(untilDestroyed(this))
.subscribe(newWid => {
this.clonedWorklowId = newWid;
- this.router.navigate([`/workflow/${this.clonedWorklowId}`]);
+ sessionStorage.setItem("cloneSuccess", "true");
+ this.router.navigate(["/dashboard/user/workflow"]);
});
}
From ef7bb9e326e5c5a948a19f01f48298cf1151f331 Mon Sep 17 00:00:00 2001
From: gspikehalo <2318002579@qq.com>
Date: Wed, 9 Oct 2024 15:09:53 -0700
Subject: [PATCH 17/44] fmt fix
---
.../component/user/user-workflow/user-workflow.component.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/gui/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts b/core/gui/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts
index 7e1c12d48e9..f067b39a590 100644
--- a/core/gui/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts
+++ b/core/gui/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts
@@ -99,7 +99,7 @@ export class UserWorkflowComponent implements AfterViewInit, OnInit {
private modalService: NzModalService,
private router: Router,
private fileSaverService: FileSaverService,
- private searchService: SearchService,
+ private searchService: SearchService
) {
this.userService
.userChanged()
From 1ead656520ff28eaa0f81c4d861f2f49a9a3c1a9 Mon Sep 17 00:00:00 2001
From: shenghao fu <2318002579@qq.com>
Date: Thu, 10 Oct 2024 13:39:47 -0700
Subject: [PATCH 18/44] fmt fix
---
.../dashboard/component/user/list-item/list-item.component.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
index 616ac4097db..a07ee7746d2 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
@@ -69,9 +69,8 @@ export class ListItemComponent implements OnInit, OnChanges {
private searchService: SearchService,
private modalService: NzModalService,
private workflowPersistService: WorkflowPersistService,
- private fileSaverService: FileSaverService,
private modal: NzModalService,
- private hubWorkflowService: HubWorkflowService
+ private hubWorkflowService: HubWorkflowService,
private downloadService: DownloadService
) {}
From 4820c2be01a33b804de2acd5ed2e4cf78c7c453c Mon Sep 17 00:00:00 2001
From: shenghao fu <2318002579@qq.com>
Date: Thu, 10 Oct 2024 14:18:15 -0700
Subject: [PATCH 19/44] quick start
---
core/gui/src/app/app-routing.module.ts | 5 +++++
core/gui/src/app/app.module.ts | 2 ++
.../component/dashboard.component.ts | 2 +-
.../src/app/hub/component/hub.component.html | 19 ++++++++++++++---
.../landing-page/landing-page.component.html | 1 +
.../landing-page/landing-page.component.scss | 0
.../landing-page.component.spec.ts | 21 +++++++++++++++++++
.../landing-page/landing-page.component.ts | 10 +++++++++
8 files changed, 56 insertions(+), 4 deletions(-)
create mode 100644 core/gui/src/app/hub/component/landing-page/landing-page.component.html
create mode 100644 core/gui/src/app/hub/component/landing-page/landing-page.component.scss
create mode 100644 core/gui/src/app/hub/component/landing-page/landing-page.component.spec.ts
create mode 100644 core/gui/src/app/hub/component/landing-page/landing-page.component.ts
diff --git a/core/gui/src/app/app-routing.module.ts b/core/gui/src/app/app-routing.module.ts
index f2bcb552995..ffb55a0ed33 100644
--- a/core/gui/src/app/app-routing.module.ts
+++ b/core/gui/src/app/app-routing.module.ts
@@ -21,6 +21,7 @@ import { HubWorkflowSearchComponent } from "./hub/component/workflow/search/hub-
import { HubWorkflowResultComponent } from "./hub/component/workflow/result/hub-workflow-result.component";
import { HubWorkflowComponent } from "./hub/component/workflow/hub-workflow.component";
import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component";
+import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component";
const routes: Routes = [];
@@ -31,6 +32,10 @@ if (environment.userSystemEnabled) {
children: [
{
path: "home",
+ component: LandingPageComponent,
+ },
+ {
+ path: "about",
component: HomeComponent,
},
{
diff --git a/core/gui/src/app/app.module.ts b/core/gui/src/app/app.module.ts
index 2f7d24b55df..b0a294d06da 100644
--- a/core/gui/src/app/app.module.ts
+++ b/core/gui/src/app/app.module.ts
@@ -138,6 +138,7 @@ import { HubWorkflowResultComponent } from "./hub/component/workflow/result/hub-
import { HubWorkflowComponent } from "./hub/component/workflow/hub-workflow.component";
import { HubWorkflowSearchBarComponent } from "./hub/component/workflow/search-bar/hub-workflow-search-bar.component";
import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component";
+import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component";
registerLocaleData(en);
@@ -226,6 +227,7 @@ registerLocaleData(en);
HubWorkflowDetailComponent,
HubWorkflowResultComponent,
GoogleLoginComponent,
+ LandingPageComponent,
],
imports: [
BrowserModule,
diff --git a/core/gui/src/app/dashboard/component/dashboard.component.ts b/core/gui/src/app/dashboard/component/dashboard.component.ts
index 741992d188e..586eff90e57 100644
--- a/core/gui/src/app/dashboard/component/dashboard.component.ts
+++ b/core/gui/src/app/dashboard/component/dashboard.component.ts
@@ -26,7 +26,7 @@ export class DashboardComponent implements OnInit {
displayForum: boolean = true;
displayNavbar: boolean = true;
isCollpased: boolean = false;
- routesWithoutNavbar: string[] = ["/workspace", "/home"];
+ routesWithoutNavbar: string[] = ["/workspace", "/home","/about"];
constructor(
private userService: UserService,
diff --git a/core/gui/src/app/hub/component/hub.component.html b/core/gui/src/app/hub/component/hub.component.html
index 8570c329259..f977141ad66 100644
--- a/core/gui/src/app/hub/component/hub.component.html
+++ b/core/gui/src/app/hub/component/hub.component.html
@@ -1,15 +1,16 @@
- Home
+
+ Home
+
Workflows
+
+
+ About
+
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.html b/core/gui/src/app/hub/component/landing-page/landing-page.component.html
new file mode 100644
index 00000000000..8d1873e93eb
--- /dev/null
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.html
@@ -0,0 +1 @@
+ landing-page works!
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.scss b/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.spec.ts b/core/gui/src/app/hub/component/landing-page/landing-page.component.spec.ts
new file mode 100644
index 00000000000..35793a21fe4
--- /dev/null
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+
+import { LandingPageComponent } from "./landing-page.component";
+
+describe("LandingPageComponent", () => {
+ let component: LandingPageComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [LandingPageComponent],
+ });
+ fixture = TestBed.createComponent(LandingPageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.ts b/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
new file mode 100644
index 00000000000..a4364b4068c
--- /dev/null
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
@@ -0,0 +1,10 @@
+import { Component } from "@angular/core";
+
+@Component({
+ selector: "texera-landing-page",
+ templateUrl: "./landing-page.component.html",
+ styleUrls: ["./landing-page.component.scss"],
+})
+export class LandingPageComponent {
+
+}
From e91f2177489c9956eccf278a316b5d5a37c0d26e Mon Sep 17 00:00:00 2001
From: shenghao fu <2318002579@qq.com>
Date: Thu, 10 Oct 2024 14:54:49 -0700
Subject: [PATCH 20/44] Basic Design
---
.../landing-page/landing-page.component.html | 13 +++++++-
.../landing-page/landing-page.component.scss | 30 +++++++++++++++++++
2 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.html b/core/gui/src/app/hub/component/landing-page/landing-page.component.html
index 8d1873e93eb..63821e55e38 100644
--- a/core/gui/src/app/hub/component/landing-page/landing-page.component.html
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.html
@@ -1 +1,12 @@
-landing-page works!
+
+
+
Welcome to Texera!
+
Start your search journey with our powerful tools.
+
+
+
+
+
+
+
+
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.scss b/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
index e69de29bb2d..ca8e7ddc341 100644
--- a/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
@@ -0,0 +1,30 @@
+.landing-page-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ height: 100vh;
+ text-align: center;
+ padding-top: 50px;
+ background-color: #f0f2f5;
+}
+
+.title-text {
+ margin-bottom: 20px;
+}
+
+.title-text h2 {
+ font-size: 2.5rem;
+ margin-bottom: 10px;
+}
+
+.title-text p {
+ font-size: 1.25rem;
+ color: #555;
+}
+
+.search-bar-wrapper {
+ width: 80%;
+ max-width: 1200px;
+ margin-bottom: 40px;
+}
From 5ccd265653de3b29f9805f9a94a9696b66ab829c Mon Sep 17 00:00:00 2001
From: shenghao fu <2318002579@qq.com>
Date: Thu, 10 Oct 2024 17:00:10 -0700
Subject: [PATCH 21/44] enable public workflow count and hyperlink
---
.../hub/workflow/HubWorkflowResource.scala | 3 +-
.../component/dashboard.component.ts | 2 +-
.../component/user/search/search.component.ts | 11 +++++--
.../src/app/hub/component/hub.component.html | 4 +--
.../landing-page/landing-page.component.html | 11 ++++++-
.../landing-page/landing-page.component.scss | 6 ++++
.../landing-page/landing-page.component.ts | 30 +++++++++++++++++--
7 files changed, 56 insertions(+), 11 deletions(-)
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
index be950997b5b..4d89f7ab1c7 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
@@ -31,9 +31,10 @@ class HubWorkflowResource {
@GET
@Path("/count")
- def getWorkflowCount: Integer = {
+ def getPublishedWorkflowCount: Integer = {
context.selectCount
.from(WORKFLOW)
+ .where(WORKFLOW.IS_PUBLISHED.eq(1.toByte))
.fetchOne(0, classOf[Integer])
}
diff --git a/core/gui/src/app/dashboard/component/dashboard.component.ts b/core/gui/src/app/dashboard/component/dashboard.component.ts
index 586eff90e57..ba7020d0f2a 100644
--- a/core/gui/src/app/dashboard/component/dashboard.component.ts
+++ b/core/gui/src/app/dashboard/component/dashboard.component.ts
@@ -26,7 +26,7 @@ export class DashboardComponent implements OnInit {
displayForum: boolean = true;
displayNavbar: boolean = true;
isCollpased: boolean = false;
- routesWithoutNavbar: string[] = ["/workspace", "/home","/about"];
+ routesWithoutNavbar: string[] = ["/workspace", "/home", "/about"];
constructor(
private userService: UserService,
diff --git a/core/gui/src/app/dashboard/component/user/search/search.component.ts b/core/gui/src/app/dashboard/component/user/search/search.component.ts
index 19da6548e91..78b50e5a125 100644
--- a/core/gui/src/app/dashboard/component/user/search/search.component.ts
+++ b/core/gui/src/app/dashboard/component/user/search/search.component.ts
@@ -64,14 +64,19 @@ export class SearchComponent implements AfterViewInit {
if (keyword) {
this.searchParam = keyword;
this.updateMasterFilterList();
+ } else {
+ this.filters.masterFilterList = [];
+ this.search();
}
});
+ console.log(this.filters.getSearchKeywords());
}
async search(): Promise {
- if (this.filters.masterFilterList.length === 0) {
- return;
- }
+ // if (this.filters.masterFilterList.length === 0) {
+ // return;
+ // }
+ console.log(this.filters.getSearchKeywords());
const sameList =
this.filters.masterFilterList.length === this.masterFilterList.length &&
this.filters.masterFilterList.every((v, i) => v === this.masterFilterList[i]);
diff --git a/core/gui/src/app/hub/component/hub.component.html b/core/gui/src/app/hub/component/hub.component.html
index f977141ad66..e6ac67f44a4 100644
--- a/core/gui/src/app/hub/component/hub.component.html
+++ b/core/gui/src/app/hub/component/hub.component.html
@@ -8,9 +8,7 @@
-
- Home
-
+ Home
Welcome to Texera!
Start your search journey with our powerful tools.
+
+ There are
+
+ {{ workflowCount }} workflows
+
+ available.
+
@@ -9,4 +19,3 @@ Welcome to Texera!
-
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.scss b/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
index ca8e7ddc341..9e869f710cb 100644
--- a/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
@@ -28,3 +28,9 @@
max-width: 1200px;
margin-bottom: 40px;
}
+
+.underline-link {
+ text-decoration: underline;
+ color: blue;
+ cursor: pointer;
+}
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.ts b/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
index a4364b4068c..6b882a516fd 100644
--- a/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
@@ -1,10 +1,36 @@
-import { Component } from "@angular/core";
+import { Component, OnInit } from "@angular/core";
+import { Observable } from "rxjs";
+import { HubWorkflowService } from "../../service/workflow/hub-workflow.service";
+import { untilDestroyed } from "@ngneat/until-destroy";
+import { Router } from "@angular/router";
@Component({
selector: "texera-landing-page",
templateUrl: "./landing-page.component.html",
styleUrls: ["./landing-page.component.scss"],
})
-export class LandingPageComponent {
+export class LandingPageComponent implements OnInit {
+ public workflowCount: number = 0;
+ constructor(
+ private hubWorkflowService: HubWorkflowService,
+ private router: Router
+ ) {}
+
+ ngOnInit(): void {
+ this.getWorkflowCount();
+ }
+
+ getWorkflowCount(): void {
+ this.hubWorkflowService
+ .getWorkflowCount()
+ .pipe(untilDestroyed(this))
+ .subscribe((count: number) => {
+ this.workflowCount = count;
+ });
+ }
+
+ navigateToSearch(): void {
+ this.router.navigate(["/dashboard/search"], { queryParams: { q: "" } });
+ }
}
From f5ae49b2df1ad8fdd7c7548a9f6caa8c3eaa5823 Mon Sep 17 00:00:00 2001
From: gspikehalo <2318002579@qq.com>
Date: Fri, 11 Oct 2024 20:55:26 -0700
Subject: [PATCH 22/44] remove unused components.
---
core/gui/src/app/app-routing.module.ts | 9 ++---
core/gui/src/app/app.module.ts | 4 ---
.../user/list-item/list-item.component.ts | 2 +-
.../component/user/search/search.component.ts | 5 ---
.../src/app/hub/component/hub.component.html | 2 +-
.../landing-page/landing-page.component.ts | 7 ++--
.../result/hub-workflow-result.component.html | 26 --------------
.../result/hub-workflow-result.component.scss | 29 ---------------
.../result/hub-workflow-result.component.ts | 23 ------------
.../hub-workflow-search-bar.component.html | 19 ----------
.../hub-workflow-search-bar.component.ts | 36 -------------------
11 files changed, 9 insertions(+), 153 deletions(-)
delete mode 100644 core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.html
delete mode 100644 core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.scss
delete mode 100644 core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.ts
delete mode 100644 core/gui/src/app/hub/component/workflow/search-bar/hub-workflow-search-bar.component.html
delete mode 100644 core/gui/src/app/hub/component/workflow/search-bar/hub-workflow-search-bar.component.ts
diff --git a/core/gui/src/app/app-routing.module.ts b/core/gui/src/app/app-routing.module.ts
index ffb55a0ed33..d9621e8fe80 100644
--- a/core/gui/src/app/app-routing.module.ts
+++ b/core/gui/src/app/app-routing.module.ts
@@ -18,7 +18,6 @@ import { AdminGmailComponent } from "./dashboard/component/admin/gmail/admin-gma
import { UserDatasetExplorerComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-explorer.component";
import { UserDatasetComponent } from "./dashboard/component/user/user-dataset/user-dataset.component";
import { HubWorkflowSearchComponent } from "./hub/component/workflow/search/hub-workflow-search.component";
-import { HubWorkflowResultComponent } from "./hub/component/workflow/result/hub-workflow-result.component";
import { HubWorkflowComponent } from "./hub/component/workflow/hub-workflow.component";
import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component";
import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component";
@@ -46,15 +45,11 @@ if (environment.userSystemEnabled) {
component: HubWorkflowComponent,
children: [
{
- path: "search",
+ path: "result",
component: HubWorkflowSearchComponent,
},
{
- path: "search/result",
- component: HubWorkflowResultComponent,
- },
- {
- path: "search/result/detail/:id",
+ path: "result/detail/:id",
component: HubWorkflowDetailComponent,
},
],
diff --git a/core/gui/src/app/app.module.ts b/core/gui/src/app/app.module.ts
index b0a294d06da..13a9e45679f 100644
--- a/core/gui/src/app/app.module.ts
+++ b/core/gui/src/app/app.module.ts
@@ -134,9 +134,7 @@ import { ListItemComponent } from "./dashboard/component/user/list-item/list-ite
import { HubComponent } from "./hub/component/hub.component";
import { HubWorkflowSearchComponent } from "./hub/component/workflow/search/hub-workflow-search.component";
import { GoogleLoginComponent } from "./dashboard/component/user/google-login/google-login.component";
-import { HubWorkflowResultComponent } from "./hub/component/workflow/result/hub-workflow-result.component";
import { HubWorkflowComponent } from "./hub/component/workflow/hub-workflow.component";
-import { HubWorkflowSearchBarComponent } from "./hub/component/workflow/search-bar/hub-workflow-search-bar.component";
import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component";
import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component";
@@ -223,9 +221,7 @@ registerLocaleData(en);
HubComponent,
HubWorkflowComponent,
HubWorkflowSearchComponent,
- HubWorkflowSearchBarComponent,
HubWorkflowDetailComponent,
- HubWorkflowResultComponent,
GoogleLoginComponent,
LandingPageComponent,
],
diff --git a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
index a07ee7746d2..88ad81734fd 100644
--- a/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
+++ b/core/gui/src/app/dashboard/component/user/list-item/list-item.component.ts
@@ -41,7 +41,7 @@ export class ListItemComponent implements OnInit, OnChanges {
ROUTER_WORKFLOW_BASE_URL = "/dashboard/user/workspace";
ROUTER_USER_PROJECT_BASE_URL = "/dashboard/user/project";
ROUTER_DATASET_BASE_URL = "/dashboard/user/dataset";
- ROUTER_WORKFLOW_DETAIL_BASE_URL = "/dashboard/hub/workflow/search/result/detail";
+ ROUTER_WORKFLOW_DETAIL_BASE_URL = "/dashboard/hub/workflow/result/detail";
entryLink: string[] = [];
public iconType: string = "";
isLiked: boolean = false;
diff --git a/core/gui/src/app/dashboard/component/user/search/search.component.ts b/core/gui/src/app/dashboard/component/user/search/search.component.ts
index 78b50e5a125..2adab8fa51e 100644
--- a/core/gui/src/app/dashboard/component/user/search/search.component.ts
+++ b/core/gui/src/app/dashboard/component/user/search/search.component.ts
@@ -64,19 +64,14 @@ export class SearchComponent implements AfterViewInit {
if (keyword) {
this.searchParam = keyword;
this.updateMasterFilterList();
- } else {
- this.filters.masterFilterList = [];
- this.search();
}
});
- console.log(this.filters.getSearchKeywords());
}
async search(): Promise {
// if (this.filters.masterFilterList.length === 0) {
// return;
// }
- console.log(this.filters.getSearchKeywords());
const sameList =
this.filters.masterFilterList.length === this.masterFilterList.length &&
this.filters.masterFilterList.every((v, i) => v === this.masterFilterList[i]);
diff --git a/core/gui/src/app/hub/component/hub.component.html b/core/gui/src/app/hub/component/hub.component.html
index e6ac67f44a4..36fa02c23ff 100644
--- a/core/gui/src/app/hub/component/hub.component.html
+++ b/core/gui/src/app/hub/component/hub.component.html
@@ -15,7 +15,7 @@
nz-tooltip="Search public workflows"
nzMatchRouter="true"
nzTooltipPlacement="right"
- routerLink="/dashboard/hub/workflow/search">
+ routerLink="/dashboard/hub/workflow/result">
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.ts b/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
index 6b882a516fd..448edca51d2 100644
--- a/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
@@ -18,19 +18,22 @@ export class LandingPageComponent implements OnInit {
) {}
ngOnInit(): void {
+ console.log("in init")
this.getWorkflowCount();
}
getWorkflowCount(): void {
+ console.log("in get count")
this.hubWorkflowService
.getWorkflowCount()
- .pipe(untilDestroyed(this))
+ // eslint-disable-next-line rxjs-angular/prefer-takeuntil
.subscribe((count: number) => {
this.workflowCount = count;
+ console.log("after count")
});
}
navigateToSearch(): void {
- this.router.navigate(["/dashboard/search"], { queryParams: { q: "" } });
+ this.router.navigate(["/dashboard/hub/workflow/result"]);
}
}
diff --git a/core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.html b/core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.html
deleted file mode 100644
index 3a302831f5e..00000000000
--- a/core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.html
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
- ID: {{ workflow.wid }}
- Description: {{ workflow.description }}
- Created On: {{ workflow.creationTime | date }}
- Modified On: {{ workflow.lastModifiedTime | date }}
-
- View Details
-
-
-
-
-
diff --git a/core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.scss b/core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.scss
deleted file mode 100644
index 4985d3c4279..00000000000
--- a/core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-.workflow-list {
- display: flex;
- flex-wrap: wrap;
- justify-content: space-around;
- padding: 20px;
- background-color: #f9f9f9;
-}
-
-.workflow-card {
- width: 300px;
- transition:
- transform 0.3s,
- box-shadow 0.3s;
-}
-
-.workflow-card:hover {
- transform: scale(1.05);
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
-}
-
-.workflow-card p {
- margin: 0;
- color: #666;
-}
-
-.workflow-card button {
- margin-top: 10px;
- width: 100%;
-}
diff --git a/core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.ts b/core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.ts
deleted file mode 100644
index 84e309120d7..00000000000
--- a/core/gui/src/app/hub/component/workflow/result/hub-workflow-result.component.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Component } from "@angular/core";
-import { HubWorkflowService } from "../../../service/workflow/hub-workflow.service";
-import { HubWorkflow } from "../../type/hub-workflow.interface";
-import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
-
-@UntilDestroy()
-@Component({
- selector: "texera-hub-workflow-result",
- templateUrl: "hub-workflow-result.component.html",
- styleUrls: ["hub-workflow-result.component.scss"],
-})
-export class HubWorkflowResultComponent {
- listOfWorkflows: HubWorkflow[] = [];
-
- constructor(hubWorkflowService: HubWorkflowService) {
- hubWorkflowService
- .getWorkflowList()
- .pipe(untilDestroyed(this))
- .subscribe(workflows => {
- this.listOfWorkflows = workflows;
- });
- }
-}
diff --git a/core/gui/src/app/hub/component/workflow/search-bar/hub-workflow-search-bar.component.html b/core/gui/src/app/hub/component/workflow/search-bar/hub-workflow-search-bar.component.html
deleted file mode 100644
index 24e603e5fa8..00000000000
--- a/core/gui/src/app/hub/component/workflow/search-bar/hub-workflow-search-bar.component.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
diff --git a/core/gui/src/app/hub/component/workflow/search-bar/hub-workflow-search-bar.component.ts b/core/gui/src/app/hub/component/workflow/search-bar/hub-workflow-search-bar.component.ts
deleted file mode 100644
index 7fdbfb50e68..00000000000
--- a/core/gui/src/app/hub/component/workflow/search-bar/hub-workflow-search-bar.component.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Component } from "@angular/core";
-import { HubWorkflowService } from "../../../service/workflow/hub-workflow.service";
-import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
-import { Router } from "@angular/router";
-
-@UntilDestroy()
-@Component({
- selector: "texera-hub-workflow-search-bar",
- templateUrl: "hub-workflow-search-bar.component.html",
-})
-export class HubWorkflowSearchBarComponent {
- inputValue?: string;
- workflowNames: string[] = [];
- filteredOptions: string[] = [];
-
- constructor(
- hubWorkflowService: HubWorkflowService,
- private router: Router
- ) {
- this.filteredOptions = this.workflowNames;
- hubWorkflowService
- .getWorkflowList()
- .pipe(untilDestroyed(this))
- .subscribe(workflows => (this.workflowNames = workflows.map(obj => obj.name)));
- }
-
- onChange(value: string) {
- this.filteredOptions = this.workflowNames.filter(
- option => option.toLowerCase().indexOf(value.toLowerCase()) !== -1
- );
- }
-
- onSubmit() {
- this.router.navigate(["/dashboard/hub/workflow/search/result"]);
- }
-}
From bf3fe046be22ee1af1c71341a68d6e8d93c397a5 Mon Sep 17 00:00:00 2001
From: gspikehalo <2318002579@qq.com>
Date: Fri, 11 Oct 2024 23:39:51 -0700
Subject: [PATCH 23/44] refactor landing page
---
.../landing-page/landing-page.component.html | 31 +++++----
.../landing-page/landing-page.component.scss | 63 ++++++++++++-------
.../landing-page/landing-page.component.ts | 3 -
3 files changed, 60 insertions(+), 37 deletions(-)
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.html b/core/gui/src/app/hub/component/landing-page/landing-page.component.html
index 31ff85e4771..18e960a1e1c 100644
--- a/core/gui/src/app/hub/component/landing-page/landing-page.component.html
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.html
@@ -1,17 +1,22 @@
-
-
Welcome to Texera!
-
Start your search journey with our powerful tools.
-
- There are
-
- {{ workflowCount }} workflows
-
- available.
-
+
+
+
+
Texera Hub
+
+ Join our community to explore public workflows, collaborate with others, and enhance your data analytics
+ capabilities. Access
+
+ {{workflowCount}} workflows
+
+ provided by Texera and our community.
+
+
+
+
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.scss b/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
index 9e869f710cb..b65de40f162 100644
--- a/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
@@ -1,36 +1,57 @@
+.search-bar-wrapper {
+ width: 100%;
+ max-width: 1200px;
+ margin-bottom: 40px;
+}
+
.landing-page-container {
display: flex;
flex-direction: column;
+ padding: 32px 20px 0 20px;
align-items: center;
- justify-content: flex-start;
- height: 100vh;
- text-align: center;
- padding-top: 50px;
- background-color: #f0f2f5;
+ height: 100%;
+ width: 100%;
}
-.title-text {
- margin-bottom: 20px;
+.search-bar {
+ max-width: 1200px;
+ width: 100%;
+}
+
+.section-intro {
+ max-width: 1200px;
+ margin-top: 20px;
+ padding: 20px 0;
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
}
-.title-text h2 {
- font-size: 2.5rem;
- margin-bottom: 10px;
+.section-intro-text {
+ max-width: 500px;
}
-.title-text p {
- font-size: 1.25rem;
- color: #555;
+.section-intro-text > h1 {
+ font-size: 40px;
+ font-weight: bold;
+ margin: 0;
}
-.search-bar-wrapper {
- width: 80%;
- max-width: 1200px;
- margin-bottom: 40px;
+.section-intro-text > p {
+ color: grey;
+ font-size: 13px;
+ font-weight: 300;
+ margin-bottom: 20px;
}
-.underline-link {
- text-decoration: underline;
- color: blue;
- cursor: pointer;
+.section-intro-img {
+ max-height: 176px;
}
+
+.browse-section {
+ max-width: 1200px;
+ width: 100%;
+ padding-bottom: 40px;
+}
+
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.ts b/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
index 448edca51d2..4e40c60b8e9 100644
--- a/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
@@ -18,18 +18,15 @@ export class LandingPageComponent implements OnInit {
) {}
ngOnInit(): void {
- console.log("in init")
this.getWorkflowCount();
}
getWorkflowCount(): void {
- console.log("in get count")
this.hubWorkflowService
.getWorkflowCount()
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
.subscribe((count: number) => {
this.workflowCount = count;
- console.log("after count")
});
}
From 275a2bff7c61d6ecfaec03c9351808f237d4320d Mon Sep 17 00:00:00 2001
From: gspikehalo <2318002579@qq.com>
Date: Sat, 12 Oct 2024 10:27:34 -0700
Subject: [PATCH 24/44] fmt fix
---
.../component/workflow/detail/hub-workflow-detail.component.html | 1 -
1 file changed, 1 deletion(-)
diff --git a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
index 9807ada0235..007552897e0 100644
--- a/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
+++ b/core/gui/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html
@@ -45,7 +45,6 @@
Workflow Detail Page
[ngClass]="{ 'disabled-button': !isLogin || !isHub }"
[disabled]="!isLogin || !isHub"
(click)="cloneWorkflow()">
-
Date: Sat, 12 Oct 2024 14:48:01 -0700
Subject: [PATCH 25/44] add browse section to the landing page
---
.../hub/workflow/HubWorkflowResource.scala | 93 ++-
core/gui/src/app/app.module.ts | 2 +
.../component/dashboard.component.scss | 1 +
.../browse-section.component.html | 39 ++
.../browse-section.component.scss | 79 +++
.../browse-section.component.spec.ts | 21 +
.../browse-section.component.ts | 14 +
.../landing-page/landing-page.component.html | 9 +
.../landing-page/landing-page.component.scss | 3 +-
.../landing-page/landing-page.component.ts | 71 ++-
.../service/workflow/hub-workflow.service.ts | 9 +
core/gui/src/assets/card_background.jpg | Bin 0 -> 414278 bytes
core/gui/src/assets/svg/hub_icon.svg | 560 ++++++++++++++++++
13 files changed, 895 insertions(+), 6 deletions(-)
create mode 100644 core/gui/src/app/hub/component/browse-section/browse-section.component.html
create mode 100644 core/gui/src/app/hub/component/browse-section/browse-section.component.scss
create mode 100644 core/gui/src/app/hub/component/browse-section/browse-section.component.spec.ts
create mode 100644 core/gui/src/app/hub/component/browse-section/browse-section.component.ts
create mode 100644 core/gui/src/assets/card_background.jpg
create mode 100644 core/gui/src/assets/svg/hub_icon.svg
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
index 4d89f7ab1c7..9a837cc61e5 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala
@@ -6,14 +6,68 @@ import edu.uci.ics.texera.web.model.jooq.generated.Tables._
import edu.uci.ics.texera.web.model.jooq.generated.enums.UserRole
import edu.uci.ics.texera.web.model.jooq.generated.tables.daos.WorkflowDao
import edu.uci.ics.texera.web.model.jooq.generated.tables.pojos.{User, Workflow}
+import edu.uci.ics.texera.web.resource.dashboard.hub.workflow.HubWorkflowResource.fetchDashboardWorkflowsByWids
import edu.uci.ics.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource
-import edu.uci.ics.texera.web.resource.dashboard.user.workflow.WorkflowResource.WorkflowWithPrivilege
+import edu.uci.ics.texera.web.resource.dashboard.user.workflow.WorkflowResource.{DashboardWorkflow, WorkflowWithPrivilege}
+import org.jooq.impl.DSL
+import scala.jdk.CollectionConverters._
import java.util
import javax.ws.rs._
import javax.ws.rs.core.MediaType
import org.jooq.types.UInteger
+import java.util.Collections
+
+object HubWorkflowResource {
+ final private lazy val context = SqlServer.createDSLContext()
+
+ def fetchDashboardWorkflowsByWids(wids: Seq[UInteger]): util.List[DashboardWorkflow] = {
+ if (wids.nonEmpty) {
+ context
+ .select(
+ WORKFLOW.NAME,
+ WORKFLOW.DESCRIPTION,
+ WORKFLOW.WID,
+ WORKFLOW.CREATION_TIME,
+ WORKFLOW.LAST_MODIFIED_TIME,
+ USER.NAME.as("ownerName"),
+ WORKFLOW_OF_USER.UID.as("ownerId")
+ )
+ .from(WORKFLOW)
+ .join(WORKFLOW_OF_USER).on(WORKFLOW.WID.eq(WORKFLOW_OF_USER.WID))
+ .join(USER).on(WORKFLOW_OF_USER.UID.eq(USER.UID))
+ .where(WORKFLOW.WID.in(wids: _*))
+ .fetch()
+ .asScala
+ .map(record => {
+ val workflow = new Workflow(
+ record.get(WORKFLOW.NAME),
+ record.get(WORKFLOW.DESCRIPTION),
+ record.get(WORKFLOW.WID),
+ null,
+ record.get(WORKFLOW.CREATION_TIME),
+ record.get(WORKFLOW.LAST_MODIFIED_TIME),
+ null
+ )
+
+ DashboardWorkflow(
+ isOwner = false,
+ accessLevel = "",
+ ownerName = record.get("ownerName", classOf[String]),
+ workflow = workflow,
+ projectIDs = List(),
+ ownerId = record.get("ownerId", classOf[UInteger])
+ )
+ })
+ .toList
+ .asJava
+ } else {
+ Collections.emptyList[DashboardWorkflow]()
+ }
+ }
+}
+
@Produces(Array(MediaType.APPLICATION_JSON))
@Path("/hub/workflow")
class HubWorkflowResource {
@@ -214,7 +268,42 @@ class HubWorkflowResource {
.from(WORKFLOW_USER_CLONES)
.where(WORKFLOW_USER_CLONES.WID.eq(wid))
.fetchOne(0, classOf[Int])
-
cloneCount
}
+
+ @GET
+ @Path("/topLovedWorkflows")
+ @Produces(Array(MediaType.APPLICATION_JSON))
+ def getTopLovedWorkflows: util.List[DashboardWorkflow] = {
+ val topLovedWorkflowsWids = context
+ .select(WORKFLOW_USER_LIKES.WID)
+ .from(WORKFLOW_USER_LIKES)
+ .groupBy(WORKFLOW_USER_LIKES.WID)
+ .orderBy(DSL.count(WORKFLOW_USER_LIKES.WID).desc())
+ .limit(8)
+ .fetchInto(classOf[UInteger])
+ .asScala.toSeq
+
+ println(fetchDashboardWorkflowsByWids(topLovedWorkflowsWids))
+
+ fetchDashboardWorkflowsByWids(topLovedWorkflowsWids)
+ }
+
+ @GET
+ @Path("/topClonedWorkflows")
+ @Produces(Array(MediaType.APPLICATION_JSON))
+ def getTopClonedWorkflows: util.List[DashboardWorkflow] = {
+ val topClonedWorkflowsWids = context
+ .select(WORKFLOW_USER_CLONES.WID)
+ .from(WORKFLOW_USER_CLONES)
+ .groupBy(WORKFLOW_USER_CLONES.WID)
+ .orderBy(DSL.count(WORKFLOW_USER_CLONES.WID).desc())
+ .limit(8)
+ .fetchInto(classOf[UInteger])
+ .asScala.toSeq
+
+ println(fetchDashboardWorkflowsByWids(topClonedWorkflowsWids))
+
+ fetchDashboardWorkflowsByWids(topClonedWorkflowsWids)
+ }
}
diff --git a/core/gui/src/app/app.module.ts b/core/gui/src/app/app.module.ts
index 13a9e45679f..b896e420361 100644
--- a/core/gui/src/app/app.module.ts
+++ b/core/gui/src/app/app.module.ts
@@ -137,6 +137,7 @@ import { GoogleLoginComponent } from "./dashboard/component/user/google-login/go
import { HubWorkflowComponent } from "./hub/component/workflow/hub-workflow.component";
import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component";
import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component";
+import { BrowseSectionComponent } from "./hub/component/browse-section/browse-section.component";
registerLocaleData(en);
@@ -224,6 +225,7 @@ registerLocaleData(en);
HubWorkflowDetailComponent,
GoogleLoginComponent,
LandingPageComponent,
+ BrowseSectionComponent,
],
imports: [
BrowserModule,
diff --git a/core/gui/src/app/dashboard/component/dashboard.component.scss b/core/gui/src/app/dashboard/component/dashboard.component.scss
index f80b1b7cebd..2da4ee9db9f 100644
--- a/core/gui/src/app/dashboard/component/dashboard.component.scss
+++ b/core/gui/src/app/dashboard/component/dashboard.component.scss
@@ -67,6 +67,7 @@ nz-content {
.nav-links {
list-style-type: none;
+ min-height: 70px;
height: 70px;
padding: 10px;
margin: 0;
diff --git a/core/gui/src/app/hub/component/browse-section/browse-section.component.html b/core/gui/src/app/hub/component/browse-section/browse-section.component.html
new file mode 100644
index 00000000000..61de4a51c57
--- /dev/null
+++ b/core/gui/src/app/hub/component/browse-section/browse-section.component.html
@@ -0,0 +1,39 @@
+ 0"
+ class="results-container">
+
{{ sectionTitle }}
+
+
+ {{ workflow.name }}
+ {{ workflow.description || 'No description available' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/core/gui/src/app/hub/component/browse-section/browse-section.component.scss b/core/gui/src/app/hub/component/browse-section/browse-section.component.scss
new file mode 100644
index 00000000000..a6915cf15a0
--- /dev/null
+++ b/core/gui/src/app/hub/component/browse-section/browse-section.component.scss
@@ -0,0 +1,79 @@
+.results-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ padding: 20px;
+
+ .results-title {
+ font-size: 24px;
+ margin-bottom: 20px;
+ text-align: center;
+ }
+
+ .workflow-cards {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ width: 100%;
+ max-width: 1200px;
+
+ .workflow-card {
+ flex: 1 1 calc(25% - 20px);
+ margin: 10px;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+
+ .card-title {
+ font-size: 18px;
+ margin: 10px 0;
+ text-align: center;
+ }
+
+ .card-description {
+ padding: 0 15px;
+ font-size: 14px;
+ color: #666;
+ }
+
+ .footer {
+ margin-top: auto;
+ display: flex;
+ justify-content: space-between;
+ padding: 10px 15px;
+ border-top: 1px solid #eee;
+
+ .footer-text {
+ font-size: 12px;
+ color: #999;
+ }
+ }
+
+ .cover-container {
+ position: relative;
+ width: 100%;
+ height: 150px;
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ img.card-cover-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ .workflow-avatar {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ background-color: grey;
+ }
+ }
+ }
+ }
+}
diff --git a/core/gui/src/app/hub/component/browse-section/browse-section.component.spec.ts b/core/gui/src/app/hub/component/browse-section/browse-section.component.spec.ts
new file mode 100644
index 00000000000..86c8c9a06c8
--- /dev/null
+++ b/core/gui/src/app/hub/component/browse-section/browse-section.component.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+
+import { BrowseSectionComponent } from "./browse-section.component";
+
+describe("BrowseSectionComponent", () => {
+ let component: BrowseSectionComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [BrowseSectionComponent],
+ });
+ fixture = TestBed.createComponent(BrowseSectionComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/core/gui/src/app/hub/component/browse-section/browse-section.component.ts b/core/gui/src/app/hub/component/browse-section/browse-section.component.ts
new file mode 100644
index 00000000000..c1ebacf5334
--- /dev/null
+++ b/core/gui/src/app/hub/component/browse-section/browse-section.component.ts
@@ -0,0 +1,14 @@
+import { Component, Input } from "@angular/core";
+import { DashboardEntry } from "../../../dashboard/type/dashboard-entry";
+
+@Component({
+ selector: "texera-browse-section",
+ templateUrl: "./browse-section.component.html",
+ styleUrls: ["./browse-section.component.scss"],
+})
+export class BrowseSectionComponent{
+ @Input() workflows: DashboardEntry[] = [];
+ @Input() sectionTitle: string = "";
+ defaultBackground: string = "../../../../../assets/card_background.jpg";
+
+}
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.html b/core/gui/src/app/hub/component/landing-page/landing-page.component.html
index 18e960a1e1c..fd5051fc864 100644
--- a/core/gui/src/app/hub/component/landing-page/landing-page.component.html
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.html
@@ -23,4 +23,13 @@ Texera Hub
+
+
+
+
+
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.scss b/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
index b65de40f162..55b4bc03aa4 100644
--- a/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.scss
@@ -7,10 +7,11 @@
.landing-page-container {
display: flex;
flex-direction: column;
- padding: 32px 20px 0 20px;
+ padding: 32px 133px 0 133px;
align-items: center;
height: 100%;
width: 100%;
+ overflow-y: auto;
}
.search-bar {
diff --git a/core/gui/src/app/hub/component/landing-page/landing-page.component.ts b/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
index 4e40c60b8e9..064ece81779 100644
--- a/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
+++ b/core/gui/src/app/hub/component/landing-page/landing-page.component.ts
@@ -1,9 +1,14 @@
import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { HubWorkflowService } from "../../service/workflow/hub-workflow.service";
-import { untilDestroyed } from "@ngneat/until-destroy";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { Router } from "@angular/router";
+import { DashboardWorkflow } from "../../../dashboard/type/dashboard-workflow.interface";
+import { SearchService } from "../../../dashboard/service/user/search.service";
+import { DashboardEntry, UserInfo } from "../../../dashboard/type/dashboard-entry";
+import { map, switchMap } from "rxjs/operators";
+@UntilDestroy()
@Component({
selector: "texera-landing-page",
templateUrl: "./landing-page.component.html",
@@ -11,22 +16,82 @@ import { Router } from "@angular/router";
})
export class LandingPageComponent implements OnInit {
public workflowCount: number = 0;
+ public topLovedWorkflows: DashboardEntry[] = [];
+ public topClonedWorkflows: DashboardEntry[] = [];
constructor(
private hubWorkflowService: HubWorkflowService,
- private router: Router
+ private router: Router,
+ private searchService: SearchService,
) {}
ngOnInit(): void {
this.getWorkflowCount();
+ this.fetchTopWorkflows(
+ this.hubWorkflowService.getTopLovedWorkflows(),
+ (workflows) => (this.topLovedWorkflows = workflows),
+ "Top Loved Workflows"
+ );
+ this.fetchTopWorkflows(
+ this.hubWorkflowService.getTopClonedWorkflows(),
+ (workflows) => (this.topClonedWorkflows = workflows),
+ "Top Cloned Workflows"
+ );
}
getWorkflowCount(): void {
this.hubWorkflowService
.getWorkflowCount()
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil
+ .pipe(untilDestroyed(this))
.subscribe((count: number) => {
this.workflowCount = count;
+ console.log("Workflow count:", this.workflowCount);
+ });
+ }
+
+ /**
+ * Helper function to fetch top workflows and associate user info with them.
+ * @param workflowsObservable Observable that returns workflows (Top Loved or Top Cloned)
+ * @param updateWorkflowsFn Function to update the component's workflow state
+ * @param workflowType Label for logging
+ */
+ fetchTopWorkflows(
+ workflowsObservable: Observable
,
+ updateWorkflowsFn: (entries: DashboardEntry[]) => void,
+ workflowType: string
+ ): void {
+ workflowsObservable
+ .pipe(
+ // eslint-disable-next-line rxjs/no-unsafe-takeuntil
+ untilDestroyed(this),
+ // 提取出所有者的 userIds
+ map((workflows: DashboardWorkflow[]) => {
+ const userIds = new Set();
+ workflows.forEach((workflow) => {
+ userIds.add(workflow.ownerId);
+ });
+ return { workflows, userIds: Array.from(userIds) };
+ }),
+ switchMap(({ workflows, userIds }) =>
+ this.searchService.getUserInfo(userIds).pipe(
+ map((userIdToInfoMap: { [key: number]: UserInfo }) => {
+ const dashboardEntries = workflows.map((workflow) => {
+ const userInfo = userIdToInfoMap[workflow.ownerId];
+ const entry = new DashboardEntry(workflow);
+ if (userInfo) {
+ entry.setOwnerName(userInfo.userName);
+ entry.setOwnerGoogleAvatar(userInfo.googleAvatar ?? "");
+ }
+ return entry;
+ });
+ return dashboardEntries;
+ })
+ )
+ )
+ )
+ .subscribe((dashboardEntries: DashboardEntry[]) => {
+ updateWorkflowsFn(dashboardEntries);
+ console.log(`${workflowType} with Owner Info:`, dashboardEntries);
});
}
diff --git a/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts b/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts
index d9f2a0cf673..b80fa74a274 100644
--- a/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts
+++ b/core/gui/src/app/hub/service/workflow/hub-workflow.service.ts
@@ -7,6 +7,7 @@ import { User } from "src/app/common/type/user";
import { Workflow } from "../../../common/type/workflow";
import { filter, map } from "rxjs/operators";
import { WorkflowUtilService } from "../../../workspace/service/workflow-graph/util/workflow-util.service";
+import { DashboardWorkflow } from "../../../dashboard/type/dashboard-workflow.interface";
export const WORKFLOW_BASE_URL = `${AppSettings.getApiEndpoint()}/workflow`;
@@ -73,4 +74,12 @@ export class HubWorkflowService {
public getCloneCount(wid: number): Observable {
return this.http.get(`${this.BASE_URL}/cloneCount/${wid}`);
}
+
+ public getTopLovedWorkflows(): Observable {
+ return this.http.get(`${this.BASE_URL}/topLovedWorkflows`);
+ }
+
+ public getTopClonedWorkflows(): Observable {
+ return this.http.get(`${this.BASE_URL}/topClonedWorkflows`);
+ }
}
diff --git a/core/gui/src/assets/card_background.jpg b/core/gui/src/assets/card_background.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..d3da0bf766600b64a94a9f2f70532072762308ab
GIT binary patch
literal 414278
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