diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts index df3eec824ca7..410b0a067886 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { SafeHtml } from '@angular/platform-browser'; import { ProgrammingExerciseBuildConfig } from 'app/entities/programming/programming-exercise-build.config'; -import { Subject, Subscription, of } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming/programming-exercise.model'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { AlertService, AlertType } from 'app/core/util/alert.service'; @@ -57,9 +57,8 @@ import { IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-setting import { Detail } from 'app/detail-overview-list/detail.model'; import { Competency } from 'app/entities/competency.model'; import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.service'; -import { switchMap, tap } from 'rxjs/operators'; +import { mergeMap, tap } from 'rxjs/operators'; import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; -import { BuildLogStatisticsDTO } from 'app/entities/programming/build-log-statistics-dto'; @Component({ selector: 'jhi-programming-exercise-detail', @@ -194,7 +193,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { this.loadingTemplateParticipationResults = false; this.loadingSolutionParticipationResults = false; }), - switchMap(() => this.profileService.getProfileInfo()), + mergeMap(() => this.profileService.getProfileInfo()), tap(async (profileInfo) => { if (profileInfo) { if (this.programmingExercise.projectKey && this.programmingExercise.templateParticipation?.buildPlanId) { @@ -224,22 +223,14 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { } } }), - switchMap(() => this.programmingExerciseSubmissionPolicyService.getSubmissionPolicyOfProgrammingExercise(exerciseId!)), + mergeMap(() => this.programmingExerciseSubmissionPolicyService.getSubmissionPolicyOfProgrammingExercise(exerciseId!)), tap((submissionPolicy) => { this.programmingExercise.submissionPolicy = submissionPolicy; }), - switchMap(() => this.programmingExerciseService.getDiffReport(this.programmingExercise.id!)), + mergeMap(() => this.programmingExerciseService.getDiffReport(this.programmingExercise.id!)), tap((gitDiffReport) => { this.processGitDiffReport(gitDiffReport); }), - switchMap(() => - this.programmingExercise.isAtLeastEditor ? this.programmingExerciseService.getBuildLogStatistics(exerciseId!) : of([] as BuildLogStatisticsDTO), - ), - tap((buildLogStatistics) => { - if (this.programmingExercise.isAtLeastEditor) { - this.programmingExercise.buildLogStatistics = buildLogStatistics; - } - }), ) .subscribe({ next: () => { @@ -248,6 +239,8 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature( programmingExercise.programmingLanguage, ).plagiarismCheckSupported; + + /** we make sure to await the results of the subscriptions (mergeMap) to only call {@link getExerciseDetails} once */ this.exerciseDetailSections = this.getExerciseDetails(); }, error: (error) => { @@ -258,6 +251,13 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { this.exerciseStatisticsSubscription = this.statisticsService.getExerciseStatistics(exerciseId!).subscribe((statistics: ExerciseManagementStatisticsDto) => { this.doughnutStats = statistics; }); + + if (this.programmingExercise.isAtLeastEditor) { + this.buildLogsSubscription = this.programmingExerciseService + .getBuildLogStatistics(exerciseId!) + .subscribe((buildLogStatistics) => (this.programmingExercise.buildLogStatistics = buildLogStatistics)); + this.exerciseDetailSections = this.getExerciseDetails(); + } }); } @@ -272,6 +272,13 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { this.exerciseStatisticsSubscription?.unsubscribe(); } + /** + * BE CAREFUL WHEN CALLING THIS METHOD!
+ * This method can cause child components to re-render, which can lead to re-initializations resulting + * in unnecessary requests putting load on the server. + * + * When adding a new call to this method, make sure that no duplicated and unnecessary requests are made. + */ getExerciseDetails(): DetailOverviewSection[] { const exercise = this.programmingExercise; exercise.buildConfig = this.programmingExerciseBuildConfig; diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts index 4fc8013ced0e..18b9e883df71 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts @@ -33,6 +33,8 @@ import { } from 'app/exercises/programming/shared/service/programming-language-feature/programming-language-feature.service'; import { MockRouter } from '../../helpers/mocks/mock-router'; import { BuildConfig } from '../../../../../main/webapp/app/entities/programming/build-config.model'; +import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; +import { BuildLogStatisticsDTO } from 'app/entities/programming/build-log-statistics-dto'; describe('ProgrammingExerciseDetailComponent', () => { let comp: ProgrammingExerciseDetailComponent; @@ -43,6 +45,8 @@ describe('ProgrammingExerciseDetailComponent', () => { let profileService: ProfileService; let programmingLanguageFeatureService: ProgrammingLanguageFeatureService; let statisticsServiceStub: jest.SpyInstance; + let gitDiffReportStub: jest.SpyInstance; + let buildLogStatisticsStub: jest.SpyInstance; let findWithTemplateAndSolutionParticipationStub: jest.SpyInstance; let router: Router; let modalService: NgbModal; @@ -75,6 +79,30 @@ describe('ProgrammingExerciseDetailComponent', () => { resolvedPostsInPercent: 50, } as ExerciseManagementStatisticsDto; + const gitDiffReport = { + templateRepositoryCommitHash: 'x1', + solutionRepositoryCommitHash: 'x2', + entries: [ + { + previousFilePath: '/src/test.java', + filePath: '/src/test.java', + previousStartLine: 1, + startLine: 1, + previousLineCount: 2, + lineCount: 2, + }, + ], + } as ProgrammingExerciseGitDiffReport; + + const buildLogStatistics = { + buildCount: 5, + agentSetupDuration: 2.5, + testDuration: 3, + scaDuration: 2, + totalJobDuration: 7.5, + dependenciesDownloadedCount: 6, + } as BuildLogStatisticsDTO; + const profileInfo = { activeProfiles: [], } as unknown as ProfileInfo; @@ -111,6 +139,8 @@ describe('ProgrammingExerciseDetailComponent', () => { findWithTemplateAndSolutionParticipationStub = jest .spyOn(exerciseService, 'findWithTemplateAndSolutionParticipationAndLatestResults') .mockReturnValue(of(new HttpResponse({ body: mockProgrammingExercise }))); + gitDiffReportStub = jest.spyOn(exerciseService, 'getDiffReport').mockReturnValue(of(gitDiffReport)); + buildLogStatisticsStub = jest.spyOn(exerciseService, 'getBuildLogStatistics').mockReturnValue(of(buildLogStatistics)); jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(profileInfo)); jest.spyOn(programmingLanguageFeatureService, 'getProgrammingLanguageFeature').mockReturnValue({ @@ -131,6 +161,7 @@ describe('ProgrammingExerciseDetailComponent', () => { comp.onParticipationChange(); tick(); expect(loadDiffSpy).toHaveBeenCalledOnce(); + expect(gitDiffReportStub).toHaveBeenCalledOnce(); expect(comp.programmingExercise.coveredLinesRatio).toBe(0.5); })); @@ -157,6 +188,25 @@ describe('ProgrammingExerciseDetailComponent', () => { expect(comp.doughnutStats.resolvedPostsInPercent).toBe(50); expect(comp.doughnutStats.absoluteAveragePoints).toBe(5); }); + + it.each([true, false])( + 'should only call service method to get build log statistics onInit if the user is at least an editor for this exercise', + async (isEditor: boolean) => { + const programmingExercise = new ProgrammingExercise(new Course(), undefined); + programmingExercise.id = 123; + programmingExercise.isAtLeastEditor = isEditor; + jest.spyOn(exerciseService, 'findWithTemplateAndSolutionParticipationAndLatestResults').mockReturnValue( + of({ body: programmingExercise } as unknown as HttpResponse), + ); + comp.ngOnInit(); + await new Promise((r) => setTimeout(r, 100)); + if (isEditor) { + expect(buildLogStatisticsStub).toHaveBeenCalledOnce(); + } else { + expect(buildLogStatisticsStub).not.toHaveBeenCalled(); + } + }, + ); }); describe('onInit for exam exercise', () => {