diff --git a/server/application-server/openapi.yaml b/server/application-server/openapi.yaml index c014e924..311e78ed 100644 --- a/server/application-server/openapi.yaml +++ b/server/application-server/openapi.yaml @@ -314,6 +314,24 @@ paths: type: array items: $ref: "#/components/schemas/LeaderboardEntry" + /activity/{login}: + get: + tags: + - activity + operationId: getActivityByUser + parameters: + - name: login + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Activity" /workspace/team/{teamId}: delete: tags: @@ -434,22 +452,6 @@ components: updatedAt: type: string format: date-time - LabelInfo: - required: - - color - - id - - name - type: object - properties: - id: - type: integer - format: int64 - name: - type: string - color: - type: string - repository: - $ref: "#/components/schemas/RepositoryInfo" UserProfile: required: - contributedRepositories @@ -493,81 +495,80 @@ components: type: string htmlUrl: type: string - PullRequestReviewInfo: + UserTeams: required: - - codeComments - - htmlUrl - id - - isDismissed - - score - - state + - login + - name + - teams + - url type: object properties: id: type: integer format: int64 - isDismissed: - type: boolean - state: + login: type: string - enum: - - COMMENTED - - APPROVED - - CHANGES_REQUESTED - - UNKNOWN - codeComments: - type: integer - format: int32 - author: - $ref: "#/components/schemas/UserInfo" - pullRequest: - $ref: "#/components/schemas/PullRequestBaseInfo" - htmlUrl: + name: type: string - score: - type: integer - format: int32 - submittedAt: + url: type: string - format: date-time - MetaData: - required: - - scheduledDay - - scheduledTime - - teams - type: object - properties: teams: + uniqueItems: true type: array items: $ref: "#/components/schemas/TeamInfo" - scheduledDay: - type: string - scheduledTime: - type: string - UserTeams: + PullRequestWithBadPractices: required: + - additions + - deletions + - htmlUrl - id - - login - - name - - teams - - url + - isDraft + - isMerged + - number + - state + - title type: object properties: id: type: integer format: int64 - login: + number: + type: integer + format: int32 + title: type: string - name: + state: type: string - url: + enum: + - OPEN + - CLOSED + isDraft: + type: boolean + isMerged: + type: boolean + labels: + type: array + items: + $ref: "#/components/schemas/LabelInfo" + repository: + $ref: "#/components/schemas/RepositoryInfo" + additions: + type: integer + format: int32 + deletions: + type: integer + format: int32 + htmlUrl: type: string - teams: - uniqueItems: true + createdAt: + type: string + format: date-time + badPractices: type: array items: - $ref: "#/components/schemas/TeamInfo" + $ref: "#/components/schemas/PullRequestBadPractice" PullRequestBaseInfo: required: - htmlUrl @@ -600,6 +601,15 @@ components: $ref: "#/components/schemas/RepositoryInfo" htmlUrl: type: string + Activity: + required: + - pullRequests + type: object + properties: + pullRequests: + type: array + items: + $ref: "#/components/schemas/PullRequestWithBadPractices" UserInfo: required: - avatarUrl @@ -646,6 +656,81 @@ components: type: array items: $ref: "#/components/schemas/LabelInfo" + PullRequestBadPractice: + type: object + properties: + title: + type: string + description: + type: string + LabelInfo: + required: + - color + - id + - name + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + color: + type: string + repository: + $ref: "#/components/schemas/RepositoryInfo" + PullRequestReviewInfo: + required: + - codeComments + - htmlUrl + - id + - isDismissed + - score + - state + type: object + properties: + id: + type: integer + format: int64 + isDismissed: + type: boolean + state: + type: string + enum: + - COMMENTED + - APPROVED + - CHANGES_REQUESTED + - UNKNOWN + codeComments: + type: integer + format: int32 + author: + $ref: "#/components/schemas/UserInfo" + pullRequest: + $ref: "#/components/schemas/PullRequestBaseInfo" + htmlUrl: + type: string + score: + type: integer + format: int32 + submittedAt: + type: string + format: date-time + MetaData: + required: + - scheduledDay + - scheduledTime + - teams + type: object + properties: + teams: + type: array + items: + $ref: "#/components/schemas/TeamInfo" + scheduledDay: + type: string + scheduledTime: + type: string LeaderboardEntry: required: - numberOfApprovals diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/ActivityController.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/ActivityController.java new file mode 100644 index 00000000..f0d8fd94 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/ActivityController.java @@ -0,0 +1,22 @@ +package de.tum.in.www1.hephaestus.activity; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/activity") +public class ActivityController { + + @Autowired + private ActivityService activityService; + + @GetMapping("/{login}") + public ResponseEntity getActivityByUser(@PathVariable String login) { + ActivityDTO activity = activityService.getActivity(login); + return ResponseEntity.ok(activity); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/ActivityDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/ActivityDTO.java new file mode 100644 index 00000000..7114df40 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/ActivityDTO.java @@ -0,0 +1,9 @@ +package de.tum.in.www1.hephaestus.activity; + +import io.micrometer.common.lang.NonNull; + +import java.util.List; + +public record ActivityDTO( + @NonNull List pullRequests) { +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/ActivityService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/ActivityService.java new file mode 100644 index 00000000..8bb5e16b --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/ActivityService.java @@ -0,0 +1,53 @@ +package de.tum.in.www1.hephaestus.activity; + +import de.tum.in.www1.hephaestus.gitprovider.issue.Issue; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequest; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequestRepository; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class ActivityService { + + @Autowired + private PullRequestRepository pullRequestRepository; + + @Autowired + private PullRequestBadPracticeRepository pullRequestBadPracticeRepository; + + @Transactional + public ActivityDTO getActivity(String login) { + + List pullRequests = pullRequestRepository + .findAssignedByLoginAndStates(login, Set.of(Issue.State.OPEN)); + + List openPulLRequestBadPractices = pullRequestBadPracticeRepository.findByLogin(login); + + Map> pullRequestBadPracticesMap = openPulLRequestBadPractices.stream() + .collect(Collectors.groupingBy( + PullRequestBadPractice::getPullrequest, + Collectors.collectingAndThen( + Collectors.toList(), + list -> list.stream() + .map(PullRequestBadPractice::getType) + .distinct() + .map(PullRequestBadPracticeDTO::fromPullRequestBadPracticeType) + .collect(Collectors.toList()) + ) + )); + + List openPullRequestsWithBadPractices = pullRequests.stream() + .map(pullRequest -> PullRequestWithBadPracticesDTO.fromPullRequest(pullRequest, + pullRequestBadPracticesMap.getOrDefault(pullRequest, List.of())) + ) + .collect(Collectors.toList()); + + return new ActivityDTO(openPullRequestsWithBadPractices); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPractice.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPractice.java new file mode 100644 index 00000000..5539e0eb --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPractice.java @@ -0,0 +1,28 @@ +package de.tum.in.www1.hephaestus.activity; + +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@ToString +public class PullRequestBadPractice { + + @Id + private Long id; + + private PullRequestBadPracticeType type; + + @ManyToOne + @JoinColumn(name = "pullrequest_id") + private PullRequest pullrequest; +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPracticeDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPracticeDTO.java new file mode 100644 index 00000000..95ccdec1 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPracticeDTO.java @@ -0,0 +1,8 @@ +package de.tum.in.www1.hephaestus.activity; + +public record PullRequestBadPracticeDTO(String title, String description) { + + public static PullRequestBadPracticeDTO fromPullRequestBadPracticeType(PullRequestBadPracticeType type) { + return new PullRequestBadPracticeDTO(type.title, type.description); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPracticeRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPracticeRepository.java new file mode 100644 index 00000000..8027e3d2 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPracticeRepository.java @@ -0,0 +1,19 @@ +package de.tum.in.www1.hephaestus.activity; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface PullRequestBadPracticeRepository extends JpaRepository { + + @Query(""" + SELECT prbp + FROM PullRequestBadPractice prbp + WHERE LOWER(:assigneeLogin) IN (SELECT LOWER(u.login) FROM prbp.pullrequest.assignees u) AND prbp.pullrequest.state = 'OPEN' + """) + List findByLogin(@Param("assigneeLogin") String assigneeLogin); +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPracticeType.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPracticeType.java new file mode 100644 index 00000000..2c914d34 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestBadPracticeType.java @@ -0,0 +1,17 @@ +package de.tum.in.www1.hephaestus.activity; + +public enum PullRequestBadPracticeType { + + EMPTY_DESCRIPTION("Empty Description", "The description of your pull request is empty."), + EMPTY_DESCRIPTION_SECTION("Empty Description Section", "The description of your pull request contains an empty section."), + UNCHECKED_CHECKBOX("Unchecked Checkbox", "A checkbox in the description of your pull request is unchecked."); + + public final String title; + + public final String description; + + PullRequestBadPracticeType(String title, String description) { + this.title = title; + this.description = description; + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestWithBadPracticesDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestWithBadPracticesDTO.java new file mode 100644 index 00000000..063168e3 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/activity/PullRequestWithBadPracticesDTO.java @@ -0,0 +1,50 @@ +package de.tum.in.www1.hephaestus.activity; + +import de.tum.in.www1.hephaestus.gitprovider.issue.Issue; +import de.tum.in.www1.hephaestus.gitprovider.label.LabelInfoDTO; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequest; +import de.tum.in.www1.hephaestus.gitprovider.repository.RepositoryInfoDTO; +import org.springframework.lang.NonNull; + +import java.time.OffsetDateTime; +import java.util.Comparator; +import java.util.List; + +public record PullRequestWithBadPracticesDTO( + @NonNull Long id, + @NonNull Integer number, + @NonNull String title, + @NonNull Issue.State state, + @NonNull Boolean isDraft, + @NonNull Boolean isMerged, + List labels, + RepositoryInfoDTO repository, + @NonNull Integer additions, + @NonNull Integer deletions, + @NonNull String htmlUrl, + OffsetDateTime createdAt, + List badPractices +) { + + public static PullRequestWithBadPracticesDTO fromPullRequest(PullRequest pullRequest, List badPractices) { + return new PullRequestWithBadPracticesDTO( + pullRequest.getId(), + pullRequest.getNumber(), + pullRequest.getTitle(), + pullRequest.getState(), + pullRequest.isDraft(), + pullRequest.isMerged(), + pullRequest.getLabels() + .stream() + .map(LabelInfoDTO::fromLabel) + .sorted(Comparator.comparing(LabelInfoDTO::name)) + .toList(), + RepositoryInfoDTO.fromRepository(pullRequest.getRepository()), + pullRequest.getAdditions(), + pullRequest.getDeletions(), + pullRequest.getHtmlUrl(), + pullRequest.getCreatedAt(), + badPractices + ); + } +} diff --git a/webapp/src/app/app.routes.ts b/webapp/src/app/app.routes.ts index a7397fad..d9ff0aee 100644 --- a/webapp/src/app/app.routes.ts +++ b/webapp/src/app/app.routes.ts @@ -11,6 +11,7 @@ import { ImprintComponent } from '@app/legal/imprint.component'; import { PrivacyComponent } from '@app/legal/privacy.component'; import { AdminGuard } from '@app/core/security/admin.guard'; import { AuthGuard } from '@app/core/security/auth.guard'; +import { ActivityDashboardComponent } from '@app/home/activity/activity-dashboard.component'; export const routes: Routes = [ // Public routes @@ -47,7 +48,8 @@ export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'user/:id', component: UserProfileComponent }, { path: 'settings', component: SettingsComponent }, - { path: 'workspace', component: WorkspaceComponent, canActivate: [AdminGuard] } + { path: 'workspace', component: WorkspaceComponent, canActivate: [AdminGuard] }, + { path: 'activity/:id', component: ActivityDashboardComponent } ] } ]; diff --git a/webapp/src/app/core/modules/openapi/.openapi-generator/FILES b/webapp/src/app/core/modules/openapi/.openapi-generator/FILES index 277cfc6f..af8f2e42 100644 --- a/webapp/src/app/core/modules/openapi/.openapi-generator/FILES +++ b/webapp/src/app/core/modules/openapi/.openapi-generator/FILES @@ -2,6 +2,8 @@ .openapi-generator-ignore README.md api.module.ts +api/activity.service.ts +api/activity.serviceInterface.ts api/api.ts api/leaderboard.service.ts api/leaderboard.serviceInterface.ts @@ -17,13 +19,16 @@ configuration.ts encoder.ts git_push.sh index.ts +model/activity.ts model/label-info.ts model/leaderboard-entry.ts model/meta-data.ts model/models.ts +model/pull-request-bad-practice.ts model/pull-request-base-info.ts model/pull-request-info.ts model/pull-request-review-info.ts +model/pull-request-with-bad-practices.ts model/repository-info.ts model/team-info.ts model/user-info.ts diff --git a/webapp/src/app/core/modules/openapi/api/activity.service.ts b/webapp/src/app/core/modules/openapi/api/activity.service.ts new file mode 100644 index 00000000..1a546978 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/api/activity.service.ts @@ -0,0 +1,161 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { Activity } from '../model/activity'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { + ActivityServiceInterface +} from './activity.serviceInterface'; + + + +@Injectable({ + providedIn: 'root' +}) +export class ActivityService implements ActivityServiceInterface { + + protected basePath = 'http://localhost'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * @param login + * @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 getActivityByUser(login: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getActivityByUser(login: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getActivityByUser(login: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getActivityByUser(login: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (login === null || login === undefined) { + throw new Error('Required parameter login was null or undefined when calling getActivityByUser.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/activity/${this.configuration.encodeParam({name: "login", value: login, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/webapp/src/app/core/modules/openapi/api/activity.serviceInterface.ts b/webapp/src/app/core/modules/openapi/api/activity.serviceInterface.ts new file mode 100644 index 00000000..c7525c30 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/api/activity.serviceInterface.ts @@ -0,0 +1,34 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { HttpHeaders } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { Activity } from '../model/models'; + + +import { Configuration } from '../configuration'; + + + +export interface ActivityServiceInterface { + defaultHeaders: HttpHeaders; + configuration: Configuration; + + /** + * + * + * @param login + */ + getActivityByUser(login: string, extraHttpRequestParams?: any): Observable; + +} diff --git a/webapp/src/app/core/modules/openapi/api/api.ts b/webapp/src/app/core/modules/openapi/api/api.ts index c77e638b..99cd91ee 100644 --- a/webapp/src/app/core/modules/openapi/api/api.ts +++ b/webapp/src/app/core/modules/openapi/api/api.ts @@ -1,3 +1,6 @@ +export * from './activity.service'; +import { ActivityService } from './activity.service'; +export * from './activity.serviceInterface'; export * from './leaderboard.service'; import { LeaderboardService } from './leaderboard.service'; export * from './leaderboard.serviceInterface'; @@ -13,4 +16,4 @@ export * from './user.serviceInterface'; export * from './workspace.service'; import { WorkspaceService } from './workspace.service'; export * from './workspace.serviceInterface'; -export const APIS = [LeaderboardService, MetaService, TeamService, UserService, WorkspaceService]; +export const APIS = [ActivityService, LeaderboardService, MetaService, TeamService, UserService, WorkspaceService]; diff --git a/webapp/src/app/core/modules/openapi/model/activity.ts b/webapp/src/app/core/modules/openapi/model/activity.ts new file mode 100644 index 00000000..5e254463 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/activity.ts @@ -0,0 +1,18 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { PullRequestWithBadPractices } from './pull-request-with-bad-practices'; + + +export interface Activity { + pullRequests: Array; +} + diff --git a/webapp/src/app/core/modules/openapi/model/models.ts b/webapp/src/app/core/modules/openapi/model/models.ts index c2764ea9..a5cf878c 100644 --- a/webapp/src/app/core/modules/openapi/model/models.ts +++ b/webapp/src/app/core/modules/openapi/model/models.ts @@ -1,9 +1,12 @@ +export * from './activity'; export * from './label-info'; export * from './leaderboard-entry'; export * from './meta-data'; +export * from './pull-request-bad-practice'; export * from './pull-request-base-info'; export * from './pull-request-info'; export * from './pull-request-review-info'; +export * from './pull-request-with-bad-practices'; export * from './repository-info'; export * from './team-info'; export * from './user-info'; diff --git a/webapp/src/app/core/modules/openapi/model/pull-request-bad-practice.ts b/webapp/src/app/core/modules/openapi/model/pull-request-bad-practice.ts new file mode 100644 index 00000000..4ac490a5 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/pull-request-bad-practice.ts @@ -0,0 +1,18 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface PullRequestBadPractice { + title?: string; + description?: string; +} + diff --git a/webapp/src/app/core/modules/openapi/model/pull-request-with-bad-practices.ts b/webapp/src/app/core/modules/openapi/model/pull-request-with-bad-practices.ts new file mode 100644 index 00000000..d1e03975 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/pull-request-with-bad-practices.ts @@ -0,0 +1,40 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { LabelInfo } from './label-info'; +import { RepositoryInfo } from './repository-info'; +import { PullRequestBadPractice } from './pull-request-bad-practice'; + + +export interface PullRequestWithBadPractices { + id: number; + number: number; + title: string; + state: PullRequestWithBadPractices.StateEnum; + isDraft: boolean; + isMerged: boolean; + labels?: Array; + repository?: RepositoryInfo; + additions: number; + deletions: number; + htmlUrl: string; + createdAt?: string; + badPractices?: Array; +} +export namespace PullRequestWithBadPractices { + export type StateEnum = 'OPEN' | 'CLOSED'; + export const StateEnum = { + Open: 'OPEN' as StateEnum, + Closed: 'CLOSED' as StateEnum + }; +} + + diff --git a/webapp/src/app/home/activity/activity-dashboard.component.html b/webapp/src/app/home/activity/activity-dashboard.component.html new file mode 100644 index 00000000..366443eb --- /dev/null +++ b/webapp/src/app/home/activity/activity-dashboard.component.html @@ -0,0 +1,61 @@ +
+
+
+
+
+

