Skip to content

Commit

Permalink
Support for repository leaderboards
Browse files Browse the repository at this point in the history
  • Loading branch information
GODrums committed Oct 4, 2024
1 parent 4bde64b commit b698eaa
Show file tree
Hide file tree
Showing 17 changed files with 245 additions and 119 deletions.
21 changes: 11 additions & 10 deletions server/application-server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ paths:
schema:
type: string
format: date
- name: repository
in: query
required: false
schema:
type: string
responses:
"200":
description: OK
Expand Down Expand Up @@ -254,9 +259,8 @@ components:
type: string
state:
type: string
description: |-
State of the PullRequest.
Does not include the state of the merge.
description: "State of the PullRequest.\r\n Does not include the state of\
\ the merge."
enum:
- CLOSED
- OPEN
Expand Down Expand Up @@ -422,15 +426,12 @@ components:
description: Display name of the user.
url:
type: string
description: |-
Unique URL to the user's profile.
Not the website a user can set in their profile.
description: "Unique URL to the user's profile.\r\n Not the website a user\
\ can set in their profile."
avatarUrl:
type: string
description: |-
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
description: "URL to the user's avatar.\r\n If unavailable, a fallback can\
\ be generated from the login, e.g. on Github:\r\n https://github.com/{login}.png"
type:
type: string
description: Type of the user. Used to distinguish between users and bots.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ SELECT new UserDTO(u.id, u.login, u.email, u.name, u.url)
FROM User u
JOIN FETCH u.reviews re
WHERE re.createdAt BETWEEN :after AND :before
AND (:repository IS NULL OR re.pullRequest.repository.nameWithOwner = :repository)
""")
List<User> findAllInTimeframe(@Param("after") OffsetDateTime after, @Param("before") OffsetDateTime before);
List<User> findAllInTimeframe(@Param("after") OffsetDateTime after, @Param("before") OffsetDateTime before,
@Param("repository") Optional<String> repository);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ public List<User> getAllUsers() {
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);
public List<User> getAllUsersInTimeframe(OffsetDateTime after, OffsetDateTime before, Optional<String> repository) {
logger.info("Getting all users in timeframe between " + after + " and " + before + " for repository: "
+ repository.orElse("all"));
return userRepository.findAllInTimeframe(after, before, repository);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public LeaderboardController(LeaderboardService leaderboardService) {
@GetMapping
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));
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Optional<LocalDate> before,
@RequestParam Optional<String> repository) {
return ResponseEntity.ok(leaderboardService.createLeaderboard(after, before, repository));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,16 @@ public LeaderboardService(UserService userService) {
this.userService = userService;
}

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

LocalDateTime afterCutOff = after.isPresent() ? after.get().atStartOfDay()
: LocalDate.now().minusDays(timeframe).atStartOfDay();
Optional<LocalDateTime> beforeCutOff = before.map(date -> date.plusDays(1).atStartOfDay());

List<User> users = userService.getAllUsersInTimeframe(afterCutOff.atOffset(ZoneOffset.UTC),
beforeCutOff.map(b -> b.atOffset(ZoneOffset.UTC)).orElse(OffsetDateTime.now()));
beforeCutOff.map(b -> b.atOffset(ZoneOffset.UTC)).orElse(OffsetDateTime.now()), repository);

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

Expand Down
13 changes: 9 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 @@ -98,13 +98,14 @@ export class LeaderboardService implements LeaderboardServiceInterface {
/**
* @param after
* @param before
* @param repository
* @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(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> {
public getLeaderboard(after?: string, before?: string, repository?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<Array<LeaderboardEntry>>;
public getLeaderboard(after?: string, before?: string, repository?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<Array<LeaderboardEntry>>>;
public getLeaderboard(after?: string, before?: string, repository?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<Array<LeaderboardEntry>>>;
public getLeaderboard(after?: string, before?: string, repository?: 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) {
Expand All @@ -115,6 +116,10 @@ export class LeaderboardService implements LeaderboardServiceInterface {
localVarQueryParameters = this.addToHttpParams(localVarQueryParameters,
<any>before, 'before');
}
if (repository !== undefined && repository !== null) {
localVarQueryParameters = this.addToHttpParams(localVarQueryParameters,
<any>repository, 'repository');
}

let localVarHeaders = this.defaultHeaders;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export interface LeaderboardServiceInterface {
*
* @param after
* @param before
* @param repository
*/
getLeaderboard(after?: string, before?: string, extraHttpRequestParams?: any): Observable<Array<LeaderboardEntry>>;
getLeaderboard(after?: string, before?: string, repository?: string, extraHttpRequestParams?: any): Observable<Array<LeaderboardEntry>>;

}
2 changes: 1 addition & 1 deletion webapp/src/app/core/modules/openapi/model/pull-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface PullRequest {
title: string;
url: string;
/**
* State of the PullRequest. Does not include the state of the merge.
* State of the PullRequest. Does not include the state of the merge.
*/
state: PullRequest.StateEnum;
additions?: number;
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/app/core/modules/openapi/model/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ export interface User {
*/
name?: string;
/**
* Unique URL to the user\'s profile. Not the website a user can set in their profile.
* Unique URL to the user\'s profile. Not the website a user can set in their profile.
*/
url: string;
/**
* 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
* 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
*/
avatarUrl?: string;
/**
Expand Down
4 changes: 3 additions & 1 deletion webapp/src/app/home/home.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<div class="flex gap-2 flex-col items-center justify-center">
<h1 class="text-3xl font-bold mb-4">Artemis Leaderboard</h1>
<div class="grid grid-cols-1 xl:grid-cols-4 gap-4 place-content-center">
<app-leaderboard-filter [after]="after()" [before]="before()" />
<div class="w-full flex flex-col items-center">
<app-leaderboard-filter [after]="after()" [before]="before()" />
</div>
<div class="xl:col-span-2 flex flex-col items-center gap-4">
@if (query.error()) {
<span class="text-xl text-destructive mt-2">An error has occurred</span>
Expand Down
21 changes: 16 additions & 5 deletions webapp/src/app/home/home.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, computed, inject } from '@angular/core';
import { Component, computed, effect, 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';
Expand All @@ -17,15 +17,26 @@ import dayjs from 'dayjs';
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 });
// leaderboard filter
protected after = computed(() => this.queryParams().get('after') ?? dayjs().day(1).format('YYYY-MM-DD'));
protected before = computed(() => this.queryParams().get('before') ?? dayjs().format('YYYY-MM-DD'));
protected repository = computed(() => this.queryParams().get('repository') ?? 'all');

constructor() {
effect(() => {
console.log('HomeComponent: effect: ', this.repository());
});
}

query = injectQuery(() => ({
queryKey: ['leaderboard', { after: this.after(), before: this.before() }],
queryFn: async () => lastValueFrom(combineLatest([this.leaderboardService.getLeaderboard(this.after(), this.before()), timer(500)]).pipe(map(([leaderboard]) => leaderboard)))
queryKey: ['leaderboard', { after: this.after(), before: this.before(), repository: this.repository() }],
queryFn: async () =>
lastValueFrom(
combineLatest([this.leaderboardService.getLeaderboard(this.after(), this.before(), this.repository() !== 'all' ? this.repository() : undefined), timer(500)]).pipe(
map(([leaderboard]) => leaderboard)
)
)
}));
}
15 changes: 3 additions & 12 deletions webapp/src/app/home/leaderboard/filter/filter.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,8 @@
<lucide-angular [img]="ListFilter" class="size-4" />
<h3 class="text-base font-semibold tracking-wide">Filter</h3>
</span>
<form ngForm>
<hlm-select [placeholder]="placeholder()" [(ngModel)]="value" name="value">
<label hlmLabel>Select week</label>
<hlm-select-trigger class="w-56">
<hlm-select-value />
</hlm-select-trigger>
<hlm-select-content class="w-56">
@for (option of options(); track option.id) {
<hlm-option [value]="option.value">{{ option.label }}</hlm-option>
}
</hlm-select-content>
</hlm-select>
<form ngForm class="flex xl:flex-col gap-4">
<app-leaderboard-filter-timeframe [after]="after()" [before]="before()" />
<app-leaderboard-filter-repository />
</form>
</div>
79 changes: 4 additions & 75 deletions webapp/src/app/home/leaderboard/filter/filter.component.ts
Original file line number Diff line number Diff line change
@@ -1,88 +1,17 @@
import { Component, computed, effect, input, signal } from '@angular/core';
import { Component, input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import dayjs from 'dayjs';
import weekOfYear from 'dayjs/plugin/weekOfYear';
import { BrnSelectModule } from '@spartan-ng/ui-select-brain';
import { HlmSelectModule } from '@spartan-ng/ui-select-helm';
import { HlmLabelModule } from '@spartan-ng/ui-label-helm';
import { ListFilter, LucideAngularModule } from 'lucide-angular';

interface SelectOption {
id: number;
value: string;
label: string;
}

dayjs.extend(weekOfYear);

function formatLabel(startDate: dayjs.Dayjs, endDate: dayjs.Dayjs | undefined) {
const calendarWeek = startDate.week();
if (!endDate) {
return `CW\xa0${calendarWeek}:\xa0${startDate.format('MMM D')}\xa0-\xa0Today`;
}

const sameMonth = startDate.month() === endDate.month();
if (sameMonth) {
return `CW\xa0${calendarWeek}:\xa0${startDate.format('MMM D')}\xa0-\xa0${endDate.format('D')}`;
} else {
return `CW\xa0${calendarWeek}:\xa0${startDate.format('MMM D')}\xa0-\xa0${endDate.format('MMM D')}`;
}
}
import { LeaderboardFilterTimeframeComponent } from './timeframe/timeframe.component';
import { LeaderboardFilterRepositoryComponent } from './repository/repository.component';

@Component({
selector: 'app-leaderboard-filter',
standalone: true,
imports: [RouterLink, LucideAngularModule, BrnSelectModule, HlmSelectModule, HlmLabelModule, FormsModule],
imports: [LucideAngularModule, FormsModule, LeaderboardFilterTimeframeComponent, LeaderboardFilterRepositoryComponent],
templateUrl: './filter.component.html'
})
export class LeaderboardFilterComponent {
protected ListFilter = ListFilter;
after = input<string>('');
before = input<string>('');

value = signal<string>(`${this.after()}.${this.before()}`);

placeholder = computed(() => {
return formatLabel(dayjs(this.after()) ?? dayjs().day(1), this.before() === undefined ? undefined : dayjs(this.before()));
});

options = computed(() => {
const now = dayjs();
let currentDate = dayjs().day(1);
const options: SelectOption[] = [
{
id: now.unix(),
value: `${currentDate.format('YYYY-MM-DD')}.${now.format('YYYY-MM-DD')}`,
label: formatLabel(currentDate, undefined)
}
];

for (let i = 0; i < 4; i++) {
const startDate = currentDate.subtract(7, 'day');
const endDate = currentDate.subtract(1, 'day');
options.push({
id: startDate.unix(),
value: `${startDate.format('YYYY-MM-DD')}.${endDate.format('YYYY-MM-DD')}`,
label: formatLabel(startDate, endDate)
});
currentDate = startDate;
}

return options;
});

constructor(private router: Router) {
effect(() => {
if (this.value().length === 1) return;
const dates = this.value().split('.');
// change query params
this.router.navigate([], {
queryParams: {
after: dates[0],
before: dates[1]
}
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<hlm-select [placeholder]="placeholder()" [(ngModel)]="value" name="value">
<label hlmLabel>Select repository</label>
<hlm-select-trigger class="w-56">
<hlm-select-value />
</hlm-select-trigger>
<hlm-select-content class="w-56">
@for (option of options(); track option.id) {
<hlm-option [value]="option.value">{{ option.label }}</hlm-option>
}
</hlm-select-content>
</hlm-select>
Loading

0 comments on commit b698eaa

Please sign in to comment.