Skip to content

Commit

Permalink
Merge branch 'develop' into 82-pull-request-component-for-collaborati…
Browse files Browse the repository at this point in the history
…on-dashboard
  • Loading branch information
GODrums committed Sep 24, 2024
2 parents 925228e + 5f050e4 commit dc46977
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 80 deletions.
13 changes: 13 additions & 0 deletions server/application-server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@ paths:
tags:
- leaderboard
operationId: getLeaderboard
parameters:
- name: after
in: query
required: false
schema:
type: string
format: date
- name: before
in: query
required: false
schema:
type: string
format: date
responses:
"200":
description: OK
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.tum.in.www1.hephaestus.codereview.user;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -39,4 +40,12 @@ SELECT new UserDTO(u.id, u.login, u.email, u.name, u.url)
JOIN FETCH u.reviews
""")
List<User> findAllWithRelations();

@Query("""
SELECT u
FROM User u
JOIN FETCH u.reviews re
WHERE re.createdAt BETWEEN :after AND :before
""")
List<User> findAllInTimeframe(@Param("after") OffsetDateTime after, @Param("before") OffsetDateTime before);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.tum.in.www1.hephaestus.codereview.user;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -31,4 +32,9 @@ public List<User> getAllUsers() {
logger.info("Getting all users");
return userRepository.findAll().stream().toList();
}

public List<User> getAllUsersInTimeframe(OffsetDateTime after, OffsetDateTime before) {
logger.info("Getting all users in timeframe between " + after + " and " + before);
return userRepository.findAllInTimeframe(after, before);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package de.tum.in.www1.hephaestus.leaderboard;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

import org.springframework.format.annotation.DateTimeFormat;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
Expand All @@ -18,7 +22,9 @@ public LeaderboardController(LeaderboardService leaderboardService) {
}

@GetMapping
public ResponseEntity<List<LeaderboardEntry>> getLeaderboard() {
return ResponseEntity.ok(leaderboardService.createLeaderboard());
public ResponseEntity<List<LeaderboardEntry>> getLeaderboard(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Optional<LocalDate> after,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Optional<LocalDate> before) {
return ResponseEntity.ok(leaderboardService.createLeaderboard(after, before));
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package de.tum.in.www1.hephaestus.leaderboard;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -36,16 +38,20 @@ public LeaderboardService(UserService userService) {
this.userService = userService;
}

public List<LeaderboardEntry> createLeaderboard() {
public List<LeaderboardEntry> createLeaderboard(Optional<LocalDate> after, Optional<LocalDate> before) {
logger.info("Creating leaderboard dataset");

List<User> users = userService.getAllUsers();
logger.info("Leaderboard has " + users.size() + " users");
LocalDateTime afterCutOff = after.isPresent() ? after.get().atStartOfDay()
: LocalDate.now().minusDays(timeframe).atStartOfDay();
Optional<LocalDateTime> beforeCutOff = before.map(date -> date.plusDays(1).atStartOfDay());

OffsetDateTime cutOffTime = new Date(System.currentTimeMillis() - 1000 * 60 * 60 * 24 * timeframe)
.toInstant().atOffset(ZoneOffset.UTC);
List<User> users = userService.getAllUsersInTimeframe(afterCutOff.atOffset(ZoneOffset.UTC),
beforeCutOff.map(b -> b.atOffset(ZoneOffset.UTC)).orElse(OffsetDateTime.now()));

logger.info("Found " + users.size() + " users for the leaderboard");

List<LeaderboardEntry> leaderboard = users.stream().map(user -> {
// ignore non-users, e.g. bots
if (user.getType() != UserType.USER) {
return null;
}
Expand All @@ -55,12 +61,11 @@ public List<LeaderboardEntry> createLeaderboard() {
Set<PullRequestReviewDTO> commentSet = new HashSet<>();

user.getReviews().stream()
.filter(review -> (review.getCreatedAt() != null && review.getCreatedAt().isAfter(cutOffTime))
|| (review.getUpdatedAt() != null && review.getUpdatedAt().isAfter(cutOffTime)))
.forEach(review -> {
if (review.getPullRequest().getAuthor().getLogin().equals(user.getLogin())) {
return;
}

PullRequestReviewDTO reviewDTO = new PullRequestReviewDTO(review.getId(), review.getCreatedAt(),
review.getUpdatedAt(), review.getSubmittedAt(), review.getState());

Expand Down
21 changes: 17 additions & 4 deletions webapp/src/app/core/modules/openapi/api/leaderboard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,25 @@ export class LeaderboardService implements LeaderboardServiceInterface {
}

/**
* @param after
* @param before
* @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<Array<LeaderboardEntry>>;
public getLeaderboard(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<Array<LeaderboardEntry>>>;
public getLeaderboard(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<Array<LeaderboardEntry>>>;
public getLeaderboard(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<any> {
public getLeaderboard(after?: string, before?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<Array<LeaderboardEntry>>;
public getLeaderboard(after?: string, before?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<Array<LeaderboardEntry>>>;
public getLeaderboard(after?: string, before?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<Array<LeaderboardEntry>>>;
public getLeaderboard(after?: string, before?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<any> {

let localVarQueryParameters = new HttpParams({encoder: this.encoder});
if (after !== undefined && after !== null) {
localVarQueryParameters = this.addToHttpParams(localVarQueryParameters,
<any>after, 'after');
}
if (before !== undefined && before !== null) {
localVarQueryParameters = this.addToHttpParams(localVarQueryParameters,
<any>before, 'before');
}

let localVarHeaders = this.defaultHeaders;

Expand Down Expand Up @@ -144,6 +156,7 @@ export class LeaderboardService implements LeaderboardServiceInterface {
return this.httpClient.request<Array<LeaderboardEntry>>('get', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
params: localVarQueryParameters,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export interface LeaderboardServiceInterface {
/**
*
*
* @param after
* @param before
*/
getLeaderboard(extraHttpRequestParams?: any): Observable<Array<LeaderboardEntry>>;
getLeaderboard(after?: string, before?: string, extraHttpRequestParams?: any): Observable<Array<LeaderboardEntry>>;

}
87 changes: 47 additions & 40 deletions webapp/src/app/core/theme/theme-switcher.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,78 @@ export enum AppTheme {
DARK = 'dark'
}

const IS_CLIENT_RENDER = typeof localStorage !== 'undefined';
const LOCAL_STORAGE_THEME_KEY = 'theme';

let selectedTheme: AppTheme | undefined = undefined;

if (IS_CLIENT_RENDER) {
selectedTheme = (localStorage.getItem(LOCAL_STORAGE_THEME_KEY) as AppTheme) || undefined;
}

@Injectable({
providedIn: 'root'
})
export class ThemeSwitcherService {
currentTheme = signal<AppTheme | undefined>(selectedTheme);
private htmlElement = document.documentElement;
private metaThemeColor = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');

currentTheme = signal<AppTheme | 'auto' | undefined>(this.getInitialTheme());

constructor() {
if (this.currentTheme() === 'auto') {
this.applySystemTheme();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.handleSystemThemeChange);
}
}

setLightTheme() {
this.currentTheme.set(AppTheme.LIGHT);
this.setToLocalStorage(AppTheme.LIGHT);
this.removeClassFromHtml('dark');
document.documentElement.setAttribute('data-color-mode', 'light');
this.applyTheme(AppTheme.LIGHT);
}

setDarkTheme() {
this.currentTheme.set(AppTheme.DARK);
this.setToLocalStorage(AppTheme.DARK);
this.addClassToHtml('dark');
document.documentElement.setAttribute('data-color-mode', 'dark');
this.applyTheme(AppTheme.DARK);
}

setSystemTheme() {
this.currentTheme.set(undefined);
this.removeFromLocalStorage();
this.currentTheme.set('auto');
localStorage.removeItem(LOCAL_STORAGE_THEME_KEY);
this.applySystemTheme();
this.updateMetaThemeColor();
}

const isSystemDark = window?.matchMedia('(prefers-color-scheme: dark)').matches ?? false;
if (isSystemDark) {
this.addClassToHtml('dark');
} else {
this.removeClassFromHtml('dark');
}
private applyTheme(theme: AppTheme) {
this.currentTheme.set(theme);
localStorage.setItem(LOCAL_STORAGE_THEME_KEY, theme);
this.htmlElement.classList.toggle(AppTheme.DARK, theme === AppTheme.DARK);
this.htmlElement.setAttribute('data-color-mode', theme);
this.updateMetaThemeColor();
}

document.documentElement.setAttribute('data-color-mode', 'auto');
private applySystemTheme() {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
this.htmlElement.classList.toggle(AppTheme.DARK, isDark);
this.htmlElement.setAttribute('data-color-mode', 'auto');
this.updateMetaThemeColor();
}

private addClassToHtml(className: string) {
if (IS_CLIENT_RENDER) {
this.removeClassFromHtml(className);
document.documentElement.classList.add(className);
private handleSystemThemeChange = (event: MediaQueryListEvent) => {
if (this.currentTheme() === 'auto') {
this.htmlElement.classList.toggle(AppTheme.DARK, event.matches);
this.updateMetaThemeColor();
}
}
};

private removeClassFromHtml(className: string) {
if (IS_CLIENT_RENDER) {
document.documentElement.classList.remove(className);
private updateMetaThemeColor() {
if (this.metaThemeColor) {
const backgroundColor = getComputedStyle(this.htmlElement).getPropertyValue('--background').trim();
this.metaThemeColor.setAttribute('content', `hsl(${backgroundColor})`);
}
}

private setToLocalStorage(theme: AppTheme) {
if (IS_CLIENT_RENDER) {
localStorage.setItem(LOCAL_STORAGE_THEME_KEY, theme);
private getInitialTheme(): AppTheme | 'auto' | undefined {
if (typeof localStorage === 'undefined') {
return 'auto';
}
}

private removeFromLocalStorage() {
if (IS_CLIENT_RENDER) {
localStorage.removeItem(LOCAL_STORAGE_THEME_KEY);
const storedTheme = localStorage.getItem(LOCAL_STORAGE_THEME_KEY) as AppTheme | null;
if (storedTheme === AppTheme.LIGHT || storedTheme === AppTheme.DARK) {
return storedTheme;
}

return 'auto';
}
}
16 changes: 12 additions & 4 deletions webapp/src/app/home/home.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Component, inject } from '@angular/core';
import { Component, computed, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { LeaderboardService } from 'app/core/modules/openapi/api/leaderboard.service';
import { LeaderboardComponent } from 'app/home/leaderboard/leaderboard.component';
import { lastValueFrom } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
selector: 'app-home',
Expand All @@ -13,9 +15,15 @@ import { lastValueFrom } from 'rxjs';
export class HomeComponent {
leaderboardService = inject(LeaderboardService);

// timeframe for leaderboard
// example: 2024-09-19
private readonly route = inject(ActivatedRoute);
private queryParams = toSignal(this.route.queryParamMap, { requireSync: true });
protected after = computed(() => this.queryParams().get('after') ?? undefined);
protected before = computed(() => this.queryParams().get('before') ?? undefined);

query = injectQuery(() => ({
queryKey: ['leaderboard'],
queryFn: async () => lastValueFrom(this.leaderboardService.getLeaderboard()),
gcTime: Infinity
queryKey: ['leaderboard', { after: this.after(), before: this.before() }],
queryFn: async () => lastValueFrom(this.leaderboardService.getLeaderboard(this.after(), this.before()))
}));
}
44 changes: 23 additions & 21 deletions webapp/src/app/home/leaderboard/leaderboard.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
@for (entry of leaderboard(); track entry.githubName) {
<tr appTableRow>
<td appTableCell class="text-center">{{ entry.rank }}</td>
<td appTableCell>
<td appTableCell class="py-2">
<a href="https://github.com/{{ entry.githubName }}" target="_blank" rel="noopener noreferrer" class="flex items-center gap-2 font-medium">
<app-avatar class="-my-2">
<app-avatar>
<app-avatar-image [src]="entry.avatarUrl ?? ''" [alt]="entry.name + '\'s avatar'" />
<app-avatar-fallback>
{{ entry.name?.slice(0, 1)?.toUpperCase() }}
Expand All @@ -23,25 +23,27 @@
</a>
</td>
<td appTableCell class="text-center">{{ entry.score }}</td>
<td appTableCell class="flex items-center gap-3">
@if (entry.changesRequested && entry.changesRequested.length > 0) {
<div class="flex items-center gap-1 text-github-danger-foreground" title="Changes Requested">
<ng-icon [svg]="octFileDiff" size="16" />
{{ entry.changesRequested.length }}
</div>
}
@if (entry.approvals && entry.approvals.length > 0) {
<div class="flex items-center gap-1 text-github-success-foreground" title="Approvals">
<ng-icon [svg]="octCheck" size="16" />
{{ entry.approvals.length }}
</div>
}
@if (entry.comments && entry.comments.length > 0) {
<div class="flex items-center gap-1 text-github-muted-foreground" title="Comments">
<ng-icon [svg]="octComment" size="16" />
{{ entry.comments.length }}
</div>
}
<td appTableCell class="py-2">
<div class="flex items-center gap-3">
@if (entry.changesRequested && entry.changesRequested.length > 0) {
<div class="flex items-center gap-1 text-github-danger-foreground" title="Changes Requested">
<ng-icon [svg]="octFileDiff" size="16" />
{{ entry.changesRequested.length }}
</div>
}
@if (entry.approvals && entry.approvals.length > 0) {
<div class="flex items-center gap-1 text-github-success-foreground" title="Approvals">
<ng-icon [svg]="octCheck" size="16" />
{{ entry.approvals.length }}
</div>
}
@if (entry.comments && entry.comments.length > 0) {
<div class="flex items-center gap-1 text-github-muted-foreground" title="Comments">
<ng-icon [svg]="octComment" size="16" />
{{ entry.comments.length }}
</div>
}
</div>
</td>
</tr>
}
Expand Down
Loading

0 comments on commit dc46977

Please sign in to comment.