Activities

+

+ You currently have {{ numberOfPullRequests() }} open pull requests and {{ numberOfBadPractices() }} detected bad + practices. +

+
+
+
+
+

Your open pull requests

+
+ @if (query.data()?.pullRequests) { + @for (pullRequest of query.data()?.pullRequests; track pullRequest.id) { + + + } + } +
+
+
+
+
+

Detected bad practices

+
+ @if (query.data()?.pullRequests) { + @for (pullRequest of query.data()?.pullRequests; track pullRequest.id) { + @for (badPractice of pullRequest.badPractices; track badPractice.title) { + + + } + } + } +
+
+
+
+
+
diff --git a/webapp/src/app/home/activity/activity-dashboard.component.ts b/webapp/src/app/home/activity/activity-dashboard.component.ts new file mode 100644 index 00000000..9bf33b30 --- /dev/null +++ b/webapp/src/app/home/activity/activity-dashboard.component.ts @@ -0,0 +1,32 @@ +import { Component, computed, inject } from '@angular/core'; +import { ActivityService } from '@app/core/modules/openapi'; +import { injectQuery } from '@tanstack/angular-query-experimental'; +import { combineLatest, lastValueFrom, map, timer } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; +import { IssueCardComponent } from '@app/user/issue-card/issue-card.component'; +import { BadPracticeCardComponent } from '@app/user/bad-practice-card/bad-practice-card.component'; + +@Component({ + selector: 'app-activity-dashboard', + standalone: true, + imports: [IssueCardComponent, BadPracticeCardComponent], + templateUrl: './activity-dashboard.component.html', + styles: `` +}) +export class ActivityDashboardComponent { + activityService = inject(ActivityService); + + protected userLogin: string | null = null; + protected numberOfPullRequests = computed(() => this.query.data()?.pullRequests?.length ?? 0); + protected numberOfBadPractices = computed(() => this.query.data()?.pullRequests?.reduce((acc, pr) => acc + (pr.badPractices?.length ?? 0), 0) ?? 0); + + constructor(private route: ActivatedRoute) { + this.userLogin = this.route.snapshot.paramMap.get('id'); + } + + query = injectQuery(() => ({ + queryKey: ['user', { id: this.userLogin }], + enabled: !!this.userLogin, + queryFn: async () => lastValueFrom(combineLatest([this.activityService.getActivityByUser(this.userLogin!), timer(400)]).pipe(map(([activity]) => activity))) + })); +} diff --git a/webapp/src/app/user/bad-practice-card/bad-practice-card.component.html b/webapp/src/app/user/bad-practice-card/bad-practice-card.component.html new file mode 100644 index 00000000..f70652e0 --- /dev/null +++ b/webapp/src/app/user/bad-practice-card/bad-practice-card.component.html @@ -0,0 +1,7 @@ + +
+

