diff --git a/server/application-server/openapi.yaml b/server/application-server/openapi.yaml index eecdda44..cca733ce 100644 --- a/server/application-server/openapi.yaml +++ b/server/application-server/openapi.yaml @@ -216,7 +216,7 @@ paths: schema: type: string format: date - - name: repository + - name: team in: query required: false schema: 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 0c17e622..48b4b6e7 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 @@ -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 { @Query("SELECT u FROM User u WHERE u.login = :login") @@ -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 findAllInTimeframe(@Param("after") OffsetDateTime after, @Param("before") OffsetDateTime before, - @Param("repository") Optional repository); + @Param("team") Optional team); @Query(""" SELECT u 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 d4f13f78..0f6a1899 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 @@ -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 { @@ -53,10 +54,10 @@ public List getAllUsersWithTeams() { return userRepository.findAll().stream().map(UserTeamsDTO::fromUser).toList(); } - public List getAllUsersInTimeframe(OffsetDateTime after, OffsetDateTime before, Optional 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 getAllUsersInTimeframe(OffsetDateTime after, OffsetDateTime before, Optional 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 getUserProfileDTO(String login) { 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 index 5ced3e6a..56637fdd 100644 --- 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 @@ -25,7 +25,7 @@ public LeaderboardController(LeaderboardService leaderboardService) { public ResponseEntity> getLeaderboard( @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Optional after, @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Optional before, - @RequestParam Optional repository) { - return ResponseEntity.ok(leaderboardService.createLeaderboard(after, before, repository)); + @RequestParam Optional team) { + return ResponseEntity.ok(leaderboardService.createLeaderboard(after, before, team)); } } 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 index 4b7668b9..0c2a7f87 100644 --- 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 @@ -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; @@ -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 createLeaderboard(Optional after, Optional before, - Optional repository) { + Optional team) { logger.info("Creating leaderboard dataset"); LocalDateTime afterCutOff = after.isPresent() ? after.get().atStartOfDay() : LocalDate.now().minusDays(timeframe).atStartOfDay(); Optional beforeCutOff = before.map(date -> date.plusDays(1).atStartOfDay()); + Optional teamEntity = team.map((t) -> teamService.getTeam(t)).orElse(Optional.empty()); List 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"); diff --git a/webapp/src/app/core/modules/openapi/api/leaderboard.service.ts b/webapp/src/app/core/modules/openapi/api/leaderboard.service.ts index 7cc7ccc9..0e73a1a9 100644 --- a/webapp/src/app/core/modules/openapi/api/leaderboard.service.ts +++ b/webapp/src/app/core/modules/openapi/api/leaderboard.service.ts @@ -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>; - public getLeaderboard(after?: string, before?: string, repository?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getLeaderboard(after?: string, before?: string, repository?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public getLeaderboard(after?: string, before?: string, repository?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + public getLeaderboard(after?: string, before?: string, team?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getLeaderboard(after?: string, before?: string, team?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getLeaderboard(after?: string, before?: string, team?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getLeaderboard(after?: string, before?: string, team?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { let localVarQueryParameters = new HttpParams({encoder: this.encoder}); if (after !== undefined && after !== null) { @@ -116,9 +116,9 @@ export class LeaderboardService implements LeaderboardServiceInterface { localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, before, 'before'); } - if (repository !== undefined && repository !== null) { + if (team !== undefined && team !== null) { localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, - repository, 'repository'); + team, 'team'); } let localVarHeaders = this.defaultHeaders; diff --git a/webapp/src/app/core/modules/openapi/api/leaderboard.serviceInterface.ts b/webapp/src/app/core/modules/openapi/api/leaderboard.serviceInterface.ts index 54aadd9a..8297cef6 100644 --- a/webapp/src/app/core/modules/openapi/api/leaderboard.serviceInterface.ts +++ b/webapp/src/app/core/modules/openapi/api/leaderboard.serviceInterface.ts @@ -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>; + getLeaderboard(after?: string, before?: string, team?: string, extraHttpRequestParams?: any): Observable>; } diff --git a/webapp/src/app/home/home.component.html b/webapp/src/app/home/home.component.html index 2d9fd936..87c7cadd 100644 --- a/webapp/src/app/home/home.component.html +++ b/webapp/src/app/home/home.component.html @@ -8,7 +8,7 @@

Artemis Leaderboard

Hi {{ userValue.name }} 👋

} - +
@if (query.error()) { diff --git a/webapp/src/app/home/home.component.ts b/webapp/src/app/home/home.component.ts index b30b692c..a499c41f 100644 --- a/webapp/src/app/home/home.component.ts +++ b/webapp/src/app/home/home.component.ts @@ -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); @@ -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; @@ -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()) })); } diff --git a/webapp/src/app/home/leaderboard/filter/filter.component.html b/webapp/src/app/home/leaderboard/filter/filter.component.html index cbc4edaf..8f956684 100644 --- a/webapp/src/app/home/leaderboard/filter/filter.component.html +++ b/webapp/src/app/home/leaderboard/filter/filter.component.html @@ -5,8 +5,8 @@

Filter

- @if (repositories() && repositories()!.length > 1) { - + @if (teams() && teams()!.length > 1) { + }
diff --git a/webapp/src/app/home/leaderboard/filter/filter.component.ts b/webapp/src/app/home/leaderboard/filter/filter.component.ts index b4d4cb72..19f3cf54 100644 --- a/webapp/src/app/home/leaderboard/filter/filter.component.ts +++ b/webapp/src/app/home/leaderboard/filter/filter.component.ts @@ -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(); + teams = input(); } diff --git a/webapp/src/app/home/leaderboard/filter/filter.stories.ts b/webapp/src/app/home/leaderboard/filter/filter.stories.ts index 5b6df996..25cbac7e 100644 --- a/webapp/src/app/home/leaderboard/filter/filter.stories.ts +++ b/webapp/src/app/home/leaderboard/filter/filter.stories.ts @@ -5,24 +5,14 @@ const meta: Meta = { 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' } } }; @@ -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, diff --git a/webapp/src/app/home/leaderboard/filter/repository/repository.component.html b/webapp/src/app/home/leaderboard/filter/team/team.component.html similarity index 89% rename from webapp/src/app/home/leaderboard/filter/repository/repository.component.html rename to webapp/src/app/home/leaderboard/filter/team/team.component.html index 17acd372..593c9b1b 100644 --- a/webapp/src/app/home/leaderboard/filter/repository/repository.component.html +++ b/webapp/src/app/home/leaderboard/filter/team/team.component.html @@ -1,5 +1,5 @@ - + diff --git a/webapp/src/app/home/leaderboard/filter/repository/repository.component.ts b/webapp/src/app/home/leaderboard/filter/team/team.component.ts similarity index 72% rename from webapp/src/app/home/leaderboard/filter/repository/repository.component.ts rename to webapp/src/app/home/leaderboard/filter/team/team.component.ts index 982bf837..c0cb2ad4 100644 --- a/webapp/src/app/home/leaderboard/filter/repository/repository.component.ts +++ b/webapp/src/app/home/leaderboard/filter/team/team.component.ts @@ -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(); +export class LeaderboardFilterTeamComponent { + teams = input.required(); value = signal(''); 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, @@ -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 diff --git a/webapp/src/app/home/leaderboard/filter/repository/repository.stories.ts b/webapp/src/app/home/leaderboard/filter/team/team.stories.ts similarity index 54% rename from webapp/src/app/home/leaderboard/filter/repository/repository.stories.ts rename to webapp/src/app/home/leaderboard/filter/team/team.stories.ts index 0fd723d7..566174a0 100644 --- a/webapp/src/app/home/leaderboard/filter/repository/repository.stories.ts +++ b/webapp/src/app/home/leaderboard/filter/team/team.stories.ts @@ -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 = { - component: LeaderboardFilterRepositoryComponent, +const meta: Meta = { + component: LeaderboardFilterTeamComponent, tags: ['autodocs'], argTypes: { - repositories: { + teams: { control: { type: 'object' }, @@ -15,11 +15,11 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: { - repositories: ['ls1intum/Artemis', 'ls1intum/Athena', 'ls1intum/Hephaestus'] + teams: ['Artemis', 'Athena', 'Hephaestus'] }, render: (args) => ({ props: args,