Skip to content

Commit

Permalink
Development: Prevent IDE settings from being loaded multiple times on…
Browse files Browse the repository at this point in the history
… the scores page
  • Loading branch information
krusche committed Oct 24, 2024
1 parent caea966 commit d3ddb92
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 30 deletions.
1 change: 1 addition & 0 deletions src/main/webapp/app/core/auth/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class AccountService implements IAccountService {
private websocketService = inject(JhiWebsocketService);
private featureToggleService = inject(FeatureToggleService);

// cached value of the user to avoid unnecessary requests to the server
private userIdentityValue?: User;
private authenticated = false;
private authenticationState = new BehaviorSubject<User | undefined>(undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,13 @@ export class CodeButtonComponent implements OnInit, OnChanges {
readonly FeatureToggle = FeatureToggle;
readonly ProgrammingLanguage = ProgrammingLanguage;

@Input()
loading = false;
@Input()
useParticipationVcsAccessToken = false;
@Input()
smallButtons: boolean;
@Input()
repositoryUri?: string;
@Input()
routerLinkForRepositoryView?: string | (string | number)[];
@Input()
participations?: ProgrammingExerciseStudentParticipation[];
@Input()
exercise?: ProgrammingExercise;
@Input() loading = false;
@Input() useParticipationVcsAccessToken = false;
@Input() smallButtons: boolean;
@Input() repositoryUri?: string;
@Input() routerLinkForRepositoryView?: string | (string | number)[];
@Input() participations?: ProgrammingExerciseStudentParticipation[];
@Input() exercise?: ProgrammingExercise;

useSsh = false;
useToken = false;
Expand Down Expand Up @@ -133,7 +126,7 @@ export class CodeButtonComponent implements OnInit, OnChanges {
this.sshKeyMissingTip = this.formatTip('artemisApp.exerciseActions.sshKeyTip', this.sshSettingsUrl);
});

this.ideSettingsService.loadIdePreferences().subscribe((programmingLanguageToIde) => {
this.ideSettingsService.loadIdePreferences().then((programmingLanguageToIde) => {
if (programmingLanguageToIde.size) {
this.programmingLanguageToIde = programmingLanguageToIde;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class IdeSettingsComponent implements OnInit {
this.PREDEFINED_IDE = predefinedIde;
});

this.ideSettingsService.loadIdePreferences().subscribe((programmingLanguageToIdeMap) => {
this.ideSettingsService.loadIdePreferences(true).then((programmingLanguageToIdeMap) => {
if (!programmingLanguageToIdeMap.has(ProgrammingLanguage.EMPTY)) {
programmingLanguageToIdeMap.set(ProgrammingLanguage.EMPTY, this.PREDEFINED_IDE[0]);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { Observable, lastValueFrom } from 'rxjs';
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { Ide, IdeMappingDTO } from 'app/shared/user-settings/ide-preferences/ide.model';
import { ProgrammingLanguage } from 'app/entities/programming/programming-exercise.model';

@Injectable({ providedIn: 'root' })
export class IdeSettingsService {
public ideSettingsUrl = 'api/ide-settings';
error?: string;
public readonly ideSettingsUrl = 'api/ide-settings';

// cached value of the ide preferences to avoid unnecessary requests to the server
private idePreferences?: Map<ProgrammingLanguage, Ide>;

constructor(private http: HttpClient) {}

Expand All @@ -20,12 +22,45 @@ export class IdeSettingsService {
return this.http.get<Ide[]>(this.ideSettingsUrl + '/predefined');
}

private ongoingRequest: Promise<Map<ProgrammingLanguage, Ide>> | undefined = undefined;
private cacheTimestamp: number | undefined = undefined; // To store the timestamp when the preferences were loaded
private readonly cacheDuration = 60 * 1000; // 1 minute in milliseconds

/**
* GET call to the server to receive the stored ide preferences of the current user
* Prevent concurrent requests by caching the ongoing request and the timestamp when the preferences were loaded
* Load the settings again after 1min in case they have changed
* @param force if true, the cache will be ignored and a new request will be made
* @return the saved ide preference which were found in the database or error
*/
public loadIdePreferences(): Observable<Map<ProgrammingLanguage, Ide>> {
return this.http.get<IdeMappingDTO[]>(this.ideSettingsUrl).pipe(map((data) => new Map<ProgrammingLanguage, Ide>(data.map((x) => [x.programmingLanguage, x.ide]))));
public loadIdePreferences(force?: boolean): Promise<Map<ProgrammingLanguage, Ide>> {
const currentTime = new Date().getTime();

// If preferences are already loaded and the cache is valid, return them immediately
if (this.idePreferences && !force && this.cacheTimestamp && currentTime - this.cacheTimestamp < this.cacheDuration) {
return Promise.resolve(this.idePreferences);
}

// If there's already an ongoing request, return that promise to prevent multiple calls
if (this.ongoingRequest) {
return this.ongoingRequest;
}

// Make the REST call and cache the ongoing request
this.ongoingRequest = lastValueFrom(
this.http.get<IdeMappingDTO[]>(this.ideSettingsUrl).pipe(
map((data) => {
this.idePreferences = new Map<ProgrammingLanguage, Ide>(data.map((x) => [x.programmingLanguage, x.ide]));
this.cacheTimestamp = new Date().getTime(); // Update the timestamp when the data is cached
return this.idePreferences;
}),
),
).finally(() => {
// Clear the ongoingRequest once the promise resolves
this.ongoingRequest = undefined;
});

return this.ongoingRequest;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { of } from 'rxjs';
import { IdeSettingsComponent } from 'app/shared/user-settings/ide-preferences/ide-settings.component';
Expand Down Expand Up @@ -34,27 +34,33 @@ describe('IdeSettingsComponent', () => {
jest.resetAllMocks();
});

it('should load predefined IDEs and IDE preferences on init', () => {
it('should load predefined IDEs and IDE preferences on init', fakeAsync(() => {
const predefinedIdes = [
{ name: 'VS Code', deepLink: 'vscode://vscode.git/clone?url={cloneUrl}' },
{ name: 'IntelliJ', deepLink: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo={cloneUrl}' },
];
const idePreferences = new Map([[ProgrammingLanguage.JAVA, predefinedIdes[0]]]);
const loadedIdePreferences = new Map([
[ProgrammingLanguage.JAVA, predefinedIdes[0]],
[ProgrammingLanguage.EMPTY, predefinedIdes[0]],
]);

mockIdeSettingsService.loadPredefinedIdes.mockReturnValue(of(predefinedIdes));
mockIdeSettingsService.loadIdePreferences.mockReturnValue(of(idePreferences));
mockIdeSettingsService.loadIdePreferences.mockReturnValue(Promise.resolve(idePreferences));

component.ngOnInit();

tick();

expect(mockIdeSettingsService.loadPredefinedIdes).toHaveBeenCalledOnce();
expect(mockIdeSettingsService.loadIdePreferences).toHaveBeenCalledOnce();
expect(component.PREDEFINED_IDE).toEqual(predefinedIdes);
expect(component.programmingLanguageToIde()).toEqual(idePreferences);
expect(component.programmingLanguageToIde()).toEqual(loadedIdePreferences);
expect(component.assignedProgrammingLanguages).toEqual([ProgrammingLanguage.JAVA]);
expect(component.remainingProgrammingLanguages).toEqual(
Object.values(ProgrammingLanguage).filter((x) => x !== ProgrammingLanguage.JAVA && x !== ProgrammingLanguage.EMPTY),
);
});
}));

it('should add a programming language and update the lists', () => {
const programmingLanguage = ProgrammingLanguage.JAVA;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TestBed } from '@angular/core/testing';
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { IdeSettingsService } from 'app/shared/user-settings/ide-preferences/ide-settings.service';
import { Ide, IdeMappingDTO } from 'app/shared/user-settings/ide-preferences/ide.model';
Expand Down Expand Up @@ -34,20 +34,22 @@ describe('IdeSettingsService', () => {
req.flush(mockIdes);
});

it('should load IDE preferences', () => {
it('should load IDE preferences', fakeAsync(() => {
const mockIdeMappingDTO: IdeMappingDTO[] = [
{ programmingLanguage: ProgrammingLanguage.JAVA, ide: { name: 'VS Code', deepLink: 'vscode://vscode.git/clone?url={cloneUrl}' } },
];
const expectedMap = new Map<ProgrammingLanguage, Ide>([[ProgrammingLanguage.JAVA, { name: 'VS Code', deepLink: 'vscode://vscode.git/clone?url={cloneUrl}' }]]);

service.loadIdePreferences().subscribe((ideMap) => {
service.loadIdePreferences().then((ideMap) => {
expect(ideMap).toEqual(expectedMap);
});

tick();

const req = httpMock.expectOne(service.ideSettingsUrl);
expect(req.request.method).toBe('GET');
req.flush(mockIdeMappingDTO);
});
}));

it('should save IDE preference', () => {
const programmingLanguage = ProgrammingLanguage.JAVA;
Expand Down

0 comments on commit d3ddb92

Please sign in to comment.