{{ repositoryName() }} #{{ number() }}

+

{{ title() }}

+
{{ description() }}
+
+
diff --git a/webapp/src/app/user/bad-practice-card/bad-practice-card.component.ts b/webapp/src/app/user/bad-practice-card/bad-practice-card.component.ts new file mode 100644 index 00000000..161f4113 --- /dev/null +++ b/webapp/src/app/user/bad-practice-card/bad-practice-card.component.ts @@ -0,0 +1,16 @@ +import { Component, input } from '@angular/core'; +import { HlmCardModule } from '@spartan-ng/ui-card-helm'; + +@Component({ + selector: 'app-bad-practice-card', + standalone: true, + imports: [HlmCardModule], + templateUrl: './bad-practice-card.component.html', + styles: `` +}) +export class BadPracticeCardComponent { + title = input(); + description = input(); + repositoryName = input(); + number = input(); +} diff --git a/webapp/src/app/user/bad-practice-card/bad-practice-card.stories.ts b/webapp/src/app/user/bad-practice-card/bad-practice-card.stories.ts new file mode 100644 index 00000000..381a20c3 --- /dev/null +++ b/webapp/src/app/user/bad-practice-card/bad-practice-card.stories.ts @@ -0,0 +1,27 @@ +import { Meta, StoryObj } from '@storybook/angular'; +import { BadPracticeCardComponent } from './bad-practice-card.component'; + +const meta: Meta = { + component: BadPracticeCardComponent, + tags: ['autodocs'] // Auto-generate docs if enabled +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'Avoid using any type', + description: 'Using the any type defeats the purpose of TypeScript.', + repositoryName: 'Hepheastus', + number: 200 + } +}; +/* +export const isLoading: Story = { + args: { + title: 'Avoid using any type', + description: 'Using the any type defeats the purpose of TypeScript.' + } +};*/