Skip to content

Commit

Permalink
Leaderboard teams filter
Browse files Browse the repository at this point in the history
  • Loading branch information
GODrums committed Oct 24, 2024
1 parent 69205e0 commit 3d59fba
Show file tree
Hide file tree
Showing 15 changed files with 67 additions and 69 deletions.
2 changes: 1 addition & 1 deletion server/application-server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ paths:
schema:
type: string
format: date
- name: repository
- name: team
in: query
required: false
schema:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import de.tum.in.www1.hephaestus.codereview.team.Team;

public interface UserRepository extends JpaRepository<User, Long> {

@Query("SELECT u FROM User u WHERE u.login = :login")
Expand Down Expand Up @@ -46,10 +48,10 @@ 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)
AND (:team IS NULL OR :team MEMBER OF u.teams)
""")
List<User> findAllInTimeframe(@Param("after") OffsetDateTime after, @Param("before") OffsetDateTime before,
@Param("repository") Optional<String> repository);
@Param("team") Optional<Team> team);

@Query("""
SELECT u
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import de.tum.in.www1.hephaestus.codereview.pullrequest.review.PullRequestReview;
import de.tum.in.www1.hephaestus.codereview.pullrequest.review.PullRequestReviewDTO;
import de.tum.in.www1.hephaestus.codereview.repository.RepositoryDTO;
import de.tum.in.www1.hephaestus.codereview.team.Team;

@Service
public class UserService {
Expand Down Expand Up @@ -53,10 +54,10 @@ public List<UserTeamsDTO> getAllUsersWithTeams() {
return userRepository.findAll().stream().map(UserTeamsDTO::fromUser).toList();
}

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);
public List<User> getAllUsersInTimeframe(OffsetDateTime after, OffsetDateTime before, Optional<Team> team) {
logger.info("Getting all users in timeframe between " + after + " and " + before + " for team: "
+ team.map(Team::getName).orElse("all"));
return userRepository.findAllInTimeframe(after, before, team);
}

