Skip to content

Commit

Permalink
Leaderboard component and homepage (#78)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Felix T.J. Dietrich <[email protected]>
  • Loading branch information
3 people authored Sep 15, 2024
1 parent 84d59a4 commit eef2dfa
Show file tree
Hide file tree
Showing 23 changed files with 459 additions and 17 deletions.
28 changes: 27 additions & 1 deletion server/application-server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,18 @@ components:
enum:
- CLOSED
- OPEN
additions:
type: integer
format: int32
deletions:
type: integer
format: int32
commits:
type: integer
format: int32
changedFiles:
type: integer
format: int32
mergedAt:
type: string
format: date-time
Expand Down Expand Up @@ -402,6 +414,7 @@ components:
User:
required:
- login
- type
- url
type: object
properties:
Expand Down Expand Up @@ -433,6 +446,12 @@ components:
URL to the user's avatar.
If unavailable, a fallback can be generated from the login, e.g. on Github:
https://github.com/{login}.png
type:
type: string
description: Type of the user. Used to distinguish between users and bots.
enum:
- USER
- BOT
pullRequests:
uniqueItems: true
type: array
Expand All @@ -458,12 +477,19 @@ components:
properties:
githubName:
type: string
avatarUrl:
type: string
name:
type: string
type:
type: string
enum:
- USER
- BOT
score:
type: integer
format: int32
total:
rank:
type: integer
format: int32
changesRequested:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ public class PullRequest extends BaseGitServiceEntity {
@NonNull
private IssueState state;

private int additions;

private int deletions;

private int commits;

private int changedFiles;

private OffsetDateTime mergedAt;

@ManyToOne(fetch = FetchType.EAGER)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package de.tum.in.www1.hephaestus.codereview.pullrequest;

import java.io.IOException;

import org.kohsuke.github.GHPullRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

Expand All @@ -9,6 +13,8 @@
@Component
public class PullRequestConverter extends BaseGitServiceEntityConverter<GHPullRequest, PullRequest> {

protected static final Logger logger = LoggerFactory.getLogger(PullRequestConverter.class);

@Override
public PullRequest convert(@NonNull GHPullRequest source) {
IssueState state = convertState(source.getState());
Expand All @@ -17,6 +23,30 @@ public PullRequest convert(@NonNull GHPullRequest source) {
pullRequest.setTitle(source.getTitle());
pullRequest.setUrl(source.getHtmlUrl().toString());
pullRequest.setState(state);
try {
pullRequest.setAdditions(source.getAdditions());
} catch (IOException e) {
logger.error("Failed to convert additions field for source {}: {}", source.getId(), e.getMessage());
pullRequest.setAdditions(0);
}
try {
pullRequest.setDeletions(source.getDeletions());
} catch (IOException e) {
logger.error("Failed to convert deletions field for source {}: {}", source.getId(), e.getMessage());
pullRequest.setDeletions(0);
}
try {
pullRequest.setCommits(source.getCommits());
} catch (IOException e) {
logger.error("Failed to convert commits field for source {}: {}", source.getId(), e.getMessage());
pullRequest.setCommits(0);
}
try {
pullRequest.setChangedFiles(source.getChangedFiles());
} catch (IOException e) {
logger.error("Failed to convert changedFiles field for source {}: {}", source.getId(), e.getMessage());
pullRequest.setChangedFiles(0);
}
if (source.getMergedAt() != null) {
pullRequest.setMergedAt(convertToOffsetDateTime(source.getMergedAt()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public class User extends BaseGitServiceEntity {
*/
private String avatarUrl;

/**
* Type of the user. Used to distinguish between users and bots.
*/
@NonNull
private UserType type;

@OneToMany(cascade = CascadeType.ALL, mappedBy = "author")
private Set<PullRequest> pullRequests = new HashSet<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,23 @@ public User convert(@NonNull org.kohsuke.github.GHUser source) {
} catch (IOException e) {
logger.error("Failed to convert user email field for source {}: {}", source.getId(), e.getMessage());
}
try {
user.setType(convertUserType(source.getType()));
} catch (IOException e) {
logger.error("Failed to convert user type field for source {}: {}", source.getId(), e.getMessage());
}
return user;
}

private UserType convertUserType(String type) {
switch (type) {
case "User":
return UserType.USER;
case "Bot":
return UserType.BOT;
default:
return UserType.USER;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.tum.in.www1.hephaestus.codereview.user;

public enum UserType {
USER, BOT
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
package de.tum.in.www1.hephaestus.leaderboard;

import com.fasterxml.jackson.annotation.JsonInclude;
import de.tum.in.www1.hephaestus.codereview.user.UserType;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record LeaderboardEntry(String githubName, String name, int score, int total, int changesRequested,
int approvals, int comments) {
@AllArgsConstructor
@Getter
@Setter
@ToString
public class LeaderboardEntry {
private String githubName;
private String avatarUrl;
private String name;
private UserType type;
private int score;
private int rank;
private int changesRequested;
private int approvals;
private int comments;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package de.tum.in.www1.hephaestus.leaderboard;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequest;
import de.tum.in.www1.hephaestus.codereview.user.User;
import de.tum.in.www1.hephaestus.codereview.user.UserService;
import de.tum.in.www1.hephaestus.codereview.user.UserType;

@Service
public class LeaderboardService {
Expand All @@ -27,10 +33,13 @@ public List<LeaderboardEntry> createLeaderboard() {
logger.info("Found " + users.size() + " users");

List<LeaderboardEntry> leaderboard = users.stream().map(user -> {
logger.info("Creating leaderboard entry for user: " + user.getLogin());
if (user.getType() != UserType.USER) {
return null;
}
int comments = user.getIssueComments().size();
AtomicInteger changesRequested = new AtomicInteger(0);
AtomicInteger changesApproved = new AtomicInteger(0);
AtomicInteger score = new AtomicInteger(0);
user.getReviews().stream().forEach(review -> {
switch (review.getState()) {
case CHANGES_REQUESTED:
Expand All @@ -42,11 +51,46 @@ public List<LeaderboardEntry> createLeaderboard() {
default:
break;
}
score.addAndGet(calculateScore(review.getPullRequest()));
});
return new LeaderboardEntry(user.getLogin(), user.getName(), 0, 0, changesRequested.get(),
return new LeaderboardEntry(user.getLogin(), user.getAvatarUrl(), user.getName(), user.getType(),
score.get(),
0, // preliminary rank
changesRequested.get(),
changesApproved.get(), comments);
}).toList();
}).filter(Objects::nonNull).collect(Collectors.toCollection(ArrayList::new));

// update ranks by score
leaderboard.sort(Comparator.comparingInt(LeaderboardEntry::getScore).reversed());
AtomicInteger rank = new AtomicInteger(1);
leaderboard.stream().forEach(entry -> {
entry.setRank(rank.get());
rank.incrementAndGet();
});

return leaderboard;
}

/**
* Calculates the score for a given pull request.
* Possible values: 1, 3, 7, 17, 33.
* Taken from the original leaderboard implementation script.
*
* @param pullRequest
* @return score
*/
private int calculateScore(PullRequest pullRequest) {
Double complexityScore = (pullRequest.getChangedFiles() * 3) + (pullRequest.getCommits() * 0.5)
+ pullRequest.getAdditions() + pullRequest.getDeletions();
if (complexityScore < 10) {
return 1; // Simple
} else if (complexityScore < 50) {
return 3; // Medium
} else if (complexityScore < 100) {
return 7; // Large
} else if (complexityScore < 500) {
return 17; // Huge
}
return 33; // Overly complex
}
}
6 changes: 6 additions & 0 deletions webapp/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"tailwindCSS.experimental.classRegex": ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]", "cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
}
}
2 changes: 1 addition & 1 deletion webapp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"private": true,
"engines": {
"node": "22"
"node": ">=20.16 <23"
},
"dependencies": {
"@angular/animations": "18.2.1",
Expand Down
57 changes: 57 additions & 0 deletions webapp/src/app/components/leaderboard/leaderboard.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<div class="flex flex-col gap-4 py-8">
<h1 class="text-3xl font-bold">Artemis Leaderboard</h1>
@if (query.isPending()) {
<span class="text-muted-foreground">Data is loading...</span>
} @else if (query.error()) {
<span class="text-destructive">An error has occurred</span>
}

@if (leaderboard(); as leaderboard) {
<div class="px-4">
<app-table>
<thead appTableHeader>
<tr appTableRow>
<th appTableHead>Rank</th>
<th appTableHead>Contributor</th>
<th appTableHead>Score</th>
<th appTableHead>Activity</th>
</tr>
</thead>
<tbody appTableBody>
@for (entry of leaderboard; track entry.githubName) {
<tr appTableRow>
<td appTableCell>{{ entry.rank }}</td>
<td appTableCell>
<a href="https://github.com/{{ entry.githubName }}" target="_blank" rel="noopener noreferrer" class="flex items-center gap-2 font-medium">
<img src="{{ entry.avatarUrl }}" width="24" height="24" />
<span class="text-muted-foreground">{{ entry.name }}</span>
</a>
</td>
<td appTableCell>{{ entry.score }}</td>
<td appTableCell class="flex items-center gap-4" title="Changes Requested">
@if (entry.changesRequested && entry.changesRequested > 0) {
<div class="flex items-center gap-2">
<app-icon-pull-request-changes-requested />
{{ entry.changesRequested }}
</div>
}
@if (entry.approvals && entry.approvals > 0) {
<div class="flex items-center gap-2" title="Approvals">
<app-icon-pull-request-approved />
{{ entry.approvals }}
</div>
}
@if (entry.comments && entry.comments > 0) {
<div class="flex items-center gap-2" title="Comments">
<app-icon-pull-request-comment />
{{ entry.comments }}
</div>
}
</td>
</tr>
}
</tbody>
</app-table>
</div>
}
</div>
Loading

0 comments on commit eef2dfa

Please sign in to comment.