diff --git a/server/application-server/openapi.yaml b/server/application-server/openapi.yaml index 756e4e6e..1e688a30 100644 --- a/server/application-server/openapi.yaml +++ b/server/application-server/openapi.yaml @@ -61,6 +61,24 @@ paths: application/json: schema: $ref: "#/components/schemas/UserDTO" + /user/{login}/full: + get: + tags: + - user + operationId: getFullUser + parameters: + - name: login + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/User" /pullrequest/{id}: get: tags: @@ -101,6 +119,20 @@ paths: type: array items: $ref: "#/components/schemas/PullRequest" + /leaderboard: + get: + tags: + - leaderboard + operationId: getLeaderboard + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/LeaderboardEntry" /admin: get: tags: @@ -210,8 +242,6 @@ components: items: $ref: "#/components/schemas/IssueCommentDTO" IssueComment: - required: - - body type: object properties: id: @@ -300,12 +330,38 @@ components: submittedAt: type: string format: date-time + comments: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/PullRequestReviewComment" pullRequest: $ref: "#/components/schemas/PullRequest" + PullRequestReviewComment: + required: + - commit + type: object + properties: + id: + type: integer + format: int64 + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + body: + type: string + author: + $ref: "#/components/schemas/User" + review: + $ref: "#/components/schemas/PullRequestReview" + commit: + type: string Repository: required: - defaultBranch - - description - name - nameWithOwner - url @@ -382,13 +438,40 @@ components: type: array items: $ref: "#/components/schemas/PullRequest" - comments: + issueComments: uniqueItems: true type: array items: $ref: "#/components/schemas/IssueComment" + reviewComments: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/PullRequestReviewComment" reviews: uniqueItems: true type: array items: $ref: "#/components/schemas/PullRequestReview" + LeaderboardEntry: + type: object + properties: + githubName: + type: string + name: + type: string + score: + type: integer + format: int32 + total: + type: integer + format: int32 + changesRequested: + type: integer + format: int32 + approvals: + type: integer + format: int32 + comments: + type: integer + format: int32 diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/Comment.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/Comment.java index d0b8ce46..3070de87 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/Comment.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/Comment.java @@ -1,10 +1,9 @@ package de.tum.in.www1.hephaestus.codereview.base; import de.tum.in.www1.hephaestus.codereview.user.User; -import jakarta.persistence.Basic; +import jakarta.persistence.Column; import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; -import jakarta.persistence.Lob; import jakarta.persistence.ManyToOne; import jakarta.persistence.MappedSuperclass; import lombok.AllArgsConstructor; @@ -20,11 +19,10 @@ @AllArgsConstructor @ToString(callSuper = true) public abstract class Comment extends BaseGitServiceEntity { - @Lob - @Basic(fetch = FetchType.EAGER) + @Column(columnDefinition = "TEXT") protected String body; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "author_id") @ToString.Exclude protected User author; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueComment.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueComment.java index 32d7adfe..103f68c7 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueComment.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueComment.java @@ -18,7 +18,7 @@ @NoArgsConstructor @ToString(callSuper = true) public class IssueComment extends Comment { - @ManyToOne(optional = false) + @ManyToOne @JoinColumn(name = "pullrequest_id", referencedColumnName = "id") @ToString.Exclude private PullRequest pullRequest; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/review/PullRequestReviewComment.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/review/PullRequestReviewComment.java new file mode 100644 index 00000000..bdf18610 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/review/PullRequestReviewComment.java @@ -0,0 +1,31 @@ +package de.tum.in.www1.hephaestus.codereview.comment.review; + +import org.springframework.lang.NonNull; + +import de.tum.in.www1.hephaestus.codereview.base.Comment; +import de.tum.in.www1.hephaestus.codereview.pullrequest.review.PullRequestReview; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Entity +@Table(name = "pull_request_review_comment") +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class PullRequestReviewComment extends Comment { + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "review_id", referencedColumnName = "id") + @ToString.Exclude + private PullRequestReview review; + + @NonNull + private String commit; +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/review/PullRequestReviewCommentConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/review/PullRequestReviewCommentConverter.java new file mode 100644 index 00000000..6eb03c59 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/review/PullRequestReviewCommentConverter.java @@ -0,0 +1,22 @@ +package de.tum.in.www1.hephaestus.codereview.comment.review; + +import org.kohsuke.github.GHPullRequestReviewComment; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntityConverter; + +@Component +public class PullRequestReviewCommentConverter + extends BaseGitServiceEntityConverter { + + @Override + public PullRequestReviewComment convert(@NonNull GHPullRequestReviewComment source) { + PullRequestReviewComment comment = new PullRequestReviewComment(); + convertBaseFields(source, comment); + comment.setBody(source.getBody()); + comment.setCommit(source.getCommitId()); + return comment; + } + +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/review/PullRequestReviewCommentRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/review/PullRequestReviewCommentRepository.java new file mode 100644 index 00000000..49e9160a --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/review/PullRequestReviewCommentRepository.java @@ -0,0 +1,7 @@ +package de.tum.in.www1.hephaestus.codereview.comment.review; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PullRequestReviewCommentRepository extends JpaRepository { + +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequest.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequest.java index 114b8073..669815be 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequest.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequest.java @@ -45,7 +45,7 @@ public class PullRequest extends BaseGitServiceEntity { private OffsetDateTime mergedAt; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "author_id") @ToString.Exclude private User author; @@ -58,7 +58,7 @@ public class PullRequest extends BaseGitServiceEntity { @ToString.Exclude private Set reviews = new HashSet<>(); - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne @JoinColumn(name = "repository_id", referencedColumnName = "id") @ToString.Exclude private Repository repository; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReview.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReview.java index 3a9b3253..daf1a197 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReview.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReview.java @@ -7,7 +7,7 @@ import org.springframework.lang.NonNull; import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntity; -import de.tum.in.www1.hephaestus.codereview.comment.review.ReviewComment; +import de.tum.in.www1.hephaestus.codereview.comment.review.PullRequestReviewComment; import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequest; import de.tum.in.www1.hephaestus.codereview.user.User; import jakarta.persistence.CascadeType; @@ -30,7 +30,7 @@ @ToString(callSuper = true) public class PullRequestReview extends BaseGitServiceEntity { - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "author_id") @ToString.Exclude private User author; @@ -38,22 +38,18 @@ public class PullRequestReview extends BaseGitServiceEntity { @NonNull private PullRequestReviewState state; - private OffsetDateTime createdAt; - private OffsetDateTime submittedAt; - private OffsetDateTime updatedAt; - @OneToMany(cascade = CascadeType.ALL, mappedBy = "review") @ToString.Exclude - private Set comments = new HashSet<>(); + private Set comments = new HashSet<>(); - @ManyToOne(optional = false) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "pullrequest_id", referencedColumnName = "id") @ToString.Exclude private PullRequest pullRequest; - public void addComment(ReviewComment comment) { + public void addComment(PullRequestReviewComment comment) { comments.add(comment); } } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReviewConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReviewConverter.java index 89f81f52..e9396ed1 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReviewConverter.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReviewConverter.java @@ -20,21 +20,11 @@ public PullRequestReview convert(@NonNull GHPullRequestReview source) { PullRequestReview review = new PullRequestReview(); convertBaseFields(source, review); review.setState(convertState(source.getState())); - try { - review.setCreatedAt(convertToOffsetDateTime(source.getCreatedAt())); - } catch (IOException e) { - logger.error("Failed to convert submittedAt field for source {}: {}", source.getId(), e.getMessage()); - } try { review.setSubmittedAt(convertToOffsetDateTime(source.getSubmittedAt())); } catch (IOException e) { logger.error("Failed to convert submittedAt field for source {}: {}", source.getId(), e.getMessage()); } - try { - review.setUpdatedAt(convertToOffsetDateTime(source.getUpdatedAt())); - } catch (IOException e) { - logger.error("Failed to convert submittedAt field for source {}: {}", source.getId(), e.getMessage()); - } return review; } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReviewRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReviewRepository.java index 99956827..ad2d2ccd 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReviewRepository.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/review/PullRequestReviewRepository.java @@ -3,6 +3,7 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository @@ -11,4 +12,12 @@ public interface PullRequestReviewRepository extends JpaRepository findByAuthor_Login(String authorLogin); Optional findByPullRequest(PullRequestReview pullRequest); + + @Query(""" + SELECT pr + FROM PullRequestReview pr + JOIN FETCH pr.comments + WHERE pr.id = :reviewId + """) + Optional findByIdWithEagerComments(Long reviewId); } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/Repository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/Repository.java index 14b7f422..7567c99f 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/Repository.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/Repository.java @@ -10,7 +10,6 @@ import io.micrometer.common.lang.Nullable; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import lombok.Getter; @@ -45,7 +44,7 @@ public class Repository extends BaseGitServiceEntity { String homepage; - @OneToMany(cascade = CascadeType.ALL, mappedBy = "repository", fetch = FetchType.EAGER) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "repository") @ToString.Exclude private Set pullRequests = new HashSet<>(); } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryRepository.java index 836d8ffb..dc25104e 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryRepository.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryRepository.java @@ -1,10 +1,19 @@ package de.tum.in.www1.hephaestus.codereview.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; @org.springframework.stereotype.Repository public interface RepositoryRepository extends JpaRepository { Repository findByNameWithOwner(String nameWithOwner); + + @Query(""" + SELECT r + FROM Repository r + JOIN FETCH r.pullRequests + WHERE r.nameWithOwner = :nameWithOwner + """) + Repository findByNameWithOwnerWithEagerPullRequests(String nameWithOwner); } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/User.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/User.java index 7ba4b3b0..61052c8b 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/User.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/User.java @@ -9,7 +9,7 @@ import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntity; import de.tum.in.www1.hephaestus.codereview.comment.IssueComment; -import de.tum.in.www1.hephaestus.codereview.comment.review.ReviewComment; +import de.tum.in.www1.hephaestus.codereview.comment.review.PullRequestReviewComment; import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequest; import de.tum.in.www1.hephaestus.codereview.pullrequest.review.PullRequestReview; import jakarta.persistence.CascadeType; @@ -64,7 +64,7 @@ public class User extends BaseGitServiceEntity { private Set issueComments = new HashSet<>(); @OneToMany(cascade = CascadeType.ALL, mappedBy = "author") - private Set reviewComments = new HashSet<>(); + private Set reviewComments = new HashSet<>(); @OneToMany(cascade = CascadeType.ALL, mappedBy = "author") private Set reviews = new HashSet<>(); @@ -73,7 +73,7 @@ public void addIssueComment(IssueComment comment) { issueComments.add(comment); } - public void addReviewComment(ReviewComment comment) { + public void addReviewComment(PullRequestReviewComment comment) { reviewComments.add(comment); } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserController.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserController.java index bc31c831..3e264422 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserController.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserController.java @@ -23,4 +23,10 @@ public ResponseEntity getUser(@PathVariable String login) { return user.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } + @GetMapping("/{login}/full") + public ResponseEntity getFullUser(@PathVariable String login) { + Optional user = userService.getUser(login); + System.out.println(user); + return user.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + } } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserConverter.java index 2d26e0fb..96d4341a 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserConverter.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserConverter.java @@ -23,9 +23,13 @@ public User convert(@NonNull org.kohsuke.github.GHUser source) { user.setAvatarUrl(source.getAvatarUrl()); try { user.setName(source.getName()); + } catch (IOException e) { + logger.error("Failed to convert user name field for source {}: {}", source.getId(), e.getMessage()); + } + try { user.setEmail(source.getEmail()); } catch (IOException e) { - logger.error("Failed to convert user fields for source {}: {}", source.getId(), e.getMessage()); + logger.error("Failed to convert user email field for source {}: {}", source.getId(), e.getMessage()); } return user; } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserRepository.java index 64696cb4..ebba4e74 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserRepository.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserRepository.java @@ -1,5 +1,6 @@ package de.tum.in.www1.hephaestus.codereview.user; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -11,10 +12,31 @@ public interface UserRepository extends JpaRepository { @Query("SELECT u FROM User u WHERE u.login = :login") Optional findUser(@Param("login") String login); + @Query(""" + SELECT u + FROM User u + JOIN FETCH u.pullRequests + JOIN FETCH u.issueComments + JOIN FETCH u.reviewComments + JOIN FETCH u.reviews + WHERE u.login = :login + """) + Optional findUserEagerly(@Param("login") String login); + @Query(""" SELECT new UserDTO(u.id, u.login, u.email, u.name, u.url) FROM User u WHERE u.login = :login """) Optional findByLogin(@Param("login") String login); + + @Query(""" + SELECT u + FROM User u + JOIN FETCH u.pullRequests + JOIN FETCH u.issueComments + JOIN FETCH u.reviewComments + JOIN FETCH u.reviews + """) + List findAllWithRelations(); } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserService.java index 1c3703a3..1913c8ef 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserService.java @@ -1,5 +1,6 @@ package de.tum.in.www1.hephaestus.codereview.user; +import java.util.List; import java.util.Optional; import org.slf4j.Logger; @@ -25,4 +26,9 @@ public Optional getUserDTO(String login) { logger.info("Getting userDTO with login: " + login); return userRepository.findByLogin(login); } + + public List getAllUsers() { + logger.info("Getting all users"); + return userRepository.findAll().stream().toList(); + } } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardController.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardController.java new file mode 100644 index 00000000..ed32ba59 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardController.java @@ -0,0 +1,24 @@ +package de.tum.in.www1.hephaestus.leaderboard; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/leaderboard") +public class LeaderboardController { + + private final LeaderboardService leaderboardService; + + public LeaderboardController(LeaderboardService leaderboardService) { + this.leaderboardService = leaderboardService; + } + + @GetMapping + public ResponseEntity> getLeaderboard() { + return ResponseEntity.ok(leaderboardService.createLeaderboard()); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardEntry.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardEntry.java new file mode 100644 index 00000000..d60fc006 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardEntry.java @@ -0,0 +1,8 @@ +package de.tum.in.www1.hephaestus.leaderboard; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record LeaderboardEntry(String githubName, String name, int score, int total, int changesRequested, + int approvals, int comments) { +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardService.java new file mode 100644 index 00000000..0ceec0d3 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardService.java @@ -0,0 +1,52 @@ +package de.tum.in.www1.hephaestus.leaderboard; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.hephaestus.codereview.user.User; +import de.tum.in.www1.hephaestus.codereview.user.UserService; + +@Service +public class LeaderboardService { + private static final Logger logger = LoggerFactory.getLogger(LeaderboardService.class); + + private final UserService userService; + + public LeaderboardService(UserService userService) { + this.userService = userService; + } + + public List createLeaderboard() { + logger.info("Creating leaderboard dataset"); + + List users = userService.getAllUsers(); + logger.info("Found " + users.size() + " users"); + + List leaderboard = users.stream().map(user -> { + logger.info("Creating leaderboard entry for user: " + user.getLogin()); + int comments = user.getIssueComments().size(); + AtomicInteger changesRequested = new AtomicInteger(0); + AtomicInteger changesApproved = new AtomicInteger(0); + user.getReviews().stream().forEach(review -> { + switch (review.getState()) { + case CHANGES_REQUESTED: + changesRequested.incrementAndGet(); + break; + case APPROVED: + changesApproved.incrementAndGet(); + break; + default: + break; + } + }); + return new LeaderboardEntry(user.getLogin(), user.getName(), 0, 0, changesRequested.get(), + changesApproved.get(), comments); + }).toList(); + + return leaderboard; + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncScheduler.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncScheduler.java index 5cbafffa..4229f91b 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncScheduler.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncScheduler.java @@ -3,13 +3,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component -public class GitHubDataSyncScheduler implements ApplicationRunner { +public class GitHubDataSyncScheduler { private static final Logger logger = LoggerFactory.getLogger(GitHubDataSyncScheduler.class); private final GitHubDataSyncService dataSyncService; @@ -17,18 +17,15 @@ public class GitHubDataSyncScheduler implements ApplicationRunner { @Value("${monitoring.runOnStartup:true}") private boolean runOnStartup; - @Value("${monitoring.repositories}") - private String[] repositoriesToMonitor; - public GitHubDataSyncScheduler(GitHubDataSyncService dataSyncService) { this.dataSyncService = dataSyncService; } - @Override - public void run(ApplicationArguments args) throws Exception { + @EventListener(ApplicationReadyEvent.class) + public void run() { if (runOnStartup) { - logger.info("Starting initial GitHub data sync for Hephaestus..."); - syncData(); + logger.info("Starting initial GitHub data sync..."); + dataSyncService.syncData(); logger.info("Initial GitHub data sync completed."); } } @@ -36,21 +33,7 @@ public void run(ApplicationArguments args) throws Exception { @Scheduled(cron = "${monitoring.repository-sync-cron}") public void syncDataCron() { logger.info("Starting scheduled GitHub data sync..."); - syncData(); + dataSyncService.syncData(); logger.info("Scheduled GitHub data sync completed."); } - - private void syncData() { - int successfullySyncedRepositories = 0; - for (String repositoryName : repositoriesToMonitor) { - try { - dataSyncService.syncRepository(repositoryName); - logger.info("GitHub data sync completed successfully for repository: " + repositoryName); - successfullySyncedRepositories++; - } catch (Exception e) { - logger.error("Error during GitHub data sync: ", e); - } - } - logger.info("GitHub data sync completed for " + successfullySyncedRepositories + "/" + repositoriesToMonitor.length + " repositories."); - } } \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncService.java index 4bfd62ae..6cc99c55 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncService.java @@ -1,11 +1,16 @@ package de.tum.in.www1.hephaestus.scheduler; import java.io.IOException; +import java.util.Collection; +import java.util.Date; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import org.kohsuke.github.GHObject; import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHPullRequestReviewComment; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; import org.kohsuke.github.GitHubBuilder; @@ -18,9 +23,9 @@ import de.tum.in.www1.hephaestus.codereview.comment.IssueComment; import de.tum.in.www1.hephaestus.codereview.comment.IssueCommentConverter; import de.tum.in.www1.hephaestus.codereview.comment.IssueCommentRepository; -import de.tum.in.www1.hephaestus.codereview.comment.review.ReviewComment; -import de.tum.in.www1.hephaestus.codereview.comment.review.ReviewCommentConverter; -import de.tum.in.www1.hephaestus.codereview.comment.review.ReviewCommentRepository; +import de.tum.in.www1.hephaestus.codereview.comment.review.PullRequestReviewComment; +import de.tum.in.www1.hephaestus.codereview.comment.review.PullRequestReviewCommentConverter; +import de.tum.in.www1.hephaestus.codereview.comment.review.PullRequestReviewCommentRepository; import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequest; import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequestConverter; import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequestRepository; @@ -41,29 +46,35 @@ public class GitHubDataSyncService { @Value("${github.authToken:null}") private String ghAuthToken; + @Value("${monitoring.repositories}") + private String[] repositoriesToMonitor; + + @Value("${monitoring.timeframe}") + private int timeframe; + private GitHub github; private final RepositoryRepository repositoryRepository; private final PullRequestRepository pullRequestRepository; private final PullRequestReviewRepository prReviewRepository; private final IssueCommentRepository commentRepository; - private final ReviewCommentRepository reviewCommentRepository; + private final PullRequestReviewCommentRepository reviewCommentRepository; private final UserRepository userRepository; private final RepositoryConverter repositoryConverter; private final PullRequestConverter pullRequestConverter; private final PullRequestReviewConverter reviewConverter; private final IssueCommentConverter commentConverter; - private final ReviewCommentConverter reviewCommentConverter; + private final PullRequestReviewCommentConverter reviewCommentConverter; private final UserConverter userConverter; public GitHubDataSyncService(RepositoryRepository repositoryRepository, PullRequestRepository pullRequestRepository, PullRequestReviewRepository prReviewRepository, - IssueCommentRepository commentRepository, ReviewCommentRepository reviewCommentRepository, + IssueCommentRepository commentRepository, PullRequestReviewCommentRepository reviewCommentRepository, UserRepository userRepository, RepositoryConverter repositoryConverter, PullRequestConverter pullRequestConverter, PullRequestReviewConverter reviewConverter, IssueCommentConverter commentConverter, - ReviewCommentConverter reviewCommentConverter, UserConverter userConverter) { + PullRequestReviewCommentConverter reviewCommentConverter, UserConverter userConverter) { logger.info("Hello from GitHubDataSyncService!"); this.repositoryRepository = repositoryRepository; @@ -81,7 +92,23 @@ public GitHubDataSyncService(RepositoryRepository repositoryRepository, PullRequ this.userConverter = userConverter; } - public void syncRepository(String repositoryName) throws IOException { + public void syncData() { + int successfullySyncedRepositories = 0; + for (String repositoryName : repositoriesToMonitor) { + try { + syncRepository(repositoryName); + logger.info("GitHub data sync completed successfully for repository: " + repositoryName); + successfullySyncedRepositories++; + } catch (Exception e) { + logger.error("Error during GitHub data sync of repository " + repositoryName + ": " + e.getMessage()); + e.printStackTrace(); + } + } + logger.info("GitHub data sync completed for " + successfullySyncedRepositories + "/" + + repositoriesToMonitor.length + " repositories for the last " + timeframe + " day(s)."); + } + + private void syncRepository(String repositoryName) throws IOException { if (ghAuthToken == null || ghAuthToken.isEmpty() || ghAuthToken.equals("null")) { logger.error("No GitHub auth token provided!"); return; @@ -99,7 +126,6 @@ public void syncRepository(String repositoryName) throws IOException { * @return The repository corresponding to the given nameWithOwner. * @throws IOException */ - @Transactional public Repository fetchRepository(String nameWithOwner) throws IOException { if (github == null) { logger.error("GitHub client not initialized correctly!"); @@ -107,7 +133,7 @@ public Repository fetchRepository(String nameWithOwner) throws IOException { } // Avoid double fetching of the same repository - Repository existingRepository = repositoryRepository.findByNameWithOwner(nameWithOwner); + Repository existingRepository = repositoryRepository.findByNameWithOwnerWithEagerPullRequests(nameWithOwner); if (existingRepository != null) { logger.info("Found existing repository: " + existingRepository); return existingRepository; @@ -120,9 +146,10 @@ public Repository fetchRepository(String nameWithOwner) throws IOException { return null; } // preliminary save to make it referenceable - repositoryRepository.save(repository); + repository = repositoryRepository.save(repository); Set prs = getPullRequestsFromGHRepository(ghRepo, repository); + logger.info("Found " + prs.size() + " PRs"); repository.setPullRequests(prs); pullRequestRepository.saveAll(prs); @@ -133,72 +160,83 @@ public Repository fetchRepository(String nameWithOwner) throws IOException { private Set getPullRequestsFromGHRepository(GHRepository ghRepo, Repository repository) throws IOException { // Retrieve PRs in pages of 10 - return ghRepo.queryPullRequests().list().withPageSize(10).toList().stream().map(pr -> { - PullRequest pullRequest = pullRequestConverter.convert(pr); - pullRequest.setRepository(repository); - pullRequestRepository.save(pullRequest); - try { - Set comments = getCommentsFromGHPullRequest(pr, pullRequest); - pullRequest.setComments(comments); - commentRepository.saveAll(comments); - } catch (IOException e) { - logger.error("Error while fetching PR comments!"); - pullRequest.setComments(new HashSet<>()); - } - try { - User prAuthor = getUserFromGHUser(pr.getUser()); - prAuthor.addPullRequest(pullRequest); - pullRequest.setAuthor(prAuthor); - } catch (IOException e) { - logger.error("Error while fetching PR author!"); - pullRequest.setAuthor(null); - } - - try { - Set reviews = pr.listReviews().toList().stream().map(review -> { - PullRequestReview prReview = reviewConverter.convert(review); + return ghRepo.queryPullRequests().list().withPageSize(20).toList().stream() + .takeWhile(pr -> isResourceRecent(pr)).map(pr -> { + PullRequest pullRequest = pullRequestRepository.save(pullRequestConverter.convert(pr)); + pullRequest.setRepository(repository); try { - User reviewAuthor = getUserFromGHUser(review.getUser()); - reviewAuthor.addReview(prReview); - prReview.setAuthor(reviewAuthor); + Collection comments = getCommentsFromGHPullRequest(pr, pullRequest); + comments = commentRepository.saveAll(comments); + for (IssueComment c : comments) { + pullRequest.addComment(c); + } } catch (IOException e) { - logger.error("Error while fetching review owner!"); + logger.error("Error while fetching PR comments!"); + pullRequest.setComments(new HashSet<>()); + } + try { + User prAuthor = getUserFromGHUser(pr.getUser()); + prAuthor.addPullRequest(pullRequest); + pullRequest.setAuthor(prAuthor); + } catch (IOException e) { + logger.error("Error while fetching PR author!"); + pullRequest.setAuthor(null); } - prReview.setPullRequest(pullRequest); - return prReview; - }).collect(Collectors.toSet()); - prReviewRepository.saveAll(reviews); - pullRequest.setReviews(reviews); - } catch (IOException e) { - logger.error("Error while fetching PR reviews!"); - pullRequest.setReviews(new HashSet<>()); - } - try { - pr.listReviewComments().toList().stream().forEach(comment -> { - ReviewComment c = reviewCommentConverter.convert(comment); - // First save the comment, so that it is referencable - reviewCommentRepository.save(c); + try { + Collection reviews = pr.listReviews().toList().stream() + .takeWhile(prr -> isResourceRecent(prr)).map(review -> { + PullRequestReview prReview = prReviewRepository + .save(reviewConverter.convert(review)); + try { + User reviewAuthor = getUserFromGHUser(review.getUser()); + reviewAuthor.addReview(prReview); + prReview.setAuthor(reviewAuthor); + } catch (IOException e) { + logger.error("Error while fetching review owner!"); + } + prReview.setPullRequest(pullRequest); + return prReview; + }).collect(Collectors.toSet()); + reviews = prReviewRepository.saveAll(reviews); + for (PullRequestReview prReview : reviews) { + pullRequest.addReview(prReview); + } + } catch (IOException e) { + logger.error("Error while fetching PR reviews!"); + pullRequest.setReviews(new HashSet<>()); + } - PullRequestReview review = getPullRequestReviewByReviewId(comment.getPullRequestReviewId()); - c.setReview(review); - User commentAuthor; try { - commentAuthor = getUserFromGHUser(comment.getUser()); - commentAuthor.addReviewComment(c); + pr.listReviewComments().withPageSize(20).toList().stream().takeWhile(rc -> isResourceRecent(rc)) + .forEach(c -> handleSinglePullRequestReviewComment(c)); } catch (IOException e) { - logger.error("Error while fetching author!"); - commentAuthor = null; + logger.error("Error while fetching PR review comments!"); } - c.setAuthor(commentAuthor); - review.addComment(c); - }); + + return pullRequest; + }).collect(Collectors.toSet()); + } + + private void handleSinglePullRequestReviewComment(GHPullRequestReviewComment comment) { + PullRequestReviewComment c = reviewCommentRepository.save(reviewCommentConverter.convert(comment)); + + Optional review = prReviewRepository + .findByIdWithEagerComments(comment.getPullRequestReviewId()); + if (review.isPresent()) { + PullRequestReview prReview = review.get(); + c.setReview(prReview); + User commentAuthor; + try { + commentAuthor = getUserFromGHUser(comment.getUser()); + commentAuthor.addReviewComment(c); } catch (IOException e) { - logger.error("Error while fetching PR review comments!"); + logger.error("Error while fetching author!"); + commentAuthor = null; } - - return pullRequest; - }).collect(Collectors.toSet()); + c.setAuthor(commentAuthor); + prReview.addComment(c); + } } /** @@ -212,9 +250,9 @@ private Set getPullRequestsFromGHRepository(GHRepository ghRepo, Re @Transactional private Set getCommentsFromGHPullRequest(GHPullRequest pr, PullRequest pullRequest) throws IOException { - return pr.queryComments().list().toList().stream() + return pr.queryComments().list().withPageSize(20).toList().stream() .map(comment -> { - IssueComment c = commentConverter.convert(comment); + IssueComment c = commentRepository.save(commentConverter.convert(comment)); c.setPullRequest(pullRequest); User commentAuthor; try { @@ -229,17 +267,26 @@ private Set getCommentsFromGHPullRequest(GHPullRequest pr, PullReq }).collect(Collectors.toSet()); } - private PullRequestReview getPullRequestReviewByReviewId(Long reviewId) { - return prReviewRepository.findById(reviewId).orElse(null); - } - private User getUserFromGHUser(org.kohsuke.github.GHUser user) { - User ghUser = userRepository.findUser(user.getLogin()).orElse(null); + User ghUser = userRepository.findUserEagerly(user.getLogin()).orElse(null); if (ghUser == null) { - ghUser = userConverter.convert(user); - userRepository.save(ghUser); + ghUser = userRepository.save(userConverter.convert(user)); } return ghUser; + } + /** + * Checks if the resource has been created within the last week. + * + * @param obj + * @return + */ + private boolean isResourceRecent(GHObject obj) { + try { + return obj.getCreatedAt().after(new Date(System.currentTimeMillis() - 1000 * 60 * 60 * 24 * timeframe)); + } catch (IOException e) { + logger.error("Error while fetching createdAt! Resource ID: " + obj.getId()); + return false; + } } } diff --git a/server/application-server/src/main/resources/application.yml b/server/application-server/src/main/resources/application.yml index a0d667ff..81e3ab31 100644 --- a/server/application-server/src/main/resources/application.yml +++ b/server/application-server/src/main/resources/application.yml @@ -10,7 +10,7 @@ spring: jpa: hibernate: # We use update for development and none for production, as we use liquibase - ddl-auto: update + ddl-auto: create-drop # show-sql: "true" properties: @@ -32,8 +32,10 @@ springdoc: monitoring: runOnStartup: true + # Fetching timeframe in days + timeframe: 7 repository-sync-cron: "0 0 3 * * ?" - repositories: ls1intum/Artemis, ls1intum/Pyris, ls1intum/Athena, ls1intum/Athena-CoFee, ls1intum/artemis-ansible-collection, ls1intum/Ares, ls1intum/Ares2, ls1intum/Aeolus, ls1intum/hades, ls1intum/Apollon, ls1intum/Hephaestus, ls1intum/Apollon_standalone + repositories: ls1intum/Artemis # Can be any OAuth token, such as the PAT github: diff --git a/webapp/src/app/core/modules/openapi/.openapi-generator/FILES b/webapp/src/app/core/modules/openapi/.openapi-generator/FILES index d3453c09..3e15b536 100644 --- a/webapp/src/app/core/modules/openapi/.openapi-generator/FILES +++ b/webapp/src/app/core/modules/openapi/.openapi-generator/FILES @@ -7,6 +7,8 @@ api/admin.serviceInterface.ts api/api.ts api/hello.service.ts api/hello.serviceInterface.ts +api/leaderboard.service.ts +api/leaderboard.serviceInterface.ts api/pull-request.service.ts api/pull-request.serviceInterface.ts api/user.service.ts @@ -18,8 +20,10 @@ index.ts model/hello.ts model/issue-comment-dto.ts model/issue-comment.ts +model/leaderboard-entry.ts model/models.ts model/pull-request-dto.ts +model/pull-request-review-comment.ts model/pull-request-review.ts model/pull-request.ts model/repository-dto.ts diff --git a/webapp/src/app/core/modules/openapi/api/api.ts b/webapp/src/app/core/modules/openapi/api/api.ts index 49c6e262..cff92dd4 100644 --- a/webapp/src/app/core/modules/openapi/api/api.ts +++ b/webapp/src/app/core/modules/openapi/api/api.ts @@ -4,10 +4,13 @@ export * from './admin.serviceInterface'; export * from './hello.service'; import { HelloService } from './hello.service'; export * from './hello.serviceInterface'; +export * from './leaderboard.service'; +import { LeaderboardService } from './leaderboard.service'; +export * from './leaderboard.serviceInterface'; export * from './pull-request.service'; import { PullRequestService } from './pull-request.service'; export * from './pull-request.serviceInterface'; export * from './user.service'; import { UserService } from './user.service'; export * from './user.serviceInterface'; -export const APIS = [AdminService, HelloService, PullRequestService, UserService]; +export const APIS = [AdminService, HelloService, LeaderboardService, PullRequestService, UserService]; diff --git a/webapp/src/app/core/modules/openapi/api/leaderboard.service.ts b/webapp/src/app/core/modules/openapi/api/leaderboard.service.ts new file mode 100644 index 00000000..7f01bf4a --- /dev/null +++ b/webapp/src/app/core/modules/openapi/api/leaderboard.service.ts @@ -0,0 +1,157 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { LeaderboardEntry } from '../model/leaderboard-entry'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { + LeaderboardServiceInterface +} from './leaderboard.serviceInterface'; + + + +@Injectable({ + providedIn: 'root' +}) +export class LeaderboardService implements LeaderboardServiceInterface { + + protected basePath = 'http://localhost'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getLeaderboard(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getLeaderboard(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getLeaderboard(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getLeaderboard(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/leaderboard`; + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/webapp/src/app/core/modules/openapi/api/leaderboard.serviceInterface.ts b/webapp/src/app/core/modules/openapi/api/leaderboard.serviceInterface.ts new file mode 100644 index 00000000..dcd0ca27 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/api/leaderboard.serviceInterface.ts @@ -0,0 +1,33 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { HttpHeaders } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { LeaderboardEntry } from '../model/models'; + + +import { Configuration } from '../configuration'; + + + +export interface LeaderboardServiceInterface { + defaultHeaders: HttpHeaders; + configuration: Configuration; + + /** + * + * + */ + getLeaderboard(extraHttpRequestParams?: any): Observable>; + +} diff --git a/webapp/src/app/core/modules/openapi/api/user.service.ts b/webapp/src/app/core/modules/openapi/api/user.service.ts index 4806a28d..2e8ba927 100644 --- a/webapp/src/app/core/modules/openapi/api/user.service.ts +++ b/webapp/src/app/core/modules/openapi/api/user.service.ts @@ -18,6 +18,8 @@ import { HttpClient, HttpHeaders, HttpParams, import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; +// @ts-ignore +import { User } from '../model/user'; // @ts-ignore import { UserDTO } from '../model/user-dto'; @@ -95,6 +97,69 @@ export class UserService implements UserServiceInterface { return httpParams; } + /** + * @param login + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getFullUser(login: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getFullUser(login: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getFullUser(login: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getFullUser(login: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (login === null || login === undefined) { + throw new Error('Required parameter login was null or undefined when calling getFullUser.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/user/${this.configuration.encodeParam({name: "login", value: login, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/full`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + /** * @param login * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. diff --git a/webapp/src/app/core/modules/openapi/api/user.serviceInterface.ts b/webapp/src/app/core/modules/openapi/api/user.serviceInterface.ts index 2221661e..65217329 100644 --- a/webapp/src/app/core/modules/openapi/api/user.serviceInterface.ts +++ b/webapp/src/app/core/modules/openapi/api/user.serviceInterface.ts @@ -13,6 +13,7 @@ import { HttpHeaders } from '@angular/comm import { Observable } from 'rxjs'; +import { User } from '../model/models'; import { UserDTO } from '../model/models'; @@ -24,6 +25,13 @@ export interface UserServiceInterface { defaultHeaders: HttpHeaders; configuration: Configuration; + /** + * + * + * @param login + */ + getFullUser(login: string, extraHttpRequestParams?: any): Observable; + /** * * diff --git a/webapp/src/app/core/modules/openapi/model/issue-comment.ts b/webapp/src/app/core/modules/openapi/model/issue-comment.ts index 5c4eaadd..2bdafa3b 100644 --- a/webapp/src/app/core/modules/openapi/model/issue-comment.ts +++ b/webapp/src/app/core/modules/openapi/model/issue-comment.ts @@ -17,7 +17,7 @@ export interface IssueComment { id?: number; createdAt?: string; updatedAt?: string; - body: string; + body?: string; author?: User; pullRequest?: PullRequest; } diff --git a/webapp/src/app/core/modules/openapi/model/leaderboard-entry.ts b/webapp/src/app/core/modules/openapi/model/leaderboard-entry.ts new file mode 100644 index 00000000..b0f61ef0 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/leaderboard-entry.ts @@ -0,0 +1,23 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface LeaderboardEntry { + githubName?: string; + name?: string; + score?: number; + total?: number; + changesRequested?: number; + approvals?: number; + comments?: number; +} + diff --git a/webapp/src/app/core/modules/openapi/model/models.ts b/webapp/src/app/core/modules/openapi/model/models.ts index 7cde1e34..410e38ed 100644 --- a/webapp/src/app/core/modules/openapi/model/models.ts +++ b/webapp/src/app/core/modules/openapi/model/models.ts @@ -1,9 +1,11 @@ export * from './hello'; export * from './issue-comment'; export * from './issue-comment-dto'; +export * from './leaderboard-entry'; export * from './pull-request'; export * from './pull-request-dto'; export * from './pull-request-review'; +export * from './pull-request-review-comment'; export * from './repository'; export * from './repository-dto'; export * from './user'; diff --git a/webapp/src/app/core/modules/openapi/model/pull-request-review-comment.ts b/webapp/src/app/core/modules/openapi/model/pull-request-review-comment.ts new file mode 100644 index 00000000..1781825d --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/pull-request-review-comment.ts @@ -0,0 +1,25 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { PullRequestReview } from './pull-request-review'; +import { User } from './user'; + + +export interface PullRequestReviewComment { + id?: number; + createdAt?: string; + updatedAt?: string; + body?: string; + author?: User; + review?: PullRequestReview; + commit: string; +} + diff --git a/webapp/src/app/core/modules/openapi/model/pull-request-review.ts b/webapp/src/app/core/modules/openapi/model/pull-request-review.ts index 8d6b247e..52c37e3b 100644 --- a/webapp/src/app/core/modules/openapi/model/pull-request-review.ts +++ b/webapp/src/app/core/modules/openapi/model/pull-request-review.ts @@ -9,6 +9,7 @@ * https://openapi-generator.tech * Do not edit the class manually. */ +import { PullRequestReviewComment } from './pull-request-review-comment'; import { User } from './user'; import { PullRequest } from './pull-request'; @@ -20,6 +21,7 @@ export interface PullRequestReview { author?: User; state: PullRequestReview.StateEnum; submittedAt?: string; + comments?: Set; pullRequest?: PullRequest; } export namespace PullRequestReview { diff --git a/webapp/src/app/core/modules/openapi/model/repository.ts b/webapp/src/app/core/modules/openapi/model/repository.ts index a202a249..3c7f9428 100644 --- a/webapp/src/app/core/modules/openapi/model/repository.ts +++ b/webapp/src/app/core/modules/openapi/model/repository.ts @@ -18,7 +18,7 @@ export interface Repository { updatedAt?: string; name: string; nameWithOwner: string; - description: string; + description?: string; defaultBranch: string; visibility: Repository.VisibilityEnum; url: string; diff --git a/webapp/src/app/core/modules/openapi/model/user.ts b/webapp/src/app/core/modules/openapi/model/user.ts index 0d991fc6..ba2f3a53 100644 --- a/webapp/src/app/core/modules/openapi/model/user.ts +++ b/webapp/src/app/core/modules/openapi/model/user.ts @@ -9,6 +9,7 @@ * https://openapi-generator.tech * Do not edit the class manually. */ +import { PullRequestReviewComment } from './pull-request-review-comment'; import { PullRequestReview } from './pull-request-review'; import { PullRequest } from './pull-request'; import { IssueComment } from './issue-comment'; @@ -36,7 +37,8 @@ export interface User { */ avatarUrl?: string; pullRequests?: Set; - comments?: Set; + issueComments?: Set; + reviewComments?: Set; reviews?: Set; } diff --git a/webapp/src/app/ui/table/table-body.directive.ts b/webapp/src/app/ui/table/table-body.directive.ts new file mode 100644 index 00000000..44dd6552 --- /dev/null +++ b/webapp/src/app/ui/table/table-body.directive.ts @@ -0,0 +1,15 @@ +import { Directive, computed, input } from '@angular/core'; +import { ClassValue } from 'clsx'; +import { cn } from 'app/utils'; + +@Directive({ + selector: 'tbody[appTableBody]', + standalone: true, + host: { + '[class]': 'computedClass()' + } +}) +export class TableBodyDirective { + class = input(); + computedClass = computed(() => cn('[&_tr:last-child]:border-0', this.class())); +} diff --git a/webapp/src/app/ui/table/table-caption.directive.ts b/webapp/src/app/ui/table/table-caption.directive.ts new file mode 100644 index 00000000..62448fc2 --- /dev/null +++ b/webapp/src/app/ui/table/table-caption.directive.ts @@ -0,0 +1,15 @@ +import { Directive, computed, input } from '@angular/core'; +import { ClassValue } from 'clsx'; +import { cn } from 'app/utils'; + +@Directive({ + selector: 'caption[appTableCaption]', + standalone: true, + host: { + '[class]': 'computedClass()' + } +}) +export class TableCaptionDirective { + class = input(); + computedClass = computed(() => cn('mt-4 text-sm text-muted-foreground', this.class())); +} diff --git a/webapp/src/app/ui/table/table-cell.directive.ts b/webapp/src/app/ui/table/table-cell.directive.ts new file mode 100644 index 00000000..7c0560d6 --- /dev/null +++ b/webapp/src/app/ui/table/table-cell.directive.ts @@ -0,0 +1,15 @@ +import { Directive, computed, input } from '@angular/core'; +import { ClassValue } from 'clsx'; +import { cn } from 'app/utils'; + +@Directive({ + selector: 'td[appTableCell]', + standalone: true, + host: { + '[class]': 'computedClass()' + } +}) +export class TableCellDirective { + class = input(); + computedClass = computed(() => cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', this.class())); +} diff --git a/webapp/src/app/ui/table/table-footer.directive.ts b/webapp/src/app/ui/table/table-footer.directive.ts new file mode 100644 index 00000000..ff9554e3 --- /dev/null +++ b/webapp/src/app/ui/table/table-footer.directive.ts @@ -0,0 +1,15 @@ +import { Directive, computed, input } from '@angular/core'; +import { ClassValue } from 'clsx'; +import { cn } from 'app/utils'; + +@Directive({ + selector: 'tfoot[appTableFooter]', + standalone: true, + host: { + '[class]': 'computedClass()' + } +}) +export class TableFooterDirective { + class = input(); + computedClass = computed(() => cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', this.class())); +} diff --git a/webapp/src/app/ui/table/table-head.directive.ts b/webapp/src/app/ui/table/table-head.directive.ts new file mode 100644 index 00000000..14a3071c --- /dev/null +++ b/webapp/src/app/ui/table/table-head.directive.ts @@ -0,0 +1,15 @@ +import { Directive, computed, input } from '@angular/core'; +import { ClassValue } from 'clsx'; +import { cn } from 'app/utils'; + +@Directive({ + selector: 'th[appTableHead]', + standalone: true, + host: { + '[class]': 'computedClass()' + } +}) +export class TableHeadDirective { + class = input(); + computedClass = computed(() => cn('h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0', this.class())); +} diff --git a/webapp/src/app/ui/table/table-header.directive.ts b/webapp/src/app/ui/table/table-header.directive.ts new file mode 100644 index 00000000..a7a9eb2d --- /dev/null +++ b/webapp/src/app/ui/table/table-header.directive.ts @@ -0,0 +1,15 @@ +import { Directive, computed, input } from '@angular/core'; +import { ClassValue } from 'clsx'; +import { cn } from 'app/utils'; + +@Directive({ + selector: 'thead[appTableHeader]', + standalone: true, + host: { + '[class]': 'computedClass()' + } +}) +export class TableHeaderDirective { + class = input(); + computedClass = computed(() => cn('w-full caption-bottom text-sm', this.class())); +} diff --git a/webapp/src/app/ui/table/table-row.directive.ts b/webapp/src/app/ui/table/table-row.directive.ts new file mode 100644 index 00000000..41b4ad32 --- /dev/null +++ b/webapp/src/app/ui/table/table-row.directive.ts @@ -0,0 +1,15 @@ +import { Directive, computed, input } from '@angular/core'; +import { ClassValue } from 'clsx'; +import { cn } from 'app/utils'; + +@Directive({ + selector: 'tr[appTableRow]', + standalone: true, + host: { + '[class]': 'computedClass()' + } +}) +export class TableRowDirective { + class = input(); + computedClass = computed(() => cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', this.class())); +} diff --git a/webapp/src/app/ui/table/table.component.ts b/webapp/src/app/ui/table/table.component.ts index 9b8b6b18..e0f0af57 100644 --- a/webapp/src/app/ui/table/table.component.ts +++ b/webapp/src/app/ui/table/table.component.ts @@ -14,6 +14,5 @@ import { cn } from 'app/utils'; }) export class TableComponent { class = input(); - computedClass = computed(() => cn('w-full caption-bottom text-sm', this.class())); } diff --git a/webapp/src/app/ui/table/table.stories.ts b/webapp/src/app/ui/table/table.stories.ts index 3c01d45a..b08b02b7 100644 --- a/webapp/src/app/ui/table/table.stories.ts +++ b/webapp/src/app/ui/table/table.stories.ts @@ -1,12 +1,12 @@ import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { TableComponent } from './table.component'; -import { TableBodyComponent } from './table-body.component'; -import { TableCaptionComponent } from './table-caption.component'; -import { TableCellComponent } from './table-cell.component'; -import { TableFooterComponent } from './table-footer.component'; -import { TableHeaderComponent } from './table-header.component'; -import { TableHeadComponent } from './table-head.component'; -import { TableRowComponent } from './table-row.component'; +import { TableBodyDirective } from './table-body.directive'; +import { TableCaptionDirective } from './table-caption.directive'; +import { TableCellDirective } from './table-cell.directive'; +import { TableFooterDirective } from './table-footer.directive'; +import { TableHeaderDirective } from './table-header.directive'; +import { TableHeadDirective } from './table-head.directive'; +import { TableRowDirective } from './table-row.directive'; type CustomArgs = { invoices: { @@ -22,7 +22,7 @@ const meta: Meta = { component: TableComponent, decorators: [ moduleMetadata({ - imports: [TableBodyComponent, TableCaptionComponent, TableCellComponent, TableFooterComponent, TableHeaderComponent, TableHeadComponent, TableRowComponent] + imports: [TableBodyDirective, TableCaptionDirective, TableCellDirective, TableFooterDirective, TableHeaderDirective, TableHeadDirective, TableRowDirective] }) ], tags: ['autodocs'], @@ -82,31 +82,29 @@ export const Default: Story = { props: args, template: ` - A list of your recent invoices. - - - Invoice - Status - Method - Amount - - - - @for (invoice of invoices; track invoice.invoice) { - - {{invoice.invoice}} - {{invoice.paymentStatus}} - {{invoice.paymentMethod}} - {{invoice.totalAmount}} - - } - - - - Total - $2,500.00 - - + A list of your recent invoices. + + + Invoice + Status + Method + Amount + + + + + {{ invoice.invoice }} + {{ invoice.paymentStatus }} + {{ invoice.paymentMethod }} + {{ invoice.totalAmount }} + + + + + Total + $2,500.00 + + ` })