public Optional<UserProfileDTO> getUserProfileDTO(String login) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public LeaderboardController(LeaderboardService leaderboardService) {
public ResponseEntity<List<LeaderboardEntry>> getLeaderboard(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Optional<LocalDate> after,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Optional<LocalDate> before,
@RequestParam Optional<String> repository) {
return ResponseEntity.ok(leaderboardService.createLeaderboard(after, before, repository));
@RequestParam Optional<String> team) {
return ResponseEntity.ok(leaderboardService.createLeaderboard(after, before, team));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequest;
import de.tum.in.www1.hephaestus.codereview.pullrequest.review.PullRequestReviewDTO;
import de.tum.in.www1.hephaestus.codereview.team.Team;
import de.tum.in.www1.hephaestus.codereview.team.TeamService;
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;
Expand All @@ -29,25 +32,25 @@
public class LeaderboardService {
private static final Logger logger = LoggerFactory.getLogger(LeaderboardService.class);

private final UserService userService;
@Autowired
private UserService userService;
@Autowired
private TeamService teamService;

@Value("${monitoring.timeframe}")
private int timeframe;

public LeaderboardService(UserService userService) {
this.userService = userService;
}

public List<LeaderboardEntry> createLeaderboard(Optional<LocalDate> after, Optional<LocalDate> before,
Optional<String> repository) {
Optional<String> team) {
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());
Optional<Team> teamEntity = team.map((t) -> teamService.getTeam(t)).orElse(Optional.empty());

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

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

Expand Down
14 changes: 7 additions & 7 deletions webapp/src/app/core/modules/openapi/api/leaderboard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ export class LeaderboardService implements LeaderboardServiceInterface {
/**
* @param after
* @param before
* @param repository
* @param team
* @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, 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> {
public getLeaderboard(after?: string, before?: string, team?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<Array<LeaderboardEntry>>;
public getLeaderboard(after?: string, before?: string, team?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<Array<LeaderboardEntry>>>;
public getLeaderboard(after?: string, before?: string, team?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<Array<LeaderboardEntry>>>;
public getLeaderboard(after?: string, before?: string, team?: 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 @@ -116,9 +116,9 @@ export class LeaderboardService implements LeaderboardServiceInterface {
localVarQueryParameters = this.addToHttpParams(localVarQueryParameters,
<any>before, 'before');
}
if (repository !== undefined && repository !== null) {
if (team !== undefined && team !== null) {
localVarQueryParameters = this.addToHttpParams(localVarQueryParameters,
<any>repository, 'repository');
<any>team, 'team');
}

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

}
2 changes: 1 addition & 1 deletion webapp/src/app/home/home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ <h1 class="text-3xl font-bold">Artemis Leaderboard</h1>
<h2 class="text-xl text-muted-foreground">Hi {{ userValue.name }} 👋</h2>
}
</div>
<app-leaderboard-filter [repositories]="metaQuery.data()?.repositoriesToMonitor" />
<app-leaderboard-filter [teams]="_teams()" />
</div>
<div class="col-span-2">
@if (query.error()) {
Expand Down
16 changes: 9 additions & 7 deletions webapp/src/app/home/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { LeaderboardComponent } from '@app/home/leaderboard/leaderboard.componen
import { LeaderboardFilterComponent } from './leaderboard/filter/filter.component';
import { SecurityStore } from '@app/core/security/security-store.service';
import { HlmAlertModule } from '@spartan-ng/ui-alert-helm';
import { MetaService } from '@app/core/modules/openapi';
import { MetaService, TeamService } from '@app/core/modules/openapi';

dayjs.extend(isoWeek);

Expand All @@ -27,6 +27,7 @@ export class HomeComponent {
securityStore = inject(SecurityStore);
metaService = inject(MetaService);
leaderboardService = inject(LeaderboardService);
teamService = inject(TeamService);

signedIn = this.securityStore.signedIn;
user = this.securityStore.loadedUser;
Expand All @@ -35,20 +36,21 @@ export class HomeComponent {
private queryParams = toSignal(this.route.queryParamMap, { requireSync: true });
protected after = computed(() => this.queryParams().get('after') ?? dayjs().isoWeekday(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');
protected teams = computed(() => this.queryParams().get('team') ?? 'all');

query = injectQuery(() => ({
queryKey: ['leaderboard', { after: this.after(), before: this.before(), repository: this.repository() }],
queryKey: ['leaderboard', { after: this.after(), before: this.before(), repository: this.teams() }],
queryFn: async () =>
lastValueFrom(
combineLatest([this.leaderboardService.getLeaderboard(this.after(), this.before(), this.repository() !== 'all' ? this.repository() : undefined), timer(500)]).pipe(
combineLatest([this.leaderboardService.getLeaderboard(this.after(), this.before(), this.teams() !== 'all' ? this.teams() : undefined), timer(500)]).pipe(
map(([leaderboard]) => leaderboard)
)
)
}));

metaQuery = injectQuery(() => ({
queryKey: ['meta'],
queryFn: async () => lastValueFrom(this.metaService.getMetaData())
protected _teams = computed(() => this.teamsQuery.data()?.map((team) => team.name) ?? []);
teamsQuery = injectQuery(() => ({
queryKey: ['teams'],
queryFn: async () => lastValueFrom(this.teamService.getTeams())
}));
}
4 changes: 2 additions & 2 deletions webapp/src/app/home/leaderboard/filter/filter.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ <h3 class="text-base font-semibold tracking-wide">Filter</h3>
</span>
<form ngForm class="flex flex-wrap gap-4">
<app-leaderboard-filter-timeframe />
@if (repositories() && repositories()!.length > 1) {
<app-leaderboard-filter-repository [repositories]="repositories()!" />
@if (teams() && teams()!.length > 1) {
<app-leaderboard-filter-team [teams]="teams()!" />
}
</form>
</div>
6 changes: 3 additions & 3 deletions webapp/src/app/home/leaderboard/filter/filter.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import { Component, input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ListFilter, LucideAngularModule } from 'lucide-angular';
import { LeaderboardFilterTimeframeComponent } from './timeframe/timeframe.component';
import { LeaderboardFilterRepositoryComponent } from './repository/repository.component';
import { LeaderboardFilterTeamComponent } from './team/team.component';

@Component({
selector: 'app-leaderboard-filter',
standalone: true,
imports: [LucideAngularModule, FormsModule, LeaderboardFilterTimeframeComponent, LeaderboardFilterRepositoryComponent],
imports: [LucideAngularModule, FormsModule, LeaderboardFilterTimeframeComponent, LeaderboardFilterTeamComponent],
templateUrl: './filter.component.html'
})
export class LeaderboardFilterComponent {
protected ListFilter = ListFilter;

repositories = input<string[]>();
teams = input<string[]>();
}
20 changes: 5 additions & 15 deletions webapp/src/app/home/leaderboard/filter/filter.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,14 @@ const meta: Meta<LeaderboardFilterComponent> = {
component: LeaderboardFilterComponent,
tags: ['autodocs'],
args: {
repositories: [
'ls1intum/Artemis',
'ls1intum/Athena',
'ls1intum/Hephaestus',
'ls1intum/Pyris',
'ls1intum/Ares2',
'ls1intum/Aeolus',
'ls1intum/hades',
'ls1intum/Apollon',
'ls1intum/Apollon_standalone'
]
teams: ['Artemis', 'Athena', 'Hephaestus', 'Iris', 'Lectures']
},
argTypes: {
repositories: {
teams: {
control: {
type: 'object'
},
description: 'List of repositories'
description: 'List of teams'
}
}
};
Expand All @@ -37,9 +27,9 @@ export const Default: Story = {
})
};

export const SingleRepository: Story = {
export const SingleTeam: Story = {
args: {
repositories: ['ls1intum/Artemis']
teams: ['ls1intum/Artemis']
},
render: (args) => ({
props: args,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<hlm-select [placeholder]="placeholder()" [(ngModel)]="value" name="value">
<label hlmLabel>Select repository</label>
<label hlmLabel>Select team</label>
<hlm-select-trigger class="w-56">
<hlm-select-value />
</hlm-select-trigger>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ interface SelectOption {
}

@Component({
selector: 'app-leaderboard-filter-repository',
selector: 'app-leaderboard-filter-team',
standalone: true,
imports: [RouterLink, BrnSelectModule, HlmSelectModule, HlmLabelModule, FormsModule],
templateUrl: './repository.component.html'
templateUrl: './team.component.html'
})
export class LeaderboardFilterRepositoryComponent {
repositories = input.required<string[]>();
export class LeaderboardFilterTeamComponent {
teams = input.required<string[]>();
value = signal<string>('');

placeholder = computed(() => {
return this.repositories().find((option) => option === this.value()) ?? 'All';
return this.teams().find((option) => option === this.value()) ?? 'All';
});

options = computed(() => {
const options: SelectOption[] = this.repositories().map((name, index) => {
const options: SelectOption[] = this.teams().map((name, index) => {
return {
id: index + 1,
value: name,
Expand All @@ -42,15 +42,15 @@ export class LeaderboardFilterRepositoryComponent {
});

constructor(private router: Router) {
this.value.set(this.router.parseUrl(this.router.url).queryParams['repository'] ?? 'all');
this.value.set(this.router.parseUrl(this.router.url).queryParams['team'] ?? 'all');

effect(() => {
if (!this.value() || this.value() === '') return;
const queryParams = this.router.parseUrl(this.router.url).queryParams;
if (this.value() === 'all') {
delete queryParams['repository'];
delete queryParams['team'];
} else {
queryParams['repository'] = this.value();
queryParams['team'] = this.value();
}
this.router.navigate([], {
queryParams
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
import { LeaderboardFilterRepositoryComponent } from './repository.component';
import { LeaderboardFilterTeamComponent } from './team.component';

const meta: Meta<LeaderboardFilterRepositoryComponent> = {
component: LeaderboardFilterRepositoryComponent,
const meta: Meta<LeaderboardFilterTeamComponent> = {
component: LeaderboardFilterTeamComponent,
tags: ['autodocs'],
argTypes: {
repositories: {
teams: {
control: {
type: 'object'
},
Expand All @@ -15,11 +15,11 @@ const meta: Meta<LeaderboardFilterRepositoryComponent> = {
};

export default meta;
type Story = StoryObj<LeaderboardFilterRepositoryComponent>;
type Story = StoryObj<LeaderboardFilterTeamComponent>;

export const Default: Story = {
args: {
repositories: ['ls1intum/Artemis', 'ls1intum/Athena', 'ls1intum/Hephaestus']
teams: ['Artemis', 'Athena', 'Hephaestus']
},
render: (args) => ({
props: args,
Expand Down

0 comments on commit 3d59fba

Please sign in to comment.